rootfig 0.2.0__tar.gz → 0.2.1__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 (92) hide show
  1. {rootfig-0.2.0 → rootfig-0.2.1}/CONTRIBUTING.md +11 -3
  2. {rootfig-0.2.0 → rootfig-0.2.1}/PKG-INFO +2 -2
  3. {rootfig-0.2.0 → rootfig-0.2.1}/README.md +1 -1
  4. {rootfig-0.2.0 → rootfig-0.2.1}/docs/gallery.md +21 -15
  5. rootfig-0.2.1/docs/hooks/gallery.py +98 -0
  6. rootfig-0.2.1/docs/images/gallery/overlay_ratio.png +0 -0
  7. {rootfig-0.2.0 → rootfig-0.2.1}/docs/images/gallery/ratio_reference.png +0 -0
  8. rootfig-0.2.1/docs/images/gallery/variable_bins.png +0 -0
  9. rootfig-0.2.1/docs/images/gallery/xbreak_ratio.png +0 -0
  10. {rootfig-0.2.0 → rootfig-0.2.1}/docs/plotting.md +8 -1
  11. rootfig-0.2.0/examples/gallery.py → rootfig-0.2.1/examples/gallery/__init__.py +115 -292
  12. rootfig-0.2.1/examples/gallery/__main__.py +17 -0
  13. rootfig-0.2.1/examples/gallery/data.py +106 -0
  14. rootfig-0.2.1/examples/gallery/registry.py +103 -0
  15. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/__init__.py +1 -1
  16. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/api.py +4 -2
  17. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/plotting/figure.py +68 -0
  18. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/plotting/style.py +79 -7
  19. {rootfig-0.2.0 → rootfig-0.2.1}/tests/test_api.py +62 -7
  20. {rootfig-0.2.0 → rootfig-0.2.1}/tests/test_gallery.py +71 -9
  21. {rootfig-0.2.0 → rootfig-0.2.1}/tests/test_plotting.py +178 -0
  22. rootfig-0.2.0/docs/hooks/gallery.py +0 -63
  23. rootfig-0.2.0/docs/images/gallery/overlay_ratio.png +0 -0
  24. rootfig-0.2.0/docs/images/gallery/variable_bins.png +0 -0
  25. rootfig-0.2.0/docs/images/gallery/xbreak_ratio.png +0 -0
  26. {rootfig-0.2.0 → rootfig-0.2.1}/.gitignore +0 -0
  27. {rootfig-0.2.0 → rootfig-0.2.1}/LICENSE +0 -0
  28. {rootfig-0.2.0 → rootfig-0.2.1}/docs/api.md +0 -0
  29. {rootfig-0.2.0 → rootfig-0.2.1}/docs/composable.md +0 -0
  30. {rootfig-0.2.0 → rootfig-0.2.1}/docs/ecosystem.md +0 -0
  31. {rootfig-0.2.0 → rootfig-0.2.1}/docs/expressions.md +0 -0
  32. {rootfig-0.2.0 → rootfig-0.2.1}/docs/images/gallery/arrays.png +0 -0
  33. {rootfig-0.2.0 → rootfig-0.2.1}/docs/images/gallery/cms_density.png +0 -0
  34. {rootfig-0.2.0 → rootfig-0.2.1}/docs/images/gallery/correlation.png +0 -0
  35. {rootfig-0.2.0 → rootfig-0.2.1}/docs/images/gallery/efficiency.png +0 -0
  36. {rootfig-0.2.0 → rootfig-0.2.1}/docs/images/gallery/expressions.png +0 -0
  37. {rootfig-0.2.0 → rootfig-0.2.1}/docs/images/gallery/fill_stats.png +0 -0
  38. {rootfig-0.2.0 → rootfig-0.2.1}/docs/images/gallery/hist2d.png +0 -0
  39. {rootfig-0.2.0 → rootfig-0.2.1}/docs/images/gallery/log_axes.png +0 -0
  40. {rootfig-0.2.0 → rootfig-0.2.1}/docs/images/gallery/luminosity.png +0 -0
  41. {rootfig-0.2.0 → rootfig-0.2.1}/docs/images/gallery/many_plots.png +0 -0
  42. {rootfig-0.2.0 → rootfig-0.2.1}/docs/images/gallery/object_vs_event.png +0 -0
  43. {rootfig-0.2.0 → rootfig-0.2.1}/docs/images/gallery/profile.png +0 -0
  44. {rootfig-0.2.0 → rootfig-0.2.1}/docs/images/gallery/quick.png +0 -0
  45. {rootfig-0.2.0 → rootfig-0.2.1}/docs/images/gallery/robust_range.png +0 -0
  46. {rootfig-0.2.0 → rootfig-0.2.1}/docs/images/gallery/stack_data.png +0 -0
  47. {rootfig-0.2.0 → rootfig-0.2.1}/docs/images/gallery/style_colors.png +0 -0
  48. {rootfig-0.2.0 → rootfig-0.2.1}/docs/index.md +0 -0
  49. {rootfig-0.2.0 → rootfig-0.2.1}/docs/quickstart.md +0 -0
  50. {rootfig-0.2.0 → rootfig-0.2.1}/mkdocs.yml +0 -0
  51. {rootfig-0.2.0 → rootfig-0.2.1}/pyproject.toml +0 -0
  52. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/_typing.py +0 -0
  53. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/errors.py +0 -0
  54. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/expressions/__init__.py +0 -0
  55. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/expressions/functions.py +0 -0
  56. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/expressions/parser.py +0 -0
  57. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/histograms/__init__.py +0 -0
  58. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/histograms/build.py +0 -0
  59. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/histograms/cutflow.py +0 -0
  60. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/histograms/efficiency.py +0 -0
  61. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/histograms/normalize.py +0 -0
  62. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/histograms/pipeline.py +0 -0
  63. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/histograms/ratio.py +0 -0
  64. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/histograms/stats.py +0 -0
  65. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/io/__init__.py +0 -0
  66. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/io/sources.py +0 -0
  67. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/model/__init__.py +0 -0
  68. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/model/binning.py +0 -0
  69. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/model/cuts.py +0 -0
  70. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/model/samples.py +0 -0
  71. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/model/style.py +0 -0
  72. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/model/units.py +0 -0
  73. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/model/variables.py +0 -0
  74. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/plotting/__init__.py +0 -0
  75. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/plotting/annotations.py +0 -0
  76. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/plotting/correlation.py +0 -0
  77. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/plotting/hist1d.py +0 -0
  78. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/plotting/hist2d.py +0 -0
  79. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/plotting/points.py +0 -0
  80. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/plotting/ratio.py +0 -0
  81. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/plotting/result.py +0 -0
  82. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/py.typed +0 -0
  83. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/selection/__init__.py +0 -0
  84. {rootfig-0.2.0 → rootfig-0.2.1}/src/rootfig/selection/columns.py +0 -0
  85. {rootfig-0.2.0 → rootfig-0.2.1}/tests/conftest.py +0 -0
  86. {rootfig-0.2.0 → rootfig-0.2.1}/tests/data/split_collection.root +0 -0
  87. {rootfig-0.2.0 → rootfig-0.2.1}/tests/test_expressions.py +0 -0
  88. {rootfig-0.2.0 → rootfig-0.2.1}/tests/test_histograms.py +0 -0
  89. {rootfig-0.2.0 → rootfig-0.2.1}/tests/test_io.py +0 -0
  90. {rootfig-0.2.0 → rootfig-0.2.1}/tests/test_model.py +0 -0
  91. {rootfig-0.2.0 → rootfig-0.2.1}/tests/test_selection.py +0 -0
  92. {rootfig-0.2.0 → rootfig-0.2.1}/tests/test_tutorials.py +0 -0
@@ -89,7 +89,9 @@ f.Close()
89
89
 
90
90
  ## Figures and the gallery
91
91
 
92
- `examples/gallery.py` is both the showcase and the image-regression suite. Each
92
+ The `examples/gallery` package is both the showcase and the image-regression
93
+ suite: `__init__.py` holds the shared `define()` block and the examples, `data.py`
94
+ writes the toy files and `registry.py` extracts the source shown in the docs. Each
93
95
  example is a small function returning a `Plot`; `docs/gallery.md` shows every
94
96
  figure next to that function's source (a MkDocs hook, `docs/hooks/gallery.py`),
95
97
  and `tests/test_gallery.py` renders all of them and, with `--mpl`, compares
@@ -97,7 +99,7 @@ them pixel-wise (pytest-mpl, RMS tolerance 2) against `docs/images/gallery/`.
97
99
  Those PNGs are therefore the documentation images *and* the baselines.
98
100
 
99
101
  ```bash
100
- MPLBACKEND=Agg uv run python examples/gallery.py # look at examples/out/*.png
102
+ MPLBACKEND=Agg uv run python examples/gallery # look at examples/out/*.png
101
103
  uv run pytest tests/test_gallery.py --mpl # compare against the baselines
102
104
  uv run pytest tests/test_gallery.py --mpl-generate-path=docs/images/gallery # accept changes
103
105
  ```
@@ -107,7 +109,13 @@ them by eye, and commit them with the code. CI compares on Linux only (fonts
107
109
  differ elsewhere) and, when a comparison fails, uploads an HTML report with
108
110
  baseline, result and difference images as the `mpl-results-*` artifact. To add
109
111
  an example, register a function with `@example(name, title)` and give it a
110
- docstring; the test suite fails until its baseline image exists.
112
+ docstring; the test suite fails until its baseline image exists. Its parameters
113
+ are attribute names of `Dataset`, it runs inside the directory holding the toy
114
+ files (so name them `"signal.root"`, never through a variable), and the hook
115
+ prints only the body (blank lines and comments included) — write it as a user
116
+ would. Put an object into `define()` — the *Setup* block of the docs page — only
117
+ when several examples use it; anything a single example needs belongs in that
118
+ example.
111
119
 
112
120
  ## Pull requests
113
121
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: rootfig
3
- Version: 0.2.0
3
+ Version: 0.2.1
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://github.com/jbeirer/rootfig#readme
@@ -68,7 +68,7 @@ rf.plot("events.root", "Muon_pt", tree="events", selection="Muon_pt > 20", bins=
68
68
 
69
69
  These and a dozen more figures, each next to the code that made it, are in the
70
70
  [gallery](https://jbeirer.github.io/rootfig/gallery/). All of them come from
71
- [`examples/gallery.py`](examples/gallery.py), which writes toy ROOT files and
71
+ [`examples/gallery`](examples/gallery/__init__.py), which writes toy ROOT files and
72
72
  draws every example in a few seconds; the same figures are pixel-compared in CI.
73
73
 
74
74
  ## Installation
@@ -35,7 +35,7 @@ rf.plot("events.root", "Muon_pt", tree="events", selection="Muon_pt > 20", bins=
35
35
 
36
36
  These and a dozen more figures, each next to the code that made it, are in the
37
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
38
+ [`examples/gallery`](examples/gallery/__init__.py), which writes toy ROOT files and
39
39
  draws every example in a few seconds; the same figures are pixel-compared in CI.
40
40
 
41
41
  ## Installation
@@ -1,33 +1,40 @@
1
1
  # Gallery
2
2
 
3
3
  Every figure on this page is made by
4
- [`examples/gallery.py`](https://github.com/jbeirer/rootfig/blob/main/examples/gallery.py).
4
+ [`examples/gallery`](https://github.com/jbeirer/rootfig/blob/main/examples/gallery/__init__.py).
5
5
  The script writes a toy dataset (three simulated processes and one "observed"
6
- sample with muons, jets and event-level quantities, as `TTree`s) and runs all
7
- examples in a few seconds:
6
+ sample with muons, jets and event-level quantities, as `TTree`s) into a
7
+ directory and runs all examples there in a few seconds:
8
8
 
9
9
  ```bash
10
- python examples/gallery.py # figures end up in examples/out/
10
+ python examples/gallery # everything ends up in examples/out/
11
11
  ```
12
12
 
13
13
  The code next to each figure is the source of that example, and the image is
14
14
  the reference picture the test suite compares against, so what you see is what
15
- the current release draws.
15
+ the current release draws. File names such as `signal.root` are those toy
16
+ files, relative to that directory, and every example assumes
16
17
 
17
18
  ```python
18
- import matplotlib.pyplot as plt
19
- import numpy as np
20
-
21
19
  import rootfig as rf
22
20
  ```
23
21
 
22
+ <!-- gallery: quick -->
23
+
24
24
  ## Setup
25
25
 
26
- The examples share a few definitions, made once (`out` is the directory with
27
- the toy files). Everything below is optional: plain strings work everywhere.
26
+ Plain strings and `(bins, low, high)` tuples are accepted everywhere, as the
27
+ one-liner shows. Once you draw more than one plot it pays to name the pieces:
28
+ `Sample`, `Variable`, `Cut` and `Style` are small frozen dataclasses (see
29
+ [Samples, variables, cuts and styles](composable.md)). The rest of this page
30
+ shares four samples, three variables and one style, defined once:
28
31
 
29
32
  <!-- gallery-setup -->
30
33
 
34
+ Anything a single example needs is defined inside that example, so every block
35
+ below is complete given the names above (plus `matplotlib.pyplot as plt` and
36
+ `numpy as np` where they appear).
37
+
31
38
  <!-- gallery -->
32
39
 
33
40
  ## Beyond figures
@@ -38,21 +45,20 @@ The same inputs feed tables and arrays:
38
45
  # entries, mean, std, sem, skewness, min, max per sample and variable
39
46
  print(rf.summarize(mc, ["MET", "Muon_pt"], selection="nMuon > 0"))
40
47
 
41
- # a cut flow: yields at a luminosity, raw counts and step efficiencies per sample
48
+ # a cut flow: yields, raw counts and step efficiencies per sample
42
49
  print(
43
50
  rf.cutflow(
44
- [zh, ww, zz],
51
+ mc,
45
52
  ["nMuon >= 2", rf.Cut("MET > 50", label="MET > 50 GeV"), "any(Jet_btag > 0.8)"],
46
- lumi="10.8 ab^-1",
47
53
  )
48
54
  )
49
55
 
50
56
  # evaluated expressions as an Awkward record array
51
- events = rf.load(sig, ["MET", "count(Muon_pt)", "first(Muon_pt)"], selection="nJet >= 2")
57
+ events = rf.load(signal, ["MET", "count(Muon_pt)", "first(Muon_pt)"], selection="nJet >= 2")
52
58
  events["MET"]
53
59
 
54
60
  # a plain hist.Hist to feed into your own code
55
- h = rf.histogram(sig, "MET", bins=(40, 0, 400), selection="nJet >= 2")
61
+ h = rf.histogram(signal, "MET", bins=(40, 0, 400), selection="nJet >= 2")
56
62
  ```
57
63
 
58
64
  Cuts compose with `&`, `|` and `~`, carry optional labels, and combine with
@@ -0,0 +1,98 @@
1
+ """MkDocs hook that fills ``docs/gallery.md`` from the ``examples/gallery`` package.
2
+
3
+ Two kinds of marker are expanded when the page is rendered:
4
+
5
+ ``<!-- gallery-setup -->``
6
+ the body of ``define()``: the samples, variables and style shared by the
7
+ examples;
8
+ ``<!-- gallery -->`` / ``<!-- gallery: name ... -->``
9
+ one section per example with its title, description, image
10
+ (``docs/images/gallery/<name>.png``) and the source of the example
11
+ function. A marker naming examples renders exactly those, so a few can be
12
+ shown before the setup section; a bare marker renders everything that no
13
+ earlier marker on the page has shown yet, in registration order.
14
+
15
+ The gallery module is imported, never executed, so building the docs needs no
16
+ data and draws nothing. Because the images are also the baselines of
17
+ ``tests/test_gallery.py``, picture and code cannot drift apart.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import importlib.util
23
+ import re
24
+ import sys
25
+ from pathlib import Path
26
+ from types import ModuleType
27
+ from typing import Any
28
+
29
+ ROOT = Path(__file__).resolve().parents[2]
30
+ GALLERY_DIR = ROOT / "examples" / "gallery"
31
+ SETUP_MARKER = "<!-- gallery-setup -->"
32
+ GALLERY_MARKER = re.compile(r"<!-- gallery(?::\s*(?P<names>[\w\s,]+?))?\s*-->")
33
+
34
+
35
+ def _load_gallery(name: str = "rootfig_gallery_docs") -> ModuleType:
36
+ """Import ``examples/gallery`` as a fresh package, without ``examples/`` on ``sys.path``.
37
+
38
+ Every call re-executes the package (``mkdocs serve`` renders the page on each
39
+ rebuild), so its submodules are evicted from ``sys.modules`` first: a cached
40
+ ``registry`` would keep its ``EXAMPLES`` list and the re-run decorators would
41
+ register every example a second time.
42
+ """
43
+ for cached in [m for m in sys.modules if m == name or m.startswith(f"{name}.")]:
44
+ del sys.modules[cached]
45
+ spec = importlib.util.spec_from_file_location(
46
+ name, GALLERY_DIR / "__init__.py", submodule_search_locations=[str(GALLERY_DIR)]
47
+ )
48
+ if spec is None or spec.loader is None:
49
+ msg = f"cannot load {GALLERY_DIR}"
50
+ raise RuntimeError(msg)
51
+ module = importlib.util.module_from_spec(spec)
52
+ sys.modules[name] = module
53
+ spec.loader.exec_module(module)
54
+ return module
55
+
56
+
57
+ def render_section(gallery: ModuleType, example: Any) -> str:
58
+ """Markdown for one example: heading, description, image, code."""
59
+ code = gallery.body_source(example.func)
60
+ return (
61
+ f"## {example.title}\n\n"
62
+ f"{example.description}\n\n"
63
+ f'![{example.title}](images/gallery/{example.name}.png){{ width="75%" }}\n\n'
64
+ f"```python\n{code}```\n"
65
+ )
66
+
67
+
68
+ def expand_markers(markdown: str, gallery: ModuleType) -> str:
69
+ """Replace every gallery marker, each example going to the first marker that asks."""
70
+ shown: set[str] = set()
71
+ by_name = {example.name: example for example in gallery.EXAMPLES}
72
+
73
+ def replace(match: re.Match[str]) -> str:
74
+ names = match.group("names")
75
+ if names is None:
76
+ wanted = [ex.name for ex in gallery.EXAMPLES if ex.name not in shown]
77
+ else:
78
+ asked = names.replace(",", " ").split()
79
+ unknown = [name for name in asked if name not in by_name]
80
+ if unknown:
81
+ msg = f"unknown gallery example(s) {unknown} in {match.group(0)!r}"
82
+ raise ValueError(msg)
83
+ # first marker wins: skip what an earlier marker (or this one) already rendered
84
+ wanted = list(dict.fromkeys(name for name in asked if name not in shown))
85
+ shown.update(wanted)
86
+ return "\n".join(render_section(gallery, by_name[name]) for name in wanted)
87
+
88
+ return GALLERY_MARKER.sub(replace, markdown)
89
+
90
+
91
+ def on_page_markdown(markdown: str, **_: Any) -> str:
92
+ """Expand the gallery markers (MkDocs ``on_page_markdown`` event)."""
93
+ if SETUP_MARKER not in markdown and not GALLERY_MARKER.search(markdown):
94
+ return markdown
95
+ gallery = _load_gallery()
96
+ setup = gallery.body_source(gallery.define, returns="omit")
97
+ markdown = markdown.replace(SETUP_MARKER, f"```python\n{setup}```")
98
+ return expand_markers(markdown, gallery)
@@ -79,7 +79,10 @@ the signal. The values are returned as a `Ratio` in `Plot.ratios`.
79
79
 
80
80
  `ratio_ylim` and
81
81
  `ratio_label` override the automatic range (at least 0.5 to 1.5, widened to
82
- cover the bulk of the points) and label (`Ratio to X` or `Data / MC`). The
82
+ cover the bulk of the points) and label (`Ratio to X` or `Data / MC`). A
83
+ rotated y label is bounded by the height of the short ratio panel, so a long
84
+ one is shrunk and, if that is not enough, wrapped onto two lines; pass a
85
+ shorter `ratio_label` such as `"Ratio"` to keep it at full size. The
83
86
  computed values are returned in `Plot.ratios` as
84
87
  [`Ratio`][rootfig.Ratio] objects (`values`, `errors`, `band`, `edges`).
85
88
 
@@ -134,6 +137,10 @@ computed values are returned in `Plot.ratios` as
134
137
  later renders exactly the layout that was computed, in the style's fonts.
135
138
  - `Plot.fig`, `Plot.ax`, `Plot.ratio_ax` are plain matplotlib objects;
136
139
  `Plot.histograms` wrap the `hist.Hist` objects with labels and statistics.
140
+ - In a notebook the figure is displayed automatically — it is a pyplot figure,
141
+ flushed by the inline backend at the end of the cell, so `%matplotlib inline`
142
+ is not needed. End the call with `;` to hide the `Plot` repr, and use
143
+ `Plot.close()` in loops that make many figures.
137
144
 
138
145
  ## 2D histograms and correlations
139
146