plotpress 0.23.2__tar.gz → 0.25.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 (60) hide show
  1. {plotpress-0.23.2 → plotpress-0.25.0}/PKG-INFO +228 -41
  2. {plotpress-0.23.2 → plotpress-0.25.0}/README.md +212 -37
  3. {plotpress-0.23.2 → plotpress-0.25.0}/plotpress/__init__.py +4 -2
  4. {plotpress-0.23.2 → plotpress-0.25.0}/plotpress/_interactive.py +22 -10
  5. plotpress-0.25.0/plotpress/_version.py +1 -0
  6. {plotpress-0.23.2 → plotpress-0.25.0}/plotpress/artists.py +24 -14
  7. {plotpress-0.23.2 → plotpress-0.25.0}/plotpress/axes.py +76 -50
  8. {plotpress-0.23.2 → plotpress-0.25.0}/plotpress/colors.py +31 -7
  9. {plotpress-0.23.2 → plotpress-0.25.0}/plotpress/figure.py +28 -5
  10. {plotpress-0.23.2 → plotpress-0.25.0}/plotpress/primitives.py +74 -0
  11. {plotpress-0.23.2 → plotpress-0.25.0}/plotpress/qt.py +9 -0
  12. {plotpress-0.23.2 → plotpress-0.25.0}/plotpress/raster.py +34 -54
  13. {plotpress-0.23.2 → plotpress-0.25.0}/plotpress/style.py +27 -0
  14. {plotpress-0.23.2 → plotpress-0.25.0}/plotpress/svg.py +9 -17
  15. {plotpress-0.23.2 → plotpress-0.25.0}/plotpress/ticker.py +18 -2
  16. {plotpress-0.23.2 → plotpress-0.25.0}/plotpress/vega.py +90 -27
  17. {plotpress-0.23.2 → plotpress-0.25.0}/plotpress/vega_lite.py +130 -47
  18. {plotpress-0.23.2 → plotpress-0.25.0}/plotpress.egg-info/PKG-INFO +228 -41
  19. {plotpress-0.23.2 → plotpress-0.25.0}/plotpress.egg-info/SOURCES.txt +3 -0
  20. {plotpress-0.23.2 → plotpress-0.25.0}/plotpress.egg-info/requires.txt +15 -0
  21. {plotpress-0.23.2 → plotpress-0.25.0}/pyproject.toml +26 -5
  22. {plotpress-0.23.2 → plotpress-0.25.0}/tests/test_axes_api_audit.py +116 -5
  23. plotpress-0.25.0/tests/test_colors.py +79 -0
  24. {plotpress-0.23.2 → plotpress-0.25.0}/tests/test_input_validation.py +61 -8
  25. plotpress-0.25.0/tests/test_primitives.py +177 -0
  26. {plotpress-0.23.2 → plotpress-0.25.0}/tests/test_qt.py +28 -0
  27. {plotpress-0.23.2 → plotpress-0.25.0}/tests/test_render_all.py +3 -4
  28. plotpress-0.25.0/tests/test_style.py +92 -0
  29. {plotpress-0.23.2 → plotpress-0.25.0}/tests/test_svg_output.py +77 -6
  30. {plotpress-0.23.2 → plotpress-0.25.0}/tests/test_vega_lite_output.py +39 -2
  31. {plotpress-0.23.2 → plotpress-0.25.0}/tests/test_vega_output.py +100 -2
  32. plotpress-0.23.2/plotpress/_version.py +0 -1
  33. {plotpress-0.23.2 → plotpress-0.25.0}/LICENSE +0 -0
  34. {plotpress-0.23.2 → plotpress-0.25.0}/plotpress/_spectral.py +0 -0
  35. {plotpress-0.23.2 → plotpress-0.25.0}/plotpress/fonts/__init__.py +0 -0
  36. {plotpress-0.23.2 → plotpress-0.25.0}/plotpress/fonts/families.py +0 -0
  37. {plotpress-0.23.2 → plotpress-0.25.0}/plotpress/fonts/installed.py +0 -0
  38. {plotpress-0.23.2 → plotpress-0.25.0}/plotpress/fonts/metrics.py +0 -0
  39. {plotpress-0.23.2 → plotpress-0.25.0}/plotpress/png.py +0 -0
  40. {plotpress-0.23.2 → plotpress-0.25.0}/plotpress/polar.py +0 -0
  41. {plotpress-0.23.2 → plotpress-0.25.0}/plotpress/transform.py +0 -0
  42. {plotpress-0.23.2 → plotpress-0.25.0}/plotpress.egg-info/dependency_links.txt +0 -0
  43. {plotpress-0.23.2 → plotpress-0.25.0}/plotpress.egg-info/top_level.txt +0 -0
  44. {plotpress-0.23.2 → plotpress-0.25.0}/setup.cfg +0 -0
  45. {plotpress-0.23.2 → plotpress-0.25.0}/tests/test_adopt_axes.py +0 -0
  46. {plotpress-0.23.2 → plotpress-0.25.0}/tests/test_axes.py +0 -0
  47. {plotpress-0.23.2 → plotpress-0.25.0}/tests/test_distributions.py +0 -0
  48. {plotpress-0.23.2 → plotpress-0.25.0}/tests/test_fonts.py +0 -0
  49. {plotpress-0.23.2 → plotpress-0.25.0}/tests/test_load_data_xarray.py +0 -0
  50. {plotpress-0.23.2 → plotpress-0.25.0}/tests/test_matplotlib_gaps.py +0 -0
  51. {plotpress-0.23.2 → plotpress-0.25.0}/tests/test_matplotlib_parity.py +0 -0
  52. {plotpress-0.23.2 → plotpress-0.25.0}/tests/test_no_global_state.py +0 -0
  53. {plotpress-0.23.2 → plotpress-0.25.0}/tests/test_performance.py +0 -0
  54. {plotpress-0.23.2 → plotpress-0.25.0}/tests/test_pick_interactive.py +0 -0
  55. {plotpress-0.23.2 → plotpress-0.25.0}/tests/test_png.py +0 -0
  56. {plotpress-0.23.2 → plotpress-0.25.0}/tests/test_polar.py +0 -0
  57. {plotpress-0.23.2 → plotpress-0.25.0}/tests/test_python_compat.py +0 -0
  58. {plotpress-0.23.2 → plotpress-0.25.0}/tests/test_spectral.py +0 -0
  59. {plotpress-0.23.2 → plotpress-0.25.0}/tests/test_summary.py +0 -0
  60. {plotpress-0.23.2 → plotpress-0.25.0}/tests/test_units.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plotpress
3
- Version: 0.23.2
4
- Summary: A fast, figure-centric, SVG-first plotting library with a matplotlib-like API and no global state.
3
+ Version: 0.25.0
4
+ Summary: A fast, figure-centric plotting library with a matplotlib-like API, no global state, and one figure exportable to SVG, PNG, PDF, interactive HTML, or Vega/Vega-Lite.
5
5
  Author: plotpress contributors
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/jrvannucci/plotpress
@@ -20,10 +20,17 @@ Requires-Dist: pywebview>=4.0; extra == "gui"
20
20
  Provides-Extra: qt
21
21
  Requires-Dist: PyQt6>=6.4; extra == "qt"
22
22
  Requires-Dist: PyQt6-WebEngine>=6.4; extra == "qt"
23
- Provides-Extra: xarray
24
- Requires-Dist: xarray>=2023.1; extra == "xarray"
25
23
  Provides-Extra: jupyter
26
24
  Requires-Dist: ipython>=7; extra == "jupyter"
25
+ Provides-Extra: viewers
26
+ Requires-Dist: plotpress[gui]; extra == "viewers"
27
+ Requires-Dist: plotpress[qt]; extra == "viewers"
28
+ Requires-Dist: plotpress[jupyter]; extra == "viewers"
29
+ Provides-Extra: xarray
30
+ Requires-Dist: xarray>=2023.1; extra == "xarray"
31
+ Provides-Extra: full
32
+ Requires-Dist: plotpress[viewers]; extra == "full"
33
+ Requires-Dist: plotpress[xarray]; extra == "full"
27
34
  Provides-Extra: dev
28
35
  Requires-Dist: pytest>=7; extra == "dev"
29
36
  Provides-Extra: browser
@@ -42,14 +49,24 @@ Requires-Dist: polars>=1.0; extra == "docs"
42
49
  Requires-Dist: adaptive>=1.0; extra == "docs"
43
50
  Requires-Dist: joblib>=1.3; extra == "docs"
44
51
  Requires-Dist: xarray>=2023.1; extra == "docs"
52
+ Provides-Extra: contrib
53
+ Requires-Dist: plotpress[dev]; extra == "contrib"
54
+ Requires-Dist: plotpress[browser]; extra == "contrib"
55
+ Requires-Dist: plotpress[bench]; extra == "contrib"
56
+ Requires-Dist: plotpress[docs]; extra == "contrib"
45
57
  Dynamic: license-file
46
58
 
47
59
  # plotpress
48
60
 
49
- A **lightweight, dependency-light** plotting library that renders **SVG and
50
- self-contained interactive HTML** through a **matplotlib-shaped** API — with
51
- **no global state** and **no compiled extension**, so it installs everywhere
52
- `pip` runs.
61
+ **Scientific plots you can explore, share, and reuse.**
62
+
63
+ A **fast, dependency-light** plotting library for scientific computing, with
64
+ a **matplotlib-shaped** API and **no compiled extension** — install it
65
+ anywhere Python does, from notebooks to CI pipelines to offline environments.
66
+ It renders one figure to **SVG, PNG, PDF, Vega/Vega-Lite, and self-contained
67
+ interactive HTML** — the HTML carrying a full pan/zoom, point-picking, and
68
+ annotation toolbar. **No global state**, either — a `Figure` owns its own
69
+ axes and its own `Style`.
53
70
 
54
71
  ```python
55
72
  import plotpress
@@ -67,6 +84,187 @@ fig.save("out.html", interactive=True) # interactive toolbar: zoom / pick / a
67
84
  fig.show() # native pop-up window
68
85
  ```
69
86
 
87
+ `out.html` above is a real, self-contained page — no server, no external JS —
88
+ with a toolbar over every axes in the figure at once. Self-contained means
89
+ genuinely shareable: the plotted data and the toolbar's own JS are both
90
+ inlined into that one file, so anyone can open and interact with it with
91
+ nothing installed on their end — no Python, no plotpress, no internet
92
+ connection, just a browser. Email it, drop it in a chat, put it on a USB
93
+ stick — it still works. Send someone a file, not a service they have to
94
+ install. PyPI/GitHub READMEs can't run the page's own script, so the four
95
+ GIFs below stand in for it; open one yourself (or click through to the
96
+ [real-applications gallery](https://jrvannucci.github.io/plotpress/auto_applications/index.html),
97
+ embedded exactly this way) and it's fully live.
98
+
99
+ **Pan / zoom**, working the same over every axes, not just the one under the cursor:
100
+
101
+ ![Wheel-zoom toward the cursor on one panel, then panning across to the next](https://raw.githubusercontent.com/jrvannucci/plotpress/main/assets/readme_zoom_pan.gif)
102
+
103
+ **Point picking**, reading a value off any axes — a mesh's `z`, not just a
104
+ line's `x`/`y` — then extracting every picked point as CSV/JSON:
105
+
106
+ ![Picking a point on a line panel and a mesh panel, then extracting both as CSV](https://raw.githubusercontent.com/jrvannucci/plotpress/main/assets/readme_point_picking.gif)
107
+
108
+ **Annotation**, a free-form note whose label box drags independently of the
109
+ point it's pinned to:
110
+
111
+ ![Dropping an annotation on a bar chart and dragging its label away from the point it's pinned to](https://raw.githubusercontent.com/jrvannucci/plotpress/main/assets/readme_annotation.gif)
112
+
113
+ **At scale**, every gesture above still works the same way on a figure with
114
+ hundreds of axes — zoom from the full grid into a handful of panels, pan,
115
+ pick a value, remove it, pick again and drag its label, pan to a distant
116
+ group, annotate, then back Home (the figure is
117
+ [`docs/examples/grouping/plot_13_full_scale_demo.py`](https://jrvannucci.github.io/plotpress/auto_examples/grouping/plot_13_full_scale_demo.html) —
118
+ 500 `pcolormesh` panels, 250 groups, each with its own colorbar):
119
+
120
+ ![Zooming from a 500-panel figure into a handful of panels, panning, picking a value, removing and re-picking it, dragging its label, panning to a distant group, annotating, then Home](https://raw.githubusercontent.com/jrvannucci/plotpress/main/assets/readme_scale_demo.gif)
121
+
122
+ ## One figure. Many destinations.
123
+
124
+ The same `Figure` built once from the matplotlib-shaped API renders to every
125
+ format below — no separate figure per output, no plugin to install:
126
+
127
+ ```
128
+ one Figure object
129
+ |
130
+ +---------------+---------------+-------+-------+---------------+---------------+
131
+ ▼ ▼ ▼ ▼ ▼ ▼
132
+ .svg .png .pdf .html Vega Vega-Lite
133
+ (vector, (raster, (vector, (SVG + JS (v5 JSON, (v5 JSON, a
134
+ the core Pillow) svglib + inlined -- real pixel- stricter, more
135
+ format) reportlab) no server space marks) declarative
136
+ round trip) grammar)
137
+ ```
138
+
139
+ `fig.save(path, ...)` dispatches on the file extension for the first four;
140
+ `fig.to_vega()` / `fig.to_vega_lite()` return a JSON specification as a plain
141
+ `dict` for a separate Vega/Vega-Lite runtime to render — a bridge from
142
+ scientific Python to web-native visualization, for handing a figure to an
143
+ existing Vega-based dashboard or notebook without rebuilding the plot from
144
+ scratch. See the
145
+ [architecture docs](https://jrvannucci.github.io/plotpress/user_guide/architecture.html)
146
+ for exactly how much of the rendering pipeline each of these six actually
147
+ shares, and where a format gets its own dedicated path instead.
148
+
149
+ ## Reading a figure back out of HTML
150
+
151
+ A plot doesn't have to be a dead image. The interactive HTML above isn't a
152
+ one-way trip: it embeds the plotted data and the figure's own layout as JSON
153
+ alongside the SVG, so a later process — with none of the Python objects that
154
+ built it still around — can read a figure back out and rebuild it. The
155
+ figure becomes a portable representation of the data it displays, not just a
156
+ picture of it:
157
+
158
+ ```
159
+ a saved .html (Figure.save(path, interactive=True))
160
+ embeds <script id="plotpress-pick"> and
161
+ id="plotpress-layout"> per figure
162
+ |
163
+
164
+ plotpress.load_data(path)
165
+ parses that embedded JSON back out
166
+ |
167
+ +--------------+--------------+
168
+ ▼ ▼
169
+ "layout" "axes"
170
+ (grid shape, each (recovered series/
171
+ axes' own decorations, mesh/pie data per axes,
172
+ groups, sup-title) keyed by title)
173
+
174
+ |
175
+
176
+ plotpress.subplots_from_layout(layout)
177
+ rebuilds the grid and every axes' own
178
+ decorations -- not the plotted data itself
179
+ |
180
+
181
+ a new, already-labeled Figure -- ready for
182
+ the caller to replot the recovered "axes"
183
+ data back into
184
+ ```
185
+
186
+ A freeform `Figure.add_axes()` rect, an inset, or a colorbar axes has no grid
187
+ cell to rebuild from — its index is listed in `layout["omitted_axes"]`
188
+ instead of silently vanishing. See the
189
+ [full API and worked examples](https://jrvannucci.github.io/plotpress/usage.html#reading-html-data)
190
+ for the round trip end to end.
191
+
192
+ For the common case of a *uniform* grid — every axes its own single
193
+ `pcolormesh` or line series, all the same shape — `plotpress.load_data_xarray()`
194
+ skips the title-keyed dict above entirely and reads the same file straight
195
+ into one `xarray.Dataset` indexed by row/column instead, with the recovered
196
+ layout still available under `ds.attrs["layout"]` for
197
+ `plotpress.subplots_from_layout()`. See the
198
+ [data round-trip example](https://jrvannucci.github.io/plotpress/auto_examples/data_roundtrip/index.html)
199
+ for both paths worked through end to end.
200
+
201
+ ## What makes it different
202
+
203
+ 1. **No `pyplot`, no globals.** There is no "current figure/axes" and no global
204
+ `rcParams`. A `Figure` owns its axes and its own `Style`; two figures never
205
+ share mutable state. `plotpress.subplots()` returns `(fig, axes)` just like
206
+ `plt.subplots()` — but touches no global state.
207
+ 2. **matplotlib-*shaped* API** so moving code either direction is mostly
208
+ mechanical: `Figure`/`Axes`, `plot`, `scatter`, `pcolormesh`,
209
+ `set_xlabel/ylabel/title`, `set_xlim/ylim`, `grid`, `legend`, `colorbar`. It
210
+ is shaped, not drop-in — there's no `pyplot` state machine and not every
211
+ matplotlib keyword is present; treat the gallery as the compatibility surface.
212
+ 3. **SVG-first + built for speed.** Output is vector SVG; only mesh/image layers
213
+ are rasterized (as a single embedded `<image>`, not thousands of rects). Each
214
+ series is one `<path>`. It's **pure Python + NumPy** — vectorized coordinate
215
+ formatting, min/max-decimated huge lines — with **no compiled extension**, so
216
+ it installs everywhere pip does.
217
+
218
+ ## Scientific visualization, end to end
219
+
220
+ plotpress is designed around the way scientific figures actually get used:
221
+
222
+ ```
223
+ Explore ──► Analyze ──► Visualize ──► Share ──► Publish ──► Archive ──► Reuse
224
+ ▲ │
225
+ └───────────────────────────────────────────────────────────────────────┘
226
+ ```
227
+
228
+ - **Explore** — interact with a measurement, simulation, image, or spectrum
229
+ while building an experiment or analysis (pan/zoom, point-picking).
230
+ - **Analyze** — the same figure workflow for signal processing, statistics,
231
+ and multidimensional data.
232
+ - **Visualize** — build and style the figure: plot, arrange subplots, apply
233
+ colormaps and normalization.
234
+ - **Share** — hand an interactive HTML figure to a collaborator — no server,
235
+ nothing to install on their end.
236
+ - **Publish** — export a publication figure as SVG/PNG/PDF, or publish a
237
+ self-contained HTML figure that carries its own data.
238
+ - **Archive** — keep the figure and its plotted data together in one
239
+ portable file.
240
+ - **Reuse** — load the figure back with `plotpress.load_data()`, recover the
241
+ data, and analyze or replot it — the cycle starts again from Explore.
242
+
243
+ ## Made for real scientific workloads
244
+
245
+ plotpress isn't just a handful of basic plotting primitives — it covers the
246
+ kinds of figures scientists actually build. See
247
+ [Supported plot types](#supported-plot-types) below for the full method
248
+ list; the categories:
249
+
250
+ - **Signal processing** — power spectral density, cross-spectral density,
251
+ coherence, spectrograms, autocorrelation, cross-correlation, and
252
+ magnitude/angle/phase spectra (pure-NumPy Welch estimators).
253
+ - **2-D and gridded data** — images, meshes (including curvilinear grids),
254
+ contours, vector fields, and logarithmic/power/symlog normalization for
255
+ large scientific fields.
256
+ - **Statistical visualization** — histograms, 2-D histograms, box plots,
257
+ violin plots, ECDFs, KDEs, event rasters, error bars, and hexbins.
258
+ - **Complex figures** — subplot grids, shared axes, colorbars (including one
259
+ shared across several axes), secondary axes, inset axes, grouped panels,
260
+ figure-level titles/labels, and mixed layouts.
261
+ - **Animation** — animated lines and meshes with frame sliders, exportable
262
+ as self-contained looping GIFs.
263
+ - **Large figures** — built to keep object and output-node counts under
264
+ control, so a very large multi-panel figure stays practical; see the "At
265
+ scale" GIF above (500 `pcolormesh` panels, 250 groups, each with its own
266
+ colorbar).
267
+
70
268
  ## What it is for
71
269
 
72
270
  plotpress is **not a matplotlib replacement**, and it does not try to match
@@ -74,13 +272,15 @@ matplotlib's twenty years of breadth (no geographic projections or triangulated
74
272
  grids, one font-metric family, no 3-D, and its polar axes project onto the 2-D
75
273
  core rather than a dedicated pipeline — see [Supported plot types](#supported-plot-types)
76
274
  below). It aims at a narrower, underserved spot: plotting where matplotlib's
77
- install footprint or global state gets in the way.
275
+ install footprint or global state gets in the way — scientific software
276
+ doesn't always run on a developer laptop.
78
277
 
79
278
  **Reach for plotpress when you want to:**
80
279
 
81
- - **Ship plots from a constrained runtime** — locked-down servers, minimal
82
- containers, Pyodide/WASM, or CI where a pure-Python + NumPy install with no
83
- build toolchain and no per-platform wheels matters.
280
+ - **Ship plots from a constrained runtime** — locked-down networks, offline
281
+ systems, minimal containers, Pyodide/WASM, CI, or shared computing
282
+ environments where a pure-Python + NumPy install with no build toolchain
283
+ and no per-platform wheels matters.
84
284
  - **Embed in web apps or notebooks** as SVG or self-contained interactive HTML
85
285
  whose JS makes no external requests (works under strict CSPs like Jupyter).
86
286
  - **Write library or server code** that should never touch a global "current
@@ -98,38 +298,24 @@ with one figure per method, and
98
298
  grouped by field, each explaining the axis, scale and colour choices the data
99
299
  forces. Every application figure is embedded live, with the interactive toolbar.
100
300
 
101
- ## What makes it different
102
-
103
- 1. **No `pyplot`, no globals.** There is no "current figure/axes" and no global
104
- `rcParams`. A `Figure` owns its axes and its own `Style`; two figures never
105
- share mutable state. `plotpress.subplots()` returns `(fig, axes)` just like
106
- `plt.subplots()` — but touches no global state.
107
- 2. **matplotlib-*shaped* API** so moving code either direction is mostly
108
- mechanical: `Figure`/`Axes`, `plot`, `scatter`, `pcolormesh`,
109
- `set_xlabel/ylabel/title`, `set_xlim/ylim`, `grid`, `legend`, `colorbar`. It
110
- is shaped, not drop-in — there's no `pyplot` state machine and not every
111
- matplotlib keyword is present; treat the gallery as the compatibility surface.
112
- 3. **SVG-first + built for speed.** Output is vector SVG; only mesh/image layers
113
- are rasterized (as a single embedded `<image>`, not thousands of rects). Each
114
- series is one `<path>`. It's **pure Python + NumPy** — vectorized coordinate
115
- formatting, min/max-decimated huge lines — with **no compiled extension**, so
116
- it installs everywhere pip does.
117
-
118
301
  ## Install
119
302
 
120
303
  ```bash
121
304
  pip install plotpress # SVG + interactive HTML + PNG/PDF export
122
- pip install plotpress[gui] # + native pop-up window (fig.show(), pywebview)
123
- pip install plotpress[qt] # + embed in a PyQt/PySide app (fig.show_qt())
124
- pip install plotpress[dev] # + pytest (contributors)
125
- pip install plotpress[bench] # + matplotlib (benchmark comparison)
305
+ pip install plotpress[full] # + every real end-user feature: viewers (gui, qt, jupyter) + xarray
306
+ pip install plotpress[contrib] # + everything a contributor needs (dev, browser, bench, docs)
126
307
  ```
127
308
 
128
309
  The standard install covers **all file output** -- SVG, interactive HTML, PNG and
129
310
  vector PDF -- with pure-wheel dependencies that install everywhere (servers, CI,
130
- notebooks). Only the native ``fig.show()`` window needs the ``[gui]`` extra,
131
- since it pulls a desktop webview stack; without it, ``fig.show()`` falls back to
132
- the browser.
311
+ notebooks). `[full]` is likely what you want if you're reaching for more than
312
+ that at all; each of its pieces (`[gui]`, `[qt]`, `[jupyter]`, `[xarray]`, or all
313
+ three viewers via `[viewers]`) also installs on its own, or combined in one
314
+ command (`pip install plotpress[gui,xarray]`), for anyone who wants less than
315
+ the full bundle -- `[gui]`, for instance, pulls a desktop webview stack for the
316
+ native `fig.show()` window that a `[qt]`-only or `[jupyter]`-only install has
317
+ no reason to carry. See [Installation](https://jrvannucci.github.io/plotpress/installation.html)
318
+ for the full extras reference.
133
319
 
134
320
  ## Output surfaces (one scene, many targets)
135
321
 
@@ -180,11 +366,12 @@ grouped into Axes, Point Picking, Annotate, and File menus:
180
366
  - **Save**/**Save As** — persist pan/zoom, every pin/annotation, and every
181
367
  toggle above to a new (or the same) self-contained HTML file.
182
368
 
183
- `fig.to_html()`/`fig.save(..., interactive=True)` accept `pick_precision`
184
- (decimal places embedded per value) and `pick_max_mesh_cells`/
185
- `pick_max_points` (a hard cap on how much of each mesh/series is embedded for
186
- picking, per artist) to bound the interactive payload for mesh- or
187
- point-heavy figures.
369
+ `fig.to_html()`/`fig.save(..., interactive=True)` also accept `extra_js` — a
370
+ raw JS string inlined as its own `<script>`, for adding a custom tool to the
371
+ same toolbar menu plus `pick_precision` (decimal places embedded per
372
+ value) and `pick_max_mesh_cells`/`pick_max_points` (a hard cap on how much of
373
+ each mesh/series is embedded for picking, per artist) to bound the
374
+ interactive payload for mesh- or point-heavy figures.
188
375
 
189
376
  Per axes: `ax.set_pickable(False)` excludes that axes from Point Picking
190
377
  (Axis Span/Zoom/Annotation still work everywhere), and
@@ -1,9 +1,14 @@
1
1
  # plotpress
2
2
 
3
- A **lightweight, dependency-light** plotting library that renders **SVG and
4
- self-contained interactive HTML** through a **matplotlib-shaped** API — with
5
- **no global state** and **no compiled extension**, so it installs everywhere
6
- `pip` runs.
3
+ **Scientific plots you can explore, share, and reuse.**
4
+
5
+ A **fast, dependency-light** plotting library for scientific computing, with
6
+ a **matplotlib-shaped** API and **no compiled extension** — install it
7
+ anywhere Python does, from notebooks to CI pipelines to offline environments.
8
+ It renders one figure to **SVG, PNG, PDF, Vega/Vega-Lite, and self-contained
9
+ interactive HTML** — the HTML carrying a full pan/zoom, point-picking, and
10
+ annotation toolbar. **No global state**, either — a `Figure` owns its own
11
+ axes and its own `Style`.
7
12
 
8
13
  ```python
9
14
  import plotpress
@@ -21,6 +26,187 @@ fig.save("out.html", interactive=True) # interactive toolbar: zoom / pick / a
21
26
  fig.show() # native pop-up window
22
27
  ```
23
28
 
29
+ `out.html` above is a real, self-contained page — no server, no external JS —
30
+ with a toolbar over every axes in the figure at once. Self-contained means
31
+ genuinely shareable: the plotted data and the toolbar's own JS are both
32
+ inlined into that one file, so anyone can open and interact with it with
33
+ nothing installed on their end — no Python, no plotpress, no internet
34
+ connection, just a browser. Email it, drop it in a chat, put it on a USB
35
+ stick — it still works. Send someone a file, not a service they have to
36
+ install. PyPI/GitHub READMEs can't run the page's own script, so the four
37
+ GIFs below stand in for it; open one yourself (or click through to the
38
+ [real-applications gallery](https://jrvannucci.github.io/plotpress/auto_applications/index.html),
39
+ embedded exactly this way) and it's fully live.
40
+
41
+ **Pan / zoom**, working the same over every axes, not just the one under the cursor:
42
+
43
+ ![Wheel-zoom toward the cursor on one panel, then panning across to the next](https://raw.githubusercontent.com/jrvannucci/plotpress/main/assets/readme_zoom_pan.gif)
44
+
45
+ **Point picking**, reading a value off any axes — a mesh's `z`, not just a
46
+ line's `x`/`y` — then extracting every picked point as CSV/JSON:
47
+
48
+ ![Picking a point on a line panel and a mesh panel, then extracting both as CSV](https://raw.githubusercontent.com/jrvannucci/plotpress/main/assets/readme_point_picking.gif)
49
+
50
+ **Annotation**, a free-form note whose label box drags independently of the
51
+ point it's pinned to:
52
+
53
+ ![Dropping an annotation on a bar chart and dragging its label away from the point it's pinned to](https://raw.githubusercontent.com/jrvannucci/plotpress/main/assets/readme_annotation.gif)
54
+
55
+ **At scale**, every gesture above still works the same way on a figure with
56
+ hundreds of axes — zoom from the full grid into a handful of panels, pan,
57
+ pick a value, remove it, pick again and drag its label, pan to a distant
58
+ group, annotate, then back Home (the figure is
59
+ [`docs/examples/grouping/plot_13_full_scale_demo.py`](https://jrvannucci.github.io/plotpress/auto_examples/grouping/plot_13_full_scale_demo.html) —
60
+ 500 `pcolormesh` panels, 250 groups, each with its own colorbar):
61
+
62
+ ![Zooming from a 500-panel figure into a handful of panels, panning, picking a value, removing and re-picking it, dragging its label, panning to a distant group, annotating, then Home](https://raw.githubusercontent.com/jrvannucci/plotpress/main/assets/readme_scale_demo.gif)
63
+
64
+ ## One figure. Many destinations.
65
+
66
+ The same `Figure` built once from the matplotlib-shaped API renders to every
67
+ format below — no separate figure per output, no plugin to install:
68
+
69
+ ```
70
+ one Figure object
71
+ |
72
+ +---------------+---------------+-------+-------+---------------+---------------+
73
+ ▼ ▼ ▼ ▼ ▼ ▼
74
+ .svg .png .pdf .html Vega Vega-Lite
75
+ (vector, (raster, (vector, (SVG + JS (v5 JSON, (v5 JSON, a
76
+ the core Pillow) svglib + inlined -- real pixel- stricter, more
77
+ format) reportlab) no server space marks) declarative
78
+ round trip) grammar)
79
+ ```
80
+
81
+ `fig.save(path, ...)` dispatches on the file extension for the first four;
82
+ `fig.to_vega()` / `fig.to_vega_lite()` return a JSON specification as a plain
83
+ `dict` for a separate Vega/Vega-Lite runtime to render — a bridge from
84
+ scientific Python to web-native visualization, for handing a figure to an
85
+ existing Vega-based dashboard or notebook without rebuilding the plot from
86
+ scratch. See the
87
+ [architecture docs](https://jrvannucci.github.io/plotpress/user_guide/architecture.html)
88
+ for exactly how much of the rendering pipeline each of these six actually
89
+ shares, and where a format gets its own dedicated path instead.
90
+
91
+ ## Reading a figure back out of HTML
92
+
93
+ A plot doesn't have to be a dead image. The interactive HTML above isn't a
94
+ one-way trip: it embeds the plotted data and the figure's own layout as JSON
95
+ alongside the SVG, so a later process — with none of the Python objects that
96
+ built it still around — can read a figure back out and rebuild it. The
97
+ figure becomes a portable representation of the data it displays, not just a
98
+ picture of it:
99
+
100
+ ```
101
+ a saved .html (Figure.save(path, interactive=True))
102
+ embeds <script id="plotpress-pick"> and
103
+ id="plotpress-layout"> per figure
104
+ |
105
+
106
+ plotpress.load_data(path)
107
+ parses that embedded JSON back out
108
+ |
109
+ +--------------+--------------+
110
+ ▼ ▼
111
+ "layout" "axes"
112
+ (grid shape, each (recovered series/
113
+ axes' own decorations, mesh/pie data per axes,
114
+ groups, sup-title) keyed by title)
115
+
116
+ |
117
+
118
+ plotpress.subplots_from_layout(layout)
119
+ rebuilds the grid and every axes' own
120
+ decorations -- not the plotted data itself
121
+ |
122
+
123
+ a new, already-labeled Figure -- ready for
124
+ the caller to replot the recovered "axes"
125
+ data back into
126
+ ```
127
+
128
+ A freeform `Figure.add_axes()` rect, an inset, or a colorbar axes has no grid
129
+ cell to rebuild from — its index is listed in `layout["omitted_axes"]`
130
+ instead of silently vanishing. See the
131
+ [full API and worked examples](https://jrvannucci.github.io/plotpress/usage.html#reading-html-data)
132
+ for the round trip end to end.
133
+
134
+ For the common case of a *uniform* grid — every axes its own single
135
+ `pcolormesh` or line series, all the same shape — `plotpress.load_data_xarray()`
136
+ skips the title-keyed dict above entirely and reads the same file straight
137
+ into one `xarray.Dataset` indexed by row/column instead, with the recovered
138
+ layout still available under `ds.attrs["layout"]` for
139
+ `plotpress.subplots_from_layout()`. See the
140
+ [data round-trip example](https://jrvannucci.github.io/plotpress/auto_examples/data_roundtrip/index.html)
141
+ for both paths worked through end to end.
142
+
143
+ ## What makes it different
144
+
145
+ 1. **No `pyplot`, no globals.** There is no "current figure/axes" and no global
146
+ `rcParams`. A `Figure` owns its axes and its own `Style`; two figures never
147
+ share mutable state. `plotpress.subplots()` returns `(fig, axes)` just like
148
+ `plt.subplots()` — but touches no global state.
149
+ 2. **matplotlib-*shaped* API** so moving code either direction is mostly
150
+ mechanical: `Figure`/`Axes`, `plot`, `scatter`, `pcolormesh`,
151
+ `set_xlabel/ylabel/title`, `set_xlim/ylim`, `grid`, `legend`, `colorbar`. It
152
+ is shaped, not drop-in — there's no `pyplot` state machine and not every
153
+ matplotlib keyword is present; treat the gallery as the compatibility surface.
154
+ 3. **SVG-first + built for speed.** Output is vector SVG; only mesh/image layers
155
+ are rasterized (as a single embedded `<image>`, not thousands of rects). Each
156
+ series is one `<path>`. It's **pure Python + NumPy** — vectorized coordinate
157
+ formatting, min/max-decimated huge lines — with **no compiled extension**, so
158
+ it installs everywhere pip does.
159
+
160
+ ## Scientific visualization, end to end
161
+
162
+ plotpress is designed around the way scientific figures actually get used:
163
+
164
+ ```
165
+ Explore ──► Analyze ──► Visualize ──► Share ──► Publish ──► Archive ──► Reuse
166
+ ▲ │
167
+ └───────────────────────────────────────────────────────────────────────┘
168
+ ```
169
+
170
+ - **Explore** — interact with a measurement, simulation, image, or spectrum
171
+ while building an experiment or analysis (pan/zoom, point-picking).
172
+ - **Analyze** — the same figure workflow for signal processing, statistics,
173
+ and multidimensional data.
174
+ - **Visualize** — build and style the figure: plot, arrange subplots, apply
175
+ colormaps and normalization.
176
+ - **Share** — hand an interactive HTML figure to a collaborator — no server,
177
+ nothing to install on their end.
178
+ - **Publish** — export a publication figure as SVG/PNG/PDF, or publish a
179
+ self-contained HTML figure that carries its own data.
180
+ - **Archive** — keep the figure and its plotted data together in one
181
+ portable file.
182
+ - **Reuse** — load the figure back with `plotpress.load_data()`, recover the
183
+ data, and analyze or replot it — the cycle starts again from Explore.
184
+
185
+ ## Made for real scientific workloads
186
+
187
+ plotpress isn't just a handful of basic plotting primitives — it covers the
188
+ kinds of figures scientists actually build. See
189
+ [Supported plot types](#supported-plot-types) below for the full method
190
+ list; the categories:
191
+
192
+ - **Signal processing** — power spectral density, cross-spectral density,
193
+ coherence, spectrograms, autocorrelation, cross-correlation, and
194
+ magnitude/angle/phase spectra (pure-NumPy Welch estimators).
195
+ - **2-D and gridded data** — images, meshes (including curvilinear grids),
196
+ contours, vector fields, and logarithmic/power/symlog normalization for
197
+ large scientific fields.
198
+ - **Statistical visualization** — histograms, 2-D histograms, box plots,
199
+ violin plots, ECDFs, KDEs, event rasters, error bars, and hexbins.
200
+ - **Complex figures** — subplot grids, shared axes, colorbars (including one
201
+ shared across several axes), secondary axes, inset axes, grouped panels,
202
+ figure-level titles/labels, and mixed layouts.
203
+ - **Animation** — animated lines and meshes with frame sliders, exportable
204
+ as self-contained looping GIFs.
205
+ - **Large figures** — built to keep object and output-node counts under
206
+ control, so a very large multi-panel figure stays practical; see the "At
207
+ scale" GIF above (500 `pcolormesh` panels, 250 groups, each with its own
208
+ colorbar).
209
+
24
210
  ## What it is for
25
211
 
26
212
  plotpress is **not a matplotlib replacement**, and it does not try to match
@@ -28,13 +214,15 @@ matplotlib's twenty years of breadth (no geographic projections or triangulated
28
214
  grids, one font-metric family, no 3-D, and its polar axes project onto the 2-D
29
215
  core rather than a dedicated pipeline — see [Supported plot types](#supported-plot-types)
30
216
  below). It aims at a narrower, underserved spot: plotting where matplotlib's
31
- install footprint or global state gets in the way.
217
+ install footprint or global state gets in the way — scientific software
218
+ doesn't always run on a developer laptop.
32
219
 
33
220
  **Reach for plotpress when you want to:**
34
221
 
35
- - **Ship plots from a constrained runtime** — locked-down servers, minimal
36
- containers, Pyodide/WASM, or CI where a pure-Python + NumPy install with no
37
- build toolchain and no per-platform wheels matters.
222
+ - **Ship plots from a constrained runtime** — locked-down networks, offline
223
+ systems, minimal containers, Pyodide/WASM, CI, or shared computing
224
+ environments where a pure-Python + NumPy install with no build toolchain
225
+ and no per-platform wheels matters.
38
226
  - **Embed in web apps or notebooks** as SVG or self-contained interactive HTML
39
227
  whose JS makes no external requests (works under strict CSPs like Jupyter).
40
228
  - **Write library or server code** that should never touch a global "current
@@ -52,38 +240,24 @@ with one figure per method, and
52
240
  grouped by field, each explaining the axis, scale and colour choices the data
53
241
  forces. Every application figure is embedded live, with the interactive toolbar.
54
242
 
55
- ## What makes it different
56
-
57
- 1. **No `pyplot`, no globals.** There is no "current figure/axes" and no global
58
- `rcParams`. A `Figure` owns its axes and its own `Style`; two figures never
59
- share mutable state. `plotpress.subplots()` returns `(fig, axes)` just like
60
- `plt.subplots()` — but touches no global state.
61
- 2. **matplotlib-*shaped* API** so moving code either direction is mostly
62
- mechanical: `Figure`/`Axes`, `plot`, `scatter`, `pcolormesh`,
63
- `set_xlabel/ylabel/title`, `set_xlim/ylim`, `grid`, `legend`, `colorbar`. It
64
- is shaped, not drop-in — there's no `pyplot` state machine and not every
65
- matplotlib keyword is present; treat the gallery as the compatibility surface.
66
- 3. **SVG-first + built for speed.** Output is vector SVG; only mesh/image layers
67
- are rasterized (as a single embedded `<image>`, not thousands of rects). Each
68
- series is one `<path>`. It's **pure Python + NumPy** — vectorized coordinate
69
- formatting, min/max-decimated huge lines — with **no compiled extension**, so
70
- it installs everywhere pip does.
71
-
72
243
  ## Install
73
244
 
74
245
  ```bash
75
246
  pip install plotpress # SVG + interactive HTML + PNG/PDF export
76
- pip install plotpress[gui] # + native pop-up window (fig.show(), pywebview)
77
- pip install plotpress[qt] # + embed in a PyQt/PySide app (fig.show_qt())
78
- pip install plotpress[dev] # + pytest (contributors)
79
- pip install plotpress[bench] # + matplotlib (benchmark comparison)
247
+ pip install plotpress[full] # + every real end-user feature: viewers (gui, qt, jupyter) + xarray
248
+ pip install plotpress[contrib] # + everything a contributor needs (dev, browser, bench, docs)
80
249
  ```
81
250
 
82
251
  The standard install covers **all file output** -- SVG, interactive HTML, PNG and
83
252
  vector PDF -- with pure-wheel dependencies that install everywhere (servers, CI,
84
- notebooks). Only the native ``fig.show()`` window needs the ``[gui]`` extra,
85
- since it pulls a desktop webview stack; without it, ``fig.show()`` falls back to
86
- the browser.
253
+ notebooks). `[full]` is likely what you want if you're reaching for more than
254
+ that at all; each of its pieces (`[gui]`, `[qt]`, `[jupyter]`, `[xarray]`, or all
255
+ three viewers via `[viewers]`) also installs on its own, or combined in one
256
+ command (`pip install plotpress[gui,xarray]`), for anyone who wants less than
257
+ the full bundle -- `[gui]`, for instance, pulls a desktop webview stack for the
258
+ native `fig.show()` window that a `[qt]`-only or `[jupyter]`-only install has
259
+ no reason to carry. See [Installation](https://jrvannucci.github.io/plotpress/installation.html)
260
+ for the full extras reference.
87
261
 
88
262
  ## Output surfaces (one scene, many targets)
89
263
 
@@ -134,11 +308,12 @@ grouped into Axes, Point Picking, Annotate, and File menus:
134
308
  - **Save**/**Save As** — persist pan/zoom, every pin/annotation, and every
135
309
  toggle above to a new (or the same) self-contained HTML file.
136
310
 
137
- `fig.to_html()`/`fig.save(..., interactive=True)` accept `pick_precision`
138
- (decimal places embedded per value) and `pick_max_mesh_cells`/
139
- `pick_max_points` (a hard cap on how much of each mesh/series is embedded for
140
- picking, per artist) to bound the interactive payload for mesh- or
141
- point-heavy figures.
311
+ `fig.to_html()`/`fig.save(..., interactive=True)` also accept `extra_js` — a
312
+ raw JS string inlined as its own `<script>`, for adding a custom tool to the
313
+ same toolbar menu plus `pick_precision` (decimal places embedded per
314
+ value) and `pick_max_mesh_cells`/`pick_max_points` (a hard cap on how much of
315
+ each mesh/series is embedded for picking, per artist) to bound the
316
+ interactive payload for mesh- or point-heavy figures.
142
317
 
143
318
  Per axes: `ax.set_pickable(False)` excludes that axes from Point Picking
144
319
  (Axis Span/Zoom/Annotation still work everywhere), and
@@ -10,8 +10,10 @@ Distinct from matplotlib in three ways:
10
10
  ``scatter``, ``pcolormesh``, ``set_xlabel``, ``legend`` mirror matplotlib so
11
11
  existing code is easy to port. ``plotpress.subplots(...)`` returns
12
12
  ``(fig, axes)`` just like ``plt.subplots(...)`` -- minus the globals.
13
- 3. **SVG-first + fast.** Output is vector SVG (with embedded raster only for
14
- mesh/image layers), optionally interactive. The hot paths are vectorized in
13
+ 3. **SVG-first, fast, and multi-format.** Output is vector SVG (with embedded
14
+ raster only for mesh/image layers) -- and from that same figure, also PNG,
15
+ PDF, self-contained interactive HTML (a full pan/zoom/pick/annotate
16
+ toolbar), or a Vega/Vega-Lite JSON spec. The hot paths are vectorized in
15
17
  NumPy and huge lines are decimated, so it is fast in **pure Python** -- no
16
18
  compiled extension, installs everywhere pip does.
17
19