Back to home page

Project CMSSW displayed by LXR

 
 

    


File indexing completed on 2024-11-26 02:34:39

0001 from builtins import range
0002 import os
0003 import sys
0004 import math
0005 import copy
0006 import array
0007 import difflib
0008 import collections
0009 
0010 import ROOT
0011 ROOT.gROOT.SetBatch(True)
0012 ROOT.PyConfig.IgnoreCommandLineOptions = True
0013 
0014 from . import html
0015 
0016 verbose=False
0017 _ratioYTitle = "Ratio"
0018 
0019 def _setStyle():
0020     _absoluteSize = True
0021     if _absoluteSize:
0022         font = 43
0023         titleSize = 22
0024         labelSize = 22
0025         statSize = 14
0026     else:
0027         font = 42
0028         titleSize = 0.05
0029         labelSize = 0.05
0030         statSize = 0.025
0031 
0032     ROOT.gROOT.SetStyle("Plain")
0033     ROOT.gStyle.SetPadRightMargin(0.07)
0034     ROOT.gStyle.SetPadLeftMargin(0.13)
0035     ROOT.gStyle.SetTitleFont(font, "XYZ")
0036     ROOT.gStyle.SetTitleSize(titleSize, "XYZ")
0037     ROOT.gStyle.SetTitleOffset(1.2, "Y")
0038     #ROOT.gStyle.SetTitleFontSize(0.05)
0039     ROOT.gStyle.SetLabelFont(font, "XYZ")
0040     ROOT.gStyle.SetLabelSize(labelSize, "XYZ")
0041     ROOT.gStyle.SetTextSize(labelSize)
0042     ROOT.gStyle.SetStatFont(font)
0043     ROOT.gStyle.SetStatFontSize(statSize)
0044 
0045     ROOT.TGaxis.SetMaxDigits(4)
0046 
0047 def _getObject(tdirectory, name):
0048     obj = tdirectory.Get(name)
0049     if not obj:
0050         if verbose:
0051             print("Did not find {obj} from {dir}".format(obj=name, dir=tdirectory.GetPath()))
0052         return None
0053     return obj
0054 
0055 def _getOrCreateObject(tdirectory, nameOrCreator):
0056     if hasattr(nameOrCreator, "create"):
0057         return nameOrCreator.create(tdirectory)
0058     return _getObject(tdirectory, nameOrCreator)
0059 
0060 class GetDirectoryCode:
0061     class FileNotExist: pass
0062     class PossibleDirsNotExist: pass
0063     class SubDirNotExist: pass
0064 
0065     @staticmethod
0066     def codesToNone(code):
0067         if code in [GetDirectoryCode.FileNotExist, GetDirectoryCode.PossibleDirsNotExist, GetDirectoryCode.SubDirNotExist]:
0068             return None
0069         return code
0070 
0071 def _getDirectoryDetailed(tfile, possibleDirs, subDir=None):
0072     """Get TDirectory from TFile."""
0073     if tfile is None:
0074         return GetDirectoryCode.FileNotExist
0075     for pdf in possibleDirs:
0076         d = tfile.Get(pdf)
0077         if d:
0078             if subDir is not None:
0079                 # Pick associator if given
0080                 d = d.Get(subDir)
0081                 if d:
0082                     return d
0083                 else:
0084                     if verbose:
0085                         print("Did not find subdirectory '%s' from directory '%s' in file %s" % (subDir, pdf, tfile.GetName()))
0086 #                        if "Step" in subDir:
0087 #                            raise Exception("Foo")
0088                     return GetDirectoryCode.SubDirNotExist
0089             else:
0090                 return d
0091     if verbose:
0092         print("Did not find any of directories '%s' from file %s" % (",".join(possibleDirs), tfile.GetName()))
0093     return GetDirectoryCode.PossibleDirsNotExist
0094 
0095 def _getDirectory(*args, **kwargs):
0096     return GetDirectoryCode.codesToNone(_getDirectoryDetailed(*args, **kwargs))
0097 
0098 def _th1ToOrderedDict(th1, renameBin=None):
0099     values = collections.OrderedDict()
0100     for i in range(1, th1.GetNbinsX()+1):
0101         binLabel = th1.GetXaxis().GetBinLabel(i)
0102         if renameBin is not None:
0103             binLabel = renameBin(binLabel)
0104         values[binLabel] = (th1.GetBinContent(i), th1.GetBinError(i))
0105     return values
0106 
0107 def _createCanvas(name, width, height):
0108     # silence warning of deleting canvas with the same name
0109     if not verbose:
0110         backup = ROOT.gErrorIgnoreLevel
0111         ROOT.gErrorIgnoreLevel = ROOT.kError
0112     canvas = ROOT.TCanvas(name, name, width, height)
0113     if not verbose:
0114         ROOT.gErrorIgnoreLevel = backup
0115     return canvas
0116 
0117 def _modifyPadForRatio(pad, ratioFactor):
0118     pad.Divide(1, 2)
0119 
0120     divisionPoint = 1-1/ratioFactor
0121 
0122     topMargin = pad.GetTopMargin()
0123     bottomMargin = pad.GetBottomMargin()
0124     divisionPoint += (1-divisionPoint)*bottomMargin # correct for (almost-)zeroing bottom margin of pad1
0125     divisionPointForPad1 = 1-( (1-divisionPoint) / (1-0.02) ) # then correct for the non-zero bottom margin, but for pad1 only
0126 
0127     # Set the lower point of the upper pad to divisionPoint
0128     pad1 = pad.cd(1)
0129     yup = 1.0
0130     ylow = divisionPointForPad1
0131     xup = 1.0
0132     xlow = 0.0
0133     pad1.SetPad(xlow, ylow, xup, yup)
0134     pad1.SetFillStyle(4000) # transparent
0135     pad1.SetBottomMargin(0.02) # need some bottom margin here for eps/pdf output (at least in ROOT 5.34)
0136 
0137     # Set the upper point of the lower pad to divisionPoint
0138     pad2 = pad.cd(2)
0139     yup = divisionPoint
0140     ylow = 0.0
0141     pad2.SetPad(xlow, ylow, xup, yup)
0142     pad2.SetFillStyle(4000) # transparent
0143     pad2.SetTopMargin(0.0)
0144     pad2.SetBottomMargin(bottomMargin/(ratioFactor*divisionPoint))
0145 
0146 def _calculateRatios(histos, ratioUncertainty=False):
0147     """Calculate the ratios for a list of histograms"""
0148 
0149     def _divideOrZero(numerator, denominator):
0150         if denominator == 0:
0151             return 0
0152         return numerator/denominator
0153 
0154     def equal(a, b):
0155         if a == 0. and b == 0.:
0156             return True
0157         return abs(a-b)/max(abs(a),abs(b)) < 1e-3
0158 
0159     def findBins(wrap, bins_xvalues):
0160         ret = []
0161         currBin = wrap.begin()
0162         i = 0
0163         while i < len(bins_xvalues) and currBin < wrap.end():
0164             (xcenter, xlow, xhigh) = bins_xvalues[i]
0165             xlowEdge = xcenter-xlow
0166             xupEdge = xcenter+xhigh
0167 
0168             (curr_center, curr_low, curr_high) = wrap.xvalues(currBin)
0169             curr_lowEdge = curr_center-curr_low
0170             curr_upEdge = curr_center+curr_high
0171 
0172             if equal(xlowEdge, curr_lowEdge) and equal(xupEdge,  curr_upEdge):
0173                 ret.append(currBin)
0174                 currBin += 1
0175                 i += 1
0176             elif curr_upEdge <= xlowEdge:
0177                 currBin += 1
0178             elif curr_lowEdge >= xupEdge:
0179                 ret.append(None)
0180                 i += 1
0181             else:
0182                 ret.append(None)
0183                 currBin += 1
0184                 i += 1
0185         if len(ret) != len(bins_xvalues):
0186             ret.extend([None]*( len(bins_xvalues) - len(ret) ))
0187         return ret
0188 
0189     # Define wrappers for TH1/TGraph/TGraph2D to have uniform interface
0190     # TODO: having more global wrappers would make some things simpler also elsewhere in the code
0191     class WrapTH1:
0192         def __init__(self, th1, uncertainty):
0193             self._th1 = th1
0194             self._uncertainty = uncertainty
0195 
0196             xaxis = th1.GetXaxis()
0197             xaxis_arr = xaxis.GetXbins()
0198             if xaxis_arr.GetSize() > 0: # unequal binning
0199                 lst = [xaxis_arr[i] for i in range(0, xaxis_arr.GetSize())]
0200                 arr = array.array("d", lst)
0201                 self._ratio = ROOT.TH1F("foo", "foo", xaxis.GetNbins(), arr)
0202             else:
0203                 self._ratio = ROOT.TH1F("foo", "foo", xaxis.GetNbins(), xaxis.GetXmin(), xaxis.GetXmax())
0204             _copyStyle(th1, self._ratio)
0205             self._ratio.SetStats(0)
0206             self._ratio.SetLineColor(ROOT.kBlack)
0207             self._ratio.SetLineWidth(1)
0208         def draw(self, style=None):
0209             st = style
0210             if st is None:
0211                 if self._uncertainty:
0212                     st = "EP"
0213                 else:
0214                     st = "HIST P"
0215             self._ratio.Draw("same "+st)
0216         def begin(self):
0217             return 1
0218         def end(self):
0219             return self._th1.GetNbinsX()+1
0220         def xvalues(self, bin):
0221             xval = self._th1.GetBinCenter(bin)
0222             xlow = xval-self._th1.GetXaxis().GetBinLowEdge(bin)
0223             xhigh = self._th1.GetXaxis().GetBinUpEdge(bin)-xval
0224             return (xval, xlow, xhigh)
0225         def yvalues(self, bin):
0226             yval = self._th1.GetBinContent(bin)
0227             yerr = self._th1.GetBinError(bin)
0228             return (yval, yerr, yerr)
0229         def y(self, bin):
0230             return self._th1.GetBinContent(bin)
0231         def divide(self, bin, scale):
0232             self._ratio.SetBinContent(bin, _divideOrZero(self._th1.GetBinContent(bin), scale))
0233             self._ratio.SetBinError(bin, _divideOrZero(self._th1.GetBinError(bin), scale))
0234         def makeRatio(self):
0235             pass
0236         def getRatio(self):
0237             return self._ratio
0238 
0239     class WrapTGraph:
0240         def __init__(self, gr, uncertainty):
0241             self._gr = gr
0242             self._uncertainty = uncertainty
0243             self._xvalues = []
0244             self._xerrslow = []
0245             self._xerrshigh = []
0246             self._yvalues = []
0247             self._yerrshigh = []
0248             self._yerrslow = []
0249         def draw(self, style=None):
0250             if self._ratio is None:
0251                 return
0252             st = style
0253             if st is None:
0254                 if self._uncertainty:
0255                     st = "PZ"
0256                 else:
0257                     st = "PX"
0258             self._ratio.Draw("same "+st)
0259         def begin(self):
0260             return 0
0261         def end(self):
0262             return self._gr.GetN()
0263         def xvalues(self, bin):
0264             return (self._gr.GetX()[bin], self._gr.GetErrorXlow(bin), self._gr.GetErrorXhigh(bin))
0265         def yvalues(self, bin):
0266             return (self._gr.GetY()[bin], self._gr.GetErrorYlow(bin), self._gr.GetErrorYhigh(bin))
0267         def y(self, bin):
0268             return self._gr.GetY()[bin]
0269         def divide(self, bin, scale):
0270             # Ignore bin if denominator is zero
0271             if scale == 0:
0272                 return
0273             # No more items in the numerator
0274             if bin >= self._gr.GetN():
0275                 return
0276             # denominator is missing an item
0277             xvals = self.xvalues(bin)
0278             xval = xvals[0]
0279 
0280             self._xvalues.append(xval)
0281             self._xerrslow.append(xvals[1])
0282             self._xerrshigh.append(xvals[2])
0283             yvals = self.yvalues(bin)
0284             self._yvalues.append(yvals[0] / scale)
0285             if self._uncertainty:
0286                 self._yerrslow.append(yvals[1] / scale)
0287                 self._yerrshigh.append(yvals[2] / scale)
0288             else:
0289                 self._yerrslow.append(0)
0290                 self._yerrshigh.append(0)
0291         def makeRatio(self):
0292             if len(self._xvalues) == 0:
0293                 self._ratio = None
0294                 return
0295             self._ratio = ROOT.TGraphAsymmErrors(len(self._xvalues), array.array("d", self._xvalues), array.array("d", self._yvalues),
0296                                                  array.array("d", self._xerrslow), array.array("d", self._xerrshigh),
0297                                                  array.array("d", self._yerrslow), array.array("d", self._yerrshigh))
0298             _copyStyle(self._gr, self._ratio)
0299         def getRatio(self):
0300             return self._ratio
0301     class WrapTGraph2D(WrapTGraph):
0302         def __init__(self, gr, uncertainty):
0303             WrapTGraph.__init__(self, gr, uncertainty)
0304         def xvalues(self, bin):
0305             return (self._gr.GetX()[bin], self._gr.GetErrorX(bin), self._gr.GetErrorX(bin))
0306         def yvalues(self, bin):
0307             return (self._gr.GetY()[bin], self._gr.GetErrorY(bin), self._gr.GetErrorY(bin))
0308 
0309     def wrap(o):
0310         if isinstance(o, ROOT.TH1) and not isinstance(o, ROOT.TH2):
0311             return WrapTH1(o, ratioUncertainty)
0312         elif isinstance(o, ROOT.TGraph):
0313             return WrapTGraph(o, ratioUncertainty)
0314         elif isinstance(o, ROOT.TGraph2D):
0315             return WrapTGraph2D(o, ratioUncertainty)
0316 
0317     wrappers = [wrap(h) for h in histos if wrap(h) is not None]
0318     if len(wrappers) < 1:
0319         return []
0320     ref = wrappers[0]
0321 
0322     wrappers_bins = []
0323     ref_bins = [ref.xvalues(b) for b in range(ref.begin(), ref.end())]
0324     for w in wrappers:
0325         wrappers_bins.append(findBins(w, ref_bins))
0326 
0327     for i, bin in enumerate(range(ref.begin(), ref.end())):
0328         (scale, ylow, yhigh) = ref.yvalues(bin)
0329         for w, bins in zip(wrappers, wrappers_bins):
0330             if bins[i] is None:
0331                 continue
0332             w.divide(bins[i], scale)
0333 
0334     for w in wrappers:
0335         w.makeRatio()
0336 
0337     return wrappers
0338 
0339 
0340 def _getXmin(obj, limitToNonZeroContent=False):
0341     if isinstance(obj, ROOT.TH1):
0342         xaxis = obj.GetXaxis()
0343         if limitToNonZeroContent:
0344             for i in range(1, obj.GetNbinsX()+1):
0345                 if obj.GetBinContent(i) != 0:
0346                     return xaxis.GetBinLowEdge(i)
0347             # None for all bins being zero
0348             return None
0349         else:
0350             return xaxis.GetBinLowEdge(xaxis.GetFirst())
0351     elif isinstance(obj, ROOT.TGraph) or isinstance(obj, ROOT.TGraph2D):
0352         m = min([obj.GetX()[i] for i in range(0, obj.GetN())])
0353         return m*0.9 if m > 0 else m*1.1
0354     raise Exception("Unsupported type %s" % str(obj))
0355 
0356 def _getXmax(obj, limitToNonZeroContent=False):
0357     if isinstance(obj, ROOT.TH1):
0358         xaxis = obj.GetXaxis()
0359         if limitToNonZeroContent:
0360             for i in range(obj.GetNbinsX(), 0, -1):
0361                 if obj.GetBinContent(i) != 0:
0362                     return xaxis.GetBinUpEdge(i)
0363             # None for all bins being zero
0364             return None
0365         else:
0366             return xaxis.GetBinUpEdge(xaxis.GetLast())
0367     elif isinstance(obj, ROOT.TGraph) or isinstance(obj, ROOT.TGraph2D):
0368         m = max([obj.GetX()[i] for i in range(0, obj.GetN())])
0369         return m*1.1 if m > 0 else m*0.9
0370     raise Exception("Unsupported type %s" % str(obj))
0371 
0372 def _getYmin(obj, limitToNonZeroContent=False):
0373     if isinstance(obj, ROOT.TH2):
0374         yaxis = obj.GetYaxis()
0375         return yaxis.GetBinLowEdge(yaxis.GetFirst())
0376     elif isinstance(obj, ROOT.TH1):
0377         if limitToNonZeroContent:
0378             lst = [obj.GetBinContent(i) for i in range(1, obj.GetNbinsX()+1) if obj.GetBinContent(i) != 0 ]
0379             return min(lst) if len(lst) != 0 else 0
0380         else:
0381             return obj.GetMinimum()
0382     elif isinstance(obj, ROOT.TGraph) or isinstance(obj, ROOT.TGraph2D):
0383         m = min([obj.GetY()[i] for i in range(0, obj.GetN())])
0384         return m*0.9 if m > 0 else m*1.1
0385     raise Exception("Unsupported type %s" % str(obj))
0386 
0387 def _getYmax(obj, limitToNonZeroContent=False):
0388     if isinstance(obj, ROOT.TH2):
0389         yaxis = obj.GetYaxis()
0390         return yaxis.GetBinUpEdge(yaxis.GetLast())
0391     elif isinstance(obj, ROOT.TH1):
0392         if limitToNonZeroContent:
0393             lst = [obj.GetBinContent(i) for i in range(1, obj.GetNbinsX()+1) if obj.GetBinContent(i) != 0 ]
0394             return max(lst) if len(lst) != 0 else 0
0395         else:
0396             return obj.GetMaximum()
0397     elif isinstance(obj, ROOT.TGraph) or isinstance(obj, ROOT.TGraph2D):
0398         m = max([obj.GetY()[i] for i in range(0, obj.GetN())])
0399         return m*1.1 if m > 0 else m*0.9
0400     raise Exception("Unsupported type %s" % str(obj))
0401 
0402 def _getYmaxWithError(th1):
0403     return max([th1.GetBinContent(i)+th1.GetBinError(i) for i in range(1, th1.GetNbinsX()+1)])
0404 
0405 def _getYminIgnoreOutlier(th1):
0406     yvals = sorted([n for n in [th1.GetBinContent(i) for i in range(1, th1.GetNbinsX()+1)] if n>0])
0407     if len(yvals) == 0:
0408         return th1.GetMinimum()
0409     if len(yvals) == 1:
0410         return yvals[0]
0411 
0412     # Define outlier as being x10 less than minimum of the 95 % of the non-zero largest values
0413     ind_min = len(yvals)-1 - int(len(yvals)*0.95)
0414     min_val = yvals[ind_min]
0415     for i in range(0, ind_min):
0416         if yvals[i] > 0.1*min_val:
0417             return yvals[i]
0418 
0419     return min_val
0420 
0421 def _getYminMaxAroundMedian(obj, coverage, coverageRange=None):
0422     inRange = lambda x: True
0423     inRange2 = lambda xmin,xmax: True
0424     if coverageRange:
0425         inRange = lambda x: coverageRange[0] <= x <= coverageRange[1]
0426         inRange2 = lambda xmin,xmax: coverageRange[0] <= xmin and xmax <= coverageRange[1]
0427 
0428     if isinstance(obj, ROOT.TH1):
0429         yvals = [obj.GetBinContent(i) for i in range(1, obj.GetNbinsX()+1) if inRange2(obj.GetXaxis().GetBinLowEdge(i), obj.GetXaxis().GetBinUpEdge(i))]
0430         yvals = [x for x in yvals if x != 0]
0431     elif isinstance(obj, ROOT.TGraph) or isinstance(obj, ROOT.TGraph2D):
0432         yvals = [obj.GetY()[i] for i in range(0, obj.GetN()) if inRange(obj.GetX()[i])]
0433     else:
0434         raise Exception("Unsupported type %s" % str(obj))
0435     if len(yvals) == 0:
0436         return (0, 0)
0437     if len(yvals) == 1:
0438         return (yvals[0], yvals[0])
0439     if len(yvals) == 2:
0440         return (yvals[0], yvals[1])
0441 
0442     yvals.sort()
0443     nvals = int(len(yvals)*coverage)
0444     if nvals < 2:
0445         # Take median and +- 1 values
0446         if len(yvals) % 2 == 0:
0447             half = len(yvals)//2
0448             return ( yvals[half-1], yvals[half] )
0449         else:
0450             middle = len(yvals)//2
0451             return ( yvals[middle-1], yvals[middle+1] )
0452     ind_min = (len(yvals)-nvals)//2
0453     ind_max = len(yvals)-1 - ind_min
0454 
0455     return (yvals[ind_min], yvals[ind_max])
0456 
0457 def _findBounds(th1s, ylog, xmin=None, xmax=None, ymin=None, ymax=None):
0458     """Find x-y axis boundaries encompassing a list of TH1s if the bounds are not given in arguments.
0459 
0460     Arguments:
0461     th1s -- List of TH1s
0462     ylog -- Boolean indicating if y axis is in log scale or not (affects the automatic ymax)
0463 
0464     Keyword arguments:
0465     xmin -- Minimum x value; if None, take the minimum of TH1s
0466     xmax -- Maximum x value; if None, take the maximum of TH1s
0467     ymin -- Minimum y value; if None, take the minimum of TH1s
0468     ymax -- Maximum y value; if None, take the maximum of TH1s
0469     """
0470 
0471     (ymin, ymax) = _findBoundsY(th1s, ylog, ymin, ymax)
0472 
0473     if xmin is None or xmax is None or isinstance(xmin, list) or isinstance(max, list):
0474         xmins = []
0475         xmaxs = []
0476         for th1 in th1s:
0477             xmins.append(_getXmin(th1, limitToNonZeroContent=isinstance(xmin, list)))
0478             xmaxs.append(_getXmax(th1, limitToNonZeroContent=isinstance(xmax, list)))
0479 
0480         # Filter out cases where histograms have zero content
0481         xmins = [h for h in xmins if h is not None]
0482         xmaxs = [h for h in xmaxs if h is not None]
0483 
0484         if xmin is None:
0485             xmin = min(xmins)
0486         elif isinstance(xmin, list):
0487             if len(xmins) == 0: # all histograms zero
0488                 xmin = min(xmin)
0489                 if verbose:
0490                     print("Histogram is zero, using the smallest given value for xmin from", str(xmin))
0491             else:
0492                 xm = min(xmins)
0493                 xmins_below = [x for x in xmin if x<=xm]
0494                 if len(xmins_below) == 0:
0495                     xmin = min(xmin)
0496                     if xm < xmin:
0497                         if verbose:
0498                             print("Histogram minimum x %f is below all given xmin values %s, using the smallest one" % (xm, str(xmin)))
0499                 else:
0500                     xmin = max(xmins_below)
0501 
0502         if xmax is None:
0503             xmax = max(xmaxs)
0504         elif isinstance(xmax, list):
0505             if len(xmaxs) == 0: # all histograms zero
0506                 xmax = max(xmax)
0507                 if verbose:
0508                     print("Histogram is zero, using the smallest given value for xmax from", str(xmin))
0509             else:
0510                 xm = max(xmaxs)
0511                 xmaxs_above = [x for x in xmax if x>xm]
0512                 if len(xmaxs_above) == 0:
0513                     xmax = max(xmax)
0514                     if xm > xmax:
0515                         if verbose:
0516                             print("Histogram maximum x %f is above all given xmax values %s, using the maximum one" % (xm, str(xmax)))
0517                 else:
0518                     xmax = min(xmaxs_above)
0519 
0520     for th1 in th1s:
0521         th1.GetXaxis().SetRangeUser(xmin, xmax)
0522 
0523     return (xmin, ymin, xmax, ymax)
0524 
0525 def _findBoundsY(th1s, ylog, ymin=None, ymax=None, coverage=None, coverageRange=None):
0526     """Find y axis boundaries encompassing a list of TH1s if the bounds are not given in arguments.
0527 
0528     Arguments:
0529     th1s -- List of TH1s
0530     ylog -- Boolean indicating if y axis is in log scale or not (affects the automatic ymax)
0531 
0532     Keyword arguments:
0533     ymin -- Minimum y value; if None, take the minimum of TH1s
0534     ymax -- Maximum y value; if None, take the maximum of TH1s
0535     coverage -- If set, use only values within the 'coverage' part around the median are used for min/max (useful for ratio)
0536     coverageRange -- If coverage and this are set, use only the x axis specified by an (xmin,xmax) pair for the coverage
0537     """
0538     if coverage is not None or isinstance(th1s[0], ROOT.TH2):
0539         # the only use case for coverage for now is ratio, for which
0540         # the scalings are not needed (actually harmful), so let's
0541         # just ignore them if 'coverage' is set
0542         #
0543         # Also for TH2 do not adjust automatic y bounds
0544         y_scale_max = lambda y: y
0545         y_scale_min = lambda y: y
0546     else:
0547         if ylog:
0548             y_scale_max = lambda y: y*1.5
0549         else:
0550             y_scale_max = lambda y: y*1.05
0551         y_scale_min = lambda y: y*0.9 # assuming log
0552 
0553     if ymin is None or ymax is None or isinstance(ymin, list) or isinstance(ymax, list):
0554         ymins = []
0555         ymaxs = []
0556         for th1 in th1s:
0557             if coverage is not None:
0558                 (_ymin, _ymax) = _getYminMaxAroundMedian(th1, coverage, coverageRange)
0559             else:
0560                 if ylog and isinstance(ymin, list):
0561                     _ymin = _getYminIgnoreOutlier(th1)
0562                 else:
0563                     _ymin = _getYmin(th1, limitToNonZeroContent=isinstance(ymin, list))
0564                 _ymax = _getYmax(th1, limitToNonZeroContent=isinstance(ymax, list))
0565 #                _ymax = _getYmaxWithError(th1)
0566 
0567             ymins.append(_ymin)
0568             ymaxs.append(_ymax)
0569 
0570         if ymin is None:
0571             ymin = min(ymins)
0572         elif isinstance(ymin, list):
0573             ym_unscaled = min(ymins)
0574             ym = y_scale_min(ym_unscaled)
0575             ymins_below = [y for y in ymin if y<=ym]
0576             if len(ymins_below) == 0:
0577                 ymin = min(ymin)
0578                 if ym_unscaled < ymin:
0579                     if verbose:
0580                         print("Histogram minimum y %f is below all given ymin values %s, using the smallest one" % (ym, str(ymin)))
0581             else:
0582                 ymin = max(ymins_below)
0583 
0584         if ymax is None:
0585             # in case ymax is automatic, ymin is set by list, and the
0586             # histograms are zero, ensure here that ymax > ymin
0587             ymax = y_scale_max(max(ymaxs+[ymin]))
0588         elif isinstance(ymax, list):
0589             ym_unscaled = max(ymaxs)
0590             ym = y_scale_max(ym_unscaled)
0591             ymaxs_above = [y for y in ymax if y>ym]
0592             if len(ymaxs_above) == 0:
0593                 ymax = max(ymax)
0594                 if ym_unscaled > ymax:
0595                     if verbose:
0596                         print("Histogram maximum y %f is above all given ymax values %s, using the maximum one" % (ym_unscaled, str(ymax)))
0597             else:
0598                 ymax = min(ymaxs_above)
0599 
0600     for th1 in th1s:
0601         th1.GetYaxis().SetRangeUser(ymin, ymax)
0602 
0603     return (ymin, ymax)
0604 
0605 def _th1RemoveEmptyBins(histos, xbinlabels):
0606     binsToRemove = set()
0607     for b in range(1, histos[0].GetNbinsX()+1):
0608         binEmpty = True
0609         for h in histos:
0610             if h.GetBinContent(b) > 0:
0611                 binEmpty = False
0612                 break
0613         if binEmpty:
0614             binsToRemove.add(b)
0615 
0616     if len(binsToRemove) > 0:
0617         # filter xbinlabels
0618         xbinlab_new = []
0619         for i in range(len(xbinlabels)):
0620             if (i+1) not in binsToRemove:
0621                 xbinlab_new.append(xbinlabels[i])
0622         xbinlabels = xbinlab_new
0623 
0624         # filter histogram bins
0625         histos_new = []
0626         for h in histos:
0627             values = []
0628             for b in range(1, h.GetNbinsX()+1):
0629                 if b not in binsToRemove:
0630                     values.append( (h.GetXaxis().GetBinLabel(b), h.GetBinContent(b), h.GetBinError(b)) )
0631 
0632             if len(values) > 0:
0633                 h_new = h.Clone(h.GetName()+"_empty")
0634                 h_new.SetBins(len(values), h.GetBinLowEdge(1), h.GetBinLowEdge(1)+len(values))
0635                 for b, (l, v, e) in enumerate(values):
0636                     h_new.GetXaxis().SetBinLabel(b+1, l)
0637                     h_new.SetBinContent(b+1, v)
0638                     h_new.SetBinError(b+1, e)
0639 
0640                 histos_new.append(h_new)
0641         histos = histos_new
0642 
0643     return (histos, xbinlabels)
0644 
0645 def _th2RemoveEmptyBins(histos, xbinlabels, ybinlabels):
0646     xbinsToRemove = set()
0647     ybinsToRemove = set()
0648     for ih, h in enumerate(histos):
0649         for bx in range(1, h.GetNbinsX()+1):
0650             binEmpty = True
0651             for by in range(1, h.GetNbinsY()+1):
0652                 if h.GetBinContent(bx, by) > 0:
0653                     binEmpty = False
0654                     break
0655             if binEmpty:
0656                 xbinsToRemove.add(bx)
0657             elif ih > 0:
0658                 xbinsToRemove.discard(bx)
0659 
0660         for by in range(1, h.GetNbinsY()+1):
0661             binEmpty = True
0662             for bx in range(1, h.GetNbinsX()+1):
0663                 if h.GetBinContent(bx, by) > 0:
0664                     binEmpty = False
0665                     break
0666             if binEmpty:
0667                 ybinsToRemove.add(by)
0668             elif ih > 0:
0669                 ybinsToRemove.discard(by)
0670 
0671     if len(xbinsToRemove) > 0 or len(ybinsToRemove) > 0:
0672         xbinlabels_new = []
0673         xbins = []
0674         for b in range(1, len(xbinlabels)+1):
0675             if b not in xbinsToRemove:
0676                 xbinlabels_new.append(histos[0].GetXaxis().GetBinLabel(b))
0677                 xbins.append(b)
0678         xbinlabels = xbinlabels_new
0679         ybinlabels_new = []
0680         ybins = []
0681         for b in range(1, len(ybinlabels)+1):
0682             if b not in ybinsToRemove:
0683                 ybinlabels.append(histos[0].GetYaxis().GetBinLabel(b))
0684                 ybins.append(b)
0685         ybinlabels = xbinlabels_new
0686 
0687         histos_new = []
0688         if len(xbinlabels) == 0 or len(ybinlabels) == 0:
0689             return (histos_new, xbinlabels, ybinlabels)
0690         for h in histos:
0691             h_new = ROOT.TH2F(h.GetName()+"_empty", h.GetTitle(), len(xbinlabels),0,len(xbinlabels), len(ybinlabels),0,len(ybinlabels))
0692             for b, l in enumerate(xbinlabels):
0693                 h_new.GetXaxis().SetBinLabel(b+1, l)
0694             for b, l in enumerate(ybinlabels):
0695                 h_new.GetYaxis().SetBinLabel(b+1, l)
0696 
0697             for ix, bx in enumerate(xbins):
0698                 for iy, by in enumerate(ybins):
0699                     h_new.SetBinContent(ix+1, iy+1, h.GetBinContent(bx, by))
0700                     h_new.SetBinError(ix+1, iy+1, h.GetBinError(bx, by))
0701             histos_new.append(h_new)
0702         histos = histos_new
0703     return (histos, xbinlabels, ybinlabels)
0704 
0705 def _mergeBinLabelsX(histos):
0706     return _mergeBinLabels([[h.GetXaxis().GetBinLabel(i) for i in range(1, h.GetNbinsX()+1)] for h in histos])
0707 
0708 def _mergeBinLabelsY(histos):
0709     return _mergeBinLabels([[h.GetYaxis().GetBinLabel(i) for i in range(1, h.GetNbinsY()+1)] for h in histos])
0710 
0711 def _mergeBinLabels(labelsAll):
0712     labels_merged = labelsAll[0]
0713     for labels in labelsAll[1:]:
0714         diff = difflib.unified_diff(labels_merged, labels, n=max(len(labels_merged), len(labels)))
0715         labels_merged = []
0716         operation = []
0717         for item in diff: # skip the "header" lines
0718             if item[:2] == "@@":
0719                 break
0720         for item in diff:
0721             operation.append(item[0])
0722             lab = item[1:]
0723             if lab in labels_merged:
0724                 # pick the last addition of the bin
0725                 ind = labels_merged.index(lab)
0726                 if operation[ind] == "-" and operation[-1] == "+":
0727                     labels_merged.remove(lab)
0728                     del operation[ind] # to keep xbinlabels and operation indices in sync
0729                 elif operation[ind] == "+" and operation[-1] == "-":
0730                     del operation[-1] # to keep xbinlabels and operation indices in sync
0731                     continue
0732                 else:
0733                     raise Exception("This should never happen")
0734             labels_merged.append(lab)
0735         # unified_diff returns empty diff if labels_merged and labels are equal
0736         # so if labels_merged is empty here, it can be just set to labels
0737         if len(labels_merged) == 0:
0738             labels_merged = labels
0739 
0740     return labels_merged
0741 
0742 def _th1IncludeOnlyBins(histos, xbinlabels):
0743     histos_new = []
0744     for h in histos:
0745         h_new = h.Clone(h.GetName()+"_xbinlabels")
0746         h_new.SetBins(len(xbinlabels), h.GetBinLowEdge(1), h.GetBinLowEdge(1)+len(xbinlabels))
0747         for i, label in enumerate(xbinlabels):
0748             bin = h.GetXaxis().FindFixBin(label)
0749             if bin >= 0:
0750                 h_new.SetBinContent(i+1, h.GetBinContent(bin))
0751                 h_new.SetBinError(i+1, h.GetBinError(bin))
0752             else:
0753                 h_new.SetBinContent(i+1, 0)
0754                 h_new.SetBinError(i+1, 0)
0755         histos_new.append(h_new)
0756     return histos_new
0757 
0758 
0759 class Subtract:
0760     """Class for subtracting two histograms"""
0761     def __init__(self, name, nameA, nameB, title=""):
0762         """Constructor
0763 
0764         Arguments:
0765         name  -- String for name of the resulting histogram (A-B)
0766         nameA -- String for A histogram
0767         nameB -- String for B histogram
0768 
0769         Keyword arguments:
0770         title -- String for a title of the resulting histogram (default "")
0771 
0772         Uncertainties are calculated with the assumption that B is a
0773         subset of A, and the histograms contain event counts.
0774         """
0775         self._name = name
0776         self._nameA = nameA
0777         self._nameB = nameB
0778         self._title = title
0779 
0780     def __str__(self):
0781         """String representation, returns the name"""
0782         return self._name
0783 
0784     def create(self, tdirectory):
0785         """Create and return the fake+duplicate histogram from a TDirectory"""
0786         histoA = _getObject(tdirectory, self._nameA)
0787         histoB = _getObject(tdirectory, self._nameB)
0788 
0789         if not histoA or not histoB:
0790             return None
0791 
0792         ret = histoA.Clone(self._name)
0793         ret.SetTitle(self._title)
0794 
0795         # Disable canExtend if it is set, otherwise setting the
0796         # overflow bin will extend instead, possibly causing weird
0797         # effects downstream
0798         ret.SetCanExtend(False)
0799 
0800         for i in range(0, histoA.GetNbinsX()+2): # include under- and overflow too
0801             val = histoA.GetBinContent(i)-histoB.GetBinContent(i)
0802             ret.SetBinContent(i, val)
0803             ret.SetBinError(i, math.sqrt(val))
0804 
0805         return ret
0806 
0807 class Transform:
0808     """Class to transform bin contents in an arbitrary way."""
0809     def __init__(self, name, histo, func, title=""):
0810         """Constructor.
0811 
0812         Argument:
0813         name  -- String for name of the resulting histogram
0814         histo -- String for a source histogram (needs to be cumulative)
0815         func  -- Function to operate on the bin content
0816         """
0817         self._name = name
0818         self._histo = histo
0819         self._func = func
0820         self._title = title
0821 
0822     def __str__(self):
0823         """String representation, returns the name"""
0824         return self._name
0825 
0826     def create(self, tdirectory):
0827         """Create and return the transformed histogram from a TDirectory"""
0828         histo = _getOrCreateObject(tdirectory, self._histo)
0829         if not histo:
0830             return None
0831 
0832         ret = histo.Clone(self._name)
0833         ret.SetTitle(self._title)
0834 
0835         # Disable canExtend if it is set, otherwise setting the
0836         # overflow bin will extend instead, possibly causing weird
0837         # effects downstream
0838         ret.SetCanExtend(False)
0839 
0840         for i in range(0, histo.GetNbinsX()+2):
0841             ret.SetBinContent(i, self._func(histo.GetBinContent(i)))
0842         return ret
0843 
0844 class FakeDuplicate:
0845     """Class to calculate the fake+duplicate rate"""
0846     def __init__(self, name, assoc, dup, reco, title=""):
0847         """Constructor.
0848 
0849         Arguments:
0850         name  -- String for the name of the resulting efficiency histogram
0851         assoc -- String for the name of the "associated" histogram
0852         dup   -- String for the name of the "duplicates" histogram
0853         reco  -- String for the name of the "reco" (denominator) histogram
0854 
0855         Keyword arguments:
0856         title  -- String for a title of the resulting histogram (default "")
0857 
0858         The result is calculated as 1 - (assoc - dup) / reco
0859         """
0860         self._name = name
0861         self._assoc = assoc
0862         self._dup = dup
0863         self._reco = reco
0864         self._title = title
0865 
0866     def __str__(self):
0867         """String representation, returns the name"""
0868         return self._name
0869 
0870     def create(self, tdirectory):
0871         """Create and return the fake+duplicate histogram from a TDirectory"""
0872         # Get the numerator/denominator histograms
0873         hassoc = _getObject(tdirectory, self._assoc)
0874         hdup = _getObject(tdirectory, self._dup)
0875         hreco = _getObject(tdirectory, self._reco)
0876 
0877         # Skip if any of them does not exist
0878         if not hassoc or not hdup or not hreco:
0879             return None
0880 
0881         hfakedup = hreco.Clone(self._name)
0882         hfakedup.SetTitle(self._title)
0883 
0884         for i in range(1, hassoc.GetNbinsX()+1):
0885             numerVal = hassoc.GetBinContent(i) - hdup.GetBinContent(i)
0886             denomVal = hreco.GetBinContent(i)
0887 
0888             fakedupVal = (1 - numerVal / denomVal) if denomVal != 0.0 else 0.0
0889             errVal = math.sqrt(fakedupVal*(1-fakedupVal)/denomVal) if (denomVal != 0.0 and fakedupVal <= 1) else 0.0
0890 
0891             hfakedup.SetBinContent(i, fakedupVal)
0892             hfakedup.SetBinError(i, errVal)
0893 
0894         return hfakedup
0895 
0896 class CutEfficiency:
0897     """Class for making a cut efficiency histograms.
0898 
0899           N after cut
0900     eff = -----------
0901             N total
0902     """
0903     def __init__(self, name, histo, title=""):
0904         """Constructor
0905 
0906         Arguments:
0907         name  -- String for name of the resulting histogram
0908         histo -- String for a source histogram (needs to be cumulative)
0909         """
0910         self._name = name
0911         self._histo = histo
0912         self._title = title
0913 
0914     def __str__(self):
0915         """String representation, returns the name"""
0916         return self._name
0917 
0918     def create(self, tdirectory):
0919         """Create and return the cut efficiency histogram from a TDirectory"""
0920         histo = _getOrCreateObject(tdirectory, self._histo)
0921         if not histo:
0922             return None
0923 
0924         # infer cumulative direction from the under/overflow bins
0925         ascending = histo.GetBinContent(0) < histo.GetBinContent(histo.GetNbinsX())
0926         if ascending:
0927             n_tot = histo.GetBinContent(histo.GetNbinsX())
0928         else:
0929             n_tot = histo.GetBinContent(0)
0930 
0931         if n_tot == 0:
0932             return histo
0933 
0934         ret = histo.Clone(self._name)
0935         ret.SetTitle(self._title)
0936 
0937         # calculate efficiency
0938         for i in range(1, histo.GetNbinsX()+1):
0939             n = histo.GetBinContent(i)
0940             val = n/n_tot
0941             errVal = math.sqrt(val*(1-val)/n_tot)
0942             ret.SetBinContent(i, val)
0943             ret.SetBinError(i, errVal)
0944         return ret
0945 
0946 class AggregateBins:
0947     """Class to create a histogram by aggregating bins of another histogram to a bin of the resulting histogram."""
0948     def __init__(self, name, histoName, mapping, normalizeTo=None, scale=None, renameBin=None, ignoreMissingBins=False, minExistingBins=None, originalOrder=False, reorder=None):
0949         """Constructor.
0950 
0951         Arguments:
0952         name      -- String for the name of the resulting histogram
0953         histoName -- String for the name of the source histogram
0954         mapping   -- Dictionary for mapping the bins (see below)
0955 
0956         Keyword arguments:
0957         normalizeTo -- Optional string of a bin label in the source histogram. If given, all bins of the resulting histogram are divided by the value of this bin.
0958         scale       -- Optional number for scaling the histogram (passed to ROOT.TH1.Scale())
0959         renameBin   -- Optional function (string -> string) to rename the bins of the input histogram
0960         originalOrder -- Boolean for using the order of bins in the histogram (default False)
0961         reorder     -- Optional function to reorder the bins
0962 
0963         Mapping structure (mapping):
0964 
0965         Dictionary (you probably want to use collections.OrderedDict)
0966         should be a mapping from the destination bin label to a list
0967         of source bin labels ("dst -> [src]").
0968         """
0969         self._name = name
0970         self._histoName = histoName
0971         self._mapping = mapping
0972         self._normalizeTo = normalizeTo
0973         self._scale = scale
0974         self._renameBin = renameBin
0975         self._ignoreMissingBins = ignoreMissingBins
0976         self._minExistingBins = minExistingBins
0977         self._originalOrder = originalOrder
0978         self._reorder = reorder
0979         if self._originalOrder and self._reorder is not None:
0980             raise Exception("reorder is not None and originalOrder is True, please set only one of them")
0981 
0982     def __str__(self):
0983         """String representation, returns the name"""
0984         return self._name
0985 
0986     def create(self, tdirectory):
0987         """Create and return the histogram from a TDirectory"""
0988         th1 = _getOrCreateObject(tdirectory, self._histoName)
0989         if th1 is None:
0990             return None
0991 
0992         binLabels = [""]*len(self._mapping)
0993         binValues = [None]*len(self._mapping)
0994 
0995         # TH1 can't really be used as a map/dict, so convert it here:
0996         values = _th1ToOrderedDict(th1, self._renameBin)
0997 
0998         binIndexOrder = [] # for reordering bins if self._originalOrder is True
0999         for i, (key, labels) in enumerate(self._mapping.items()):
1000             sumTime = 0.
1001             sumErrorSq = 0.
1002             nsum = 0
1003             for l in labels:
1004                 try:
1005                     sumTime += values[l][0]
1006                     sumErrorSq += values[l][1]**2
1007                     nsum += 1
1008                 except KeyError:
1009                     pass
1010 
1011             if nsum > 0:
1012                 binValues[i] = (sumTime, math.sqrt(sumErrorSq))
1013             binLabels[i] = key
1014 
1015             ivalue = len(values)+1
1016             if len(labels) > 0:
1017                 # first label doesn't necessarily exist (especially for
1018                 # the iteration timing plots), so let's test them all
1019                 for lab in labels:
1020                     if lab in values:
1021                         ivalue = list(values.keys()).index(lab)
1022                         break
1023             binIndexOrder.append( (ivalue, i) )
1024 
1025         if self._originalOrder:
1026             binIndexOrder.sort(key=lambda t: t[0])
1027             tmpVal = []
1028             tmpLab = []
1029             for i in range(0, len(binValues)):
1030                 fromIndex = binIndexOrder[i][1]
1031                 tmpVal.append(binValues[fromIndex])
1032                 tmpLab.append(binLabels[fromIndex])
1033             binValues = tmpVal
1034             binLabels = tmpLab
1035         if self._reorder is not None:
1036             order = self._reorder(tdirectory, binLabels)
1037             binValues = [binValues[i] for i in order]
1038             binLabels = [binLabels[i] for i in order]
1039 
1040         if self._minExistingBins is not None and (len(binValues)-binValues.count(None)) < self._minExistingBins:
1041             return None
1042 
1043         if self._ignoreMissingBins:
1044             for i, val in enumerate(binValues):
1045                 if val is None:
1046                     binLabels[i] = None
1047             binValues = [v for v in binValues if v is not None]
1048             binLabels = [v for v in binLabels if v is not None]
1049             if len(binValues) == 0:
1050                 return None
1051 
1052         result = ROOT.TH1F(self._name, self._name, len(binValues), 0, len(binValues))
1053         for i, (value, label) in enumerate(zip(binValues, binLabels)):
1054             if value is not None:
1055                 result.SetBinContent(i+1, value[0])
1056                 result.SetBinError(i+1, value[1])
1057             result.GetXaxis().SetBinLabel(i+1, label)
1058 
1059         if self._normalizeTo is not None:
1060             bin = th1.GetXaxis().FindBin(self._normalizeTo)
1061             if bin <= 0:
1062                 print("Trying to normalize {name} to {binlabel}, which does not exist".format(name=self._name, binlabel=self._normalizeTo))
1063                 sys.exit(1)
1064             value = th1.GetBinContent(bin)
1065             if value != 0:
1066                 result.Scale(1/value)
1067 
1068         if self._scale is not None:
1069             result.Scale(self._scale)
1070 
1071         return result
1072 
1073 class AggregateHistos:
1074     """Class to create a histogram by aggregating integrals of another histogram."""
1075     def __init__(self, name, mapping, normalizeTo=None):
1076         """Constructor.
1077 
1078         Arguments:
1079         name    -- String for the name of the resulting histogram
1080         mapping -- Dictionary for mapping the bin label to a histogram name
1081 
1082         Keyword arguments:
1083         normalizeTo -- Optional string for a histogram. If given, all bins of the resulting histograqm are divided by the integral of this histogram.
1084         """
1085         self._name = name
1086         self._mapping = mapping
1087         self._normalizeTo = normalizeTo
1088 
1089     def __str__(self):
1090         """String representation, returns the name"""
1091         return self._name
1092 
1093     def create(self, tdirectory):
1094         """Create and return the histogram from a TDirectory"""
1095         result = []
1096         for key, histoName in self._mapping.items():
1097             th1 = _getObject(tdirectory, histoName)
1098             if th1 is None:
1099                 continue
1100             result.append( (key, th1.Integral(0, th1.GetNbinsX()+1)) ) # include under- and overflow bins
1101         if len(result) == 0:
1102             return None
1103 
1104         res = ROOT.TH1F(self._name, self._name, len(result), 0, len(result))
1105 
1106         for i, (name, count) in enumerate(result):
1107             res.SetBinContent(i+1, count)
1108             res.GetXaxis().SetBinLabel(i+1, name)
1109 
1110         if self._normalizeTo is not None:
1111             th1 = _getObject(tdirectory, self._normalizeTo)
1112             if th1 is None:
1113                 return None
1114             scale = th1.Integral(0, th1.GetNbinsX()+1)
1115             res.Scale(1/scale)
1116 
1117         return res
1118 
1119 class ROC:
1120     """Class to construct a ROC curve (e.g. efficiency vs. fake rate) from two histograms"""
1121     def __init__(self, name, xhistoName, yhistoName, zaxis=False):
1122         """Constructor.
1123 
1124         Arguments:
1125         name       -- String for the name of the resulting histogram
1126         xhistoName -- String for the name of the x-axis histogram (or another "creator" object)
1127         yhistoName -- String for the name of the y-axis histogram (or another "creator" object)
1128 
1129         Keyword arguments:
1130         zaxis -- If set to True (default False), create a TGraph2D with z axis showing the cut value (recommended drawStyle 'pcolz')
1131         """
1132         self._name = name
1133         self._xhistoName = xhistoName
1134         self._yhistoName = yhistoName
1135         self._zaxis = zaxis
1136 
1137     def __str__(self):
1138         """String representation, returns the name"""
1139         return self._name
1140 
1141     def create(self, tdirectory):
1142         """Create and return the histogram from a TDirectory"""
1143         xhisto = _getOrCreateObject(tdirectory, self._xhistoName)
1144         yhisto = _getOrCreateObject(tdirectory, self._yhistoName);
1145         if xhisto is None or yhisto is None:
1146             return None
1147 
1148         x = []
1149         xerrup = []
1150         xerrdown = []
1151         y = []
1152         yerrup = []
1153         yerrdown = []
1154         z = []
1155 
1156         for i in range(1, xhisto.GetNbinsX()+1):
1157             x.append(xhisto.GetBinContent(i))
1158             xerrup.append(xhisto.GetBinError(i))
1159             xerrdown.append(xhisto.GetBinError(i))
1160 
1161             y.append(yhisto.GetBinContent(i))
1162             yerrup.append(yhisto.GetBinError(i))
1163             yerrdown.append(yhisto.GetBinError(i))
1164 
1165             z.append(xhisto.GetXaxis().GetBinUpEdge(i))
1166 
1167         # If either axis has only zeroes, no graph makes no point
1168         if x.count(0.0) == len(x) or y.count(0.0) == len(y):
1169             return None
1170 
1171         arr = lambda v: array.array("d", v)
1172         gr = None
1173         if self._zaxis:
1174             gr = ROOT.TGraph2D(len(x), arr(x), arr(y), arr(z))
1175         else:
1176             gr = ROOT.TGraphAsymmErrors(len(x), arr(x), arr(y), arr(xerrdown), arr(xerrup), arr(yerrdown), arr(yerrup))
1177         gr.SetTitle("")
1178         return gr
1179 
1180 
1181 # Plot styles
1182 _plotStylesColor = [4, 2, ROOT.kBlack, ROOT.kOrange+7, ROOT.kMagenta-3, ROOT.kGreen+2]
1183 _plotStylesMarker = [21, 20, 22, 34, 33, 23]
1184 
1185 def _drawFrame(pad, bounds, zmax=None, xbinlabels=None, xbinlabelsize=None, xbinlabeloption=None, ybinlabels=None, suffix=""):
1186     """Function to draw a frame
1187 
1188     Arguments:
1189     pad    -- TPad to where the frame is drawn
1190     bounds -- List or 4-tuple for (xmin, ymin, xmax, ymax)
1191 
1192     Keyword arguments:
1193     zmax            -- Maximum Z, needed for TH2 histograms
1194     xbinlabels      -- Optional list of strings for x axis bin labels
1195     xbinlabelsize   -- Optional number for the x axis bin label size
1196     xbinlabeloption -- Optional string for the x axis bin options (passed to ROOT.TH1.LabelsOption())
1197     suffix          -- Optional string for a postfix of the frame name
1198     """
1199     if xbinlabels is None and ybinlabels is None:
1200         frame = pad.DrawFrame(*bounds)
1201     else:
1202         # Special form needed if want to set x axis bin labels
1203         nbins = len(xbinlabels)
1204         if ybinlabels is None:
1205             frame = ROOT.TH1F("hframe"+suffix, "", nbins, bounds[0], bounds[2])
1206             frame.SetMinimum(bounds[1])
1207             frame.SetMaximum(bounds[3])
1208             frame.GetYaxis().SetLimits(bounds[1], bounds[3])
1209         else:
1210             ybins = len(ybinlabels)
1211             frame = ROOT.TH2F("hframe"+suffix, "", nbins,bounds[0],bounds[2], ybins,bounds[1],bounds[3])
1212             frame.SetMaximum(zmax)
1213 
1214         frame.SetBit(ROOT.TH1.kNoStats)
1215         frame.SetBit(ROOT.kCanDelete)
1216         frame.Draw("")
1217 
1218         xaxis = frame.GetXaxis()
1219         for i in range(nbins):
1220             xaxis.SetBinLabel(i+1, xbinlabels[i])
1221         if xbinlabelsize is not None:
1222             xaxis.SetLabelSize(xbinlabelsize)
1223         if xbinlabeloption is not None:
1224             frame.LabelsOption(xbinlabeloption)
1225 
1226         if ybinlabels is not None:
1227             yaxis = frame.GetYaxis()
1228             for i, lab in enumerate(ybinlabels):
1229                 yaxis.SetBinLabel(i+1, lab)
1230             if xbinlabelsize is not None:
1231                 yaxis.SetLabelSize(xbinlabelsize)
1232             if xbinlabeloption is not None:
1233                 frame.LabelsOption(xbinlabeloption, "Y")
1234 
1235     return frame
1236 
1237 class Frame:
1238     """Class for creating and managing a frame for a simple, one-pad plot"""
1239     def __init__(self, pad, bounds, zmax, nrows, xbinlabels=None, xbinlabelsize=None, xbinlabeloption=None, ybinlabels=None):
1240         self._pad = pad
1241         self._frame = _drawFrame(pad, bounds, zmax, xbinlabels, xbinlabelsize, xbinlabeloption, ybinlabels)
1242 
1243         yoffsetFactor = 1
1244         xoffsetFactor = 0
1245 
1246         self._frame.GetYaxis().SetTitleOffset(self._frame.GetYaxis().GetTitleOffset()*yoffsetFactor)
1247         self._frame.GetXaxis().SetTitleOffset(self._frame.GetXaxis().GetTitleOffset()*xoffsetFactor)
1248 
1249 
1250     def setLogx(self, log):
1251         self._pad.SetLogx(log)
1252 
1253     def setLogy(self, log):
1254         self._pad.SetLogy(log)
1255 
1256     def setGridx(self, grid):
1257         self._pad.SetGridx(grid)
1258 
1259     def setGridy(self, grid):
1260         self._pad.SetGridy(grid)
1261 
1262     def adjustMarginLeft(self, adjust):
1263         self._pad.SetLeftMargin(self._pad.GetLeftMargin()+adjust)
1264         # Need to redraw frame after adjusting the margin
1265         self._pad.cd()
1266         self._frame.Draw("")
1267 
1268     def adjustMarginRight(self, adjust):
1269         self._pad.SetRightMargin(self._pad.GetRightMargin()+adjust)
1270         # Need to redraw frame after adjusting the margin
1271         self._pad.cd()
1272         self._frame.Draw("")
1273 
1274     def setTitle(self, title):
1275         self._frame.SetTitle(title)
1276 
1277     def setXTitle(self, title):
1278         self._frame.GetXaxis().SetTitle(title)
1279 
1280     def setXTitleSize(self, size):
1281         self._frame.GetXaxis().SetTitleSize(size)
1282 
1283     def setXTitleOffset(self, offset):
1284         self._frame.GetXaxis().SetTitleOffset(offset)
1285 
1286     def setXLabelSize(self, size):
1287         self._frame.GetXaxis().SetLabelSize(size)
1288 
1289     def setYTitle(self, title):
1290         self._frame.GetYaxis().SetTitle(title)
1291 
1292     def setYTitleSize(self, size):
1293         self._frame.GetYaxis().SetTitleSize(size)
1294 
1295     def setYTitleOffset(self, offset):
1296         self._frame.GetYaxis().SetTitleOffset(offset)
1297 
1298     def redrawAxis(self):
1299         self._pad.RedrawAxis()
1300 
1301 class FrameRatio:
1302     """Class for creating and managing a frame for a ratio plot with two subpads"""
1303     def __init__(self, pad, bounds, zmax, ratioBounds, ratioFactor, nrows, xbinlabels=None, xbinlabelsize=None, xbinlabeloption=None, ratioYTitle=_ratioYTitle):
1304         self._parentPad = pad
1305         self._pad = pad.cd(1)
1306         if xbinlabels is not None:
1307             self._frame = _drawFrame(self._pad, bounds, zmax, [""]*len(xbinlabels))
1308         else:
1309             self._frame = _drawFrame(self._pad, bounds, zmax)
1310         self._padRatio = pad.cd(2)
1311         self._frameRatio = _drawFrame(self._padRatio, ratioBounds, zmax, xbinlabels, xbinlabelsize, xbinlabeloption)
1312 
1313         self._frame.GetXaxis().SetLabelSize(0)
1314         self._frame.GetXaxis().SetTitleSize(0)
1315 
1316         yoffsetFactor = ratioFactor
1317         xoffsetFactor = 0
1318 
1319         self._frame.GetYaxis().SetTitleOffset(self._frameRatio.GetYaxis().GetTitleOffset()*yoffsetFactor)
1320         self._frameRatio.GetYaxis().SetLabelSize(int(self._frameRatio.GetYaxis().GetLabelSize()*0.8))
1321         self._frameRatio.GetYaxis().SetTitleOffset(self._frameRatio.GetYaxis().GetTitleOffset()*yoffsetFactor)
1322         self._frameRatio.GetXaxis().SetTitleOffset(self._frameRatio.GetXaxis().GetTitleOffset()*xoffsetFactor)
1323 
1324         self._frameRatio.GetYaxis().SetNdivisions(4, 5, 0)
1325 
1326         self._frameRatio.GetYaxis().SetTitle(ratioYTitle)
1327 
1328     def setLogx(self, log):
1329         self._pad.SetLogx(log)
1330         self._padRatio.SetLogx(log)
1331 
1332     def setLogy(self, log):
1333         self._pad.SetLogy(log)
1334 
1335     def setGridx(self, grid):
1336         self._pad.SetGridx(grid)
1337         self._padRatio.SetGridx(grid)
1338 
1339     def setGridy(self, grid):
1340         self._pad.SetGridy(grid)
1341         self._padRatio.SetGridy(grid)
1342 
1343     def adjustMarginLeft(self, adjust):
1344         self._pad.SetLeftMargin(self._pad.GetLeftMargin()+adjust)
1345         self._padRatio.SetLeftMargin(self._padRatio.GetLeftMargin()+adjust)
1346         # Need to redraw frame after adjusting the margin
1347         self._pad.cd()
1348         self._frame.Draw("")
1349         self._padRatio.cd()
1350         self._frameRatio.Draw("")
1351 
1352     def adjustMarginRight(self, adjust):
1353         self._pad.SetRightMargin(self._pad.GetRightMargin()+adjust)
1354         self._padRatio.SetRightMargin(self._padRatio.GetRightMargin()+adjust)
1355         # Need to redraw frames after adjusting the margin
1356         self._pad.cd()
1357         self._frame.Draw("")
1358         self._padRatio.cd()
1359         self._frameRatio.Draw("")
1360 
1361     def setTitle(self, title):
1362         self._frame.SetTitle(title)
1363 
1364     def setXTitle(self, title):
1365         self._frameRatio.GetXaxis().SetTitle(title)
1366 
1367     def setXTitleSize(self, size):
1368         self._frameRatio.GetXaxis().SetTitleSize(size)
1369 
1370     def setXTitleOffset(self, offset):
1371         self._frameRatio.GetXaxis().SetTitleOffset(offset)
1372 
1373     def setXLabelSize(self, size):
1374         self._frameRatio.GetXaxis().SetLabelSize(size)
1375 
1376     def setYTitle(self, title):
1377         self._frame.GetYaxis().SetTitle(title)
1378 
1379     def setYTitleRatio(self, title):
1380         self._frameRatio.GetYaxis().SetTitle(title)
1381 
1382     def setYTitleSize(self, size):
1383         self._frame.GetYaxis().SetTitleSize(size)
1384         self._frameRatio.GetYaxis().SetTitleSize(size)
1385 
1386     def setYTitleOffset(self, offset):
1387         self._frame.GetYaxis().SetTitleOffset(offset)
1388         self._frameRatio.GetYaxis().SetTitleOffset(offset)
1389 
1390     def redrawAxis(self):
1391         self._padRatio.RedrawAxis()
1392         self._pad.RedrawAxis()
1393 
1394         self._parentPad.cd()
1395 
1396         # pad to hide the lowest y axis label of the main pad
1397         xmin=0.065
1398         ymin=0.285
1399         xmax=0.128
1400         ymax=0.33
1401         self._coverPad = ROOT.TPad("coverpad", "coverpad", xmin, ymin, xmax, ymax)
1402         self._coverPad.SetBorderMode(0)
1403         self._coverPad.Draw()
1404 
1405         self._pad.cd()
1406         self._pad.Pop() # Move the first pad on top
1407 
1408 class FrameTGraph2D:
1409     """Class for creating and managing a frame for a plot from TGraph2D"""
1410     def __init__(self, pad, bounds, histos, ratioOrig, ratioFactor):
1411         self._pad = pad
1412         if ratioOrig:
1413             self._pad = pad.cd(1)
1414 
1415             # adjust margins because of not having the ratio, we want
1416             # the same bottom margin, so some algebra gives this
1417             (xlow, ylow, width, height) = (self._pad.GetXlowNDC(), self._pad.GetYlowNDC(), self._pad.GetWNDC(), self._pad.GetHNDC())
1418             xup = xlow+width
1419             yup = ylow+height
1420 
1421             bottomMargin = self._pad.GetBottomMargin()
1422             bottomMarginNew = ROOT.gStyle.GetPadBottomMargin()
1423 
1424             ylowNew = yup - (1-bottomMargin)/(1-bottomMarginNew) * (yup-ylow)
1425             topMarginNew = self._pad.GetTopMargin() * (yup-ylow)/(yup-ylowNew)
1426 
1427             self._pad.SetPad(xlow, ylowNew, xup, yup)
1428             self._pad.SetTopMargin(topMarginNew)
1429             self._pad.SetBottomMargin(bottomMarginNew)
1430 
1431         self._xtitleoffset = 1.8
1432         self._ytitleoffset = 2.3
1433 
1434         self._firstHisto = histos[0]
1435 
1436     def setLogx(self, log):
1437         pass
1438 
1439     def setLogy(self, log):
1440         pass
1441 
1442     def setGridx(self, grid):
1443         pass
1444 
1445     def setGridy(self, grid):
1446         pass
1447 
1448     def adjustMarginLeft(self, adjust):
1449         self._pad.SetLeftMargin(self._pad.GetLeftMargin()+adjust)
1450         self._pad.cd()
1451 
1452     def adjustMarginRight(self, adjust):
1453         self._pad.SetRightMargin(self._pad.GetRightMargin()+adjust)
1454         self._pad.cd()
1455 
1456     def setTitle(self, title):
1457         pass
1458 
1459     def setXTitle(self, title):
1460         self._xtitle = title
1461 
1462     def setXTitleSize(self, size):
1463         self._xtitlesize = size
1464 
1465     def setXTitleOffset(self, size):
1466         self._xtitleoffset = size
1467 
1468     def setXLabelSize(self, size):
1469         self._xlabelsize = size
1470 
1471     def setYTitle(self, title):
1472         self._ytitle = title
1473 
1474     def setYTitleSize(self, size):
1475         self._ytitlesize = size
1476 
1477     def setYTitleOffset(self, offset):
1478         self._ytitleoffset = offset
1479 
1480     def setZTitle(self, title):
1481         self._firstHisto.GetZaxis().SetTitle(title)
1482 
1483     def setZTitleOffset(self, offset):
1484         self._firstHisto.GetZaxis().SetTitleOffset(offset)
1485 
1486     def redrawAxis(self):
1487         # set top view
1488         epsilon = 1e-7
1489         self._pad.SetPhi(epsilon)
1490         self._pad.SetTheta(90+epsilon)
1491 
1492         self._firstHisto.GetXaxis().SetTitleOffset(self._xtitleoffset)
1493         self._firstHisto.GetYaxis().SetTitleOffset(self._ytitleoffset)
1494 
1495         if hasattr(self, "_xtitle"):
1496             self._firstHisto.GetXaxis().SetTitle(self._xtitle)
1497         if hasattr(self, "_xtitlesize"):
1498             self._firstHisto.GetXaxis().SetTitleSize(self._xtitlesize)
1499         if hasattr(self, "_xlabelsize"):
1500             self._firstHisto.GetXaxis().SetLabelSize(self._labelsize)
1501         if hasattr(self, "_ytitle"):
1502             self._firstHisto.GetYaxis().SetTitle(self._ytitle)
1503         if hasattr(self, "_ytitlesize"):
1504             self._firstHisto.GetYaxis().SetTitleSize(self._ytitlesize)
1505         if hasattr(self, "_ytitleoffset"):
1506             self._firstHisto.GetYaxis().SetTitleOffset(self._ytitleoffset)
1507 
1508 class PlotText:
1509     """Abstraction on top of TLatex"""
1510     def __init__(self, x, y, text, size=None, bold=True, align="left", color=ROOT.kBlack, font=None):
1511         """Constructor.
1512 
1513         Arguments:
1514         x     -- X coordinate of the text (in NDC)
1515         y     -- Y coordinate of the text (in NDC)
1516         text  -- String to draw
1517         size  -- Size of text (None for the default value, taken from gStyle)
1518         bold  -- Should the text be bold?
1519         align -- Alignment of text (left, center, right)
1520         color -- Color of the text
1521         font  -- Specify font explicitly
1522         """
1523         self._x = x
1524         self._y = y
1525         self._text = text
1526 
1527         self._l = ROOT.TLatex()
1528         self._l.SetNDC()
1529         if not bold:
1530             self._l.SetTextFont(self._l.GetTextFont()-20) # bold -> normal
1531         if font is not None:
1532             self._l.SetTextFont(font)
1533         if size is not None:
1534             self._l.SetTextSize(size)
1535         if isinstance(align, str):
1536             if align.lower() == "left":
1537                 self._l.SetTextAlign(11)
1538             elif align.lower() == "center":
1539                 self._l.SetTextAlign(21)
1540             elif align.lower() == "right":
1541                 self._l.SetTextAlign(31)
1542             else:
1543                 raise Exception("Error: Invalid option '%s' for text alignment! Options are: 'left', 'center', 'right'."%align)
1544         else:
1545             self._l.SetTextAlign(align)
1546         self._l.SetTextColor(color)
1547 
1548     def Draw(self, options=None):
1549         """Draw the text to the current TPad.
1550 
1551         Arguments:
1552         options -- For interface compatibility, ignored
1553 
1554         Provides interface compatible with ROOT's drawable objects.
1555         """
1556         self._l.DrawLatex(self._x, self._y, self._text)
1557 
1558 
1559 class PlotTextBox:
1560     """Class for drawing text and a background box."""
1561     def __init__(self, xmin, ymin, xmax, ymax, lineheight=0.04, fillColor=ROOT.kWhite, transparent=True, **kwargs):
1562         """Constructor
1563 
1564         Arguments:
1565         xmin        -- X min coordinate of the box (NDC)
1566         ymin        -- Y min coordinate of the box (NDC) (if None, deduced automatically)
1567         xmax        -- X max coordinate of the box (NDC)
1568         ymax        -- Y max coordinate of the box (NDC)
1569         lineheight  -- Line height
1570         fillColor   -- Fill color of the box
1571         transparent -- Should the box be transparent? (in practive the TPave is not created)
1572 
1573         Keyword arguments are forwarded to constructor of PlotText
1574         """
1575         # ROOT.TPave Set/GetX1NDC() etc don't seem to work as expected.
1576         self._xmin = xmin
1577         self._xmax = xmax
1578         self._ymin = ymin
1579         self._ymax = ymax
1580         self._lineheight = lineheight
1581         self._fillColor = fillColor
1582         self._transparent = transparent
1583         self._texts = []
1584         self._textArgs = {}
1585         self._textArgs.update(kwargs)
1586 
1587         self._currenty = ymax
1588 
1589     def addText(self, text):
1590         """Add text to current position"""
1591         self._currenty -= self._lineheight
1592         self._texts.append(PlotText(self._xmin+0.01, self._currenty, text, **self._textArgs))
1593 
1594     def width(self):
1595         return self._xmax-self._xmin
1596 
1597     def move(self, dx=0, dy=0, dw=0, dh=0):
1598         """Move the box and the contained text objects
1599 
1600         Arguments:
1601         dx -- Movement in x (positive is to right)
1602         dy -- Movement in y (positive is to up)
1603         dw -- Increment of width (negative to decrease width)
1604         dh -- Increment of height (negative to decrease height)
1605 
1606         dx and dy affect to both box and text objects, dw and dh
1607         affect the box only.
1608         """
1609         self._xmin += dx
1610         self._xmax += dx
1611         if self._ymin is not None:
1612             self._ymin += dy
1613         self._ymax += dy
1614 
1615         self._xmax += dw
1616         if self._ymin is not None:
1617             self._ymin -= dh
1618 
1619         for t in self._texts:
1620             t._x += dx
1621             t._y += dy
1622 
1623     def Draw(self, options=""):
1624         """Draw the box and the text to the current TPad.
1625 
1626         Arguments:
1627         options -- Forwarded to ROOT.TPave.Draw(), and the Draw() of the contained objects
1628         """
1629         if not self._transparent:
1630             ymin = self.ymin
1631             if ymin is None:
1632                 ymin = self.currenty - 0.01
1633             self._pave = ROOT.TPave(self.xmin, self.ymin, self.xmax, self.ymax, 0, "NDC")
1634             self._pave.SetFillColor(self.fillColor)
1635             self._pave.Draw(options)
1636         for t in self._texts:
1637             t.Draw(options)
1638 
1639 def _copyStyle(src, dst):
1640     properties = []
1641     if hasattr(src, "GetLineColor") and hasattr(dst, "SetLineColor"):
1642         properties.extend(["LineColor", "LineStyle", "LineWidth"])
1643     if hasattr(src, "GetFillColor") and hasattr(dst, "SetFillColor"):
1644         properties.extend(["FillColor", "FillStyle"])
1645     if hasattr(src, "GetMarkerColor") and hasattr(dst, "SetMarkerColor"):
1646         properties.extend(["MarkerColor", "MarkerSize", "MarkerStyle"])
1647 
1648     for prop in properties:
1649         getattr(dst, "Set"+prop)(getattr(src, "Get"+prop)())
1650 
1651 class PlotEmpty:
1652     """Denotes an empty place in a group."""
1653     def __init__(self):
1654         pass
1655 
1656     def getName(self):
1657         return None
1658 
1659     def drawRatioUncertainty(self):
1660         return False
1661 
1662     def create(self, *args, **kwargs):
1663         pass
1664 
1665     def isEmpty(self):
1666         return True
1667 
1668     def getNumberOfHistograms(self):
1669         return 0
1670 
1671 class Plot:
1672     """Represents one plot, comparing one or more histograms."""
1673     def __init__(self, name, **kwargs):
1674         """ Constructor.
1675 
1676         Arguments:
1677         name -- String for name of the plot, or Efficiency object
1678 
1679         Keyword arguments:
1680         fallback     -- Dictionary for specifying fallback (default None)
1681         outname      -- String for an output name of the plot (default None for the same as 'name')
1682         title        -- String for a title of the plot (default None)
1683         xtitle       -- String for x axis title (default None)
1684         xtitlesize   -- Float for x axis title size (default None)
1685         xtitleoffset -- Float for x axis title offset (default None)
1686         xlabelsize   -- Float for x axis label size (default None)
1687         ytitle       -- String for y axis title (default None)
1688         ytitlesize   -- Float for y axis title size (default None)
1689         ytitleoffset -- Float for y axis title offset (default None)
1690         ztitle       -- String for z axis title (default None)
1691         ztitleoffset -- Float for z axis title offset (default None)
1692         xmin         -- Float for x axis minimum (default None, i.e. automatic)
1693         xmax         -- Float for x axis maximum (default None, i.e. automatic)
1694         ymin         -- Float for y axis minimum (default 0)
1695         ymax         -- Float for y axis maximum (default None, i.e. automatic)
1696         xlog         -- Bool for x axis log status (default False)
1697         ylog         -- Bool for y axis log status (default False)
1698         xgrid        -- Bool for x axis grid status (default True)
1699         ygrid        -- Bool for y axis grid status (default True)
1700         stat         -- Draw stat box? (default False)
1701         fit          -- Do gaussian fit? (default False)
1702         statx        -- Stat box x coordinate (default 0.65)
1703         staty        -- Stat box y coordinate (default 0.8)
1704         statyadjust  -- List of floats for stat box y coordinate adjustments (default None)
1705         normalizeToUnitArea -- Normalize histograms to unit area? (default False)
1706         normalizeToNumberOfEvents -- Normalize histograms to number of events? If yes, the PlotFolder needs 'numberOfEventsHistogram' set to a histogram filled once per event (default False)
1707         profileX     -- Take histograms via ProfileX()? (default False)
1708         fitSlicesY   -- Take histograms via FitSlicesY() (default False)
1709         rebinX       -- rebin x axis (default None)
1710         scale        -- Scale histograms by a number (default None)
1711         xbinlabels   -- List of x axis bin labels (if given, default None)
1712         xbinlabelsize -- Size of x axis bin labels (default None)
1713         xbinlabeloption -- Option string for x axis bin labels (default None)
1714         removeEmptyBins -- Bool for removing empty bins, but only if histogram has bin labels (default False)
1715         printBins    -- Bool for printing bin values, but only if histogram has bin labels (default False)
1716         drawStyle    -- If "hist", draw as line instead of points (default None)
1717         drawCommand  -- Deliver this to Draw() (default: None for same as drawStyle)
1718         lineWidth    -- If drawStyle=="hist", the width of line (default 2)
1719         legendDx     -- Float for moving TLegend in x direction for separate=True (default None)
1720         legendDy     -- Float for moving TLegend in y direction for separate=True (default None)
1721         legendDw     -- Float for changing TLegend width for separate=True (default None)
1722         legendDh     -- Float for changing TLegend height for separate=True (default None)
1723         legend       -- Bool to enable/disable legend (default True)
1724         adjustMarginLeft  -- Float for adjusting left margin (default None)
1725         adjustMarginRight  -- Float for adjusting right margin (default None)
1726         ratio        -- Possibility to disable ratio for this particular plot (default None)
1727         ratioYmin    -- Float for y axis minimum in ratio pad (default: list of values)
1728         ratioYmax    -- Float for y axis maximum in ratio pad (default: list of values)
1729         ratioFit     -- Fit straight line in ratio? (default None)
1730         ratioUncertainty -- Plot uncertainties on ratio? (default True)
1731         ratioCoverageXrange -- Range of x axis values (xmin,xmax) to limit the automatic ratio y axis range calculation to (default None for disabled)
1732         histogramModifier -- Function to be called in create() to modify the histograms (default None)
1733         """
1734         self._name = name
1735 
1736         def _set(attr, default):
1737             setattr(self, "_"+attr, kwargs.get(attr, default))
1738 
1739         _set("fallback", None)
1740         _set("outname", None)
1741 
1742         _set("title", None)
1743         _set("xtitle", None)
1744         _set("xtitlesize", None)
1745         _set("xtitleoffset", None)
1746         _set("xlabelsize", None)
1747         _set("ytitle", None)
1748         _set("ytitlesize", None)
1749         _set("ytitleoffset", None)
1750         _set("ztitle", None)
1751         _set("ztitleoffset", None)
1752 
1753         _set("xmin", None)
1754         _set("xmax", None)
1755         _set("ymin", 0.)
1756         _set("ymax", None)
1757 
1758         _set("xlog", False)
1759         _set("ylog", False)
1760         _set("xgrid", True)
1761         _set("ygrid", True)
1762 
1763         _set("stat", False)
1764         _set("fit", False)
1765 
1766         _set("statx", 0.65)
1767         _set("staty", 0.8)
1768         _set("statyadjust", None)
1769 
1770         _set("normalizeToUnitArea", False)
1771         _set("normalizeToNumberOfEvents", False)
1772         _set("profileX", False)
1773         _set("fitSlicesY", False)
1774         _set("rebinX", None)
1775 
1776         _set("scale", None)
1777         _set("xbinlabels", None)
1778         _set("xbinlabelsize", None)
1779         _set("xbinlabeloption", None)
1780         _set("removeEmptyBins", False)
1781         _set("printBins", False)
1782 
1783         _set("drawStyle", "EP")
1784         _set("drawCommand", None)
1785         _set("lineWidth", 2)
1786 
1787         _set("legendDx", None)
1788         _set("legendDy", None)
1789         _set("legendDw", None)
1790         _set("legendDh", None)
1791         _set("legend", True)
1792 
1793         _set("adjustMarginLeft", None)
1794         _set("adjustMarginRight", None)
1795 
1796         _set("ratio", None)
1797         _set("ratioYmin", [0, 0.2, 0.5, 0.7, 0.8, 0.9, 0.95])
1798         _set("ratioYmax", [1.05, 1.1, 1.2, 1.3, 1.5, 1.8, 2, 2.5, 3, 4, 5])
1799         _set("ratioFit", None)
1800         _set("ratioUncertainty", True)
1801         _set("ratioCoverageXrange", None)
1802 
1803         _set("histogramModifier", None)
1804 
1805         self._histograms = []
1806 
1807     def setProperties(self, **kwargs):
1808         for name, value in kwargs.items():
1809             if not hasattr(self, "_"+name):
1810                 raise Exception("No attribute '%s'" % name)
1811             setattr(self, "_"+name, value)
1812 
1813     def clone(self, **kwargs):
1814         if not self.isEmpty():
1815             raise Exception("Plot can be cloned only before histograms have been created")
1816         cl = copy.copy(self)
1817         cl.setProperties(**kwargs)
1818         return cl
1819 
1820     def getNumberOfHistograms(self):
1821         """Return number of existing histograms."""
1822         return len([h for h in self._histograms if h is not None])
1823 
1824     def isEmpty(self):
1825         """Return true if there are no histograms created for the plot"""
1826         return self.getNumberOfHistograms() == 0
1827 
1828     def isTGraph2D(self):
1829         for h in self._histograms:
1830             if isinstance(h, ROOT.TGraph2D):
1831                 return True
1832         return False
1833 
1834     def isRatio(self, ratio):
1835         if self._ratio is None:
1836             return ratio
1837         return ratio and self._ratio
1838 
1839     def setName(self, name):
1840         self._name = name
1841 
1842     def getName(self):
1843         if self._outname is not None:
1844             return self._outname
1845         if isinstance(self._name, list):
1846             return str(self._name[0])
1847         else:
1848             return str(self._name)
1849 
1850     def drawRatioUncertainty(self):
1851         """Return true if the ratio uncertainty should be drawn"""
1852         return self._ratioUncertainty
1853 
1854     def _createOne(self, name, index, tdir, nevents):
1855         """Create one histogram from a TDirectory."""
1856         if tdir == None:
1857             return None
1858 
1859         # If name is a list, pick the name by the index
1860         if isinstance(name, list):
1861             name = name[index]
1862 
1863         h = _getOrCreateObject(tdir, name)
1864         if h is not None and self._normalizeToNumberOfEvents and nevents is not None and nevents != 0:
1865             h.Scale(1.0/nevents)
1866         return h
1867 
1868     def create(self, tdirNEvents, requireAllHistograms=False):
1869         """Create histograms from list of TDirectories"""
1870         self._histograms = [self._createOne(self._name, i, tdirNEvent[0], tdirNEvent[1]) for i, tdirNEvent in enumerate(tdirNEvents)]
1871 
1872         if self._fallback is not None:
1873             profileX = [self._profileX]*len(self._histograms)
1874             for i in range(0, len(self._histograms)):
1875                 if self._histograms[i] is None:
1876                     self._histograms[i] = self._createOne(self._fallback["name"], i, tdirNEvents[i][0], tdirNEvents[i][1])
1877                     profileX[i] = self._fallback.get("profileX", self._profileX)
1878 
1879         if self._histogramModifier is not None:
1880             self._histograms = self._histogramModifier(self._histograms)
1881 
1882         if len(self._histograms) > len(_plotStylesColor):
1883             raise Exception("More histograms (%d) than there are plot styles (%d) defined. Please define more plot styles in this file" % (len(self._histograms), len(_plotStylesColor)))
1884 
1885         # Modify histograms here in case self._name returns numbers
1886         # and self._histogramModifier creates the histograms from
1887         # these numbers
1888         def _modifyHisto(th1, profileX):
1889             if th1 is None:
1890                 return None
1891 
1892             if profileX:
1893                 th1 = th1.ProfileX()
1894 
1895             if self._fitSlicesY:
1896                 ROOT.TH1.AddDirectory(True)
1897                 th1.FitSlicesY()
1898                 th1 = ROOT.gDirectory.Get(th1.GetName()+"_2")
1899                 th1.SetDirectory(None)
1900                 #th1.SetName(th1.GetName()+"_ref")
1901                 ROOT.TH1.AddDirectory(False)
1902 
1903             if self._title is not None:
1904                 th1.SetTitle(self._title)
1905 
1906             if self._scale is not None:
1907                 th1.Scale(self._scale)
1908 
1909             return th1
1910 
1911         if self._fallback is not None:
1912             self._histograms = list(map(_modifyHisto, self._histograms, profileX))
1913         else:
1914             self._histograms = list(map(lambda h: _modifyHisto(h, self._profileX), self._histograms))
1915         if requireAllHistograms and None in self._histograms:
1916             self._histograms = [None]*len(self._histograms)
1917 
1918     def _setStats(self, histos, startingX, startingY):
1919         """Set stats box."""
1920         if not self._stat:
1921             for h in histos:
1922                 if h is not None and hasattr(h, "SetStats"):
1923                     h.SetStats(0)
1924             return
1925 
1926         def _doStats(h, col, dy):
1927             if h is None:
1928                 return
1929             h.SetStats(True)
1930 
1931             if self._fit and h.GetEntries() > 0.5:
1932                 h.Fit("gaus", "Q")
1933                 f = h.GetListOfFunctions().FindObject("gaus")
1934                 if f == None:
1935                     h.SetStats(0)
1936                     return
1937                 f.SetLineColor(col)
1938                 f.SetLineWidth(1)
1939             h.Draw()
1940             ROOT.gPad.Update()
1941             st = h.GetListOfFunctions().FindObject("stats")
1942             if self._fit:
1943                 st.SetOptFit(0o010)
1944                 st.SetOptStat(1001)
1945             st.SetOptStat(1110)
1946             st.SetX1NDC(startingX)
1947             st.SetX2NDC(startingX+0.3)
1948             st.SetY1NDC(startingY+dy)
1949             st.SetY2NDC(startingY+dy+0.12)
1950             st.SetTextColor(col)
1951 
1952         dy = 0.0
1953         for i, h in enumerate(histos):
1954             if self._statyadjust is not None and i < len(self._statyadjust):
1955                 dy += self._statyadjust[i]
1956 
1957             _doStats(h, _plotStylesColor[i], dy)
1958             dy -= 0.16
1959 
1960     def _normalize(self):
1961         """Normalise histograms to unit area"""
1962 
1963         for h in self._histograms:
1964             if h is None:
1965                 continue
1966             i = h.Integral()
1967             if i == 0:
1968                 continue
1969             if h.GetSumw2().fN <= 0: # to suppress warning
1970                 h.Sumw2()
1971             h.Scale(1.0/i)
1972 
1973     def draw(self, pad, ratio, ratioFactor, nrows):
1974         """Draw the histograms using values for a given algorithm."""
1975 #        if len(self._histograms) == 0:
1976 #            print "No histograms for plot {name}".format(name=self._name)
1977 #            return
1978 
1979         isTGraph2D = self.isTGraph2D()
1980         if isTGraph2D:
1981             # Ratios for the TGraph2Ds is not that interesting
1982             ratioOrig = ratio
1983             ratio = False
1984 
1985         if self._normalizeToUnitArea:
1986             self._normalize()
1987 
1988         if self._rebinX is not None:
1989             for h in self._histograms:
1990                 h.Rebin(self._rebinX)
1991 
1992         def _styleMarker(h, msty, col):
1993             h.SetMarkerStyle(msty)
1994             h.SetMarkerColor(col)
1995             h.SetMarkerSize(0.7)
1996             h.SetLineColor(col)
1997             h.SetLineWidth(1)
1998 
1999         def _styleHist(h, msty, col):
2000             _styleMarker(h, msty, col)
2001             h.SetLineColor(col)
2002             h.SetLineWidth(self._lineWidth)
2003 
2004         # Use marker or hist style
2005         style = _styleMarker
2006         if "hist" in self._drawStyle.lower():
2007             style = _styleHist
2008         if len(self._histograms) > 0 and isinstance(self._histograms[0], ROOT.TGraph):
2009             if "l" in self._drawStyle.lower():
2010                 style = _styleHist
2011 
2012         # Apply style to histograms, filter out Nones
2013         histos = []
2014         for i, h in enumerate(self._histograms):
2015             if h is None:
2016                 continue
2017             style(h, _plotStylesMarker[i], _plotStylesColor[i])
2018             histos.append(h)
2019         if len(histos) == 0:
2020             if verbose:
2021                 print("No histograms for plot {name}".format(name=self.getName()))
2022             return
2023 
2024         # Extract x bin labels, make sure that only bins with same
2025         # label are compared with each other
2026         histosHaveBinLabels = len(histos[0].GetXaxis().GetBinLabel(1)) > 0
2027         xbinlabels = self._xbinlabels
2028         ybinlabels = None
2029         if xbinlabels is None:
2030             if histosHaveBinLabels:
2031                 xbinlabels = _mergeBinLabelsX(histos)
2032                 if isinstance(histos[0], ROOT.TH2):
2033                     ybinlabels = _mergeBinLabelsY(histos)
2034 
2035                 if len(histos) > 1: # don't bother if only one histogram
2036                     # doing this for TH2 is pending for use case, for now there is only 1 histogram/plot for TH2
2037                     histos = _th1IncludeOnlyBins(histos, xbinlabels)
2038                     self._tmp_histos = histos # need to keep these in memory too ...
2039 
2040         # Remove empty bins, but only if histograms have bin labels
2041         if self._removeEmptyBins and histosHaveBinLabels:
2042             # at this point, all histograms have been "equalized" by their x binning and labels
2043             # therefore remove bins which are empty in all histograms
2044             if isinstance(histos[0], ROOT.TH2):
2045                 (histos, xbinlabels, ybinlabels) = _th2RemoveEmptyBins(histos, xbinlabels, ybinlabels)
2046             else:
2047                 (histos, xbinlabels) = _th1RemoveEmptyBins(histos, xbinlabels)
2048             self._tmp_histos = histos # need to keep these in memory too ...
2049             if len(histos) == 0:
2050                 if verbose:
2051                     print("No histograms with non-empty bins for plot {name}".format(name=self.getName()))
2052                 return
2053 
2054         if self._printBins and histosHaveBinLabels:
2055             print("####################")
2056             print(self._name)
2057             width = max([len(l) for l in xbinlabels])
2058             tmp = "%%-%ds " % width
2059             for b in range(1, histos[0].GetNbinsX()+1):
2060                 s = tmp % xbinlabels[b-1]
2061                 for h in histos:
2062                     s += "%.3f " % h.GetBinContent(b)
2063                 print(s)
2064             print()
2065 
2066         bounds = _findBounds(histos, self._ylog,
2067                              xmin=self._xmin, xmax=self._xmax,
2068                              ymin=self._ymin, ymax=self._ymax)
2069         zmax = None
2070         if isinstance(histos[0], ROOT.TH2):
2071             zmax = max([h.GetMaximum() for h in histos])
2072 
2073         # need to keep these in memory
2074         self._mainAdditional = []
2075         self._ratioAdditional = []
2076 
2077         if ratio:
2078             self._ratios = _calculateRatios(histos, self._ratioUncertainty) # need to keep these in memory too ...
2079             ratioHistos = [h for h in [r.getRatio() for r in self._ratios[1:]] if h is not None]
2080 
2081             if len(ratioHistos) > 0:
2082                 ratioBoundsY = _findBoundsY(ratioHistos, ylog=False, ymin=self._ratioYmin, ymax=self._ratioYmax, coverage=0.68, coverageRange=self._ratioCoverageXrange)
2083             else:
2084                 ratioBoundsY = (0.9, 1,1) # hardcoded default in absence of valid ratio calculations
2085 
2086             if self._ratioFit is not None:
2087                 for i, rh in enumerate(ratioHistos):
2088                     tf_line = ROOT.TF1("line%d"%i, "[0]+x*[1]")
2089                     tf_line.SetRange(self._ratioFit["rangemin"], self._ratioFit["rangemax"])
2090                     fitres = rh.Fit(tf_line, "RINSQ")
2091                     tf_line.SetLineColor(rh.GetMarkerColor())
2092                     tf_line.SetLineWidth(2)
2093                     self._ratioAdditional.append(tf_line)
2094                     box = PlotTextBox(xmin=self._ratioFit.get("boxXmin", 0.14), ymin=None, # None for automatix
2095                                       xmax=self._ratioFit.get("boxXmax", 0.35), ymax=self._ratioFit.get("boxYmax", 0.09),
2096                                       color=rh.GetMarkerColor(), font=43, size=11, lineheight=0.02)
2097                     box.move(dx=(box.width()+0.01)*i)
2098                     #box.addText("Const: %.4f" % fitres.Parameter(0))
2099                     #box.addText("Slope: %.4f" % fitres.Parameter(1))
2100                     box.addText("Const: %.4f#pm%.4f" % (fitres.Parameter(0), fitres.ParError(0)))
2101                     box.addText("Slope: %.4f#pm%.4f" % (fitres.Parameter(1), fitres.ParError(1)))
2102                     self._mainAdditional.append(box)
2103 
2104 
2105         # Create bounds before stats in order to have the
2106         # SetRangeUser() calls made before the fit
2107         #
2108         # stats is better to be called before frame, otherwise get
2109         # mess in the plot (that frame creation cleans up)
2110         if ratio:
2111             pad.cd(1)
2112         self._setStats(histos, self._statx, self._staty)
2113 
2114         # Create frame
2115         if isTGraph2D:
2116             frame = FrameTGraph2D(pad, bounds, histos, ratioOrig, ratioFactor)
2117         else:
2118             if ratio:
2119                 ratioBounds = (bounds[0], ratioBoundsY[0], bounds[2], ratioBoundsY[1])
2120                 frame = FrameRatio(pad, bounds, zmax, ratioBounds, ratioFactor, nrows, xbinlabels, self._xbinlabelsize, self._xbinlabeloption)
2121             else:
2122                 frame = Frame(pad, bounds, zmax, nrows, xbinlabels, self._xbinlabelsize, self._xbinlabeloption, ybinlabels=ybinlabels)
2123 
2124         # Set log and grid
2125         frame.setLogx(self._xlog)
2126         frame.setLogy(self._ylog)
2127         frame.setGridx(self._xgrid)
2128         frame.setGridy(self._ygrid)
2129 
2130         # Construct draw option string
2131         opt = "sames" # s for statbox or something?
2132         ds = ""
2133         if self._drawStyle is not None:
2134             ds = self._drawStyle
2135         if self._drawCommand is not None:
2136             ds = self._drawCommand
2137         if len(ds) > 0:
2138             opt += " "+ds
2139 
2140         # Set properties of frame
2141         frame.setTitle(histos[0].GetTitle())
2142         if self._xtitle == 'Default':
2143             frame.setXTitle( histos[0].GetXaxis().GetTitle() )
2144         elif self._xtitle is not None:
2145             frame.setXTitle(self._xtitle)
2146         if self._xtitlesize is not None:
2147             frame.setXTitleSize(self._xtitlesize)
2148         if self._xtitleoffset is not None:
2149             frame.setXTitleOffset(self._xtitleoffset)
2150         if self._xlabelsize is not None:
2151             frame.setXLabelSize(self._xlabelsize)
2152         if self._ytitle == 'Default':
2153             frame.setYTitle( histos[0].GetYaxis().GetTitle() )
2154         elif self._ytitle is not None:
2155             frame.setYTitle(self._ytitle)
2156         if self._ytitlesize is not None:
2157             frame.setYTitleSize(self._ytitlesize)
2158         if self._ytitleoffset is not None:
2159             frame.setYTitleOffset(self._ytitleoffset)
2160         if self._ztitle is not None:
2161             frame.setZTitle(self._ztitle)
2162         if self._ztitleoffset is not None:
2163             frame.setZTitleOffset(self._ztitleoffset)
2164         if self._adjustMarginLeft is not None:
2165             frame.adjustMarginLeft(self._adjustMarginLeft)
2166         if self._adjustMarginRight is not None:
2167             frame.adjustMarginRight(self._adjustMarginRight)
2168         elif "z" in opt:
2169             frame.adjustMarginLeft(0.03)
2170             frame.adjustMarginRight(0.08)
2171 
2172         # Draw histograms
2173         if ratio:
2174             frame._pad.cd()
2175 
2176         for i, h in enumerate(histos):
2177             o = opt
2178             if isTGraph2D and i == 0:
2179                 o = o.replace("sames", "")
2180             h.Draw(o)
2181 
2182         for addl in self._mainAdditional:
2183             addl.Draw("same")
2184 
2185         # Draw ratios
2186         if ratio and len(self._ratios) > 0:
2187             frame._padRatio.cd()
2188             firstRatio = self._ratios[0].getRatio()
2189             if self._ratioUncertainty and firstRatio is not None:
2190                 firstRatio.SetFillStyle(1001)
2191                 firstRatio.SetFillColor(ROOT.kGray)
2192                 firstRatio.SetLineColor(ROOT.kGray)
2193                 firstRatio.SetMarkerColor(ROOT.kGray)
2194                 firstRatio.SetMarkerSize(0)
2195                 self._ratios[0].draw("E2")
2196                 frame._padRatio.RedrawAxis("G") # redraw grid on top of the uncertainty of denominator
2197             for r in self._ratios[1:]:
2198                 r.draw()
2199 
2200             for addl in self._ratioAdditional:
2201                 addl.Draw("same")
2202 
2203         frame.redrawAxis()
2204         self._frame = frame # keep the frame in memory for sure
2205 
2206     def addToLegend(self, legend, legendLabels, denomUncertainty):
2207         """Add histograms to a legend.
2208 
2209         Arguments:
2210         legend       -- TLegend
2211         legendLabels -- List of strings for the legend labels
2212         """
2213         first = denomUncertainty
2214         for h, label in zip(self._histograms, legendLabels):
2215             if h is None:
2216                 first = False
2217                 continue
2218             if first:
2219                 self._forLegend = h.Clone()
2220                 self._forLegend.SetFillStyle(1001)
2221                 self._forLegend.SetFillColor(ROOT.kGray)
2222                 entry = legend.AddEntry(self._forLegend, label, "lpf")
2223                 first = False
2224             else:
2225                 legend.AddEntry(h, label, "LP")
2226 
2227 class PlotGroup(object):
2228     """Group of plots, results a TCanvas"""
2229     def __init__(self, name, plots, **kwargs):
2230         """Constructor.
2231 
2232         Arguments:
2233         name  -- String for name of the TCanvas, used also as the basename of the picture files
2234         plots -- List of Plot objects
2235 
2236         Keyword arguments:
2237         ncols    -- Number of columns (default 2)
2238         legendDx -- Float for moving TLegend in x direction (default None)
2239         legendDy -- Float for moving TLegend in y direction (default None)
2240         legendDw -- Float for changing TLegend width (default None)
2241         legendDh -- Float for changing TLegend height (default None)
2242         legend   -- Bool for disabling legend (default True for legend being enabled)
2243         overrideLegendLabels -- List of strings for legend labels, if given, these are used instead of the ones coming from Plotter (default None)
2244         onlyForPileup  -- Plots this group only for pileup samples
2245         """
2246         super(PlotGroup, self).__init__()
2247 
2248         self._name = name
2249         self._plots = plots
2250 
2251         def _set(attr, default):
2252             setattr(self, "_"+attr, kwargs.get(attr, default))
2253 
2254         _set("ncols", 2)
2255 
2256         _set("legendDx", None)
2257         _set("legendDy", None)
2258         _set("legendDw", None)
2259         _set("legendDh", None)
2260         _set("legend", True)
2261 
2262         _set("overrideLegendLabels", None)
2263 
2264         _set("onlyForPileup", False)
2265 
2266         self._ratioFactor = 1.25
2267 
2268     def setProperties(self, **kwargs):
2269         for name, value in kwargs.items():
2270             if not hasattr(self, "_"+name):
2271                 raise Exception("No attribute '%s'" % name)
2272             setattr(self, "_"+name, value)
2273 
2274     def getName(self):
2275         return self._name
2276 
2277     def getPlots(self):
2278         return self._plots
2279 
2280     def remove(self, name):
2281         for i, plot in enumerate(self._plots):
2282             if plot.getName() == name:
2283                 del self._plots[i]
2284                 return
2285         raise Exception("Did not find Plot '%s' from PlotGroup '%s'" % (name, self._name))
2286 
2287     def clear(self):
2288         self._plots = []
2289 
2290     def append(self, plot):
2291         self._plots.append(plot)
2292 
2293     def getPlot(self, name):
2294         for plot in self._plots:
2295             if plot.getName() == name:
2296                 return plot
2297         raise Exception("No Plot named '%s'" % name)
2298 
2299     def onlyForPileup(self):
2300         """Return True if the PlotGroup is intended only for pileup samples"""
2301         return self._onlyForPileup
2302 
2303     def create(self, tdirectoryNEvents, requireAllHistograms=False):
2304         """Create histograms from a list of TDirectories.
2305 
2306         Arguments:
2307         tdirectoryNEvents    -- List of (TDirectory, nevents) pairs
2308         requireAllHistograms -- If True, a plot is produced if histograms from all files are present (default: False)
2309         """
2310         for plot in self._plots:
2311             plot.create(tdirectoryNEvents, requireAllHistograms)
2312 
2313     def draw(self, legendLabels, prefix=None, separate=False, saveFormat=".pdf", ratio=True, directory=""):
2314         """Draw the histograms using values for a given algorithm.
2315 
2316         Arguments:
2317         legendLabels  -- List of strings for legend labels (corresponding to the tdirectories in create())
2318         prefix        -- Optional string for file name prefix (default None)
2319         separate      -- Save the plots of a group to separate files instead of a file per group (default False)
2320         saveFormat   -- String specifying the plot format (default '.pdf')
2321         ratio        -- Add ratio to the plot (default True)
2322         directory     -- Directory where to save the file (default "")
2323         """
2324 
2325         if self._overrideLegendLabels is not None:
2326             legendLabels = self._overrideLegendLabels
2327 
2328         # Do not draw the group if it would be empty
2329         onlyEmptyPlots = True
2330         for plot in self._plots:
2331             if not plot.isEmpty():
2332                 onlyEmptyPlots = False
2333                 break
2334         if onlyEmptyPlots:
2335             return []
2336 
2337         if separate:
2338             return self._drawSeparate(legendLabels, prefix, saveFormat, ratio, directory)
2339 
2340         cwidth = 500*self._ncols
2341         nrows = int((len(self._plots)+self._ncols-1)/self._ncols) # this should work also for odd n
2342         cheight = 500 * nrows
2343 
2344         if ratio:
2345             cheight = int(cheight*self._ratioFactor)
2346 
2347         canvas = _createCanvas(self._name, cwidth, cheight)
2348 
2349         canvas.Divide(self._ncols, nrows)
2350         if ratio:
2351             for i, plot in enumerate(self._plots):
2352                 pad = canvas.cd(i+1)
2353                 self._modifyPadForRatio(pad)
2354 
2355         # Draw plots to canvas
2356         for i, plot in enumerate(self._plots):
2357             pad = canvas.cd(i+1)
2358             if not plot.isEmpty():
2359                 plot.draw(pad, ratio, self._ratioFactor, nrows)
2360 
2361         if plot._legend:
2362           # Setup legend
2363           canvas.cd()
2364           if len(self._plots) <= 4:
2365               lx1 = 0.2
2366               lx2 = 0.9
2367               ly1 = 0.48
2368               ly2 = 0.53
2369           else:
2370               lx1 = 0.1
2371               lx2 = 0.9
2372               ly1 = 0.64
2373               ly2 = 0.67
2374           if self._legendDx is not None:
2375               lx1 += self._legendDx
2376               lx2 += self._legendDx
2377           if self._legendDy is not None:
2378               ly1 += self._legendDy
2379               ly2 += self._legendDy
2380           if self._legendDw is not None:
2381               lx2 += self._legendDw
2382           if self._legendDh is not None:
2383               ly1 -= self._legendDh
2384           plot = max(self._plots, key=lambda p: p.getNumberOfHistograms())
2385           denomUnc = sum([p.drawRatioUncertainty() for p in self._plots]) > 0
2386           legend = self._createLegend(plot, legendLabels, lx1, ly1, lx2, ly2,
2387                                       denomUncertainty=(ratio and denomUnc))
2388 
2389         return self._save(canvas, saveFormat, prefix=prefix, directory=directory)
2390 
2391     def _drawSeparate(self, legendLabels, prefix, saveFormat, ratio, directory):
2392         """Internal method to do the drawing to separate files per Plot instead of a file per PlotGroup"""
2393         width = 500
2394         height = 500
2395 
2396         lx1def = 0.6
2397         lx2def = 0.95
2398         ly1def = 0.85
2399         ly2def = 0.95
2400 
2401         ret = []
2402 
2403         for plot in self._plots:
2404             if plot.isEmpty():
2405                 continue
2406 
2407             canvas = _createCanvas(self._name+"Single", width, height)
2408             canvasRatio = _createCanvas(self._name+"SingleRatio", width, int(height*self._ratioFactor))
2409 
2410             # from TDRStyle
2411             for c in [canvas, canvasRatio]:
2412                 c.SetTopMargin(0.05)
2413                 c.SetBottomMargin(0.13)
2414                 c.SetLeftMargin(0.16)
2415                 c.SetRightMargin(0.05)
2416 
2417             ratioForThisPlot = plot.isRatio(ratio)
2418             c = canvas
2419             if ratioForThisPlot:
2420                 c = canvasRatio
2421                 c.cd()
2422                 self._modifyPadForRatio(c)
2423 
2424             # Draw plot to canvas
2425             c.cd()
2426             plot.draw(c, ratioForThisPlot, self._ratioFactor, 1)
2427 
2428             if plot._legend:
2429                 # Setup legend
2430                 lx1 = lx1def
2431                 lx2 = lx2def
2432                 ly1 = ly1def
2433                 ly2 = ly2def
2434 
2435                 if plot._legendDx is not None:
2436                     lx1 += plot._legendDx
2437                     lx2 += plot._legendDx
2438                 if plot._legendDy is not None:
2439                     ly1 += plot._legendDy
2440                     ly2 += plot._legendDy
2441                 if plot._legendDw is not None:
2442                     lx2 += plot._legendDw
2443                 if plot._legendDh is not None:
2444                     ly1 -= plot._legendDh
2445 
2446                 c.cd()
2447                 legend = self._createLegend(plot, legendLabels, lx1, ly1, lx2, ly2, textSize=0.03,
2448                                             denomUncertainty=(ratioForThisPlot and plot.drawRatioUncertainty))
2449 
2450             ret.extend(self._save(c, saveFormat, prefix=prefix, postfix="/"+plot.getName(), single=True, directory=directory))
2451         return ret
2452 
2453     def _modifyPadForRatio(self, pad):
2454         """Internal method to set divide a pad to two for ratio plots"""
2455         _modifyPadForRatio(pad, self._ratioFactor)
2456 
2457     def _createLegend(self, plot, legendLabels, lx1, ly1, lx2, ly2, textSize=0.016, denomUncertainty=True):
2458         if not self._legend:
2459             return None
2460 
2461         l = ROOT.TLegend(lx1, ly1, lx2, ly2)
2462         l.SetTextSize(textSize)
2463         l.SetLineColor(1)
2464         l.SetLineWidth(1)
2465         l.SetLineStyle(1)
2466         l.SetFillColor(0)
2467         l.SetMargin(0.07)
2468 
2469         plot.addToLegend(l, legendLabels, denomUncertainty)
2470         l.Draw()
2471         return l
2472 
2473     def _save(self, canvas, saveFormat, prefix=None, postfix=None, single=False, directory=""):
2474         # Save the canvas to file and clear
2475         name = self._name
2476         if not os.path.exists(directory+'/'+name):
2477             os.makedirs(directory+'/'+name, exist_ok=True)
2478         if prefix is not None:
2479             name = prefix+name
2480         if postfix is not None:
2481             name = name+postfix
2482         name = os.path.join(directory, name)
2483 
2484         if not verbose: # silence saved file printout
2485             backup = ROOT.gErrorIgnoreLevel
2486             ROOT.gErrorIgnoreLevel = ROOT.kWarning
2487         canvas.SaveAs(name+saveFormat)
2488         if not verbose:
2489             ROOT.gErrorIgnoreLevel = backup
2490 
2491         if single:
2492             canvas.Clear()
2493             canvas.SetLogx(False)
2494             canvas.SetLogy(False)
2495         else:
2496             canvas.Clear("D") # keep subpads
2497 
2498         return [name+saveFormat]
2499 
2500 class PlotOnSideGroup(PlotGroup):
2501     """Resembles DQM GUI's "On side" layout.
2502 
2503     Like PlotGroup, but has only a description of a single plot. The
2504     plot is drawn separately for each file. Useful for 2D histograms."""
2505 
2506     def __init__(self, name, plot, ncols=2, onlyForPileup=False):
2507         super(PlotOnSideGroup, self).__init__(name, [], ncols=ncols, legend=False, onlyForPileup=onlyForPileup)
2508         self._plot = plot
2509         self._plot.setProperties(ratio=False)
2510 
2511     def append(self, *args, **kwargs):
2512         raise Exception("PlotOnSideGroup.append() is not implemented")
2513 
2514     def create(self, tdirectoryNEvents, requireAllHistograms=False):
2515         self._plots = []
2516         for i, element in enumerate(tdirectoryNEvents):
2517             pl = self._plot.clone()
2518             pl.create([element], requireAllHistograms)
2519             pl.setName(pl.getName()+"_"+str(i))
2520             self._plots.append(pl)
2521 
2522     def draw(self, *args, **kwargs):
2523         kargs = copy.copy(kwargs)
2524         kargs["ratio"] = False
2525         return super(PlotOnSideGroup, self).draw(*args, **kargs)
2526 
2527 class PlotFolder:
2528 
2529     """Represents a collection of PlotGroups, produced from a single folder in a DQM file"""
2530     def __init__(self, *plotGroups, **kwargs):
2531         """Constructor.
2532 
2533         Arguments:
2534         plotGroups     -- List of PlotGroup objects
2535 
2536         Keyword arguments
2537         loopSubFolders -- Should the subfolders be looped over? (default: True)
2538         onlyForPileup  -- Plots this folder only for pileup samples
2539         onlyForElectron -- Plots this folder only for electron samples
2540         onlyForConversion -- Plots this folder only for conversion samples
2541         onlyForBHadron -- Plots this folder only for B-hadron samples
2542         purpose        -- html.PlotPurpose member class for the purpose of the folder, used for grouping of the plots to the HTML pages
2543         page           -- Optional string for the page in HTML generatin
2544         section        -- Optional string for the section within a page in HTML generation
2545         numberOfEventsHistogram -- Optional path to histogram filled once per event. Needed if there are any plots normalized by number of events. Path is relative to "possibleDqmFolders".
2546         """
2547         self._plotGroups = list(plotGroups)
2548         self._loopSubFolders = kwargs.pop("loopSubFolders", True)
2549         self._onlyForPileup = kwargs.pop("onlyForPileup", False)
2550         self._onlyForElectron = kwargs.pop("onlyForElectron", False)
2551         self._onlyForConversion = kwargs.pop("onlyForConversion", False)
2552         self._onlyForBHadron = kwargs.pop("onlyForBHadron", False)
2553         self._purpose = kwargs.pop("purpose", None)
2554         self._page = kwargs.pop("page", None)
2555         self._section = kwargs.pop("section", None)
2556         self._numberOfEventsHistogram = kwargs.pop("numberOfEventsHistogram", None)
2557         if len(kwargs) > 0:
2558             raise Exception("Got unexpected keyword arguments: "+ ",".join(kwargs.keys()))
2559 
2560     def loopSubFolders(self):
2561         """Return True if the PlotGroups of this folder should be applied to the all subfolders"""
2562         return self._loopSubFolders
2563 
2564     def onlyForPileup(self):
2565         """Return True if the folder is intended only for pileup samples"""
2566         return self._onlyForPileup
2567 
2568     def onlyForElectron(self):
2569         return self._onlyForElectron
2570 
2571     def onlyForConversion(self):
2572         return self._onlyForConversion
2573 
2574     def onlyForBHadron(self):
2575         return self._onlyForBHadron
2576 
2577     def getPurpose(self):
2578         return self._purpose
2579 
2580     def getPage(self):
2581         return self._page
2582 
2583     def getSection(self):
2584         return self._section
2585 
2586     def getNumberOfEventsHistogram(self):
2587         return self._numberOfEventsHistogram
2588 
2589     def append(self, plotGroup):
2590         self._plotGroups.append(plotGroup)
2591 
2592     def set(self, plotGroups):
2593         self._plotGroups = plotGroups
2594 
2595     def getPlotGroups(self):
2596         return self._plotGroups
2597 
2598     def getPlotGroup(self, name):
2599         for pg in self._plotGroups:
2600             if pg.getName() == name:
2601                 return pg
2602         raise Exception("No PlotGroup named '%s'" % name)
2603 
2604     def create(self, dirsNEvents, labels, isPileupSample=True, requireAllHistograms=False):
2605         """Create histograms from a list of TFiles.
2606 
2607         Arguments:
2608         dirsNEvents   -- List of (TDirectory, nevents) pairs
2609         labels -- List of strings for legend labels corresponding the files
2610         isPileupSample -- Is sample pileup (some PlotGroups may limit themselves to pileup)
2611         requireAllHistograms -- If True, a plot is produced if histograms from all files are present (default: False)
2612         """
2613 
2614         if len(dirsNEvents) != len(labels):
2615             raise Exception("len(dirsNEvents) should be len(labels), now they are %d and %d" % (len(dirsNEvents), len(labels)))
2616 
2617         self._labels = labels
2618 
2619         for pg in self._plotGroups:
2620             if pg.onlyForPileup() and not isPileupSample:
2621                 continue
2622             pg.create(dirsNEvents, requireAllHistograms)
2623 
2624     def draw(self, prefix=None, separate=False, saveFormat=".pdf", ratio=True, directory=""):
2625         """Draw and save all plots using settings of a given algorithm.
2626 
2627         Arguments:
2628         prefix   -- Optional string for file name prefix (default None)
2629         separate -- Save the plots of a group to separate files instead of a file per group (default False)
2630         saveFormat   -- String specifying the plot format (default '.pdf')
2631         ratio    -- Add ratio to the plot (default True)
2632         directory -- Directory where to save the file (default "")
2633         """
2634         ret = []
2635 
2636         for pg in self._plotGroups:
2637             ret.extend(pg.draw(self._labels, prefix=prefix, separate=separate, saveFormat=saveFormat, ratio=ratio, directory=directory))
2638         return ret
2639 
2640 
2641     # These are to be overridden by derived classes for customisation
2642     def translateSubFolder(self, dqmSubFolderName):
2643         """Method called to (possibly) translate a subfolder name to more 'readable' form
2644 
2645         The implementation in this (base) class just returns the
2646         argument. The idea is that a deriving class might want to do
2647         something more complex (like trackingPlots.TrackingPlotFolder
2648         does)
2649         """
2650         return dqmSubFolderName
2651 
2652     def iterSelectionName(self, plotFolderName, translatedDqmSubFolder):
2653         """Iterate over possible selections name (used in output directory name and legend) from the name of PlotterFolder, and a return value of translateSubFolder"""
2654         ret = ""
2655         if plotFolderName != "":
2656             ret += "_"+plotFolderName
2657         if translatedDqmSubFolder is not None:
2658             ret += "_"+translatedDqmSubFolder
2659         yield ret
2660 
2661     def limitSubFolder(self, limitOnlyTo, translatedDqmSubFolder):
2662         """Return True if this subfolder should be processed
2663 
2664         Arguments:
2665         limitOnlyTo            -- List/set/similar containing the translatedDqmSubFolder
2666         translatedDqmSubFolder -- Return value of translateSubFolder
2667         """
2668         return translatedDqmSubFolder in limitOnlyTo
2669 
2670 class DQMSubFolder:
2671     """Class to hold the original name and a 'translated' name of a subfolder in the DQM ROOT file"""
2672     def __init__(self, subfolder, translated):
2673         self.subfolder = subfolder
2674         self.translated = translated
2675 
2676     def equalTo(self, other):
2677         """Equality is defined by the 'translated' name"""
2678         return self.translated == other.translated
2679 
2680 class PlotterFolder:
2681     """Plotter for one DQM folder.
2682 
2683     This class is supposed to be instantiated by the Plotter class (or
2684     PlotterItem, to be more specific), and not used directly by the
2685     user.
2686     """
2687     def __init__(self, name, possibleDqmFolders, dqmSubFolders, plotFolder, fallbackNames, fallbackDqmSubFolders, tableCreators):
2688         """
2689         Constructor
2690 
2691         Arguments:
2692         name               -- Name of the folder (is used in the output directory naming)
2693         possibleDqmFolders -- List of strings for possible directories of histograms in TFiles
2694         dqmSubFolders      -- List of lists of strings for list of subfolders per input file, or None if no subfolders
2695         plotFolder         -- PlotFolder object
2696         fallbackNames      -- List of names for backward compatibility (can be empty). These are used only by validation.Validation (class responsible of the release validation workflow) in case the reference file pointed by 'name' does not exist.
2697         fallbackDqmSubFolders -- List of dicts of (string->string) for mapping the subfolder names found in the first file to another names. Use case is comparing files that have different iteration naming convention.
2698         tableCreators      -- List of PlotterTableItem objects for tables to be created from this folder
2699         """
2700         self._name = name
2701         self._possibleDqmFolders = possibleDqmFolders
2702         self._plotFolder = plotFolder
2703         #self._dqmSubFolders = [map(lambda sf: DQMSubFolder(sf, self._plotFolder.translateSubFolder(sf)), lst) for lst in dqmSubFolders]
2704         if dqmSubFolders is None:
2705             self._dqmSubFolders = None
2706         else:
2707             # Match the subfolders between files in case the lists differ
2708             # equality is by the 'translated' name
2709             subfolders = {}
2710             for sf_list in dqmSubFolders:
2711                 for sf in sf_list:
2712                     sf_translated = self._plotFolder.translateSubFolder(sf)
2713                     if sf_translated is not None and not sf_translated in subfolders:
2714                         subfolders[sf_translated] = DQMSubFolder(sf, sf_translated)
2715 
2716             self._dqmSubFolders = sorted(subfolders.values(), key=lambda sf: sf.subfolder)
2717 
2718         self._fallbackNames = fallbackNames
2719         self._fallbackDqmSubFolders = fallbackDqmSubFolders
2720         self._tableCreators = tableCreators
2721 
2722     def getName(self):
2723         return self._name
2724 
2725     def getPurpose(self):
2726         return self._plotFolder.getPurpose()
2727 
2728     def getPage(self):
2729         return self._plotFolder.getPage()
2730 
2731     def getSection(self):
2732         return self._plotFolder.getSection()
2733 
2734     def onlyForPileup(self):
2735         return self._plotFolder.onlyForPileup()
2736 
2737     def onlyForElectron(self):
2738         return self._plotFolder.onlyForElectron()
2739 
2740     def onlyForConversion(self):
2741         return self._plotFolder.onlyForConversion()
2742 
2743     def onlyForBHadron(self):
2744         return self._plotFolder.onlyForBHadron()
2745 
2746     def getPossibleDQMFolders(self):
2747         return self._possibleDqmFolders
2748 
2749     def getDQMSubFolders(self, limitOnlyTo=None):
2750         """Get list of subfolders, possibly limiting to some of them.
2751 
2752         Keyword arguments:
2753         limitOnlyTo -- Object depending on the PlotFolder type for limiting the set of subfolders to be processed
2754         """
2755 
2756         if self._dqmSubFolders is None:
2757             return [None]
2758 
2759         if limitOnlyTo is None:
2760             return self._dqmSubFolders
2761 
2762         return [s for s in self._dqmSubFolders if self._plotFolder.limitSubFolder(limitOnlyTo, s.translated)]
2763 
2764     def getTableCreators(self):
2765         return self._tableCreators
2766 
2767     def getSelectionNameIterator(self, dqmSubFolder):
2768         """Get a generator for the 'selection name', looping over the name and fallbackNames"""
2769         for name in [self._name]+self._fallbackNames:
2770             for selname in self._plotFolder.iterSelectionName(name, dqmSubFolder.translated if dqmSubFolder is not None else None):
2771                 yield selname
2772 
2773     def getSelectionName(self, dqmSubFolder):
2774         return next(self.getSelectionNameIterator(dqmSubFolder))
2775 
2776     def create(self, files, labels, dqmSubFolder, isPileupSample=True, requireAllHistograms=False):
2777         """Create histograms from a list of TFiles.
2778         Arguments:
2779         files  -- List of TFiles
2780         labels -- List of strings for legend labels corresponding the files
2781         dqmSubFolder -- DQMSubFolder object for a subfolder (or None for no subfolder)
2782         isPileupSample -- Is sample pileup (some PlotGroups may limit themselves to pileup)
2783         requireAllHistograms -- If True, a plot is produced if histograms from all files are present (default: False)
2784         """
2785 
2786         subfolder = dqmSubFolder.subfolder if dqmSubFolder is not None else None
2787         neventsHisto = self._plotFolder.getNumberOfEventsHistogram()
2788         dirsNEvents = []
2789 
2790         for tfile in files:
2791             ret = _getDirectoryDetailed(tfile, self._possibleDqmFolders, subfolder)
2792             # If file and any of possibleDqmFolders exist but subfolder does not, try the fallbacks
2793             if ret is GetDirectoryCode.SubDirNotExist:
2794                 for fallbackFunc in self._fallbackDqmSubFolders:
2795                     fallback = fallbackFunc(subfolder)
2796                     if fallback is not None:
2797                         ret = _getDirectoryDetailed(tfile, self._possibleDqmFolders, fallback)
2798                         if ret is not GetDirectoryCode.SubDirNotExist:
2799                             break
2800             d = GetDirectoryCode.codesToNone(ret)
2801             nev = None
2802             if neventsHisto is not None and tfile is not None:
2803                 hnev = _getObject(tfile, neventsHisto)
2804                 if hnev is not None:
2805                     nev = hnev.GetEntries()
2806             dirsNEvents.append( (d, nev) )
2807 
2808         self._plotFolder.create(dirsNEvents, labels, isPileupSample, requireAllHistograms)
2809 
2810     def draw(self, *args, **kwargs):
2811         """Draw and save all plots using settings of a given algorithm."""
2812         return self._plotFolder.draw(*args, **kwargs)
2813 
2814 
2815 class PlotterInstance:
2816     """Instance of plotter that knows the directory content, holds many folders."""
2817     def __init__(self, folders):
2818         self._plotterFolders = [f for f in folders if f is not None]
2819 
2820     def iterFolders(self, limitSubFoldersOnlyTo=None):
2821         for plotterFolder in self._plotterFolders:
2822             limitOnlyTo = None
2823             if limitSubFoldersOnlyTo is not None:
2824                 limitOnlyTo = limitSubFoldersOnlyTo.get(plotterFolder.getName(), None)
2825 
2826             for dqmSubFolder in plotterFolder.getDQMSubFolders(limitOnlyTo=limitOnlyTo):
2827                 yield plotterFolder, dqmSubFolder
2828 
2829 # Helper for Plotter
2830 class PlotterItem:
2831     def __init__(self, name, possibleDirs, plotFolder, fallbackNames=[], fallbackDqmSubFolders=[]):
2832         """ Constructor
2833 
2834         Arguments:
2835         name          -- Name of the folder (is used in the output directory naming)
2836         possibleDirs  -- List of strings for possible directories of histograms in TFiles
2837         plotFolder    -- PlotFolder object
2838 
2839         Keyword arguments
2840         fallbackNames -- Optional list of names for backward compatibility. These are used only by validation.Validation (class responsible of the release validation workflow) in case the reference file pointed by 'name' does not exist.
2841         fallbackDqmSubFolders -- Optional list of functions for (string->string) mapping the subfolder names found in the first file to another names (function should return None for no mapping). Use case is comparing files that have different iteration naming convention.
2842         """
2843         self._name = name
2844         self._possibleDirs = possibleDirs
2845         self._plotFolder = plotFolder
2846         self._fallbackNames = fallbackNames
2847         self._fallbackDqmSubFolders = fallbackDqmSubFolders
2848         self._tableCreators = []
2849 
2850     def getName(self):
2851         return self._name
2852 
2853     def getPlotFolder(self):
2854         return self._plotFolder
2855 
2856     def appendTableCreator(self, tc):
2857         self._tableCreators.append(tc)
2858 
2859     def readDirs(self, files):
2860         """Read available subfolders from the files
2861 
2862         Arguments:
2863         files -- List of strings for paths to files, or list of TFiles
2864 
2865         For each file, loop over 'possibleDirs', and read the
2866         subfolders of first one that exists.
2867 
2868         Returns a PlotterFolder if at least one file for which one of
2869         'possibleDirs' exists. Otherwise, return None to signal that
2870         there is nothing available for this PlotFolder.
2871         """
2872         subFolders = None
2873         if self._plotFolder.loopSubFolders():
2874             subFolders = []
2875         possibleDirFound = False
2876         for fname in files:
2877             if fname is None:
2878                 continue
2879 
2880             isOpenFile = isinstance(fname, ROOT.TFile)
2881             if isOpenFile:
2882                 tfile = fname
2883             else:
2884                 tfile = ROOT.TFile.Open(fname)
2885             for pd in self._possibleDirs:
2886                 d = tfile.Get(pd)
2887                 if d:
2888                     possibleDirFound = True
2889                     if subFolders is not None:
2890                         subf = []
2891                         for key in d.GetListOfKeys():
2892                             if isinstance(key.ReadObj(), ROOT.TDirectory):
2893                                 subf.append(key.GetName())
2894                         subFolders.append(subf)
2895                     break
2896                 else:
2897                     print("Did not find directory '%s' from file %s" % (pd, tfile.GetName()))
2898 
2899             if not isOpenFile:
2900                 tfile.Close()
2901 
2902         if not possibleDirFound:
2903             return None
2904 
2905         return PlotterFolder(self._name, self._possibleDirs, subFolders, self._plotFolder, self._fallbackNames, self._fallbackDqmSubFolders, self._tableCreators)
2906 
2907 class PlotterTableItem:
2908     def __init__(self, possibleDirs, tableCreator):
2909         self._possibleDirs = possibleDirs
2910         self._tableCreator = tableCreator
2911 
2912     def create(self, openFiles, legendLabels, dqmSubFolder):
2913         if isinstance(dqmSubFolder, list):
2914             if len(dqmSubFolder) != len(openFiles):
2915                 raise Exception("When dqmSubFolder is a list, len(dqmSubFolder) should be len(openFiles), now they are %d and %d" % (len(dqmSubFolder), len(openFiles)))
2916         else:
2917             dqmSubFolder = [dqmSubFolder]*len(openFiles)
2918         dqmSubFolder = [sf.subfolder if sf is not None else None for sf in dqmSubFolder]
2919 
2920         tbl = []
2921         for f, sf in zip(openFiles, dqmSubFolder):
2922             data = None
2923             tdir = _getDirectory(f, self._possibleDirs, sf)
2924             if tdir is not None:
2925                 data = self._tableCreator.create(tdir)
2926             tbl.append(data)
2927 
2928         # Check if we have any content
2929         allNones = True
2930         colLen = 0
2931         for col in tbl:
2932             if col is not None:
2933                 allNones = False
2934                 colLen = len(col)
2935                 break
2936         if allNones:
2937             return None
2938 
2939         # Replace all None columns with lists of column length
2940         for i in range(len(tbl)):
2941             if tbl[i] is None:
2942                 tbl[i] = [None]*colLen
2943 
2944         return html.Table(columnHeaders=legendLabels, rowHeaders=self._tableCreator.headers(), table=tbl,
2945                           purpose=self._tableCreator.getPurpose(), page=self._tableCreator.getPage(), section=self._tableCreator.getSection(dqmSubFolder[0]))
2946 
2947 class Plotter:
2948     """Contains PlotFolders, i.e. the information what plots to do, and creates a helper object to actually produce the plots."""
2949     def __init__(self):
2950         self._plots = []
2951         _setStyle()
2952         ROOT.TH1.AddDirectory(False)
2953 
2954     def append(self, *args, **kwargs):
2955         """Append a plot folder to the plotter.
2956 
2957         All arguments are forwarded to the constructor of PlotterItem.
2958         """
2959         self._plots.append(PlotterItem(*args, **kwargs))
2960 
2961     def appendTable(self, attachToFolder, *args, **kwargs):
2962         for plotterItem in self._plots:
2963             if plotterItem.getName() == attachToFolder:
2964                 plotterItem.appendTableCreator(PlotterTableItem(*args, **kwargs))
2965                 return
2966         raise Exception("Did not find plot folder '%s' when trying to attach a table creator to it" % attachToFolder)
2967 
2968     def clear(self):
2969         """Remove all plot folders and tables"""
2970         self._plots = []
2971 
2972     def getPlotFolderNames(self):
2973         return [item.getName() for item in self._plots]
2974 
2975     def getPlotFolders(self):
2976         return [item.getPlotFolder() for item in self._plots]
2977 
2978     def getPlotFolder(self, name):
2979         for item in self._plots:
2980             if item.getName() == name:
2981                 return item.getPlotFolder()
2982         raise Exception("No PlotFolder named '%s'" % name)
2983 
2984     def readDirs(self, *files):
2985         """Returns PlotterInstance object, which knows how exactly to produce the plots for these files"""
2986         return PlotterInstance([plotterItem.readDirs(files) for plotterItem in self._plots])