Back to home page

Project CMSSW displayed by LXR

 
 

    


File indexing completed on 2022-02-16 06:16:15

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 = 1
1247         if nrows == 2:
1248             yoffsetFactor *= 2
1249             xoffsetFactor *= 2
1250         elif nrows >= 5:
1251             yoffsetFactor *= 1.5
1252             xoffsetFactor *= 1.5
1253         elif nrows >= 3:
1254             yoffsetFactor *= 4
1255             xoffsetFactor *= 3
1256 
1257         self._frame.GetYaxis().SetTitleOffset(self._frame.GetYaxis().GetTitleOffset()*yoffsetFactor)
1258         self._frame.GetXaxis().SetTitleOffset(self._frame.GetXaxis().GetTitleOffset()*xoffsetFactor)
1259 
1260 
1261     def setLogx(self, log):
1262         self._pad.SetLogx(log)
1263 
1264     def setLogy(self, log):
1265         self._pad.SetLogy(log)
1266 
1267     def setGridx(self, grid):
1268         self._pad.SetGridx(grid)
1269 
1270     def setGridy(self, grid):
1271         self._pad.SetGridy(grid)
1272 
1273     def adjustMarginLeft(self, adjust):
1274         self._pad.SetLeftMargin(self._pad.GetLeftMargin()+adjust)
1275         # Need to redraw frame after adjusting the margin
1276         self._pad.cd()
1277         self._frame.Draw("")
1278 
1279     def adjustMarginRight(self, adjust):
1280         self._pad.SetRightMargin(self._pad.GetRightMargin()+adjust)
1281         # Need to redraw frame after adjusting the margin
1282         self._pad.cd()
1283         self._frame.Draw("")
1284 
1285     def setTitle(self, title):
1286         self._frame.SetTitle(title)
1287 
1288     def setXTitle(self, title):
1289         self._frame.GetXaxis().SetTitle(title)
1290 
1291     def setXTitleSize(self, size):
1292         self._frame.GetXaxis().SetTitleSize(size)
1293 
1294     def setXTitleOffset(self, offset):
1295         self._frame.GetXaxis().SetTitleOffset(offset)
1296 
1297     def setXLabelSize(self, size):
1298         self._frame.GetXaxis().SetLabelSize(size)
1299 
1300     def setYTitle(self, title):
1301         self._frame.GetYaxis().SetTitle(title)
1302 
1303     def setYTitleSize(self, size):
1304         self._frame.GetYaxis().SetTitleSize(size)
1305 
1306     def setYTitleOffset(self, offset):
1307         self._frame.GetYaxis().SetTitleOffset(offset)
1308 
1309     def redrawAxis(self):
1310         self._pad.RedrawAxis()
1311 
1312 class FrameRatio:
1313     """Class for creating and managing a frame for a ratio plot with two subpads"""
1314     def __init__(self, pad, bounds, zmax, ratioBounds, ratioFactor, nrows, xbinlabels=None, xbinlabelsize=None, xbinlabeloption=None, ratioYTitle=_ratioYTitle):
1315         self._parentPad = pad
1316         self._pad = pad.cd(1)
1317         if xbinlabels is not None:
1318             self._frame = _drawFrame(self._pad, bounds, zmax, [""]*len(xbinlabels))
1319         else:
1320             self._frame = _drawFrame(self._pad, bounds, zmax)
1321         self._padRatio = pad.cd(2)
1322         self._frameRatio = _drawFrame(self._padRatio, ratioBounds, zmax, xbinlabels, xbinlabelsize, xbinlabeloption)
1323 
1324         self._frame.GetXaxis().SetLabelSize(0)
1325         self._frame.GetXaxis().SetTitleSize(0)
1326 
1327         yoffsetFactor = ratioFactor
1328         divisionPoint = 1-1/ratioFactor
1329         xoffsetFactor = 1/divisionPoint #* 0.6
1330 
1331         if nrows == 1:
1332             xoffsetFactor *= 0.6
1333         elif nrows == 2:
1334             yoffsetFactor *= 2
1335             xoffsetFactor *= 1.5
1336         elif nrows == 3:
1337             yoffsetFactor *= 4
1338             xoffsetFactor *= 2.3
1339         elif nrows >= 4:
1340             yoffsetFactor *= 5
1341             xoffsetFactor *= 3
1342 
1343         self._frame.GetYaxis().SetTitleOffset(self._frameRatio.GetYaxis().GetTitleOffset()*yoffsetFactor)
1344         self._frameRatio.GetYaxis().SetLabelSize(int(self._frameRatio.GetYaxis().GetLabelSize()*0.8))
1345         self._frameRatio.GetYaxis().SetTitleOffset(self._frameRatio.GetYaxis().GetTitleOffset()*yoffsetFactor)
1346         self._frameRatio.GetXaxis().SetTitleOffset(self._frameRatio.GetXaxis().GetTitleOffset()*xoffsetFactor)
1347 
1348         self._frameRatio.GetYaxis().SetNdivisions(4, 5, 0)
1349 
1350         self._frameRatio.GetYaxis().SetTitle(ratioYTitle)
1351 
1352     def setLogx(self, log):
1353         self._pad.SetLogx(log)
1354         self._padRatio.SetLogx(log)
1355 
1356     def setLogy(self, log):
1357         self._pad.SetLogy(log)
1358 
1359     def setGridx(self, grid):
1360         self._pad.SetGridx(grid)
1361         self._padRatio.SetGridx(grid)
1362 
1363     def setGridy(self, grid):
1364         self._pad.SetGridy(grid)
1365         self._padRatio.SetGridy(grid)
1366 
1367     def adjustMarginLeft(self, adjust):
1368         self._pad.SetLeftMargin(self._pad.GetLeftMargin()+adjust)
1369         self._padRatio.SetLeftMargin(self._padRatio.GetLeftMargin()+adjust)
1370         # Need to redraw frame after adjusting the margin
1371         self._pad.cd()
1372         self._frame.Draw("")
1373         self._padRatio.cd()
1374         self._frameRatio.Draw("")
1375 
1376     def adjustMarginRight(self, adjust):
1377         self._pad.SetRightMargin(self._pad.GetRightMargin()+adjust)
1378         self._padRatio.SetRightMargin(self._padRatio.GetRightMargin()+adjust)
1379         # Need to redraw frames after adjusting the margin
1380         self._pad.cd()
1381         self._frame.Draw("")
1382         self._padRatio.cd()
1383         self._frameRatio.Draw("")
1384 
1385     def setTitle(self, title):
1386         self._frame.SetTitle(title)
1387 
1388     def setXTitle(self, title):
1389         self._frameRatio.GetXaxis().SetTitle(title)
1390 
1391     def setXTitleSize(self, size):
1392         self._frameRatio.GetXaxis().SetTitleSize(size)
1393 
1394     def setXTitleOffset(self, offset):
1395         self._frameRatio.GetXaxis().SetTitleOffset(offset)
1396 
1397     def setXLabelSize(self, size):
1398         self._frameRatio.GetXaxis().SetLabelSize(size)
1399 
1400     def setYTitle(self, title):
1401         self._frame.GetYaxis().SetTitle(title)
1402 
1403     def setYTitleRatio(self, title):
1404         self._frameRatio.GetYaxis().SetTitle(title)
1405 
1406     def setYTitleSize(self, size):
1407         self._frame.GetYaxis().SetTitleSize(size)
1408         self._frameRatio.GetYaxis().SetTitleSize(size)
1409 
1410     def setYTitleOffset(self, offset):
1411         self._frame.GetYaxis().SetTitleOffset(offset)
1412         self._frameRatio.GetYaxis().SetTitleOffset(offset)
1413 
1414     def redrawAxis(self):
1415         self._padRatio.RedrawAxis()
1416         self._pad.RedrawAxis()
1417 
1418         self._parentPad.cd()
1419 
1420         # pad to hide the lowest y axis label of the main pad
1421         xmin=0.065
1422         ymin=0.285
1423         xmax=0.128
1424         ymax=0.33
1425         self._coverPad = ROOT.TPad("coverpad", "coverpad", xmin, ymin, xmax, ymax)
1426         self._coverPad.SetBorderMode(0)
1427         self._coverPad.Draw()
1428 
1429         self._pad.cd()
1430         self._pad.Pop() # Move the first pad on top
1431 
1432 class FrameTGraph2D:
1433     """Class for creating and managing a frame for a plot from TGraph2D"""
1434     def __init__(self, pad, bounds, histos, ratioOrig, ratioFactor):
1435         self._pad = pad
1436         if ratioOrig:
1437             self._pad = pad.cd(1)
1438 
1439             # adjust margins because of not having the ratio, we want
1440             # the same bottom margin, so some algebra gives this
1441             (xlow, ylow, width, height) = (self._pad.GetXlowNDC(), self._pad.GetYlowNDC(), self._pad.GetWNDC(), self._pad.GetHNDC())
1442             xup = xlow+width
1443             yup = ylow+height
1444 
1445             bottomMargin = self._pad.GetBottomMargin()
1446             bottomMarginNew = ROOT.gStyle.GetPadBottomMargin()
1447 
1448             ylowNew = yup - (1-bottomMargin)/(1-bottomMarginNew) * (yup-ylow)
1449             topMarginNew = self._pad.GetTopMargin() * (yup-ylow)/(yup-ylowNew)
1450 
1451             self._pad.SetPad(xlow, ylowNew, xup, yup)
1452             self._pad.SetTopMargin(topMarginNew)
1453             self._pad.SetBottomMargin(bottomMarginNew)
1454 
1455         self._xtitleoffset = 1.8
1456         self._ytitleoffset = 2.3
1457 
1458         self._firstHisto = histos[0]
1459 
1460     def setLogx(self, log):
1461         pass
1462 
1463     def setLogy(self, log):
1464         pass
1465 
1466     def setGridx(self, grid):
1467         pass
1468 
1469     def setGridy(self, grid):
1470         pass
1471 
1472     def adjustMarginLeft(self, adjust):
1473         self._pad.SetLeftMargin(self._pad.GetLeftMargin()+adjust)
1474         self._pad.cd()
1475 
1476     def adjustMarginRight(self, adjust):
1477         self._pad.SetRightMargin(self._pad.GetRightMargin()+adjust)
1478         self._pad.cd()
1479 
1480     def setTitle(self, title):
1481         pass
1482 
1483     def setXTitle(self, title):
1484         self._xtitle = title
1485 
1486     def setXTitleSize(self, size):
1487         self._xtitlesize = size
1488 
1489     def setXTitleOffset(self, size):
1490         self._xtitleoffset = size
1491 
1492     def setXLabelSize(self, size):
1493         self._xlabelsize = size
1494 
1495     def setYTitle(self, title):
1496         self._ytitle = title
1497 
1498     def setYTitleSize(self, size):
1499         self._ytitlesize = size
1500 
1501     def setYTitleOffset(self, offset):
1502         self._ytitleoffset = offset
1503 
1504     def setZTitle(self, title):
1505         self._firstHisto.GetZaxis().SetTitle(title)
1506 
1507     def setZTitleOffset(self, offset):
1508         self._firstHisto.GetZaxis().SetTitleOffset(offset)
1509 
1510     def redrawAxis(self):
1511         # set top view
1512         epsilon = 1e-7
1513         self._pad.SetPhi(epsilon)
1514         self._pad.SetTheta(90+epsilon)
1515 
1516         self._firstHisto.GetXaxis().SetTitleOffset(self._xtitleoffset)
1517         self._firstHisto.GetYaxis().SetTitleOffset(self._ytitleoffset)
1518 
1519         if hasattr(self, "_xtitle"):
1520             self._firstHisto.GetXaxis().SetTitle(self._xtitle)
1521         if hasattr(self, "_xtitlesize"):
1522             self._firstHisto.GetXaxis().SetTitleSize(self._xtitlesize)
1523         if hasattr(self, "_xlabelsize"):
1524             self._firstHisto.GetXaxis().SetLabelSize(self._labelsize)
1525         if hasattr(self, "_ytitle"):
1526             self._firstHisto.GetYaxis().SetTitle(self._ytitle)
1527         if hasattr(self, "_ytitlesize"):
1528             self._firstHisto.GetYaxis().SetTitleSize(self._ytitlesize)
1529         if hasattr(self, "_ytitleoffset"):
1530             self._firstHisto.GetYaxis().SetTitleOffset(self._ytitleoffset)
1531 
1532 class PlotText:
1533     """Abstraction on top of TLatex"""
1534     def __init__(self, x, y, text, size=None, bold=True, align="left", color=ROOT.kBlack, font=None):
1535         """Constructor.
1536 
1537         Arguments:
1538         x     -- X coordinate of the text (in NDC)
1539         y     -- Y coordinate of the text (in NDC)
1540         text  -- String to draw
1541         size  -- Size of text (None for the default value, taken from gStyle)
1542         bold  -- Should the text be bold?
1543         align -- Alignment of text (left, center, right)
1544         color -- Color of the text
1545         font  -- Specify font explicitly
1546         """
1547         self._x = x
1548         self._y = y
1549         self._text = text
1550 
1551         self._l = ROOT.TLatex()
1552         self._l.SetNDC()
1553         if not bold:
1554             self._l.SetTextFont(self._l.GetTextFont()-20) # bold -> normal
1555         if font is not None:
1556             self._l.SetTextFont(font)
1557         if size is not None:
1558             self._l.SetTextSize(size)
1559         if isinstance(align, str):
1560             if align.lower() == "left":
1561                 self._l.SetTextAlign(11)
1562             elif align.lower() == "center":
1563                 self._l.SetTextAlign(21)
1564             elif align.lower() == "right":
1565                 self._l.SetTextAlign(31)
1566             else:
1567                 raise Exception("Error: Invalid option '%s' for text alignment! Options are: 'left', 'center', 'right'."%align)
1568         else:
1569             self._l.SetTextAlign(align)
1570         self._l.SetTextColor(color)
1571 
1572     def Draw(self, options=None):
1573         """Draw the text to the current TPad.
1574 
1575         Arguments:
1576         options -- For interface compatibility, ignored
1577 
1578         Provides interface compatible with ROOT's drawable objects.
1579         """
1580         self._l.DrawLatex(self._x, self._y, self._text)
1581 
1582 
1583 class PlotTextBox:
1584     """Class for drawing text and a background box."""
1585     def __init__(self, xmin, ymin, xmax, ymax, lineheight=0.04, fillColor=ROOT.kWhite, transparent=True, **kwargs):
1586         """Constructor
1587 
1588         Arguments:
1589         xmin        -- X min coordinate of the box (NDC)
1590         ymin        -- Y min coordinate of the box (NDC) (if None, deduced automatically)
1591         xmax        -- X max coordinate of the box (NDC)
1592         ymax        -- Y max coordinate of the box (NDC)
1593         lineheight  -- Line height
1594         fillColor   -- Fill color of the box
1595         transparent -- Should the box be transparent? (in practive the TPave is not created)
1596 
1597         Keyword arguments are forwarded to constructor of PlotText
1598         """
1599         # ROOT.TPave Set/GetX1NDC() etc don't seem to work as expected.
1600         self._xmin = xmin
1601         self._xmax = xmax
1602         self._ymin = ymin
1603         self._ymax = ymax
1604         self._lineheight = lineheight
1605         self._fillColor = fillColor
1606         self._transparent = transparent
1607         self._texts = []
1608         self._textArgs = {}
1609         self._textArgs.update(kwargs)
1610 
1611         self._currenty = ymax
1612 
1613     def addText(self, text):
1614         """Add text to current position"""
1615         self._currenty -= self._lineheight
1616         self._texts.append(PlotText(self._xmin+0.01, self._currenty, text, **self._textArgs))
1617 
1618     def width(self):
1619         return self._xmax-self._xmin
1620 
1621     def move(self, dx=0, dy=0, dw=0, dh=0):
1622         """Move the box and the contained text objects
1623 
1624         Arguments:
1625         dx -- Movement in x (positive is to right)
1626         dy -- Movement in y (positive is to up)
1627         dw -- Increment of width (negative to decrease width)
1628         dh -- Increment of height (negative to decrease height)
1629 
1630         dx and dy affect to both box and text objects, dw and dh
1631         affect the box only.
1632         """
1633         self._xmin += dx
1634         self._xmax += dx
1635         if self._ymin is not None:
1636             self._ymin += dy
1637         self._ymax += dy
1638 
1639         self._xmax += dw
1640         if self._ymin is not None:
1641             self._ymin -= dh
1642 
1643         for t in self._texts:
1644             t._x += dx
1645             t._y += dy
1646 
1647     def Draw(self, options=""):
1648         """Draw the box and the text to the current TPad.
1649 
1650         Arguments:
1651         options -- Forwarded to ROOT.TPave.Draw(), and the Draw() of the contained objects
1652         """
1653         if not self._transparent:
1654             ymin = self.ymin
1655             if ymin is None:
1656                 ymin = self.currenty - 0.01
1657             self._pave = ROOT.TPave(self.xmin, self.ymin, self.xmax, self.ymax, 0, "NDC")
1658             self._pave.SetFillColor(self.fillColor)
1659             self._pave.Draw(options)
1660         for t in self._texts:
1661             t.Draw(options)
1662 
1663 def _copyStyle(src, dst):
1664     properties = []
1665     if hasattr(src, "GetLineColor") and hasattr(dst, "SetLineColor"):
1666         properties.extend(["LineColor", "LineStyle", "LineWidth"])
1667     if hasattr(src, "GetFillColor") and hasattr(dst, "SetFillColor"):
1668         properties.extend(["FillColor", "FillStyle"])
1669     if hasattr(src, "GetMarkerColor") and hasattr(dst, "SetMarkerColor"):
1670         properties.extend(["MarkerColor", "MarkerSize", "MarkerStyle"])
1671 
1672     for prop in properties:
1673         getattr(dst, "Set"+prop)(getattr(src, "Get"+prop)())
1674 
1675 class PlotEmpty:
1676     """Denotes an empty place in a group."""
1677     def __init__(self):
1678         pass
1679 
1680     def getName(self):
1681         return None
1682 
1683     def drawRatioUncertainty(self):
1684         return False
1685 
1686     def create(self, *args, **kwargs):
1687         pass
1688 
1689     def isEmpty(self):
1690         return True
1691 
1692     def getNumberOfHistograms(self):
1693         return 0
1694 
1695 class Plot:
1696     """Represents one plot, comparing one or more histograms."""
1697     def __init__(self, name, **kwargs):
1698         """ Constructor.
1699 
1700         Arguments:
1701         name -- String for name of the plot, or Efficiency object
1702 
1703         Keyword arguments:
1704         fallback     -- Dictionary for specifying fallback (default None)
1705         outname      -- String for an output name of the plot (default None for the same as 'name')
1706         title        -- String for a title of the plot (default None)
1707         xtitle       -- String for x axis title (default None)
1708         xtitlesize   -- Float for x axis title size (default None)
1709         xtitleoffset -- Float for x axis title offset (default None)
1710         xlabelsize   -- Float for x axis label size (default None)
1711         ytitle       -- String for y axis title (default None)
1712         ytitlesize   -- Float for y axis title size (default None)
1713         ytitleoffset -- Float for y axis title offset (default None)
1714         ztitle       -- String for z axis title (default None)
1715         ztitleoffset -- Float for z axis title offset (default None)
1716         xmin         -- Float for x axis minimum (default None, i.e. automatic)
1717         xmax         -- Float for x axis maximum (default None, i.e. automatic)
1718         ymin         -- Float for y axis minimum (default 0)
1719         ymax         -- Float for y axis maximum (default None, i.e. automatic)
1720         xlog         -- Bool for x axis log status (default False)
1721         ylog         -- Bool for y axis log status (default False)
1722         xgrid        -- Bool for x axis grid status (default True)
1723         ygrid        -- Bool for y axis grid status (default True)
1724         stat         -- Draw stat box? (default False)
1725         fit          -- Do gaussian fit? (default False)
1726         statx        -- Stat box x coordinate (default 0.65)
1727         staty        -- Stat box y coordinate (default 0.8)
1728         statyadjust  -- List of floats for stat box y coordinate adjustments (default None)
1729         normalizeToUnitArea -- Normalize histograms to unit area? (default False)
1730         normalizeToNumberOfEvents -- Normalize histograms to number of events? If yes, the PlotFolder needs 'numberOfEventsHistogram' set to a histogram filled once per event (default False)
1731         profileX     -- Take histograms via ProfileX()? (default False)
1732         fitSlicesY   -- Take histograms via FitSlicesY() (default False)
1733         rebinX       -- rebin x axis (default None)
1734         scale        -- Scale histograms by a number (default None)
1735         xbinlabels   -- List of x axis bin labels (if given, default None)
1736         xbinlabelsize -- Size of x axis bin labels (default None)
1737         xbinlabeloption -- Option string for x axis bin labels (default None)
1738         removeEmptyBins -- Bool for removing empty bins, but only if histogram has bin labels (default False)
1739         printBins    -- Bool for printing bin values, but only if histogram has bin labels (default False)
1740         drawStyle    -- If "hist", draw as line instead of points (default None)
1741         drawCommand  -- Deliver this to Draw() (default: None for same as drawStyle)
1742         lineWidth    -- If drawStyle=="hist", the width of line (default 2)
1743         legendDx     -- Float for moving TLegend in x direction for separate=True (default None)
1744         legendDy     -- Float for moving TLegend in y direction for separate=True (default None)
1745         legendDw     -- Float for changing TLegend width for separate=True (default None)
1746         legendDh     -- Float for changing TLegend height for separate=True (default None)
1747         legend       -- Bool to enable/disable legend (default True)
1748         adjustMarginLeft  -- Float for adjusting left margin (default None)
1749         adjustMarginRight  -- Float for adjusting right margin (default None)
1750         ratio        -- Possibility to disable ratio for this particular plot (default None)
1751         ratioYmin    -- Float for y axis minimum in ratio pad (default: list of values)
1752         ratioYmax    -- Float for y axis maximum in ratio pad (default: list of values)
1753         ratioFit     -- Fit straight line in ratio? (default None)
1754         ratioUncertainty -- Plot uncertainties on ratio? (default True)
1755         ratioCoverageXrange -- Range of x axis values (xmin,xmax) to limit the automatic ratio y axis range calculation to (default None for disabled)
1756         histogramModifier -- Function to be called in create() to modify the histograms (default None)
1757         """
1758         self._name = name
1759 
1760         def _set(attr, default):
1761             setattr(self, "_"+attr, kwargs.get(attr, default))
1762 
1763         _set("fallback", None)
1764         _set("outname", None)
1765 
1766         _set("title", None)
1767         _set("xtitle", None)
1768         _set("xtitlesize", None)
1769         _set("xtitleoffset", None)
1770         _set("xlabelsize", None)
1771         _set("ytitle", None)
1772         _set("ytitlesize", None)
1773         _set("ytitleoffset", None)
1774         _set("ztitle", None)
1775         _set("ztitleoffset", None)
1776 
1777         _set("xmin", None)
1778         _set("xmax", None)
1779         _set("ymin", 0.)
1780         _set("ymax", None)
1781 
1782         _set("xlog", False)
1783         _set("ylog", False)
1784         _set("xgrid", True)
1785         _set("ygrid", True)
1786 
1787         _set("stat", False)
1788         _set("fit", False)
1789 
1790         _set("statx", 0.65)
1791         _set("staty", 0.8)
1792         _set("statyadjust", None)
1793 
1794         _set("normalizeToUnitArea", False)
1795         _set("normalizeToNumberOfEvents", False)
1796         _set("profileX", False)
1797         _set("fitSlicesY", False)
1798         _set("rebinX", None)
1799 
1800         _set("scale", None)
1801         _set("xbinlabels", None)
1802         _set("xbinlabelsize", None)
1803         _set("xbinlabeloption", None)
1804         _set("removeEmptyBins", False)
1805         _set("printBins", False)
1806 
1807         _set("drawStyle", "EP")
1808         _set("drawCommand", None)
1809         _set("lineWidth", 2)
1810 
1811         _set("legendDx", None)
1812         _set("legendDy", None)
1813         _set("legendDw", None)
1814         _set("legendDh", None)
1815         _set("legend", True)
1816 
1817         _set("adjustMarginLeft", None)
1818         _set("adjustMarginRight", None)
1819 
1820         _set("ratio", None)
1821         _set("ratioYmin", [0, 0.2, 0.5, 0.7, 0.8, 0.9, 0.95])
1822         _set("ratioYmax", [1.05, 1.1, 1.2, 1.3, 1.5, 1.8, 2, 2.5, 3, 4, 5])
1823         _set("ratioFit", None)
1824         _set("ratioUncertainty", True)
1825         _set("ratioCoverageXrange", None)
1826 
1827         _set("histogramModifier", None)
1828 
1829         self._histograms = []
1830 
1831     def setProperties(self, **kwargs):
1832         for name, value in kwargs.items():
1833             if not hasattr(self, "_"+name):
1834                 raise Exception("No attribute '%s'" % name)
1835             setattr(self, "_"+name, value)
1836 
1837     def clone(self, **kwargs):
1838         if not self.isEmpty():
1839             raise Exception("Plot can be cloned only before histograms have been created")
1840         cl = copy.copy(self)
1841         cl.setProperties(**kwargs)
1842         return cl
1843 
1844     def getNumberOfHistograms(self):
1845         """Return number of existing histograms."""
1846         return len([h for h in self._histograms if h is not None])
1847 
1848     def isEmpty(self):
1849         """Return true if there are no histograms created for the plot"""
1850         return self.getNumberOfHistograms() == 0
1851 
1852     def isTGraph2D(self):
1853         for h in self._histograms:
1854             if isinstance(h, ROOT.TGraph2D):
1855                 return True
1856         return False
1857 
1858     def isRatio(self, ratio):
1859         if self._ratio is None:
1860             return ratio
1861         return ratio and self._ratio
1862 
1863     def setName(self, name):
1864         self._name = name
1865 
1866     def getName(self):
1867         if self._outname is not None:
1868             return self._outname
1869         if isinstance(self._name, list):
1870             return str(self._name[0])
1871         else:
1872             return str(self._name)
1873 
1874     def drawRatioUncertainty(self):
1875         """Return true if the ratio uncertainty should be drawn"""
1876         return self._ratioUncertainty
1877 
1878     def _createOne(self, name, index, tdir, nevents):
1879         """Create one histogram from a TDirectory."""
1880         if tdir == None:
1881             return None
1882 
1883         # If name is a list, pick the name by the index
1884         if isinstance(name, list):
1885             name = name[index]
1886 
1887         h = _getOrCreateObject(tdir, name)
1888         if h is not None and self._normalizeToNumberOfEvents and nevents is not None and nevents != 0:
1889             h.Scale(1.0/nevents)
1890         return h
1891 
1892     def create(self, tdirNEvents, requireAllHistograms=False):
1893         """Create histograms from list of TDirectories"""
1894         self._histograms = [self._createOne(self._name, i, tdirNEvent[0], tdirNEvent[1]) for i, tdirNEvent in enumerate(tdirNEvents)]
1895 
1896         if self._fallback is not None:
1897             profileX = [self._profileX]*len(self._histograms)
1898             for i in range(0, len(self._histograms)):
1899                 if self._histograms[i] is None:
1900                     self._histograms[i] = self._createOne(self._fallback["name"], i, tdirNEvents[i][0], tdirNEvents[i][1])
1901                     profileX[i] = self._fallback.get("profileX", self._profileX)
1902 
1903         if self._histogramModifier is not None:
1904             self._histograms = self._histogramModifier(self._histograms)
1905 
1906         if len(self._histograms) > len(_plotStylesColor):
1907             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)))
1908 
1909         # Modify histograms here in case self._name returns numbers
1910         # and self._histogramModifier creates the histograms from
1911         # these numbers
1912         def _modifyHisto(th1, profileX):
1913             if th1 is None:
1914                 return None
1915 
1916             if profileX:
1917                 th1 = th1.ProfileX()
1918 
1919             if self._fitSlicesY:
1920                 ROOT.TH1.AddDirectory(True)
1921                 th1.FitSlicesY()
1922                 th1 = ROOT.gDirectory.Get(th1.GetName()+"_2")
1923                 th1.SetDirectory(None)
1924                 #th1.SetName(th1.GetName()+"_ref")
1925                 ROOT.TH1.AddDirectory(False)
1926 
1927             if self._title is not None:
1928                 th1.SetTitle(self._title)
1929 
1930             if self._scale is not None:
1931                 th1.Scale(self._scale)
1932 
1933             return th1
1934 
1935         if self._fallback is not None:
1936             self._histograms = list(map(_modifyHisto, self._histograms, profileX))
1937         else:
1938             self._histograms = list(map(lambda h: _modifyHisto(h, self._profileX), self._histograms))
1939         if requireAllHistograms and None in self._histograms:
1940             self._histograms = [None]*len(self._histograms)
1941 
1942     def _setStats(self, histos, startingX, startingY):
1943         """Set stats box."""
1944         if not self._stat:
1945             for h in histos:
1946                 if h is not None and hasattr(h, "SetStats"):
1947                     h.SetStats(0)
1948             return
1949 
1950         def _doStats(h, col, dy):
1951             if h is None:
1952                 return
1953             h.SetStats(True)
1954 
1955             if self._fit and h.GetEntries() > 0.5:
1956                 h.Fit("gaus", "Q")
1957                 f = h.GetListOfFunctions().FindObject("gaus")
1958                 if f == None:
1959                     h.SetStats(0)
1960                     return
1961                 f.SetLineColor(col)
1962                 f.SetLineWidth(1)
1963             h.Draw()
1964             ROOT.gPad.Update()
1965             st = h.GetListOfFunctions().FindObject("stats")
1966             if self._fit:
1967                 st.SetOptFit(0o010)
1968                 st.SetOptStat(1001)
1969             st.SetOptStat(1110)
1970             st.SetX1NDC(startingX)
1971             st.SetX2NDC(startingX+0.3)
1972             st.SetY1NDC(startingY+dy)
1973             st.SetY2NDC(startingY+dy+0.12)
1974             st.SetTextColor(col)
1975 
1976         dy = 0.0
1977         for i, h in enumerate(histos):
1978             if self._statyadjust is not None and i < len(self._statyadjust):
1979                 dy += self._statyadjust[i]
1980 
1981             _doStats(h, _plotStylesColor[i], dy)
1982             dy -= 0.16
1983 
1984     def _normalize(self):
1985         """Normalise histograms to unit area"""
1986 
1987         for h in self._histograms:
1988             if h is None:
1989                 continue
1990             i = h.Integral()
1991             if i == 0:
1992                 continue
1993             if h.GetSumw2().fN <= 0: # to suppress warning
1994                 h.Sumw2()
1995             h.Scale(1.0/i)
1996 
1997     def draw(self, pad, ratio, ratioFactor, nrows):
1998         """Draw the histograms using values for a given algorithm."""
1999 #        if len(self._histograms) == 0:
2000 #            print "No histograms for plot {name}".format(name=self._name)
2001 #            return
2002 
2003         isTGraph2D = self.isTGraph2D()
2004         if isTGraph2D:
2005             # Ratios for the TGraph2Ds is not that interesting
2006             ratioOrig = ratio
2007             ratio = False
2008 
2009         if self._normalizeToUnitArea:
2010             self._normalize()
2011 
2012         if self._rebinX is not None:
2013             for h in self._histograms:
2014                 h.Rebin(self._rebinX)
2015 
2016         def _styleMarker(h, msty, col):
2017             h.SetMarkerStyle(msty)
2018             h.SetMarkerColor(col)
2019             h.SetMarkerSize(0.7)
2020             h.SetLineColor(1)
2021             h.SetLineWidth(1)
2022 
2023         def _styleHist(h, msty, col):
2024             _styleMarker(h, msty, col)
2025             h.SetLineColor(col)
2026             h.SetLineWidth(self._lineWidth)
2027 
2028         # Use marker or hist style
2029         style = _styleMarker
2030         if "hist" in self._drawStyle.lower():
2031             style = _styleHist
2032         if len(self._histograms) > 0 and isinstance(self._histograms[0], ROOT.TGraph):
2033             if "l" in self._drawStyle.lower():
2034                 style = _styleHist
2035 
2036         # Apply style to histograms, filter out Nones
2037         histos = []
2038         for i, h in enumerate(self._histograms):
2039             if h is None:
2040                 continue
2041             style(h, _plotStylesMarker[i], _plotStylesColor[i])
2042             histos.append(h)
2043         if len(histos) == 0:
2044             if verbose:
2045                 print("No histograms for plot {name}".format(name=self.getName()))
2046             return
2047 
2048         # Extract x bin labels, make sure that only bins with same
2049         # label are compared with each other
2050         histosHaveBinLabels = len(histos[0].GetXaxis().GetBinLabel(1)) > 0
2051         xbinlabels = self._xbinlabels
2052         ybinlabels = None
2053         if xbinlabels is None:
2054             if histosHaveBinLabels:
2055                 xbinlabels = _mergeBinLabelsX(histos)
2056                 if isinstance(histos[0], ROOT.TH2):
2057                     ybinlabels = _mergeBinLabelsY(histos)
2058 
2059                 if len(histos) > 1: # don't bother if only one histogram
2060                     # doing this for TH2 is pending for use case, for now there is only 1 histogram/plot for TH2
2061                     histos = _th1IncludeOnlyBins(histos, xbinlabels)
2062                     self._tmp_histos = histos # need to keep these in memory too ...
2063 
2064         # Remove empty bins, but only if histograms have bin labels
2065         if self._removeEmptyBins and histosHaveBinLabels:
2066             # at this point, all histograms have been "equalized" by their x binning and labels
2067             # therefore remove bins which are empty in all histograms
2068             if isinstance(histos[0], ROOT.TH2):
2069                 (histos, xbinlabels, ybinlabels) = _th2RemoveEmptyBins(histos, xbinlabels, ybinlabels)
2070             else:
2071                 (histos, xbinlabels) = _th1RemoveEmptyBins(histos, xbinlabels)
2072             self._tmp_histos = histos # need to keep these in memory too ...
2073             if len(histos) == 0:
2074                 if verbose:
2075                     print("No histograms with non-empty bins for plot {name}".format(name=self.getName()))
2076                 return
2077 
2078         if self._printBins and histosHaveBinLabels:
2079             print("####################")
2080             print(self._name)
2081             width = max([len(l) for l in xbinlabels])
2082             tmp = "%%-%ds " % width
2083             for b in range(1, histos[0].GetNbinsX()+1):
2084                 s = tmp % xbinlabels[b-1]
2085                 for h in histos:
2086                     s += "%.3f " % h.GetBinContent(b)
2087                 print(s)
2088             print()
2089 
2090         bounds = _findBounds(histos, self._ylog,
2091                              xmin=self._xmin, xmax=self._xmax,
2092                              ymin=self._ymin, ymax=self._ymax)
2093         zmax = None
2094         if isinstance(histos[0], ROOT.TH2):
2095             zmax = max([h.GetMaximum() for h in histos])
2096 
2097         # need to keep these in memory
2098         self._mainAdditional = []
2099         self._ratioAdditional = []
2100 
2101         if ratio:
2102             self._ratios = _calculateRatios(histos, self._ratioUncertainty) # need to keep these in memory too ...
2103             ratioHistos = [h for h in [r.getRatio() for r in self._ratios[1:]] if h is not None]
2104 
2105             if len(ratioHistos) > 0:
2106                 ratioBoundsY = _findBoundsY(ratioHistos, ylog=False, ymin=self._ratioYmin, ymax=self._ratioYmax, coverage=0.68, coverageRange=self._ratioCoverageXrange)
2107             else:
2108                 ratioBoundsY = (0.9, 1,1) # hardcoded default in absence of valid ratio calculations
2109 
2110             if self._ratioFit is not None:
2111                 for i, rh in enumerate(ratioHistos):
2112                     tf_line = ROOT.TF1("line%d"%i, "[0]+x*[1]")
2113                     tf_line.SetRange(self._ratioFit["rangemin"], self._ratioFit["rangemax"])
2114                     fitres = rh.Fit(tf_line, "RINSQ")
2115                     tf_line.SetLineColor(rh.GetMarkerColor())
2116                     tf_line.SetLineWidth(2)
2117                     self._ratioAdditional.append(tf_line)
2118                     box = PlotTextBox(xmin=self._ratioFit.get("boxXmin", 0.14), ymin=None, # None for automatix
2119                                       xmax=self._ratioFit.get("boxXmax", 0.35), ymax=self._ratioFit.get("boxYmax", 0.09),
2120                                       color=rh.GetMarkerColor(), font=43, size=11, lineheight=0.02)
2121                     box.move(dx=(box.width()+0.01)*i)
2122                     #box.addText("Const: %.4f" % fitres.Parameter(0))
2123                     #box.addText("Slope: %.4f" % fitres.Parameter(1))
2124                     box.addText("Const: %.4f#pm%.4f" % (fitres.Parameter(0), fitres.ParError(0)))
2125                     box.addText("Slope: %.4f#pm%.4f" % (fitres.Parameter(1), fitres.ParError(1)))
2126                     self._mainAdditional.append(box)
2127 
2128 
2129         # Create bounds before stats in order to have the
2130         # SetRangeUser() calls made before the fit
2131         #
2132         # stats is better to be called before frame, otherwise get
2133         # mess in the plot (that frame creation cleans up)
2134         if ratio:
2135             pad.cd(1)
2136         self._setStats(histos, self._statx, self._staty)
2137 
2138         # Create frame
2139         if isTGraph2D:
2140             frame = FrameTGraph2D(pad, bounds, histos, ratioOrig, ratioFactor)
2141         else:
2142             if ratio:
2143                 ratioBounds = (bounds[0], ratioBoundsY[0], bounds[2], ratioBoundsY[1])
2144                 frame = FrameRatio(pad, bounds, zmax, ratioBounds, ratioFactor, nrows, xbinlabels, self._xbinlabelsize, self._xbinlabeloption)
2145             else:
2146                 frame = Frame(pad, bounds, zmax, nrows, xbinlabels, self._xbinlabelsize, self._xbinlabeloption, ybinlabels=ybinlabels)
2147 
2148         # Set log and grid
2149         frame.setLogx(self._xlog)
2150         frame.setLogy(self._ylog)
2151         frame.setGridx(self._xgrid)
2152         frame.setGridy(self._ygrid)
2153 
2154         # Construct draw option string
2155         opt = "sames" # s for statbox or something?
2156         ds = ""
2157         if self._drawStyle is not None:
2158             ds = self._drawStyle
2159         if self._drawCommand is not None:
2160             ds = self._drawCommand
2161         if len(ds) > 0:
2162             opt += " "+ds
2163 
2164         # Set properties of frame
2165         frame.setTitle(histos[0].GetTitle())
2166         if self._xtitle == 'Default':
2167             frame.setXTitle( histos[0].GetXaxis().GetTitle() )
2168         elif self._xtitle is not None:
2169             frame.setXTitle(self._xtitle)
2170         if self._xtitlesize is not None:
2171             frame.setXTitleSize(self._xtitlesize)
2172         if self._xtitleoffset is not None:
2173             frame.setXTitleOffset(self._xtitleoffset)
2174         if self._xlabelsize is not None:
2175             frame.setXLabelSize(self._xlabelsize)
2176         if self._ytitle == 'Default':
2177             frame.setYTitle( histos[0].GetYaxis().GetTitle() )
2178         elif self._ytitle is not None:
2179             frame.setYTitle(self._ytitle)
2180         if self._ytitlesize is not None:
2181             frame.setYTitleSize(self._ytitlesize)
2182         if self._ytitleoffset is not None:
2183             frame.setYTitleOffset(self._ytitleoffset)
2184         if self._ztitle is not None:
2185             frame.setZTitle(self._ztitle)
2186         if self._ztitleoffset is not None:
2187             frame.setZTitleOffset(self._ztitleoffset)
2188         if self._adjustMarginLeft is not None:
2189             frame.adjustMarginLeft(self._adjustMarginLeft)
2190         if self._adjustMarginRight is not None:
2191             frame.adjustMarginRight(self._adjustMarginRight)
2192         elif "z" in opt:
2193             frame.adjustMarginLeft(0.03)
2194             frame.adjustMarginRight(0.08)
2195 
2196         # Draw histograms
2197         if ratio:
2198             frame._pad.cd()
2199 
2200         for i, h in enumerate(histos):
2201             o = opt
2202             if isTGraph2D and i == 0:
2203                 o = o.replace("sames", "")
2204             h.Draw(o)
2205 
2206         for addl in self._mainAdditional:
2207             addl.Draw("same")
2208 
2209         # Draw ratios
2210         if ratio and len(self._ratios) > 0:
2211             frame._padRatio.cd()
2212             firstRatio = self._ratios[0].getRatio()
2213             if self._ratioUncertainty and firstRatio is not None:
2214                 firstRatio.SetFillStyle(1001)
2215                 firstRatio.SetFillColor(ROOT.kGray)
2216                 firstRatio.SetLineColor(ROOT.kGray)
2217                 firstRatio.SetMarkerColor(ROOT.kGray)
2218                 firstRatio.SetMarkerSize(0)
2219                 self._ratios[0].draw("E2")
2220                 frame._padRatio.RedrawAxis("G") # redraw grid on top of the uncertainty of denominator
2221             for r in self._ratios[1:]:
2222                 r.draw()
2223 
2224             for addl in self._ratioAdditional:
2225                 addl.Draw("same")
2226 
2227         frame.redrawAxis()
2228         self._frame = frame # keep the frame in memory for sure
2229 
2230     def addToLegend(self, legend, legendLabels, denomUncertainty):
2231         """Add histograms to a legend.
2232 
2233         Arguments:
2234         legend       -- TLegend
2235         legendLabels -- List of strings for the legend labels
2236         """
2237         first = denomUncertainty
2238         for h, label in zip(self._histograms, legendLabels):
2239             if h is None:
2240                 first = False
2241                 continue
2242             if first:
2243                 self._forLegend = h.Clone()
2244                 self._forLegend.SetFillStyle(1001)
2245                 self._forLegend.SetFillColor(ROOT.kGray)
2246                 entry = legend.AddEntry(self._forLegend, label, "lpf")
2247                 first = False
2248             else:
2249                 legend.AddEntry(h, label, "LP")
2250 
2251 class PlotGroup(object):
2252     """Group of plots, results a TCanvas"""
2253     def __init__(self, name, plots, **kwargs):
2254         """Constructor.
2255 
2256         Arguments:
2257         name  -- String for name of the TCanvas, used also as the basename of the picture files
2258         plots -- List of Plot objects
2259 
2260         Keyword arguments:
2261         ncols    -- Number of columns (default 2)
2262         legendDx -- Float for moving TLegend in x direction (default None)
2263         legendDy -- Float for moving TLegend in y direction (default None)
2264         legendDw -- Float for changing TLegend width (default None)
2265         legendDh -- Float for changing TLegend height (default None)
2266         legend   -- Bool for disabling legend (default True for legend being enabled)
2267         overrideLegendLabels -- List of strings for legend labels, if given, these are used instead of the ones coming from Plotter (default None)
2268         onlyForPileup  -- Plots this group only for pileup samples
2269         """
2270         super(PlotGroup, self).__init__()
2271 
2272         self._name = name
2273         self._plots = plots
2274 
2275         def _set(attr, default):
2276             setattr(self, "_"+attr, kwargs.get(attr, default))
2277 
2278         _set("ncols", 2)
2279 
2280         _set("legendDx", None)
2281         _set("legendDy", None)
2282         _set("legendDw", None)
2283         _set("legendDh", None)
2284         _set("legend", True)
2285 
2286         _set("overrideLegendLabels", None)
2287 
2288         _set("onlyForPileup", False)
2289 
2290         self._ratioFactor = 1.25
2291 
2292     def setProperties(self, **kwargs):
2293         for name, value in kwargs.items():
2294             if not hasattr(self, "_"+name):
2295                 raise Exception("No attribute '%s'" % name)
2296             setattr(self, "_"+name, value)
2297 
2298     def getName(self):
2299         return self._name
2300 
2301     def getPlots(self):
2302         return self._plots
2303 
2304     def remove(self, name):
2305         for i, plot in enumerate(self._plots):
2306             if plot.getName() == name:
2307                 del self._plots[i]
2308                 return
2309         raise Exception("Did not find Plot '%s' from PlotGroup '%s'" % (name, self._name))
2310 
2311     def clear(self):
2312         self._plots = []
2313 
2314     def append(self, plot):
2315         self._plots.append(plot)
2316 
2317     def getPlot(self, name):
2318         for plot in self._plots:
2319             if plot.getName() == name:
2320                 return plot
2321         raise Exception("No Plot named '%s'" % name)
2322 
2323     def onlyForPileup(self):
2324         """Return True if the PlotGroup is intended only for pileup samples"""
2325         return self._onlyForPileup
2326 
2327     def create(self, tdirectoryNEvents, requireAllHistograms=False):
2328         """Create histograms from a list of TDirectories.
2329 
2330         Arguments:
2331         tdirectoryNEvents    -- List of (TDirectory, nevents) pairs
2332         requireAllHistograms -- If True, a plot is produced if histograms from all files are present (default: False)
2333         """
2334         for plot in self._plots:
2335             plot.create(tdirectoryNEvents, requireAllHistograms)
2336 
2337     def draw(self, legendLabels, prefix=None, separate=False, saveFormat=".pdf", ratio=True, directory=""):
2338         """Draw the histograms using values for a given algorithm.
2339 
2340         Arguments:
2341         legendLabels  -- List of strings for legend labels (corresponding to the tdirectories in create())
2342         prefix        -- Optional string for file name prefix (default None)
2343         separate      -- Save the plots of a group to separate files instead of a file per group (default False)
2344         saveFormat   -- String specifying the plot format (default '.pdf')
2345         ratio        -- Add ratio to the plot (default True)
2346         directory     -- Directory where to save the file (default "")
2347         """
2348 
2349         if self._overrideLegendLabels is not None:
2350             legendLabels = self._overrideLegendLabels
2351 
2352         # Do not draw the group if it would be empty
2353         onlyEmptyPlots = True
2354         for plot in self._plots:
2355             if not plot.isEmpty():
2356                 onlyEmptyPlots = False
2357                 break
2358         if onlyEmptyPlots:
2359             return []
2360 
2361         if separate:
2362             return self._drawSeparate(legendLabels, prefix, saveFormat, ratio, directory)
2363 
2364         cwidth = 500*self._ncols
2365         nrows = int((len(self._plots)+self._ncols-1)/self._ncols) # this should work also for odd n
2366         cheight = 500 * nrows
2367 
2368         if ratio:
2369             cheight = int(cheight*self._ratioFactor)
2370 
2371         canvas = _createCanvas(self._name, cwidth, cheight)
2372 
2373         canvas.Divide(self._ncols, nrows)
2374         if ratio:
2375             for i, plot in enumerate(self._plots):
2376                 pad = canvas.cd(i+1)
2377                 self._modifyPadForRatio(pad)
2378 
2379         # Draw plots to canvas
2380         for i, plot in enumerate(self._plots):
2381             pad = canvas.cd(i+1)
2382             if not plot.isEmpty():
2383                 plot.draw(pad, ratio, self._ratioFactor, nrows)
2384 
2385         if plot._legend:
2386           # Setup legend
2387           canvas.cd()
2388           if len(self._plots) <= 4:
2389               lx1 = 0.2
2390               lx2 = 0.9
2391               ly1 = 0.48
2392               ly2 = 0.53
2393           else:
2394               lx1 = 0.1
2395               lx2 = 0.9
2396               ly1 = 0.64
2397               ly2 = 0.67
2398           if self._legendDx is not None:
2399               lx1 += self._legendDx
2400               lx2 += self._legendDx
2401           if self._legendDy is not None:
2402               ly1 += self._legendDy
2403               ly2 += self._legendDy
2404           if self._legendDw is not None:
2405               lx2 += self._legendDw
2406           if self._legendDh is not None:
2407               ly1 -= self._legendDh
2408           plot = max(self._plots, key=lambda p: p.getNumberOfHistograms())
2409           denomUnc = sum([p.drawRatioUncertainty() for p in self._plots]) > 0
2410           legend = self._createLegend(plot, legendLabels, lx1, ly1, lx2, ly2,
2411                                       denomUncertainty=(ratio and denomUnc))
2412 
2413         return self._save(canvas, saveFormat, prefix=prefix, directory=directory)
2414 
2415     def _drawSeparate(self, legendLabels, prefix, saveFormat, ratio, directory):
2416         """Internal method to do the drawing to separate files per Plot instead of a file per PlotGroup"""
2417         width = 500
2418         height = 500
2419 
2420         lx1def = 0.6
2421         lx2def = 0.95
2422         ly1def = 0.85
2423         ly2def = 0.95
2424 
2425         ret = []
2426 
2427         for plot in self._plots:
2428             if plot.isEmpty():
2429                 continue
2430 
2431             canvas = _createCanvas(self._name+"Single", width, height)
2432             canvasRatio = _createCanvas(self._name+"SingleRatio", width, int(height*self._ratioFactor))
2433 
2434             # from TDRStyle
2435             for c in [canvas, canvasRatio]:
2436                 c.SetTopMargin(0.05)
2437                 c.SetBottomMargin(0.13)
2438                 c.SetLeftMargin(0.16)
2439                 c.SetRightMargin(0.05)
2440 
2441             ratioForThisPlot = plot.isRatio(ratio)
2442             c = canvas
2443             if ratioForThisPlot:
2444                 c = canvasRatio
2445                 c.cd()
2446                 self._modifyPadForRatio(c)
2447 
2448             # Draw plot to canvas
2449             c.cd()
2450             plot.draw(c, ratioForThisPlot, self._ratioFactor, 1)
2451 
2452             if plot._legend:
2453                 # Setup legend
2454                 lx1 = lx1def
2455                 lx2 = lx2def
2456                 ly1 = ly1def
2457                 ly2 = ly2def
2458 
2459                 if plot._legendDx is not None:
2460                     lx1 += plot._legendDx
2461                     lx2 += plot._legendDx
2462                 if plot._legendDy is not None:
2463                     ly1 += plot._legendDy
2464                     ly2 += plot._legendDy
2465                 if plot._legendDw is not None:
2466                     lx2 += plot._legendDw
2467                 if plot._legendDh is not None:
2468                     ly1 -= plot._legendDh
2469 
2470                 c.cd()
2471                 legend = self._createLegend(plot, legendLabels, lx1, ly1, lx2, ly2, textSize=0.03,
2472                                             denomUncertainty=(ratioForThisPlot and plot.drawRatioUncertainty))
2473 
2474             ret.extend(self._save(c, saveFormat, prefix=prefix, postfix="/"+plot.getName(), single=True, directory=directory))
2475         return ret
2476 
2477     def _modifyPadForRatio(self, pad):
2478         """Internal method to set divide a pad to two for ratio plots"""
2479         _modifyPadForRatio(pad, self._ratioFactor)
2480 
2481     def _createLegend(self, plot, legendLabels, lx1, ly1, lx2, ly2, textSize=0.016, denomUncertainty=True):
2482         if not self._legend:
2483             return None
2484 
2485         l = ROOT.TLegend(lx1, ly1, lx2, ly2)
2486         l.SetTextSize(textSize)
2487         l.SetLineColor(1)
2488         l.SetLineWidth(1)
2489         l.SetLineStyle(1)
2490         l.SetFillColor(0)
2491         l.SetMargin(0.07)
2492 
2493         plot.addToLegend(l, legendLabels, denomUncertainty)
2494         l.Draw()
2495         return l
2496 
2497     def _save(self, canvas, saveFormat, prefix=None, postfix=None, single=False, directory=""):
2498         # Save the canvas to file and clear
2499         name = self._name
2500         if not os.path.exists(directory+'/'+name):
2501             os.makedirs(directory+'/'+name, exist_ok=True)
2502         if prefix is not None:
2503             name = prefix+name
2504         if postfix is not None:
2505             name = name+postfix
2506         name = os.path.join(directory, name)
2507 
2508         if not verbose: # silence saved file printout
2509             backup = ROOT.gErrorIgnoreLevel
2510             ROOT.gErrorIgnoreLevel = ROOT.kWarning
2511         canvas.SaveAs(name+saveFormat)
2512         if not verbose:
2513             ROOT.gErrorIgnoreLevel = backup
2514 
2515         if single:
2516             canvas.Clear()
2517             canvas.SetLogx(False)
2518             canvas.SetLogy(False)
2519         else:
2520             canvas.Clear("D") # keep subpads
2521 
2522         return [name+saveFormat]
2523 
2524 class PlotOnSideGroup(PlotGroup):
2525     """Resembles DQM GUI's "On side" layout.
2526 
2527     Like PlotGroup, but has only a description of a single plot. The
2528     plot is drawn separately for each file. Useful for 2D histograms."""
2529 
2530     def __init__(self, name, plot, ncols=2, onlyForPileup=False):
2531         super(PlotOnSideGroup, self).__init__(name, [], ncols=ncols, legend=False, onlyForPileup=onlyForPileup)
2532         self._plot = plot
2533         self._plot.setProperties(ratio=False)
2534 
2535     def append(self, *args, **kwargs):
2536         raise Exception("PlotOnSideGroup.append() is not implemented")
2537 
2538     def create(self, tdirectoryNEvents, requireAllHistograms=False):
2539         self._plots = []
2540         for i, element in enumerate(tdirectoryNEvents):
2541             pl = self._plot.clone()
2542             pl.create([element], requireAllHistograms)
2543             pl.setName(pl.getName()+"_"+str(i))
2544             self._plots.append(pl)
2545 
2546     def draw(self, *args, **kwargs):
2547         kargs = copy.copy(kwargs)
2548         kargs["ratio"] = False
2549         return super(PlotOnSideGroup, self).draw(*args, **kargs)
2550 
2551 class PlotFolder:
2552 
2553     """Represents a collection of PlotGroups, produced from a single folder in a DQM file"""
2554     def __init__(self, *plotGroups, **kwargs):
2555         """Constructor.
2556 
2557         Arguments:
2558         plotGroups     -- List of PlotGroup objects
2559 
2560         Keyword arguments
2561         loopSubFolders -- Should the subfolders be looped over? (default: True)
2562         onlyForPileup  -- Plots this folder only for pileup samples
2563         onlyForElectron -- Plots this folder only for electron samples
2564         onlyForConversion -- Plots this folder only for conversion samples
2565         onlyForBHadron -- Plots this folder only for B-hadron samples
2566         purpose        -- html.PlotPurpose member class for the purpose of the folder, used for grouping of the plots to the HTML pages
2567         page           -- Optional string for the page in HTML generatin
2568         section        -- Optional string for the section within a page in HTML generation
2569         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".
2570         """
2571         self._plotGroups = list(plotGroups)
2572         self._loopSubFolders = kwargs.pop("loopSubFolders", True)
2573         self._onlyForPileup = kwargs.pop("onlyForPileup", False)
2574         self._onlyForElectron = kwargs.pop("onlyForElectron", False)
2575         self._onlyForConversion = kwargs.pop("onlyForConversion", False)
2576         self._onlyForBHadron = kwargs.pop("onlyForBHadron", False)
2577         self._purpose = kwargs.pop("purpose", None)
2578         self._page = kwargs.pop("page", None)
2579         self._section = kwargs.pop("section", None)
2580         self._numberOfEventsHistogram = kwargs.pop("numberOfEventsHistogram", None)
2581         if len(kwargs) > 0:
2582             raise Exception("Got unexpected keyword arguments: "+ ",".join(kwargs.keys()))
2583 
2584     def loopSubFolders(self):
2585         """Return True if the PlotGroups of this folder should be applied to the all subfolders"""
2586         return self._loopSubFolders
2587 
2588     def onlyForPileup(self):
2589         """Return True if the folder is intended only for pileup samples"""
2590         return self._onlyForPileup
2591 
2592     def onlyForElectron(self):
2593         return self._onlyForElectron
2594 
2595     def onlyForConversion(self):
2596         return self._onlyForConversion
2597 
2598     def onlyForBHadron(self):
2599         return self._onlyForBHadron
2600 
2601     def getPurpose(self):
2602         return self._purpose
2603 
2604     def getPage(self):
2605         return self._page
2606 
2607     def getSection(self):
2608         return self._section
2609 
2610     def getNumberOfEventsHistogram(self):
2611         return self._numberOfEventsHistogram
2612 
2613     def append(self, plotGroup):
2614         self._plotGroups.append(plotGroup)
2615 
2616     def set(self, plotGroups):
2617         self._plotGroups = plotGroups
2618 
2619     def getPlotGroups(self):
2620         return self._plotGroups
2621 
2622     def getPlotGroup(self, name):
2623         for pg in self._plotGroups:
2624             if pg.getName() == name:
2625                 return pg
2626         raise Exception("No PlotGroup named '%s'" % name)
2627 
2628     def create(self, dirsNEvents, labels, isPileupSample=True, requireAllHistograms=False):
2629         """Create histograms from a list of TFiles.
2630 
2631         Arguments:
2632         dirsNEvents   -- List of (TDirectory, nevents) pairs
2633         labels -- List of strings for legend labels corresponding the files
2634         isPileupSample -- Is sample pileup (some PlotGroups may limit themselves to pileup)
2635         requireAllHistograms -- If True, a plot is produced if histograms from all files are present (default: False)
2636         """
2637 
2638         if len(dirsNEvents) != len(labels):
2639             raise Exception("len(dirsNEvents) should be len(labels), now they are %d and %d" % (len(dirsNEvents), len(labels)))
2640 
2641         self._labels = labels
2642 
2643         for pg in self._plotGroups:
2644             if pg.onlyForPileup() and not isPileupSample:
2645                 continue
2646             pg.create(dirsNEvents, requireAllHistograms)
2647 
2648     def draw(self, prefix=None, separate=False, saveFormat=".pdf", ratio=True, directory=""):
2649         """Draw and save all plots using settings of a given algorithm.
2650 
2651         Arguments:
2652         prefix   -- Optional string for file name prefix (default None)
2653         separate -- Save the plots of a group to separate files instead of a file per group (default False)
2654         saveFormat   -- String specifying the plot format (default '.pdf')
2655         ratio    -- Add ratio to the plot (default True)
2656         directory -- Directory where to save the file (default "")
2657         """
2658         ret = []
2659 
2660         for pg in self._plotGroups:
2661             ret.extend(pg.draw(self._labels, prefix=prefix, separate=separate, saveFormat=saveFormat, ratio=ratio, directory=directory))
2662         return ret
2663 
2664 
2665     # These are to be overridden by derived classes for customisation
2666     def translateSubFolder(self, dqmSubFolderName):
2667         """Method called to (possibly) translate a subfolder name to more 'readable' form
2668 
2669         The implementation in this (base) class just returns the
2670         argument. The idea is that a deriving class might want to do
2671         something more complex (like trackingPlots.TrackingPlotFolder
2672         does)
2673         """
2674         return dqmSubFolderName
2675 
2676     def iterSelectionName(self, plotFolderName, translatedDqmSubFolder):
2677         """Iterate over possible selections name (used in output directory name and legend) from the name of PlotterFolder, and a return value of translateSubFolder"""
2678         ret = ""
2679         if plotFolderName != "":
2680             ret += "_"+plotFolderName
2681         if translatedDqmSubFolder is not None:
2682             ret += "_"+translatedDqmSubFolder
2683         yield ret
2684 
2685     def limitSubFolder(self, limitOnlyTo, translatedDqmSubFolder):
2686         """Return True if this subfolder should be processed
2687 
2688         Arguments:
2689         limitOnlyTo            -- List/set/similar containing the translatedDqmSubFolder
2690         translatedDqmSubFolder -- Return value of translateSubFolder
2691         """
2692         return translatedDqmSubFolder in limitOnlyTo
2693 
2694 class DQMSubFolder:
2695     """Class to hold the original name and a 'translated' name of a subfolder in the DQM ROOT file"""
2696     def __init__(self, subfolder, translated):
2697         self.subfolder = subfolder
2698         self.translated = translated
2699 
2700     def equalTo(self, other):
2701         """Equality is defined by the 'translated' name"""
2702         return self.translated == other.translated
2703 
2704 class PlotterFolder:
2705     """Plotter for one DQM folder.
2706 
2707     This class is supposed to be instantiated by the Plotter class (or
2708     PlotterItem, to be more specific), and not used directly by the
2709     user.
2710     """
2711     def __init__(self, name, possibleDqmFolders, dqmSubFolders, plotFolder, fallbackNames, fallbackDqmSubFolders, tableCreators):
2712         """
2713         Constructor
2714 
2715         Arguments:
2716         name               -- Name of the folder (is used in the output directory naming)
2717         possibleDqmFolders -- List of strings for possible directories of histograms in TFiles
2718         dqmSubFolders      -- List of lists of strings for list of subfolders per input file, or None if no subfolders
2719         plotFolder         -- PlotFolder object
2720         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.
2721         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.
2722         tableCreators      -- List of PlotterTableItem objects for tables to be created from this folder
2723         """
2724         self._name = name
2725         self._possibleDqmFolders = possibleDqmFolders
2726         self._plotFolder = plotFolder
2727         #self._dqmSubFolders = [map(lambda sf: DQMSubFolder(sf, self._plotFolder.translateSubFolder(sf)), lst) for lst in dqmSubFolders]
2728         if dqmSubFolders is None:
2729             self._dqmSubFolders = None
2730         else:
2731             # Match the subfolders between files in case the lists differ
2732             # equality is by the 'translated' name
2733             subfolders = {}
2734             for sf_list in dqmSubFolders:
2735                 for sf in sf_list:
2736                     sf_translated = self._plotFolder.translateSubFolder(sf)
2737                     if sf_translated is not None and not sf_translated in subfolders:
2738                         subfolders[sf_translated] = DQMSubFolder(sf, sf_translated)
2739 
2740             self._dqmSubFolders = sorted(subfolders.values(), key=lambda sf: sf.subfolder)
2741 
2742         self._fallbackNames = fallbackNames
2743         self._fallbackDqmSubFolders = fallbackDqmSubFolders
2744         self._tableCreators = tableCreators
2745 
2746     def getName(self):
2747         return self._name
2748 
2749     def getPurpose(self):
2750         return self._plotFolder.getPurpose()
2751 
2752     def getPage(self):
2753         return self._plotFolder.getPage()
2754 
2755     def getSection(self):
2756         return self._plotFolder.getSection()
2757 
2758     def onlyForPileup(self):
2759         return self._plotFolder.onlyForPileup()
2760 
2761     def onlyForElectron(self):
2762         return self._plotFolder.onlyForElectron()
2763 
2764     def onlyForConversion(self):
2765         return self._plotFolder.onlyForConversion()
2766 
2767     def onlyForBHadron(self):
2768         return self._plotFolder.onlyForBHadron()
2769 
2770     def getPossibleDQMFolders(self):
2771         return self._possibleDqmFolders
2772 
2773     def getDQMSubFolders(self, limitOnlyTo=None):
2774         """Get list of subfolders, possibly limiting to some of them.
2775 
2776         Keyword arguments:
2777         limitOnlyTo -- Object depending on the PlotFolder type for limiting the set of subfolders to be processed
2778         """
2779 
2780         if self._dqmSubFolders is None:
2781             return [None]
2782 
2783         if limitOnlyTo is None:
2784             return self._dqmSubFolders
2785 
2786         return [s for s in self._dqmSubFolders if self._plotFolder.limitSubFolder(limitOnlyTo, s.translated)]
2787 
2788     def getTableCreators(self):
2789         return self._tableCreators
2790 
2791     def getSelectionNameIterator(self, dqmSubFolder):
2792         """Get a generator for the 'selection name', looping over the name and fallbackNames"""
2793         for name in [self._name]+self._fallbackNames:
2794             for selname in self._plotFolder.iterSelectionName(name, dqmSubFolder.translated if dqmSubFolder is not None else None):
2795                 yield selname
2796 
2797     def getSelectionName(self, dqmSubFolder):
2798         return next(self.getSelectionNameIterator(dqmSubFolder))
2799 
2800     def create(self, files, labels, dqmSubFolder, isPileupSample=True, requireAllHistograms=False):
2801         """Create histograms from a list of TFiles.
2802         Arguments:
2803         files  -- List of TFiles
2804         labels -- List of strings for legend labels corresponding the files
2805         dqmSubFolder -- DQMSubFolder object for a subfolder (or None for no subfolder)
2806         isPileupSample -- Is sample pileup (some PlotGroups may limit themselves to pileup)
2807         requireAllHistograms -- If True, a plot is produced if histograms from all files are present (default: False)
2808         """
2809 
2810         subfolder = dqmSubFolder.subfolder if dqmSubFolder is not None else None
2811         neventsHisto = self._plotFolder.getNumberOfEventsHistogram()
2812         dirsNEvents = []
2813 
2814         for tfile in files:
2815             ret = _getDirectoryDetailed(tfile, self._possibleDqmFolders, subfolder)
2816             # If file and any of possibleDqmFolders exist but subfolder does not, try the fallbacks
2817             if ret is GetDirectoryCode.SubDirNotExist:
2818                 for fallbackFunc in self._fallbackDqmSubFolders:
2819                     fallback = fallbackFunc(subfolder)
2820                     if fallback is not None:
2821                         ret = _getDirectoryDetailed(tfile, self._possibleDqmFolders, fallback)
2822                         if ret is not GetDirectoryCode.SubDirNotExist:
2823                             break
2824             d = GetDirectoryCode.codesToNone(ret)
2825             nev = None
2826             if neventsHisto is not None and tfile is not None:
2827                 hnev = _getObject(tfile, neventsHisto)
2828                 if hnev is not None:
2829                     nev = hnev.GetEntries()
2830             dirsNEvents.append( (d, nev) )
2831 
2832         self._plotFolder.create(dirsNEvents, labels, isPileupSample, requireAllHistograms)
2833 
2834     def draw(self, *args, **kwargs):
2835         """Draw and save all plots using settings of a given algorithm."""
2836         return self._plotFolder.draw(*args, **kwargs)
2837 
2838 
2839 class PlotterInstance:
2840     """Instance of plotter that knows the directory content, holds many folders."""
2841     def __init__(self, folders):
2842         self._plotterFolders = [f for f in folders if f is not None]
2843 
2844     def iterFolders(self, limitSubFoldersOnlyTo=None):
2845         for plotterFolder in self._plotterFolders:
2846             limitOnlyTo = None
2847             if limitSubFoldersOnlyTo is not None:
2848                 limitOnlyTo = limitSubFoldersOnlyTo.get(plotterFolder.getName(), None)
2849 
2850             for dqmSubFolder in plotterFolder.getDQMSubFolders(limitOnlyTo=limitOnlyTo):
2851                 yield plotterFolder, dqmSubFolder
2852 
2853 # Helper for Plotter
2854 class PlotterItem:
2855     def __init__(self, name, possibleDirs, plotFolder, fallbackNames=[], fallbackDqmSubFolders=[]):
2856         """ Constructor
2857 
2858         Arguments:
2859         name          -- Name of the folder (is used in the output directory naming)
2860         possibleDirs  -- List of strings for possible directories of histograms in TFiles
2861         plotFolder    -- PlotFolder object
2862 
2863         Keyword arguments
2864         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.
2865         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.
2866         """
2867         self._name = name
2868         self._possibleDirs = possibleDirs
2869         self._plotFolder = plotFolder
2870         self._fallbackNames = fallbackNames
2871         self._fallbackDqmSubFolders = fallbackDqmSubFolders
2872         self._tableCreators = []
2873 
2874     def getName(self):
2875         return self._name
2876 
2877     def getPlotFolder(self):
2878         return self._plotFolder
2879 
2880     def appendTableCreator(self, tc):
2881         self._tableCreators.append(tc)
2882 
2883     def readDirs(self, files):
2884         """Read available subfolders from the files
2885 
2886         Arguments:
2887         files -- List of strings for paths to files, or list of TFiles
2888 
2889         For each file, loop over 'possibleDirs', and read the
2890         subfolders of first one that exists.
2891 
2892         Returns a PlotterFolder if at least one file for which one of
2893         'possibleDirs' exists. Otherwise, return None to signal that
2894         there is nothing available for this PlotFolder.
2895         """
2896         subFolders = None
2897         if self._plotFolder.loopSubFolders():
2898             subFolders = []
2899         possibleDirFound = False
2900         for fname in files:
2901             if fname is None:
2902                 continue
2903 
2904             isOpenFile = isinstance(fname, ROOT.TFile)
2905             if isOpenFile:
2906                 tfile = fname
2907             else:
2908                 tfile = ROOT.TFile.Open(fname)
2909             for pd in self._possibleDirs:
2910                 d = tfile.Get(pd)
2911                 if d:
2912                     possibleDirFound = True
2913                     if subFolders is not None:
2914                         subf = []
2915                         for key in d.GetListOfKeys():
2916                             if isinstance(key.ReadObj(), ROOT.TDirectory):
2917                                 subf.append(key.GetName())
2918                         subFolders.append(subf)
2919                     break
2920                 else:
2921                     print("Did not find directory '%s' from file %s" % (pd, tfile.GetName()))
2922 
2923             if not isOpenFile:
2924                 tfile.Close()
2925 
2926         if not possibleDirFound:
2927             return None
2928 
2929         return PlotterFolder(self._name, self._possibleDirs, subFolders, self._plotFolder, self._fallbackNames, self._fallbackDqmSubFolders, self._tableCreators)
2930 
2931 class PlotterTableItem:
2932     def __init__(self, possibleDirs, tableCreator):
2933         self._possibleDirs = possibleDirs
2934         self._tableCreator = tableCreator
2935 
2936     def create(self, openFiles, legendLabels, dqmSubFolder):
2937         if isinstance(dqmSubFolder, list):
2938             if len(dqmSubFolder) != len(openFiles):
2939                 raise Exception("When dqmSubFolder is a list, len(dqmSubFolder) should be len(openFiles), now they are %d and %d" % (len(dqmSubFolder), len(openFiles)))
2940         else:
2941             dqmSubFolder = [dqmSubFolder]*len(openFiles)
2942         dqmSubFolder = [sf.subfolder if sf is not None else None for sf in dqmSubFolder]
2943 
2944         tbl = []
2945         for f, sf in zip(openFiles, dqmSubFolder):
2946             data = None
2947             tdir = _getDirectory(f, self._possibleDirs, sf)
2948             if tdir is not None:
2949                 data = self._tableCreator.create(tdir)
2950             tbl.append(data)
2951 
2952         # Check if we have any content
2953         allNones = True
2954         colLen = 0
2955         for col in tbl:
2956             if col is not None:
2957                 allNones = False
2958                 colLen = len(col)
2959                 break
2960         if allNones:
2961             return None
2962 
2963         # Replace all None columns with lists of column length
2964         for i in range(len(tbl)):
2965             if tbl[i] is None:
2966                 tbl[i] = [None]*colLen
2967 
2968         return html.Table(columnHeaders=legendLabels, rowHeaders=self._tableCreator.headers(), table=tbl,
2969                           purpose=self._tableCreator.getPurpose(), page=self._tableCreator.getPage(), section=self._tableCreator.getSection(dqmSubFolder[0]))
2970 
2971 class Plotter:
2972     """Contains PlotFolders, i.e. the information what plots to do, and creates a helper object to actually produce the plots."""
2973     def __init__(self):
2974         self._plots = []
2975         _setStyle()
2976         ROOT.TH1.AddDirectory(False)
2977 
2978     def append(self, *args, **kwargs):
2979         """Append a plot folder to the plotter.
2980 
2981         All arguments are forwarded to the constructor of PlotterItem.
2982         """
2983         self._plots.append(PlotterItem(*args, **kwargs))
2984 
2985     def appendTable(self, attachToFolder, *args, **kwargs):
2986         for plotterItem in self._plots:
2987             if plotterItem.getName() == attachToFolder:
2988                 plotterItem.appendTableCreator(PlotterTableItem(*args, **kwargs))
2989                 return
2990         raise Exception("Did not find plot folder '%s' when trying to attach a table creator to it" % attachToFolder)
2991 
2992     def clear(self):
2993         """Remove all plot folders and tables"""
2994         self._plots = []
2995 
2996     def getPlotFolderNames(self):
2997         return [item.getName() for item in self._plots]
2998 
2999     def getPlotFolders(self):
3000         return [item.getPlotFolder() for item in self._plots]
3001 
3002     def getPlotFolder(self, name):
3003         for item in self._plots:
3004             if item.getName() == name:
3005                 return item.getPlotFolder()
3006         raise Exception("No PlotFolder named '%s'" % name)
3007 
3008     def readDirs(self, *files):
3009         """Returns PlotterInstance object, which knows how exactly to produce the plots for these files"""
3010         return PlotterInstance([plotterItem.readDirs(files) for plotterItem in self._plots])