Repository navigation
Releases: jrvannucci/plotpress
Release list
0.43.0
Changed
- Grouped
style.py,colors.py,ticker.py, anddates.pyinto
plotpress/style/-- the "how a value becomes appearance" cluster
(Style, colormaps, tick locating/formatting, its datetime extension).
style.pybecomesstyle/__init__.py(it names the package);colors.py,
ticker.py,dates.pybecome plain siblings with no re-export. The
genuinely public API this exposes (plotpress.Normalize,plotpress. register_cmap, etc.) is unaffected -- it's re-exported through
plotpress/__init__.py's existing lazy-attribute layer, which only needed
its internal path strings updated. Code importingplotpress.colors/
.ticker/.datesdirectly needs thestyle.-prefixed path. - Moved
qt.pyintoplotpress/gui/andpng.pyinto
plotpress/backends/--png.pyis only ever used by the render
backends (SVG/Vega/Vega-Lite embedding PNG data URIs), never byaxes.py
or anything outsidebackends/. - Grouped the remaining top-level modules into
backends/andcore/.
svg/,raster.py,vega.py,vega_lite.py, and_interactive.py
(+_js/) moved intoplotpress/backends/-- every format aFigure
renders itself to, matching AGENTS.md's own render-pipeline diagram.
artists.py,primitives.py, andtransform.pymoved into
plotpress/core/-- the shared scene/geometry layer every backend reads
from. Neither new package re-exports (unlikefigure//svg/'s own
__init__.pys, which preserve their pre-split single-file import path on
purpose): these were already independent top-level modules, so there is no
single prior path to preserve, and the new one isplotpress.backends.svg/
plotpress.core.artists/etc. This does change any code importing
plotpress.svg/plotpress.raster/plotpress.vega/plotpress.vega_lite/
plotpress._interactive/plotpress.artists/plotpress.primitives/
plotpress.transformdirectly (all internal-facing, underscore-prefixed
members throughout) -- update those imports to thebackends./core.
prefixed path. - Moved the package to a
src/layout.plotpress/now lives at
src/plotpress/rather than the repo root, so an accidentalimport plotpresswhile developing can't silently resolve to the working tree
instead of the actually-installed package. No public API or import path
changed (import plotpressis identical); this only moves where the
source lives on disk. If you have a local editable install, re-runpip install -e .after pulling this. figure.pysplit intoplotpress/figure/. The root object had grown to
5000+ lines; it's now six submodules --_core(Figureitself, plus the
grid/group machinery it's genuinely mutually coupled to), the two leaf
modules it depends on (_layout,_html_options), and three things built
from aFigurerather than needed by it (_template,_report,
_io) -- all re-exported fromplotpress/figure/__init__.py. No public
API changed.svg.pysplit intoplotpress/svg/. The SVG backend had grown to
3300+ lines across 90 top-level functions with no classes to group
them; it's now eight submodules by concern (formatting, ticks/frame,
text/annotations, legend, per-artist rendering,Figure.group()boxes,
the entry point, and the interactive-HTML metadata payloads), all
re-exported fromplotpress/svg/__init__.pyso every existing
from plotpress.svg import Xkeeps resolving unchanged. No public API
changed.- The interactive toolbar's JS moved out of
_interactive.pyinto
plotpress/_js/*.js. The Python file had grown to ~5650 lines, nearly
all of it one JS string literal that had roughly doubled this session
from the Slice tool alone. The JS is now split into ten real.jsfiles
by tool/feature (toolbar/pan-zoom, mode buttons, Slice, ticks/pins,
Extract, Save, sliders, ...), assembled into the same script at import
time -- verified byte-for-byte identical to the pre-split output. No
public API changed.
Fixed
set_aspect/set_box_aspectreject a non-positive value.aspect=0
used to crash the whole render with aZeroDivisionError; a negative
aspect silently corrupted that axes' geometry instead. Both now raise
ValueErrorat the call site.- An explicit, non-positive limit on a log axis now raises, instead of
crashing or silently blanking the axis.set_ylim(-5, -1)on a
log-scaled axis used to crash tick generation withValueError: math domain error;set_xlim(-1, 100)used to silently NaN-blank the whole
axis with no error. Both close at the same shared validation point,
regardless of the orderset_xscale/set_xlim/set_ylimwere called in.
A one-sided limit (the open end left to autoscale, e.g.set_ylim(bottom= -5)) used to slip past this same check and still silently blank the
axis -- it is now validated per end, not only when both ends are set. violinplot()/kdeplot()no longer crash on a constant-valued sample
once it's large enough (>= 4000 points) to use the binned KDE estimator --
the degenerate grid produced NaN positions that crashednp.bincount.get_cmap()/register_cmap()no longer hand out the shared colormap
array by reference. Mutating a value returned from either function, or
a LUT already passed toregister_cmap(), used to silently corrupt that
colormap for every other figure/artist in the process; both now always
return/store a copy, on every path (registering a colormap and then
mutating what it handed back used to still corrupt the registry).- Huge-line decimation no longer lets a
NaNmask a real peak. A pixel
column containing both a genuine extreme value and aNaN(a common
shape for a real time series with missing samples) used to pick the NaN
as that column's rendered maximum, silently dropping the true peak. Figure.to_vega()/to_vega_lite()now honorerrorbar(..., errorevery=)
and export a mapped marker shape (square/diamond/the four
triangle_*) instead of always rendering a circle. A marker shape with no
native Vega/Vega-Lite equivalent (+/x/|/_) still falls back to a
circle, now with a caveat naming the axes instead of silently.hist()now ignores non-finite values, matching every sibling
distribution method (kdeplot/ecdfplot/rugplot/boxplot/
violinplot) -- it used to raise a bare, context-free NumPy error on a
NaN in the data. It also now warns when a dataset had no finite values at
all and was dropped entirely, matchingboxplot()/violinplot()below.boxplot()/violinplot()warn when a dataset is dropped entirely
(all its values were non-finite) instead of silently rendering one fewer
box/violin with no indication anything was missing.Figure.print_layout_summary()/Axes.print_summary()no longer report
a false "OK" whento_vega()/to_vega_lite()actually raised -- the
crash now shows up as a gap in the summary instead of being swallowed.
Axes.print_summary()now also flags when there is a figure-level export
gap it can't attribute to that specific axes, rather than only checking
its own axes and missing one it can't name.
Documentation
- Several docs gallery scripts and cross-references still pointed at
pre-reorganization module paths (plotpress.raster,plotpress. artists, and stale:mod:/:class:/:func:roles across several
docstrings) after the moves above. None were caught by this session's own
earlier docs-build checks, because sphinx-gallery caches each script's
execution by a hash of its own source text -- a script whose only problem
was its import target moving elsewhere never re-executed, and kept
reusing its stale pre-move result build after build. Fixed, and confirmed
this time with every gallery cache wiped first so every script actually
re-ran.
0.42.4
One Slice fix on top of 0.42.3.
Fixed
- Slice: Point Picking pins no longer float over the "profile replaces heatmap" view. A pin anchored in an axes' own data space kept its old pixel while that view stood in for the data, so a reading of
x=0.292, y=0.125, z=1.212sat at they=0.125pixel of an axis that had started reading in the slice's value, pointing at a mesh (or, since 0.42.3, a line) that is not on screen. Annotations anchored the same way did the same. They are now hidden with the series they belong to and restored with them -- on a view change, or on switching Slice off. Pins placed on the profile itself stay, since the thing they point at is what is showing. Hidden, never deleted:Extractstill carries them, and they come back untouched. This predates 0.42.3, which only widened it from mesh pins to line and scatter pins as well. A pin selected with an arrow key active is also deselected when it's hidden this way, rather than staying the live target of arrow-key stepping while invisible.
Full Changelog: 0.42.3...0.42.4
0.42.3
One Slice fix on top of 0.42.2.
Fixed
- Slice: the "profile replaces heatmap" view no longer leaves overlaid series stranded on an axis that has changed meaning. That view re-labels the vertical axis in the slice's value, and hid the mesh but nothing else drawn on the same axes -- so a
plot()line aty=0.5, or ascatter()aty=0.25, kept its old pixel position while the ticks beside it started reading in z, showing a value it never had (a line markingy=0.5sat at0.41on the new axis). The same rect was also claimed whole by the profile's own hit-test, so those series could not be picked while the view was on, and a click on one reported the mesh underneath at a shifted x. Everything the axes draws in its own data space is now hidden with the mesh and restored with it -- switching views, or switching Slice off entirely. The companion and cursor views are untouched: they keep the heatmap and the axis still reads in y, so an overlay is still meaningful there and still picks. Note this is deliberately all-or-nothing: for an X slice the horizontal axis does survive, so a strictly vertical reference line would still have been readable, but singling those out would hide one series and keep another for reasons invisible to the reader.
Full Changelog: 0.42.2...0.42.3
0.42.2
Two layout fixes on top of 0.42.1.
Fixed
subplot_size=now works without an explicittight_layout()call. Onlytight_layout()measures the decorations the solve works around, and the render-time re-fit skipped any figure that had never had it called by hand -- sosubplot_size=quietly did nothing for a caller who just built the figure and saved it, which is exactly what its own documentation said would work. Asking for a subplot size now arms that re-fit.subplots_adjust()still overrides it, since that sets the margins directly and leaves nothing to solve, and so doesset_size_inches(): an explicit figure size is the caller taking control back, and the pending solve would otherwise resize the figure away from it on the next render.- A wrong-typed
group=now says so.remove_group()andset_group_visible()accepted anything non-Nonein theirgroup=slot and only failed later, somewhere else:set_group_visible(grp, False)-- a natural order to guess, sinceremove_group()really does take the group first -- boundgroup=Falseand died on'bool' object has no attribute '_raw', andremove_group("G0")bound the title positionally and died on'str' object has no attribute 'flat_axes'. Both now raise aTypeErrornaming what was passed, and the string case suggeststitle=instead.
Full Changelog: 0.42.1...0.42.2
0.42.1 — Slice pin and config fixes
Fixed
- Slice: rebuilding the sliders no longer throws away the profile pins.
buildSliceSliders()tears the sliders down and rebuilds them on every
change, and that teardown deleted every pin placed on a companion strip -- so
toggling "Link all matching axes" or changing which axes are sliced silently
lost them, although neither changes what a pin points at. The two cases that
do are handled where they happen: switching Slice off (no profile left to
point at) and changing orientation (a pin's index means a sample along the
other axis afterwards). An axes leaving the scope still drops its own. - Slice: a
nullin the embedded option config no longer reads as a number.
isFinite(null)is true in JavaScript and+nullis 0, sopanel_size,
range_min/range_maxandindexeach took a null as a real value --
panel_size: nullcollapsed the companion strip onto its minimum size
instead of the 0.3 default.Figure.to_html()rejects a null before it can
get there, so this only reached a saved page edited by hand, but the config
is read the same way either way.
0.42.0 — size by the subplot, not the figure
Added
-
subplot_size=onsubplots()/subplots_from_groups()-- give the size
of one subplot in inches and havefigsizesolved for it, instead of sizing
the whole figure and discovering what that left each panel::fig, axes = plotpress.subplots(20, 25, subplot_size=(1.2, 0.9))A readable panel is a fixed size, so on a large grid what a caller knows is
how big one panel should be -- not what 500 of them plus their tick labels,
titles, colorbars, group boxes andsupxlabeladd up to.
figsize=(ncols * 1.2, nrows * 0.9)is the usual guess and is always wrong,
because none of that surrounding furniture scales with the grid the way the
panels do. The room it needs is measured and added on top, so the panels come
out the size asked for.tight_layout()does the solving, so the answer
reflects the labels actually set rather than a guess made before they
existed. A colorbar, a group box or a suptitle grows the figure rather than
eating into the panels --subplot_sizeis the size of the plotting box,
not of the grid cell the colorbar is carved out of.
Fixed
- The global slider bar could grow off the top of the window. "Link all
matching axes" makes one global slider per compatible group, so a figure
whose meshes come in many different grid shapes stacks many of them in the
bar fixed to the bottom of the window. Nothing bounded that column: 24 groups
made a 1319px bar in a 900px viewport, and since aposition: fixedelement
does not scroll with the page, the eight sliders above the top edge could not
be reached at all. The bar is now capped to the room it actually has and
scrolls inside itself, and re-caps when the window resizes.
Documentation
- A worked example for
subplot_size=, which opens by multiplying out a
figsizethe obvious way and printing what the panels actually came to
(1.00 x 0.45in where 1.40 x 1.10 was intended), then states the same
requirement withsubplot_size=and holds it across three grid sizes and a
per-axes colorbar. The 500-panel grouped demo and the 500-panel scale example
now use it too -- both were hand-multiplyingfigsize=(NCOLS * 1.6, NROWS * 1.6), the pattern this replaces, and neither was getting the panel
size it named.
0.41.1 — tick, menu and gallery fixes
Documentation
- README and docs homepage corrected and tidied. Both still described the
toolbar as it stood before the Slice tool andoptions=existed; both are now
documented. Several claims had drifted and are fixed: 24 built-in colormaps
(the list beside it already named 27), "a hundred-odd" real-application
figures (there are over 160), "one font-metric family" (four are bundled), and
a homepage claim that 3-D axes project onto the 2-D core -- there is no 3-D at
all; polar is the thing that projects.barbswas listed as upcoming though it
ships, and the plot-types table was missing it along withecdfplot,
kdeplotandpcolormesh_frames. "Supported plot types" is rewritten from a
single run-on paragraph into one table per grouping, the Roadmap section is
gone, and a new GIF shows the Slice companion panel driven by one coupled
slider across two panels.
Changed
- Gallery figures no longer open with Slice switched on. Every tool is still
offered in the toolbar; none starts active. Slice reshapes the axes it runs on
(carving out a companion strip, docking a slider), so starting it enabled meant
a reader met a modified figure before choosing to modify it. The five examples
that specifically demonstrate Slice still turn it on themselves.
Fixed
-
A toolbar menu could open past the edge of an embedded figure. A figure
embedded in a page -- aReportpanel, a docs gallery iframe -- gets whatever
height the host gave it, and the Slice menu is the tallest of them. With
nothing bounding it, the bottom of the list fell outside the document: clipped
away rather than scrolled off, since an iframe has no viewport of its own to
scroll, leaving those options unreachable. An open menu is now measured and
kept inside the viewport -- scrolling internally, or flipping above its button
when that side has more room -- and the same clamp keeps a menu near the right
edge on screen. -
Tick decades are computed from the decimal literal, not
pow. Neither
Python's**nor JavaScript'sMath.powis required to return the correctly
rounded power of ten, and both miss:10.0**23and10.0**126differ from
their literals, and some JavaScript engines return a different double for
10**-5than others. On an engine that rounds it the other way, an axis over
[1, 1.0001]lost its first tick entirely once zoomed --ceil(vmin / step)
landed on the next integer -- so the interactive view disagreed with the ticks
the static render had already drawn. Both sides now build a decade by parsing
the decimal literal, which is correctly rounded by spec in both languages.
0.41.0 — Interactive Slice tool
Added
-
Interactive Slice tool for pcolormesh/imshow (
options=["slice"]).
Scrub any row or column of a mesh as a 1-D profile with a play/step
slider, toggle between the heatmap (with a dashed cursor) and the profile,
fix the value axis to auto / colorbar / custom bounds, and couple every
mesh sharing the same grid to one global slider with "Link all matching
axes" -- built for figures with hundreds of meshes, where per-axes link
checkboxes are not workable. Off until "Enable Slice" is checked; curvilinear
meshes are excluded (no single row/column to slice). -
Modular toolbar via
options=.to_html/save/show/
show_in_jupyter/Report.save(andPlotPressWidget) take
options=[...]for tools beyond the core toolbar (Pan/Zoom, Home, Fit
Width, Axes, Point Picking, Annotate, File), which every page keeps.
Nothing existing changes unless asked for. -
Slice companion panel. The Slice menu's profile can be drawn in a
strip carved out of the mesh's own axes (above it for an X slice, to its
left for a Y slice), beside the heatmap and aligned through every pan/zoom,
instead of replacing it. A radio in the menu switches between that
(the default), the profile replacing the heatmap, and a cursor-only heatmap.
Point Picking works on the strip -- a click pins the nearest profile sample
(x,y, value), which follows the slider and pan/zoom, steps with the
arrow keys (staying on the strip's edge, value in red, if it goes off the
range) -- and Snap pins to
slice (a checkbox in the menu's own "Slice view" group) links pins in both
directions: a heatmap pin gets a mirror on the shown profile, and a pin placed
on the profile gets one on the heatmap cell it points at. Extract ignores profile
pins and reports the heatmap cell instead (never the same cell twice). When profile pins are left out
(Snap off), the Extract panel warns and points to Snap pins to slice. The original stays
put and each pair shares a label color.
A colorbar attached to a sliced mesh shrinks to the heatmap's share of its axes
when the strip is above it, so the two stay aligned. -
A colorbar shared by several axes shrinks with their heatmaps when every one of
them gets a strip (it stays as drawn if only some do). -
Save / Save As keep the Slice state. A saved page reopens with Slice enabled,
in the same view and orientation, on the same chosen axes, with the same link/snap
settings, value range and slider position, and with its profile pins. -
Point Picking works on the "Profile replaces heatmap" view too. A click picks a
profile sample instead of a hidden heatmap cell, and pins carry across a switch
between the two profile views. -
The companion strip now works on more axes. Axes with fixed ticks
(set_xticks/set_yticks),axis("off"), and axes with atwinx/twiny
or secondary axes (which shrink together with their parent) get a strip too;
their static ticks are remapped to the smaller heatmap. An inset axes is left
out of Slice altogether, and the axes it sits on still gets just the cursor and
slider. Also
fixes a twin's ticks being redrawn on the wrong edge (and its parent's axis
doubled) when the view changed. -
Choose which axes to slice. The Slice menu has an "Axes to slice" radio --
All axes or Selected axes -- where selecting axes is done by clicking them on the
figure; only the chosen axes get a slider, cursor and strip,
and "Link all matching axes" couples just those. Theaxesstartup setting takes
the axes (or indices) to start with. -
Startup settings for a tool. Pass
optionsas a dict to start a tool in
a chosen state -- e.g.options={"slice": {"enabled": True, "view": "companion", "orientation": "y", "link_all": True, "range": "colorbar"}}--
rather than switched off; values are validated up front. -
Gridlines on the Slice profile. A "Gridlines on profile" checkbox (and a
gridstartup setting, default on) draws light lines on the strip and on the
profile-replaces-heatmap view at both the value ticks and the heatmap's own ticks. -
Docs: Slice examples. Four examples in the gridded-data gallery (a heatmap
row, columns with the replace view, linked meshes, chosen axes) and one at scale
(a thousand meshes). Every live pcolormesh/image figure in the plot-type
reference and real-application galleries now opens with Slice enabled, and an
example can set its own state with_gallery_interactive_options.
Performance
- Slice on a figure with ~1000 pcolormeshes. Enabling Slice went from ~14 s
(without "Link all") to under a second, a linked slider step from ~3 s to under
0.1 s, and view/orientation switches from 7-9 s to under 0.5 s. Choosing a subset
of axes stays at tens of milliseconds per step. The cost was layout thrashing
(measuring while writing) and per-axes scans of the whole document.
Changed
- The Extract panel now shows JSON by default, with CSV/JSON radios to switch
the text, and its "Copy CSV" button is now "Copy" (it copies whichever format is
showing). The two download buttons are unchanged. - New logo, docs sidebar image and favicon. Refreshed artwork for the three
images already wired in (assets/plotpress-logo.png,docs/_static/logo.png,
docs/_static/favicon.png); nothing that references them changed.
Fixed
- Slice: a strip could take more than its own axes. The strip's 28px
legibility floor ignored how tall the axes actually was, so on a stack of short
panels (14 rows in a 5in figure is ~23px each) it claimed more than the whole
rect and left the heatmap with a negative one -- which draws nothing at all,
silently, since a negative width/height raises no error. The floor now gives way
on an axes too small to hold both. - Slice: an all-NaN row showed the previous row's profile. Under "Profile
replaces heatmap", stepping onto a row with no values left the last row's line on
screen: the slider read row 4 while the profile plotted row 3. A pin on such a
slice likewise stayed visible, still reporting the value it last read. - Slice no longer fights the legend. Slice and the legend's click-to-hide
toggle both wrote a mesh's ownstyle.display, so a slider step brought back a
mesh the reader had just hidden. Each now has its own channel, and a mesh stays
hidden while either wants it hidden. - Tick labels stayed put through a zoom. Three long-standing divergences
between the labels Python renders and the ones the interactive view recomputes:
an axis with a fractionalset_xlocatorbase relabelled 0/2.5/5/7.5/10 as
0/3/5/8/10 after the first pan; a lone tick at 0 rendered0e0; and a tick at
exactly +-0.5 read0/-0as drawn but1/-1once zoomed, because
JavaScript rounds a halfway value away from zero where Python rounds half to
even. A new 2,707-case parity sweep (tests/test_tick_parity_node.py) covers
the whole range x locator x format x date x categorical x log grid. - Tick positions land on their exact step multiples.
nice_ticksand
multiple_ticksaccumulated their step by repeated addition, so a tick that
should be 5.5 came out at 5.499999999999998 and one that should be 0 at
-3.4e-13 -- the latter printed as a literal-3.4e-13axis label in the static
SVG.
0.40.1
Fix interactive SVG output dropping a filled path's interior whenever the path also had an outline. fill_between(..., edgecolor=..., linewidth=...) now preserves its fill color and opacity while drawing the requested stroke, matching PNG output. Adds regression coverage for fill_between() and fill_betweenx().
0.40.0
Fixes
- Group boxes no longer shrink/mislocate when member axes are removed.
Axes.remove()used to freeze a group's box based on whatever the layout looked like the moment its last axes left it, shrinking removal-by-removal down to that lone survivor's own cell. It now freezes on the first departure instead, and a latertight_layout()reflow repositions that frozen box against its former cells' current geometry -- so it keeps matching still-populated sibling groups' width/height rather than staying pixel-locked to a stale, pre-relayout snapshot. - Fixed
tight_layout()skipping margin reservation for a fully-emptied group's own title band, which could let its title overlap the row above.
Added
Axes.set_xlabel()/set_ylabel()now takesize=/fontsize=, matchingset_title()'s existing per-axes override -- independently size one axes' x/y label without a wholeStylecopy per figure. Wired through layout margins, SVG/PNG rendering,align_xlabels()/align_ylabels(),tight_layout(auto_label_scale=True), and the template save/load round-trip.