plotpress 0.23.2__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.
- plotpress-0.23.2/LICENSE +21 -0
- plotpress-0.23.2/PKG-INFO +378 -0
- plotpress-0.23.2/README.md +332 -0
- plotpress-0.23.2/plotpress/__init__.py +108 -0
- plotpress-0.23.2/plotpress/_interactive.py +2849 -0
- plotpress-0.23.2/plotpress/_spectral.py +154 -0
- plotpress-0.23.2/plotpress/_version.py +1 -0
- plotpress-0.23.2/plotpress/artists.py +1382 -0
- plotpress-0.23.2/plotpress/axes.py +3221 -0
- plotpress-0.23.2/plotpress/colors.py +498 -0
- plotpress-0.23.2/plotpress/figure.py +3084 -0
- plotpress-0.23.2/plotpress/fonts/__init__.py +51 -0
- plotpress-0.23.2/plotpress/fonts/families.py +192 -0
- plotpress-0.23.2/plotpress/fonts/installed.py +82 -0
- plotpress-0.23.2/plotpress/fonts/metrics.py +265 -0
- plotpress-0.23.2/plotpress/png.py +93 -0
- plotpress-0.23.2/plotpress/polar.py +240 -0
- plotpress-0.23.2/plotpress/primitives.py +335 -0
- plotpress-0.23.2/plotpress/qt.py +427 -0
- plotpress-0.23.2/plotpress/raster.py +1316 -0
- plotpress-0.23.2/plotpress/style.py +91 -0
- plotpress-0.23.2/plotpress/svg.py +2589 -0
- plotpress-0.23.2/plotpress/ticker.py +212 -0
- plotpress-0.23.2/plotpress/transform.py +85 -0
- plotpress-0.23.2/plotpress/vega.py +1324 -0
- plotpress-0.23.2/plotpress/vega_lite.py +1199 -0
- plotpress-0.23.2/plotpress.egg-info/PKG-INFO +378 -0
- plotpress-0.23.2/plotpress.egg-info/SOURCES.txt +54 -0
- plotpress-0.23.2/plotpress.egg-info/dependency_links.txt +1 -0
- plotpress-0.23.2/plotpress.egg-info/requires.txt +39 -0
- plotpress-0.23.2/plotpress.egg-info/top_level.txt +1 -0
- plotpress-0.23.2/pyproject.toml +126 -0
- plotpress-0.23.2/setup.cfg +4 -0
- plotpress-0.23.2/tests/test_adopt_axes.py +195 -0
- plotpress-0.23.2/tests/test_axes.py +409 -0
- plotpress-0.23.2/tests/test_axes_api_audit.py +428 -0
- plotpress-0.23.2/tests/test_distributions.py +266 -0
- plotpress-0.23.2/tests/test_fonts.py +414 -0
- plotpress-0.23.2/tests/test_input_validation.py +640 -0
- plotpress-0.23.2/tests/test_load_data_xarray.py +383 -0
- plotpress-0.23.2/tests/test_matplotlib_gaps.py +2495 -0
- plotpress-0.23.2/tests/test_matplotlib_parity.py +270 -0
- plotpress-0.23.2/tests/test_no_global_state.py +97 -0
- plotpress-0.23.2/tests/test_performance.py +55 -0
- plotpress-0.23.2/tests/test_pick_interactive.py +3128 -0
- plotpress-0.23.2/tests/test_png.py +178 -0
- plotpress-0.23.2/tests/test_polar.py +135 -0
- plotpress-0.23.2/tests/test_python_compat.py +59 -0
- plotpress-0.23.2/tests/test_qt.py +261 -0
- plotpress-0.23.2/tests/test_render_all.py +162 -0
- plotpress-0.23.2/tests/test_spectral.py +112 -0
- plotpress-0.23.2/tests/test_summary.py +104 -0
- plotpress-0.23.2/tests/test_svg_output.py +3591 -0
- plotpress-0.23.2/tests/test_units.py +163 -0
- plotpress-0.23.2/tests/test_vega_lite_output.py +650 -0
- plotpress-0.23.2/tests/test_vega_output.py +459 -0
plotpress-0.23.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 plotpress contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
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.
|
|
5
|
+
Author: plotpress contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jrvannucci/plotpress
|
|
8
|
+
Project-URL: Documentation, https://jrvannucci.github.io/plotpress/
|
|
9
|
+
Project-URL: Changelog, https://github.com/jrvannucci/plotpress/blob/main/CHANGELOG.md
|
|
10
|
+
Keywords: plotting,svg,visualization,matplotlib,figure
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: numpy>=1.21
|
|
15
|
+
Requires-Dist: pillow>=9
|
|
16
|
+
Requires-Dist: svglib>=1.4
|
|
17
|
+
Requires-Dist: reportlab>=3.6
|
|
18
|
+
Provides-Extra: gui
|
|
19
|
+
Requires-Dist: pywebview>=4.0; extra == "gui"
|
|
20
|
+
Provides-Extra: qt
|
|
21
|
+
Requires-Dist: PyQt6>=6.4; extra == "qt"
|
|
22
|
+
Requires-Dist: PyQt6-WebEngine>=6.4; extra == "qt"
|
|
23
|
+
Provides-Extra: xarray
|
|
24
|
+
Requires-Dist: xarray>=2023.1; extra == "xarray"
|
|
25
|
+
Provides-Extra: jupyter
|
|
26
|
+
Requires-Dist: ipython>=7; extra == "jupyter"
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
29
|
+
Provides-Extra: browser
|
|
30
|
+
Requires-Dist: pytest>=8.2; extra == "browser"
|
|
31
|
+
Requires-Dist: playwright>=1.40; extra == "browser"
|
|
32
|
+
Provides-Extra: bench
|
|
33
|
+
Requires-Dist: matplotlib>=3.5; extra == "bench"
|
|
34
|
+
Requires-Dist: seaborn>=0.12; extra == "bench"
|
|
35
|
+
Requires-Dist: plotly>=5.0; extra == "bench"
|
|
36
|
+
Provides-Extra: docs
|
|
37
|
+
Requires-Dist: sphinx>=7; extra == "docs"
|
|
38
|
+
Requires-Dist: sphinx-gallery>=0.15; extra == "docs"
|
|
39
|
+
Requires-Dist: sphinx-rtd-theme>=2.0; extra == "docs"
|
|
40
|
+
Requires-Dist: matplotlib>=3.5; extra == "docs"
|
|
41
|
+
Requires-Dist: polars>=1.0; extra == "docs"
|
|
42
|
+
Requires-Dist: adaptive>=1.0; extra == "docs"
|
|
43
|
+
Requires-Dist: joblib>=1.3; extra == "docs"
|
|
44
|
+
Requires-Dist: xarray>=2023.1; extra == "docs"
|
|
45
|
+
Dynamic: license-file
|
|
46
|
+
|
|
47
|
+
# plotpress
|
|
48
|
+
|
|
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.
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import plotpress
|
|
56
|
+
import numpy as np
|
|
57
|
+
|
|
58
|
+
fig, ax = plotpress.subplots()
|
|
59
|
+
x = np.linspace(0, 4 * np.pi, 400)
|
|
60
|
+
ax.plot(x, np.sin(x), label="sin")
|
|
61
|
+
ax.plot(x, np.cos(x), label="cos", linestyle="--")
|
|
62
|
+
ax.set_xlabel("x"); ax.set_ylabel("y"); ax.legend()
|
|
63
|
+
|
|
64
|
+
fig.save("out.svg") # static vector SVG
|
|
65
|
+
fig.save("out.png"); fig.save("out.pdf") # raster + vector export
|
|
66
|
+
fig.save("out.html", interactive=True) # interactive toolbar: zoom / pick / annotate
|
|
67
|
+
fig.show() # native pop-up window
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## What it is for
|
|
71
|
+
|
|
72
|
+
plotpress is **not a matplotlib replacement**, and it does not try to match
|
|
73
|
+
matplotlib's twenty years of breadth (no geographic projections or triangulated
|
|
74
|
+
grids, one font-metric family, no 3-D, and its polar axes project onto the 2-D
|
|
75
|
+
core rather than a dedicated pipeline — see [Supported plot types](#supported-plot-types)
|
|
76
|
+
below). It aims at a narrower, underserved spot: plotting where matplotlib's
|
|
77
|
+
install footprint or global state gets in the way.
|
|
78
|
+
|
|
79
|
+
**Reach for plotpress when you want to:**
|
|
80
|
+
|
|
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.
|
|
84
|
+
- **Embed in web apps or notebooks** as SVG or self-contained interactive HTML
|
|
85
|
+
whose JS makes no external requests (works under strict CSPs like Jupyter).
|
|
86
|
+
- **Write library or server code** that should never touch a global "current
|
|
87
|
+
figure" or a process-wide `rcParams`.
|
|
88
|
+
|
|
89
|
+
**Reach for matplotlib** (or seaborn, Plotly) when you need publication-grade
|
|
90
|
+
typography across arbitrary fonts, the full plot-type gallery, 3-D, or the
|
|
91
|
+
deep ecosystem that pandas, seaborn and scikit-learn plot into.
|
|
92
|
+
|
|
93
|
+
Two galleries in the docs, on separate pages: a
|
|
94
|
+
[plot-type reference](https://jrvannucci.github.io/plotpress/auto_examples/index.html)
|
|
95
|
+
with one figure per method, and
|
|
96
|
+
[real applications](https://jrvannucci.github.io/plotpress/auto_applications/index.html)
|
|
97
|
+
— a hundred-odd worked figures built from the data real measurements produce,
|
|
98
|
+
grouped by field, each explaining the axis, scale and colour choices the data
|
|
99
|
+
forces. Every application figure is embedded live, with the interactive toolbar.
|
|
100
|
+
|
|
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
|
+
## Install
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
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)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The standard install covers **all file output** -- SVG, interactive HTML, PNG and
|
|
129
|
+
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.
|
|
133
|
+
|
|
134
|
+
## Output surfaces (one scene, many targets)
|
|
135
|
+
|
|
136
|
+
| Call | Result |
|
|
137
|
+
|------|--------|
|
|
138
|
+
| `fig.save("x.svg")` | static vector SVG |
|
|
139
|
+
| `fig.save("x.png")` / `fig.savefig(...)` | raster PNG (supersampled Pillow backend) |
|
|
140
|
+
| `fig.save("x.pdf")` | vector PDF (svglib + reportlab) |
|
|
141
|
+
| `fig.save("x.html", interactive=True)` | interactive HTML (self-contained JS toolbar) |
|
|
142
|
+
| `fig.to_svg()` / `fig.to_html()` | string, for embedding |
|
|
143
|
+
| `fig._repr_svg_()` | inline SVG in Jupyter |
|
|
144
|
+
| `fig.show()` | native pop-up window (pywebview, `[gui]` extra; falls back to browser) |
|
|
145
|
+
| `fig.show_qt()` | embed in a PyQt/PySide app (`plotpress.qt`, `[qt]` extra) |
|
|
146
|
+
|
|
147
|
+
## Interactive figures
|
|
148
|
+
|
|
149
|
+
Interactive HTML and pop-up output carry a self-contained vanilla-JS toolbar (no
|
|
150
|
+
external requests, so it works under strict CSPs like Jupyter and sandboxed
|
|
151
|
+
webviews). Nothing is active until you pick a tool:
|
|
152
|
+
|
|
153
|
+
Pan/Zoom and Home sit standalone on the toolbar's left; everything else is
|
|
154
|
+
grouped into Axes, Point Picking, Annotate, and File menus:
|
|
155
|
+
|
|
156
|
+
- **Pan/Zoom** — plain-wheel whole-figure zoom/pan, for wherever
|
|
157
|
+
holding Ctrl (Axis Zoom's whole-figure gesture, below) is awkward.
|
|
158
|
+
**Home** restores its magnification back to natural size.
|
|
159
|
+
- **Axis Span** — drag to pan a single plot's data window (log-aware).
|
|
160
|
+
- **Axis Zoom** — rubber-band box to zoom *one* axes in **data space** (ticks
|
|
161
|
+
recompute, markers keep a constant size); Ctrl+wheel (or a trackpad pinch)
|
|
162
|
+
zooms the *whole figure* instead, centered on the cursor. **Reset All
|
|
163
|
+
Axes** restores every axes' own pan/zoom back to its original view; neither
|
|
164
|
+
Reset button clears pins/annotations — double-click a single plot under
|
|
165
|
+
Axis Span/Zoom to reset just that one.
|
|
166
|
+
- **Point Picking** — click to pin the nearest data point's value; arrow keys
|
|
167
|
+
step along the series (nearest-neighbour for scatter, cell-by-cell for
|
|
168
|
+
meshes/contours), reporting extra dims (`z`, `c`, …). Click a pin, or use
|
|
169
|
+
**Clear Points**, to remove it. Its label box (connected to the marker by a
|
|
170
|
+
leader arrow) is draggable while Point Picking is active. **Hide
|
|
171
|
+
Points** toggles every pin's visibility without deleting them, and
|
|
172
|
+
**Extract** copies/downloads them all as CSV/JSON, or hands them back to
|
|
173
|
+
the kernel (`fig.show(wait_for_extract=True)`).
|
|
174
|
+
- **Annotation** — drop a user-written note anywhere on the figure, not
|
|
175
|
+
locked to any datum; **Clear Annotations** removes only these, leaving
|
|
176
|
+
Point Picking pins untouched (Escape clears both kinds at once, and
|
|
177
|
+
deselects the active tool). Its box drags the same way, while Annotation
|
|
178
|
+
is active. **Hide Annotations** toggles every note's visibility (plus any
|
|
179
|
+
boxed callout the figure itself drew) without deleting them.
|
|
180
|
+
- **Save**/**Save As** — persist pan/zoom, every pin/annotation, and every
|
|
181
|
+
toggle above to a new (or the same) self-contained HTML file.
|
|
182
|
+
|
|
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.
|
|
188
|
+
|
|
189
|
+
Per axes: `ax.set_pickable(False)` excludes that axes from Point Picking
|
|
190
|
+
(Axis Span/Zoom/Annotation still work everywhere), and
|
|
191
|
+
`ax.set_pick_context(**kwargs)` attaches extra key/value context — e.g. a
|
|
192
|
+
panel's spine color — that rides along on every record picked from it. Every
|
|
193
|
+
picked record also always carries `axes_title` (falling back to a generated
|
|
194
|
+
name when the axes has no title) plus `xlabel`/`ylabel` and `zlabel` (the
|
|
195
|
+
title of any colorbar attached to that axes, shared or not), so a value
|
|
196
|
+
pulled out of context still says what it means.
|
|
197
|
+
|
|
198
|
+
3-D data via `ax.plot_frames(...)` adds a **slider** (play/pause/step) over the
|
|
199
|
+
extra dimension; multiple sliders can be linked by a shared index.
|
|
200
|
+
|
|
201
|
+
## Supported plot types
|
|
202
|
+
|
|
203
|
+
plotpress covers the core of matplotlib's "Plot types" reference grid:
|
|
204
|
+
|
|
205
|
+
| | | |
|
|
206
|
+
|---|---|---|
|
|
207
|
+
| `plot` (lines) | `scatter` (+ `c`/`cmap`) | `bar` / `barh` |
|
|
208
|
+
| `hist` | `step` | `fill_between` |
|
|
209
|
+
| `stem` | `errorbar` (x/y err + caps) | `imshow` |
|
|
210
|
+
| `pcolormesh` | `pie` | `plot_frames` (slider) |
|
|
211
|
+
| `boxplot` | `violinplot` (KDE) | `eventplot` |
|
|
212
|
+
| `quiver` | `contour` (marching squares) | `hist2d` |
|
|
213
|
+
| `stackplot` | `contourf` (filled) | `hexbin` |
|
|
214
|
+
| `matshow` | `spy` | `broken_barh` |
|
|
215
|
+
| `stairs` | `axline` | |
|
|
216
|
+
|
|
217
|
+
**Signal processing** (pure-NumPy Welch estimators): `psd`, `csd`, `cohere`,
|
|
218
|
+
`magnitude_spectrum`, `angle_spectrum`, `phase_spectrum`, `specgram`, `xcorr`,
|
|
219
|
+
`acorr`.
|
|
220
|
+
|
|
221
|
+
**Polar** (`projection="polar"`): `plot`, `scatter`, `fill`, with
|
|
222
|
+
`set_rmax`/`set_rlim`/`set_rticks`/`set_thetagrids` and orientation control,
|
|
223
|
+
projected onto the 2-D core — see the
|
|
224
|
+
[limitations docs](https://jrvannucci.github.io/plotpress/user_guide/limitations.html)
|
|
225
|
+
for the caveats. No 3-D (see below).
|
|
226
|
+
|
|
227
|
+
Plus reference marks & fills — `axhline`/`axvline`, `axhspan`/`axvspan`,
|
|
228
|
+
`fill`/`fill_between`/`fill_betweenx`, `hlines`/`vlines` — and axis control:
|
|
229
|
+
**log scales** (`set_xscale`/`set_yscale`/`loglog`/`semilogx`),
|
|
230
|
+
**`set_aspect("equal")`**, `set_xlim/ylim`, `set_xticks/yticks`,
|
|
231
|
+
`set_xticklabels/yticklabels`, `invert_xaxis/yaxis`, `margins`, `grid`,
|
|
232
|
+
`set_axis_off`, **`subplots(sharex=…, sharey=…)`** (plus post-hoc
|
|
233
|
+
`sharex()`/`sharey()`), and **`twinx`/`twiny`** (overlaid axes with a second
|
|
234
|
+
y/x axis), `tick_params` (per-axes, per-x/y-axis tick styling), and
|
|
235
|
+
matplotlib `"C0"`..`"CN"` cycle colors. Plus **`fig.tight_layout()`**
|
|
236
|
+
(auto-margins so labels never overflow) and **`fig.subplots_adjust(...)`** /
|
|
237
|
+
**`GridSpec`** row/column spans for direct margin control, `ax.spines`
|
|
238
|
+
(per-side visible/color/linewidth), `secondary_xaxis`/`secondary_yaxis`
|
|
239
|
+
(a mirrored, unit-converted second axis) and `inset_axes` (a nested axes),
|
|
240
|
+
`align_xlabels`/`align_ylabels`, text (`ax.text`, `ax.annotate` with
|
|
241
|
+
arrows), figure-level `suptitle`/`supxlabel`/`supylabel`,
|
|
242
|
+
`fig.colorbar(...)` (single **or shared across a list of axes**),
|
|
243
|
+
`legend(loc=…, ncol=…, title=…)`, named colors (`"red"`, `"k"`, …), and
|
|
244
|
+
colormaps `viridis`, `plasma`, `inferno`, `magma`, `cividis`, `coolwarm`,
|
|
245
|
+
`RdBu`, `gray` (+ any `_r` reversed variant) with `Normalize`, `LogNorm`,
|
|
246
|
+
`PowerNorm`, or `SymLogNorm` scaling.
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
python examples/plot_types.py # plot / scatter / bar / hist / pie / imshow / ...
|
|
250
|
+
python examples/plot_types_2.py # boxplot / violin / quiver / contour / hist2d / ...
|
|
251
|
+
python examples/gallery.py # line/scatter/pcolormesh/subplots
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**Not yet implemented** (would need new primitives): `streamplot`/`barbs`,
|
|
255
|
+
triangulation (`tri*`), and geographic / map projections. These are the main
|
|
256
|
+
remaining plot-type gaps vs matplotlib's full gallery.
|
|
257
|
+
|
|
258
|
+
## Testing
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
pip install plotpress[dev] # pytest
|
|
262
|
+
python -m pytest -m "not perf" # fast unit + output tests (~2s)
|
|
263
|
+
python -m pytest -m perf -s # timing tests + speedup report (needs matplotlib)
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
The suite covers the no-global-state invariants, plotting/autoscale logic,
|
|
267
|
+
transforms/tickers/colors, a lossless PNG round-trip, SVG/HTML well-formedness
|
|
268
|
+
and structure, and performance (regression guards + a comparative claim vs
|
|
269
|
+
matplotlib).
|
|
270
|
+
|
|
271
|
+
### Point-picking tests (opt-in)
|
|
272
|
+
|
|
273
|
+
Point picking runs in JavaScript inside the interactive HTML, so it is tested
|
|
274
|
+
end-to-end in a real browser: each case clicks the pixel where the renderer drew
|
|
275
|
+
a known datum and asserts the marker reports that datum, across every pickable
|
|
276
|
+
plot type (line, scatter, bar, stem, errorbar, quiver, eventplot, boxplot,
|
|
277
|
+
violin, fill, pcolormesh, imshow, pie) and awkward axes (log, inverted,
|
|
278
|
+
`set_aspect`, multi-subplot).
|
|
279
|
+
|
|
280
|
+
These need a browser, so they are deselected by default and skip cleanly when it
|
|
281
|
+
is missing:
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
pip install plotpress[browser] && playwright install chromium
|
|
285
|
+
python -m pytest -m browser
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Benchmarks
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
pip install plotpress[bench] # matplotlib, for comparison
|
|
292
|
+
python benchmarks/benchmark.py # plotpress vs matplotlib, plot build + SVG output
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Representative run (best of 3, one machine — build **and** serialize to SVG,
|
|
296
|
+
both using the object-oriented API):
|
|
297
|
+
|
|
298
|
+
| scenario | plotpress | matplotlib | speedup |
|
|
299
|
+
|----------|------:|-----------:|--------:|
|
|
300
|
+
| pcolormesh 300×300 | ~16 ms | ~6400 ms | **~400×** |
|
|
301
|
+
| many axes (8×8 grid) | ~40 ms | ~1600 ms | **~40×** |
|
|
302
|
+
| scatter, 5k points | ~15 ms | ~220 ms | **~14×** |
|
|
303
|
+
| single line, 100k points | ~9 ms | ~48 ms | **~5.6×** |
|
|
304
|
+
|
|
305
|
+
**Honest caveat:** plotpress's win comes from avoiding matplotlib's per-`Artist`
|
|
306
|
+
Python overhead (many axes) and from rasterizing meshes to one `<image>` instead
|
|
307
|
+
of tens of thousands of vector cells (pcolormesh). The *single huge polyline*
|
|
308
|
+
case used to be a loss (pure-Python float→string serialization of 100k points);
|
|
309
|
+
it's now a win via **min/max path decimation** — a monotonic-x line is reduced
|
|
310
|
+
to first/last/min/max per pixel column before serializing, which is visually
|
|
311
|
+
lossless (spikes preserved), keeps the output **vector**, and needs no compiled
|
|
312
|
+
backend. Coordinate formatting itself is already vectorized with `numpy.char`.
|
|
313
|
+
|
|
314
|
+
## Roadmap
|
|
315
|
+
|
|
316
|
+
**Done:** pure-Python core with a self-contained object model; static SVG,
|
|
317
|
+
interactive HTML, and native-window output; PNG + vector-PDF export; the full
|
|
318
|
+
"Plot types" grid above; log scales and equal aspect; `tight_layout`; text /
|
|
319
|
+
annotations and figure-level titles; per-axes **data** zoom / pan / box-zoom with
|
|
320
|
+
live ticks, point-picking + extraction, in-browser annotation, and sliders for
|
|
321
|
+
3-D data.
|
|
322
|
+
|
|
323
|
+
**Pure Python, and staying that way.** plotpress is deliberately pure Python +
|
|
324
|
+
NumPy with no compiled extension — it installs everywhere pip does, no build
|
|
325
|
+
toolchain, no per-platform wheels. Speed comes from NumPy, not native code:
|
|
326
|
+
coordinate formatting is vectorized, huge lines are min/max-decimated (the
|
|
327
|
+
100k-point line runs ~5.6× vs matplotlib), and curvilinear / Gouraud meshes
|
|
328
|
+
scan-convert in NumPy. The "installs everywhere" promise is a first-class
|
|
329
|
+
feature, not a trade-off.
|
|
330
|
+
|
|
331
|
+
**Next:**
|
|
332
|
+
- Finish unifying the SVG and raster renderers behind the shared primitive
|
|
333
|
+
layer (pure Python) so features aren't implemented twice.
|
|
334
|
+
- More plot types: `streamplot`/`barbs` and triangulation (`tri*`).
|
|
335
|
+
- Deeper polar (polar bars, cross-collection depth sorting).
|
|
336
|
+
- Hover tooltips; decimation for huge scatter collections.
|
|
337
|
+
|
|
338
|
+
## Architecture notes
|
|
339
|
+
|
|
340
|
+
`plotpress/` layout:
|
|
341
|
+
|
|
342
|
+
| Module | Responsibility |
|
|
343
|
+
|--------|----------------|
|
|
344
|
+
| `figure.py` | `Figure`, `subplots()`, layout, save/show/`_repr_*` |
|
|
345
|
+
| `axes.py` | `Axes`: plotting methods, limits, autoscale |
|
|
346
|
+
| `polar.py` | `PolarAxes`: (θ, r) projection + polar frame, built from existing artists |
|
|
347
|
+
| `_spectral.py` | pure-NumPy Welch spectral estimators (psd/csd/cohere/specgram/…) |
|
|
348
|
+
| `artists.py` | data-only scene primitives (`Line2D`, `ScatterCollection`, `QuadMesh`) |
|
|
349
|
+
| `style.py` | per-figure `Style` (replaces global `rcParams`) |
|
|
350
|
+
| `transform.py` | vectorized data→pixel transforms (linear + log scales) |
|
|
351
|
+
| `colors.py` | `Normalize`, colormap LUTs, colormap application |
|
|
352
|
+
| `ticker.py` | "nice number" + log tick locations, label formatting |
|
|
353
|
+
| `svg.py` | the renderer: scene → SVG string (+ per-axes metadata) |
|
|
354
|
+
| `primitives.py` | backend-agnostic pixel-space primitives + one artist→primitive converter |
|
|
355
|
+
| `png.py` | stdlib-only PNG encoder for mesh/image layers |
|
|
356
|
+
| `raster.py` | Pillow raster backend for PNG export; svglib/reportlab for PDF |
|
|
357
|
+
| `fonts/` | bundled width tables + the family registry (layout only; no glyph rasterization) |
|
|
358
|
+
| `_interactive.py` | inlined vanilla JS: toolbar, per-axes zoom, picking, annotate, sliders, export |
|
|
359
|
+
| `qt.py` | optional PyQt/PySide WebEngine widget + window (`fig.show_qt()`, `[qt]` extra) |
|
|
360
|
+
|
|
361
|
+
Artists never render themselves — they just hold arrays. The geometry of each
|
|
362
|
+
artist is computed once in `primitives.py`; `svg.py` and `raster.py` are thin
|
|
363
|
+
emitters over that shared primitive vocabulary, so an artist is defined in one
|
|
364
|
+
place, not per backend.
|
|
365
|
+
|
|
366
|
+
**Fonts.** A figure is laid out *before* anything draws its glyphs — SVG emits
|
|
367
|
+
`<text>` and lets the viewer rasterize — so plotpress has to predict text width
|
|
368
|
+
from bundled metric tables. That keeps layout identical on every machine with no
|
|
369
|
+
font-file dependency. Bundled are the base-14 metric families — **Helvetica,
|
|
370
|
+
Times and Courier**, each in regular / bold / italic / bold-italic — plus
|
|
371
|
+
**DejaVu Sans**, which covers the metric-compatible clones too (Arial and
|
|
372
|
+
Liberation Sans are Helvetica, Liberation Serif is Times, Liberation Mono is
|
|
373
|
+
Courier). Families outside those groups — Verdana, Tahoma, Arial Black, Arial
|
|
374
|
+
Narrow — have proprietary metrics, so they render but are measured as Helvetica
|
|
375
|
+
and need hand-tuned `figsize`; `Style(measure_installed_fonts=True)` opts into
|
|
376
|
+
measuring the real file on this machine instead, trading cross-machine
|
|
377
|
+
reproducibility for fidelity. PNG export picks a matching face, falling back to
|
|
378
|
+
Pillow's built-in font where the system has none.
|