Back to home page

Project CMSSW displayed by LXR

 
 

    


File indexing completed on 2023-03-30 02:17:00

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