API reference
Top level
|
|
|
Convenience constructor mirroring |
|
Build a figure from a |
|
Describes an outer grid of groups, each its own inner grid of axes -- so a figure like "four quadrants, each its own 2x2 cluster of plots" can be built by describing that shape directly, instead of hand-deriving which cells of one big flat grid each quadrant's axes actually occupy. |
|
One of a figure's registered groups (see |
|
An ordered collection of figures combined into one self-contained HTML file. |
|
Read back the plotted data embedded in a self-contained interactive HTML file written by |
|
Read one figure's plotted data back as a single |
|
Read back a |
|
Rebuild a figure from a template dict: the same grid shape, |
|
Pull one panel out of a |
|
Visual configuration for a single figure. |
|
Linearly map data to [0, 1] using |
|
Return a 256x3 uint8 LUT for |
|
Named colormaps, including the |
Figure
- class plotpress.figure.Figure(figsize=(6.4, 4.8), style: Style = None, facecolor=None)[source]
-
- text(x, y, s, ha='left', va='baseline', fontsize=None, color=None, alpha=1.0, bbox=None)[source]
Draw text at figure-fraction coordinates
(x, y)–(0, 0)is the bottom-left corner,(1, 1)the top-right, independent of any axes’ data coordinates.alpha/bboxmatchAxes.text()–bboxdraws a filled/ bordered box behind the text (see there for its keys).
- group(title, axes, id=None, linestyle='--', color='black', linewidth=1.5, title_position='top', pad=8.0, fontsize=None, supxlabel=None, supylabel=None, supxlabel_size=None, supylabel_size=None, visible=True, _outer_row=None, _outer_col=None, _axes_grid=None)[source]
Draw a labeled box around a set of axes – e.g. a cluster of related panels in a larger grid.
The leading-underscore
_outer_row/_outer_col/_axes_gridare forGroupLayoutto record a group’s own outer position and inner grid shape – not meant to be passed directly; doing so with coordinates that collide with a realGroupLayout’s own can makeget_group()’srow=/col=lookup ambiguous between the two.idis a second, exact-match way to find this group again later viaget_group(), alongsidetitle– unlike an axes’ ownid(seeAxes.set_id()), a group’sidis not required to be unique;get_group()raises if more than one group shares it, the same as it would for an ambiguous title.axesis any subset of this figure’s own axes, typically adjacent cells in a subplot grid; the box is the tight bounding rectangle of their individual positions (nothing about grid adjacency is checked) – expanded to also clear each axes’ own tick labels, axis labels, and title, not just its bare plot rect – pluspadpixels of clearance.padis a single number for the same clearance on all four sides, or a 4-item(left, right, top, bottom)sequence for unequal padding – e.g. tighter on the side that already butts against a neighboring group, looser on the side carrying the title.title_positionis one of"top"/"bottom"/"left"/"right", placingtitlejust outside that edge of the box.supxlabel/supylabelare this group’s own shared axis labels – the group-scoped equivalent ofFigure.supxlabel()/supylabel(), for a cluster of panels that all share one x/y quantity so no individual axes needs its ownset_xlabel/set_ylabel. Unliketitle, whichtitle_positioncan place on any of the four sides, these always draw at a fixed edge –supxlabelcentered along the bottom,supylabelcentered along the left, rotated – the same fixed placementFigure.supxlabel/supylabelthemselves use. The difference from the figure-level version is where: these draw inside the box, between its border and its member axes, rather than outside the whole grid – the box grows to make room for them (the same way it already grows forpad), rather than shrinking any axes.supxlabel_size/supylabel_sizeoverride the default size (label_size-derived, matchingFigure.supxlabel/supylabel’s own default) independently offontsize(which only ever sizestitle).visible=Falsehides the box, title, and any supxlabel/supylabel without forgetting any of it –Figure.set_group_visible()flips it back later by the sametitle/id/Grouplookupremove_group()uses. The same convention asAxes.set_visible(): a hidden group still reserves its own margin intight_layout(), so toggling it doesn’t reflow anything else – unlikeremove_group(), which really does delete it (axes included).Returns
selffor chaining; several groups may be added to one figure.
- get_groups() list[source]
This figure’s registered groups (see
group()), each as aGroup.A snapshot for a group with no inherent grid shape (a direct
group()call) – itsGroup.axesis a fresh list copy, so removing an axes afterward (seeAxes.remove()) never changes aGroupalready handed back here. For aGroupLayout-built group,Group.axesis the shared underlying(row, col)grid, so a later removal does show up in one already held – re-callget_group()/get_groups()instead of holding onto a stale reference if that distinction matters. Read it to find which axes already belong to a named group before combining it with another or extending it, or to answer “what groups does this figure have” for one you didn’t build yourself. Works identically whether a group came from a directgroup()call,subplots_from_groups()(which callsgroup()internally, once per group), or both mixed in one figure.
- get_group(row: int = None, col: int = None, title: str = None, id=None) Group[source]
The one group matching exactly one of:
(row, col)together (this group’s own outer position – only groups built viaGroupLayouthave one; seeGroup),title, orid. Raises if none or more than one group matches. See Irregular group shapes (deleted axes) for a worked example.
- remove_group(group: Group = None, title: str = None, id=None)[source]
Remove a group entirely: every one of its axes (via
Axes.remove(), sosharex/shareylinks and any id stay consistent) and the group’s own box/title registration.Pass the
Groupobject itself (fromget_groups()/get_group()), or find it bytitle/idthe same wayget_group()does – exactly one of the three. Leaves a blank rectangle where the group was, the same asAxes.remove()leaves a gap rather than reflowing the rest of the grid to fill it – callfig.tight_layout(collapse="grid")afterward to shrink away any row/column that removal left completely empty. See A dashboard mixing group shapes and deleted axes for a worked example.
- set_group_visible(visible: bool, group: Group = None, title: str = None, id=None)[source]
Show or hide a group’s box/title/supxlabel/supylabel – the same
visible=group()itself takes, settable again after the fact. Pass theGroupobject itself (fromget_groups()/get_group()), or find it bytitle/idthe same wayget_group()does – exactly one of the three.Unlike
remove_group(), this never touches the group’s own axes or deletes anything – a hidden group still reserves its own margin intight_layout()(the same conventionAxes.set_visible()uses), so toggling it back and forth doesn’t reflow the rest of the grid each time.
- get_ax(row: int = None, col: int = None, title: str = None, id=None, many: bool = False)[source]
The axes matching exactly one of:
(row, col)together (this axes’ own position in a plain, ungrouped grid – for an axes inside aGroupLayout-built group, this is that group’s position in the shared internal grid, not usually what you want; useGroup.get_ax()for that instead),title(seeAxes.set_title()), orid(seeAxes.set_id()).A
twinx/twiny/secondary_xaxis/secondary_yaxiscopies its parent’s own(row, col)verbatim (they overlay the same cell), so arow=/col=lookup that reaches one reaches both – give the twin its ownidif it needs to be found unambiguously this way.Raises if no axes matches, or – unless
many=True– if more than one does (impossible forid, which is unique per figure by construction; titles may legitimately repeat, and a twin/secondary pair at onerow=/col=counts as two).many=Truereturns every match as a list instead (even a single one, so the return type doesn’t depend on how many happened to match). See Mosaic titles, planning a layout ahead, and an ordinary grid’s own lookup for the plain-gridrow=/col=case worked through.
- group_spacing(wspace=None, hspace=None)[source]
Reserve extra pixels between subplots for
group()boxes, without touching anything elsetight_layout()already sizes.Two groups facing each other across an interior grid boundary – neither one’s title touching that boundary, so neither gets the outer-edge margin
tight_layout()reserves automatically – can collide there: each box still needs room for its own tick labels and padding beyond its bare axes, and the ordinary column/row gap (sized only from the axes’ own decorations) is not guaranteed to be enough.wspace/hspace(pixels, added on top of that gap, one or both) fix exactly that, independent of the tick-label-driven spacing itself – unlike reaching forsubplots_adjust(), which would also throw away every margintight_layout()already computed (titles, tick labels, colorbars, a legend,suptitle/supxlabel/supylabel) and require respecifying all of them by hand just to widen one gap.A title that does face an interior boundary (
title_positionpointing into the gap rather than out to the figure’s own edge) is a different case, handled automatically without this method at all: it always gets at least enough clearance to avoid drawing on the neighboring group’s own box, the same guaranteetight_layout()already gives a title facing the true outer edge. Passwspace/hspacehere to reserve more than that automatic minimum (room to visually separate the two boxes, not just keep their titles from colliding), or when neither title faces the boundary at all – the pure box-padding collision this method was originally written for, which nothing reserves for on its own.Applies only to the row/column boundaries that actually sit on the edge of a group’s bounding box – not every interior gap alike. Two rows paired inside the same group (a group spanning them both) stay exactly as tight as
tight_layout()would put them; only the boundary between that group and its neighbor – where their two boxes would otherwise collide – grows. A group spanning several rows/columns still only widens the boundaries at its own edges, not every boundary it happens to pass through.The figure grows to hold the extra room rather than shrinking the axes to fit it:
tight_layout()adds exactly the reserved pixels (each boundary that needs it, once) ontofigsizeitself, so a plot’s own size is the same with or without this call, and calling it again with a different value re-derives the growth from the size last given to the constructor orset_size_inches()rather than compounding onto an already-grown figure.Only takes effect through
tight_layout(); has no effect after asubplots_adjust()call, which sets every margin manually.
- clf()[source]
Clear the figure: drop every axes and figure-level decoration.
Keeps
figsize/style– use a newFigurefor those.
- clear()
Clear the figure: drop every axes and figure-level decoration.
Keeps
figsize/style– use a newFigurefor those.
- add_axes(rect, projection=None) Axes[source]
Add an axes at
rect = (left, bottom, width, height)(fractions).projection='polar'makes it aPolarAxes.
- add_subplot(nrows=1, ncols=1, index=1, projection=None) Axes[source]
Add the
index-th axes (1-based) of annrowsxncolsgrid.nrowsmay instead be aSubplotSpecfromfig.add_gridspec(...)[...], for an axes spanning multiple rows/ columns – its initial rect covers only the span’s top-left cell; calltight_layout()/subplots_adjust()afterward to size it to the full span.projectionaccepts the same values asadd_axes()('polar').
- add_gridspec(nrows=1, ncols=1, **kwargs) GridSpec[source]
Return a
GridSpecfor slicing into row/column spans.fig.add_subplot(fig.add_gridspec(2, 2)[0, :])spans both columns of the top row. Anyleft/right/top/bottom/wspace/hspacekwargs become this figure’s margins immediately – seeGridSpec.
- adopt_axes(ax) Axes[source]
Merge an axes built standalone – most commonly a copy that just crossed a process boundary – into this figure, in place of whichever of this figure’s own axes shares its grid position.
A process boundary always hands back a copy: pickling an axes to send it into a
joblib/multiprocessingworker and back never preserves object identity, however it looks –ax.figureon what comes back is a copy of this figure too, notself, and that copy’s ownax.axeslist still has the worker’s version of everything, not this figure’s. Passed straight tofig.axes.append(ax), it would render at the wrong position (or not enter the layout at all) and leaveax.figurepointing at that disconnected copy.adopt_axesfixes both: finds the axes already inself.axeswhoseSubplotSpecmatchesax’s (same grid shape and cell span) and replaces it there – same list position, sotight_layout()/subplots_adjust()keep placing it exactly where that slot always was – and reparentsax.figuretoself.A colorbar axes (
ax._subplotspec is None, since it was never placed on the grid itself) has no slot to match – it is appended instead, sincecolorbar()always creates one that never existed in this figure to begin with. Adopt it and the axes it belongs to from the same returned result (e.g. both elements of a worker’sreturn ax, cax): pickling preserves the object graph within one call, socax’s own reference toaxsurvives the round trip already pointing at the exact object this adopts, without anything further to fix up here.Only ever carries one axes’ worth of state across that boundary – anything that compares axes by identity across more than one of them (
group(), a colorbar shared over several axes,align_xlabels()) has to run after every worker’s result has been adopted, against the real, adopted objects – never before dispatch, and never inside the worker itself.An id set before crossing the process boundary (see
Axes.set_id()) is re-validated and re-registered against this figure’s own id index here – the worker’s own figure never knew about this one’s other axes, so a collision between two workers’ independently-chosen ids is only ever catchable at the merge point, which is exactly this method. If the axes being replaced already belonged to agroup()(built, unusually, before dispatching it to a worker rather than after, the order the rest of this docstring recommends), every group referencing it – its flat list and, for aGroupLayout-built one, its own(row, col)grid – is updated in place to reference the newly adopted axes instead, so a lookup through it doesn’t keep pointing at the orphaned pre-adoption object.
- subplots(nrows=1, ncols=1, squeeze=True, sharex=False, sharey=False, projection=None)[source]
Create a grid of axes; return a single Axes or a NumPy array of them.
sharex/shareylink the grid so autoscaling spans every subplot (shared limits) and inner tick labels are hidden, like matplotlib.projection='polar'makes every axes in the grid polar.
- tight_layout(pad=0.02, collapse=None, auto_label_scale=False)[source]
Auto-fit subplot margins so ticks/labels/titles never overflow.
Measures each axes’ decorations with the bundled font metrics and re-lays-out the subplot grid. Safe to call before or after
colorbar(); any colorbar over this grid is re-fitted afterwards. Also safe to call before the titles and axis labels exist: the fit is re-applied at render time if any of them change (see_settle_layout()).The margin this reserves is only ever sized from one text row per tick/label – it has no way to know an unrotated x tick label is wide enough to run into its neighbor, or that a title/group title is wider than the box it’s centered over, without deciding on your behalf whether the right fix is a smaller font, rotated labels, shorter text, or a wider figure. By default this only warns about those cases, naming a concrete fix for each. Pass
auto_label_scale=Trueto have it pick one of those fixes itself for the cases with a settable per-instance size – x tick labels (tick_params()’slabelsize), an axes title (set_title()’ssize), an axes x label (set_xlabel()’ssize), and a group title (group()’sfontsize) – shrinking each just enough to fit, down to a legibility floor. Anything that still doesn’t fit once its floor is reached still only warns.collapsereclaims whitespaceremove()/remove_group()leave behind, since neither reflows the grid on its own – a removed axes’ row/column keeps itsnrows/ncolsexactly as it was (nothing else in the figure knows it’s gone), and an emptied group’s box freezes in its last position rather than disappearing (seeremove()’s own docstring). Three values:None(default): unchanged – nothing collapses."grid": shrink any row/column of the grid that is now entirely empty (every axes that used to occupy it has been removed). A surviving axes never moves relative to its siblings – only whole empty rows/columns disappear, never a single gap inside an otherwise-populated one. Also drops anygroup()whose members have all been removed, reclaiming the space its frozen box was holding."tight": pack groups and axes as close together as possible without breaking groupings – not yet implemented (raisesNotImplementedError). Unlike"grid", this needs a real packing algorithm: agroup()can hold arbitrary, non-contiguous axes with no rectangular shape to pack, and aGroupLayoutgroup’s cells share one grid with every other group, so packing one tighter can shift another’s rows/ columns too. Use"grid"for the well-defined subset of this in the meantime.
- subplots_adjust(left=None, right=None, top=None, bottom=None, wspace=None, hspace=None)[source]
Directly set the subplot grid’s margins (matplotlib’s own knobs).
Only the given kwargs change; the others keep their last value (initially matplotlib’s own defaults). Mutually exclusive with
tight_layout()– both rewrite every grid axes’ rect from scratch, so whichever is called last wins; this also clearstight_layout’s pending re-fit so_settle_layout()doesn’t undo it on the next render.
- align_xlabels(axes=None)[source]
Align the x-axis labels of
axes(default: all) to one baseline.Panels with different tick-label widths otherwise put their x label at different heights below the box. Only axes side by side in the same row (matching
SubplotSpecrow span) are aligned with each other – like matplotlib, this does not pull together labels in different rows, which sit under different boxes at different y positions and have no shared “depth” worth matching. Axes with no_subplotspec(a customadd_axeslayout) form one fallback group together. Re-applied automatically aftertight_layout()/subplots_adjust()reflow the grid.
- align_ylabels(axes=None)[source]
Align the y-axis labels of
axes(default: all) to one column.See
align_xlabels(): this aligns the leftmost position any panel’s y label needs, but only among axes stacked in the same column (matchingSubplotSpeccolumn span) – panels in different columns sit under different boxes and are not pulled together.
- align_labels(axes=None)[source]
Align both x and y axis labels; see
align_xlabels()/align_ylabels().
- legend(ax=None, loc='lower center', ncol=1, title=None, pad=0.01, fontsize=None, framealpha=0.85, handles=None, labels=None, bbox_to_anchor=None) Figure[source]
One legend for the whole figure, drawn from labelled artists.
The counterpart to
colorbar()over a list of axes: a grid whose panels all plot the same series wants one legend, not the same entries repeated in every panel. Labels are de-duplicated across the axes, so each series appears once however many panels draw it.axselects which axes contribute (default: all of them).fontsize/framealphamatchAxes.legend().handlesoverrides which artists appear – any plotpress artist, from any axes (or none), in the order given, regardless of their ownlabel– the only way to legend a figure whose panels are meshes/contours/filled regions with no labeled line artist to draw from. Pair withlabelsto also override the text shown for each, positionally; without it, each handle’s ownlabelis used.handles/labelstake precedence overax(a handle already names its own source).locnames a placement in figure coordinates. The four outside placements –"lower center","upper center","right"and"center left"(also"center right") – reserve a band at that edge and shrink the subplot grid to fit, so the legend never lands on a plot. Any other name overlays without reserving, matching how an axes legend sits inside its own rect.bbox_to_anchor=(x, y), in whole- figure fraction coordinates ((0, 0)bottom-left,(1, 1)top-right), places theloccorner of the legend box at that exact point instead ofloc’s own inset/edge position – the common way to put a figure-level legend just outside every panel, e.g.loc="upper left", bbox_to_anchor=(1.0, 1.0). Whether space is reserved is still purely up toloc(one of the four named edges above, or not) –bbox_to_anchoronly fine-tunes where inside (or outside) that reservation, if any, the box actually lands, and can place it outside the figure canvas entirely.Order relative to
tight_layout()does not matter – the reservation is re-applied whenever the grid is reflowed.
- colorbar(mappable, ax, fraction=0.05, pad=0.02, label=None, ticks=None, format=None) Axes[source]
Add a colorbar for
mappable.axmay be a singleAxes(the colorbar steals space from it) or a list / array of axes (one shared colorbar spanning them all, placed on their right – the grid is squeezed to make room). All the axes should share the mappable’svmin/vmaxfor the shared bar to describe them accurately.labelsets what the color scale means (equivalent to, and just a convenience for,cax.set_title(label)on the returned axes – there is no separateset_label).ticksfixes the bar’s own tick positions instead of the norm’s auto-generated ones (aBoundaryNorm’s bin edges, or just a shorter list for a crowded scale);formatoverrides the tick labels’ formatting – a%-style string ("%.1f","%d%%") or a callable taking one value and returning its label – over whichever tick values end up in play.Order relative to
tight_layout()does not matter – the steal is recorded and re-applied whenever the grid is reflowed.
- to_vega(mesh_data: bool = False) dict[source]
A real Vega (not Vega-Lite) v5 JSON specification, as a plain
dict–json.dumps(fig.to_vega(), indent=2)for the string, or hand the dict itself to a Vega runtime that already accepts a Python object.Unlike
to_svg()/to_html(), the result needs a separate Vega renderer to actually draw (vega-embedin a browser, thevg2svg/vg2pngCLI tools, an Observable notebook, IPython’s ownvegaMIME renderer, …) – it is a real, standalone, portable specification, not a rendered artifact, so no plotpress or Python is needed at render time. One axes becomes one Vegagroupmark with its own local scales/axes/marks, positioned at that axes’ own resolved pixel rect. Line/scatter/bar charts use genuinefield/scale-encoded marks; everything else reuses the same pixel-space primitivesto_svg()itself draws from (plotpress.primitives), so it is visually exact but frozen at this export’s own size/limits – not reactive to a Vega zoom/pan signal or a runtime domain change the way the line/scatter/bar marks are. Seeplotpress.vega’s own module docstring for the full design rationale, including what’s skipped (box plots, violins, quiver, contour, event plots, wind barbs, tables – each emits aUserWarningnaming it and continues exporting the rest of the figure) and what never carries over regardless (plotpress’s own interactive toolbar; Vega has its own separate interaction model instead, reachable by wiring upsignalson the result).mesh_data=Trueopts apcolormesh/mesh-backedimshowinto real per-cellrectmarks with a genuine field+scale color encoding, instead of the default rasterizedimagemark – reactive and queryable, but only for meshes small/simple enough to stay unambiguous (a rectilinear grid, a plain linear color norm, a colormap with a matching named Vega scheme, at most ~2000 cells – the same thresholdpcolormesh(rasterized=None)’s own auto-mode already uses). A mesh that doesn’t qualify still gets the image mark, with aUserWarningnaming why.
- to_vega_lite(mesh_data: bool = False) tuple[source]
A Vega-Lite v5 specification for this figure.
Unlike
to_svg()/to_html()/to_vega(), which all return one plain value, this returns(result, caveats)– a deliberate, documented departure from its siblings, not an oversight.resultis{"grid": <spec> | None, "standalone": [<spec>, ...]}: a combined spec for whatever axes compose cleanly into Vega-Lite’shconcat/vconcatgrid, plus a list of independent specs for anything that doesn’t (a single axes with nothing to grid against, a free-formadd_axes()/inset_axes()panel, a mismatched-shape multi-grid figure).caveatsis a list of human-readable strings describing every structural compromise made building the result – data for a caller deciding what to do with a partially-composed figure, not just console noise; every entry is also re-emitted as aUserWarning, so a caller who ignores the tuple still sees the same warning.Vega-Lite’s mark vocabulary is closed (no raw path-per-datum mark the way Vega has) and its composition model is grid-like, not arbitrary-pixel-positioned, so this is a stricter target than
to_vega()in both what a single axes can draw and how several axes can be arranged together – seeplotpress.vega_lite’s own module docstring for the full fidelity-tier breakdown (what maps natively, what needs a layered workaround, and what has no Vega-Lite mapping at all and warns instead) and the exact figure-composition algorithm.mesh_data=Trueopts apcolormesh/mesh-backedimshowinto real per-cellrectmarks with a genuine field+scale color encoding, instead of the default rasterizedimagemark – the same opt-in, same eligibility rules (a rectilinear grid, a plain linear color norm, a colormap with a matching named Vega scheme, at most ~2000 cells), and same warn-and-fall-back-to-image behavior otherwise, asto_vega()’s ownmesh_data.
- to_template() dict[source]
A reusable, data-free snapshot of this figure’s own structure and styling – grid shape,
group()boxes, every axes’ own decorations, spine colors, tick overrides, ids, twin/secondary/ inset overlays, colorbar styling, and this figure’s ownStyle– everything needed to rebuild an identically laid-out, identically styled blank figure viaplotpress.figure_from_template(), with none of the data actually plotted into it.This is the exact same payload
to_html()embeds forplotpress.load_data()to read back under its own"template"key – there is only one shape, used both when explicitly building a reusable template ahead of time (this method;save_template()writes it as plain, human-editable JSON, with no plotted data anywhere in it) and when recovering a saved figure’s structure alongside its data. Seeplotpress.svg.template_metadata()for the full field-by-field breakdown of what’s captured and the real, still-irreducible gaps (a colorbar’s actual color mapping, and a non-JSON-safe colorbarticks/format).
- save_template(path) None[source]
Write
to_template()’s result topathas plain, indented JSON – deliberately human-readable/diffable/editable, since a template is meant to be hand-tuned and checked into version control, not treated as an opaque blob. Seeplotpress.load_template()/plotpress.figure_from_template()to read it back.
- print_layout_summary() None[source]
Print a plain-English orientation to this figure’s layout – how many axes, how they’re arranged (a grid, spans, twins, insets, colorbars, free-form panels), what’s plotted on each one, and whether each would export cleanly to
to_vega()/to_vega_lite(). Meant for a REPL/notebook, when a figure came from somewhere else (a saved layout, an imported HTML file, code you didn’t write) and the fastest way to understand it is to just ask it – not for programmatic use (nothing here is returned; seeprint_summary()for one axes at a time, or readfig.axes/ax.artistsdirectly for that).Named
print_*(not e.g.layout_summary) so it tab-completes alongside every other summary method this library adds – seeprint_summary()for the per-axes one.
- to_html(interactive: bool = True, wait_extract: bool = False, pick_precision: int = 6, pick_max_mesh_cells: int = 250000, pick_max_points: int = 20000, binary_pick_data: bool = True, standalone: bool = True, include_default_js: bool = True, extra_js: str = None, options=None) str[source]
Serialize to a self-contained HTML document.
optionsadds optional toolbar menus, by name. Every page already has Pan/Zoom, Home, Fit Width, Axes, Point Picking, Annotate, and File; the rest are opt-in:"slice"– the Slice menu, scrubbing a row/column of a pcolormesh/imshow as a 1-D profile (shown only when the figure has a mesh to slice). A radio in the menu picks how the profile is shown: in a strip beside the heatmap ("companion", the default), in the heatmap’s place ("replace"), or not at all with just a cursor on the heatmap ("cursor").
Pass a list of names, or a dict to also set the tool’s startup state:
options={"slice": {"enabled": True, "view": "companion", "orientation": "y", "link_all": True, "range": "colorbar"}}. The settings areenabled,view,orientation("x"/"y"),link_all,snap_pins(mirror Point Picking pins onto the profile),grid(gridlines on the profile, defaultTrue),range("auto"/"colorbar"/"custom", the last withrange_min/range_max),index(the starting row/column),panel_size(the companion strip’s fraction of the axes, 0.1-0.6, default 0.3), andaxes("all", or a list of the axes – or their indices – to slice; the rest stay plain heatmaps). The embedded data payloads are the same either way, soload_data()reads any interactive HTML back regardless of which options it was saved with.standalone(default) centers the figure at its natural pixel size on a full-height page – right for a file opened directly in its own tab. Set itFalsewhen this HTML is going into a container you don’t control the size of (an<iframe>embedding it, say, asReportdoes): the SVG instead scales to fill whatever width it is given, and the page no longer forces itself to at least a full viewport tall, which centering a shorter figure inside would otherwise pad with empty space above and below it.pick_precisionsets the decimal places of the embedded point-pick arrays (the mesh z grids dominate the file size for mesh-heavy figures); lower it to shrink the HTML at the cost of readout precision.pick_max_mesh_cells/pick_max_pointscap how much of each mesh’s/series’ own data is embedded for picking, per artist – so a figure with many mesh-bearing axes (a grid of pcolormeshes, say) does not multiply the default cap by the axes count. A mesh over the cap is block-averaged down to it rather than dropped – a click still answers with a real value, but it’s the mean of every original cell folded into whichever coarser one the click landed in, not the exact value at that point, and that cell’s own x/y is the wider block’s center, not the original grid’s. The rendered mesh itself is never downsampled (only the pick payload is), so nothing about the image hints this happened – aUserWarningnaming every affected axes does instead, whenever a mesh actually crosses the cap. Raisepick_max_mesh_cellsfor full-resolution picking on a mesh this large, at the cost of a bigger embedded payload. A series over the point cap falls back to a geometry-only x/y readout instead (dropped, not downsampled – there’s no missing-value problem an x/y-only click needs solving the way a mesh’s z does).binary_pick_dataembeds long numeric arrays (mesh z grids, animated line frames) as base64 float32/float16 bytes instead of JSON number text – roughly half the size at effectively the same decode speed as JSON, benchmarked against gzip compressing the JSON instead (smaller, but 5-7x slower to decode:DecompressionStreamoverhead dominates at these payload sizes). It also restructures the per-axes metadata payload column-wise (one array per field instead of one object per axes), which matters once a figure has hundreds of axes: that payload has no long arrays of its own, so its cost is JSON key names repeated once per axes rather than a big number array – columnar layout states each key once, and the numeric columns that leaves then get the same binary encoding. SetFalsefor the exact plain-JSON payload, e.g. to inspect it by hand or diff it against an older plotpress version.include_default_js(defaultTrue) controls whether plotpress’s own toolbar/pan/zoom/pick JS (plotpress._interactive.INTERACTIVE_JS) is included at all. Set itFalseto get the#plotpress-meta/#plotpress-pick/#plotpress-styleJSON payloads (assuminginteractive=True) with none of plotpress’s own JS behavior layered on top – for building interactivity entirely from scratch against that data andextra_js, rather than extending what’s already there.binary_pick_data=Falseis worth pairing with this: the default binary encoding needs plotpress’s own decoder, which is exactly what this is turning off.extra_jsis a raw JS string inlined as its own<script>block, after plotpress’s own (wheninclude_default_jsisTrue) sowindow.plotpressAddTool/plotpressGetMarkersalready exist by the time it runs. Withinclude_default_js=True(the default), use it to add to the existing toolbar –window.plotpressAddTool({label, onClick})for an always-on action button, or{label, mode, onClick, onEnter, onExit, cursor}for one that joins the same single-selection group as Axis Span/Axis Zoom/Point Picking, called back with(event, userSpacePoint)on a click the built-in modes don’t already claim. Withinclude_default_js=False, it’s the only JS this page gets – write your own toolbar/interactivity entirely, working from#plotpress-svgand the JSON payloads directly. Nothing about supplying this fetches anything external on its own – it is inlined the same as plotpress’s own JS, keeping the “no external requests” guarantee intact regardless of what it contains.
- save(path, interactive: bool = False, scale: int = 2, pick_precision: int = 6, pick_max_mesh_cells: int = 250000, pick_max_points: int = 20000, binary_pick_data: bool = True, fps: int = 10, slider_unit: str = 'main', label_frames: bool = True, include_default_js: bool = True, extra_js: str = None, dpi: float = None, transparent: bool = False, format: str = None, options=None)[source]
Save by extension:
.svg,.html,.png,.pdf,.gif,.eps,.jpg/.jpeg, or.webp.pathmay also be a file-like object (aBytesIO, an open file) instead of a filename – the standardfig.savefig(buf, format="png")idiom for serving a figure without touching disk.formatnames the format explicitly (a bare extension, with or without the leading dot); it is required whenpathis a file-like object, since there is no filename to read an extension from, and optional otherwise, where it overrides whatever the path’s own extension would have picked.All formats work with the standard install (PNG/JPEG/WebP are a supersampled raster; PDF/EPS are vector).
pick_precision/pick_max_mesh_cells/pick_max_points/binary_pick_data/include_default_js/extra_jsapply only to interactive HTML (seeto_html())..gifneeds at least oneAxes.plot_frames()orAxes.pcolormesh_frames()series – it animates through that series’ frames atfps, the same data an interactive HTML slider scrubs through, as a self-contained looping file;slider_unitpicks which slider drives the animation for figures with more than one, andlabel_framesstamps each frame with its slider value since a GIF has no slider to show it on (seeplotpress.raster.save_gif())..jpg/.jpeg/.webpare raster, like.png(and share itsscale), but lossy – JPEG and WebP compress dense mesh/image content smaller than PNG at some cost to sharp text/line edges; PNG stays the better default unless a downstream consumer specifically needs one of these..epsis vector, like.pdf– for submission pipelines that still require EPS specifically.dpi(PNG/JPEG only) overridesStyle.dpifor this save alone, without mutating the figure – a physically larger/smaller image at the same layout proportions (fonts, markers and margins all scale with it, exactly as they would ifStyle.dpiitself had been set that way), and the dpi value the saved file’s own metadata reports.transparent(PNG only – JPEG has no alpha channel) drops the figure’s own background fill, leaving the outer canvas transparent instead of painted withStyle.facecolor; each axes’ ownfacecoloris unaffected.
- show(interactive: bool = True, wait_for_extract: bool = False, options=None)[source]
Display in a native pop-up window (via pywebview if installed).
Returns the list of markers the user extracted in the window (each a dict of values:
x,y, any extra dims,axes(index),axes_title(if that axes has one),kind), or an empty list if none were extracted. Point Picking markers only – Extract lives under the Point Picking menu and no longer includes annotation notes (dropped via any of the three Annotate tools), which have no export of their own.With
wait_for_extract=Truethe call becomes an interactive point- picking session: the kernel blocks, the user drops markers and clicks Extract, and that returns the markers to the kernel and closes the window (no manual close needed).The native window needs the
[gui]extra (pip install plotpress[gui]). Without it, this falls back to opening the figure in the default browser and returnsNone(use the in-page Extract panel to copy/download).
- show_qt(title='plotpress', block=True, interactive=True, pick_precision=6)[source]
Display in a native Qt window (PyQt/PySide), for Qt-based apps.
Thin wrapper around
plotpress.qt.view. Needs a Qt binding with WebEngine (pip install plotpress[qt]). To embed the figure inside your own Qt layout instead of a standalone window, useplotpress.qt.PlotPressWidgetdirectly.
- show_in_jupyter(width=None, height=None, interactive: bool = True, pick_precision: int = 6, pick_max_mesh_cells: int = 250000, pick_max_points: int = 20000, binary_pick_data: bool = True, include_default_js: bool = True, extra_js: str = None, options=None)[source]
Display inline in a notebook cell with the full interactive toolbar.
Evaluating a figure directly (
figas a cell’s last expression) renders it inline as static SVG viaFigure._repr_svg_– there is deliberately no_repr_html_, since Jupyter preferstext/htmloverimage/svg+xmlwhen a MIME bundle offers both, and a full interactive HTML document dropped into an output cell that way renders messily and its<script>doesn’t run there regardless.This instead wraps the same self-contained HTML
to_html()produces in an<iframe>, which does isolate and run the inlined JS – so the toolbar, pan/zoom, and point-picking all work exactly as they do in a saved.htmlfile opened in a browser.width/heightdefault to the figure’s own pixel size (figsizexstyle.dpi); pass either to override. The rest of the keyword arguments are forwarded toto_html()(see there for what each controls).Returns an
IPython.display.HTMLobject – return it as a cell’s last expression, or pass it toIPython.display.display(). Needs IPython (pip install plotpress[jupyter]), which any real Jupyter environment already has.
- plotpress.subplots(nrows=1, ncols=1, figsize=(6.4, 4.8), style: Style = None, facecolor=None, squeeze=True, sharex=False, sharey=False, projection=None, subplot_size=None)[source]
Convenience constructor mirroring
matplotlib.pyplot.subplots.Unlike matplotlib, this creates and returns a fresh, fully independent figure – there is no global state touched.
sharex/shareylink the grid’s limits and hide inner tick labels.projection='polar'makes the axes polar.subplot_size=(w, h)sizes one subplot in inches instead of the whole figure, andfigsizeis then solved for rather than given:# every panel exactly 1.2 x 0.9in, whatever the grid costs around it fig, axes = plotpress.subplots(20, 25, subplot_size=(1.2, 0.9))
This is the useful knob once a grid is large: a readable panel is a fixed size, so what a caller actually knows is how big one panel should be, not what the 500 of them plus their tick labels, titles, colorbars, group boxes and
supxlabeladd up to.figsize=(ncols * 1.2, nrows * 0.9)is the usual guess and it is always wrong, because none of that surrounding furniture scales with the grid the way the panels do.Whatever room the decorations need is measured and added on top, so the panels come out the requested size rather than that size minus the margins.
tight_layout()does the solving, so it has to run (it already does at render time if you never call it yourself) – and because it measures real text, the answer accounts for the labels actually set, not a guess made before they existed.figsizeis ignored whensubplot_sizeis given, beyond seeding the first pass. Callingset_size_inches()afterwards takes that control back: the figure keeps the size you set and the panels land wherever they land.
Grouping
- plotpress.subplots_from_groups(layout: GroupLayout, figsize=(6.4, 4.8), style: Style = None, facecolor=None, squeeze=True, sharex=False, sharey=False, projection=None, subplot_size=None)[source]
Build a figure from a
GroupLayout– see there for how to describe the outer/inner grid shapes. Mirrorssubplots()’s own signature and creates a fresh, independentFigurethe same way.subplot_size=(w, h)works as it does onsubplots()– see there – and sizes one axes, not one group: a layout of 2x1 groups asked forsubplot_size=(1.6, 1.6)gives 1.6in-square panels, with each group box ending up twice that tall plus its title band. The space those boxes and their titles need is measured and added, not taken out of the panels.Returns
(fig, axes):axesis shaped likelayout’s own outer grid (a bare value for a 1x1 layout, a 1-D array for a single outer row/column, otherwise 2-D), and each present cell holds that group’s own inner axes array – whateversubplots()itself would return for that group’s shape. An outer cell with no group registered isNone.sharex/shareylink limits within each group only (every group is its own independent cluster, the same as callingsubplots()separately for each one) – not across the whole figure.Call
Figure.tight_layout()/Figure.group_spacing()on the returned figure afterward exactly as you would for any other grid; a group’s own box comes from an ordinaryFigure.group()call this makes internally, so it works the same way in every respect – includingFigure.get_groups()reading it back later.Round-tripping a figure built this way through
load_data()/figure_from_template()recovers every axes and group correctly, but – since a group’s own cells are spans on the shared grid, not single non-spanning ones –axeson the way back isfigure_from_template()’s own flat-list fallback, not reconstructed back into this same nested shape.
- class plotpress.figure.GroupLayout(nrows: int, ncols: int)[source]
Describes an outer grid of groups, each its own inner grid of axes – so a figure like “four quadrants, each its own 2x2 cluster of plots” can be built by describing that shape directly, instead of hand-deriving which cells of one big flat grid each quadrant’s axes actually occupy:
layout = plotpress.GroupLayout(2, 2) layout.add_group(0, 0, 2, 2, title="Group (0,0)") layout.add_group(0, 1, 2, 2, title="Group (0,1)") layout.add_group(1, 0, 2, 2, title="Group (1,0)") layout.add_group(1, 1, 2, 2, title="Group (1,1)") fig, axes = plotpress.subplots_from_groups(layout, figsize=(12, 10)) axes[0, 0][1, 1].plot(x, y) # group (0,0)'s own bottom-right axes
Every group may have a different inner shape – there is no requirement that they match. Internally this is resolved onto one plain, flat grid (the least common multiple of every group’s own row/column count, times this layout’s own outer shape) with each group’s axes as ordinary, non-overlapping
SubplotSpecspans within it – so once built, the figure is completely ordinary: every axes has one flat_subplotspecin one shared grid, exactly likesubplots()/Figure.add_gridspec()already produce.tight_layout(),align_xlabels()/align_ylabels,Figure.to_vega/to_vega_lite, and twin/secondary axes all keep working completely unmodified – none of them ever finds out the figure was built this way.Only one level of grouping – a group cannot itself contain groups. Every group also occupies exactly one outer cell (no outer row/column spans yet); pass a taller/wider inner shape instead if a group should take up more of the figure than its neighbors.
Keep every group’s row count sharing a small common multiple with its neighbors’ (and likewise for columns): the shared grid’s own size is the least common multiple of every group’s own row/column count, so mixing incompatible shapes – a 5-row group alongside 2-row and 3-row ones, say – can need a far finer shared grid than any of them alone would suggest (
lcm(5, 2, 3) = 30). Past about 40 rows or columns,tight_layout()’s own cell-size floor (each cell needs at least 2% of the figure’s width/height, an ordinary-grid safeguard that was never tuned for a resolution this fine) can silently drop every row/column gap – includinggroup_spacing()’s own deliberate reservation between groups – to keep cells from shrinking to nothing, which reads as groups overlapping rather than a sizing problem.subplots_from_groups()warns when this happens; prefer shapes like 2, 4, and 8 over 2, 3, and 5 to avoid it in the first place.- add_group(row: int, col: int, nrows: int = None, ncols: int = None, mask=None, axes_ids=None, axes_titles=None, title: str = None, id=None, linestyle='--', color='black', linewidth=1.5, title_position='top', pad=8.0, fontsize=None, supxlabel=None, supylabel=None, supxlabel_size=None, supylabel_size=None, visible=True)[source]
Place a group at outer cell
(row, col)with annrowsxncolsinner grid of axes. Returnsselfso calls can chain.Presence (which inner cells actually get an axes – for an irregular group: an L-shape, a ring, a hole in the middle, instead of a plain rectangle) comes from whichever of these is given –
nrows/ncolsare then inferred from it, rather than needed separately:mask, annrowsxncolsarray-like of truthy/falsy values – falsy means no axes there. Pass real booleans/ints, not strings:numpycasts a non-empty string toTrueregardless of its content ("False"included), so a mask read back from text (a CSV column, say) needs converting first.axes_ids, an array-like of the same shape,str | None– a non-Noneentry means an axes and sets its id (seeset_id()) in one step, mosaic-style.axes_titles, the same idea for each axes’ title (seeset_title()) instead of its id.
Giving more than one of these is fine as long as they agree on which cells are present – raises, naming the cell, if they don’t. With none of them, plain
nrows/ncols(both required then) place a full rectangle with no per-axes id/title.title/idand the styling kwargs (linestyle/color/linewidth/title_position/pad/fontsize/supxlabel/supylabel/supxlabel_size/supylabel_size/visible) matchFigure.group()exactly – passed straight through to it once this group’s real axes exist. A group is only registered (and so only findable viaFigure.get_group(), including byid) when it has atitle–idwithout one raises, since it would otherwise silently do nothing.
- remove_group(row: int, col: int)[source]
Remove the planned group at outer cell
(row, col)– beforesubplots_from_groups()builds anything, so that cell simply has no group (and no axes) at all. Raises if there’s no group there. To remove a group from an already-built figure instead, seeFigure.remove_group(). See Mosaic titles, planning a layout ahead, and an ordinary grid’s own lookup for a worked example.
- class plotpress.figure.Group(raw: dict)[source]
One of a figure’s registered groups (see
Figure.group()/Figure.get_groups()/Figure.get_group()) – a read-only snapshot, not something to construct directly.title/idmatch whateverFigure.group()(orGroupLayout.add_group(), which calls it internally) was given.outer_row/outer_colare this group’s own position in itsGroupLayout’s outer grid –Nonefor a group built by a directFigure.group()call, which has no such position.axesis the 2-D(row, col)arraysubplots_from_groups()itself returned for this group (Nonefor an absent cell) when built from a shaped layout; otherwise (a directFigure.group()call, with no inherent grid shape) it’s a plain flat list.- flat_axes()[source]
Every real axes in this group as a plain flat list, regardless of whether
axesitself is shaped (a(row, col)array,Nonefor an absent cell skipped) or already flat (a manually builtgroup()with no grid shape). See Irregular group shapes (deleted axes) for a worked example.
- get_ax(row: int = None, col: int = None, title: str = None, id=None, many: bool = False)[source]
The axes matching exactly one of:
(row, col)together (this group’s own inner position – raises if this group has no grid shape, i.e. it wasn’t built from aGroupLayout),title, orid, scoped to this group’s own axes only. SeeFigure.get_ax()for themany=behavior, and Irregular group shapes (deleted axes) for a worked example.
Report
- class plotpress.figure.Report(title: str = None, description: str = None)[source]
An ordered collection of figures combined into one self-contained HTML file.
Each figure keeps its own independent interactivity – its own toolbar, pan/zoom, point-picking, annotations – because it is embedded in its own
<iframe>rather than spliced directly into the page. An interactive figure’s JS (plotpress._interactive) assumes it owns the page: fixed element ids (plotpress-svg,plotpress-meta, …) and a document-level toolbar, so several figures sharing one page directly would collide – the same reason the docs gallery embeds every live figure this way (seedocs/conf.py’s_interactive_embed). An iframe gives each figure its own document instead, at no real cost to “one file”: each figure’s already-self-contained HTML (seeFigure.to_html()) is inlined via the iframe’ssrcdocattribute rather than referenced as a separate file, so the report is still a single, self-contained HTML document with no external requests.Add figures with
add(), in the order they should appear, then write the combined file withsave():report = plotpress.Report(title="Weekly QA sweep", description="Four sensor batches, one figure each.") report.add(fig_a, title="Batch A", details="Baseline run, no anomalies.") report.add(fig_b, title="Batch B", details="Elevated noise floor after 14:00.") report.save("qa_sweep.html")
- add(figure: Figure, title: str = None, details: str = None) Report[source]
Append
figureto the report; returnsselfso calls can chain.title(a short heading) anddetails(a longer description) are optional per-figure annotations rendered above the embedded figure. Figures appear in the HTML in the order they were added – there is no separate ordering mechanism to keep in sync.
- save(path: str, interactive: bool = True, pick_precision: int = 6, pick_max_mesh_cells: int = 250000, pick_max_points: int = 20000, binary_pick_data: bool = True, collapsed: bool = False, options=None) str[source]
Write every added figure, in order, to one self-contained HTML file.
interactiveand thepick_*/binary_pick_dataarguments are forwarded to each figure’s ownFigure.to_html()– see there for what they mean. Every figure in the report shares the same settings; callFigure.to_html()directly (and write the file yourself) for a mix of interactive and static figures on one page.Every entry is collapsible: a click anywhere on its “Figure N”/title header hides just that entry’s figure, leaving its title and details visible – a long report reads as a scannable outline instead of a wall of figures. A Collapse All/Expand All button above the first entry does the same for every one at once.
collapsed=Truestarts every entry collapsed instead of open, and genuinely defers each one: rather than embed it as a livesrcdocthat just sits hidden, the escaped document is parked in a plain data attribute and only ever assigned to the iframe – triggering the real parse/render – the first time a reader actually expands that entry. A collapsed figure’s own toolbar/pan-zoom/pick-data JS never runs until then, so a report with many (or heavy) figures opens instantly regardless of how many it holds, at the cost of a brief render on each entry’s first expand instead.
- plotpress.load_data(path: str, by_index: bool = False)[source]
Read back the plotted data embedded in a self-contained interactive HTML file written by
Figure.to_html()/Figure.save()orReport.save().By default, returns a dict keyed by each figure’s own title (a
Reportentry’sReport.add()title; a generated"Figure N"– 1-based, matching the label aReportpage itself shows – for an entry with none, or for a bareFigure’s HTML, which has no report-level title at all). Each figure’s own value has"details"(a Report entry’s longer description, orNone)"axes"(itself a dict keyed by each axes’ own title, falling back to"axes {index}"– matching a picked record’saxes_titlefallback – for an untitled one), and"template":{"series": [{"kind": "line", "x": array, "y": array, "vals": {name: array, ...}, "label": str | None, "color": str | None}, ...], "meshes": [{"x": array, # 1-D cell centers (None if curvilinear) "y": array, # 1-D cell centers (None if curvilinear) "z": array, # 2-D, shape (ny, nx), row 0 = ymin "extent": (xmin, xmax, ymin, ymax), "curvilinear": bool}, ...], "pies": [...], "title": str | None, "xlabel": str | None, "ylabel": str | None, "zlabel": str | None, "xlim": (float, float) | None, "ylim": (float, float) | None, "xscale": str, "yscale": str}
A series’
"label"/"color"are the artist’s ownlabel=/ (single, resolved)color=at save time –Nonefor a file saved before these existed, an unlabeled/uncolored series, a colormap-mappedscatter(c=...)(no one color to report), or a kind with no single meaningful color/label at all (box/violin/quiver/event/ contour). Real for"line"/"scatter"/"stem"/"errorbar"/"bar", which is enough to rebuild a labeled, colored legend after replotting recovered data – seefigure_from_template()."template"is the figure-level structure and styling – grid shape/position, every decoration (title, labels, limits, scale, …), spine colors, tick overrides, and id of each subplot-grid axes, anyFigure.group()boxes, twin/secondary/inset overlays, colorbar styling, this figure’s ownStyle, and its sup-title/label – needed to rebuild an equivalent, already-styled figure, independent of the per-axes data above. This is the exact same dictFigure.to_template()produces (seeplotpress.svg.template_metadata()for the full field-by-field breakdown) – pass it straight toplotpress.figure_from_template()to recreate the source figure’s grid, every axes’ own decorations and styling, its groups, and its overlays, before replotting recovered data into it – see Reloading data from a saved HTML. A file saved before 3-D support was removed can still report the literal"3d"here (this function only reads back whatever string was stored, it doesn’t validate it) –figure_from_template()raises a clear “unknown projection” for that one, since it cannot rebuild an axes kind that no longer exists.Axes placed with a freeform
Figure.add_axes()rect (no grid cell) and colorbar axes are absent from"axes"– their indices are listed in"omitted_axes"instead – and a group’s own"n_members"is its original member count, before any unrecoverable member was filtered out of its"axes"list, so a caller can tell a group that lost one apart from one that didn’t."legend"is recorded but not auto-applied byfigure_from_template– see that function’s own docstring for why. A file saved before this block existed loads as{"figsize": None, "axes": {}, "groups": [], "omitted_axes": [], "suptitle": None, "supxlabel": None, "supylabel": None, "facecolor": None, "style": None, "overlays": [], "insets": [], "colorbars": []}, and one saved by an in-between version (before some of these fields existed) has the same empty defaults padded in for whichever it predates.Title keys are convenient but not guaranteed unique – two figures (or two axes within one figure) sharing the same title no longer collide silently: the later one is disambiguated with a
" (2)"," (3)", … suffix rather than overwriting (and losing) the earlier one, and aUserWarningnames every collision resolved this way. Passby_index=Truewhen even that renaming matters, or when a stable, order-based key is simply more useful than a name: this returns a list of per-figure dicts instead (one per figure embedded in the file, in the order they appear – a bare figure’s HTML still comes back as a one-item list), each with the same"title"/"details"/"axes"/"template"shape as above except"axes"is keyed by plain integer index rather than title – and never renamed, since there is no title collision to resolve when the key is a position instead of a name.Only works on HTML saved with
interactive=True: a static SVG or aninteractive=FalseHTML embeds no data to read back, only drawn shapes, and raisesValueError. Recovered arrays reflect whatever precision/caps were in effect at save time (pick_precision,pick_max_points,pick_max_mesh_cells) – they are not guaranteed bit-exact copies of the original data for a series/mesh that was rounded or capped on the way out. A mesh that crossedpick_max_mesh_cellsat save time comes back at that coarser, block-averaged resolution, not the original grid’s – seeFigure.to_html()’s own docstring for exactly what that averaging costs.
- plotpress.load_data_xarray(path: str, figure=None)[source]
Read one figure’s plotted data back as a single
xarray.Dataset, dimensioned by the figure’s own axes grid (row/col, from the same layoutload_data()already returns) instead ofload_data()’s title-keyed dict of dicts.Needs the optional
xarraydependency:pip install plotpress[xarray].Built for the case Reloading data from a saved HTML already showcases – a uniform grid of same-shaped scientific measurements (every panel its own
pcolormesh, or its own single line series) – where a title-keyed dict of dicts is the wrong tool entirely: a caller wanting “the z value at row 2, column 3” has to already know that panel’s title (or fall back toload_data()’s ownby_index=True, still just a flat list with no row/column structure of its own), loop over every panel by hand to stack them into one array, and hope no two panels happened to share a title – seeload_data()’s own now-fixed collision handling, which this sidesteps structurally rather than by disambiguating: xarray indexes by integer row/column position, never by a string title, so there is no title to collide on in the first place.Only supports a uniform rectangular grid – every axes a single, non-spanning cell (as
plotpress.subplots()/Figure.add_subplot()place them, never a row/column span fromadd_gridspec) – where every axes with data carries exactly one mesh (all the same shape, non-curvilinear) or exactly one line series (all the same length), never a mix of the two kinds, and never more than one series/mesh on a single axes. A cell with no axes at all, or an axes nothing was ever plotted on, is fine – it comes back NaN (itsx/ytoo, in the per-panel-coordinate case), distinguished from a panel whose real data legitimately happened to be all-NaN by thehas_datacoordinate below. RaisesValueError, naming exactly what about the figure didn’t fit, for anything else – a mixed grid, a span, multiple series per axes, differing mesh shapes – pointing atload_data()(by_index=Truefor the title-collision-proof form) as the fallback for a figure this doesn’t cover.The returned
Datasethasrow/colcoordinates plus each panel’s owntitle/xlabel/ylabel(""for a missing panel) andhas_data(Truefor a grid cell an axes with plotted data actually occupies,Falsefor one with no axes or nothing plotted) as(row, col)coordinates; a mesh grid’sx/yare shared 1-D coordinates when every panel used the identical grid, else per-panel(row, col, x)/(row, col, y)arrays – and its data variable isz, dimensioned(row, col, y, x). A line grid’s data variable isy, dimensioned(row, col, point), withxthe same shared-or-per-panel choice..attrscarries the recovered figure’s ownfigsizeand title, plus"template"– the exact same dictload_data()returns under that key, ready to pass straight tofigure_from_template()without a second, separateload_data()call just to get it –ds.attrs["template"], not a duplicate parse of the file.figureselects which figure to load from a multi-figureReportfile – an int index (0-based, save order) or the exact string title aReportentry was given. Left asNone(the default), the file must have exactly one figure, or this raises naming how many it actually found.
- plotpress.load_template(path: str) dict[source]
Read back a
Figure.save_template()file: plain JSON, no HTML parsing involved, unlikeload_data(). Pass the result tofigure_from_template()to rebuild the figure it describes.
- plotpress.figure_from_template(template, figsize=None, style: Style = None, facecolor=None)[source]
Rebuild a figure from a template dict: the same grid shape,
Figure.group()boxes, per-axes decorations, spine colors, tick overrides, ids, twin/secondary/inset overlays, colorbar styling, andStyle– everythingto_template()/plotpress.svg.template_metadata()capture. This is the one reconstruction function for two different starting points that produce the identical dict shape:A reusable, data-free template – built with
Figure.to_template()/save_template()and read back withplotpress.load_template(), with no plotted data anywhere in it. Replot into the returned (blank) axes the same way you would afterplotpress.subplots(...).A figure recovered from a saved HTML export – read via
plotpress.load_data()’s own"template"key, alongside that same call’s"series"/"meshes"/"pies"data to replot. This also carries the recovered figure’s real title/labels/limits/scale/ grid/aspect/spines/ticks/style/overlays – everything about how it looked, so nothing here needs re-setting by hand, only the data itself needs replotting back in.
figsize/facecoloroverride the template’s own saved values.styleoverridestemplate["style"]outright; a template saved before"style"existed falls back to a fresh, defaultStylewhen no override is given, the same fallback shapefigsize/facecoloralready use for their own missing/older keys.Returns
(fig, axes). When every recorded axes is a single, non-spanning cell that exactly tiles onenrowsxncolsgrid,axesmirrors whatplotpress.subplots(nrows, ncols)itself would hand back – a bareAxesfor a 1x1 grid, a 1-D array for a single row/column, otherwise a 2-D array indexedaxes[row, col]. Anything else (row/column spans fromadd_gridspec, mismatched grids across axes, or no grid-placed axes at all) falls back to a flat list of axes in their original save order – still fully usable, just not array-indexable by row/column. Twins/secondaries/insets are already built and attached tofig.axesbut are not folded into this return value – the same wayax.twinx()isn’t folded intoplotpress.subplots()’s own return either; give an axes (or its parent) anidbefore saving the template if a lookup afterward needs to find it reliably, viafig.get_ax(id=...).Colorbars are documented in
template["colorbars"](which axes had one, and itsfraction/pad/label/ticks/format) but never auto-built – a colorbar needs a live mappable, which doesn’t exist until real data is plotted. Callfig.colorbar(mesh, ax=...)yourself once you’ve replotted, passing those same styling knobs back if you want them preserved. An axes that had alegend()is recorded too, but never auto-applied either – a legend draws from already-plotted, labeled artists, none of which exist on a freshly rebuilt axes yet; callax.legend(**entry["legend"])yourself once you’ve replotted into it.Warns (
UserWarning) whentemplate["omitted_axes"]has an axes beyond what"overlays"/"insets"account for – a freeformFigure.add_axes()rect has no recorded position to rebuild from, so it’s simply missing from the returned figure; the warning is the only signal of that, since a caller with no other axes count to compare against would otherwise have no way to notice. A separate warning names anyFigure.group()whose own box lost a member to that same drop – the group is still created around whichever of its axes did come back, just smaller than the original.
- plotpress.select_panel(ds, title=None, row=None, col=None, multiple=False)[source]
Pull one panel out of a
load_data_xarray()grid, droppingrow/colentirely instead of leaving them behind as length-1 dimensions –ds.isel(row=r, col=c)already does exactly that for a scalarr/c, which is all this is: that call, plus resolvingtitleto the one(row, col)position it names.Pass either
title(matched againstds["title"], the same stringload_data()/a panel’s ownax.set_title()used) or bothrow/col(plain 0-based grid position) – not a mix of the two, and not neither. RaisesValueErrorwhentitlematches no panel at all. Whentitlematches more than one panel (two panels sharing a title, so there is no name left to disambiguate by), this raises too unlessmultiple=True, which returns every match as a list instead of picking one.multiple=Truealways returns alistofDatasets – one item for a uniquetitleor an explicitrow=/col=, or one per match for a duplicatedtitle– rather than a list only sometimes and a bareDatasetotherwise, so a caller that always wants to loop over the result doesn’t have to branch on how many panels actually matched.Each returned
Datasetkeeps every data variable/coordinateload_data_xarray()built, just withoutrow/col– a mesh panel’szis(y, x)instead of(row, col, y, x), a line panel’syis(point,)instead of(row, col, point), andtitle/xlabel/ylabel/has_datacome back as plain scalar attributes of that one panel rather than(row, col)arrays.ds = plotpress.load_data_xarray(path) panel = plotpress.select_panel(ds, title="panel 4") panel["z"].plot() # a plain (y, x) DataArray, xarray's own .plot() # Two panels both titled "control" -- get both instead of raising. controls = plotpress.select_panel(ds, title="control", multiple=True) for p in controls: p["z"].plot()
Axes
- class plotpress.axes.Axes(figure, rect)[source]
- transAxes = <object object>
Pass to
text()/annotate()’stransform=for an axes-fraction position –(0, 0)bottom-left,(1, 1)top-right – instead of data coordinates, e.g. a label pinned to a corner regardless of xlim/ylim.
- set_prop_cycle(color)[source]
Set this axes’ own color cycle, independent of the figure’s.
ax.styleis the same object asax.figure.style(not a per-axes copy), so this stores the override on the axes rather than mutatingself.style.color_cycle– that would leak the override to every other axes on the figure.
- plot(*args, color=None, linewidth=None, linestyle=None, label=None, alpha=1.0, values=None, marker=None, markersize=None, markerfacecolor=None, markeredgecolor=None, markeredgewidth=None, zorder=0)[source]
Plot
y,x, y, orx, y, fmtas a line. Returns theLine2D.fmtis matplotlib’s format-string shorthand ('ro-','k.','C1--') – any of a color, a linestyle, and a marker, in one string (see_parse_fmt()). An explicitcolor=/linestyle=/marker=keyword overrides whateverfmtsays for that piece; a marker with no linestyle character infmtmeans no connecting line, matplotlib’s own convention.valuesis an optional{name: array}of extra per-point dimensions (e.g.z) surfaced when a point is picked interactively.markerdraws a shape at each vertex in addition to the line itself (markersizein points, default matches the style’s own marker size;markerfacecolordefaults to the line’s owncolor;markeredgecolor/markeredgewidthoutline it, the same asscatter()’sedgecolors/linewidths). Beyond round ("o"/"."),"s"/"^"/"v"/"<"/">"/"D"/"d"/"+"/"x"/"X"/"|"/"_"render as their own real shape; anything else falls back to a round dot with a warning (see_warn_marker_shape()), the same limitationscatter()/errorbar()share.x/yneed not be plain numbers:Datetime-like (
numpy.datetime64,datetime.date/datetime.datetime, or a sequence of those – a pandasSeries/DatetimeIndexalready becomes one of these throughnumpy.asarray) plots at its real position in time, so an irregular gap between two points still looks proportionally different from a small one. Tick locations and labels then follow a calendar-aware scheme (year/month/day/hour/… – whichever tier fits ~5 ticks across the current view) instead of plain numbers.Strings turn that axis categorical: each distinct value gets an integer position (0, 1, 2, …) in the order it’s first seen on this axes – across every plotting call, so two series naming the same categories share positions – with the strings themselves as the tick labels, one per category.
Once either kind touches an axis it stays that way for every artist plotted against it afterward (mixing plain numbers into an already categorical/date axis on the same dimension isn’t meaningful and isn’t supported). See Datetime axes, categorical axes, and declarative tick specs and Datetime Gantt chart with milestones for worked examples.
- scatter(x, y, s=None, c=None, color=None, marker='o', label=None, alpha=1.0, cmap='viridis', norm=None, vmin=None, vmax=None, values=None, zorder=0, edgecolors=None, linewidths=None)[source]
Scatter
yvsx.cmaps values throughcmap.valuesis an optional{name: array}of extra per-point dimensions (e.g.zor a 4th value) surfaced by point picking; the color dimensioncis included automatically.edgecolors/linewidthsoutline every marker in the collection (one color/width for the whole call, not per-point) – the same contrast marker matplotlib draws to keep overlapping same-color points distinguishable. Givingedgecolorswith nolinewidthsstill draws a visible outline, at matplotlib’s own default width.Beyond round,
"s"/"^"/"v"/"<"/">"/"D"/"d"/"+"/"x"/"X"/"|"/"_"render as their own real shape (see_warn_marker_shape()); anything else falls back to a round dot with a warning.x/ymay also be datetime-like (real time-proportional spacing) or strings (a categorical axis, positions 0, 1, 2, … in first-occurrence order) – seeplot()for both.
- plot_frames(x, Y, slider_values=None, slider_label='frame', shared=True, slider_group=None, color=None, linewidth=None, linestyle='-', label=None, alpha=1.0, zorder=0)[source]
Plot 3-D data as a line with a slider over the extra dimension.
Yhas shape(n_frames, n_points);xis shared(n_points,)or per-frame(n_frames, n_points).Slider scope:
shared=True(default) – this series joins the figure’s single global slider, so all sharedplot_framespanels scrub together.shared=False– this axes gets its own slider docked beneath it. Passslider_group="name"to give several axes the same connection index: each still has its own docked slider, but the UI shows an index badge and a checkbox to link them so they scrub together on demand.
slider_valueslabels the extra axis (defaults to0..n-1).
- pcolormesh(*args, cmap='viridis', norm=None, vmin=None, vmax=None, shading='flat', zorder=0, alpha=1.0, label=None, rasterized=None)[source]
Pseudocolor plot of a 2-D array.
Signatures:
pcolormesh(C)orpcolormesh(X, Y, C).X/Ymay be 2-D for a curvilinear grid.shading="gouraud"smoothly interpolates the color between grid nodes instead of flat cells.alpha/labelmatchimshow()– its own animated siblingpcolormesh_frames()already had both; this one just hadn’t caught up.A non-uniform rectilinear grid (cell widths that vary) normally has to be resampled into the SVG’s one embedded raster image, which can lose a cell narrower than one output pixel entirely – see Non-uniform meshes: vector cells by default, and when raster comes back.
rasterizedcontrols how that grid is drawn:None(default) – automatic. A uniform grid rasterizes (its fast path is already a lossless, byte-identical copy, so there is nothing to gain from vectors). A non-uniform grid under_VECTOR_CELL_LIMIT(~2000) cells draws as exact vector<rect>elements instead – no resampling, so no cell can ever be too thin to draw. Past that cell count it falls back to the raster path, to keep the file size from scaling with cell count the way one-mark-per-point artists do.True/False– force raster or vector outright, overriding the automatic choice above (even on a uniform grid, or a huge one –Falsethere warns that the SVG will scale with cell count, since_VECTOR_CELL_LIMITis only ever consulted by auto mode). A curvilinear grid (2-DX/Y) has no vector path at all – its cells aren’t axis-aligned rects – so it always rasterizes andrasterized=Falsethere warns that it was ignored, rather than silently drawing raster when exact cells were asked for.
Either way, if the raster path ends up dropping a cell, a warning names it. Vector cells are an SVG/PDF-only fix – a PNG export always takes the raster path regardless of this setting (a PNG is pixels by definition), so a mesh that vectorized fine for SVG can still drop the same cell if you also export it as PNG; pass
rasterized=Trueonce to see what that export would actually lose.The returned
QuadMeshexposes the resolved decision for introspection:.rasterized(what you passed),.vectorized(what actually happened),.n_cells, and.dropped_x/.dropped_y(the cell indices, if any, the raster path would drop along each axis – seedocs/examples/limitations/plot_05_pcolormesh_vector_cell_limit.pyfor a worked example reading them).
- pcolor(*args, **kwargs)[source]
Alias of
pcolormesh()– matplotlib itself now recommendspcolormesh(faster, and this library’s own vector/raster cell handling already only exists on that path);pcoloris kept only so code written against matplotlib’s name still runs unchanged.
- pcolormesh_frames(*args, slider_values=None, slider_label='frame', shared=True, slider_group=None, cmap='viridis', norm=None, vmin=None, vmax=None, shading='flat', label=None, alpha=1.0, zorder=0)[source]
Plot 4-D data as a pcolormesh with a slider over the extra dimension.
Signatures:
pcolormesh_frames(C)orpcolormesh_frames(X, Y, C), matchingpcolormesh()exceptCcarries a leading frame axis – shape(n_frames, ny, nx)rather than(ny, nx).X/Yare shared across every frame; only the color data animates. The colour scale is autoscaled to every frame’s data at once, so it stays fixed while scrubbing rather than jumping frame to frame.Slider scope and
slider_values/slider_labelmatchplot_frames()exactly – see there forshared/slider_group.Unlike
pcolormesh(), this always rasterizes – there is norasterizedkwarg here – since the interactive slider scrubs by swapping one embedded image per frame, and per-cell vector geometry would need it to rewrite every cell’s fill on every frame instead. A non-uniform grid can still silently drop a thin cell the same way a static mesh can; a warning names it if so.
- bar(x, height, width=0.8, bottom=0.0, align='center', color=None, edgecolor=None, linewidth=0.8, label=None, alpha=1.0, yerr=None, xerr=None, capsize=3.0, ecolor=None, hatch=None, zorder=0)[source]
Vertical bar chart.
align(matplotlib’s own choices) is"center"(default: each bar centered on its ownx) or"edge"(xis the bar’s left edge instead – pass a negativewidthfor a right edge).yerr/xerrdraw error bars centered at each bar’s own top (bottom + height), composed from the same whiskers-and-capserrorbar()already draws (no connecting line, no marker) – so they autoscale and render exactly like a standalone error bar would.ecolor(default black, independent of the bars’ owncolor) matches matplotlib’s own bar-error-bar default.hatchtiles a pattern over the fill – one of"/","\\","|","-","+","x"(matplotlib’s own single-density hatch characters; always drawn in black, matching matplotlib’s default) – the standard way to distinguish grouped bars in greyscale print or for a colorblind reader, when color alone can’t. Any other value is ignored (plain fill), not an error.xmay also be strings – a categorical axis, one bar per distinct value, positioned at 0, 1, 2, … in first-occurrence order (seeplot()) – the standardbar(["Q1", "Q2", ...], values)idiom.
- barh(y, width, height=0.8, left=0.0, align='center', color=None, edgecolor=None, linewidth=0.8, label=None, alpha=1.0, xerr=None, yerr=None, capsize=3.0, ecolor=None, hatch=None, zorder=0)[source]
Horizontal bar chart.
align/xerr/yerr/capsize/ecolor/hatchmatchbar(), centered at each bar’s own right edge (left + width).ymay be strings, the same categorical axisbar()’sxsupports – see More categorical axes and declarative tick formats for a worked example.
- bar_label(bars, labels=None, fmt='{:g}', padding=0.0, color=None, fontsize=None, zorder=6)[source]
Label each bar in the
Barsbars(bar()/barh()’s own return value) with its height/width, just outside the bar’s tip – above for a positive vertical bar, below for a negative one; right/left the same way for a horizontal one.labelsoverrides the text shown, positionally (default: each bar’s own value formatted withfmt).paddingnudges the label away from the tip as a fraction of the axis span – matplotlib measures its ownpaddingin points; there is no such absolute unit here, so this is the closest equivalent, not a literal drop-in value.Returns the list of
Textlabels added, one per bar, in the same order asbars.pos.
- hist(x, bins=10, range=None, color=None, edgecolor='#ffffff', linewidth=None, label=None, alpha=1.0, density=False, zorder=0, histtype='bar', cumulative=False, weights=None, stacked=False, orientation='vertical')[source]
Histogram. Returns
(counts, edges, bars).linewidthoverrides the edge width, which otherwise defaults to0.6forhisttype="bar"(a divider between adjacent bars) or1.5for"step"/"stepfilled"(the outline itself is the only mark) – matching siblingbar()’s ownlinewidth=, which this method had no way to reach before.orientation="horizontal"bins along the y-axis instead (bars extend rightward from the y-axis; a"step"/"stepfilled"outline runs along y too).xmay be a single array or a sequence of arrays – multiple datasets share one set of bin edges (from their combined range whenbinsis a count rather than explicit edges), overlaid by default or, withstacked=True, stacked bottom-to-top in the order given.color/labelmay then be a matching list, one per dataset (a bare value applies to all, same as a single dataset).histtypeis"bar"(default: filled bars with dividers between them),"step"(unfilled outline, no dividers) or"stepfilled"(filled outline, no dividers) – matplotlib’s own three.barsis aBarsfor"bar"(one per dataset, a list if there’s more than one) or aPolygonstaircase outline for"step"/"stepfilled".cumulativerunning-sums each dataset’s own counts left to right.weights(matchingx’s own shape, or one array per dataset) weights each sample instead of counting it as 1.
- step(x, y, where='pre', color=None, linewidth=None, linestyle=None, label=None, alpha=1.0, zorder=0)[source]
Step (staircase) plot.
linestyleforwards straight toplot()– a dashed or dotted step, for overlaying two step curves distinguishably. Nomarker=: the staircase’s own corner vertices aren’t the real data points (each is repeated to draw the vertical/horizontal jump), so a marker drawn at every vertex would double up per point and land on the corner instead of the actual sample – plot the realx/yseparately withscatter()if markers at the original points are wanted.
- fill_between(x, y1, y2=0.0, where=None, interpolate=False, color=None, alpha=0.4, label=None, edgecolor=None, linewidth=0.0, zorder=0)[source]
Fill the area between
y1andy2.edgecolor/linewidthoutline the filled region – the same two optionsfill()already has, since both draw the same closed-path primitive; there was no reason the outline wasfill()-only.where(a boolean mask matchingx, typicallyy1 > y2or similar) restricts the fill to its contiguousTrueruns – each its own artist. By default each run stops at the last sample still inside it, leaving a visible gap up to wherey1/y2actually cross;interpolate=Trueextends each run to that exact linearly-interpolated crossing point instead (matplotlib’s own default for this case), for the common “shade where y1 exceeds y2” idiom. Returns a list of runs, one per contiguous region, instead of a single artist whenwhereis given.
- fill_betweenx(y, x1, x2=0.0, where=None, interpolate=False, color=None, alpha=0.4, label=None, edgecolor=None, linewidth=0.0, zorder=0)[source]
Fill the horizontal area between
x1andx2acrossy.edgecolor/linewidthmatchfill_between().where(a boolean mask matchingy) restricts the fill to its contiguousTrueruns, the same way, andinterpolate=Trueextends each run tox1/x2’s exact linearly-interpolated crossing point the same way – returns a list of artists, one per run, instead of a single one when given.
- fill(x, y, color=None, alpha=1.0, edgecolor=None, linewidth=0.0, label=None, zorder=0)[source]
Fill an arbitrary polygon given by vertices
x/y.
- hlines(y, xmin, xmax, color=None, linewidth=None, linestyle='-', label=None, alpha=1.0, zorder=0)[source]
Draw horizontal line segments at each
yfromxmintoxmax.
- vlines(x, ymin, ymax, color=None, linewidth=None, linestyle='-', label=None, alpha=1.0, zorder=0)[source]
Draw vertical line segments at each
xfromymintoymax.
- stem(x, y=None, baseline=0.0, color=None, linecolor=None, markercolor=None, label=None, zorder=0)[source]
Stem plot.
colorsets both the stems and the marker at once, matching every other line/marker method’s owncolor=convention;linecolor/markercoloroverride it independently where a stem plot’s two colorable parts need to differ (the same “a shared default, plus a more specific override” shapeerrorbar()’secolorhas).
- errorbar(x, y, yerr=None, xerr=None, fmt='', color=None, marker=<object object>, markersize=None, capsize=3.0, linestyle=<object object>, linewidth=None, label=None, alpha=1.0, zorder=0, ecolor=None, elinewidth=None, capthick=None, errorevery=1)[source]
Line/markers with error bars.
fmtis matplotlib’s 5th positional argument here too (its own real signature iserrorbar(x, y, yerr, xerr, fmt, ...)) – a format string like'ro-'(seeplot()/_parse_fmt()). This used to be plotpress’s owncolorslot, so a matplotlib caller’s 5th positional argument – almost always a fmt string – silently landed incolorinstead, rendering with whatever garbage color string that happened to be and no error anywhere. An explicitcolor=/marker=/linestyle=keyword still overrides whateverfmtsays for that piece.ecolor/elinewidthstyle the whiskers/caps independently of the connecting line and marker – each falls back tocolor(resolved the same way) /linewidthif not given, so nothing changes unless you pass them.capthick(the caps’ own width) falls back toelinewidthin turn.erroreverydraws a whisker/cap only every Nth point (default every point) – the connecting line and every marker still draw in full, only the error bars themselves thin out. For a dense series (thousands of samples), a whisker on every point paints solid black;errorevery=20keeps the uncertainty visible without the clutter.
- imshow(X, cmap='viridis', norm=None, vmin=None, vmax=None, extent=None, origin='upper', alpha=1.0, label=None, zorder=0, interpolation='nearest', aspect=None)[source]
Display an image / 2-D array.
interpolation="nearest"(default) draws each data cell as a crisp pixel block, however far the SVG scales it – anything else ("bilinear","antialiased", …) lets the browser smooth it instead. Only affects SVG output: raster (PNG/PDF) output already samples at its own fixed resolution, so there’s no separate scaling step for this to change.aspect, if given, is applied viaset_aspect()(this axes’ own aspect, not per-image) – left alone by default, unlike matplotlib’s ownimshow(), which forces'equal'even without an explicitaspect=(seematshow(), which does the same here).
- matshow(A, cmap='viridis', norm=None, vmin=None, vmax=None, alpha=1.0, label=None, zorder=0)[source]
Display a matrix as an image (origin at top, square cells).
- spy(A, cmap='gray_r', alpha=1.0, label=None, zorder=0)[source]
Show the sparsity pattern of
A– nonzero entries drawn dark.
- pie(x, labels=None, colors=None, startangle=90.0, radius=1.0, autopct=None, alpha=1.0, zorder=0)[source]
Pie chart. Hides the axis and fixes an equal-aspect square view.
- boxplot(x, positions=None, widths=0.5, color=None, orientation='vertical', vert=None, label=None, alpha=1.0, zorder=0, whis=1.5, showfliers=True, showmeans=False, labels=None, tick_labels=None)[source]
Box-and-whisker plot of one or more datasets.
whissets the whisker reach in IQRs pastq1/q3(matching matplotlib’s own default of1.5); points past that are drawn as fliers unlessshowfliers=Falsedrops them instead.vertis matplotlib’s olderTrue/Falsespelling oforientation(True->"vertical",False->"horizontal") – an explicitorientation=still wins if both are given.labels/tick_labels(matplotlib 3.9 renamed the former to the latter; both work here) label each box at its ownpositionsentry, viaset_xticks()/set_yticks().showmeansadds a marker at each box’s own mean, alongside the median line already drawn – round, like every plotpress marker (see_warn_marker_shape()), not matplotlib’s own triangle.
- violinplot(data, positions=None, widths=0.5, color=None, orientation='vertical', vert=None, label=None, points=100, cut=0.0, inner=None, alpha=0.55, zorder=0, showmeans=False, showmedians=False)[source]
Violin plot (kernel-density silhouettes).
cutextends each density past its data extremes by that many bandwidths (seaborn’s default is 2; 0 clips at the observed range).inneroverlays a summary of the raw data inside each violin:'box'(IQR bar + 1.5-IQR whiskers + median dot),'quartile'(lines across the density at Q1/median/Q3),'stick'(one line per observation), orNone.vertis matplotlib’s olderTrue/Falsespelling oforientation.showmeans/showmedianseach draw one solid/ dashed line across the violin at that value, independent ofinner(which summarizes the raw data a different way – combine either or both freely).
- kdeplot(data, color=None, linewidth=None, fill=False, alpha=0.3, points=200, cut=3.0, label=None, zorder=0)[source]
Kernel-density estimate of a 1-D sample.
cutextends the evaluation grid past the data extremes by that many bandwidths, so the tails decay to zero instead of being clipped.
- ecdfplot(data, color=None, linewidth=None, complementary=False, label=None, alpha=1.0, zorder=0)[source]
Empirical cumulative distribution of a 1-D sample.
- rugplot(x, height=0.03, side='bottom', color=None, linewidth=1.0, label=None, alpha=1.0, zorder=0)[source]
Tick marks at each observation along one edge of the axes.
heightis a fraction of the axes rectangle, resolved at draw time, so repeated rugs share a baseline and never shift the autoscale.side='left'rugs the y axis instead of the x axis.
- eventplot(positions, lineoffsets=None, linelengths=0.8, color=None, orientation='horizontal', label=None, alpha=1.0, zorder=0)[source]
Raster of event lines (one row per sequence).
- quiver(X, Y, U, V, scale=None, color=None, label=None, alpha=1.0, zorder=0)[source]
Field of arrows.
scalemaps (U, V) to data units (auto if None).
- arrow(x, y, dx, dy, color=None, alpha=1.0, label=None, zorder=0)[source]
Draw a single arrow from
(x, y)to(x + dx, y + dy), in data coordinates throughout (unlike matplotlib’s ownhead_width/head_length, measured in points).A thin wrapper over
quiver()with one vector andscale=1– that already draws exactly this, an arrow from a point by a data-space(dx, dy)offset, without quiver’s usual auto-scaling (which would size a single arrow to nearly the whole axes).
- quiverkey(Q, X, Y, U, label, coordinates='axes', labelpos='E', color=None, alpha=1.0, fontsize=None, zorder=5)[source]
A reference arrow near
(X, Y)showing what a vector of lengthU(inQ’s own data units) looks like, for theQuiverQreturned byquiver().coordinates="axes"(the default, matching matplotlib) treats(X, Y)as an axes fraction, resolved to a data point from this axes’ current limits at call time – unlike text’s owntransform=ax.transAxes, the key arrow is a data-anchoredQuiverunder the hood (there is no axes-fraction form of one), so it moves with a later data zoom/pan the way any other plotted artist does, rather than staying pinned to the corner.coordinates="data"gives(X, Y)directly in data coordinates.labelposplaceslabel"E"/"W"/"N"/"S"of the arrow (default east, matching matplotlib).
- barbs(X, Y, U, V, length=7.0, color=None, alpha=1.0, label=None, zorder=0)[source]
Wind barbs at
(X, Y): a shaft pointing(U, V)’s direction, with flags/full/half ticks near the tip encodinghypot(U, V)by the usual meteorological convention – a triangular pennant per 50 units of speed, a full tick per 10, a half tick for a remainder>= 5(speed rounded to the nearest 5 first), and a bare circle for a calm reading under 5.Unlike
quiver(),length(points, like a marker size) fixes the shaft’s physical length for every barb the same way regardless of magnitude – only the ticks near the tip encode speed, matching matplotlib.U/Vtherefore only set direction here, not shaft length; there is noscale=to tune.
- contour(*args, levels=8, colors=None, cmap='viridis', vmin=None, vmax=None, linewidths=None, linestyles=None, negative_linestyles='dashed', label=None, alpha=1.0, zorder=0)[source]
Contour lines.
contour(Z)orcontour(x, y, Z).Colors (when
colorsisn’t given explicitly) come from mapping each level’s own value throughcmap, normalized byvmin/vmax(defaulting toZ’s own min/max) – the same normalizationcontourf()uses, so an explicitvmin/vmaxcolors both the same way, and non-uniformlevels(e.g.[0, 1, 2, 10]) get each level’s true position on the scale, not just its rank among them.linewidths/linestylesare a single value or one per level (matchinglevels’ own length). Whencolorsis given explicitly (a single flat color, not acmapgradient) andlinestylesis left unset, negative levels default tonegative_linestyles(“dashed”) instead of solid – matplotlib’s own convention for reading a stream-function/vorticity/anomaly field’s sign without a colorbar. This default only applies to an explicit single color: a colormapped contour set already encodes sign through hue, so every level stays solid unless asked otherwise.
- contourf(*args, levels=8, cmap='viridis', vmin=None, vmax=None, alpha=1.0, label=None, zorder=0)[source]
Filled contours.
contourf(Z)orcontourf(x, y, Z).Rendered as a single embedded image whose colormap is banded (one flat color per level interval), so the returned value works with
fig.colorbar.levelsis a band count or explicit boundaries.
- clabel(CS, levels=None, fmt='%1.3g', fontsize=None, colors=None, inline=True, zorder=6)[source]
Label
CS(theContourcontour()returned) with each level’s own value, placed along its line.Unlike matplotlib, this places exactly one label per level (at the middle of its longest run of segments), not one per disconnected contour island, and does not break the line to make room for the label –
inlineis accepted for signature compatibility but has no effect here; the label’s own contrast halo keeps it legible over the line regardless.levelsrestricts labeling to a subset ofCS’s own levels (default: every level).fmtis a %-style format string or a callable taking the level value.colorsoverrides the label color (default: matches each level’s own line color).Returns the list of
Textlabels added.
- hexbin(x, y, gridsize=20, cmap='viridis', mincnt=1, label=None, norm=None, vmin=None, vmax=None, edgecolors=None, linewidths=None, alpha=1.0, zorder=0)[source]
Hexagonal 2-D binning of points
x/y(colormapped counts).Returns a mappable collection of hexagons (works with
fig.colorbar).norm/vmin/vmaxnormalize the counts exactly as they do forpcolormeshandimshow. Bin counts routinely span several decades – a density plot’s peak can hold a thousand times what its tails do – and a linear ramp then paints everything but the peak the same colour, sonorm=LogNorm()is often the difference between a readable density map and two blobs.edgecolors/linewidthsoutline each hexagon – the standard look for a sparse/low-count hexbin, where separating adjacent cells matters more than for a dense one.edgecolors=None(default) draws no outline, matching the previous behavior.linewidthsis one width for every hexagon – unlike matplotlib’s ownhexbin, there is no per-hexagon outline width here (the collection this builds on has one shared edge width, the same asscatter’s ownlinewidths=).
- hist2d(x, y, bins=20, range=None, cmap='viridis', norm=None, vmin=None, vmax=None, alpha=1.0, zorder=0)[source]
2-D histogram rendered as an image. Returns
(counts, image).Takes the same
norm/vmin/vmaxashexbin(), and for the same reason: counts are rarely uniform enough for a linear ramp.
- psd(x, NFFT=256, Fs=2, noverlap=0, detrend=True, window=None, color=None, linewidth=None, label=None, alpha=1.0, zorder=0)[source]
Power spectral density (Welch). Returns
(Pxx, freqs, line).
- csd(x, y, NFFT=256, Fs=2, noverlap=0, detrend=True, window=None, color=None, linewidth=None, label=None, alpha=1.0, zorder=0)[source]
Cross spectral density magnitude. Returns
(Pxy, freqs, line).
- cohere(x, y, NFFT=256, Fs=2, noverlap=0, detrend=True, window=None, color=None, linewidth=None, label=None, alpha=1.0, zorder=0)[source]
Magnitude-squared coherence. Returns
(Cxy, freqs, line).
- magnitude_spectrum(x, Fs=2, detrend=True, window=None, scale=None, color=None, linewidth=None, label=None, alpha=1.0, zorder=0)[source]
Magnitude spectrum
|X(f)|.scale='dB'plots decibels.Returns
(spectrum, freqs, line).
- angle_spectrum(x, Fs=2, detrend=True, window=None, color=None, linewidth=None, label=None, alpha=1.0, zorder=0)[source]
Wrapped phase spectrum (radians). Returns
(angles, freqs, line).
- phase_spectrum(x, Fs=2, detrend=True, window=None, color=None, linewidth=None, label=None, alpha=1.0, zorder=0)[source]
Unwrapped phase spectrum (radians). Returns
(phase, freqs, line).
- specgram(x, NFFT=256, Fs=2, noverlap=128, detrend=True, window=None, cmap='viridis', norm=None, vmin=None, vmax=None, alpha=1.0, zorder=0)[source]
Spectrogram (power in dB). Returns
(spectrum, freqs, t, image).
- xcorr(x, y, normed=True, detrend=False, maxlags=10, usevlines=True, color=None, marker='o', markersize=None, linewidth=None, label=None, alpha=1.0, zorder=0)[source]
Cross-correlation of
xandyover+-maxlags.Returns
(lags, c, lines, markers)wherelinesis the stem collection (usevlines) or connecting line, andmarkersis the dot at each lag.alpha/zorderapply to both.
- set_aspect(aspect)[source]
Set the axes aspect.
'equal'= 1 data-unit is equal in x and y;'auto'fills the box (default); a number sets the y/x unit ratio. Implemented box-adjust: the drawn box shrinks to honor the ratio.
- set_box_aspect(aspect)[source]
Fix the axes’ own physical height/width ratio, independent of the data range – unlike
set_aspect(), which shrinks the box to keep one data unit the same size in x and y, this never looks at the data at all.None(the default) leaves the box filling its allocated space, as normal. The box shrinks and centers within its allocated space to hit the ratio, the same “box-adjust” strategyset_aspect()uses.
- text(x, y, s, color=None, fontsize=None, ha='left', va='baseline', rotation=0.0, outline=None, alpha=1.0, bbox=None, zorder=0, fontweight='normal', fontstyle='normal', transform=None)[source]
Draw text
sat data coordinates(x, y).outlineis a halo color drawn behind the glyphs so the label stays readable over whatever it lands on. The default picks white or black by the text’s own luminance; passFalseto switch it off, or a color to choose your own. It only ever helps – on a plain background the halo is the background color and invisible – and a label in the data area is placed before anyone knows what will end up underneath it.alphafades the glyphs themselves, independent ofbbox’s ownalpha(the box’s fill can be more or less transparent than the text drawn over it).bboxdraws a filled/bordered box behind the text instead of (or as well as) theoutlinehalo – matplotlib’sbbox=dict, a subset of its keys:facecolor/fc(default white),edgecolor/ec(default none),alpha(default1.0),pad(pixels around the text, default4.0),boxstyle("square"or"round"), andlinewidth. Pass{}for the defaults.fontweight("normal"/"bold", or any matplotlib weight name/ number –>= 600counts as bold) andfontstyle("normal"/"italic"/"oblique") select the glyph face; both also feed the width measurementbboxsizes against and the leader inannotate()anchors to, so a bold or italic label still gets a tight box/leader rather than one sized for the regular face.smay contain\nfor a multi-line label – each line is independently aligned perha(matplotlib’s defaultmultialignment), and the block as a whole is placed perva("top"anchors the block’s top edge,"bottom"its bottom edge,"center"its middle,"baseline"the first line’s baseline).transform=ax.transAxesplaces(x, y)as an axes-fraction position instead of data coordinates –(0, 0)is the axes’ bottom-left corner,(1, 1)its top-right, regardless of the current xlim/ylim – e.g. a corner label or watermark that should stay put under autoscaling, panning, or a data zoom:ax.text(0.95, 0.95, "top right", transform=ax.transAxes, ha="right", va="top")
- annotate(text, xy, xytext=None, color=None, fontsize=None, ha='left', va='baseline', arrowprops=None, outline=None, alpha=1.0, bbox=None, zorder=0, fontweight='normal', fontstyle='normal', textcoords=None)[source]
Annotate the point
xywithtextplaced atxytext.Pass
arrowprops={"color": ...}(or{}) to draw an arrow from the text toxy.arrowpropsalso acceptsalpha, applied to the arrow only – independent of the text’s ownalpha. The leader starts at the edge of the text’s bounding box nearestxy– preferring the middle of an edge – so it never sets off across its own label; withbboxset, that edge is the box’s own edge, not the bare text’s, so the leader visibly touches the box instead of stopping short of it.outline/alpha/bbox/fontweight/fontstyle/ multi-linetextall matchtext().textcoords=ax.transAxesplacesxytextas an axes-fraction position – the label sits at a fixed spot on the axes frame while its arrow still points at the data coordinatexy, e.g. a callout pinned to a corner regardless of where the data it labels ends up after a pan or zoom.xyitself always stays data coordinates.
- table(cellText, rowLabels=None, colLabels=None, cellColours=None, rowColours=None, colColours=None, loc='center', bbox=None, fontsize=None, alpha=1.0, zorder=6)[source]
A grid of text cells drawn on top of this axes.
cellTextis a list of rows, each a list of cell strings.rowLabels/colLabelsadd a labeled header column/row.cellColours/rowColours/colColoursare matching row-major grids (or single lists for the header row/column) of fill colors, all optional.Positioned in axes-fraction space (
loc, a matplotlib corner/edge name – see_TABLE_LOC_ANCHORfor the full set – or an explicitbbox=(x0, y0, w, h)), the same astext()’s owntransform=ax.transAxes: it describes a spot on the axes frame, not the data, and stays there under a later pan/zoom. Column widths divide the box evenly; there is no per-columncolWidths=sizing by content yet.
- set_axis_on()[source]
Undo
set_axis_off().
- axis(*args, **kwargs)[source]
matplotlib’s overloaded
axis()convenience.axis('off')/axis('on')toggle the whole axis decoration;axis('equal')sets a 1:1 aspect ratio;axis([xmin, xmax, ymin, ymax])sets both limits at once; with no arguments, returns the current(xmin, xmax, ymin, ymax). Always returns that 4-tuple.
- set(**kwargs)[source]
Bulk-set several properties in one call, e.g.:
ax.set(xlim=(0, 10), ylabel="y", title="demo")
Each keyword
foo=valuedispatches to this axes’ ownset_foo(value)– matplotlib’sAxes.set()works the same way, generated from everyset_*method it has. Only covers setters that take a single value (the vast majority:xlim,ylim,xlabel,title,xscale,xticks,aspect,box_aspect,facecolor,xmargin,visible, … – not the handful of no-argument toggles likeset_axis_off()). Raises on any keyword with no matchingset_*method, naming all of them at once rather than stopping at the first.
- set_pickable(pickable=True)[source]
Include or exclude this axes from Point Picking.
Falsemakes this axes behave, for that tool only, as if a click there landed outside every axes – so restricting picking to one panel of a figure isset_pickable(False)on the others. Annotate Point resolves to a datum the same way Point Picking does, so it respects this too; Axis Span, Axis Zoom, Pan/Zoom, Annotate, and Annotate Arrow are unaffected. Every axes is pickable by default.
- set_pick_context(**kwargs)[source]
Attach extra key/value context to this axes’ point-picking output.
Every marker/annotation record extracted from this axes – CSV/JSON via the toolbar’s Extract panel, or
window.plotpressGetMarkers()– carries these keys alongside its own fields, e.g.:ax.set_pick_context(edge_color=ax.spines["top"].get_color())
so a click on that panel reports which one it came from by more than a bare index or title. A context key that collides with a structured field the record already sets (
x,y,kind, …) is ignored for that record – the picked data always wins. Calling this again adds to, rather than replaces, the existing context.
- remove()[source]
Detach this axes from its figure.
Also drops it from any
sharex/shareygroup it belonged to (those lists are shared by reference with every sibling, so removing from them in place – not reassigning – detaches from all of them at once), releases its own id (seeset_id()) so another axes may reuse it, and drops it from anygroup()it belonged to – both that group’s flat axes list and, for aGroupLayout-built one, its own inner(row, col)grid – so neither keeps a stale reference to a detached axes. A group loses its live, axes-derived box the moment any one of its axes leaves this way – not just once the last one does: its bounding rect at that instant (figure-fraction, so it survives a laterset_size_inches/dpi change) is frozen and stands in for the usual axes-derived bounds from then on, whether the group still has other members or none at all – seesvg._group_bbox. Freezing on the first departure rather than the last is what keeps the box wrapping the group’s original structure (e.g. a whole row it was drawn around) instead of shrinking down, removal by removal, to wherever whichever axes happened to leave last, and finally disappearing to just that one’s own rect. One axes shared between two groups (an unusual but not prevented case) comes out of both, freezing either that hadn’t already been. Colorbar/legend space this axes’ neighbors ceded to it is not automatically reclaimed; calltight_layout()again for that. Neither this frozen box nor the whitespace this axes’ own removed grid cell leaves behind shrinks on its own – callfig.tight_layout(collapse="grid")to reclaim both: it drops any group left with zero members entirely and shrinks any row/column of the grid that’s now completely empty. A group still holding some members keeps its frozen box even after that call – collapse= “grid” only ever drops a fully-emptied group, it doesn’t re-tighten a partially-emptied one’s box around whatever is left.Also removes (recursively, via this same method, so each gets its own full cleanup) any
twinx()/twiny()/secondary_xaxis()/secondary_yaxis()overlay built from this axes. Neither kind draws anything meaningful on its own – a twin shares this axes’ own grid cell and one of its two data dimensions; a secondary draws no data at all, only a mirrored copy of this axes’ own limits – so leaving one behind after removing the axes it depends on would strand it: still infigure.axes, still rendered, with its own_twin_of/_secondary_ofnow pointing at an axes no longer part of the figure, and (a twin especially) no spine/box of its own to visually anchor it – just a lone data trace and a stray column of tick labels floating with nothing around them.Also drops this axes from the
_cbar_parentsof any colorbar (colorbar()) built against it –_layout_colorbarre-derives the bar’s position from its parents’ current rects every time, so a stale parent would otherwise keep contributing a frozen, no-longer-updating rect to that math indefinitely. A colorbar can span several axes at once, so removing one parent only shrinks the list; removing the last one removes the colorbar too (recursively, via this same method) rather than leaving it floating with no plot left to explain it.
- cla()[source]
Reset this axes to a freshly-created state, keeping its position.
Detaches from any
sharex/shareygroup first, using the same in-place-removal trick asremove()(those lists are shared by reference with every sibling), since the constructor about to run would otherwise just drop the reference and leave the group missing its own member – a cleared axes contributing no data is autoscale- neutral, but it would still receive a shared explicit limit from a sibling’sset_xlim/set_ylim. Also releases this axes’ own id (seeset_id()) first – the constructor resetsself._idtoNonesame as every other attribute, which without this would leave the figure’s own id index still pointing at this (now blank) axes, incorrectly blocking another axes from claiming the id this one just gave up.Re-runs the constructor (so a subclass like
PolarAxesresets its own extra state too) without duplicating the attribute list here, then restores the figure position and grid membership that the constructor doesn’t know about.
- clear()
Reset this axes to a freshly-created state, keeping its position.
Detaches from any
sharex/shareygroup first, using the same in-place-removal trick asremove()(those lists are shared by reference with every sibling), since the constructor about to run would otherwise just drop the reference and leave the group missing its own member – a cleared axes contributing no data is autoscale- neutral, but it would still receive a shared explicit limit from a sibling’sset_xlim/set_ylim. Also releases this axes’ own id (seeset_id()) first – the constructor resetsself._idtoNonesame as every other attribute, which without this would leave the figure’s own id index still pointing at this (now blank) axes, incorrectly blocking another axes from claiming the id this one just gave up.Re-runs the constructor (so a subclass like
PolarAxesresets its own extra state too) without duplicating the attribute list here, then restores the figure position and grid membership that the constructor doesn’t know about.
- axvline(x, color=None, linewidth=None, linestyle='--', label=None, alpha=1.0, zorder=0)[source]
Draw a vertical line at data coordinate
x(like matplotlib).xmay be datetime-like or a string, the same asplot()’s ownx/y– including on a categorical axis, where a string not already among this axis’ categories is added as a new one (matchingplot()’s own first-seen-wins rule) rather than raising. That differs fromset_xlim(), which treats an unknown category string as an error instead: a limit is a boundary, not data, so it never silently introduces a new category the way a real, visible mark like this one does.
- axline(xy1, xy2=None, slope=None, color=None, linewidth=None, linestyle='-', label=None, alpha=1.0, zorder=0)[source]
Draw an infinite line through
xy1(viaslopeor a second point).Spans the whole axes and does not affect autoscaling, like matplotlib.
- broken_barh(xranges, yrange, color=None, alpha=1.0, label=None, zorder=0)[source]
Draw a row of rectangles from
(xstart, xwidth)spans atyrange.yrangeis(ystart, yheight). Handy for Gantt / timeline charts.Each span’s
xstartmay be datetime-like or a string, the same asplot()’s ownx– a task’s start date, say.xwidthstays a plain number in either case: it’s a duration, not a position, and on a date axis that duration is in days, the unitplot()converts every date to. See Datetime Gantt chart with milestones for a worked (real-dates Gantt chart) example.
- stairs(values, edges=None, color=None, linewidth=None, linestyle='-', label=None, alpha=1.0, zorder=0)[source]
Step outline from bin
edges(lenvalues+ 1), like matplotlib.
- axhline(y, color=None, linewidth=None, linestyle='--', label=None, alpha=1.0, zorder=0)[source]
Draw a horizontal line at data coordinate
y(like matplotlib).ymay be datetime-like or a string, the same asplot()’s ownx/y– seeaxvline()’s own docstring for what that means on a categorical axis (a new string is added as a category, unlikeset_ylim(), which raises instead).
- axvspan(xmin, xmax, color='#1f77b4', alpha=0.3, label=None, zorder=0)[source]
Shade a vertical band between x=``xmin`` and x=``xmax``.
- axhspan(ymin, ymax, color='#1f77b4', alpha=0.3, label=None, zorder=0)[source]
Shade a horizontal band between y=``ymin`` and y=``ymax``.
- set_xlim(left=None, right=None)[source]
Set the x limits. Returns the stored
(left, right).Accepts
set_xlim(lo, hi),set_xlim((lo, hi)), orNoneon either side to autoscale just that end –set_xlim(0, None)pins the left edge and lets the data decide the right. BothNoneclears back to full autoscaling.A bound may also be datetime-like or a string – resolved through this axis’ own existing date/category mapping the same way
plot()’sx/yare, soset_xlim("2024-01-01", "2024-06-01")orset_xlim("Q1", "Q3")work once the axis is already date/categorical (a string limit on an axis that hasn’t seen any categories yet has nothing to resolve against and raises). See Datetime Gantt chart with milestones for a worked example.
- set_ylim(bottom=None, top=None)[source]
Set the y limits; same forms as
set_xlim().
- tick_params(axis='both', which='major', labelsize=None, length=None, width=None, color=None, labelcolor=None, labelrotation=None)[source]
Style this axes’ tick marks and labels (a subset of matplotlib’s).
labelsize(tick-label font),length/width(tick marks),color(mark color),labelcolor(label color).axisselects"x","y", or"both"(default) – each axis keeps its own override, sotick_params(axis='x', color='red')recolors only the x ticks.whichselects"major","minor", or"both"; minor ticks have no labels, solabelsize/labelcolor/labelrotationonly ever affect major ticks.labelrotationangles the tick labels (degrees, counterclockwise – matchingtext()’s ownrotation), the standard fix for long labels crowding into their neighbors:ax.tick_params(axis='x', labelrotation=45)turns a bottom row of long category names diagonal instead of letting them overlap. A nonzero rotation also right-anchors the label to its tick (matplotlib needs a separateha='right'for this; this is the only sensible look for a diagonal tick label, so it’s automatic here rather than a second knob nobody would set differently).tight_layout()reserves the rotated label’s actual measured extent, not just one text line, so the row below never overlaps it.
- set_xbound(lower, upper)[source]
Set the x data limits (alias of
set_xlim()).
- set_ybound(lower, upper)[source]
Set the y data limits (alias of
set_ylim()).
- get_xbound()[source]
The resolved x limits, always
(low, high)regardless ofinvert_xaxis()– unlikeget_xlim(), which reports them in whatever direction they’re actually drawn.
- get_ybound()[source]
The resolved y limits, always
(low, high); seeget_xbound().
- margins(m=None, x=None, y=None)[source]
Set fractional padding around the autoscaled data (like matplotlib).
margins(0.1)pads both axes 10%; per-axis viax=/y=. This is a persistent setting – unlike a one-shotset_xlimnudge, it keeps re-applying as the resolved data limits change (e.g. after more data is plotted), because it’s consumed inside_pad()on every autoscale resolve rather than baked into_xlim/_ylimhere.
- autoscale(enable=True, axis='both', tight=None)[source]
Re-enable (or freeze) autoscaling on
axis('x'/'y'/'both').enable=Falsefreezes the axis at its current resolved limits.tight=Truealso zeroes that axis’ margin.
- set_autoscalex_on(b)[source]
Enable/disable x autoscaling (shorthand for
autoscale()withaxis='x').
- set_autoscaley_on(b)[source]
Enable/disable y autoscaling; see
set_autoscalex_on().
- set_xticks(ticks, labels=None, minor=False)[source]
Set explicit x tick locations. Pass
[]to hide ticks.ticksmay also be datetime-like or a list of strings – resolved through the same coercionplot()’sx/yuse (see its docstring), soset_xticks(["Q1", "Q2", "Q3"])both declares those as this axis’ categories and pins the tick positions in one call, even before any data has been plotted. See More categorical axes and declarative tick formats for a worked example.labelsoptionally sets the tick label strings in the same call (matplotlib’s combinedset_xticks(ticks, labels)form) – ignored whenminor=True, since minor ticks never carry labels here.minor=Truesets minor tick positions instead of major ones, and (matching matplotlib) implicitly turns minor ticks on – the same flagminorticks_on()sets – so they actually get drawn rather than silently sitting unused.
- set_yticks(ticks, labels=None, minor=False)[source]
Set explicit y tick locations. Pass
[]to hide ticks.labels/minormatchset_xticks();ticksaccepts the same datetime-like/string forms too.
- set_xticklabels(labels)[source]
Set explicit x tick label strings (pair with
set_xticks()).
- set_yticklabels(labels)[source]
Set explicit y tick label strings (pair with
set_yticks()).
- set_xlocator(spec)[source]
Set a declarative x tick-location rule, e.g.
ax.set_xlocator({"kind": "multiple", "base": np.pi / 2})to force a tick at every multiple of pi/2 regardless of what the default “nice number” scheme would pick.specis plain, JSON-serializable data – a dict naming a scheme (currently just"multiple", matplotlib’sMultipleLocator) – rather than a locator object, so the exact same rule can be replayed by the interactive HTML’s client-side zoom/pan rebuild, which can’t execute Python. PassNoneto go back to the default scheme.Ranks below an explicit literal
set_xticks()array and this axis’ own categories (if any), and above date/log/default ticking – seeresolve_axis_ticks()for the full chain. See Datetime axes, categorical axes, and declarative tick specs for a worked example.
- set_ylocator(spec)[source]
Set a declarative y tick-location rule; see
set_xlocator().
- set_xformat(spec)[source]
Set a declarative x tick-label rule:
"percent","comma","eng","pi"(each also a dict with options, e.g.{"kind": "percent", "decimals": 1}), a raw%-style format string ("$%.0f"), or a callablevalue -> str.Everything except a callable is plain, JSON-serializable data, so it replays identically in the interactive HTML’s client-side zoom/pan rebuild; a callable only ever renders in the static SVG/PNG/PDF – it can’t cross into JS, so a zoomed interactive figure falls back to default formatting for that axis instead. Pass
Noneto go back to the default. Seeapply_tick_format()for the full spec grammar, andresolve_axis_tick_labels()for where this ranks against explicit literal labels, categories, and date formatting. See Datetime axes, categorical axes, and declarative tick specs and More categorical axes and declarative tick formats for worked examples (multiple-of-pi, comma, percent, and a raw %-string format).
- set_yformat(spec)[source]
Set a declarative y tick-label rule; see
set_xformat().
- get_xticklabels()[source]
The x tick label strings that will actually be drawn: explicit ones if set, else this axis’ own categories, else formatted date/declarative/default text – plain strings rather than matplotlib’s
Textobjects, matching every other read-only accessor in this class.
- get_yticklabels()[source]
The y tick label strings that will actually be drawn; see
get_xticklabels().
- invert_xaxis()[source]
Reverse the x-axis direction (larger values to the left).
Applies to every axes sharing this x-axis. Direction is part of a shared axis just as its limits are, and inverting one panel of a
sharexcolumn while its neighbours keep counting the other way produces a grid that lines up numerically and reads backwards – with no tick labels on the inner panels to give it away.
- invert_yaxis()[source]
Reverse the y-axis direction (larger values at the bottom).
Applies to every axes sharing this y-axis; see
invert_xaxis().
Link this axes’ x-limits/autoscale to
other’s, after the fact.Unlike
plotpress.subplots(sharex=True)(set up at grid-creation time), this merges two already-existing axes’ share groups.
Link this axes’ y-limits/autoscale to
other’s, after the fact.
- label_outer()[source]
Hide tick labels except on the bottom row / left column of its grid.
No-op for an axes that isn’t part of an
add_subplot/subplotsgrid (_subplotspec is None).
- secondary_xaxis(location='top', label=None)[source]
Return an axis mirroring this axes’ x-limits (same units).
Unlike
twiny(), a secondary axis draws no data of its own – it just tracks this axes’ x-limits wherever they end up, drawn alonglocation('top'or'bottom'). Custom unit-conversion (matplotlib’sfunctions=) is not supported; usetwiny()if the second axis needs independent data.
- secondary_yaxis(location='right', label=None)[source]
Return an axis mirroring this axes’ y-limits (same units).
See
secondary_xaxis();locationis'left'or'right'.
- inset_axes(bounds, projection=None)[source]
Add a small axes inset within this one.
bounds = (x0, y0, w, h)are fractions of this axes’ box, not the figure’s –[0.6, 0.6, 0.35, 0.35]puts a inset in the upper-right corner. Tracks this axes through latertight_layout/subplots_adjustcalls (it is not itself a grid member).
- indicate_inset(bounds, inset_ax=None, edgecolor='black', alpha=0.5, linewidth=1.0, zorder=4.5)[source]
Draw a rectangle on this axes marking the data region
bounds = (x0, y0, width, height)– typically the region aninset_axes()zooms into.Unlike matplotlib, this does not also draw connector lines from the rectangle’s corners to
inset_ax’s own corners – those cross from one axes’ own clipped drawing area into another’s, a figure-level connection this library has no artist for yet.inset_axis accepted (and ignored) only so matplotlib’s own call signature still works; the marker rectangle itself is drawn either way.
- indicate_inset_zoom(inset_ax, edgecolor='black', alpha=0.5, linewidth=1.0, zorder=4.5)[source]
indicate_inset(), withboundstaken frominset_ax’s own current x/y limits – the common case of “this inset already shows a zoomed-in view of my data, mark which region that is”.
- set_position(pos)[source]
Move this axes to an explicit
(left, bottom, width, height)(figure fractions), opting it out of grid auto-layout: a latertight_layout/subplots_adjustwill no longer reposition it, matching matplotlib.
- get_position()[source]
This axes’
(left, bottom, width, height)in figure fractions.Returns the nominal rect, not the
set_aspect-adjusted box used at render time (matching matplotlib’s ownget_position()/apply_aspect()split).
- set_xlabel(xlabel, visible=True, size=None, fontsize=None)[source]
Set the x-axis label.
visible=Falsestores it without drawing it –get_xlabel(), theload_data()layout round-trip, and a picked point’s Extract record still report it, but it isn’t rendered and reserves no margin (see alsoset_xlabel_visible()). Passing text again with the defaultvisible=Truere-shows it.sizeoverrides the style’slabel_sizefor this axes’ x label only, the same per-axes escape hatchset_title()already has – a small-multiples grid can want each panel’s own label a few points high without a wholeStylecopy per figure changing every other label too.fontsizeis accepted as matplotlib spells it.
- set_ylabel(ylabel, visible=True, size=None, fontsize=None)[source]
Set the y-axis label.
visible=Falsestores it without drawing it – seeset_xlabel()andset_ylabel_visible().size/fontsizeoverride the style’slabel_sizefor this axes’ y label only, same asset_xlabel()’s own.
- set_xlabel_visible(visible=True)[source]
Show or hide the x-axis label without changing its text. A hidden label is still stored –
get_xlabel(), the layout round-trip, and Extract records keep reporting it – it just isn’t drawn and reserves no margin.
- set_ylabel_visible(visible=True)[source]
Show or hide the y-axis label without changing its text – see
set_xlabel_visible().
- get_xlabel_visible()[source]
Whether the x-axis label is drawn (default
True).Falseafterset_xlabel(..., visible=False)orset_xlabel_visible(False).
- set_title(label, size=None, fontsize=None)[source]
Set this axes’ title.
sizeoverrides the style’s title size.Worth having per-axes rather than only on the style: a small-multiples grid of several hundred panels needs a title a few points high, and the alternative – a whole
Stylecopy per figure – changes every other title too.fontsizeis accepted as matplotlib spells it.
- grid(visible=True, axis='both', which='major', alpha=None)[source]
Show or hide the gridlines at the major (and/or minor) tick positions.
axisrestricts the gridlines to"x"or"y"only (default"both") – the standard clean look for a bar/category chart, where gridlines along the category axis carry no information.whichdraws lines at"major"tick positions (default),"minor"(seeminorticks_on()), or"both".alphaoverrides this axes’ gridline opacity;None(the default) falls back to the figure style’s owngrid_alpha, the same “override vs. style default” conventionSpineand the per-axes tick overrides already use.
- legend(loc='upper right', ncol=1, title=None, handles=None, labels=None, fontsize=None, framealpha=0.85, bbox_to_anchor=None)[source]
Enable a legend (by default, drawn from artists that have a
label).locis a matplotlib-style corner/edge name (e.g."upper left","lower center","center";"best"maps to upper right).ncollays the entries out in that many columns;titleadds a heading row.fontsizeoverrides the entry/title text size (default: the style’s own tick label size).framealphais the legend box’s own background opacity (matplotlib’s default is0.8;0.85matches what this box already drew before the value was configurable).bbox_to_anchor=(x, y), in this axes’ own fraction coordinates ((0, 0)bottom-left,(1, 1)top-right – matplotlib’s own default transform for it), places theloccorner of the legend box at that exact point instead of inset within the axes box – the common way to put a legend outside the plot entirely, e.g.loc="upper left", bbox_to_anchor=(1.02, 1)for just past the right edge. Unlikelocalone, this can and often does place the box outside the axes’ own drawn area.handlesoverrides which artists appear – any plotpress artist (from this axes, another, or never added to one at all), in the order given, regardless of their ownlabel. Pair withlabelsto also override the text shown for each, positionally; without it, each handle’s ownlabelis used.Returns a
Legendhandle – also available later viaget_legend()– for repositioning/restyling or hiding the legend after the fact without a fulllegend(...)call.
- get_legend()[source]
The current
Legend, orNoneiflegend()was never called (or was hidden viaLegend.remove()/set_visible(False)).
- get_legend_handles_labels()[source]
(handles, labels)for whateverlegend()would currently draw –_legend_handles(fromlegend(handles=...)) if set, else every artist on this axes carrying alabel, in call order. Mirrorsplotpress.svg._legend_layout()’s own source-selection exactly, so this always answers “what would the legend show right now”, not a separate approximation of it.
- set_id(id)[source]
Set a plain, undrawn identifier for this axes – for later retrieval via
get_ax()/get_ax(), distinct fromset_title()(which is drawn on the plot, and may legitimately repeat across several axes). Unlike a title,idmust be unique across this axes’ whole figure – raises if another axes already has it. PassNoneto clear it, always allowed regardless of what else in the figure already has an id. See Irregular group shapes (deleted axes) for a worked example, including the collision case.
- get_xticks()[source]
The resolved x tick locations: explicit if set, else one per category, else a locator/date/log/”nice number” scheme – see
resolve_axis_ticks()for the full chain.
- get_yticks()[source]
The resolved y tick locations; see
get_xticks().
- print_summary() None[source]
Print a plain-English orientation to this one axes – where it sits (a grid cell, a span, a twin/secondary/inset/colorbar), its scales/limits/labels, what’s plotted on it, and whether it would export cleanly to
to_vega()/to_vega_lite(). The per-axes half ofprint_layout_summary(); nothing is returned, matching that method’s own “ask it, don’t parse it” intent.Named
print_*(not e.g.summary) so it tab-completes alongside every other summary method this library adds – seeprint_layout_summary()for the whole-figure one.
Ticks & dates
Backs set_xlocator()/set_ylocator/
set_xformat/set_yformat’s spec grammar, and the datetime conversion
plot() (and every other plotting method) applies
automatically to datetime-like x/y data – documented here for
anyone building a spec by hand, or converting a date to/from the plain
float days-since-epoch every plotpress axis works in internally.
Datetime axis support: converting date/time values to and from the plain float days-since-epoch every other axis already works in, plus date-aware tick locating and formatting.
The epoch is 1970-01-01 (Unix epoch), the same one modern matplotlib uses
(matplotlib.dates switched to it in 3.3) – not because the exact epoch
matters for anything plotpress does internally (any fixed origin round-trips
identically), but because it means a value already converted for matplotlib
(matplotlib.dates.date2num(...)) is also a valid plotpress date value,
and vice versa.
A date axis is otherwise a perfectly ordinary linear axis: the data is
converted to float days once, at plot time (see Axes._as_axis_data), and
everything downstream (autoscale, the transform, panning/zooming) works on
that float exactly like it would for any other linear quantity. Only tick
locations (date_ticks()) and tick labels (format_date_ticks())
need to know the axis is date-flavored.
See Datetime axes, categorical axes, and declarative tick specs and Datetime Gantt chart with milestones for worked examples.
- plotpress.dates.is_datetime_like(value) bool[source]
True for anything
to_days()can convert: anumpy.datetime64scalar/array, adatetime.date/datetime.datetime, or a sequence of those (what a plain Python list of dates, or apandasSeries/DatetimeIndexconverted throughnumpy.asarray, already is).
- plotpress.dates.to_days(value) ndarray[source]
Convert date/time data to a plain
float64array of days since the 1970-01-01 epoch – the representation every other plotpress axis already uses, so nothing downstream needs to know the data was ever a date.A missing timestamp (
numpy.datetime64("NaT"), the datetime analogue ofNaN) maps toNaN, not a huge-but-finite float –NaT’s ownint64encoding is the minimum representableint64, which without this would silently become “roughly 300,000 years before 1970” instead of being excluded the way a real missing value should be.
- plotpress.dates.days_to_datetime64(days) ndarray[source]
Inverse of
to_days(): float days since epoch ->datetime64[us].
- plotpress.dates.date_ticks(vmin: float, vmax: float, n: int = 5) ndarray[source]
Tick locations (float days since epoch) for a date axis, chosen at a sensible calendar tier – years, months, days, hours, minutes or seconds – for the given span, and landing on round boundaries within that tier (the first of the month, midnight, the top of the hour) rather than at arbitrary offsets from wherever the view happens to start.
- plotpress.dates.format_date_ticks(values) List[str][source]
Format a set of date tick values (float days since epoch), all at one calendar tier chosen from the set’s own span – so a year’s worth of monthly ticks reads
2024-01,2024-02, … while a day’s worth of hourly ticks reads14:00,15:00, … instead of every label repeating a full, unchanging date.
Tick location and label formatting (“nice numbers” 1-2-5 algorithm),
plus the declarative locator/formatter specs
set_xlocator()/set_xformat accept.
A locator spec is a small dict naming a scheme ({"kind": "multiple",
"base": ...}) rather than a matplotlib-style Locator object – it has
to be plain, JSON-serializable data so the exact same tick-placement rule
can be replayed client-side when an interactive figure is zoomed/panned (see
_interactive.py’s own mirror of every function here). A formatter
spec is the same idea for labels: a name, a %-style string, or (Python-
only, see apply_tick_format()) a callable.
See Datetime axes, categorical axes, and declarative tick specs and More categorical axes and declarative tick formats for worked examples.
- plotpress.ticker.pow10(exp: int) float[source]
10**expas the correctly rounded double, for an integerexp.Neither Python’s
**nor JavaScript’sMath.powis required to give the correctly rounded power of ten, and both really do miss:10.0**23and10.0**126differ from the1e23/1e126literals here, and a CI runner’s V8 returned a different double for10**-5than the author’s machine did – which shifted a whole axis’ first tick, becauseceil(vmin / step)landed on a different integer.Parsing the decimal literal is correctly rounded by both languages’ specs, so it is the one definition that agrees everywhere. Tick decades go through this on both sides (see
_interactive.py’s ownpow10) so the static render and the interactive rebuild cannot drift apart.
- plotpress.ticker.log_floor(vmax: float) float[source]
A sensible positive floor for a log-scale bound that reached zero or negative – three decades below
vmax(or1e-3ifvmaxitself isn’t positive either), never exactly zero.Shared by
log_ticks()/minor_ticks()here and byLogNorm, which used to independently floor at a fixed1e-300instead – for aLogNorm(vmin=0, vmax=...)colorbar, that meant the tick positions (via this function) and the actual color mapping (via the old fixed floor) disagreed on where the scale’s usable bottom sits, visually compressing almost the entire real data range into one end of the colormap.
- plotpress.ticker.nice_ticks(vmin: float, vmax: float, n: int = 5) ndarray[source]
Return ~``n`` evenly spaced “nice” tick locations within [vmin, vmax].
Order-independent:
set_xlim(hi, lo)reverses the axis (like matplotlib), so the caller may passvmin > vmax. The tick locations are the same either way – the reversal is handled by the transform – so normalize here.
- plotpress.ticker.log_ticks(vmin: float, vmax: float) ndarray[source]
Tick locations for a log axis, all lying within [vmin, vmax].
Decades where the range spans them. The containment matters: a tick outside the limits transforms to a pixel outside the axes box, and nothing clips tick labels – so an out-of-range decade is drawn into whatever sits next to the axes, typically the neighboring subplot. Autoscale margins alone are enough to trigger it: data from 0.01 upward gets a limit just under 0.01, which used to pull in a 0.001 tick a whole panel away.
Ranges narrower than a decade have no decade inside them, so they fall back to 1-2-5 subdivisions and then to plain
nice_ticks()– an axis with no labels at all is worse than one whose labels are not powers of ten.Order-independent (see
nice_ticks()): a reversed log limit still gets its ticks rather than silently rendering none.
- plotpress.ticker.minor_ticks(major: ndarray, vmin: float, vmax: float, scale: str = 'linear') ndarray[source]
Unlabeled minor tick locations between/around
major, within [vmin, vmax].Linear: subdivides the major step by a count keyed off its leading digit (1->5, 2->4, 5->5), matching
nice_ticks()’s 1-2-5 convention, so minor ticks land on round subdivisions of whatever stepnice_tickschose. Log: the 2..9 sub-decade marks within each decade the range spans.
- plotpress.ticker.format_tick(v: float) str[source]
Format a tick value compactly (fixed or scientific as appropriate).
- plotpress.ticker.format_ticks(values) List[str][source]
Format a set of ticks so no two labels collide.
format_tickalone rounds each value to one mantissa digit, which turns a narrow band at high magnitude into six identical labels (ticks across [100000, 101000] all read “1e5”). For an evenly spaced set, pick a single shared exponent and carry enough mantissa digits to resolve the tick step, so the labels stay distinct and comparable.Unevenly spaced sets – log decades, mainly – keep the per-value form, where each label already carries its own exponent.
- plotpress.ticker.multiple_ticks(vmin: float, vmax: float, base: float, offset: float = 0.0) ndarray[source]
Tick locations at every multiple of
base(plusoffset) within[vmin, vmax]– matplotlib’sMultipleLocator. The standard way to force ticks at, say, everypi/2on a trig plot’s axis, regardless of whatnice_ticks()’s own 1-2-5 scheme would have chosen.
- plotpress.ticker.resolve_tick_locations(vmin: float, vmax: float, scale: str = 'linear', locator=None, is_date: bool = False) ndarray[source]
The location half of tick resolution, in the priority every renderer (SVG, raster, the interactive JS rebuild) applies identically: an explicit
locatorspec first, then a date axis, then a log scale, then the default “nice number” scheme. An explicit, literal tick array (Axes.set_xticks) is resolved by the caller before this is ever reached – it wins over all of these.
- plotpress.ticker.apply_locator(spec, vmin: float, vmax: float) ndarray[source]
Resolve a locator spec (see
set_xlocator()) into tick locations for the current[vmin, vmax].
- plotpress.ticker.resolve_tick_format(values, fmt=None, is_date: bool = False) List[str][source]
The label half of tick resolution, mirroring
resolve_tick_locations()’s priority: an explicitfmtspec first, then a date axis, then the default numeric formatting. Explicit, literal tick labels (Axes.set_xticklabels) are resolved by the caller before this is ever reached.
- plotpress.ticker.resolve_axis_ticks(vmin: float, vmax: float, explicit=None, categorical: bool = False, categories=None, scale: str = 'linear', locator=None, is_date: bool = False) ndarray[source]
Tick locations for one axis dimension, at the full priority every renderer (SVG, raster, the interactive JS rebuild) must apply identically: an explicit literal array (
Axes.set_xticks) first, then one tick per category on a categorical axis, then an explicitapply_locator()spec, then a date axis, then a log scale, then the default “nice number” scheme.
- plotpress.ticker.resolve_axis_tick_labels(values, explicit=None, categorical: bool = False, categories=None, fmt=None, is_date: bool = False) List[str][source]
Tick labels for one axis dimension, mirroring
resolve_axis_ticks()’s priority: explicit literal labels (Axes.set_xticklabels) first, then this axis’ own category names, then an explicitapply_tick_format()spec, then a date axis, then plain numeric formatting.
- plotpress.ticker.apply_tick_format(spec, values) List[str][source]
Apply a formatter spec (see
set_xformat()) to tickvalues.specis one of:a callable
value -> str(Python-only – seeset_xformat’s own docstring for why this can’t survive an interactive zoom);"percent"/"comma"/"eng"/"pi"(or the same as a dict with"kind"plus options, e.g.{"kind": "percent", "decimals": 1});any other string containing
"%", applied as a plain%-style format applied to each value ("%.2f","$%.0f").
Style & colors
- class plotpress.style.Style(facecolor: str = '#ffffff', dpi: float = 100.0, axes_facecolor: str = '#ffffff', spine_color: str = '#000000', spine_width: float = 0.8, font_family: str = 'Helvetica, Arial, sans-serif', font_size: float = 10.0, title_size: float = 12.0, label_size: float = 11.0, text_color: str = '#000000', measure_installed_fonts: bool = False, tick_size: float = 3.5, tick_width: float = 0.8, tick_label_size: float = 9.0, tick_label_rotation: float = 0.0, grid_color: str = '#b0b0b0', grid_width: float = 0.6, grid_alpha: float = 0.6, line_width: float = 1.5, marker_size: float = 6.0, color_cycle: List[str] = <factory>)[source]
Visual configuration for a single figure.
Create a variant without mutating the original via
copy().- text_width(text: str, size: float, bold: bool = False, italic: bool = False) float[source]
Predicted width of
textin pixels, per this style’s font settings.The measuring entry point layout should use: it carries
font_familyandmeasure_installed_fontswith it, so no caller has to remember to pass either.
- class plotpress.colors.Normalize(vmin=None, vmax=None)[source]
Linearly map data to [0, 1] using
vmin/vmax.Unset limits are inferred from the data on first use. That inference writes back to the instance, so artists take a private copy (see
resolve_norm()) rather than scaling the norm you handed them – one norm passed to two figures would otherwise pin the second to the first’s data range.- autoscale_none(A)[source]
Fill unset limits from the data, tolerating a fully masked field.
A frame where every cell is
nanis a real case, not a mistake – a detector exposure that failed quality control, a tile with no coverage, one panel of a stack that a shared norm still has to accept. NumPy’snanminwarns on an all-NaN slice and returns NaN, which then propagated into the transform; fall back to a unit range instead, since there is nothing to scale and every cell will be drawn transparent.