-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathplot.py
More file actions
executable file
·3603 lines (3075 loc) · 138 KB
/
plot.py
File metadata and controls
executable file
·3603 lines (3075 loc) · 138 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
'''
@file plot.py
@author Riley Xu - [email protected], [email protected]
@date June 22, 2021
@brief Declarative ROOT plotting tools
@requires Python >= 3.6 (setupATLAS && lsetup "root recommended" && lsetup "python centos7-3.9")
Riley's declarative ROOT plotting toolkit. Instead of manually adjusting each histogram,
you can specify style in aggregate. Typical usage would look like:
import plot
plot.plot([h1, h2], # plots two histograms in the same canvas
opts = ['HIST', 'PE'], # plot h1 in 'HIST' format and h2 in 'PE' format
linecolor = plot.colors.tableu, # sets each histogram's line color using a MPL colormap
linewidth = 3, # applied to both histograms
subtitle = '#sqrt{s} = 13 TeV^{-1}', # text options can accept TLatex formatters
legend = ['Hist 1', 'Hist 2'],
ytitle = 'Events',
xtitle = 'm_{T}(W) [GeV]',
filename = 'my_plot.png',
)
Please see the `examples/` folder for more examples!
The input also doesn't have to be all histograms, you can include TF1s and TGraphs too.
In addition, this script takes care of several things missing in ROOT: automatic axis
ranges, easier legend placement, title text and subtext, etc. It also defines helper
functions to make some common plot types, like ratio plots. A full list of top-level
functions is shown below, along with a list of common options they accept.
The [colors] class defines many useful colors and some Matplotlib colormaps, which can be
accessed easily as `plot.colors.tableu(0)`. See the docstring for the class for more info.
The format of the saved image is inferred by ROOT based on the extension in the filename.
If the extension is omitted, the canvas will be saved for each extension listed in
[plot.file_formats]. This is convenient to save a plot as both a pdf and a png, for
example.
-----------------------------------------------------------------------------------------
TOP-LEVEL PLOTTING FUNCTIONS
-----------------------------------------------------------------------------------------
Most of these functions expect one or more lists of TObjects as the graph inputs, and
accept a variety of common options listed below. See the individual docstring for more
info.
-----------------------------------------------------------------------------------------
plot
The basic go-to plotting function. Plots everything onto the same axis.
plot_ratio
Plots a ratio plot; a main plot is shown on top with a separate, smaller plot shown
on bottom. This function doesn't calcualte any actual ratios, you pass it instead two
lists of TObjects.
plot_ratio3
Similar to [plot_ratio] but with two subplots beneath a main plot.
plot_two_scale
Plots two y-axes on a shared x-axis. TODO might be a bit outdated.
plot_tiered
Similar to a violin plot. Plots each input histogram at separated y-values. Useful
for eye-balling differences between many different distributions, which would get
crowded on a standard plot.
plot_discrete_bins
Plots histograms in discrete x bins, so histograms appear side by side (like a bar
chart). Useful when histograms are very similar, and would overlap otherwise.
Plotter
The underlying plotting class used by everything above. Useful for creating
custom images containing multiple canvases, or more complicated series plots.
For example, if you want to plot a stack of histograms with some overlaid info, like
data points and statistical errors, it's much easier to use the [Plotter] class
directly. See `examples/stack_plot.py` for an example.
-----------------------------------------------------------------------------------------
COMMON PLOTTING OPTIONS
-----------------------------------------------------------------------------------------
BASIC STYLE
--------------------------------------------------------
All of these options can be specified as a single value to apply to all input TObjects, a
list of values matching each input TObject, or a function that takes the index into the
list of TObjects.
--------------------------------------------------------
opts
Options to TObject::Draw(), such as 'HIST' or 'PE'. Note that you don't need to
specify 'SAME' or 'A', unless you're calling _plot on the same pad more than once.
For TH2s, you can also input things like 'TEXT:4.2f' to specify a printf formatter
when drawing with 'TEXT'.
line<color/style/width> default: 'auto'
Sets the corresponding TAttLine properties. See the ROOT documentation for input
values. Set to None to do nothing. In the auto mode, linecolor will be set to the
colors.tableu colormap, as long as all there are multiple objects and they are all
black. Otherwise, the default is None.
marker<color/style/size> default: 'auto'
Sets the corresponding TAttMarker properties. See the ROOT documentation for input
values. Set to None to do nothing. In the auto mode, markercolor will be set to the
colors.tableu colormap, as long as all there are multiple objects and they are all
black. Otherwise, the default is None.
fill<color/style> default: None
Sets the corresponding TAttFill properties. See the ROOT documentation for input
values. Set to None to do nothing.
TEXT AND FILE NAMES
--------------------------------------------------------
filename
Path to save the image to. If the filename ends with an extension like '.png', the
file will be saved in that format. If no extension is given, saves the file with
each extension in [plot.file_formats].
text_pos default: 'auto'
Location of title / legend.
Can be a combination of (top/bottom) and/or (left/right),
so for example 'top' will place the title in the top-left corner and the legend in
the top-right, while 'top left' will place both in the top-left corner. You can also
specify 'forward diagonal' or 'backward diagonal' to place the title and legend in
diagonally opposite corners. You can add 'reverse' to some of these to reverse the
title and legend positions.
Set to 'auto' to automatically choose the best option (the one that requires the
least amount of whitespace), or a list of the options above to only do the optimization
over specific options.
title default: 'ATLAS Internal'
Title text to display in the plot. Any instance of "ATLAS" will be replaced by the
approriate bold, italic string.
subtitle
Additional text that is displayed below the title. This can be a string or a
list of strings, with the latter putting each entry on a new line.
title_size default: 0.05
ROOT text size for the title.
text_size default: 0.035
ROOT text size for non-title text, including subtitle and legend.
WARNING ROOT has a bug with measuring text that isn't at some golden sizes. It seems
0.05 and 0.035 work well. This may cause right aligning to be broken; it seems the
longer the text the more off it'll be.
text_spacing default: 1.0
Multiplicative factor for modifying the spacing between title/text/legend.
text_offset_<left/right/bottom/top> default: 0.05
Offset of text elements (titles/legend) from the axes, in pad units. These are only
used for relevant [text_pos] options.
PAD/CANVAS
-----------------------------------------------------
<left/right/bottom/top>_margin
Sets the pad margins.
AXES
-----------------------------------------------------
log<x/y/z>
Sets the canvas to use log x/y/z axes.
<x/y/z>title
Titles for the x/y/z axes.
<x/y/z>divs
See TAxis::SetNdivisions. Sets the number of ticks on each axis.
<x/y/z>_range default: (None, None)
Specify a list or a tuple of the (min, max) range of the axis to plot. You can set
either entry to None to automatically fit plot contents. Set the entire argument to
None to use default ROOT behavior.
y_pad_bot/top default: 0.05
If using an automatic y-axis range, minimum amount of padding at the bottom/top so that
the data points don't crowd the edges. Also useful to make room for titles and legends.
The value is in axis coordinates, so a value of 0.05 on both bottom and top makes the
data only appear (at most) in the center 90% of the axes.
The automatic text placement procedure may increase the actual amount of padding used
to fit the title text without overlapping the data.
y_min/max default: None
If using an automatic y-axis range, clamp the range to within the specified values.
Set to `None` to disable.
y_text_data_spacing default: 0.02
If using an automatic y-axis range, minimum amount of vertical padding between text
elements (titles and legend) and data points. Value is in pad coordinates.
x_pad_left/right default: 0
If using an automatic x-axis range, amount of padding at the left/right so that the
data points don't crowd the edges. The value is in axes coordinates, so a value of
0.1 on both makes the data only appear in the center 80% of the plot.
ignore_outliers_y default: 0
If using an automatic y-axis range, will ignore points when calculating the min/max
bounds if they're more than this many standard deviations away from the mean. Set to
0 to disable
title_offset_<x/y/z>
ROOT TGaxis title offset.
x_bin_labels default: None
Specify a list of bin labels for the x axis. Should match the number of x bins.
x_labels_option default: None
Set a ROOT TAxis.LabelsOption() option.
https://root.cern.ch/doc/master/classTAxis.html#a05dd3c5b4c3a1e32213544e35a33597c
LEGEND
-----------------------------------------------------
legend default: 'auto'
Specification for the legend. Several different modes are available:
1) None
No legend is created.
2) [label]
Supply a list of string labels for each object. An empty string will omit
the respective entry from the legend.
3) 'auto'
Auto creates the legend using the object ROOT names as labels.
4) [(obj, label, opt)]
Manually set each legend entry using a ROOT.TObject as the formatter, a
string label, and the icon draw option.
For options (2) and (3), the icon is drawn using the option(s) specified by
[legend_opts], and the order of the legend can be manipulated by [legend_order].
legend_order default: None
Reorders and trims the legend. Input a list of indexes into the list in [legend], so
for example [3, 0, 1] will place the 4th entry first and omit the 3rd.
legend_opts default: context-dependent
A list matching the legend labels that changes how the symbol is drawn. Can be any
mix of the letters 'PEFL' for point, error bars, fill, line. An empty string will omit
the respective entry from the legend.
legend_columns default: 1
Number of columns to split the legend across.
legend_vertical_order default: False
When [legend_columns] > 1, the entries will go left to right by default. Set this
option to true to go top to bottom instead.
OTHER
-----------------------------------------------------
stack default: False
Plots a list of histograms as a stack. The histograms are added cumulatively to create
the stack, with the first histogram being the bottom of the stack. The legend is
reversed to match the stack order visually.
Note that this option only works with TH1s. See also `examples/stack_plot.py` for
adding extra plot items outside of the stack.
-----------------------------------------------------------------------------------------
COLORS
-----------------------------------------------------------------------------------------
See the color class docstring for more info, and the class implementation for a full list
of colors.
-----------------------------------------------------------------------------------------
Colormaps:
colors.tableu: Matplotlib 'tableau' colormap
colors.pastel: Matplotlib 'Pastel1' colormap
-----------------------------------------------------------------------------------------
UTILITY FUNCTIONS
-----------------------------------------------------------------------------------------
See each function's docstring for more info. There are many more than listed here too.
-----------------------------------------------------------------------------------------
HISTOGRAM MANIPULATION
-----------------------------------------------------
normalize
Normalizes a histogram in multiple ways, such as forcing it to have unit area, or
convert it into a cumulative distribution.
rebin
An easier histogram rebinning function that can handle various modes, like variable
bins or finding a user-width binning.
rebin2d
Rebins a 2D histogram with variable bins on each axis.
integral_user
Calculates the integral of a histogram using a user-coordinate range instead of a
bin range.
undo_width_scaling
Undoes the scaling from h.Scale(1, 'width')
IterRoot
Turns TH1 and TGraphs into iterators. Useful for defining generic functions that can
operate on either.
OTHER
-----------------------------------------------------
colors_from_palette
Returns a list of equally spaced colors from a ROOT palette.
save_canvas_transparent
Saves a transparent canvas instead of the default white background. Set the global
variable [plot.save_transparent] to True to enable in the plot functions above.
format
Automatically formats a list of TObjects.
'''
from __future__ import annotations
import ROOT # type: ignore
import itertools
import math
import ctypes
import numpy as np
import os
import sys
import bisect
ROOT.gROOT.SetBatch(ROOT.kTRUE)
ROOT.gROOT.SetStyle("ATLAS")
ROOT.gROOT.ForceStyle()
ROOT.TGaxis.SetMaxDigits(4) # Number of digis to show on an axis, above which exponential notation is used
file_formats = ['png', 'pdf']
save_transparent_png = False
##############################################################################
### PLOTTING ###
##############################################################################
class Plotter:
'''
Main plotting class. Usually you should not bother with this class itself
and use the wrapper functions instead.
The main workflow is:
1. __init__()
Sets the pad and title text configuration. Most pad-level configuration options
should be supplied here.
2. add()
Adds a list of ROOT objects to the draw stack, with formatting and legend. This
function can be called repeatedly. All style options should be passed here.
3. compile()
Processes object-dependent configuration, such as auto ranging, text placement,
and creating the legend object. Note that many properties such as [y_range] are
not set until [compile] is called.
3. draw()
Draws all objects in the draw stack.
An optional shortcut is to supply the [objs] arguement to __init__(), which will call
the other steps automatically.
See this file's docstring for configuration options, that can be passed in kwargs to
the various functions.
-------------------------------- Implementation Details --------------------------------
Much of the confusion in ROOT plotting is caused by the coupling of axes to individual
histograms. This leads to such confusing things as needing to call "SAME" in the draw
options, or errors such as
h1.Draw()
h2.Draw('SAME')
h2.GetXaxis().SetTitle('this will not show up!')
because the axes being drawn are from h1, not h2. This class remedies this problem by
defining a dedicated "frame" histogram, that is always drawn first using the 'AXIS'
option. This also solves the annoying problem that TGraphs behave differently from TH1s,
especially regarding axis limits.
The frame histogram is not created until the call to [compile], which first processes
all objects input with [add] to calculate things like automatic axis ranges, etc. Note
that you normally don't have to call [compile] yourself, since it is called in [draw].
However, sometimes it useful to create the frame histogram before drawing. For example,
if you want to retrieve the axis limits before initiating the draw step. You can also
supply your own frame histogram by using the [_frame] option in [__init__].
--------------------------------- Object Properties ---------------------------------
These properties are empty initially and extended by calls to [add].
@property objs : [TObject]
Main list of ROOT objects which dictate algorithms like auto axis ranges and text
placement.
@property draw_objs : [TObject]
This is a superset of [objs] that may also contain things like TMarker added via
[add_primitives]. The order of this list is the draw order of the objects (which
is therefore also the z-ordering of the objects). Note that if you modify this
list, make sure to also modify [draw_opts].
@property draw_opts : [str]
A list of plotting options in parallel with [draw_objs], passed to the ROOT
TObject.Draw() function.
@property legend_items : [(TObject, str label, str draw_option)]
A list of ROOT TLegend entry parameters, in the order they appear. You can modify
it manually, for example to add custom legend entries.
-------------------------------- Compiled Properties --------------------------------
These properties only exist after calling [compile].
@property <x/y/z>_range : (float min, float max) or None
The axis limits in user coordinates. These may be None, in which case default
ROOT behavior is used (you will have to retrieve the ranges yourself via ROOT).
@property frame : TH1F or TObject
The frame histogram as mentioned above. Generally this will be a custom created
TH1F, unless you pass a [_frame] to [__init__]. Also, if plotting a TH2 or one
of the ranges above is None, will be the first object in [objs] instead.
@property legends : [TLegend]
A list of all the legends. Note that this class generates one TLegend per legend
entry to have fine-grained control over the entry placement.
@property legend_<width/height/rows/columns>
@property data_y_<min/max/pos> : float or None
The min/max/min-positive value of the data in [objs].
-------------------------------- Internal Properties --------------------------------
@property text_<...>
@property title_<...>
@property _y_min_pad_<bot/top> : float
The axis-units padding supplied by the user.
@property _y_pad_<bot/top> : float
A mutable member used to calculate the actual padding needed, which may be
increased from the minimum specified above.
@property auto_y_<bot/top> : bool
Whether the yrange is fixed or can be adjusted.
'''
def __init__(self, pad,
objs=None,
y_pad=None, y_pad_bot=0.05, y_pad_top=0.05,
_do_draw=True, _frame=None,
**kwargs
):
'''
See file docstring for [kwarg] options.
@param _do_draw
Skip draw if false. Only applies when [objs] is not None.
@param _frame
Supply a custom frame object.
'''
### Initialize ###
self.frame = _frame
self.objs = []
self.draw_objs = []
self.draw_opts = []
self.legend_items = []
self.cache = []
self.compiled = False # If False, need to call compile()
self.is_2d = False
### Pad ###
self.pad = pad
self._set_pad_properties(**kwargs)
### Titles ###
self._set_text_properties(**kwargs)
self._make_titles(**kwargs)
### Axis ###
if y_pad is not None:
self._y_min_pad_bot = y_pad
self._y_min_pad_top = y_pad
else:
self._y_min_pad_bot = y_pad_bot
self._y_min_pad_top = y_pad_top
self._y_pad_bot = self._y_min_pad_bot
self._y_pad_top = self._y_min_pad_top
### Other ###
self.args = kwargs # This is a cache of kwargs that we pass to [compile] and [draw] so that you can specify all the relevant args up front
### Draw ###
# Shorcut: if objs is supplied to the constructor, draw immediately.
if objs:
self.add(objs, **kwargs)
self.compile(**kwargs)
if _do_draw:
self.draw()
#####################################################################################
### PAD AND FRAME ###
#####################################################################################
def _set_frame_ranges(self):
if self.x_range is not None:
if 'TGraph' in self.frame.ClassName():
self.frame.GetXaxis().SetLimits(*self.x_range)
else:
self.frame.GetXaxis().SetRangeUser(*self.x_range)
if self.y_range is not None:
self.frame.GetYaxis().SetRangeUser(*self.y_range)
if self.z_range is not None:
self.frame.GetZaxis().SetRangeUser(*self.z_range)
def _create_frame(self, **kwargs):
if self.x_range and self.y_range and not self.is_2d:
if labels := kwargs.get('x_bin_labels'):
for h in self.objs:
if 'TGraph' not in h.ClassName() and h.GetXaxis().GetNbins() == len(labels):
self.frame = h.Clone()
break
else:
self.frame = ROOT.TH1F('h_frame', '', len(labels), *self.x_range)
self.frame.SetDirectory(0)
else:
self.frame = ROOT.TH1F('h_frame', '', 1, *self.x_range)
self.frame.SetDirectory(0)
else:
# use objs[0] as the frame to preserve default ROOT behavior
# Note Z axis settings MUST be applied on the histogram drawn with 'Z' option, don't clone!
self.frame = self.objs[0]
def _set_pad_properties(self, logx=None, logy=None, logz=None, left_margin=None, right_margin=None, bottom_margin=None, top_margin=None, **_):
self.logy = logy
self.pad.cd()
if logx is not None: self.pad.SetLogx(logx)
if logy is not None: self.pad.SetLogy(logy)
if logz is not None: self.pad.SetLogz(logz)
if bottom_margin is not None: self.pad.SetBottomMargin(bottom_margin)
if top_margin is not None: self.pad.SetTopMargin(top_margin)
if left_margin is not None: self.pad.SetLeftMargin(left_margin)
if right_margin is None:
self.auto_right_margin = True
else:
self.auto_right_margin = False
self.pad.SetRightMargin(right_margin)
def _auto_right_margin(self):
'''
This needs to be called after objects have been added, so we can test against
the 'COLZ' option.
'''
if not self.auto_right_margin: return
if not self.is_2d: return
if not 'Z' in self.draw_opts[0]: return
if self.frame.GetZaxis().GetTitle():
self.pad.SetRightMargin(0.18)
else:
self.pad.SetRightMargin(0.15)
def user_to_axes_x(self, x):
return user_to_axes_x(self.pad, self.frame, x)
def user_to_axes_y(self, y):
return user_to_axes_y(self.pad, self.frame, y)
def user_to_axes(self, x, y):
return user_to_axes(self.pad, self.frame, (x, y))
def user_to_pad(self, x, y):
return user_to_pad(self.pad, self.frame, (x, y))
def pad_to_axes_x(self, x):
return pad_to_axes_x(self.pad, self.frame, x)
def pad_to_axes_y(self, y):
return pad_to_axes_y(self.pad, self.frame, y)
def pad_to_axes_height(self, height):
return pad_to_axes_height(self.pad, self.frame, height)
def pad_to_axes(self, x, y):
return pad_to_axes(self.pad, self.frame, (x, y))
def axes_to_pad_x(self, x):
return axes_to_pad_x(self.pad, self.frame, x)
def axes_to_pad_y(self, y):
return axes_to_pad_y(self.pad, self.frame, y)
def reset_pad(self, pad):
self.compiled = False
self.pad = pad
self._set_pad_properties(**self.args)
self._set_text_properties(**self.args)
self._make_titles(**self.args)
#####################################################################################
### RANGES ###
#####################################################################################
def _auto_x_range(self, **kwargs):
if self.is_2d: return kwargs.get('x_range')
return _auto_x_range(objs=self.objs, **kwargs)
def _get_padded_range(self, data_min, data_max, pad_bot, pad_top):
'''
Returns the user-coordinate range given the data min/max (also user coords) and
the amount of padding. Here the padding is given in canvas or axis units.
'''
data_height = 1.0 - pad_bot - pad_top
diff = data_max - data_min
out_min = data_min - pad_bot * diff / data_height
out_max = data_max + pad_top * diff / data_height
return out_min, out_max
def _pad_y_range(self, y_min=None, y_max=None, **kwargs):
'''
If using auto axis limits, updates [self.y_range] with extra padding defined by
[self._y_pad_bot/top]. Respects the threholds [y_min] and [y_max] which take
priority. Also resets the limits on [self.frame].
'''
if self.is_2d: return None
if not self.auto_y_bot and not self.auto_y_top: return
### Data units ###
data_min, data_max = self.y_range
if data_min >= 0 and not self.pad.GetLogy():
if y_min is None or y_min < 0: y_min = 0
# Here we enforce that y_min >= 0 when the data is all positive
### Adjust for log ###
if self.logy:
data_min = np.log10(data_min)
data_max = np.log10(data_max)
if y_min is not None:
y_min = np.log10(y_min) if y_min > 0 else None
if y_max is not None:
if y_max <= 0: warning(f'Plotter._pad_y_range() passed a negative y_max={y_max} but plot is in logy mode')
y_max = np.log10(y_max) if y_max > 0 else None
### First pass padding ###
pad_bot = self._y_pad_bot if self.auto_y_bot else 0
pad_top = self._y_pad_top if self.auto_y_top else 0
out_min, out_max = self._get_padded_range(data_min, data_max, pad_bot, pad_top)
### Apply constraints ###
rerun = False
if y_min is not None and out_min < y_min:
data_min = y_min
pad_bot = 0
rerun = True
if y_max is not None and out_max > y_max:
data_max = y_max
pad_top = 0
rerun = True
if rerun:
out_min, out_max = self._get_padded_range(data_min, data_max, pad_bot, pad_top)
### Undo log, fix ticks ###
if self.logy:
out_min = np.power(10, out_min)
out_max = np.power(10, out_max)
else:
out_min, out_max = _fix_bad_yticks(out_min, out_max, pad_bot == 0, pad_top == 0, ydivs=kwargs.get('ydivs'))
### Output ###
self.y_range = (out_min, out_max)
if self.frame:
self.frame.GetYaxis().SetRangeUser(*self.y_range)
def _auto_y_range(self, y_range='auto', y_min=None, y_max=None, ignore_outliers_y=0, **_):
'''
This function
- parses [y_range] to determine the auto behavior
- sets the members listed below
- returns (min, max) to be used as the base y range. It does not do any
padding, but does respect [y_min] and [y_max].
@sets
self.data_y_[min/max/pos]
self.auto_y_[bot/top]
'''
self.data_y_min = None
self.data_y_max = None
self.data_y_pos = None
self.auto_y_bot = False
self.auto_y_top = False
### No auto range ###
if y_range is None: return None
if self.is_2d:
if y_range != 'auto': return y_range
else: return None
### Set tracking members ###
if y_range == 'auto': y_range = (None, None)
self.auto_y_bot = y_range[0] is None
self.auto_y_top = y_range[1] is None
if not self.auto_y_bot and not self.auto_y_top: # short circuit
return y_range
### Get data min/max ###
self.data_y_min, self.data_y_pos, self.data_y_max = get_minmax_y(self.objs, x_range=self.x_range, ignore_outliers_y=ignore_outliers_y)
if self.data_y_min is None or self.data_y_max is None or (self.data_y_pos is None and self.pad.GetLogy()):
self.auto_y_bot = False
self.auto_y_top = False
return None
### Output ###
# No padding here! See _pad_y_range
out_min = self.data_y_pos if self.pad.GetLogy() else self.data_y_min
out_max = self.data_y_max
if not self.auto_y_bot:
out_min = y_range[0]
elif y_min is not None and out_min < y_min:
out_min = y_min
self.auto_y_bot = False
if not self.auto_y_top:
out_max = y_range[1]
elif y_max is not None and out_max > y_max:
out_max = y_max
self.auto_y_top = False
return out_min, out_max
def _auto_z_range(self, z_range=None, **kwargs):
if z_range is None: return None
if z_range[0] is not None and z_range[1] is not None: return z_range
min_val = None
max_val = None
for obj in self.objs:
if 'TH2' in obj.ClassName():
for y in range(1, obj.GetNbinsY() + 1):
for x in range(1, obj.GetNbinsX() + 1):
v = obj.GetBinContent(x, y)
if v == 0: continue
if min_val is None or v < min_val: min_val = v
if max_val is None or v > max_val: max_val = v
if z_range[0] is not None: min_val = z_range[0]
if z_range[1] is not None: max_val = z_range[1]
return (min_val, max_val)
#####################################################################################
### MAIN PROCESSING ###
#####################################################################################
def add(self, objs,
opts='', pos=None, legend_pos=None,
stack=False, reverse_legend_for_stack=True,
**kwargs):
'''
Adds a list of objects to the plotter.
@param opts
The ROOT drawing option for each object. Like the formatting options, this
can be a single string, applied to each of [objs], a list in parallel with
[objs], or a function that takes an index into [objs] and returns the string
option.
@param pos
By default, objects from subsequent calls of [add] are plotted on top of
current objects. Set [pos] to an index in the list [draw_objs] to alter
the z-order of the drawing.
@param legend_pos
Index of where to insert the legend entries for these [objs]. By default
will append them to the end.
@param stack
If True, [objs] must be a list of TH1s that will be added into a stack. No
other objects should be included.
@param reverse_legend_for_stack
By default, if [stack], the legend will be reversed, which matches the visual
order of the histograms in the plot. Supplying [legend_order] will override
this option.
@param kwargs
Options for [_apply_common_opts] and [_get_legend_list].
@modifies
self.objs
self.draw_objs
self.draw_opts
self.legend_items
'''
if not objs: return
if not self.objs:
self.is_2d = 'TH2' in objs[0].ClassName()
### Replace objs ###
orig_objs = objs
objs = list(objs)
if stack:
objs = _make_stack(objs)
for i,obj in enumerate(objs):
if obj.ClassName().startswith('TF'):
objs[i] = obj.GetHistogram().Clone()
# the histogram is maintained by the TF1 and will be updated with parameter
# changes, so it must be cloned.
### Styles ###
apply_common_root_styles(objs, **kwargs)
### Styles ###
draw_opts = []
for i,obj in enumerate(objs):
draw_opts.append(_arg(opts, i))
obj.__rxplot_draw_opt = draw_opts[-1] # this just sets a python attribute for convenience
if draw_opts[i] == '' and orig_objs[i].ClassName().startswith('TF'):
draw_opts[i] = 'C' # this is default draw option for TF1, but since we replace it with the hist, must manually set
if (len(draw_opts) > 1 or self.draw_opts) and 'TH2' in objs[0].ClassName() and 'Z' in draw_opts[-1]:
warning('plotter::add() 2D histograms plotted with "Z" option must be passed first in order for z-axis settings to work!')
if 'E1' in draw_opts[-1] and ROOT.gStyle.GetEndErrorSize() == 0:
warning("It looks like you're trying to draw a histogram with the 'E1' option, but the style is forcing the end caps to 0. Use ROOT.gStyle.SetEndErrorSize(4) to fix.")
### Legend ###
if stack and 'legend_opts' not in kwargs:
kwargs['legend_opts'] = 'F'
legend_items = self._get_legend_list(objs, draw_opts, **kwargs)
### Plot stack in reverse order ###
if stack:
objs.reverse()
draw_opts.reverse()
if reverse_legend_for_stack and 'legend_order' not in kwargs:
legend_items.reverse()
### Output ###
self.objs.extend(objs)
if pos is not None:
self.draw_objs[pos:pos] = objs
self.draw_opts[pos:pos] = draw_opts
else:
self.draw_objs.extend(objs)
self.draw_opts.extend(draw_opts)
if legend_pos is not None:
self.legend_items[legend_pos:legend_pos] = legend_items
else:
self.legend_items.extend(legend_items)
def add_primitives(self, objs, opts='', pos=None, **kwargs):
'''
Similar to [add] but for ROOT primitives like TMarker or TBox. This function does
not create legend entries, and the added objects do not participate in auto
ranging and text placement.
@modifies
self.draw_objs
self.draw_opts
'''
draw_opts = [_arg(opts, i) for i in range(len(objs))]
if pos is not None:
self.draw_objs[pos:pos] = objs
self.draw_opts[pos:pos] = draw_opts
else:
self.draw_objs.extend(objs)
self.draw_opts.extend(draw_opts)
def compile(self, **kwargs):
'''
This function should be called after all [add] calls. This will calculate ranges,
make the legend, and place the text.
TODO due to ROOT weirdness, calls to GetXsize() may break after saving the canvas.
So be careful when calling compile() again after saving the canvas. Can use
[reset_pad] though.
https://root-forum.cern.ch/t/tlatex-getxsize-bug/57515
'''
self.compiled = True
self.args.update(kwargs)
### Range parsing ###
self.x_range = self._auto_x_range(**self.args)
self.y_range = self._auto_y_range(**self.args)
self.z_range = self._auto_z_range(**self.args)
### Frame ###
if self.frame is None:
self._create_frame(**self.args) # This needs x_range
self._set_frame_ranges()
_apply_frame_opts(self.frame, **self.args)
self._auto_right_margin()
_fix_axis_sizing(self.frame, self.pad, **self.args)
### Legend and Text ###
# These are the actual text locations in pad coordinates. They have to wait until
# the pad margins are finalized.
self.text_left = self.pad.GetLeftMargin() + self.text_offset_left
self.text_right = 1 - self.pad.GetRightMargin() - self.text_offset_right
self.text_top = 1 - self.pad.GetTopMargin() - self.text_offset_top
self.text_bottom = self.pad.GetBottomMargin() + self.text_offset_bottom
self._make_legend(**self.args)
self._auto_text_pos_and_pad(**self.args)
self._place_text_from_textpos(self.text_pos)
self._pad_y_range(**self.args)
#####################################################################################
### TITLES ###
#####################################################################################
def _create_atlas_title(self):
tex = ROOT.TLatex(0, 0, 'ATLAS')
tex.SetNDC()
tex.SetTextFont(72)
tex.SetTextSize(self.title_size)
tex.SetTextAlign(ROOT.kVAlignBottom + ROOT.kHAlignLeft)
return tex
def _create_title(self, y, title):
'''
Creates the title text and appends it to [self.title_lines]. Custom treament for the
ATLAS logo.
@param y
Starting y to place the title line (i.e. the top of the text).
@returns
The height of the added text.
'''
x = 0
texts = []
### ATLAS logo ###
if 'ATLAS' in title:
atlas = self._create_atlas_title()
texts.append([x, atlas])
self.titles.append(atlas)
x += atlas.GetXsize() + 0.01 # 0.115*696*c.GetWh()/(472*c.GetWw())
height = atlas.GetYsize()
title = title[len('ATLAS '):]
### Remaining title ###
if title:
tex = ROOT.TLatex(0, 0, title)
tex.SetNDC()
tex.SetTextFont(42)
tex.SetTextSize(self.title_size)
tex.SetTextAlign(ROOT.kVAlignBottom + ROOT.kHAlignLeft)
texts.append([x, tex])
self.titles.append(tex)
height = tex.GetYsize()
# Here we align bottom so the text has the same baseline, but then we need to add the height first
y += height
self.title_lines.append((y, texts))
return y
def _make_titles(self, title='ATLAS Internal', subtitle=None, **kwargs):
'''
Creates all title and subtitle text, and places them relative to their bounding
box. All text is aligned bottom, i.e. the y coordinate is the baseline.
@sets
self.title_lines
List of lines, where each line is a pair (y, texts) and `texts` is a list
of pairs (x, ROOT.TLatex). The x and y are offsets from the top-left of
the text bounding box, and are used to directly place the TLatex depending
on its alignment. For example, when left-top aligned, the x-y refer to the
left edge and baseline.
self.titles
A flattened version of the above list, with just the TLatex objects.
self.title_height
'''
self.title_lines = []
self.titles = []
y = 0 # running offset (downwards) from top-left of all text
if title:
y = self._create_title(y, title)
y += self.text_spacing
if subtitle is not None:
if isinstance(subtitle, str):
subtitle = [subtitle]
for i,sub in enumerate(subtitle):
if i > 0: y += self.text_spacing
tex = ROOT.TLatex(0, 0, sub)
tex.SetNDC()
tex.SetTextFont(42)
tex.SetTextSize(self.text_size)
tex.SetTextAlign(ROOT.kVAlignTop + ROOT.kHAlignLeft)
# We use top align here and add the full height of the text below. This
# ensures that the subtitle lines never overlap, but will make the baselineskip
# between each line inconsistent.
self.title_lines.append([y, [[0, tex]]])
self.titles.append(tex)
y += get_tlatex_size(tex)[1]
self.title_height = y
self._format_titles()
def _format_titles(self):
for t in self.titles:
if self.text_back_color is not None:
pass #TODO
def _get_line_width(self, line):
'''
@param line
A list of (x, ROOT.TLatex) pairs. Assumes the entries are in left-to-right
order.
'''
x_start = line[0][0] # left edge of first text
x_end = line[-1][0] + line[-1][1].GetXsize() # right edge of last text
return x_end - x_start
def _place_titles(self, x0, y0, align):
'''
Sets the x/y positions of the TLatex objects in self.titles
@param x0 left or right edge of the texts, depending on [align]
@param y0 top edge of the texts
@param align either 'left' or 'right'
'''
for line in self.title_lines:
y = y0 - line[0]
line_width = self._get_line_width(line[1])
for x_offset,tex in line[1]:
if align == 'left':
x = x0 + x_offset
else:
x = x0 - line_width + x_offset
tex.SetX(x)
tex.SetY(y)
#####################################################################################
### LEGEND ###
#####################################################################################
def _default_legend_opt(self, plot_opt):
'''
Returns the default legend option given a plotting option.
'''
opt = ''
if 'HIST' in plot_opt or 'L' in plot_opt or 'C' in plot_opt:
opt += 'L'
if 'P' in plot_opt:
opt += 'P'
if 'E' in plot_opt:
if not 'P' in opt and not 'L' in opt:
opt += 'LE'
else:
opt += 'E'
return opt or 'L'