Limitations
Every one of these is a consequence of a deliberate design choice – SVG-first rendering, no compiled extension, no global state. They are documented here so you can tell in advance whether a trade-off affects your use, rather than discovering it in a figure. Worked examples with measurements live in the limitations gallery, and the ones about size – where scatter stops scaling, where extra mesh cells stop reaching the screen, what interactivity costs, and why contour output has no ceiling – are measured at the foot of the large-scale gallery.
Only bundled metric families are measured accurately
A figure is laid out before anything draws its glyphs: SVG emits <text>
and the viewer rasterizes it. plotpress therefore has to predict how wide
text will be, and it predicts from bundled advance-width tables. That keeps
layout identical on every machine with no font-file dependency – but it is
only correct for families those tables describe.
Bundled are the base-14 metric families – Helvetica, Times and Courier, each in regular / bold / italic / bold-italic – plus DejaVu Sans. Any family that resolves to one of them is measured exactly:
Family |
Measured as |
Result |
|---|---|---|
Helvetica, Arial, Liberation Sans, Arimo |
Helvetica |
accurate (the default stack) |
Times New Roman, Liberation Serif, Tinos |
Times |
accurate |
Courier New, Liberation Mono, Cousine |
Courier |
accurate |
DejaVu Sans |
DejaVu Sans |
accurate |
Verdana |
Helvetica (+16% needed) |
legend and label text overruns its box |
Arial Black |
Helvetica (+26% needed) |
overruns |
Arial Narrow and condensed faces |
Helvetica (-18% needed) |
margins come out too generous |
The families in the lower group have proprietary metrics that match nothing
bundled, so they fall back to Helvetica. They still render – but their legend
boxes and axis margins were sized for the wrong font, so expect to hand-tune
figsize and spacing. See
Font metrics: which families plotpress can measure.
Weight is modelled, style is available
Bold is not a free variation on regular – Helvetica-Bold runs 5-9% wider on
realistic label strings. The elements plotpress draws bold (the legend title
and suptitle) are measured with the bold tables, in both the SVG and raster
backends. Italic tables are bundled for the same reason, though nothing in the
default styling draws italic yet.
Why not just measure the real font? Because layout would then depend on
which fonts happen to be installed, and the same script would produce different
margins on different machines. This is a determinism trade-off, not a technical
barrier – so it is offered as a choice rather than refused outright. Set
Style(measure_installed_fonts=True) and plotpress measures the file on this
machine instead, which is the right call when you need an unmeasurable family to
fit and can live with layout that varies across machines. See
Measuring the fonts you actually have.
The measuring is done by Pillow, which is already required for PNG export, so the option costs no extra install. For comparison, matplotlib solves the general problem by bundling 8.5 MB of fonts and linking FreeType through a C extension.
The tables themselves are generated from authoritative sources rather than
typed by hand – see tools/gen_font_metrics.py – and a test re-derives them
from those sources to catch drift.
Text width is an estimate, not a measurement
Even with a perfectly compatible font, renderers round each glyph’s advance to a whole pixel while the metrics table is continuous. Predicted and drawn widths therefore differ slightly.
This is inherent to laying out text you do not rasterize, and it is small: at
the default fig.save(..., scale=2) supersampling it is about 0.1%. It
shows up as a little slack in a margin, never as overlap. Only scale=1
makes it visible at all.
PNG is a second renderer, not a rasterized SVG
fig.save("out.png") draws the figure again through Pillow rather than
rasterizing the SVG, because every SVG rasterizer available in Python needs
cairo – a system library, not a pip wheel. Keeping PNG export dependency-free
means maintaining a parallel backend in raster.py.
Both backends consume the same primitives and the same layout, so output matches closely, but they are not guaranteed pixel-identical. SVG and PDF are the reference: PDF references the base-14 Helvetica directly and is exact. The one visible disagreement is a newline in a label – see Text is single-line, and PNG is a separate renderer.
Density estimates are approximate for large samples
kdeplot and violinplot use the exact kernel sum for small samples. Above
a few thousand observations that sum’s grid x n intermediate dominates
(~0.9 s and 20M floats at 100k), so they switch to linear binning: the data
is binned onto the grid once and the result convolved with the kernel, which is
independent of sample size and handles millions of points in milliseconds.
The binned estimate is an approximation – typically well under 1% from the
exact curve, and always a proper density that integrates to 1. It is least
accurate where a coarse grid cannot resolve the bandwidth, which happens when
heavy-tailed outliers stretch the range; raise points= there if the peak
looks blocky. The switch-over is by sample size alone, so a given dataset always
renders the same way. See
Density estimates: binned above a few thousand points.
Polar is projected onto the 2-D core
Polar (projection="polar") axes are real, but built by projecting the data
into the existing 2-D renderer rather than by a dedicated polar pipeline – so
a few things follow from that choice:
Orientation is fixed before plotting.
set_theta_direction/set_theta_zero_locationproject the data as it is added, so they raise if called after the first plot.Polar covers
plot/scatter/fillonly – other plot types are not polar-aware.
There is no 3-D plotting – pure-Python and no compiled extension rules out a real depth-buffered/perspective pipeline, and an orthographic-projection approximation over the 2-D core (as an earlier version of this library had) was consistently weaker than dedicated 3-D tools without being meaningfully simpler to build than doing 2-D well; it was removed rather than kept as a permanently-second-tier feature.
A callable tick formatter doesn’t survive interactive zoom
set_xformat()/set_yformat accept a plain
callable (value -> str) alongside the named specs ("percent",
"comma", "eng", "pi", a raw %-style string). Every named
spec is plain, JSON-serializable data, so the interactive HTML’s
client-side zoom/pan rebuild can replay the exact same rule in JavaScript.
A Python callable cannot cross that boundary: it renders correctly in the
static SVG/PNG/PDF, but a zoomed interactive figure using one falls back to
this axis’ default numeric formatting instead of trying (and failing) to
serialize arbitrary Python code. Use a named spec instead of a callable
when the figure will be exported interactive and needs to stay correctly
formatted after a zoom.
A plain number on an already date- or category-flavored axis is undefined
Once an axis has seen datetime-like or string data on a given dimension
(x or y), a later plotting call passing a plain number on that same
dimension is not a supported idiom – the number is plotted as-is (matching
whatever positions the axis already uses), rather than raising or being
converted. Keep one kind of data per axis dimension.
Not implemented
Deliberate omissions, listed so you do not go looking:
No geographic / map projections (these need projection-database and datum machinery out of proportion to a pure-Python library).
No
streamplot, and no triangulation (tri*) plot types.No animation API.
plot_framesgives a slider over an extra dimension in interactive HTML, which covers the common case.No text layout beyond single-line strings – no rich text, no math/LaTeX rendering. A
\nin a label is not portable: the raster backend breaks the line, while SVG collapses it to a space. Use separatetext()calls if you need two lines.No tidy-dataframe or semantic-mapping API (
hue=,FacetGrid). The seaborn-style methods take plain arrays.No global configuration. This one is the point of the library rather than a gap – see How it works.