rootfig 0.2.1__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. rootfig-0.3.0/PKG-INFO +214 -0
  2. rootfig-0.3.0/README.md +181 -0
  3. {rootfig-0.2.1 → rootfig-0.3.0}/docs/composable.md +7 -5
  4. {rootfig-0.2.1 → rootfig-0.3.0}/docs/hooks/gallery.py +2 -1
  5. rootfig-0.3.0/docs/images/gallery/arrays.png +0 -0
  6. rootfig-0.3.0/docs/images/gallery/cms_density.png +0 -0
  7. rootfig-0.3.0/docs/images/gallery/correlation.png +0 -0
  8. rootfig-0.3.0/docs/images/gallery/expressions.png +0 -0
  9. rootfig-0.3.0/docs/images/gallery/fill_stats.png +0 -0
  10. rootfig-0.3.0/docs/images/gallery/hist2d.png +0 -0
  11. rootfig-0.3.0/docs/images/gallery/log_axes.png +0 -0
  12. rootfig-0.3.0/docs/images/gallery/luminosity.png +0 -0
  13. rootfig-0.3.0/docs/images/gallery/many_plots.png +0 -0
  14. rootfig-0.3.0/docs/images/gallery/object_vs_event.png +0 -0
  15. rootfig-0.3.0/docs/images/gallery/overlay_ratio.png +0 -0
  16. rootfig-0.3.0/docs/images/gallery/quick.png +0 -0
  17. rootfig-0.3.0/docs/images/gallery/ratio_reference.png +0 -0
  18. rootfig-0.3.0/docs/images/gallery/robust_range.png +0 -0
  19. rootfig-0.3.0/docs/images/gallery/stack_data.png +0 -0
  20. rootfig-0.3.0/docs/images/gallery/style_colors.png +0 -0
  21. rootfig-0.3.0/docs/images/gallery/variable_bins.png +0 -0
  22. rootfig-0.3.0/docs/images/gallery/xbreak_ratio.png +0 -0
  23. {rootfig-0.2.1 → rootfig-0.3.0}/docs/plotting.md +95 -6
  24. {rootfig-0.2.1 → rootfig-0.3.0}/docs/quickstart.md +5 -0
  25. {rootfig-0.2.1 → rootfig-0.3.0}/examples/gallery/__init__.py +24 -14
  26. {rootfig-0.2.1 → rootfig-0.3.0}/examples/gallery/registry.py +5 -2
  27. {rootfig-0.2.1 → rootfig-0.3.0}/pyproject.toml +1 -1
  28. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/__init__.py +1 -1
  29. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/api.py +31 -7
  30. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/histograms/pipeline.py +11 -3
  31. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/model/__init__.py +10 -1
  32. rootfig-0.3.0/src/rootfig/model/binning.py +465 -0
  33. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/model/variables.py +8 -5
  34. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/plotting/figure.py +9 -3
  35. {rootfig-0.2.1 → rootfig-0.3.0}/tests/test_api.py +93 -1
  36. {rootfig-0.2.1 → rootfig-0.3.0}/tests/test_model.py +207 -5
  37. {rootfig-0.2.1 → rootfig-0.3.0}/tests/test_plotting.py +7 -7
  38. rootfig-0.2.1/PKG-INFO +0 -201
  39. rootfig-0.2.1/README.md +0 -168
  40. rootfig-0.2.1/docs/images/gallery/arrays.png +0 -0
  41. rootfig-0.2.1/docs/images/gallery/cms_density.png +0 -0
  42. rootfig-0.2.1/docs/images/gallery/correlation.png +0 -0
  43. rootfig-0.2.1/docs/images/gallery/expressions.png +0 -0
  44. rootfig-0.2.1/docs/images/gallery/fill_stats.png +0 -0
  45. rootfig-0.2.1/docs/images/gallery/hist2d.png +0 -0
  46. rootfig-0.2.1/docs/images/gallery/log_axes.png +0 -0
  47. rootfig-0.2.1/docs/images/gallery/luminosity.png +0 -0
  48. rootfig-0.2.1/docs/images/gallery/many_plots.png +0 -0
  49. rootfig-0.2.1/docs/images/gallery/object_vs_event.png +0 -0
  50. rootfig-0.2.1/docs/images/gallery/overlay_ratio.png +0 -0
  51. rootfig-0.2.1/docs/images/gallery/quick.png +0 -0
  52. rootfig-0.2.1/docs/images/gallery/ratio_reference.png +0 -0
  53. rootfig-0.2.1/docs/images/gallery/robust_range.png +0 -0
  54. rootfig-0.2.1/docs/images/gallery/stack_data.png +0 -0
  55. rootfig-0.2.1/docs/images/gallery/style_colors.png +0 -0
  56. rootfig-0.2.1/docs/images/gallery/variable_bins.png +0 -0
  57. rootfig-0.2.1/docs/images/gallery/xbreak_ratio.png +0 -0
  58. rootfig-0.2.1/src/rootfig/model/binning.py +0 -242
  59. {rootfig-0.2.1 → rootfig-0.3.0}/.gitignore +0 -0
  60. {rootfig-0.2.1 → rootfig-0.3.0}/CONTRIBUTING.md +0 -0
  61. {rootfig-0.2.1 → rootfig-0.3.0}/LICENSE +0 -0
  62. {rootfig-0.2.1 → rootfig-0.3.0}/docs/api.md +0 -0
  63. {rootfig-0.2.1 → rootfig-0.3.0}/docs/ecosystem.md +0 -0
  64. {rootfig-0.2.1 → rootfig-0.3.0}/docs/expressions.md +0 -0
  65. {rootfig-0.2.1 → rootfig-0.3.0}/docs/gallery.md +0 -0
  66. {rootfig-0.2.1 → rootfig-0.3.0}/docs/images/gallery/efficiency.png +0 -0
  67. {rootfig-0.2.1 → rootfig-0.3.0}/docs/images/gallery/profile.png +0 -0
  68. {rootfig-0.2.1 → rootfig-0.3.0}/docs/index.md +0 -0
  69. {rootfig-0.2.1 → rootfig-0.3.0}/examples/gallery/__main__.py +0 -0
  70. {rootfig-0.2.1 → rootfig-0.3.0}/examples/gallery/data.py +0 -0
  71. {rootfig-0.2.1 → rootfig-0.3.0}/mkdocs.yml +0 -0
  72. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/_typing.py +0 -0
  73. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/errors.py +0 -0
  74. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/expressions/__init__.py +0 -0
  75. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/expressions/functions.py +0 -0
  76. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/expressions/parser.py +0 -0
  77. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/histograms/__init__.py +0 -0
  78. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/histograms/build.py +0 -0
  79. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/histograms/cutflow.py +0 -0
  80. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/histograms/efficiency.py +0 -0
  81. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/histograms/normalize.py +0 -0
  82. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/histograms/ratio.py +0 -0
  83. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/histograms/stats.py +0 -0
  84. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/io/__init__.py +0 -0
  85. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/io/sources.py +0 -0
  86. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/model/cuts.py +0 -0
  87. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/model/samples.py +0 -0
  88. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/model/style.py +0 -0
  89. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/model/units.py +0 -0
  90. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/plotting/__init__.py +0 -0
  91. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/plotting/annotations.py +0 -0
  92. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/plotting/correlation.py +0 -0
  93. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/plotting/hist1d.py +0 -0
  94. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/plotting/hist2d.py +0 -0
  95. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/plotting/points.py +0 -0
  96. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/plotting/ratio.py +0 -0
  97. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/plotting/result.py +0 -0
  98. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/plotting/style.py +0 -0
  99. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/py.typed +0 -0
  100. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/selection/__init__.py +0 -0
  101. {rootfig-0.2.1 → rootfig-0.3.0}/src/rootfig/selection/columns.py +0 -0
  102. {rootfig-0.2.1 → rootfig-0.3.0}/tests/conftest.py +0 -0
  103. {rootfig-0.2.1 → rootfig-0.3.0}/tests/data/split_collection.root +0 -0
  104. {rootfig-0.2.1 → rootfig-0.3.0}/tests/test_expressions.py +0 -0
  105. {rootfig-0.2.1 → rootfig-0.3.0}/tests/test_gallery.py +0 -0
  106. {rootfig-0.2.1 → rootfig-0.3.0}/tests/test_histograms.py +0 -0
  107. {rootfig-0.2.1 → rootfig-0.3.0}/tests/test_io.py +0 -0
  108. {rootfig-0.2.1 → rootfig-0.3.0}/tests/test_selection.py +0 -0
  109. {rootfig-0.2.1 → rootfig-0.3.0}/tests/test_tutorials.py +0 -0
rootfig-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,214 @@
1
+ Metadata-Version: 2.5
2
+ Name: rootfig
3
+ Version: 0.3.0
4
+ Summary: Publication-quality figures straight from ROOT trees, without ROOT: uproot + Awkward + hist + mplhep with a TTree::Draw-like API.
5
+ Project-URL: Homepage, https://github.com/jbeirer/rootfig
6
+ Project-URL: Documentation, https://jbeirer.github.io/rootfig/
7
+ Project-URL: Repository, https://github.com/jbeirer/rootfig
8
+ Project-URL: Issues, https://github.com/jbeirer/rootfig/issues
9
+ Author-email: Joshua Falco Beirer <jbeirer@cern.ch>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: HEP,RNTuple,ROOT,TTree,awkward,histogram,matplotlib,mplhep,plotting,uproot
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Scientific/Engineering :: Physics
23
+ Classifier: Topic :: Scientific/Engineering :: Visualization
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.12
26
+ Requires-Dist: awkward>=2.8
27
+ Requires-Dist: hist>=2.7.2
28
+ Requires-Dist: matplotlib>=3.11
29
+ Requires-Dist: mplhep>=1.3
30
+ Requires-Dist: numpy>=1.26
31
+ Requires-Dist: uproot>=5.7.4
32
+ Description-Content-Type: text/markdown
33
+
34
+ <p align="center">
35
+ <picture>
36
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jbeirer/rootfig/main/.github/assets/rootfig-logo-dark.svg">
37
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/jbeirer/rootfig/main/.github/assets/rootfig-logo-light.svg">
38
+ <img src="https://raw.githubusercontent.com/jbeirer/rootfig/main/.github/assets/rootfig-logo-light.svg" alt="rootfig" width="560">
39
+ </picture>
40
+ </p>
41
+
42
+ <p align="center">
43
+ <strong>Publication-quality figures straight from ROOT trees, without ROOT.</strong>
44
+ </p>
45
+
46
+ <p align="center">
47
+ <a href="https://jbeirer.github.io/rootfig/">Documentation</a> ·
48
+ <a href="https://jbeirer.github.io/rootfig/gallery/">Gallery</a> ·
49
+ <a href="https://jbeirer.github.io/rootfig/quickstart/">Quick start</a>
50
+ </p>
51
+
52
+ <p align="center">
53
+ <a href="https://jbeirer.github.io/rootfig/"><img src="https://img.shields.io/badge/docs-online-blue" alt="Documentation"></a>
54
+ <a href="https://doi.org/10.5281/zenodo.22726311"><img src="https://zenodo.org/badge/1366702602.svg" alt="DOI"></a>
55
+ <a href="https://github.com/jbeirer/rootfig/actions/workflows/ci.yml"><img src="https://github.com/jbeirer/rootfig/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
56
+ <a href="https://codecov.io/gh/jbeirer/rootfig"><img src="https://codecov.io/gh/jbeirer/rootfig/branch/main/graph/badge.svg" alt="codecov"></a>
57
+ <a href="https://pypi.org/project/rootfig/"><img src="https://img.shields.io/pypi/v/rootfig.svg" alt="PyPI"></a>
58
+ <a href="https://pypi.org/project/rootfig/"><img src="https://img.shields.io/pypi/pyversions/rootfig.svg" alt="Python"></a>
59
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
60
+ </p>
61
+
62
+ Go from a ROOT file to a styled figure in one call. Choose a variable, add a
63
+ selection, and plot:
64
+
65
+ ```python
66
+ import rootfig as rf
67
+
68
+ rf.plot("events.root", "Muon_pt", tree="events", selection="Muon_pt > 20", bins=50)
69
+ ```
70
+
71
+ Start with a single distribution; add samples, weights, stacks and ratio
72
+ panels as your analysis grows. Every plot gives you a matplotlib figure to
73
+ customise and save.
74
+
75
+ <p align="center">
76
+ <a href="https://jbeirer.github.io/rootfig/gallery/#logarithmic-axes-with-log-spaced-bins"><img src="docs/images/gallery/log_axes.png" alt="Logarithmic axes with log-spaced bins" width="46%"></a>
77
+ &nbsp;&nbsp;
78
+ <a href="https://jbeirer.github.io/rootfig/gallery/#a-broken-x-axis-peak-and-far-tail-without-the-empty-middle"><img src="docs/images/gallery/xbreak_ratio.png" alt="Broken x axis with a ratio panel" width="46%"></a>
79
+ </p>
80
+ <p align="center">
81
+ <a href="https://jbeirer.github.io/rootfig/gallery/#a-correlation-matrix"><img src="docs/images/gallery/correlation.png" alt="A correlation matrix" width="46%"></a>
82
+ &nbsp;&nbsp;
83
+ <a href="https://jbeirer.github.io/rootfig/gallery/#a-two-dimensional-histogram"><img src="docs/images/gallery/hist2d.png" alt="Two-dimensional histogram" width="46%"></a>
84
+ </p>
85
+
86
+ **[Explore the gallery →](https://jbeirer.github.io/rootfig/gallery/)**
87
+ See each figure alongside the code that makes it, from simple overlays to
88
+ stacked data/MC comparisons, broken axes and 2D histograms.
89
+
90
+ ## Installation
91
+
92
+ ```bash
93
+ pip install rootfig
94
+ # or
95
+ uv add rootfig
96
+ ```
97
+
98
+ Python 3.12 or newer. No ROOT installation is needed; `TTree` and `RNTuple`
99
+ files are both supported.
100
+
101
+ ## Compare samples in one call
102
+
103
+ ```python
104
+ import rootfig as rf
105
+
106
+ # Overlay two samples, each normalised to unity, with a Signal / Background panel.
107
+ rf.plot(
108
+ {"Signal": "signal.root", "Background": "background.root"},
109
+ "Muon_pt",
110
+ tree="events",
111
+ selection="abs(Muon_eta) < 2.5",
112
+ weight="event_weight",
113
+ bins=(50, 0, 200),
114
+ normalize=True,
115
+ ratio="Background",
116
+ )
117
+ ```
118
+
119
+ ## Build up to a full analysis
120
+
121
+ Define samples, variables, cuts and styles once, then reuse them across plots:
122
+
123
+ ```python
124
+ import rootfig as rf
125
+
126
+ signal = rf.Sample("sig_*.root", tree="events", label="Signal", weight="mc_weight")
127
+ background = rf.Sample("bkg.root", tree="events", label="Background", weight="mc_weight")
128
+ data = rf.Sample("data.root", tree="events", label="Data", is_data=True)
129
+
130
+ pt = rf.Variable("Muon_pt", bins=(50, 0, 200), label=r"$p_T^{\mu}$", unit="GeV")
131
+ baseline = rf.Cut("nMuon >= 1") & "abs(Muon_eta) < 2.5"
132
+ style = rf.Style(experiment="ATLAS", status="Internal", lumi=140, com=13.6)
133
+
134
+ p = rf.plot(
135
+ [background, signal],
136
+ pt,
137
+ observed=data,
138
+ selection=baseline,
139
+ stack=True,
140
+ ratio=True,
141
+ logy=True,
142
+ style=style,
143
+ )
144
+ p.ax.set_ylim(top=1e5) # it is a normal matplotlib Axes
145
+ p.save("muon_pt.pdf")
146
+ ```
147
+
148
+ Everything you get back is a standard object: `p.fig` and `p.ax` are
149
+ matplotlib `Figure`/`Axes`, `p.hists` are `hist.Hist` objects, and
150
+ `rf.load(...)` returns Awkward arrays.
151
+
152
+ ## What you can do
153
+
154
+ - **Select events and objects with readable expressions.** Write cuts such as
155
+ `count(Jet_pt) >= 2` or `Muon_pt > 20`; event and object selections have
156
+ explicit rules, and event weights carry through to each selected object.
157
+ - **Compare samples with a few keywords.** Overlays, stacks, data points and
158
+ ratio panels share binning and propagate histogram uncertainties; bin edges
159
+ and `(n, low, high)` are used as given, while a range inferred from the data
160
+ ignores far outliers, so `-999` sentinels do not set the axis. Normalise to
161
+ unity, density, bin width or luminosity.
162
+ - **Style figures for your analysis.** Add experiment labels, units, log axes
163
+ and broken axes, then refine the result with matplotlib.
164
+ - **Go beyond 1D plots.** Draw 2D histograms, correlations, efficiencies,
165
+ profiles, resolutions and significance panels; produce cut flows and
166
+ summary statistics from the same inputs.
167
+ - **Work directly with your files.** Read `TTree` and `RNTuple` data, combine
168
+ files with globs, limit entry ranges for quick checks, and use EDM4hep
169
+ split collections. Only the branches your expressions need are read.
170
+
171
+ ## Documentation
172
+
173
+ **[Read the docs](https://jbeirer.github.io/rootfig/)** or
174
+ **[browse the gallery](https://jbeirer.github.io/rootfig/gallery/)** for examples
175
+ with figures and code.
176
+
177
+ - [Quick start](https://jbeirer.github.io/rootfig/quickstart/): your first plot, selections and weights.
178
+ - [Expressions and selections](https://jbeirer.github.io/rootfig/expressions/): syntax and event/object rules.
179
+ - [Samples, variables, cuts and styles](https://jbeirer.github.io/rootfig/composable/): reusable analysis definitions.
180
+ - [Plotting options](https://jbeirer.github.io/rootfig/plotting/): binning, normalisation, panels and styling.
181
+ - [API reference](https://jbeirer.github.io/rootfig/api/): full signatures and options.
182
+
183
+ ## Relation to the ecosystem
184
+
185
+ `rootfig` brings a `TTree::Draw`-like workflow to the Scientific Python HEP
186
+ stack, building on familiar libraries:
187
+
188
+ | Task | Library | What rootfig adds |
189
+ | --- | --- | --- |
190
+ | Reading ROOT files | [uproot](https://github.com/scikit-hep/uproot5) | file globs, tree auto-detection, reading only the required branches |
191
+ | Jagged arrays | [Awkward Array](https://github.com/scikit-hep/awkward) | the per-event/per-object rules for cuts and weights |
192
+ | Histograms | [hist](https://github.com/scikit-hep/hist) / boost-histogram | shared binning, robust automatic ranges, normalisation, ratios |
193
+ | Drawing | [mplhep](https://github.com/scikit-hep/mplhep) + matplotlib | overlays, stacks, ratio panels, labels and legends with good defaults |
194
+
195
+ If you already have `hist.Hist` objects, `rf.plot_histograms` draws them with
196
+ the same options. If you want the arrays, `rf.load` returns them. See
197
+ [the ecosystem guide](https://jbeirer.github.io/rootfig/ecosystem/) for details.
198
+
199
+ ## Development
200
+
201
+ ```bash
202
+ git clone https://github.com/jbeirer/rootfig
203
+ cd rootfig
204
+ uv sync --all-groups
205
+ uv run pytest
206
+ uv run ruff check . && uv run ruff format --check .
207
+ uv run mypy
208
+ ```
209
+
210
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
211
+
212
+ ## License
213
+
214
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,181 @@
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jbeirer/rootfig/main/.github/assets/rootfig-logo-dark.svg">
4
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/jbeirer/rootfig/main/.github/assets/rootfig-logo-light.svg">
5
+ <img src="https://raw.githubusercontent.com/jbeirer/rootfig/main/.github/assets/rootfig-logo-light.svg" alt="rootfig" width="560">
6
+ </picture>
7
+ </p>
8
+
9
+ <p align="center">
10
+ <strong>Publication-quality figures straight from ROOT trees, without ROOT.</strong>
11
+ </p>
12
+
13
+ <p align="center">
14
+ <a href="https://jbeirer.github.io/rootfig/">Documentation</a> ·
15
+ <a href="https://jbeirer.github.io/rootfig/gallery/">Gallery</a> ·
16
+ <a href="https://jbeirer.github.io/rootfig/quickstart/">Quick start</a>
17
+ </p>
18
+
19
+ <p align="center">
20
+ <a href="https://jbeirer.github.io/rootfig/"><img src="https://img.shields.io/badge/docs-online-blue" alt="Documentation"></a>
21
+ <a href="https://doi.org/10.5281/zenodo.22726311"><img src="https://zenodo.org/badge/1366702602.svg" alt="DOI"></a>
22
+ <a href="https://github.com/jbeirer/rootfig/actions/workflows/ci.yml"><img src="https://github.com/jbeirer/rootfig/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
23
+ <a href="https://codecov.io/gh/jbeirer/rootfig"><img src="https://codecov.io/gh/jbeirer/rootfig/branch/main/graph/badge.svg" alt="codecov"></a>
24
+ <a href="https://pypi.org/project/rootfig/"><img src="https://img.shields.io/pypi/v/rootfig.svg" alt="PyPI"></a>
25
+ <a href="https://pypi.org/project/rootfig/"><img src="https://img.shields.io/pypi/pyversions/rootfig.svg" alt="Python"></a>
26
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
27
+ </p>
28
+
29
+ Go from a ROOT file to a styled figure in one call. Choose a variable, add a
30
+ selection, and plot:
31
+
32
+ ```python
33
+ import rootfig as rf
34
+
35
+ rf.plot("events.root", "Muon_pt", tree="events", selection="Muon_pt > 20", bins=50)
36
+ ```
37
+
38
+ Start with a single distribution; add samples, weights, stacks and ratio
39
+ panels as your analysis grows. Every plot gives you a matplotlib figure to
40
+ customise and save.
41
+
42
+ <p align="center">
43
+ <a href="https://jbeirer.github.io/rootfig/gallery/#logarithmic-axes-with-log-spaced-bins"><img src="docs/images/gallery/log_axes.png" alt="Logarithmic axes with log-spaced bins" width="46%"></a>
44
+ &nbsp;&nbsp;
45
+ <a href="https://jbeirer.github.io/rootfig/gallery/#a-broken-x-axis-peak-and-far-tail-without-the-empty-middle"><img src="docs/images/gallery/xbreak_ratio.png" alt="Broken x axis with a ratio panel" width="46%"></a>
46
+ </p>
47
+ <p align="center">
48
+ <a href="https://jbeirer.github.io/rootfig/gallery/#a-correlation-matrix"><img src="docs/images/gallery/correlation.png" alt="A correlation matrix" width="46%"></a>
49
+ &nbsp;&nbsp;
50
+ <a href="https://jbeirer.github.io/rootfig/gallery/#a-two-dimensional-histogram"><img src="docs/images/gallery/hist2d.png" alt="Two-dimensional histogram" width="46%"></a>
51
+ </p>
52
+
53
+ **[Explore the gallery →](https://jbeirer.github.io/rootfig/gallery/)**
54
+ See each figure alongside the code that makes it, from simple overlays to
55
+ stacked data/MC comparisons, broken axes and 2D histograms.
56
+
57
+ ## Installation
58
+
59
+ ```bash
60
+ pip install rootfig
61
+ # or
62
+ uv add rootfig
63
+ ```
64
+
65
+ Python 3.12 or newer. No ROOT installation is needed; `TTree` and `RNTuple`
66
+ files are both supported.
67
+
68
+ ## Compare samples in one call
69
+
70
+ ```python
71
+ import rootfig as rf
72
+
73
+ # Overlay two samples, each normalised to unity, with a Signal / Background panel.
74
+ rf.plot(
75
+ {"Signal": "signal.root", "Background": "background.root"},
76
+ "Muon_pt",
77
+ tree="events",
78
+ selection="abs(Muon_eta) < 2.5",
79
+ weight="event_weight",
80
+ bins=(50, 0, 200),
81
+ normalize=True,
82
+ ratio="Background",
83
+ )
84
+ ```
85
+
86
+ ## Build up to a full analysis
87
+
88
+ Define samples, variables, cuts and styles once, then reuse them across plots:
89
+
90
+ ```python
91
+ import rootfig as rf
92
+
93
+ signal = rf.Sample("sig_*.root", tree="events", label="Signal", weight="mc_weight")
94
+ background = rf.Sample("bkg.root", tree="events", label="Background", weight="mc_weight")
95
+ data = rf.Sample("data.root", tree="events", label="Data", is_data=True)
96
+
97
+ pt = rf.Variable("Muon_pt", bins=(50, 0, 200), label=r"$p_T^{\mu}$", unit="GeV")
98
+ baseline = rf.Cut("nMuon >= 1") & "abs(Muon_eta) < 2.5"
99
+ style = rf.Style(experiment="ATLAS", status="Internal", lumi=140, com=13.6)
100
+
101
+ p = rf.plot(
102
+ [background, signal],
103
+ pt,
104
+ observed=data,
105
+ selection=baseline,
106
+ stack=True,
107
+ ratio=True,
108
+ logy=True,
109
+ style=style,
110
+ )
111
+ p.ax.set_ylim(top=1e5) # it is a normal matplotlib Axes
112
+ p.save("muon_pt.pdf")
113
+ ```
114
+
115
+ Everything you get back is a standard object: `p.fig` and `p.ax` are
116
+ matplotlib `Figure`/`Axes`, `p.hists` are `hist.Hist` objects, and
117
+ `rf.load(...)` returns Awkward arrays.
118
+
119
+ ## What you can do
120
+
121
+ - **Select events and objects with readable expressions.** Write cuts such as
122
+ `count(Jet_pt) >= 2` or `Muon_pt > 20`; event and object selections have
123
+ explicit rules, and event weights carry through to each selected object.
124
+ - **Compare samples with a few keywords.** Overlays, stacks, data points and
125
+ ratio panels share binning and propagate histogram uncertainties; bin edges
126
+ and `(n, low, high)` are used as given, while a range inferred from the data
127
+ ignores far outliers, so `-999` sentinels do not set the axis. Normalise to
128
+ unity, density, bin width or luminosity.
129
+ - **Style figures for your analysis.** Add experiment labels, units, log axes
130
+ and broken axes, then refine the result with matplotlib.
131
+ - **Go beyond 1D plots.** Draw 2D histograms, correlations, efficiencies,
132
+ profiles, resolutions and significance panels; produce cut flows and
133
+ summary statistics from the same inputs.
134
+ - **Work directly with your files.** Read `TTree` and `RNTuple` data, combine
135
+ files with globs, limit entry ranges for quick checks, and use EDM4hep
136
+ split collections. Only the branches your expressions need are read.
137
+
138
+ ## Documentation
139
+
140
+ **[Read the docs](https://jbeirer.github.io/rootfig/)** or
141
+ **[browse the gallery](https://jbeirer.github.io/rootfig/gallery/)** for examples
142
+ with figures and code.
143
+
144
+ - [Quick start](https://jbeirer.github.io/rootfig/quickstart/): your first plot, selections and weights.
145
+ - [Expressions and selections](https://jbeirer.github.io/rootfig/expressions/): syntax and event/object rules.
146
+ - [Samples, variables, cuts and styles](https://jbeirer.github.io/rootfig/composable/): reusable analysis definitions.
147
+ - [Plotting options](https://jbeirer.github.io/rootfig/plotting/): binning, normalisation, panels and styling.
148
+ - [API reference](https://jbeirer.github.io/rootfig/api/): full signatures and options.
149
+
150
+ ## Relation to the ecosystem
151
+
152
+ `rootfig` brings a `TTree::Draw`-like workflow to the Scientific Python HEP
153
+ stack, building on familiar libraries:
154
+
155
+ | Task | Library | What rootfig adds |
156
+ | --- | --- | --- |
157
+ | Reading ROOT files | [uproot](https://github.com/scikit-hep/uproot5) | file globs, tree auto-detection, reading only the required branches |
158
+ | Jagged arrays | [Awkward Array](https://github.com/scikit-hep/awkward) | the per-event/per-object rules for cuts and weights |
159
+ | Histograms | [hist](https://github.com/scikit-hep/hist) / boost-histogram | shared binning, robust automatic ranges, normalisation, ratios |
160
+ | Drawing | [mplhep](https://github.com/scikit-hep/mplhep) + matplotlib | overlays, stacks, ratio panels, labels and legends with good defaults |
161
+
162
+ If you already have `hist.Hist` objects, `rf.plot_histograms` draws them with
163
+ the same options. If you want the arrays, `rf.load` returns them. See
164
+ [the ecosystem guide](https://jbeirer.github.io/rootfig/ecosystem/) for details.
165
+
166
+ ## Development
167
+
168
+ ```bash
169
+ git clone https://github.com/jbeirer/rootfig
170
+ cd rootfig
171
+ uv sync --all-groups
172
+ uv run pytest
173
+ uv run ruff check . && uv run ruff format --check .
174
+ uv run mypy
175
+ ```
176
+
177
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
178
+
179
+ ## License
180
+
181
+ MIT. See [LICENSE](LICENSE).
@@ -61,14 +61,16 @@ What to histogram and how to present it.
61
61
  ```python
62
62
  pt = rf.Variable("Muon_pt", bins=(50, 0, 200), label=r"$p_T^{\mu}$", unit="GeV")
63
63
  met = rf.Variable(
64
- "MET / 1000", bins=40, range="robust", label=r"$E_T^{miss}$", unit="TeV", log=True, name="met"
64
+ "MET / 1000", bins=40, range="auto", label=r"$E_T^{miss}$", unit="TeV", log=True, name="met"
65
65
  )
66
66
  ```
67
67
 
68
- - `bins`: an `int` (range from the data), `(n, low, high)`, a sequence of
69
- edges (e.g. `rf.log_bins(30, 1, 1000)`), or a `hist.axis.Regular`/`Variable`.
70
- - `range`: `(low, high)`, `"auto"` (finite min/max over all samples) or
71
- `"robust"` (ignores far outliers such as `-999` sentinels).
68
+ - `bins`: an `int` (range inferred from the data), `(n, low, high)`, a sequence
69
+ of edges (e.g. `rf.log_bins(30, 1, 1000)`), or a `hist.axis.Regular`/`Variable`.
70
+ - `range`: `(low, high)`, `"robust"` (the default: ignores far outliers such as
71
+ `-999` sentinels and cuts a thin tail, both of which then land in the
72
+ under/overflow) or `"auto"` (the finite min/max over all samples). See
73
+ [Binning and range](plotting.md#binning-and-range).
72
74
  - `label` and `unit` form the axis label `label [unit]`; the unit also appears
73
75
  in the automatic y label (`Events / 4 GeV`).
74
76
  - `name` is used for file names by `Plot.save(directory)`; it must be a plain
@@ -60,7 +60,8 @@ def render_section(gallery: ModuleType, example: Any) -> str:
60
60
  return (
61
61
  f"## {example.title}\n\n"
62
62
  f"{example.description}\n\n"
63
- f'![{example.title}](images/gallery/{example.name}.png){{ width="75%" }}\n\n'
63
+ f"![{example.title}](images/gallery/{example.name}.png)"
64
+ f'{{ width="{example.image_width}" }}\n\n'
64
65
  f"```python\n{code}```\n"
65
66
  )
66
67
 
@@ -86,12 +86,98 @@ shorter `ratio_label` such as `"Ratio"` to keep it at full size. The
86
86
  computed values are returned in `Plot.ratios` as
87
87
  [`Ratio`][rootfig.Ratio] objects (`values`, `errors`, `band`, `edges`).
88
88
 
89
+ ## Binning and range
90
+
91
+ `bins` takes an `int`, a `(n, low, high)` triple, a sequence of edges or a
92
+ `hist` axis, and is shared by every sample of one plot (and by the numerator
93
+ and denominator of an [efficiency](#efficiencies)).
94
+
95
+ With an integer `bins` the range comes from `range`:
96
+
97
+ | `range` | Meaning |
98
+ | --- | --- |
99
+ | `(low, high)` | explicit |
100
+ | `"robust"` | **default**: the min/max of the data, ignoring values far from the bulk |
101
+ | `"auto"` | the full finite minimum and maximum over all samples |
102
+
103
+ ```python
104
+ rf.plot("events.root", "d0_significance", bins=50) # robust
105
+ rf.plot("events.root", "d0_significance", bins=50, range="auto") # full extent
106
+ rf.plot("events.root", "d0_significance", bins=50, range=(-5, 5)) # explicit
107
+ ```
108
+
109
+ !!! note "Rejected values are not discarded"
110
+
111
+ A value outside the range is **not removed from the data**: it goes to the
112
+ under/overflow, shown by the flow arrows (`flow="show"` turns them into
113
+ visible bins, `flow="sum"` folds them into the edge bins). Statistics boxes
114
+ and `rf.summarize` are computed before binning, so means and entry counts
115
+ cover the full sample whichever range is used.
116
+
117
+ This happens in two steps. Outliers are rejected by their modified z-score
118
+ (`0.6745 * |x - median| / MAD`), with a threshold of 30, which removes sentinels
119
+ and anything else far from the bulk. That is done within each sample, and the
120
+ ranges they keep are unioned, so a sample is judged against its own median and
121
+ spread: a signal offset from a background is not an outlier merely because the
122
+ background outnumbers it, and a sample keeps the same values whether it is
123
+ plotted alone or in an overlay. The threshold is then tightened for as
124
+ long as each step costs no more than an *additional* 1 percent of any one
125
+ sample, by entries and by weight. Additional is meant literally: the budget is
126
+ measured against what the first step already moved out of the view, which may
127
+ be a good deal more than 1 percent. This second step cuts a tail that reaches
128
+ far but thins out smoothly, the kind a distance threshold keeps and that leaves
129
+ the interesting part of the distribution in a corner of the axis.
130
+
131
+ The budget is charged per sample rather than over the pooled entries, so a small
132
+ signal sitting far from a large background keeps its own place on the axis
133
+ instead of being cut as a rounding error, and it is charged against the weight a
134
+ cut would remove as well as the entries, so a handful of high-weight entries is
135
+ not treated as negligible. A sample with fewer than 20 distinct values is
136
+ categorical — counts, flags, multiplicities — has no tail to cut and gets no
137
+ budget at all, so the second step takes no value off its axis; this is decided
138
+ per sample too, and holds when it is overlaid with a continuous one. If MAD is zero, the mean absolute deviation from
139
+ the median is used instead. Every candidate is padded by 5 percent and clamped
140
+ to the `"auto"` range (whose upper edge is nudged above the maximum to include
141
+ it), so the range never reaches past the data. Degenerate ranges are widened
142
+ symmetrically. The threshold cannot distinguish sentinels from valid data.
143
+ Cases where you may want `range="auto"`:
144
+
145
+ - a distribution with a long tail (log-normal, Student-t, or an invariant mass
146
+ with a continuum) has valid tail entries pushed into the flow bins — this is
147
+ what the second step is for, so `"auto"` is the way to see the whole tail;
148
+ - a sparse discrete distribution can lose rare valid values from the visible
149
+ range, for example the ones in a binary sample with 999 zeros and one one;
150
+ - a lone value far from a bulk of near-identical ones looks exactly like a
151
+ sentinel and is rejected with them, however real it is;
152
+ - `xbreak=(a, b)` is validated against the inferred axis, so a break meant to
153
+ span a far tail needs `range="auto"` or an explicit range.
154
+
155
+ Inferring a robust range requires additional median and deviation calculations
156
+ over the combined samples, with additional time and memory costs. An explicit
157
+ range avoids range inference.
158
+
159
+ !!! tip "The plot is mostly empty space"
160
+
161
+ The inferred range cuts a thin tail, but only as far as its coverage
162
+ budget allows. A distribution whose tail carries more than that — a heavy
163
+ Student-t, a steeply falling spectrum over several decades — still spreads
164
+ the axis over bins holding a fraction of a percent of the peak. Three ways
165
+ out, in order of how often they are what you want:
166
+
167
+ - `range=(a, b)` around the core. Nothing is lost: entries outside go to
168
+ the flow bins, where `flow="hint"` (the default) marks them with arrows
169
+ and `flow="sum"` folds them into the edge bins.
170
+ - `logy=True`, which makes the tail visible instead of hiding it.
171
+ - `xbreak=(a, b)` to cut the empty middle out and keep both ends, with
172
+ `range="auto"` or an explicit range so the break lies inside the axis.
173
+
89
174
  ## Axes
90
175
 
91
176
  - `logx`, `logy`: logarithmic scales. Log-spaced bins: `bins=rf.log_bins(n, low, high)`.
92
177
  - `xlim`, `ylim`: limits; `ylim=(None, 1e4)` keeps the automatic lower value.
93
- Automatic y limits leave room for the legend and label (a factor 1.45 in
94
- linear scale, 30 in log scale).
178
+ Automatic y limits add a small margin above the tallest bin (a factor 1.2 in
179
+ linear scale, 12 in log scale) and then raise it further as the drawn
180
+ legend, label, statistics box and text lines need.
95
181
  - `xbreak=(a, b)`: cut the range between `a` and `b` out of the x axis and
96
182
  draw the two remaining segments side by side with break marks, sharing the
97
183
  y axis (and the ratio panel, if any). Useful for a peak plus a far tail or
@@ -99,7 +185,8 @@ computed values are returned in `Plot.ratios` as
99
185
  (`Plot.ratio_ax_right`). Not available together with `ax=` or `flow="show"`.
100
186
 
101
187
  ![Broken x axis with a ratio panel](images/gallery/xbreak_ratio.png){ width="60%" }
102
- - `flow`: how under/overflow is shown, `"hint"` (small arrows, default),
188
+ - `flow`: how under/overflow is shown (this is where entries outside an
189
+ inferred [range](#binning-and-range) end up), `"hint"` (small arrows, default),
103
190
  `"show"` (extra bins labelled `<low` / `>high`, added on a side as soon as any
104
191
  sample has content there, identical for all samples and the ratio panel),
105
192
  `"sum"` (added to the edge bins before anything is computed, so ratios,
@@ -110,9 +197,11 @@ computed values are returned in `Plot.ratios` as
110
197
  - `xlabel`, `ylabel`, `unit`, `title`. The title sits above the axes, where
111
198
  the CMS-style label is also drawn; with such a style prefer `text=`.
112
199
  - Automatic y limits leave room for the legend, the experiment label, the
113
- statistics box and `text` lines: the upper limit is raised until none of
114
- them covers a histogram (the legend picks a free upper corner). A `ylim`
115
- with an explicit upper value switches this off.
200
+ statistics box and `text` lines: a small fixed margin is added above the
201
+ tallest bin, and the upper limit is then raised until none of them covers a
202
+ histogram (the legend picks a free upper corner). Room is only made for
203
+ what is actually drawn, so a plot without annotations keeps the margin.
204
+ A `ylim` with an explicit upper value switches this off.
116
205
 
117
206
  ## Legend, labels, text and statistics
118
207
 
@@ -19,6 +19,11 @@ the weight (nothing else), evaluates them, fills a `hist.Hist` and draws it.
19
19
  If the file contains exactly one tree you can leave `tree` out. The file can
20
20
  also be a glob (`"run_*.root"`), a list of files, or `"file.root:tree"`.
21
21
 
22
+ A bare `bins=50` infers the range from the data, ignoring far outliers so that
23
+ sentinel values such as `-999` do not set the axis; pass `range=(low, high)` to
24
+ be explicit or `range="auto"` for the full extent (see
25
+ [Binning and range](plotting.md#binning-and-range)).
26
+
22
27
  The return value is a [`Plot`][rootfig.Plot] with `fig`, `ax`, `hists` and a
23
28
  `save()` method:
24
29