How it works
Design in one sentence
Artists are data holders; their geometry is computed once into backend-agnostic
primitives (primitives.py) that svg.py and raster.py emit. There is
no global state, and no compiled extension – it is pure Python + NumPy.
No global state
Unlike matplotlib, there is no pyplot layer and no global rcParams. A
Figure owns its axes and its own
Style. plotpress.subplots() returns a fresh,
fully independent figure.
This is what makes building axes threadable and parallelizable: with
nothing shared across figures, several threads each building their own
independent Figure have no mutable global state to race over. Across
processes the same property means a Figure/Axes pickles cleanly –
no global registry to somehow reconcile on the other side – which is why a
joblib/multiprocessing worker can build a real, complete axes
(fit + plot in one call) in its own process and hand it back rather than
returning only plain arrays for the parent to replot. What a process
boundary does not preserve is object identity: the axes that comes back
is a copy, never the original, so adopt_axes()
merges it into the real figure in place of whichever of its own axes shares
that grid position. See Figures and layout (“Building a figure across
processes”) for the full API, and
Fitting and plotting across processes, from a lazy parquet scan
for a worked example – a lazy parquet scan and curve fit inside a joblib
worker, merged back with adopt_axes().
SVG-first, selectively raster
Lines, bars, scatter, contours and text are vector <path>/<text>. Only
2-D fields (pcolormesh, imshow, hist2d, curvilinear contour fill)
are rasterized – each to a single embedded <image>, so a 500x500 grid
costs one DOM node instead of 250,000 rectangles. Scatter markers are
zero-length round-capped strokes, so vector-effect: non-scaling-stroke keeps
them a constant size under interactive zoom.
Module layout
Module |
Responsibility |
|---|---|
|
|
|
|
|
|
|
pure-NumPy Welch spectral estimators |
|
data-only scene primitives |
|
per-figure |
|
vectorized data->pixel transforms (linear + log) |
|
|
|
“nice number” + log tick locations |
|
backend-agnostic primitives + artist converter |
|
SVG emitter over the shared primitives |
|
stdlib-only PNG encoder for image layers |
|
Pillow PNG backend; svglib/reportlab PDF |
|
bundled width tables + the family registry (layout only) |
|
inlined vanilla JS: toolbar, zoom, pick, sliders |
|
|
|
|
Compiling to other renderers
plotpress already has one real, shared intermediate representation –
artist_to_prims(), converting an artists.py
scene object into backend-agnostic pixel-space prims (Path, Markers,
Rect, Segments, PolygonBatch, ImagePrim). svg.py,
raster.py, and most of vega.py all compile from that one shared
representation, not from three independent reimplementations of the same
geometry:
Axes.artists (artists.py: Line2D, Bars, Pie, Text, ...)
│
transform.py│ data space -> pixel space
v
artist_to_prims() (primitives.py)
the one shared, backend-agnostic
pixel-space representation
┌───────────────────┼───────────────────┐
v v v
svg.py raster.py vega.py
(SVG string) (PNG via Pillow, (Vega v5 JSON,
PDF via svglib) most mark kinds)
That diagram is honest only as far as it goes – two real exceptions:
``_interactive.py`` is not a fourth compiler off the shared prims. It is vanilla JS layered onto
svg.py’s own output – pan/zoom/point-pick read and mutate the rendered SVG DOM in the browser, not a shared IR. There is no interactive-JS “compiler”; there is one hand-written JS payload bolted onto one specific SVG shape.``vega.py`` has a second, un-shared path.
Line2D(unmarked),ScatterCollection,Bars,ErrorBar,Stem,Pie, andText/Annotationget their own dedicated builders invega.py, written directly againstartists.py’s fields rather than throughartist_to_prims()– real duplication withsvg.py’s renderers for the same artist kinds, not IR reuse. This isn’t an oversight left unfixed: a Vegafield/scale-encoded mark (reactive to a runtime domain change) and a frozen pixel path are different shapes of output, not different syntax for the same one – reusing the prims layer for those kinds would mean giving up that reactivity. Seeplotpress.vega’s own module docstring for the full trade-off.``vega_lite.py`` barely touches the shared prims layer at all. Vega-Lite’s mark vocabulary is closed – no raw path-per-datum mark the way Vega has – so
artist_to_prims()’s pixel-space prims have nowhere to plug in for most artist kinds; almost every mark builder invega_lite.pyis hand-written directly againstartists.py’s own fields instead, a third independent translation of the same handful of artist kinds (the one partial exception is its mesh/image mark, which does reuse the samergba()/extent()pairartist_to_prims()’s own(QuadMesh, Image)branch reads). Seeplotpress.vega_lite’s own module docstring for its three fidelity tiers and the figure-composition algorithm Vega-Lite’s grid-only layout model forces that neithervega.pynor any pixel-space backend needs.
How a page actually loads
to_html(interactive=True) and to_vega() both end up “a chart in a
web page,” but they hand the browser two fundamentally different things,
with a real practical consequence for how each can be shared.
to_html() pre-renders everything: to_svg()’s
SVG string and _interactive.py’s JS payload are both inlined into one
.html file. Opening it is the entire render step – there is nothing
left to compute, fetch, or parse; the file already is the chart. That is
what makes it a genuinely portable artifact: no server, no separate JS
runtime, no plotpress or Python on the viewing end, not even a network
connection – see the project README’s own point about sharing one of
these files directly.
to_vega() renders nothing at all. It returns a JSON spec – data
describing a chart, not a chart – which only becomes a picture once
handed to a real Vega runtime (vega-embed’s JS, typically loaded from
a CDN). That runtime does the actual drawing at load time, in the
browser; plotpress’s own involvement ends the moment the JSON is
produced. This is the right shape for handing a figure to an existing
Vega-based dashboard or notebook – the reason to_vega()/to_vega_lite()
exist at all – but the resulting page is not standalone: without that
runtime already present, the JSON is just inert data.
Performance
Avoiding matplotlib’s per-Artist Python overhead makes plotpress much
faster for many-axes figures, and rasterizing meshes to one image makes
pcolormesh dramatically cheaper. Even a single huge polyline is a win:
coordinate formatting is vectorized with numpy.char and monotonic lines are
min/max-decimated per pixel column before serialization (visually lossless), so
a 100k-point line is several times faster than matplotlib – all in pure Python.
See the project README for benchmark numbers.