rootfig 0.1.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 (86) hide show
  1. rootfig-0.1.0/.gitignore +21 -0
  2. rootfig-0.1.0/CHANGELOG.md +87 -0
  3. rootfig-0.1.0/CONTRIBUTING.md +129 -0
  4. rootfig-0.1.0/LICENSE +21 -0
  5. rootfig-0.1.0/PKG-INFO +202 -0
  6. rootfig-0.1.0/README.md +168 -0
  7. rootfig-0.1.0/docs/api.md +51 -0
  8. rootfig-0.1.0/docs/composable.md +128 -0
  9. rootfig-0.1.0/docs/ecosystem.md +59 -0
  10. rootfig-0.1.0/docs/expressions.md +145 -0
  11. rootfig-0.1.0/docs/gallery.md +68 -0
  12. rootfig-0.1.0/docs/hooks/gallery.py +63 -0
  13. rootfig-0.1.0/docs/images/gallery/arrays.png +0 -0
  14. rootfig-0.1.0/docs/images/gallery/cms_density.png +0 -0
  15. rootfig-0.1.0/docs/images/gallery/correlation.png +0 -0
  16. rootfig-0.1.0/docs/images/gallery/efficiency.png +0 -0
  17. rootfig-0.1.0/docs/images/gallery/expressions.png +0 -0
  18. rootfig-0.1.0/docs/images/gallery/fill_stats.png +0 -0
  19. rootfig-0.1.0/docs/images/gallery/hist2d.png +0 -0
  20. rootfig-0.1.0/docs/images/gallery/log_axes.png +0 -0
  21. rootfig-0.1.0/docs/images/gallery/luminosity.png +0 -0
  22. rootfig-0.1.0/docs/images/gallery/many_plots.png +0 -0
  23. rootfig-0.1.0/docs/images/gallery/object_vs_event.png +0 -0
  24. rootfig-0.1.0/docs/images/gallery/overlay_ratio.png +0 -0
  25. rootfig-0.1.0/docs/images/gallery/profile.png +0 -0
  26. rootfig-0.1.0/docs/images/gallery/quick.png +0 -0
  27. rootfig-0.1.0/docs/images/gallery/ratio_reference.png +0 -0
  28. rootfig-0.1.0/docs/images/gallery/robust_range.png +0 -0
  29. rootfig-0.1.0/docs/images/gallery/stack_data.png +0 -0
  30. rootfig-0.1.0/docs/images/gallery/style_colors.png +0 -0
  31. rootfig-0.1.0/docs/images/gallery/variable_bins.png +0 -0
  32. rootfig-0.1.0/docs/images/gallery/xbreak_ratio.png +0 -0
  33. rootfig-0.1.0/docs/index.md +25 -0
  34. rootfig-0.1.0/docs/plotting.md +197 -0
  35. rootfig-0.1.0/docs/quickstart.md +111 -0
  36. rootfig-0.1.0/examples/gallery.py +635 -0
  37. rootfig-0.1.0/mkdocs.yml +70 -0
  38. rootfig-0.1.0/pyproject.toml +151 -0
  39. rootfig-0.1.0/src/rootfig/__init__.py +97 -0
  40. rootfig-0.1.0/src/rootfig/_typing.py +21 -0
  41. rootfig-0.1.0/src/rootfig/api.py +1297 -0
  42. rootfig-0.1.0/src/rootfig/errors.py +73 -0
  43. rootfig-0.1.0/src/rootfig/expressions/__init__.py +34 -0
  44. rootfig-0.1.0/src/rootfig/expressions/functions.py +151 -0
  45. rootfig-0.1.0/src/rootfig/expressions/parser.py +462 -0
  46. rootfig-0.1.0/src/rootfig/histograms/__init__.py +67 -0
  47. rootfig-0.1.0/src/rootfig/histograms/build.py +197 -0
  48. rootfig-0.1.0/src/rootfig/histograms/cutflow.py +195 -0
  49. rootfig-0.1.0/src/rootfig/histograms/efficiency.py +214 -0
  50. rootfig-0.1.0/src/rootfig/histograms/normalize.py +124 -0
  51. rootfig-0.1.0/src/rootfig/histograms/pipeline.py +186 -0
  52. rootfig-0.1.0/src/rootfig/histograms/ratio.py +161 -0
  53. rootfig-0.1.0/src/rootfig/histograms/stats.py +183 -0
  54. rootfig-0.1.0/src/rootfig/io/__init__.py +12 -0
  55. rootfig-0.1.0/src/rootfig/io/sources.py +487 -0
  56. rootfig-0.1.0/src/rootfig/model/__init__.py +27 -0
  57. rootfig-0.1.0/src/rootfig/model/binning.py +237 -0
  58. rootfig-0.1.0/src/rootfig/model/cuts.py +85 -0
  59. rootfig-0.1.0/src/rootfig/model/samples.py +284 -0
  60. rootfig-0.1.0/src/rootfig/model/style.py +144 -0
  61. rootfig-0.1.0/src/rootfig/model/units.py +107 -0
  62. rootfig-0.1.0/src/rootfig/model/variables.py +95 -0
  63. rootfig-0.1.0/src/rootfig/plotting/__init__.py +78 -0
  64. rootfig-0.1.0/src/rootfig/plotting/annotations.py +165 -0
  65. rootfig-0.1.0/src/rootfig/plotting/correlation.py +93 -0
  66. rootfig-0.1.0/src/rootfig/plotting/figure.py +458 -0
  67. rootfig-0.1.0/src/rootfig/plotting/hist1d.py +421 -0
  68. rootfig-0.1.0/src/rootfig/plotting/hist2d.py +59 -0
  69. rootfig-0.1.0/src/rootfig/plotting/points.py +78 -0
  70. rootfig-0.1.0/src/rootfig/plotting/ratio.py +163 -0
  71. rootfig-0.1.0/src/rootfig/plotting/result.py +131 -0
  72. rootfig-0.1.0/src/rootfig/plotting/style.py +308 -0
  73. rootfig-0.1.0/src/rootfig/py.typed +0 -0
  74. rootfig-0.1.0/src/rootfig/selection/__init__.py +23 -0
  75. rootfig-0.1.0/src/rootfig/selection/columns.py +479 -0
  76. rootfig-0.1.0/tests/conftest.py +181 -0
  77. rootfig-0.1.0/tests/data/split_collection.root +0 -0
  78. rootfig-0.1.0/tests/test_api.py +1008 -0
  79. rootfig-0.1.0/tests/test_expressions.py +310 -0
  80. rootfig-0.1.0/tests/test_gallery.py +111 -0
  81. rootfig-0.1.0/tests/test_histograms.py +775 -0
  82. rootfig-0.1.0/tests/test_io.py +321 -0
  83. rootfig-0.1.0/tests/test_model.py +420 -0
  84. rootfig-0.1.0/tests/test_plotting.py +977 -0
  85. rootfig-0.1.0/tests/test_selection.py +397 -0
  86. rootfig-0.1.0/tests/test_tutorials.py +63 -0
@@ -0,0 +1,21 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ coverage.xml
12
+ htmlcov/
13
+ site/
14
+ *.root
15
+ !tests/data/*.root
16
+ .ipynb_checkpoints/
17
+ .DS_Store
18
+
19
+ # generated example output and pytest-mpl results
20
+ examples/out/
21
+ mpl-results/
@@ -0,0 +1,87 @@
1
+ # Changelog
2
+
3
+ All notable changes to rootfig are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
+ [Semantic Versioning](https://semver.org/): until 1.0 the public API may
6
+ change in minor releases, with deprecation warnings where practical.
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-09-11
11
+
12
+ First release.
13
+
14
+ ### Added
15
+
16
+ - `rf.plot`: one call from ROOT files (TTree or RNTuple, globs, multiple
17
+ files) to a styled matplotlib figure, with selections, weights, shared
18
+ binning, overlays, stacks, observed-data points, normalisation, ratio
19
+ panels, log axes, flow-bin display, a broken x axis (`xbreak`), legends,
20
+ statistics boxes and experiment labels.
21
+ - `rf.plot_histograms` for existing `hist.Hist` objects (plain count
22
+ storages are converted to `Weight` storage; overlays may differ in
23
+ binning), `rf.plot2d`, `rf.correlation`, `rf.summarize`,
24
+ `rf.histogram`/`rf.histograms`, `rf.load`, `rf.evaluate`, `rf.ratio`,
25
+ `rf.log_bins`, `rf.use_style`.
26
+ - `rf.cutflow()`: weighted yields, raw counts and step efficiencies after
27
+ successive cuts, per sample (`CutflowTable`, `Cutflow`).
28
+ - `rf.efficiency()`: pass/total versus a variable with Wilson intervals
29
+ (`Efficiency`); `rf.profile()`: mean or standard deviation of `y` in bins
30
+ of `x` (`Profile`), for resolution plots.
31
+ - Significance panel: `ratio="significance"` / `"s/sqrt(b)"` /
32
+ `"s/sqrt(s+b)"` or `("s/sqrt(b)", "Signal")`; `rf.significance()`.
33
+ - Luminosity normalisation: `Sample(xsec=..., ngen=...)` plus `lumi=` scales
34
+ simulated samples to expected yields (`xsec × lumi / ngen`, units in
35
+ strings such as `"1.2 fb"`, `"10.8 ab^-1"`); `ngen` may name an object in
36
+ the file (a `TParameter` such as FCCAnalyses' `eventsProcessed`, or a
37
+ sum-of-weights histogram). `LuminosityError`.
38
+ - `Sample`, `Variable`, `Cut` and `Style` descriptions for composable
39
+ analysis scripts; `Style.lumi`/`Style.com` accept units and
40
+ `Style(lumi_unit=..., com_unit=...)` set the defaults for numbers.
41
+ - Expression language in Python syntax with `and`/`or`/`not`, chained
42
+ comparisons, backtick-quoted and dotted branch names
43
+ (`ReconstructedParticles.momentum.x`, as podio/EDM4hep files write them),
44
+ NumPy functions, per-event reductions (`count`, `sum`, `min`, `max`,
45
+ `mean`, `any`, `all`, `first`, ...) and kinematic helpers `pt`, `p`,
46
+ `theta`, `costheta`, `eta`, `phi`, `mass` from Cartesian components.
47
+ - Documented and tested per-event/per-object semantics for selections and
48
+ weights, with explicit errors for ambiguous combinations. Fixed-size
49
+ collections (`float x[3]` branches, two-dimensional NumPy arrays) follow
50
+ the per-object rules like variable-length lists. Split TTree branches and
51
+ nested RNTuple fields are listed and read by their dotted leaf name.
52
+ - Experiment-neutral default style with a 40-colour cycle (`tab10` extended
53
+ by the `tab20` light companions and `tab20b`/`tab20c`); mplhep styles and
54
+ labels for ATLAS, CMS, LHCb, ALICE and DUNE via `Style(experiment=...)`.
55
+ - Automatic y headroom accounts for the actual legend, experiment label,
56
+ statistics box and text sizes, so they never cover a histogram (unless
57
+ `ylim` sets the upper value).
58
+ - A gallery (`examples/gallery.py`, `docs/gallery.md`) with 20 examples
59
+ covering the plotting options, each shown next to its code, doubling as an
60
+ image-regression suite (`tests/test_gallery.py`, pytest-mpl) whose
61
+ baselines are the documentation images.
62
+ - Lower-layer entry points for callers building on rootfig:
63
+ `rootfig.selection.prepare`/`boolean_mask`, `rootfig.histograms.fill`,
64
+ `normalize_hist`, `as_weight_storage`, `read_arrays`, `source_length`,
65
+ `rootfig.plotting.draw_histograms`, `show_flow_bins`, `fold_flow_bins`.
66
+
67
+ ### Behaviour worth knowing
68
+
69
+ - `nan`/`inf` values are dropped with a `RootfigWarning`
70
+ (`nonfinite="error"` raises), also in cut flows; missing values (`None`),
71
+ missing collections and missing event weights drop the entry or event and
72
+ are counted separately in `Summary`.
73
+ - Negative weights: a weighted variance that comes out negative gives `nan`
74
+ for the standard deviation of `Summary` and `Profile` (and the profile
75
+ error); an efficiency outside `[0, 1]` has `nan` interval bounds and
76
+ warns. Histogram contents, means and yields are unaffected.
77
+ - `flow="show"` and `flow="sum"` are applied by rootfig before ratios,
78
+ significances, stack bands and limits are computed, identically for all
79
+ samples; `normalize="width"`/`"density"` divide flow bins by the
80
+ neighbouring bin width.
81
+ - Ratios, significances and efficiencies require identical bin edges (up to
82
+ a millionth of a bin width). `Sample.scale`, `xsec`, `ngen` and `lumi`
83
+ must be finite. `logx`/`logy` default to the `Variable`'s `log` flag.
84
+ - Requires Python 3.12 and matplotlib 3.10 or newer.
85
+
86
+ [Unreleased]: https://github.com/jbeirer/rootfig/compare/v0.1.0...HEAD
87
+ [0.1.0]: https://github.com/jbeirer/rootfig/releases/tag/v0.1.0
@@ -0,0 +1,129 @@
1
+ # Contributing to rootfig
2
+
3
+ Thanks for helping. Issues and pull requests are welcome at
4
+ <https://github.com/jbeirer/rootfig>.
5
+
6
+ ## Development setup
7
+
8
+ rootfig is managed with [uv](https://docs.astral.sh/uv/).
9
+
10
+ ```bash
11
+ git clone https://github.com/jbeirer/rootfig
12
+ cd rootfig
13
+ uv sync --all-groups # runtime, dev and docs dependencies
14
+ uv run pre-commit install # optional: run the linters on every commit
15
+ ```
16
+
17
+ ## Checks
18
+
19
+ ```bash
20
+ uv run pytest # tests (ROOT files are generated on the fly)
21
+ uv run pytest --cov # with coverage
22
+ uv run ruff check . && uv run ruff format --check .
23
+ uv run mypy # strict type checking of src/
24
+ uv run mkdocs serve # documentation preview
25
+ uv build && uvx twine check dist/* # packaging
26
+ ```
27
+
28
+ CI runs all of these on Python 3.12, 3.13 and 3.14 (Linux), plus macOS
29
+ on 3.13. Tests that use ROOT's tutorial files run only when
30
+ `root-config` is available locally and are skipped otherwise; do not add
31
+ tests that require ROOT or network access.
32
+
33
+ ## Layout
34
+
35
+ ```
36
+ src/rootfig/
37
+ api.py plot(), histogram(), load(), ... (orchestration only)
38
+ errors.py exception hierarchy
39
+ expressions/ parse, validate and evaluate expression strings
40
+ io/ file and in-memory data sources
41
+ model/ Sample, Variable, Cut, Style, binning
42
+ selection/ per-event / per-object semantics -> flat columns
43
+ histograms/ filling, normalisation, ratios, statistics, pipeline
44
+ plotting/ matplotlib/mplhep rendering, styles, annotations
45
+ tests/ one module per layer plus end-to-end API tests
46
+ docs/ MkDocs sources
47
+ ```
48
+
49
+ Keep the layers independent: `plotting` must not read files, `selection`
50
+ must not know about matplotlib, and so on. Public functions carry NumPy-style
51
+ docstrings and full type hints.
52
+
53
+ ## Test data
54
+
55
+ Almost all tests generate their ROOT files with uproot on the fly. The one
56
+ committed file, `tests/data/split_collection.root`, has a split
57
+ `std::vector<struct>` branch and a `TParameter` the way podio/EDM4hep and
58
+ FCCAnalyses write them. uproot cannot write such a file, so it was produced
59
+ with PyROOT (ROOT 6.40) by this script; rerun it only if the layout has to
60
+ change, and keep the file small:
61
+
62
+ ```python
63
+ import ROOT
64
+
65
+ ROOT.gInterpreter.Declare("""
66
+ struct Vec3 { float x; float y; float z; };
67
+ struct Particle { Vec3 momentum; float energy; int charge; };
68
+ """)
69
+ f = ROOT.TFile("split_collection.root", "RECREATE")
70
+ t = ROOT.TTree("events", "events")
71
+ v = ROOT.std.vector("Particle")()
72
+ t.Branch("ReconstructedParticles", v, 32000, 99) # split level 99 -> dotted sub-branches
73
+ r = ROOT.TRandom3(1)
74
+ for _ in range(200):
75
+ v.clear()
76
+ for _ in range(r.Poisson(3)):
77
+ p = ROOT.Particle()
78
+ p.momentum.x, p.momentum.y, p.momentum.z = r.Gaus(0, 20), r.Gaus(0, 20), r.Gaus(0, 20)
79
+ p.energy = (p.momentum.x**2 + p.momentum.y**2 + p.momentum.z**2) ** 0.5 + 0.1
80
+ p.charge = -1 if r.Uniform() < 0.5 else 1
81
+ v.push_back(p)
82
+ t.Fill()
83
+ ROOT.TParameter("int")("eventsProcessed", 200).Write()
84
+ f.Write()
85
+ f.Close()
86
+ ```
87
+
88
+ ## Figures and the gallery
89
+
90
+ `examples/gallery.py` is both the showcase and the image-regression suite. Each
91
+ example is a small function returning a `Plot`; `docs/gallery.md` shows every
92
+ figure next to that function's source (a MkDocs hook, `docs/hooks/gallery.py`),
93
+ and `tests/test_gallery.py` renders all of them and, with `--mpl`, compares
94
+ them pixel-wise (pytest-mpl, RMS tolerance 2) against `docs/images/gallery/`.
95
+ Those PNGs are therefore the documentation images *and* the baselines.
96
+
97
+ ```bash
98
+ MPLBACKEND=Agg uv run python examples/gallery.py # look at examples/out/*.png
99
+ uv run pytest tests/test_gallery.py --mpl # compare against the baselines
100
+ uv run pytest tests/test_gallery.py --mpl-generate-path=docs/images/gallery # accept changes
101
+ ```
102
+
103
+ After any visual change: regenerate the baselines, open the PNGs and check
104
+ them by eye, and commit them with the code. CI compares on Linux only (fonts
105
+ differ elsewhere) and, when a comparison fails, uploads an HTML report with
106
+ baseline, result and difference images as the `mpl-results-*` artifact. To add
107
+ an example, register a function with `@example(name, title)` and give it a
108
+ docstring; the test suite fails until its baseline image exists.
109
+
110
+ ## Pull requests
111
+
112
+ - Add tests for behaviour changes; unit tests assert histogram contents and
113
+ matplotlib structure. Rendered output is covered by the gallery (below).
114
+ - Update `CHANGELOG.md` under "Unreleased".
115
+ - Run the checks above before pushing; `pre-commit run --all-files` does most
116
+ of it.
117
+
118
+ ## Releasing
119
+
120
+ 1. Update the version in `src/rootfig/__init__.py` and move the changelog
121
+ entries under a new heading.
122
+ 2. Commit, tag `vX.Y.Z`, push the tag.
123
+ 3. The `release.yml` workflow builds the distribution and publishes it to
124
+ PyPI via Trusted Publishing (configure the publisher on PyPI first:
125
+ repository `jbeirer/rootfig`, workflow `release.yml`, environment `pypi`).
126
+
127
+ The documentation is published by the `docs` job of `ci.yml` on every push to
128
+ `main` (`mkdocs gh-deploy` to the `gh-pages` branch). Once, in the repository
129
+ settings, set GitHub Pages to serve from that branch.
rootfig-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joshua Falco Beirer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
rootfig-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,202 @@
1
+ Metadata-Version: 2.5
2
+ Name: rootfig
3
+ Version: 0.1.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://github.com/jbeirer/rootfig#readme
7
+ Project-URL: Repository, https://github.com/jbeirer/rootfig
8
+ Project-URL: Issues, https://github.com/jbeirer/rootfig/issues
9
+ Project-URL: Changelog, https://github.com/jbeirer/rootfig/blob/main/CHANGELOG.md
10
+ Author-email: Joshua Falco Beirer <jbeirer@cern.ch>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: HEP,RNTuple,ROOT,TTree,awkward,histogram,matplotlib,mplhep,plotting,uproot
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Scientific/Engineering :: Physics
24
+ Classifier: Topic :: Scientific/Engineering :: Visualization
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.12
27
+ Requires-Dist: awkward>=2.6
28
+ Requires-Dist: hist>=2.7
29
+ Requires-Dist: matplotlib>=3.10
30
+ Requires-Dist: mplhep>=0.3.50
31
+ Requires-Dist: numpy>=1.26
32
+ Requires-Dist: uproot>=5.3
33
+ Description-Content-Type: text/markdown
34
+
35
+ # rootfig
36
+
37
+ **Publication-quality figures straight from ROOT trees, without ROOT.**
38
+
39
+ `rootfig` is the `TTree::Draw` workflow for the Scientific Python HEP stack:
40
+ give it ROOT files, a tree, an expression, a selection and a weight, and get a
41
+ styled matplotlib figure back in one call. It reads with
42
+ [uproot](https://github.com/scikit-hep/uproot5), computes with
43
+ [Awkward Array](https://github.com/scikit-hep/awkward), fills
44
+ [hist](https://github.com/scikit-hep/hist) histograms and draws with
45
+ [mplhep](https://github.com/scikit-hep/mplhep). It adds the missing glue:
46
+ predictable per-event/per-object selection semantics, weights, shared
47
+ binning across samples, normalisation, ratio panels and good defaults.
48
+
49
+ [![CI](https://github.com/jbeirer/rootfig/actions/workflows/ci.yml/badge.svg)](https://github.com/jbeirer/rootfig/actions/workflows/ci.yml)
50
+ [![codecov](https://codecov.io/gh/jbeirer/rootfig/branch/main/graph/badge.svg)](https://codecov.io/gh/jbeirer/rootfig)
51
+ [![PyPI](https://img.shields.io/pypi/v/rootfig.svg)](https://pypi.org/project/rootfig/)
52
+ [![Python](https://img.shields.io/pypi/pyversions/rootfig.svg)](https://pypi.org/project/rootfig/)
53
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
54
+
55
+ ```python
56
+ import rootfig as rf
57
+
58
+ rf.plot("events.root", "Muon_pt", tree="events", selection="Muon_pt > 20", bins=50)
59
+ ```
60
+
61
+ <p align="center">
62
+ <img src="docs/images/gallery/stack_data.png" alt="Stacked simulation with data and a ratio panel" width="48%">
63
+ <img src="docs/images/gallery/xbreak_ratio.png" alt="Broken x axis with a ratio panel" width="48%">
64
+ </p>
65
+ <p align="center">
66
+ <img src="docs/images/gallery/object_vs_event.png" alt="Per-object versus per-event selections" width="48%">
67
+ <img src="docs/images/gallery/hist2d.png" alt="Two-dimensional histogram" width="48%">
68
+ </p>
69
+
70
+ These and a dozen more figures, each next to the code that made it, are in the
71
+ [gallery](https://jbeirer.github.io/rootfig/gallery/). All of them come from
72
+ [`examples/gallery.py`](examples/gallery.py), which writes toy ROOT files and
73
+ draws every example in a few seconds; the same figures are pixel-compared in CI.
74
+
75
+ ## Installation
76
+
77
+ ```bash
78
+ pip install rootfig
79
+ # or
80
+ uv add rootfig
81
+ ```
82
+
83
+ Python 3.12 or newer. No ROOT installation is needed; `TTree` and `RNTuple`
84
+ files are both supported.
85
+
86
+ ## Quick start
87
+
88
+ ```python
89
+ import rootfig as rf
90
+
91
+ # Overlay two samples, normalised to unity, with a ratio panel.
92
+ rf.plot(
93
+ ["signal.root", "background.root"],
94
+ "Muon_pt",
95
+ tree="events",
96
+ selection="abs(Muon_eta) < 2.5",
97
+ weight="event_weight",
98
+ bins=(50, 0, 200),
99
+ normalize=True,
100
+ ratio=True,
101
+ )
102
+ ```
103
+
104
+ For analysis scripts with many samples, variables and plots, describe things
105
+ once and reuse them:
106
+
107
+ ```python
108
+ import rootfig as rf
109
+
110
+ signal = rf.Sample("sig_*.root", tree="events", label="Signal", weight="mc_weight")
111
+ background = rf.Sample("bkg.root", tree="events", label="Background", weight="mc_weight")
112
+ data = rf.Sample("data.root", tree="events", label="Data", is_data=True)
113
+
114
+ pt = rf.Variable("Muon_pt", bins=(50, 0, 200), label=r"$p_T^{\mu}$", unit="GeV")
115
+ baseline = rf.Cut("nMuon >= 1") & "abs(Muon_eta) < 2.5"
116
+ style = rf.Style(experiment="ATLAS", status="Internal", lumi=140, com=13.6)
117
+
118
+ p = rf.plot(
119
+ [background, signal],
120
+ pt,
121
+ observed=data,
122
+ selection=baseline,
123
+ stack=True,
124
+ ratio=True,
125
+ logy=True,
126
+ style=style,
127
+ )
128
+ p.ax.set_ylim(top=1e5) # it is a normal matplotlib Axes
129
+ p.save("muon_pt.pdf")
130
+ ```
131
+
132
+ Everything you get back is a standard object: `p.fig` and `p.ax` are
133
+ matplotlib `Figure`/`Axes`, `p.hists` are `hist.Hist` objects, and
134
+ `rf.load(...)` returns Awkward arrays.
135
+
136
+ ## Features
137
+
138
+ - **One call from files to figure**, reading only the branches the expressions need.
139
+ - **Expressions in Python syntax**: `sqrt(px**2 + py**2)`, `count(Jet_pt) >= 2`,
140
+ `` `jet1_b-tag` > 0.5 ``, `and`/`or`/`not`, chained comparisons.
141
+ - **Jagged branches done right**: per-object cuts mask objects, per-event
142
+ cuts drop events, ambiguous combinations raise a clear error instead of
143
+ silently broadcasting.
144
+ - **Weights**: per-event weights broadcast onto objects, per-object weights,
145
+ constant scale factors, multiplicative combination of sample and plot weights.
146
+ - **Histograms with uncertainties** (`hist` with `Weight` storage), shared
147
+ binning across samples, automatic or robust ranges, log bins, flow bins.
148
+ - **Overlays, stacks, data points, ratio panels** with correct error
149
+ propagation for weighted histograms and a reference-uncertainty band.
150
+ - **Normalisation**: to unity, density, per bin width, or to a number; or to
151
+ a **luminosity** from cross sections and generated-event counts
152
+ (`Sample(xsec="0.2 pb", ngen="eventsProcessed")`, `lumi="10.8 ab^-1"`).
153
+ - **Analysis tables and panels**: cut flows with yields and efficiencies,
154
+ significance panels (S/√B), efficiency-versus-variable plots with binomial
155
+ intervals, profiles and resolutions.
156
+ - **Experiment-neutral defaults**, with mplhep styles and labels for ATLAS,
157
+ CMS, LHCb, ALICE and DUNE one keyword away; any other experiment name, GeV
158
+ and ab⁻¹ work too.
159
+ - **EDM4hep-friendly**: sub-branches of split collections are addressed as
160
+ `ReconstructedParticles.momentum.x`, with `pt`, `p`, `theta`, `costheta`,
161
+ `eta`, `phi` and `mass` helpers.
162
+ - **Also**: 2D histograms, summary statistics tables, statistics boxes,
163
+ correlation matrices, multi-file globs, entry ranges for quick looks.
164
+
165
+ ## Documentation
166
+
167
+ - [Quick start](docs/quickstart.md)
168
+ - [Expressions and selections](docs/expressions.md)
169
+ - [Samples, variables, cuts and styles](docs/composable.md)
170
+ - [Plotting options](docs/plotting.md)
171
+ - [Relation to uproot, Awkward, hist, mplhep and matplotlib](docs/ecosystem.md)
172
+
173
+ ## Relation to the ecosystem
174
+
175
+ `rootfig` does not replace any of the libraries it builds on:
176
+
177
+ | Task | Library | What rootfig adds |
178
+ | --- | --- | --- |
179
+ | Reading ROOT files | uproot | file globs, tree auto-detection, reading only the required branches |
180
+ | Jagged arrays | Awkward Array | the per-event/per-object rules for cuts and weights |
181
+ | Histograms | hist / boost-histogram | shared binning, automatic ranges, normalisation, ratios |
182
+ | Drawing | mplhep + matplotlib | overlays, stacks, ratio panels, labels and legends with good defaults |
183
+
184
+ If you already have `hist.Hist` objects, `rf.plot_histograms` draws them with
185
+ the same options. If you want the arrays, `rf.load` returns them.
186
+
187
+ ## Development
188
+
189
+ ```bash
190
+ git clone https://github.com/jbeirer/rootfig
191
+ cd rootfig
192
+ uv sync --all-groups
193
+ uv run pytest
194
+ uv run ruff check . && uv run ruff format --check .
195
+ uv run mypy
196
+ ```
197
+
198
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
199
+
200
+ ## License
201
+
202
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,168 @@
1
+ # rootfig
2
+
3
+ **Publication-quality figures straight from ROOT trees, without ROOT.**
4
+
5
+ `rootfig` is the `TTree::Draw` workflow for the Scientific Python HEP stack:
6
+ give it ROOT files, a tree, an expression, a selection and a weight, and get a
7
+ styled matplotlib figure back in one call. It reads with
8
+ [uproot](https://github.com/scikit-hep/uproot5), computes with
9
+ [Awkward Array](https://github.com/scikit-hep/awkward), fills
10
+ [hist](https://github.com/scikit-hep/hist) histograms and draws with
11
+ [mplhep](https://github.com/scikit-hep/mplhep). It adds the missing glue:
12
+ predictable per-event/per-object selection semantics, weights, shared
13
+ binning across samples, normalisation, ratio panels and good defaults.
14
+
15
+ [![CI](https://github.com/jbeirer/rootfig/actions/workflows/ci.yml/badge.svg)](https://github.com/jbeirer/rootfig/actions/workflows/ci.yml)
16
+ [![codecov](https://codecov.io/gh/jbeirer/rootfig/branch/main/graph/badge.svg)](https://codecov.io/gh/jbeirer/rootfig)
17
+ [![PyPI](https://img.shields.io/pypi/v/rootfig.svg)](https://pypi.org/project/rootfig/)
18
+ [![Python](https://img.shields.io/pypi/pyversions/rootfig.svg)](https://pypi.org/project/rootfig/)
19
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
20
+
21
+ ```python
22
+ import rootfig as rf
23
+
24
+ rf.plot("events.root", "Muon_pt", tree="events", selection="Muon_pt > 20", bins=50)
25
+ ```
26
+
27
+ <p align="center">
28
+ <img src="docs/images/gallery/stack_data.png" alt="Stacked simulation with data and a ratio panel" width="48%">
29
+ <img src="docs/images/gallery/xbreak_ratio.png" alt="Broken x axis with a ratio panel" width="48%">
30
+ </p>
31
+ <p align="center">
32
+ <img src="docs/images/gallery/object_vs_event.png" alt="Per-object versus per-event selections" width="48%">
33
+ <img src="docs/images/gallery/hist2d.png" alt="Two-dimensional histogram" width="48%">
34
+ </p>
35
+
36
+ These and a dozen more figures, each next to the code that made it, are in the
37
+ [gallery](https://jbeirer.github.io/rootfig/gallery/). All of them come from
38
+ [`examples/gallery.py`](examples/gallery.py), which writes toy ROOT files and
39
+ draws every example in a few seconds; the same figures are pixel-compared in CI.
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ pip install rootfig
45
+ # or
46
+ uv add rootfig
47
+ ```
48
+
49
+ Python 3.12 or newer. No ROOT installation is needed; `TTree` and `RNTuple`
50
+ files are both supported.
51
+
52
+ ## Quick start
53
+
54
+ ```python
55
+ import rootfig as rf
56
+
57
+ # Overlay two samples, normalised to unity, with a ratio panel.
58
+ rf.plot(
59
+ ["signal.root", "background.root"],
60
+ "Muon_pt",
61
+ tree="events",
62
+ selection="abs(Muon_eta) < 2.5",
63
+ weight="event_weight",
64
+ bins=(50, 0, 200),
65
+ normalize=True,
66
+ ratio=True,
67
+ )
68
+ ```
69
+
70
+ For analysis scripts with many samples, variables and plots, describe things
71
+ once and reuse them:
72
+
73
+ ```python
74
+ import rootfig as rf
75
+
76
+ signal = rf.Sample("sig_*.root", tree="events", label="Signal", weight="mc_weight")
77
+ background = rf.Sample("bkg.root", tree="events", label="Background", weight="mc_weight")
78
+ data = rf.Sample("data.root", tree="events", label="Data", is_data=True)
79
+
80
+ pt = rf.Variable("Muon_pt", bins=(50, 0, 200), label=r"$p_T^{\mu}$", unit="GeV")
81
+ baseline = rf.Cut("nMuon >= 1") & "abs(Muon_eta) < 2.5"
82
+ style = rf.Style(experiment="ATLAS", status="Internal", lumi=140, com=13.6)
83
+
84
+ p = rf.plot(
85
+ [background, signal],
86
+ pt,
87
+ observed=data,
88
+ selection=baseline,
89
+ stack=True,
90
+ ratio=True,
91
+ logy=True,
92
+ style=style,
93
+ )
94
+ p.ax.set_ylim(top=1e5) # it is a normal matplotlib Axes
95
+ p.save("muon_pt.pdf")
96
+ ```
97
+
98
+ Everything you get back is a standard object: `p.fig` and `p.ax` are
99
+ matplotlib `Figure`/`Axes`, `p.hists` are `hist.Hist` objects, and
100
+ `rf.load(...)` returns Awkward arrays.
101
+
102
+ ## Features
103
+
104
+ - **One call from files to figure**, reading only the branches the expressions need.
105
+ - **Expressions in Python syntax**: `sqrt(px**2 + py**2)`, `count(Jet_pt) >= 2`,
106
+ `` `jet1_b-tag` > 0.5 ``, `and`/`or`/`not`, chained comparisons.
107
+ - **Jagged branches done right**: per-object cuts mask objects, per-event
108
+ cuts drop events, ambiguous combinations raise a clear error instead of
109
+ silently broadcasting.
110
+ - **Weights**: per-event weights broadcast onto objects, per-object weights,
111
+ constant scale factors, multiplicative combination of sample and plot weights.
112
+ - **Histograms with uncertainties** (`hist` with `Weight` storage), shared
113
+ binning across samples, automatic or robust ranges, log bins, flow bins.
114
+ - **Overlays, stacks, data points, ratio panels** with correct error
115
+ propagation for weighted histograms and a reference-uncertainty band.
116
+ - **Normalisation**: to unity, density, per bin width, or to a number; or to
117
+ a **luminosity** from cross sections and generated-event counts
118
+ (`Sample(xsec="0.2 pb", ngen="eventsProcessed")`, `lumi="10.8 ab^-1"`).
119
+ - **Analysis tables and panels**: cut flows with yields and efficiencies,
120
+ significance panels (S/√B), efficiency-versus-variable plots with binomial
121
+ intervals, profiles and resolutions.
122
+ - **Experiment-neutral defaults**, with mplhep styles and labels for ATLAS,
123
+ CMS, LHCb, ALICE and DUNE one keyword away; any other experiment name, GeV
124
+ and ab⁻¹ work too.
125
+ - **EDM4hep-friendly**: sub-branches of split collections are addressed as
126
+ `ReconstructedParticles.momentum.x`, with `pt`, `p`, `theta`, `costheta`,
127
+ `eta`, `phi` and `mass` helpers.
128
+ - **Also**: 2D histograms, summary statistics tables, statistics boxes,
129
+ correlation matrices, multi-file globs, entry ranges for quick looks.
130
+
131
+ ## Documentation
132
+
133
+ - [Quick start](docs/quickstart.md)
134
+ - [Expressions and selections](docs/expressions.md)
135
+ - [Samples, variables, cuts and styles](docs/composable.md)
136
+ - [Plotting options](docs/plotting.md)
137
+ - [Relation to uproot, Awkward, hist, mplhep and matplotlib](docs/ecosystem.md)
138
+
139
+ ## Relation to the ecosystem
140
+
141
+ `rootfig` does not replace any of the libraries it builds on:
142
+
143
+ | Task | Library | What rootfig adds |
144
+ | --- | --- | --- |
145
+ | Reading ROOT files | uproot | file globs, tree auto-detection, reading only the required branches |
146
+ | Jagged arrays | Awkward Array | the per-event/per-object rules for cuts and weights |
147
+ | Histograms | hist / boost-histogram | shared binning, automatic ranges, normalisation, ratios |
148
+ | Drawing | mplhep + matplotlib | overlays, stacks, ratio panels, labels and legends with good defaults |
149
+
150
+ If you already have `hist.Hist` objects, `rf.plot_histograms` draws them with
151
+ the same options. If you want the arrays, `rf.load` returns them.
152
+
153
+ ## Development
154
+
155
+ ```bash
156
+ git clone https://github.com/jbeirer/rootfig
157
+ cd rootfig
158
+ uv sync --all-groups
159
+ uv run pytest
160
+ uv run ruff check . && uv run ruff format --check .
161
+ uv run mypy
162
+ ```
163
+
164
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
165
+
166
+ ## License
167
+
168
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,51 @@
1
+ # API reference
2
+
3
+ ## Top level
4
+
5
+ ::: rootfig.plot
6
+ ::: rootfig.plot_histograms
7
+ ::: rootfig.plot2d
8
+ ::: rootfig.histogram
9
+ ::: rootfig.histograms
10
+ ::: rootfig.load
11
+ ::: rootfig.summarize
12
+ ::: rootfig.correlation
13
+ ::: rootfig.cutflow
14
+ ::: rootfig.efficiency
15
+ ::: rootfig.profile
16
+ ::: rootfig.significance
17
+ ::: rootfig.evaluate
18
+ ::: rootfig.ratio
19
+ ::: rootfig.log_bins
20
+ ::: rootfig.use_style
21
+
22
+ ## Descriptions
23
+
24
+ ::: rootfig.Sample
25
+ ::: rootfig.Variable
26
+ ::: rootfig.Cut
27
+ ::: rootfig.Style
28
+
29
+ ## Results
30
+
31
+ ::: rootfig.Plot
32
+ ::: rootfig.Histogram
33
+ ::: rootfig.Ratio
34
+ ::: rootfig.Summary
35
+ ::: rootfig.SummaryTable
36
+ ::: rootfig.Cutflow
37
+ ::: rootfig.CutflowTable
38
+ ::: rootfig.Efficiency
39
+ ::: rootfig.Profile
40
+
41
+ ## Errors and warnings
42
+
43
+ ::: rootfig.errors
44
+
45
+ ## Lower layers
46
+
47
+ ::: rootfig.io
48
+ ::: rootfig.expressions
49
+ ::: rootfig.selection
50
+ ::: rootfig.histograms
51
+ ::: rootfig.plotting