rootfig 0.2.2__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 (107) hide show
  1. {rootfig-0.2.2 → rootfig-0.3.0}/PKG-INFO +41 -24
  2. {rootfig-0.2.2 → rootfig-0.3.0}/README.md +40 -23
  3. {rootfig-0.2.2 → rootfig-0.3.0}/docs/composable.md +7 -5
  4. {rootfig-0.2.2 → 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.2 → rootfig-0.3.0}/docs/plotting.md +95 -6
  24. {rootfig-0.2.2 → rootfig-0.3.0}/docs/quickstart.md +5 -0
  25. {rootfig-0.2.2 → rootfig-0.3.0}/examples/gallery/__init__.py +24 -14
  26. {rootfig-0.2.2 → rootfig-0.3.0}/examples/gallery/registry.py +5 -2
  27. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/__init__.py +1 -1
  28. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/api.py +31 -7
  29. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/histograms/pipeline.py +11 -3
  30. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/model/__init__.py +10 -1
  31. rootfig-0.3.0/src/rootfig/model/binning.py +465 -0
  32. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/model/variables.py +8 -5
  33. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/plotting/figure.py +9 -3
  34. {rootfig-0.2.2 → rootfig-0.3.0}/tests/test_api.py +93 -1
  35. {rootfig-0.2.2 → rootfig-0.3.0}/tests/test_model.py +207 -5
  36. {rootfig-0.2.2 → rootfig-0.3.0}/tests/test_plotting.py +7 -7
  37. rootfig-0.2.2/docs/images/gallery/arrays.png +0 -0
  38. rootfig-0.2.2/docs/images/gallery/cms_density.png +0 -0
  39. rootfig-0.2.2/docs/images/gallery/correlation.png +0 -0
  40. rootfig-0.2.2/docs/images/gallery/expressions.png +0 -0
  41. rootfig-0.2.2/docs/images/gallery/fill_stats.png +0 -0
  42. rootfig-0.2.2/docs/images/gallery/hist2d.png +0 -0
  43. rootfig-0.2.2/docs/images/gallery/log_axes.png +0 -0
  44. rootfig-0.2.2/docs/images/gallery/luminosity.png +0 -0
  45. rootfig-0.2.2/docs/images/gallery/many_plots.png +0 -0
  46. rootfig-0.2.2/docs/images/gallery/object_vs_event.png +0 -0
  47. rootfig-0.2.2/docs/images/gallery/overlay_ratio.png +0 -0
  48. rootfig-0.2.2/docs/images/gallery/quick.png +0 -0
  49. rootfig-0.2.2/docs/images/gallery/ratio_reference.png +0 -0
  50. rootfig-0.2.2/docs/images/gallery/robust_range.png +0 -0
  51. rootfig-0.2.2/docs/images/gallery/stack_data.png +0 -0
  52. rootfig-0.2.2/docs/images/gallery/style_colors.png +0 -0
  53. rootfig-0.2.2/docs/images/gallery/variable_bins.png +0 -0
  54. rootfig-0.2.2/docs/images/gallery/xbreak_ratio.png +0 -0
  55. rootfig-0.2.2/src/rootfig/model/binning.py +0 -242
  56. {rootfig-0.2.2 → rootfig-0.3.0}/.gitignore +0 -0
  57. {rootfig-0.2.2 → rootfig-0.3.0}/CONTRIBUTING.md +0 -0
  58. {rootfig-0.2.2 → rootfig-0.3.0}/LICENSE +0 -0
  59. {rootfig-0.2.2 → rootfig-0.3.0}/docs/api.md +0 -0
  60. {rootfig-0.2.2 → rootfig-0.3.0}/docs/ecosystem.md +0 -0
  61. {rootfig-0.2.2 → rootfig-0.3.0}/docs/expressions.md +0 -0
  62. {rootfig-0.2.2 → rootfig-0.3.0}/docs/gallery.md +0 -0
  63. {rootfig-0.2.2 → rootfig-0.3.0}/docs/images/gallery/efficiency.png +0 -0
  64. {rootfig-0.2.2 → rootfig-0.3.0}/docs/images/gallery/profile.png +0 -0
  65. {rootfig-0.2.2 → rootfig-0.3.0}/docs/index.md +0 -0
  66. {rootfig-0.2.2 → rootfig-0.3.0}/examples/gallery/__main__.py +0 -0
  67. {rootfig-0.2.2 → rootfig-0.3.0}/examples/gallery/data.py +0 -0
  68. {rootfig-0.2.2 → rootfig-0.3.0}/mkdocs.yml +0 -0
  69. {rootfig-0.2.2 → rootfig-0.3.0}/pyproject.toml +0 -0
  70. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/_typing.py +0 -0
  71. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/errors.py +0 -0
  72. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/expressions/__init__.py +0 -0
  73. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/expressions/functions.py +0 -0
  74. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/expressions/parser.py +0 -0
  75. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/histograms/__init__.py +0 -0
  76. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/histograms/build.py +0 -0
  77. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/histograms/cutflow.py +0 -0
  78. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/histograms/efficiency.py +0 -0
  79. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/histograms/normalize.py +0 -0
  80. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/histograms/ratio.py +0 -0
  81. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/histograms/stats.py +0 -0
  82. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/io/__init__.py +0 -0
  83. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/io/sources.py +0 -0
  84. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/model/cuts.py +0 -0
  85. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/model/samples.py +0 -0
  86. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/model/style.py +0 -0
  87. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/model/units.py +0 -0
  88. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/plotting/__init__.py +0 -0
  89. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/plotting/annotations.py +0 -0
  90. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/plotting/correlation.py +0 -0
  91. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/plotting/hist1d.py +0 -0
  92. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/plotting/hist2d.py +0 -0
  93. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/plotting/points.py +0 -0
  94. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/plotting/ratio.py +0 -0
  95. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/plotting/result.py +0 -0
  96. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/plotting/style.py +0 -0
  97. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/py.typed +0 -0
  98. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/selection/__init__.py +0 -0
  99. {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/selection/columns.py +0 -0
  100. {rootfig-0.2.2 → rootfig-0.3.0}/tests/conftest.py +0 -0
  101. {rootfig-0.2.2 → rootfig-0.3.0}/tests/data/split_collection.root +0 -0
  102. {rootfig-0.2.2 → rootfig-0.3.0}/tests/test_expressions.py +0 -0
  103. {rootfig-0.2.2 → rootfig-0.3.0}/tests/test_gallery.py +0 -0
  104. {rootfig-0.2.2 → rootfig-0.3.0}/tests/test_histograms.py +0 -0
  105. {rootfig-0.2.2 → rootfig-0.3.0}/tests/test_io.py +0 -0
  106. {rootfig-0.2.2 → rootfig-0.3.0}/tests/test_selection.py +0 -0
  107. {rootfig-0.2.2 → rootfig-0.3.0}/tests/test_tutorials.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: rootfig
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: Publication-quality figures straight from ROOT trees, without ROOT: uproot + Awkward + hist + mplhep with a TTree::Draw-like API.
5
5
  Project-URL: Homepage, https://github.com/jbeirer/rootfig
6
6
  Project-URL: Documentation, https://jbeirer.github.io/rootfig/
@@ -31,22 +31,35 @@ Requires-Dist: numpy>=1.26
31
31
  Requires-Dist: uproot>=5.7.4
32
32
  Description-Content-Type: text/markdown
33
33
 
34
- # rootfig
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>
35
41
 
36
- **Publication-quality figures straight from ROOT trees, without ROOT.**
42
+ <p align="center">
43
+ <strong>Publication-quality figures straight from ROOT trees, without ROOT.</strong>
44
+ </p>
37
45
 
38
- [![Docs](https://img.shields.io/badge/docs-online-blue)](https://jbeirer.github.io/rootfig/)
39
- [![CI](https://github.com/jbeirer/rootfig/actions/workflows/ci.yml/badge.svg)](https://github.com/jbeirer/rootfig/actions/workflows/ci.yml)
40
- [![codecov](https://codecov.io/gh/jbeirer/rootfig/branch/main/graph/badge.svg)](https://codecov.io/gh/jbeirer/rootfig)
41
- [![PyPI](https://img.shields.io/pypi/v/rootfig.svg)](https://pypi.org/project/rootfig/)
42
- [![Python](https://img.shields.io/pypi/pyversions/rootfig.svg)](https://pypi.org/project/rootfig/)
43
- [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
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>
44
51
 
45
- **[Documentation](https://jbeirer.github.io/rootfig/) ·
46
- [Gallery](https://jbeirer.github.io/rootfig/gallery/) ·
47
- [Quick start](https://jbeirer.github.io/rootfig/quickstart/)**
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>
48
61
 
49
- Go from ROOT files to a styled figure in one call. Choose a variable, add a
62
+ Go from a ROOT file to a styled figure in one call. Choose a variable, add a
50
63
  selection, and plot:
51
64
 
52
65
  ```python
@@ -57,15 +70,17 @@ rf.plot("events.root", "Muon_pt", tree="events", selection="Muon_pt > 20", bins=
57
70
 
58
71
  Start with a single distribution; add samples, weights, stacks and ratio
59
72
  panels as your analysis grows. Every plot gives you a matplotlib figure to
60
- customise and save. No ROOT installation required.
73
+ customise and save.
61
74
 
62
75
  <p align="center">
63
- <a href="https://jbeirer.github.io/rootfig/gallery/"><img src="docs/images/gallery/stack_data.png" alt="Stacked simulation with data and a ratio panel" width="48%"></a>
64
- <a href="https://jbeirer.github.io/rootfig/gallery/"><img src="docs/images/gallery/xbreak_ratio.png" alt="Broken x axis with a ratio panel" width="48%"></a>
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>
65
79
  </p>
66
80
  <p align="center">
67
- <a href="https://jbeirer.github.io/rootfig/gallery/"><img src="docs/images/gallery/object_vs_event.png" alt="Per-object versus per-event selections" width="48%"></a>
68
- <a href="https://jbeirer.github.io/rootfig/gallery/"><img src="docs/images/gallery/hist2d.png" alt="Two-dimensional histogram" width="48%"></a>
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>
69
84
  </p>
70
85
 
71
86
  **[Explore the gallery →](https://jbeirer.github.io/rootfig/gallery/)**
@@ -88,16 +103,16 @@ files are both supported.
88
103
  ```python
89
104
  import rootfig as rf
90
105
 
91
- # Overlay two samples, normalised to unity, with a ratio panel.
106
+ # Overlay two samples, each normalised to unity, with a Signal / Background panel.
92
107
  rf.plot(
93
- ["signal.root", "background.root"],
108
+ {"Signal": "signal.root", "Background": "background.root"},
94
109
  "Muon_pt",
95
110
  tree="events",
96
111
  selection="abs(Muon_eta) < 2.5",
97
112
  weight="event_weight",
98
113
  bins=(50, 0, 200),
99
114
  normalize=True,
100
- ratio=True,
115
+ ratio="Background",
101
116
  )
102
117
  ```
103
118
 
@@ -140,8 +155,10 @@ matplotlib `Figure`/`Axes`, `p.hists` are `hist.Hist` objects, and
140
155
  `count(Jet_pt) >= 2` or `Muon_pt > 20`; event and object selections have
141
156
  explicit rules, and event weights carry through to each selected object.
142
157
  - **Compare samples with a few keywords.** Overlays, stacks, data points and
143
- ratio panels share binning and propagate histogram uncertainties.
144
- Normalise to unity, density, bin width or luminosity.
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.
145
162
  - **Style figures for your analysis.** Add experiment labels, units, log axes
146
163
  and broken axes, then refine the result with matplotlib.
147
164
  - **Go beyond 1D plots.** Draw 2D histograms, correlations, efficiencies,
@@ -172,7 +189,7 @@ stack, building on familiar libraries:
172
189
  | --- | --- | --- |
173
190
  | Reading ROOT files | [uproot](https://github.com/scikit-hep/uproot5) | file globs, tree auto-detection, reading only the required branches |
174
191
  | Jagged arrays | [Awkward Array](https://github.com/scikit-hep/awkward) | the per-event/per-object rules for cuts and weights |
175
- | Histograms | [hist](https://github.com/scikit-hep/hist) / boost-histogram | shared binning, automatic ranges, normalisation, ratios |
192
+ | Histograms | [hist](https://github.com/scikit-hep/hist) / boost-histogram | shared binning, robust automatic ranges, normalisation, ratios |
176
193
  | Drawing | [mplhep](https://github.com/scikit-hep/mplhep) + matplotlib | overlays, stacks, ratio panels, labels and legends with good defaults |
177
194
 
178
195
  If you already have `hist.Hist` objects, `rf.plot_histograms` draws them with
@@ -1,19 +1,32 @@
1
- # rootfig
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>
2
8
 
3
- **Publication-quality figures straight from ROOT trees, without ROOT.**
9
+ <p align="center">
10
+ <strong>Publication-quality figures straight from ROOT trees, without ROOT.</strong>
11
+ </p>
4
12
 
5
- [![Docs](https://img.shields.io/badge/docs-online-blue)](https://jbeirer.github.io/rootfig/)
6
- [![CI](https://github.com/jbeirer/rootfig/actions/workflows/ci.yml/badge.svg)](https://github.com/jbeirer/rootfig/actions/workflows/ci.yml)
7
- [![codecov](https://codecov.io/gh/jbeirer/rootfig/branch/main/graph/badge.svg)](https://codecov.io/gh/jbeirer/rootfig)
8
- [![PyPI](https://img.shields.io/pypi/v/rootfig.svg)](https://pypi.org/project/rootfig/)
9
- [![Python](https://img.shields.io/pypi/pyversions/rootfig.svg)](https://pypi.org/project/rootfig/)
10
- [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
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>
11
18
 
12
- **[Documentation](https://jbeirer.github.io/rootfig/) ·
13
- [Gallery](https://jbeirer.github.io/rootfig/gallery/) ·
14
- [Quick start](https://jbeirer.github.io/rootfig/quickstart/)**
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>
15
28
 
16
- Go from ROOT files to a styled figure in one call. Choose a variable, add a
29
+ Go from a ROOT file to a styled figure in one call. Choose a variable, add a
17
30
  selection, and plot:
18
31
 
19
32
  ```python
@@ -24,15 +37,17 @@ rf.plot("events.root", "Muon_pt", tree="events", selection="Muon_pt > 20", bins=
24
37
 
25
38
  Start with a single distribution; add samples, weights, stacks and ratio
26
39
  panels as your analysis grows. Every plot gives you a matplotlib figure to
27
- customise and save. No ROOT installation required.
40
+ customise and save.
28
41
 
29
42
  <p align="center">
30
- <a href="https://jbeirer.github.io/rootfig/gallery/"><img src="docs/images/gallery/stack_data.png" alt="Stacked simulation with data and a ratio panel" width="48%"></a>
31
- <a href="https://jbeirer.github.io/rootfig/gallery/"><img src="docs/images/gallery/xbreak_ratio.png" alt="Broken x axis with a ratio panel" width="48%"></a>
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>
32
46
  </p>
33
47
  <p align="center">
34
- <a href="https://jbeirer.github.io/rootfig/gallery/"><img src="docs/images/gallery/object_vs_event.png" alt="Per-object versus per-event selections" width="48%"></a>
35
- <a href="https://jbeirer.github.io/rootfig/gallery/"><img src="docs/images/gallery/hist2d.png" alt="Two-dimensional histogram" width="48%"></a>
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>
36
51
  </p>
37
52
 
38
53
  **[Explore the gallery →](https://jbeirer.github.io/rootfig/gallery/)**
@@ -55,16 +70,16 @@ files are both supported.
55
70
  ```python
56
71
  import rootfig as rf
57
72
 
58
- # Overlay two samples, normalised to unity, with a ratio panel.
73
+ # Overlay two samples, each normalised to unity, with a Signal / Background panel.
59
74
  rf.plot(
60
- ["signal.root", "background.root"],
75
+ {"Signal": "signal.root", "Background": "background.root"},
61
76
  "Muon_pt",
62
77
  tree="events",
63
78
  selection="abs(Muon_eta) < 2.5",
64
79
  weight="event_weight",
65
80
  bins=(50, 0, 200),
66
81
  normalize=True,
67
- ratio=True,
82
+ ratio="Background",
68
83
  )
69
84
  ```
70
85
 
@@ -107,8 +122,10 @@ matplotlib `Figure`/`Axes`, `p.hists` are `hist.Hist` objects, and
107
122
  `count(Jet_pt) >= 2` or `Muon_pt > 20`; event and object selections have
108
123
  explicit rules, and event weights carry through to each selected object.
109
124
  - **Compare samples with a few keywords.** Overlays, stacks, data points and
110
- ratio panels share binning and propagate histogram uncertainties.
111
- Normalise to unity, density, bin width or luminosity.
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.
112
129
  - **Style figures for your analysis.** Add experiment labels, units, log axes
113
130
  and broken axes, then refine the result with matplotlib.
114
131
  - **Go beyond 1D plots.** Draw 2D histograms, correlations, efficiencies,
@@ -139,7 +156,7 @@ stack, building on familiar libraries:
139
156
  | --- | --- | --- |
140
157
  | Reading ROOT files | [uproot](https://github.com/scikit-hep/uproot5) | file globs, tree auto-detection, reading only the required branches |
141
158
  | Jagged arrays | [Awkward Array](https://github.com/scikit-hep/awkward) | the per-event/per-object rules for cuts and weights |
142
- | Histograms | [hist](https://github.com/scikit-hep/hist) / boost-histogram | shared binning, automatic ranges, normalisation, ratios |
159
+ | Histograms | [hist](https://github.com/scikit-hep/hist) / boost-histogram | shared binning, robust automatic ranges, normalisation, ratios |
143
160
  | Drawing | [mplhep](https://github.com/scikit-hep/mplhep) + matplotlib | overlays, stacks, ratio panels, labels and legends with good defaults |
144
161
 
145
162
  If you already have `hist.Hist` objects, `rf.plot_histograms` draws them with
@@ -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
 
@@ -125,15 +125,16 @@ def quick() -> rf.Plot:
125
125
  @example("overlay_ratio", "Several samples, normalised, with a ratio panel")
126
126
  def overlay_ratio() -> rf.Plot:
127
127
  """A ``{label: file}`` mapping gives one histogram per sample with a binning shared by
128
- all. ``normalize=True`` scales each to unit area and ``ratio=True`` adds a panel with
129
- every sample divided by the first, uncertainties propagated."""
128
+ all. An integer ``bins`` infers the range from every sample at once, cutting the thin
129
+ end of the tail. ``normalize=True`` scales each to unit area and ``ratio=True`` adds a
130
+ panel with every sample divided by the first, uncertainties propagated."""
130
131
  return rf.plot(
131
132
  {"Signal": "signal.root", "Z + jets": "background.root"},
132
133
  "Muon_pt",
133
134
  tree="events",
134
135
  selection="Muon_isTight and abs(Muon_eta) < 2.5",
135
136
  weight="weight",
136
- bins=(40, 0, 400),
137
+ bins=40,
137
138
  unit="GeV",
138
139
  normalize=True,
139
140
  ratio=True,
@@ -204,17 +205,21 @@ def log_axes(signal: rf.Sample, zjets: rf.Sample, diboson: rf.Sample) -> rf.Plot
204
205
  )
205
206
 
206
207
 
207
- @example("robust_range", "Automatic ranges: full versus robust")
208
+ @example(
209
+ "robust_range",
210
+ "Automatic ranges: robust by default, or the full extent",
211
+ image_width="100%",
212
+ )
208
213
  def robust_range(signal: rf.Sample, zjets: rf.Sample) -> rf.Plot:
209
- """Sentinels such as ``-999`` wreck an automatic range. ``range="robust"`` ignores far
210
- outliers when choosing the range (nothing is removed from the data, they end up in the
211
- underflow). Passing ``ax=`` draws into your own axes, so two rootfig plots share one
212
- figure."""
214
+ """Sentinels such as ``-999`` wreck an automatic range, so an integer ``bins`` without an
215
+ explicit range infers one robustly: far outliers are ignored when choosing the axis, and
216
+ the thin end of a tail is cut as long as few entries leave the view (nothing is removed
217
+ from the data, it ends up in the flow bins, where the arrows point at it).
218
+ ``range="auto"`` asks for the full finite minimum and maximum instead. Passing ``ax=``
219
+ draws into your own axes, so two rootfig plots share one figure."""
213
220
  _, (left, right) = plt.subplots(1, 2, figsize=(11, 4.2))
214
221
  rf.plot([signal, zjets], "lep_iso", bins=40, range="auto", ax=left, title='range="auto"')
215
- return rf.plot(
216
- [signal, zjets], "lep_iso", bins=40, range="robust", ax=right, title='range="robust"'
217
- )
222
+ return rf.plot([signal, zjets], "lep_iso", bins=40, ax=right, title="default (robust)")
218
223
 
219
224
 
220
225
  @example("xbreak_ratio", "A broken x axis: peak and far tail without the empty middle")
@@ -233,6 +238,7 @@ def xbreak_ratio(
233
238
  logy=True,
234
239
  xbreak=(125, 195),
235
240
  style=atlas,
241
+ figsize=(7, 5.6),
236
242
  )
237
243
 
238
244
 
@@ -339,27 +345,31 @@ def arrays() -> rf.Plot:
339
345
  def hist2d(signal: rf.Sample, met: rf.Variable) -> rf.Plot:
340
346
  """``rf.plot2d`` fills a 2D histogram of one sample; both variables must have the same
341
347
  structure (both per-event here). ``logz`` and ``cmap`` control the colour scale."""
342
- return rf.plot2d(
348
+ p = rf.plot2d(
343
349
  signal,
344
350
  rf.Variable("sum(Jet_pt)", bins=(40, 0, 800), label=r"$H_T$", unit="GeV"),
345
351
  met,
346
352
  selection="nJet >= 2",
347
353
  logz=True,
348
354
  cmap="magma",
349
- zlabel="Events",
350
355
  )
356
+ p.fig.axes[-1].set_ylabel("") # Omit the colour-bar title in this gallery layout.
357
+ return p
351
358
 
352
359
 
353
360
  @example("correlation", "A correlation matrix")
354
361
  def correlation(signal: rf.Sample) -> rf.Plot:
355
362
  """``rf.correlation`` computes the (weighted) linear correlation of several per-event
356
363
  quantities and draws it as an annotated matrix; ``percent=True`` labels cells in percent."""
357
- return rf.correlation(
364
+ p = rf.correlation(
358
365
  signal,
359
366
  ["MET", "sum(Jet_pt)", "nJet", "nMuon", "m_ll"],
360
367
  labels=[r"$E_T^{miss}$", r"$H_T$", r"$N_{jet}$", r"$N_{\mu}$", r"$m_{\ell\ell}$"],
361
368
  percent=True,
369
+ figsize=(7, 5.6),
362
370
  )
371
+ p.fig.axes[-1].set_ylabel("") # Omit the colour-bar title in this gallery layout.
372
+ return p
363
373
 
364
374
 
365
375
  @example("luminosity", "Cross sections and a luminosity instead of hand-made scale factors")
@@ -27,6 +27,7 @@ class Example:
27
27
  name: str
28
28
  title: str
29
29
  func: Callable[..., rf.Plot]
30
+ image_width: str = "75%"
30
31
 
31
32
  @property
32
33
  def description(self) -> str:
@@ -47,11 +48,13 @@ class Example:
47
48
  EXAMPLES: list[Example] = []
48
49
 
49
50
 
50
- def example(name: str, title: str) -> Callable[[Callable[..., rf.Plot]], Callable[..., rf.Plot]]:
51
+ def example(
52
+ name: str, title: str, *, image_width: str = "75%"
53
+ ) -> Callable[[Callable[..., rf.Plot]], Callable[..., rf.Plot]]:
51
54
  """Register a gallery example."""
52
55
 
53
56
  def register(func: Callable[..., rf.Plot]) -> Callable[..., rf.Plot]:
54
- EXAMPLES.append(Example(name, title, func))
57
+ EXAMPLES.append(Example(name, title, func, image_width=image_width))
55
58
  return func
56
59
 
57
60
  return register
@@ -52,7 +52,7 @@ from rootfig.histograms import (
52
52
  from rootfig.model import Cut, Sample, Style, Variable, log_bins
53
53
  from rootfig.plotting import Plot, use_style
54
54
 
55
- __version__ = "0.2.2"
55
+ __version__ = "0.3.0"
56
56
 
57
57
  __all__ = [
58
58
  "BinningError",