Publication-quality figures straight from ROOT trees and histograms, without ROOT.
Documentation · Gallery · Quick start
Go from a ROOT file to a styled figure in one call. Choose a variable, add a selection, and plot:
import rootfig as rf
rf.plot("events.root", "Muon_pt", tree="events", selection="Muon_pt > 20", bins=50)
Start with a single distribution; add samples, weights, stacks and ratio panels as your analysis grows. Every plot gives you a matplotlib figure to customise and save.
Explore the gallery → See each figure alongside the code that makes it, from simple overlays to stacked data/MC comparisons, broken axes and 2D histograms, in the neutral style or in that of ATLAS, CMS, LHCb, ALICE or DUNE.
Installation
pip install rootfig
# or
uv add rootfig
Python 3.12 or newer. No ROOT installation is needed; TTree and RNTuple
files and stored TH1/TH2 histograms are all supported.
Compare samples in one call
import rootfig as rf
# Overlay two samples, each normalised to unity, with a Signal / Background panel.
rf.plot(
{"Signal": "signal.root", "Background": "background.root"},
"Muon_pt",
tree="events",
selection="abs(Muon_eta) < 2.5",
weight="event_weight",
bins=(50, 0, 200),
normalize=True,
panel="ratio",
reference="Background",
)
Build up to a full analysis
Define samples, variables, cuts and styles once, then reuse them across plots:
import rootfig as rf
signal = rf.Sample("sig_*.root", tree="events", label="Signal", weight="mc_weight")
background = rf.Sample("bkg.root", tree="events", label="Background", weight="mc_weight")
data = rf.Sample("data.root", tree="events", label="Data", is_data=True)
pt = rf.Variable("Muon_pt", bins=(50, 0, 200), label=r"$p_T^{\mu}$", unit="GeV")
baseline = rf.Cut("nMuon >= 1") & "abs(Muon_eta) < 2.5"
style = rf.Style(experiment="ATLAS", status="Internal", lumi=140, com=13.6)
p = rf.plot(
[background, signal],
pt,
observed=data,
selection=baseline,
stack=True,
panel="ratio",
logy=True,
style=style,
)
p.ax.set_ylim(top=1e5) # it is a normal matplotlib Axes
p.save("muon_pt.pdf")
Everything you get back is a standard object: p.fig and p.ax are
matplotlib Figure/Axes, p.hists are hist.Hist objects, and
rf.load(...) returns Awkward arrays.
For histogram files, Group sums processes and a Variable crops and merges
their bins just as it bins a tree. PlotBook draws the variants from one
preparation; rf.ALL discovers every shared histogram for an overview.
# Histogram files: one per process, already scaled to 5 ab^-1
ww = rf.Sample("outputs/p8_ee_WW_ecm240.root", label="WW")
zz = rf.Sample("outputs/p8_ee_ZZ_ecm240.root", label="ZZ")
zh = rf.Sample("outputs/p8_ee_ZH_ecm240.root", label="ZH")
fcc = rf.Style(experiment="FCC-ee", status="Simulation", com="240 GeV", lumi="5 ab^-1")
book = rf.PlotBook(
[rf.Group([ww, zz], label="VV"), zh],
[rf.Variable("zmumu_recoil_m", bins=(200, 120, 140), label="Recoil mass", unit="GeV")],
variants={"stack": {"stack": True}, "nostack": {"stack": ["VV"]}},
plot_kwargs={"style": fcc},
)
book.save_pdf("zh.pdf") # rf.ALL in place of the list plots every histogram the files share
What you can do
- Select events and objects with readable expressions. Write cuts such as
count(Jet_pt) >= 2orMuon_pt > 20; event and object selections have explicit rules, and event weights carry through to each selected object. - Compare samples with a few keywords. Overlays, stacks, data points and a
lower panel (ratio, difference, relative difference, pull or significance,
against a reference you name) share binning and propagate histogram
uncertainties; bin edges
and
(n, low, high)are used as given, while a range inferred from the data ignores far outliers, so-999sentinels do not set the axis. Normalise to unity, density, bin width or luminosity; stack some samples and overlay the rest. Draw several samples as one histogram withrf.Group, each keeping its own weights, cross section and systematics. - Show systematic uncertainties. Attach weight, branch, file or normalisation variations to a sample; stacks and lower panels draw the combined statistical and systematic band, and every component stays accessible.
- Style figures for your analysis. Add experiment labels, units, log axes and broken axes, then refine the result with matplotlib.
- Produce whole sets of plots.
rf.PlotBookruns onerf.plotcall over variables × selections × variants, lazily, and saves each under a deterministic name or all of them as one multipage PDF;rf.ALLdiscovers the variables from the files, andselect()filters the book down while iterating on a plot. - Go beyond 1D plots. Draw 2D histograms, correlations, efficiencies, profiles, resolutions and significance panels; produce cut flows and summary statistics from the same inputs.
- Work directly with your files. Read
TTreeandRNTupledata, combine files with globs, limit entry ranges for quick checks, and use EDM4hep split collections. Only the branches your expressions need are read.
Documentation
Read the docs or browse the gallery for examples with figures and code.
- Quick start: your first plot, selections and weights.
- Expressions and selections: syntax and event/object rules.
- Samples, variables, cuts and styles: reusable analysis definitions.
- Plotting options: binning, normalisation, panels and styling.
- API reference: full signatures and options.
Relation to the ecosystem
rootfig brings a TTree::Draw-like workflow to the Scientific Python HEP
stack, building on familiar libraries:
| Task | Library | What rootfig adds |
|---|---|---|
| Reading ROOT files | uproot | file globs, tree auto-detection, reading only the required branches |
| Jagged arrays | Awkward Array | the per-event/per-object rules for cuts and weights |
| Histograms | hist / boost-histogram | shared binning, robust automatic ranges, normalisation, ratios |
| Drawing | mplhep + matplotlib | overlays, stacks, ratio panels, labels and legends with good defaults |
If your histograms already exist, rf.plot draws them too: name a TH1
stored in the file instead of a branch (rf.plot("zh_histo.root", "m_recoil")),
or pass hist.Hist objects directly. If you want the arrays, rf.load returns them. See
the ecosystem guide for details.
Development
git clone https://github.com/jbeirer/rootfig
cd rootfig
uv sync --all-groups
uv run pytest
uv run ruff check . && uv run ruff format --check .
uv run mypy
See CONTRIBUTING.md for details.
Citation
If rootfig is useful in your research, please cite it:
@software{rootfig,
author = {Beirer, Joshua Falco},
doi = {10.5281/zenodo.22726311},
license = {MIT},
title = {{rootfig}},
url = {https://github.com/jbeirer/rootfig},
year = {2026}
}
License
MIT. See LICENSE.
Release files for rootfig 0.9.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| rootfig-0.9.0.tar.gz | 19.9 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| rootfig-0.9.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 20.1 MB
Release files / rootfig-0.9.0.tar.gz
| Download URL | rootfig-0.9.0.tar.gz |
|---|---|
| Size | 19.9 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c0f8b41ef16a519dda975386e4b8116ae15bb1d26e51a0603eb03357227cf8d7
|
|
BLAKE2b-256 checksum How to use checksums |
9ce63cd03d0979fb18b248c2bf065d51f33fb3e18de3aa0cdab244d66484b0b5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.
Transparency logRelease files / rootfig-0.9.0-py3-none-any.whl
| Download URL | rootfig-0.9.0-py3-none-any.whl |
|---|---|
| Size | 228.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2a967a9cf1cd51f2255b82b0a642e951d0a23a32bc5e13790dbbe9ddc6c458d2
|
|
BLAKE2b-256 checksum How to use checksums |
c06725136c38db049e1c3b02c80e971a2e85887f28a7f8ed300ffbc963c3cac9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.
Transparency log