Skip to content

Commit 9946d91

Browse files
authored
The Ultimate Plotting Function (TUPF) (#681)
The Ultimate Plotting Function (TUPF) The function make_plot allows to produce a nice plot with a single command while supporting the usual object types and offering the flexibility to set most of the style aspects. Description Make a plot with objects from a list (list_obj). Returns a TCanvas and a list of other created ROOT objects. Minimum example: make_plot("canvas", list_obj=[histogram], path=".") To have access to the created object, do: canvas, list_can = make_plot("canvas", list_obj=[histogram]) Supports: - plotting of histograms (TH??), graphs (TGraph*), text fields (TLatex) and any other objects derived from TObject in any count and order - automatic calculation of plotting ranges (x, y) based on the data (histograms and graphs) - arbitrary x range - automatic style settings - optional plotting of the legend (enabled by providing the coordinates) - automatic adding of legend entries (in the plotting order) - logarithmic scale of x, y, z axes (logscale), (format: string containing any of x, y, z) - saving the canvas to a specified location (path) in a specified format (suffix) - access to created ROOT objects Adjustable parameters: - title and axis titles (title), (format: "title_plot;title_x;title_y") - plotting options for histograms and graphs (opt_plot_h, opt_plot_g), (format: see THistPainter and TGraphPainter, respectively) - legend position (leg_pos), (format: [x_min, y_min, x_max, y_max]) - labels of legend entries (labels_obj) - styles of legend entries (opt_leg_h, opt_leg_g), (format: see TLegend::AddEntry) - colours and markers (colours, markers), (format: list of numbers or named values) - canvas margins (margins_c), (format: [bottom, left, top, right]) - offsets of axis titles (offsets_xy), (format: [x, y]) - maximum number of digits of the axis labels (maxdigits) - x range (range_x), (format: [x_min, x_max]) - vertical margins between the horizontal axes and the data (margins_y), (format: [lower, upper] expressed as fractions of the total plotting range) - including the error bars in the range calculations (with_errors), (format: string containing any of x, y)
1 parent b79ed3a commit 9946d91

File tree

1 file changed

+282
-17
lines changed

1 file changed

+282
-17
lines changed

machine_learning_hep/utilities.py

Lines changed: 282 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
import pandas as pd
3131
import lz4
3232
from machine_learning_hep.selectionutils import select_runs
33-
from ROOT import TGraphAsymmErrors # pylint: disable=import-error, no-name-in-module
33+
from ROOT import TObject, TCanvas, TLegend, TH1, TLatex, TGraph, TGraphAsymmErrors # pylint: disable=import-error, no-name-in-module
3434
from ROOT import kBlack, kRed, kGreen, kBlue, kYellow, kOrange, kMagenta, kCyan, kGray # pylint: disable=import-error, no-name-in-module
3535
from ROOT import kOpenCircle, kOpenSquare, kOpenDiamond, kOpenCross, kOpenStar, kOpenThreeTriangles # pylint: disable=import-error, no-name-in-module
3636
from ROOT import kOpenFourTrianglesX, kOpenDoubleDiamond, kOpenFourTrianglesPlus, kOpenCrossX # pylint: disable=import-error, no-name-in-module
@@ -448,26 +448,67 @@ def get_plot_range(val_min, val_max, margin_min, margin_max, logscale=False):
448448
val_max_plot = val_max + k_max * val_range
449449
return val_min_plot, val_max_plot
450450

451-
def get_y_window_gr(l_gr: list):
452-
'''Return the minimum and maximum value so that all the points of the graphs in the list
453-
fit in the range including the error bars.'''
451+
def get_x_window_gr(l_gr: list, with_errors=True):
452+
'''Return the minimum and maximum x value so that all the points of the graphs in the list
453+
fit in the range (by default including the error bars).'''
454+
def err_low(graph):
455+
return graph.GetEXlow if isinstance(graph, TGraphAsymmErrors) else graph.GetEX
456+
def err_high(graph):
457+
return graph.GetEXhigh if isinstance(graph, TGraphAsymmErrors) else graph.GetEX
458+
454459
if not isinstance(l_gr, list):
455460
l_gr = [l_gr]
456-
y_min = min([min([(gr.GetY())[i] - (gr.GetEYlow())[i] \
457-
for i in range(gr.GetN())]) for gr in l_gr])
458-
y_max = max([max([(gr.GetY())[i] + (gr.GetEYhigh())[i] \
459-
for i in range(gr.GetN())]) for gr in l_gr])
461+
x_min = float("inf")
462+
x_max = float("-inf")
463+
for gr in l_gr:
464+
for i in range(gr.GetN()):
465+
x_min = min(x_min, (gr.GetX())[i] - ((err_low(gr)())[i] if with_errors else 0))
466+
x_max = max(x_max, (gr.GetX())[i] + ((err_high(gr)())[i] if with_errors else 0))
467+
return x_min, x_max
468+
469+
def get_x_window_his(l_his: list):
470+
'''Return the minimum and maximum x value so that all the bins of the histograms in the list
471+
fit in the range.'''
472+
if not isinstance(l_his, list):
473+
l_his = [l_his]
474+
x_min = float("inf")
475+
x_max = float("-inf")
476+
for his in l_his:
477+
x_min = min(x_min, his.GetXaxis().GetBinLowEdge(1))
478+
x_max = max(x_max, his.GetXaxis().GetBinUpEdge(his.GetNbinsX()))
479+
return x_min, x_max
480+
481+
def get_y_window_gr(l_gr: list, with_errors=True):
482+
'''Return the minimum and maximum y value so that all the points of the graphs in the list
483+
fit in the range (by default including the error bars).'''
484+
def err_low(graph):
485+
return graph.GetEYlow if isinstance(graph, TGraphAsymmErrors) else graph.GetEY
486+
def err_high(graph):
487+
return graph.GetEYhigh if isinstance(graph, TGraphAsymmErrors) else graph.GetEY
488+
489+
if not isinstance(l_gr, list):
490+
l_gr = [l_gr]
491+
y_min = float("inf")
492+
y_max = float("-inf")
493+
for gr in l_gr:
494+
for i in range(gr.GetN()):
495+
y_min = min(y_min, (gr.GetY())[i] - ((err_low(gr)())[i] if with_errors else 0))
496+
y_max = max(y_max, (gr.GetY())[i] + ((err_high(gr)())[i] if with_errors else 0))
460497
return y_min, y_max
461498

462-
def get_y_window_his(l_his: list):
463-
'''Return the minimum and maximum value so that all the points of the histograms in the list
464-
fit in the range including the error bars.'''
499+
def get_y_window_his(l_his: list, with_errors=True):
500+
'''Return the minimum and maximum y value so that all the points of the histograms in the list
501+
fit in the range (by default including the error bars).'''
465502
if not isinstance(l_his, list):
466503
l_his = [l_his]
467-
y_min = min([min([his.GetBinContent(i + 1) - his.GetBinError(i + 1) \
468-
for i in range(his.GetNbinsX())]) for his in l_his])
469-
y_max = max([max([his.GetBinContent(i + 1) + his.GetBinError(i + 1) \
470-
for i in range(his.GetNbinsX())]) for his in l_his])
504+
y_min = float("inf")
505+
y_max = float("-inf")
506+
for his in l_his:
507+
for i in range(his.GetNbinsX()):
508+
cont = his.GetBinContent(i + 1)
509+
err = his.GetBinError(i + 1) if with_errors else 0
510+
y_min = min(y_min, cont - err)
511+
y_max = max(y_max, cont + err)
471512
return y_min, y_max
472513

473514
def get_colour(i: int):
@@ -514,7 +555,7 @@ def setup_legend(legend, textsize=0.03):
514555
legend.SetTextSize(textsize)
515556
legend.SetTextFont(42)
516557

517-
def setup_tgraph(tg_, colour=1, alphastyle=0.3, fillstyle=1001):
558+
def setup_tgraph(tg_, colour=1, markerstyle=kOpenCircle, size=1.5, alphastyle=0.3, fillstyle=1001):
518559
tg_.GetXaxis().SetTitleSize(0.04)
519560
tg_.GetXaxis().SetTitleOffset(1.0)
520561
tg_.GetYaxis().SetTitleSize(0.04)
@@ -523,7 +564,9 @@ def setup_tgraph(tg_, colour=1, alphastyle=0.3, fillstyle=1001):
523564
tg_.SetLineWidth(2)
524565
tg_.SetLineColor(colour)
525566
tg_.SetFillStyle(fillstyle)
526-
tg_.SetMarkerSize(0)
567+
tg_.SetMarkerSize(size)
568+
tg_.SetMarkerStyle(markerstyle)
569+
tg_.SetMarkerColor(colour)
527570

528571
def draw_latex(latex, colour=1, textsize=0.03):
529572
latex.SetNDC()
@@ -532,6 +575,228 @@ def draw_latex(latex, colour=1, textsize=0.03):
532575
latex.SetTextFont(42)
533576
latex.Draw()
534577

578+
def make_plot(name, path=None, suffix="eps", title="", size=None, margins_c=None, # pylint: disable=too-many-arguments, too-many-branches, too-many-statements, too-many-locals
579+
list_obj=None, labels_obj=None,
580+
leg_pos=None, opt_leg_h="P", opt_leg_g="P", opt_plot_h="", opt_plot_g="P0",
581+
offsets_xy=None, maxdigits=3, colours=None, markers=None,
582+
range_x=None, margins_y=None, with_errors="xy", logscale=None):
583+
"""
584+
Make a plot with objects from a list (list_obj).
585+
Returns a TCanvas and a list of other created ROOT objects.
586+
Minimum example:
587+
make_plot("canvas", list_obj=[histogram], path=".")
588+
To have access to the created object, do:
589+
canvas, list_can = make_plot("canvas", list_obj=[histogram])
590+
Features:
591+
- plotting of histograms (TH??), graphs (TGraph*), text fields (TLatex) and any other objects
592+
derived from TObject in any count and order
593+
- automatic calculation of plotting ranges (x, y) based on the data (histograms and graphs)
594+
- arbitrary x range
595+
- automatic style settings
596+
- optional plotting of the legend (enabled by providing the coordinates)
597+
- automatic adding of legend entries (in the plotting order)
598+
- logarithmic scale of x, y, z axes (logscale), (format: string containing any of x, y, z)
599+
- saving the canvas to a specified location (path) in a specified format (suffix)
600+
- access to created ROOT objects
601+
Adjustable parameters:
602+
- title and axis titles (title), (format: "title_plot;title_x;title_y")
603+
- canvas size (size), (format: [width, height])
604+
- plotting options for histograms and graphs (opt_plot_h, opt_plot_g),
605+
(format: see THistPainter and TGraphPainter, respectively)
606+
- legend position (leg_pos), (format: [x_min, y_min, x_max, y_max])
607+
- labels of legend entries (labels_obj)
608+
- styles of legend entries (opt_leg_h, opt_leg_g), (format: see TLegend::AddEntry)
609+
- colours and markers (colours, markers), (format: list of numbers or named values)
610+
- canvas margins (margins_c), (format: [bottom, left, top, right])
611+
- offsets of axis titles (offsets_xy), (format: [x, y])
612+
- maximum number of digits of the axis labels (maxdigits)
613+
- x range (range_x), (format: [x_min, x_max])
614+
- vertical margins between the horizontal axes and the data (margins_y), (format: [lower, upper]
615+
expressed as fractions of the total plotting range)
616+
- including the error bars in the range calculations (with_errors),
617+
(format: string containing any of x, y)
618+
"""
619+
620+
# HELPING FUNCTIONS
621+
622+
def min0_gr(graph):
623+
""" Get the minimum positive y value in the graph. """
624+
list_pos = [y for y in graph.GetY() if y > 0]
625+
return min(list_pos) if list_pos else float("inf")
626+
627+
def get_my_colour(i: int):
628+
if colours and isinstance(colours, list) and len(colours) > 0:
629+
return colours[i % len(colours)]
630+
return get_colour(i)
631+
632+
def get_my_marker(i: int):
633+
if markers and isinstance(markers, list) and len(markers) > 0:
634+
return markers[i % len(markers)]
635+
return get_marker(i)
636+
637+
def plot_graph(graph):
638+
setup_tgraph(graph, get_my_colour(counter_plot), get_my_marker(counter_plot))
639+
graph.SetTitle(title)
640+
graph.GetXaxis().SetLimits(x_min_plot, x_max_plot)
641+
graph.GetYaxis().SetRangeUser(y_min_plot, y_max_plot)
642+
graph.GetXaxis().SetMaxDigits(maxdigits)
643+
graph.GetYaxis().SetMaxDigits(maxdigits)
644+
if offsets_xy:
645+
graph.GetXaxis().SetTitleOffset(offsets_xy[0])
646+
graph.GetYaxis().SetTitleOffset(offsets_xy[1])
647+
if leg and n_labels > counter_plot:
648+
leg.AddEntry(graph, labels_obj[counter_plot], opt_leg_g)
649+
graph.Draw(opt_plot_g + "A" if counter_plot == 0 else opt_plot_g)
650+
651+
def plot_histogram(histogram):
652+
# If nothing has been plotted yet, plot an empty graph to set the exact ranges.
653+
if counter_plot == 0:
654+
gr = TGraph(histogram)
655+
gr.SetMarkerSize(0)
656+
gr.SetTitle(title)
657+
gr.GetXaxis().SetLimits(x_min_plot, x_max_plot)
658+
gr.GetYaxis().SetRangeUser(y_min_plot, y_max_plot)
659+
gr.GetXaxis().SetMaxDigits(maxdigits)
660+
gr.GetYaxis().SetMaxDigits(maxdigits)
661+
if offsets_xy:
662+
gr.GetXaxis().SetTitleOffset(offsets_xy[0])
663+
gr.GetYaxis().SetTitleOffset(offsets_xy[1])
664+
gr.Draw("AP")
665+
list_new.append(gr)
666+
setup_histogram(histogram, get_my_colour(counter_plot), get_my_marker(counter_plot))
667+
if leg and n_labels > counter_plot:
668+
leg.AddEntry(histogram, labels_obj[counter_plot], opt_leg_h)
669+
histogram.Draw(opt_plot_h)
670+
671+
def plot_latex(latex):
672+
draw_latex(latex)
673+
674+
def is_histogram(obj):
675+
return isinstance(obj, TH1)
676+
677+
def is_graph(obj):
678+
return isinstance(obj, TGraph)
679+
680+
def is_latex(obj):
681+
return isinstance(obj, TLatex)
682+
683+
# BODY STARTS HERE
684+
685+
if not (isinstance(list_obj, list) and len(list_obj) > 0):
686+
print("Error: Empty list of objects")
687+
return None, None
688+
689+
list_new = [] # list of created objects that need to exist outside the function
690+
if not (isinstance(offsets_xy, list) and len(offsets_xy) == 2):
691+
offsets_xy = None
692+
if not isinstance(labels_obj, list):
693+
labels_obj = []
694+
n_labels = len(labels_obj)
695+
if margins_y is None:
696+
margins_y = [0.05, 0.05]
697+
698+
# create and set canvas
699+
can = TCanvas(name, name)
700+
setup_canvas(can)
701+
if isinstance(size, list) and len(size) == 2:
702+
can.SetCanvasSize(*size)
703+
# set canvas margins
704+
if isinstance(margins_c, list) and len(margins_c) > 0:
705+
for setter, value in zip([can.SetBottomMargin, can.SetLeftMargin,
706+
can.SetTopMargin, can.SetRightMargin], margins_c):
707+
setter(value)
708+
# set logarithmic scale for selected axes
709+
log_y = False
710+
if isinstance(logscale, str) and len(logscale) > 0:
711+
for setter, axis in zip([can.SetLogx, can.SetLogy, can.SetLogz], ["x", "y", "z"]):
712+
if axis in logscale:
713+
setter()
714+
if axis == "y":
715+
log_y = True
716+
717+
# create and set legend
718+
leg = None
719+
if isinstance(leg_pos, list) and len(leg_pos) == 4:
720+
leg = TLegend(*leg_pos)
721+
setup_legend(leg)
722+
list_new.append(leg)
723+
724+
# range calculation
725+
list_h = [] # list of histograms
726+
list_g = [] # list of graphs
727+
for obj in list_obj:
728+
if is_histogram(obj):
729+
list_h.append(obj)
730+
elif is_graph(obj):
731+
list_g.append(obj)
732+
# get x range of histograms
733+
x_min_h, x_max_h = float("inf"), float("-inf")
734+
if len(list_h) > 0:
735+
x_min_h, x_max_h = get_x_window_his(list_h)
736+
# get x range of graphs
737+
x_min_g, x_max_g = float("inf"), float("-inf")
738+
if len(list_g) > 0:
739+
x_min_g, x_max_g = get_x_window_gr(list_g, "x" in with_errors)
740+
# get total x range
741+
x_min = min(x_min_h, x_min_g)
742+
x_max = max(x_max_h, x_max_g)
743+
# get plotting x range
744+
x_min_plot, x_max_plot = x_min, x_max
745+
if isinstance(range_x, list) and len(range_x) == 2:
746+
x_min_plot, x_max_plot = range_x
747+
748+
# get y range of histograms
749+
y_min_h, y_max_h = float("inf"), float("-inf")
750+
if len(list_h) > 0:
751+
y_min_h, y_max_h = get_y_window_his(list_h, "y" in with_errors)
752+
if log_y and y_min_h <= 0:
753+
y_min_h = min([h.GetMinimum(0) for h in list_h])
754+
# get y range of graphs
755+
y_min_g, y_max_g = float("inf"), float("-inf")
756+
if len(list_g) > 0:
757+
y_min_g, y_max_g = get_y_window_gr(list_g, "y" in with_errors)
758+
if log_y and y_min_g <= 0:
759+
y_min_g = min([min0_gr(g) for g in list_g])
760+
# get total y range
761+
y_min = min(y_min_h, y_min_g)
762+
y_max = max(y_max_h, y_max_g)
763+
# get plotting y range
764+
y_min_plot, y_max_plot = y_min, y_max
765+
if isinstance(margins_y, list) and len(margins_y) == 2:
766+
y_min_plot, y_max_plot = get_plot_range(y_min, y_max, *margins_y, log_y)
767+
768+
# append "same" to the histogram plotting option if needed
769+
opt_plot_h = opt_plot_h.lower()
770+
opt_not_in = all(opt not in opt_plot_h for opt in ("same", "lego", "surf"))
771+
if opt_not_in:
772+
opt_plot_h += " same"
773+
774+
# plot objects
775+
counter_plot = 0 # counter of plotted histograms and graphs
776+
for obj in list_obj:
777+
if is_histogram(obj):
778+
plot_histogram(obj)
779+
counter_plot += 1
780+
elif is_graph(obj):
781+
plot_graph(obj)
782+
counter_plot += 1
783+
elif is_latex(obj):
784+
plot_latex(obj)
785+
elif isinstance(obj, TObject):
786+
obj.Draw()
787+
else:
788+
continue
789+
790+
# plot legend
791+
if leg:
792+
leg.Draw()
793+
794+
# save canvas if necessary info provided
795+
if path and name and suffix:
796+
can.SaveAs("%s/%s.%s" % (path, name, suffix))
797+
798+
return can, list_new
799+
535800
def tg_sys(central, variations):
536801
shapebins_centres = []
537802
shapebins_contents = []

0 commit comments

Comments
 (0)