Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ samples/histograms/*

bin/*
build/*

*.pyc
13 changes: 11 additions & 2 deletions configs/examples/minimal_plotter_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
from Legend import Legend
from Histogram import Histogram
from HistogramNormalizer import NormalizationType
from CmsLabelsManager import CmsLabel

year = "2018"
show_cms_labels = True
label_outside_axes = True

# Allowed labels: paper, paper_sim, paper_supplementary, paper_sim_supplementary, pas, pas_sim, pas_supplementary,
# pas_sim_supplementary, thesis, thesis_sim, private_data_sim, private_data, private_sim.
cms_label = CmsLabel.paper_sim_supplementary

samples = (
Sample(
Expand All @@ -21,8 +30,8 @@

histograms = (
# name title logx logy norm_type rebin xmin xmax ymin ymax xlabel ylabel
Histogram("Muon_pt" , "Muon p_{T}", False, True, NormalizationType.to_lumi, 5, 0 , 150, 1, 1e3 , "p_{T} [GeV]", "# events (2018)"),
Histogram("Muon_eta", "Muon #eta", False, False, NormalizationType.to_lumi, 5, -2.4, 2.4, 0, 70 , "#eta" , "# events (2018)"),
Histogram("Muon_pt" , "Muon p_{T}", False, True, NormalizationType.to_lumi, 5, None , None, None, None , "p_{T} [GeV]", "# events (2018)"),
Histogram("Muon_eta", "Muon #eta", False, False, NormalizationType.to_lumi, 5, None, None, None, None , "#eta" , "# events (2018)"),
)
luminosity = 63670. # pb^-1 (2018)

Expand Down
119 changes: 75 additions & 44 deletions pylibs/plotting/CmsLabelsManager.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,23 @@
import ROOT
from enum import Enum


class CmsLabel(Enum):
"""Allowed CMS plot labels from the CMS plotting guidelines."""

paper = ("CMS", None)
paper_sim = ("CMS", "Simulation")
paper_supplementary = ("CMS", "Supplementary")
paper_sim_supplementary = ("CMS", "Simulation Supplementary")
pas = ("CMS", "Preliminary")
pas_sim = ("CMS", "Simulation Preliminary")
pas_supplementary = ("CMS", "Preliminary")
pas_sim_supplementary = ("CMS", "Simulation Preliminary")
thesis = ("CMS", "Work in progress")
thesis_sim = ("CMS", "Simulation Work in progress")
private_data_sim = (None, "Private work (CMS data/simulation)")
private_data = (None, "Private work (CMS data)")
private_sim = (None, "Private work (CMS simulation)")


class CmsLabelsManager:
Expand All @@ -9,16 +28,16 @@ def __init__(self, config):
if hasattr(self.config, "show_cms_labels"):
self.show_labels = self.config.show_cms_labels

self.cmsText = "CMS"
if hasattr(self.config, "label_text"):
self.cmsText = self.config.label_text
cms_label = getattr(self.config, "cms_label", CmsLabel.paper)
if not isinstance(cms_label, CmsLabel):
raise TypeError("cms_label must be a CmsLabel enum value")
if hasattr(self.config, "label_text") or hasattr(self.config, "extraText"):
raise ValueError("Use cms_label with a CmsLabel enum value instead of free text")
self.cmsText, self.extraText = cms_label.value

self.cmsTextFont = 61
self.cmsTextFont = 63

self.extraText = None
if hasattr(self.config, "extraText"):
self.extraText = self.config.extraText
self.extraTextFont = 52
self.extraTextFont = 53

self.lumiTextSize = 0.6

Expand All @@ -27,25 +46,23 @@ def __init__(self, config):
else:
self.lumiTextOffset = -0.

self.cmsTextSize = 0.75
self.labelTextSize = 20
self.cmsTextOffset = 0.1

if config.show_ratio_plots:
self.relPosX = 0.045
self.relPosY = 0.035
self.relExtraDY = 1.2
self.relPosX = 0.055
self.relPosY = 0.04
else:
self.relPosX = 0.045
self.relPosY = 0.070
self.relExtraDY = 1.2
self.relPosX = 0.055
self.relPosY = 0.04

if hasattr(self.config, "label_x"):
self.relPosX = self.config.label_x

if hasattr(self.config, "label_y"):
self.relPosY = self.config.label_y

self.extraOverCmsTextSize = 0.76
self.label_outside_axes = getattr(self.config, "label_outside_axes", False)

if hasattr(self.config, "lumi_unit"):
lumi_unit = self.config.lumi_unit
Expand Down Expand Up @@ -95,6 +112,10 @@ def __setVariables(self, pad):

def __drawLumiText(self):
lumiText = self.lumi + self.collision_energy
if self.label_outside_axes:
_, label_pos_y = self.__label_position()
else:
label_pos_y = 1-self.top + 0.015

latex = ROOT.TLatex()
latex.SetNDC()
Expand All @@ -103,8 +124,9 @@ def __drawLumiText(self):
latex.SetTextFont(43)
latex.SetTextAlign(31)

latex.SetTextSize(20)
latex.DrawLatex(1-self.right, 1-self.top + self.lumiTextOffset * self.top, lumiText)
latex.SetTextSize(self.labelTextSize)
right_boundary = 1-self.right
latex.DrawLatex(right_boundary, label_pos_y + self.lumiTextOffset * self.top, lumiText)

def __drawLogo(self):
posX_ = self.left + 0.045 * \
Expand All @@ -122,54 +144,63 @@ def __drawLogo(self):
pad_logo.Modified()

def __drawCmsText(self):

posX_ = self.left + self.relPosX*(1-self.left-self.right)
posY_ = 1-self.top - self.relPosY*(1-self.top-self.bottom)
posX_, posY_ = self.__label_position()

latex = ROOT.TLatex()
latex.SetNDC()
if self.cmsText is None:
return

latex.SetTextFont(self.cmsTextFont)
latex.SetTextSize(self.cmsTextSize*self.top)
latex.SetTextAlign(13)
latex.SetTextSize(self.labelTextSize)
latex.SetTextAlign(11 if self.label_outside_axes else 13)
latex.DrawLatex(posX_, posY_, self.cmsText)

def __drawExtraCmsText(self):
if self.extraText is None:
return

posX_ = self.left + self.relPosX*(1-self.left-self.right)
posY_ = 1-self.top - self.relPosY*(1-self.top-self.bottom)
posX_, posY_ = self.__label_position()
if self.label_outside_axes and self.cmsText is not None:
posX_ += 55.0/self.width

latex = ROOT.TLatex()
latex.SetNDC()
latex.SetTextFont(self.extraTextFont)
latex.SetTextAlign(13)
extraTextSize = self.extraOverCmsTextSize * self.cmsTextSize
latex.SetTextSize(extraTextSize*self.top)
latex.DrawLatex(posX_, posY_ - self.relExtraDY *
self.cmsTextSize*self.top, self.extraText)
latex.SetTextAlign(11 if self.label_outside_axes else 13)
latex.SetTextSize(self.labelTextSize)
extra_pos_y = posY_
if not self.label_outside_axes:
extra_pos_y -= (self.labelTextSize + 6.0)/self.height
latex.DrawLatex(posX_, extra_pos_y, self.extraText)

def __label_position(self):
if self.label_outside_axes:
return self.left, 1 - self.top + 0.015
return (self.left + self.relPosX*(1-self.left-self.right),
1-self.top - self.relPosY*(1-self.top-self.bottom))

def drawLabels2D(self, canvas):
latex = ROOT.TLatex()
latex.SetTextFont(self.cmsTextFont)
latex.SetTextSize(self.cmsTextSize*self.top)

latex.SetTextAlign(13)
latex.SetNDC()
latex.DrawLatex(0.1, 0.99, self.cmsText)
if self.cmsText is not None:
latex = ROOT.TLatex()
latex.SetTextFont(self.cmsTextFont)
latex.SetTextSize(self.labelTextSize)
latex.SetTextAlign(13)
latex.SetNDC()
latex.DrawLatex(0.1, 0.99, self.cmsText)

canvas.Update()
cms_width = latex.GetTextSize()
print(f"cms width: {cms_width}")
cms_width = 55.0/canvas.GetWw()

if self.extraText is not None:

latex = ROOT.TLatex()
latex.SetTextFont(self.extraTextFont)
latex.SetTextAlign(13)
extraTextSize = self.extraOverCmsTextSize * self.cmsTextSize
latex.SetTextSize(extraTextSize*self.top)
latex.SetTextSize(self.labelTextSize)
latex.SetNDC()
posX_ = 0.1 + 2*cms_width + 0.01
posY_ = 0.99 - 0.01
posX_ = 0.1 if self.cmsText is None else 0.1 + cms_width
posY_ = 0.99 if self.cmsText is None else 0.99 - 0.01
latex.DrawLatex(posX_, posY_, self.extraText)


Expand All @@ -180,5 +211,5 @@ def drawLabels2D(self, canvas):
latex.SetTextColor(ROOT.kBlack)
latex.SetTextFont(43)
latex.SetTextAlign(31)
latex.SetTextSize(20)
latex.SetTextSize(self.labelTextSize)
latex.DrawLatex(1-self.right, 1-self.top + 2*self.lumiTextOffset * self.top + 0.01, lumiText)
31 changes: 17 additions & 14 deletions pylibs/plotting/Histogram.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from dataclasses import dataclass
from copy import deepcopy
from array import array
from typing import Optional
import ROOT

from Sample import SampleType
Expand All @@ -16,10 +17,10 @@ class Histogram:
log_y: bool = False
norm_type: int = NormalizationType.to_lumi
rebin: int = 1
x_min: float = 0.0
x_max: float = 0.0
y_min: float = 0.0
y_max: float = 0.0
x_min: Optional[float] = None
x_max: Optional[float] = None
y_min: Optional[float] = None
y_max: Optional[float] = None
x_label: str = ""
y_label: str = ""
suffix: str = ""
Expand Down Expand Up @@ -63,11 +64,13 @@ def load(self, input_file):
if not self.isGood():
return

if self.x_max > 0 or self.x_min > 0:
if self.x_min is not None or self.x_max is not None:
original_bins = [self.hist.GetBinLowEdge(i) for i in range(1, self.hist.GetNbinsX() + 2)]
new_bin_edges = [x for x in original_bins if self.x_min <= x <= self.x_max]
if self.x_max not in new_bin_edges:
new_bin_edges.append(self.x_max)
x_min = self.x_min if self.x_min is not None else original_bins[0]
x_max = self.x_max if self.x_max is not None else original_bins[-1]
new_bin_edges = [x for x in original_bins if x_min <= x <= x_max]
if x_max not in new_bin_edges:
new_bin_edges.append(x_max)

new_n_bins = len(new_bin_edges) - 1
new_histogram = ROOT.TH1F(f"{self.hist.GetName()}_{self.rand.Integer(1000000)}",
Expand Down Expand Up @@ -128,12 +131,12 @@ class Histogram2D:
norm_type: int = NormalizationType.to_lumi
x_rebin: int = 1
y_rebin: int = 1
x_min: float = 0.0
x_max: float = 0.0
y_min: float = 0.0
y_max: float = 0.0
z_min: float = 0.0
z_max: float = 0.0
x_min: Optional[float] = None
x_max: Optional[float] = None
y_min: Optional[float] = None
y_max: Optional[float] = None
z_min: Optional[float] = None
z_max: Optional[float] = None
x_label: str = ""
y_label: str = ""
z_label: str = ""
Expand Down
34 changes: 30 additions & 4 deletions pylibs/plotting/HistogramPlotter.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,8 +321,17 @@ def __drawLineAtOne(self, canvas, hist):
if not self.show_ratios:
return

ratio_stack = self.__getRatioStack(hist)
if ratio_stack is None or not hasattr(ratio_stack, "GetHistogram"):
return
ratio_histogram = ratio_stack.GetHistogram()
if ratio_histogram is None:
return
x_min = hist.x_min if hist.x_min is not None else ratio_histogram.GetXaxis().GetXmin()
x_max = hist.x_max if hist.x_max is not None else ratio_histogram.GetXaxis().GetXmax()

global line
line = ROOT.TLine(hist.x_min, 1, hist.x_max, 1)
line = ROOT.TLine(x_min, 1, x_max, 1)
line.SetLineColor(ROOT.kBlack)
line.SetLineStyle(ROOT.kDashed)

Expand Down Expand Up @@ -402,9 +411,26 @@ def __drawHists(self, canvas, hist):
stack = self.stacks[sample_type][hist.getName()]
if stack.GetNhists() > 0:
stack.Draw(options)
self.styler.setupFigure(stack, hist)
firstPlotted = True

# Apply automatic limits only after every sample has been drawn. A
# THStack's axis is established by its first draw, while subsequent
# stacks (signals/data) can contain larger contributions.
plotted_histograms = []
for sample_type in SampleType:
stack = self.stacks[sample_type][hist.getName()]
if stack.GetNhists() == 0:
continue
stack_histograms = stack.GetStack()
if stack_histograms and stack_histograms.GetSize() > 0:
plotted_histograms.append(stack_histograms.Last())

first_stack = next((self.stacks[sample_type][hist.getName()]
for sample_type in SampleType
if self.stacks[sample_type][hist.getName()].GetNhists() > 0), None)
if first_stack is not None:
self.styler.setupFigure(first_stack, hist, source_histograms=plotted_histograms)

def __drawRatioHists(self, canvas, hist):
canvas.cd(1)

Expand Down Expand Up @@ -442,7 +468,7 @@ def drawStacks(self):
self.__drawHists(canvas, hist)
self.__drawUncertainties(canvas, hist)
self.__drawLegends(canvas, hist)
self.cmsLabelsManager.drawLabels(canvas)
self.cmsLabelsManager.drawLabels(canvas.GetPad(1))

# make sure plot border is on top of everything else
canvas.GetPad(1).GetFrame().SetLineWidth(2)
Expand Down Expand Up @@ -500,7 +526,7 @@ def drawRatioStacks(self):

self.__drawRatioHists(canvas, hist_nom)
self.__drawLegends(canvas, hist_nom)
self.cmsLabelsManager.drawLabels(canvas)
self.cmsLabelsManager.drawLabels(canvas.GetPad(1))

canvas.Update()

Expand Down
9 changes: 7 additions & 2 deletions pylibs/plotting/Legend.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class Legend:
options: str = ""
title: str = ""
text_size: float = 20
fill_color: int = None

def getRootLegend(self):
legend = TLegend(self.x1, self.y1, self.x2, self.y2)
Expand All @@ -19,8 +20,12 @@ def getRootLegend(self):

def __setupLegend(self, legend):
legend.SetBorderSize(0)
# legend.SetFillColor(0)
# legend.SetFillStyle(0)
# Keep the plot visible behind the legend by default. A fill color
# opts back into a solid legend background for users who need one.
legend.SetFillStyle(0)
if self.fill_color is not None:
legend.SetFillColor(self.fill_color)
legend.SetFillStyle(1001)
legend.SetTextFont(43)
legend.SetTextSize(self.text_size)

Expand Down
Loading
Loading