import collections
import ROOT
from .commonHelpers.logger import logger
logger = logger.getChild(__name__)
from . import pyrootHelpers as PH
from .histograms import HistogramStack, getHM
from .line import Line, CutLine
from . import globalStyle as gst
from .labels import scaleYPosTopMargin
[docs]class Pad(ROOT.TPad):
def __init__(self, name, size=(0,0,1,1), logy=False, xTitle=None,
yTitle= None, xTitleOffset=1, yTitleOffset=gst.yTitleOffset,
yMinimum=None, yMaximum=None, yAxisNDivisions=None,
gridy=None, removeXLabels=False, setNoExponent=False,
xTitleScale=1, yTitleScale=1, xAxisLabelsOption=None,
insideTopMargin=None,
debugText=[],
customTexts=None):
self.xlow, self.ylow, self.xhigh, self.yhigh = size
super(Pad, self).__init__(name, name, self.xlow, self.ylow, self.xhigh, self.yhigh)
self.Draw() # do we need that here?
self.drawables = [] # containes all objects on which .draw() will be called
self.logy = logy
self.xTitle = xTitle
self.xTitleOffset = xTitleOffset
self.yTitle = yTitle
self.yTitleOffset = yTitleOffset
self.debugText = debugText
if customTexts is not None:
self.customTexts = customTexts
else:
self.customTexts = []
self.yMinimum = yMinimum
self.yMaximum = yMaximum
self.yAxisNDivisions = yAxisNDivisions
self.gridy = gridy
self.removeXLabels = removeXLabels
self.setNoExponent = setNoExponent
self.xTitleScale=xTitleScale
self.yTitleScale=yTitleScale
self.insideTopMargin = insideTopMargin
self.xAxisLabelsOption = xAxisLabelsOption
self.histogramsMinimum = None
self.histogramsMaximum = None
self.legend = None
self.referenceHist = None # In case there is no histogram assigned to the plot the axis are determined from this histogram
self.projection = False
self.relSize = 1
[docs] def getHistograms(self):
for db in self.drawables:
if issubclass(type(db), ROOT.TH1):
yield db
if issubclass(type(db), ROOT.THStack):
yield db
[docs] def draw(self, rangeErrors=False, printOverflowBins=False):
logger.debug('drawing {}'.format(self))
logger.debug('drawables: {}'.format(self.drawables))
self.cd()
if self.gridy is not None:
logger.debug("Setting Gridy!")
self.SetGridy(self.gridy)
# Force recalculation of min and max
self.setMinMax()
if not self.logy:
self.SetLogy(0)
else:
self.SetLogy(1)
if self.referenceHist is not None:
logger.debug("Adding reference hist {}".format(self.referenceHist))
self.drawables.append(self.referenceHist)
self.drawables.sort(key=lambda db: db.zIndex)
# decorate histograms and set min/max
for hist in self.getHistograms():
logger.debug("Call SetMinimum for {} (setting to {})".format(hist, self.getMinimum()))
hist.SetMinimum(self.getMinimum())
hist.minimum = self.getMinimum()
logger.debug("Call SetMaximum for {} (setting to {})".format(hist, self.getMaximum()))
hist.SetMaximum(self.getMaximum())
hist.maximum = self.getMaximum()
self.decorateDrawable(hist)
# draw the first histogram
firstDrawn = None
for hist in self.getHistograms():
hist.firstDraw()
firstDrawn = hist
break
# draw the rest
for drawable in self.drawables:
drawable.minimum = self.getMinimum()
drawable.maximum = self.getMaximum()
drawable.logy = self.logy
if not drawable is firstDrawn:
logger.debug("Drawing {} to pad {}".format(drawable, self.GetName()))
drawable.draw(drawString='same')
self.printOverflow(printOverflowBins)
self.drawText()
if self.legend is not None:
self.legend.draw()
self.reDrawAxes() # Puts axis ticks above backgrounds
self.Draw()
self.Modified()
self.Update()
[docs] def setMinMax(self):
"""Set Minimum and maximum calculated from all histogram-like objects"""
self.histogramsMinimum, self.histogramsMaximum = PH.getMinMaxWithErrors(*self.getHistograms(),
maximumWithErrors=gst.maximumWithErrors,
minimumWithErrors=gst.minimumWithErrors)
logger.debug("Determined Minimum: {}".format(self.histogramsMinimum))
logger.debug("Determined Maximum: {}".format(self.histogramsMaximum))
[docs] def getHistogramsMaximum(self, errors=False):
"""Get the maximum of all histogram-like objects"""
if self.histogramsMaximum is None:
self.setMinMax()
return self.histogramsMaximum
[docs] def getHistogramsMinimum(self):
"""Get the minimum of all histogram-like objects"""
if self.histogramsMinimum is None:
self.setMinMax()
return self.histogramsMinimum
[docs] def getMinimum(self, errors=False):
"""Get the minimum which is going to be used for the actual plot"""
if self.yMinimum is not None:
logger.debug("Using custom minimum {}".format(self.yMinimum))
return self.yMinimum
minimum = self.getHistogramsMinimum()
preliminary_maximum = self.getHistogramsMaximum()
if minimum is None:
minimum = 0.
if minimum > 0:
if self.logy:
minimum = 0.1*minimum
else:
minimum = 0
if minimum <= 0 and self.logy:
logger.debug("Log scale - using {} instead of {} as Minimum".format(gst.defaultLogyYmin, minimum))
minimum = gst.defaultLogyYmin
if minimum >= preliminary_maximum:
logger.warning("Minimum might become larger as maximum (globalStyle.defaultLogyYmin = {})"
"- setting to 10% of (unscaled) maximum: {}"
.format(gst.defaultLogyYmin, 0.1*preliminary_maximum))
minimum = 0.1*preliminary_maximum
logger.debug("Returning minimum: {}".format(minimum))
return minimum
[docs] def getMaximum(self, errors=False):
"""Get the maximum which is going to be used for the actual plot"""
if self.yMaximum is not None:
logger.debug("Using custom maximum {}".format(self.yMaximum))
return self.yMaximum
minimum = self.getMinimum()
maximum = self.getHistogramsMaximum()
if self.insideTopMargin is not None:
returnmax = PH.calculateMarginMaximum(self.insideTopMargin/self.GetAbsHNDC(), minimum, maximum, logy=self.logy)
else:
if not self.logy:
returnmax = 1.5*maximum
else:
returnmax = 5*maximum
if maximum < minimum:
raise ValueError("Maximum got smaller than minimum! - Something is wrong")
if self.logy and returnmax <= 0:
logger.warning("Maximum <= 0! Returning : {}".format(2*minimum))
return 2*minimum
else:
logger.debug("determined Maximum: {}".format(returnmax))
return returnmax
[docs] def printOverflow(self, printOverflowBins):
threshold = 5
if printOverflowBins:
for histogram in self.getHistograms():
try:
if histogram.underflow()/histogram.Integral() > threshold/100.:
self.debugText.append('uflow {}, {:.1e}'.format(histogram.legend, histogram.underflow()))
except ZeroDivisionError:
if histogram.underflow() > 0.0:
self.debugText.append('uflow {}, {:.1e}'.format(histogram.legend, histogram.underflow()))
try:
if histogram.overflow()/histogram.Integral() > threshold/100.:
self.debugText.append('oflow {}, {:.1e}'.format(histogram.legend, histogram.overflow()))
except ZeroDivisionError:
if histogram.overflow() > 0.0:
self.debugText.append('oflow {}, {:.1e}'.format(histogram.legend, histogram.overflow()))
[docs] def drawText(self):
yspacing = 0.06
yup = 0.95
for i, line in enumerate(self.debugText):
l = ROOT.TLatex()
l.SetNDC()
l.SetTextColor(ROOT.kBlack)
l.DrawLatex(0.2,yup-i*yspacing, "{}".format(line))
for xpos, ypos, text in self.customTexts:
l = ROOT.TLatex()
l.SetNDC()
l.SetTextColor(ROOT.kBlack)
l.SetTextFont(gst.customTextFont)
l.SetTextSize(gst.customTextSize)
l.DrawLatex(xpos, scaleYPosTopMargin(ypos), "{}".format(text))
[docs] def reDrawAxes(self):
self.RedrawAxis()
self.RedrawAxis("g")
[docs] def getXTitle(self):
""" algorithm to get our xTitle for the pad """
# if explicitly set use this one
if self.xTitle is not None:
return self.xTitle
candidates = collections.OrderedDict()
for d in self.drawables:
try:
title = d.getXTitle()
if title is not None:
candidates[title] = 1
except AttributeError as e1:
logger.debug(e1)
candidates = list(candidates.keys())
logger.debug("X-Title candidates: {}".format(candidates))
if len(candidates) == 1:
return candidates[0]
elif len(candidates) == 0:
logger.debug("no xTitle found for {}".format(self))
return ''
else:
logger.warn("multiple xTitles found: {} - taking first one".format(candidates))
return candidates[0]
[docs] def getYTitle(self):
""" algorithm to get our yTitle for the pad
Currently - this is exactly the same function as for the xTitle - maybe generalise this
"""
# if explicitly set use this one
if self.yTitle is not None:
return self.yTitle
candidates = collections.OrderedDict()
for d in self.drawables:
try:
title = d.getYTitle()
if title is not None:
candidates[title] = 1
except AttributeError as e1:
logger.debug(e1)
candidates = list(candidates.keys())
logger.debug("Y-Title candidates: {}".format(candidates))
if len(candidates) == 1:
return candidates[0]
elif len(candidates) == 0:
logger.debug("no yTitle found for {}".format(self))
return ''
else:
logger.warn("multiple yTitles found: {} - taking first one".format(candidates))
return candidates[0]
# def getRange(self):
# try:
# return self.rangeDown, self.rangeUp
# except AttributeError:
# for drawable in self.drawables:
# try:
# self.rangeDown = drawable.GetXaxis().GetBinLowEdge(1)
# self.rangeUp = drawable.GetXaxis().GetBinUpEdge(drawable.GetNbinsX())
# return self.rangeDown, self.rangeUp
# except (AttributeError, ReferenceError):
# pass
[docs] def addReferenceHist(self, hist):
refHist = hist.Clone(next(PH.tempNames))
refHist.Reset()
refHist.SetDirectory(0)
# refHist.Fill(1,1)
self.referenceHist = getHM(refHist)
self.referenceHist.cloneAttributes(self.referenceHist, hist)
self.referenceHist.zIndex = -100
self.referenceHist.style = "axis"
[docs] def addVertLine(self, cutValue, **kwargs):
line = CutLine(cutValue, self.getMinimum(), cutValue, 0.8*self.getMaximum(), **kwargs)
self.drawables.append(line)
[docs] def decorateDrawable(self, drawable):
if issubclass(type(drawable), ROOT.THStack):
# Take axis hist of HistogramStack
xaxis = drawable.axisHist.GetXaxis()
yaxis = drawable.axisHist.GetYaxis()
else:
xaxis = drawable.GetXaxis()
yaxis = drawable.GetYaxis()
if xaxis == None:
raise AttributeError('no axis found for {}',format(drawable))
xaxis.SetTitle(self.getXTitle())
yaxis.SetTitle(self.getYTitle())
if not self.yAxisNDivisions is None:
# ToDo: Check how this interferes with SetRangeUser
yaxis.SetNdivisions(self.yAxisNDivisions, True)
if self.setNoExponent:
yaxis.SetNoExponent(True)
for axis in [xaxis, yaxis]:
if gst.labelFont is not None:
axis.SetLabelFont(gst.labelFont)
axis.SetTitleFont(gst.labelFont)
if gst.labelFontSize is not None:
if axis == xaxis:
titleScale = self.xTitleScale
elif axis == yaxis:
titleScale = self.yTitleScale
else:
titleScale = 1.
axis.SetLabelSize(gst.labelFontSize)
axis.SetTitleSize(gst.labelFontSize*titleScale)
if self.xAxisLabelsOption is not None:
xaxis.LabelsOption(self.xAxisLabelsOption)
# fucking order matters!
xaxis.SetTitleOffset(self.xTitleOffset/self.yTitleScale)
yaxis.SetTitleOffset(self.yTitleOffset/self.yTitleScale)
if self.removeXLabels:
xaxis.SetLabelSize(0)
xaxis.SetTitleSize(0)
# if issubclass(type(drawable), ROOT.TGraph):
# # histo = drawable.GetHistogram()
# # histo.GetXaxis().SetRangeUser(*self.getRange())
# # zaxis = histo.GetZaxis()
# pass
# else:
# zaxis = drawable.GetZaxis()
# try:
# drawable.GetXaxis().SetRangeUser(*self.getRange())
# except ReferenceError:
# pass