API reference

Top level

Figure([figsize, style, facecolor])

subplots([nrows, ncols, figsize, style, ...])

Convenience constructor mirroring matplotlib.pyplot.subplots.

subplots_from_groups(layout[, figsize, ...])

Build a figure from a GroupLayout -- see there for how to describe the outer/inner grid shapes.

GroupLayout(nrows, ncols)

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.

Group(raw)

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.

Report([title, description])

An ordered collection of figures combined into one self-contained HTML file.

load_data(path[, by_index])

Read back the plotted data embedded in a self-contained interactive HTML file written by Figure.to_html()/Figure.save() or Report.save().

load_data_xarray(path[, figure])

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 layout load_data() already returns) instead of load_data()'s title-keyed dict of dicts.

load_template(path)

Read back a Figure.save_template() file: plain JSON, no HTML parsing involved, unlike load_data().

figure_from_template(template[, figsize, ...])

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, and Style -- everything to_template()/ plotpress.svg.template_metadata() capture.

select_panel(ds[, title, row, col, multiple])

Pull one panel out of a load_data_xarray() grid, dropping row/col entirely instead of leaving them behind as length-1 dimensions -- ds.isel(row=r, col=c) already does exactly that for a scalar r/c, which is all this is: that call, plus resolving title to the one (row, col) position it names.

Style(facecolor, dpi, axes_facecolor, ...)

Visual configuration for a single figure.

Normalize([vmin, vmax])

Linearly map data to [0, 1] using vmin/vmax.

get_cmap(name)

Return a 256x3 uint8 LUT for name (or pass an LUT through).

available_colormaps()

Named colormaps, including the _r reversed variants.

Figure

class plotpress.figure.Figure(figsize=(6.4, 4.8), style: Style = None, facecolor=None)[source]
suptitle(text, size=None)[source]

Set a global title centered across the whole figure.

supxlabel(text, size=None)[source]

Set a global x label centered along the bottom of the figure.

supylabel(text, size=None)[source]

Set a global y label centered along the left of the figure.

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/bbox match Axes.text()bbox draws 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_grid are for GroupLayout to 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 real GroupLayout’s own can make get_group()’s row=/col= lookup ambiguous between the two.

id is a second, exact-match way to find this group again later via get_group(), alongside title – unlike an axes’ own id (see Axes.set_id()), a group’s id is not required to be unique; get_group() raises if more than one group shares it, the same as it would for an ambiguous title.

axes is 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 – plus pad pixels of clearance. pad is 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_position is one of "top"/"bottom"/"left"/ "right", placing title just outside that edge of the box.

supxlabel/supylabel are this group’s own shared axis labels – the group-scoped equivalent of Figure.supxlabel()/ supylabel(), for a cluster of panels that all share one x/y quantity so no individual axes needs its own set_xlabel/ set_ylabel. Unlike title, which title_position can place on any of the four sides, these always draw at a fixed edge – supxlabel centered along the bottom, supylabel centered along the left, rotated – the same fixed placement Figure.supxlabel/supylabel themselves 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 for pad), rather than shrinking any axes. supxlabel_size/supylabel_size override the default size (label_size-derived, matching Figure.supxlabel/supylabel’s own default) independently of fontsize (which only ever sizes title).

visible=False hides the box, title, and any supxlabel/supylabel without forgetting any of it – Figure.set_group_visible() flips it back later by the same title/id/Group lookup remove_group() uses. The same convention as Axes.set_visible(): a hidden group still reserves its own margin in tight_layout(), so toggling it doesn’t reflow anything else – unlike remove_group(), which really does delete it (axes included).

Returns self for chaining; several groups may be added to one figure.

get_groups() list[source]

This figure’s registered groups (see group()), each as a Group.

A snapshot for a group with no inherent grid shape (a direct group() call) – its Group.axes is a fresh list copy, so removing an axes afterward (see Axes.remove()) never changes a Group already handed back here. For a GroupLayout -built group, Group.axes is the shared underlying (row, col) grid, so a later removal does show up in one already held – re-call get_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 direct group() call, subplots_from_groups() (which calls group() 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 via GroupLayout have one; see Group), title, or id. 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(), so sharex/sharey links and any id stay consistent) and the group’s own box/title registration.

Pass the Group object itself (from get_groups()/ get_group()), or find it by title/id the same way get_group() does – exactly one of the three. Leaves a blank rectangle where the group was, the same as Axes.remove() leaves a gap rather than reflowing the rest of the grid to fill it – call fig.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 the Group object itself (from get_groups()/get_group()), or find it by title/ id the same way get_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 in tight_layout() (the same convention Axes.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 a GroupLayout-built group, this is that group’s position in the shared internal grid, not usually what you want; use Group.get_ax() for that instead), title (see Axes.set_title()), or id (see Axes.set_id()).

A twinx/twiny/secondary_xaxis/secondary_yaxis copies its parent’s own (row, col) verbatim (they overlay the same cell), so a row=/col= lookup that reaches one reaches both – give the twin its own id if it needs to be found unambiguously this way.

Raises if no axes matches, or – unless many=True – if more than one does (impossible for id, which is unique per figure by construction; titles may legitimately repeat, and a twin/secondary pair at one row=/col= counts as two). many=True returns 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-grid row=/col= case worked through.

group_spacing(wspace=None, hspace=None)[source]

Reserve extra pixels between subplots for group() boxes, without touching anything else tight_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 for subplots_adjust(), which would also throw away every margin tight_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_position pointing 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 guarantee tight_layout() already gives a title facing the true outer edge. Pass wspace/hspace here 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) onto figsize itself, 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 or set_size_inches() rather than compounding onto an already-grown figure.

Only takes effect through tight_layout(); has no effect after a subplots_adjust() call, which sets every margin manually.

set_size_inches(w, h=None)[source]

Resize the figure. Accepts (w, h) or two separate arguments.

get_size_inches()[source]
set_dpi(dpi)[source]
get_dpi()[source]
delaxes(ax)[source]

Remove ax from this figure (delegates to Axes.remove()).

clf()[source]

Clear the figure: drop every axes and figure-level decoration.

Keeps figsize/style – use a new Figure for those.

clear()

Clear the figure: drop every axes and figure-level decoration.

Keeps figsize/style – use a new Figure for those.

add_axes(rect, projection=None) Axes[source]

Add an axes at rect = (left, bottom, width, height) (fractions).

projection='polar' makes it a PolarAxes.

add_subplot(nrows=1, ncols=1, index=1, projection=None) Axes[source]

Add the index-th axes (1-based) of an nrows x ncols grid.

nrows may instead be a SubplotSpec from fig.add_gridspec(...)[...], for an axes spanning multiple rows/ columns – its initial rect covers only the span’s top-left cell; call tight_layout()/subplots_adjust() afterward to size it to the full span.

projection accepts the same values as add_axes() ('polar').

add_gridspec(nrows=1, ncols=1, **kwargs) GridSpec[source]

Return a GridSpec for slicing into row/column spans.

fig.add_subplot(fig.add_gridspec(2, 2)[0, :]) spans both columns of the top row. Any left/right/top/bottom/wspace/ hspace kwargs become this figure’s margins immediately – see GridSpec.

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/multiprocessing worker and back never preserves object identity, however it looks – ax.figure on what comes back is a copy of this figure too, not self, and that copy’s own ax.axes list still has the worker’s version of everything, not this figure’s. Passed straight to fig.axes.append(ax), it would render at the wrong position (or not enter the layout at all) and leave ax.figure pointing at that disconnected copy. adopt_axes fixes both: finds the axes already in self.axes whose SubplotSpec matches ax’s (same grid shape and cell span) and replaces it there – same list position, so tight_layout()/subplots_adjust() keep placing it exactly where that slot always was – and reparents ax.figure to self.

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, since colorbar() 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’s return ax, cax): pickling preserves the object graph within one call, so cax’s own reference to ax survives 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 a group() (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 a GroupLayout-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/sharey link 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=True to have it pick one of those fixes itself for the cases with a settable per-instance size – x tick labels (tick_params()’s labelsize), an axes title (set_title()’s size), an axes x label (set_xlabel()’s size), and a group title (group()’s fontsize) – 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.

collapse reclaims whitespace remove()/ remove_group() leave behind, since neither reflows the grid on its own – a removed axes’ row/column keeps its nrows/ncols exactly 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 (see remove()’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 any group() 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 (raises NotImplementedError). Unlike "grid", this needs a real packing algorithm: a group() can hold arbitrary, non-contiguous axes with no rectangular shape to pack, and a GroupLayout group’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 clears tight_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 SubplotSpec row 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 custom add_axes layout) form one fallback group together. Re-applied automatically after tight_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 (matching SubplotSpec column 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.

ax selects which axes contribute (default: all of them). fontsize/framealpha match Axes.legend().

handles overrides which artists appear – any plotpress artist, from any axes (or none), in the order given, regardless of their own label – the only way to legend a figure whose panels are meshes/contours/filled regions with no labeled line artist to draw from. Pair with labels to also override the text shown for each, positionally; without it, each handle’s own label is used. handles/labels take precedence over ax (a handle already names its own source).

loc names 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 the loc corner of the legend box at that exact point instead of loc’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 to loc (one of the four named edges above, or not) – bbox_to_anchor only 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.

ax may be a single Axes (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’s vmin/vmax for the shared bar to describe them accurately.

label sets what the color scale means (equivalent to, and just a convenience for, cax.set_title(label) on the returned axes – there is no separate set_label). ticks fixes the bar’s own tick positions instead of the norm’s auto-generated ones (a BoundaryNorm’s bin edges, or just a shorter list for a crowded scale); format overrides 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_svg() str[source]
to_vega(mesh_data: bool = False) dict[source]

A real Vega (not Vega-Lite) v5 JSON specification, as a plain dictjson.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-embed in a browser, the vg2svg/vg2png CLI tools, an Observable notebook, IPython’s own vega MIME 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 Vega group mark with its own local scales/axes/marks, positioned at that axes’ own resolved pixel rect. Line/scatter/bar charts use genuine field/scale-encoded marks; everything else reuses the same pixel-space primitives to_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. See plotpress.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 a UserWarning naming 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 up signals on the result).

mesh_data=True opts a pcolormesh/mesh-backed imshow into real per-cell rect marks with a genuine field+scale color encoding, instead of the default rasterized image mark – 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 threshold pcolormesh(rasterized=None)’s own auto-mode already uses). A mesh that doesn’t qualify still gets the image mark, with a UserWarning naming 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. result is {"grid": <spec> | None, "standalone": [<spec>, ...]}: a combined spec for whatever axes compose cleanly into Vega-Lite’s hconcat/vconcat grid, plus a list of independent specs for anything that doesn’t (a single axes with nothing to grid against, a free-form add_axes()/inset_axes() panel, a mismatched-shape multi-grid figure). caveats is 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 a UserWarning, 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 – see plotpress.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=True opts a pcolormesh/mesh-backed imshow into real per-cell rect marks with a genuine field+scale color encoding, instead of the default rasterized image mark – 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, as to_vega()’s own mesh_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 own Style – everything needed to rebuild an identically laid-out, identically styled blank figure via plotpress.figure_from_template(), with none of the data actually plotted into it.

This is the exact same payload to_html() embeds for plotpress.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. See plotpress.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 colorbar ticks/format).

save_template(path) None[source]

Write to_template()’s result to path as 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. See plotpress.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; see print_summary() for one axes at a time, or read fig.axes/ax.artists directly for that).

Named print_* (not e.g. layout_summary) so it tab-completes alongside every other summary method this library adds – see print_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.

options adds 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 are enabled, view, orientation ("x"/"y"), link_all, snap_pins (mirror Point Picking pins onto the profile), grid (gridlines on the profile, default True), range ("auto"/"colorbar"/"custom", the last with range_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), and axes ("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, so load_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 it False when this HTML is going into a container you don’t control the size of (an <iframe> embedding it, say, as Report does): 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_precision sets 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_points cap 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 – a UserWarning naming every affected axes does instead, whenever a mesh actually crosses the cap. Raise pick_max_mesh_cells for 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_data embeds 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: DecompressionStream overhead 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. Set False for the exact plain-JSON payload, e.g. to inspect it by hand or diff it against an older plotpress version.

include_default_js (default True) controls whether plotpress’s own toolbar/pan/zoom/pick JS (plotpress._interactive.INTERACTIVE_JS) is included at all. Set it False to get the #plotpress-meta/ #plotpress-pick/#plotpress-style JSON payloads (assuming interactive=True) with none of plotpress’s own JS behavior layered on top – for building interactivity entirely from scratch against that data and extra_js, rather than extending what’s already there. binary_pick_data=False is worth pairing with this: the default binary encoding needs plotpress’s own decoder, which is exactly what this is turning off.

extra_js is a raw JS string inlined as its own <script> block, after plotpress’s own (when include_default_js is True) so window.plotpressAddTool/plotpressGetMarkers already exist by the time it runs. With include_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. With include_default_js=False, it’s the only JS this page gets – write your own toolbar/interactivity entirely, working from #plotpress-svg and 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.

path may also be a file-like object (a BytesIO, an open file) instead of a filename – the standard fig.savefig(buf, format="png") idiom for serving a figure without touching disk. format names the format explicitly (a bare extension, with or without the leading dot); it is required when path is 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_js apply only to interactive HTML (see to_html()). .gif needs at least one Axes.plot_frames() or Axes.pcolormesh_frames() series – it animates through that series’ frames at fps, the same data an interactive HTML slider scrubs through, as a self-contained looping file; slider_unit picks which slider drives the animation for figures with more than one, and label_frames stamps each frame with its slider value since a GIF has no slider to show it on (see plotpress.raster.save_gif()).

.jpg/.jpeg/.webp are raster, like .png (and share its scale), 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. .eps is vector, like .pdf – for submission pipelines that still require EPS specifically.

dpi (PNG/JPEG only) overrides Style.dpi for 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 if Style.dpi itself 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 with Style.facecolor; each axes’ own facecolor is unaffected.

savefig(path, **kwargs)[source]

Alias for save() (matplotlib-compatible name).

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=True the 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 returns None (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, use plotpress.qt.PlotPressWidget directly.

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 (fig as a cell’s last expression) renders it inline as static SVG via Figure._repr_svg_ – there is deliberately no _repr_html_, since Jupyter prefers text/html over image/svg+xml when 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 .html file opened in a browser.

width/height default to the figure’s own pixel size (figsize x style.dpi); pass either to override. The rest of the keyword arguments are forwarded to to_html() (see there for what each controls).

Returns an IPython.display.HTML object – return it as a cell’s last expression, or pass it to IPython.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/sharey link 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, and figsize is 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 supxlabel add 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. figsize is ignored when subplot_size is given, beyond seeding the first pass. Calling set_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. Mirrors subplots()’s own signature and creates a fresh, independent Figure the same way.

subplot_size=(w, h) works as it does on subplots() – see there – and sizes one axes, not one group: a layout of 2x1 groups asked for subplot_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): axes is shaped like layout’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 – whatever subplots() itself would return for that group’s shape. An outer cell with no group registered is None. sharex/sharey link limits within each group only (every group is its own independent cluster, the same as calling subplots() 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 ordinary Figure.group() call this makes internally, so it works the same way in every respect – including Figure.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 – axes on the way back is figure_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 SubplotSpec spans within it – so once built, the figure is completely ordinary: every axes has one flat _subplotspec in one shared grid, exactly like subplots()/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 – including group_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 an nrows x ncols inner grid of axes. Returns self so 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/ncols are then inferred from it, rather than needed separately:

  • mask, an nrows x ncols array-like of truthy/falsy values – falsy means no axes there. Pass real booleans/ints, not strings: numpy casts a non-empty string to True regardless 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-None entry means an axes and sets its id (see set_id()) in one step, mosaic-style.

  • axes_titles, the same idea for each axes’ title (see set_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/id and the styling kwargs (linestyle/color/ linewidth/title_position/pad/fontsize/supxlabel/ supylabel/supxlabel_size/supylabel_size/visible) match Figure.group() exactly – passed straight through to it once this group’s real axes exist. A group is only registered (and so only findable via Figure.get_group(), including by id) when it has a titleid without 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) – before subplots_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, see Figure.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/id match whatever Figure.group() (or GroupLayout.add_group(), which calls it internally) was given. outer_row/outer_col are this group’s own position in its GroupLayout’s outer grid – None for a group built by a direct Figure.group() call, which has no such position. axes is the 2-D (row, col) array subplots_from_groups() itself returned for this group (None for an absent cell) when built from a shaped layout; otherwise (a direct Figure.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 axes itself is shaped (a (row, col) array, None for an absent cell skipped) or already flat (a manually built group() 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 a GroupLayout), title, or id, scoped to this group’s own axes only. See Figure.get_ax() for the many= 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 (see docs/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 (see Figure.to_html()) is inlined via the iframe’s srcdoc attribute 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 with save():

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 figure to the report; returns self so calls can chain.

title (a short heading) and details (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.

interactive and the pick_*/binary_pick_data arguments are forwarded to each figure’s own Figure.to_html() – see there for what they mean. Every figure in the report shares the same settings; call Figure.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=True starts every entry collapsed instead of open, and genuinely defers each one: rather than embed it as a live srcdoc that 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() or Report.save().

By default, returns a dict keyed by each figure’s own title (a Report entry’s Report.add() title; a generated "Figure N" – 1-based, matching the label a Report page itself shows – for an entry with none, or for a bare Figure’s HTML, which has no report-level title at all). Each figure’s own value has "details" (a Report entry’s longer description, or None) "axes" (itself a dict keyed by each axes’ own title, falling back to "axes {index}" – matching a picked record’s axes_title fallback – 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 own label=/ (single, resolved) color= at save time – None for a file saved before these existed, an unlabeled/uncolored series, a colormap-mapped scatter(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 – see figure_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, any Figure.group() boxes, twin/secondary/inset overlays, colorbar styling, this figure’s own Style, 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 dict Figure.to_template() produces (see plotpress.svg.template_metadata() for the full field-by-field breakdown) – pass it straight to plotpress.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 by figure_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 a UserWarning names every collision resolved this way. Pass by_index=True when 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 an interactive=False HTML embeds no data to read back, only drawn shapes, and raises ValueError. 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 crossed pick_max_mesh_cells at save time comes back at that coarser, block-averaged resolution, not the original grid’s – see Figure.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 layout load_data() already returns) instead of load_data()’s title-keyed dict of dicts.

Needs the optional xarray dependency: 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 to load_data()’s own by_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 – see load_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 from add_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 (its x/y too, in the per-panel-coordinate case), distinguished from a panel whose real data legitimately happened to be all-NaN by the has_data coordinate below. Raises ValueError, 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 at load_data() (by_index=True for the title-collision-proof form) as the fallback for a figure this doesn’t cover.

The returned Dataset has row/col coordinates plus each panel’s own title/xlabel/ylabel ("" for a missing panel) and has_data (True for a grid cell an axes with plotted data actually occupies, False for one with no axes or nothing plotted) as (row, col) coordinates; a mesh grid’s x/y are 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 is z, dimensioned (row, col, y, x). A line grid’s data variable is y, dimensioned (row, col, point), with x the same shared-or-per-panel choice. .attrs carries the recovered figure’s own figsize and title, plus "template" – the exact same dict load_data() returns under that key, ready to pass straight to figure_from_template() without a second, separate load_data() call just to get it – ds.attrs["template"], not a duplicate parse of the file.

figure selects which figure to load from a multi-figure Report file – an int index (0-based, save order) or the exact string title a Report entry was given. Left as None (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, unlike load_data(). Pass the result to figure_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, and Style – everything to_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 with plotpress.load_template(), with no plotted data anywhere in it. Replot into the returned (blank) axes the same way you would after plotpress.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/facecolor override the template’s own saved values. style overrides template["style"] outright; a template saved before "style" existed falls back to a fresh, default Style when no override is given, the same fallback shape figsize/facecolor already use for their own missing/older keys.

Returns (fig, axes). When every recorded axes is a single, non-spanning cell that exactly tiles one nrows x ncols grid, axes mirrors what plotpress.subplots(nrows, ncols) itself would hand back – a bare Axes for a 1x1 grid, a 1-D array for a single row/column, otherwise a 2-D array indexed axes[row, col]. Anything else (row/column spans from add_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 to fig.axes but are not folded into this return value – the same way ax.twinx() isn’t folded into plotpress.subplots()’s own return either; give an axes (or its parent) an id before saving the template if a lookup afterward needs to find it reliably, via fig.get_ax(id=...).

Colorbars are documented in template["colorbars"] (which axes had one, and its fraction/pad/label/ticks/format) but never auto-built – a colorbar needs a live mappable, which doesn’t exist until real data is plotted. Call fig.colorbar(mesh, ax=...) yourself once you’ve replotted, passing those same styling knobs back if you want them preserved. An axes that had a legend() 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; call ax.legend(**entry["legend"]) yourself once you’ve replotted into it.

Warns (UserWarning) when template["omitted_axes"] has an axes beyond what "overlays"/"insets" account for – a freeform Figure.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 any Figure.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, dropping row/col entirely instead of leaving them behind as length-1 dimensions – ds.isel(row=r, col=c) already does exactly that for a scalar r/c, which is all this is: that call, plus resolving title to the one (row, col) position it names.

Pass either title (matched against ds["title"], the same string load_data()/a panel’s own ax.set_title() used) or both row/col (plain 0-based grid position) – not a mix of the two, and not neither. Raises ValueError when title matches no panel at all. When title matches more than one panel (two panels sharing a title, so there is no name left to disambiguate by), this raises too unless multiple=True, which returns every match as a list instead of picking one.

multiple=True always returns a list of Datasets – one item for a unique title or an explicit row=/col=, or one per match for a duplicated title – rather than a list only sometimes and a bare Dataset otherwise, so a caller that always wants to loop over the result doesn’t have to branch on how many panels actually matched.

Each returned Dataset keeps every data variable/coordinate load_data_xarray() built, just without row/col – a mesh panel’s z is (y, x) instead of (row, col, y, x), a line panel’s y is (point,) instead of (row, col, point), and title/xlabel/ylabel/has_data come 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()’s transform= 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.style is the same object as ax.figure.style (not a per-axes copy), so this stores the override on the axes rather than mutating self.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, or x, y, fmt as a line. Returns the Line2D.

fmt is matplotlib’s format-string shorthand ('ro-', 'k.', 'C1--') – any of a color, a linestyle, and a marker, in one string (see _parse_fmt()). An explicit color=/ linestyle=/marker= keyword overrides whatever fmt says for that piece; a marker with no linestyle character in fmt means no connecting line, matplotlib’s own convention.

values is an optional {name: array} of extra per-point dimensions (e.g. z) surfaced when a point is picked interactively.

marker draws a shape at each vertex in addition to the line itself (markersize in points, default matches the style’s own marker size; markerfacecolor defaults to the line’s own color; markeredgecolor/markeredgewidth outline it, the same as scatter()’s edgecolors/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 limitation scatter()/errorbar() share.

x/y need not be plain numbers:

  • Datetime-like (numpy.datetime64, datetime.date/ datetime.datetime, or a sequence of those – a pandas Series/DatetimeIndex already becomes one of these through numpy.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 y vs x. c maps values through cmap.

values is an optional {name: array} of extra per-point dimensions (e.g. z or a 4th value) surfaced by point picking; the color dimension c is included automatically.

edgecolors/linewidths outline 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. Giving edgecolors with no linewidths still 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/y may also be datetime-like (real time-proportional spacing) or strings (a categorical axis, positions 0, 1, 2, … in first-occurrence order) – see plot() 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.

Y has shape (n_frames, n_points); x is 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 shared plot_frames panels scrub together.

  • shared=False – this axes gets its own slider docked beneath it. Pass slider_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_values labels the extra axis (defaults to 0..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) or pcolormesh(X, Y, C). X/Y may be 2-D for a curvilinear grid. shading="gouraud" smoothly interpolates the color between grid nodes instead of flat cells. alpha/label match imshow() – its own animated sibling pcolormesh_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. rasterized controls 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 – False there warns that the SVG will scale with cell count, since _VECTOR_CELL_LIMIT is only ever consulted by auto mode). A curvilinear grid (2-D X/Y) has no vector path at all – its cells aren’t axis-aligned rects – so it always rasterizes and rasterized=False there 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=True once to see what that export would actually lose.

The returned QuadMesh exposes 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 – see docs/examples/limitations/plot_05_pcolormesh_vector_cell_limit.py for a worked example reading them).

pcolor(*args, **kwargs)[source]

Alias of pcolormesh() – matplotlib itself now recommends pcolormesh (faster, and this library’s own vector/raster cell handling already only exists on that path); pcolor is 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) or pcolormesh_frames(X, Y, C), matching pcolormesh() except C carries a leading frame axis – shape (n_frames, ny, nx) rather than (ny, nx). X/Y are 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_label match plot_frames() exactly – see there for shared/slider_group.

Unlike pcolormesh(), this always rasterizes – there is no rasterized kwarg 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 own x) or "edge" (x is the bar’s left edge instead – pass a negative width for a right edge).

yerr/xerr draw error bars centered at each bar’s own top (bottom + height), composed from the same whiskers-and-caps errorbar() 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’ own color) matches matplotlib’s own bar-error-bar default.

hatch tiles 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.

x may also be strings – a categorical axis, one bar per distinct value, positioned at 0, 1, 2, … in first-occurrence order (see plot()) – the standard bar(["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/hatch match bar(), centered at each bar’s own right edge (left + width). y may be strings, the same categorical axis bar()’s x supports – 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 Bars bars (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.

labels overrides the text shown, positionally (default: each bar’s own value formatted with fmt). padding nudges the label away from the tip as a fraction of the axis span – matplotlib measures its own padding in points; there is no such absolute unit here, so this is the closest equivalent, not a literal drop-in value.

Returns the list of Text labels added, one per bar, in the same order as bars.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).

linewidth overrides the edge width, which otherwise defaults to 0.6 for histtype="bar" (a divider between adjacent bars) or 1.5 for "step"/"stepfilled" (the outline itself is the only mark) – matching sibling bar()’s own linewidth=, 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).

x may be a single array or a sequence of arrays – multiple datasets share one set of bin edges (from their combined range when bins is a count rather than explicit edges), overlaid by default or, with stacked=True, stacked bottom-to-top in the order given. color/label may then be a matching list, one per dataset (a bare value applies to all, same as a single dataset).

histtype is "bar" (default: filled bars with dividers between them), "step" (unfilled outline, no dividers) or "stepfilled" (filled outline, no dividers) – matplotlib’s own three. bars is a Bars for "bar" (one per dataset, a list if there’s more than one) or a Polygon staircase outline for "step"/"stepfilled".

cumulative running-sums each dataset’s own counts left to right. weights (matching x’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.

linestyle forwards straight to plot() – a dashed or dotted step, for overlaying two step curves distinguishably. No marker=: 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 real x/y separately with scatter() 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 y1 and y2.

edgecolor/linewidth outline the filled region – the same two options fill() already has, since both draw the same closed-path primitive; there was no reason the outline was fill()-only.

where (a boolean mask matching x, typically y1 > y2 or similar) restricts the fill to its contiguous True runs – each its own artist. By default each run stops at the last sample still inside it, leaving a visible gap up to where y1/y2 actually cross; interpolate=True extends 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 when where is 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 x1 and x2 across y.

edgecolor/linewidth match fill_between(). where (a boolean mask matching y) restricts the fill to its contiguous True runs, the same way, and interpolate=True extends each run to x1/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 y from xmin to xmax.

vlines(x, ymin, ymax, color=None, linewidth=None, linestyle='-', label=None, alpha=1.0, zorder=0)[source]

Draw vertical line segments at each x from ymin to ymax.

stem(x, y=None, baseline=0.0, color=None, linecolor=None, markercolor=None, label=None, zorder=0)[source]

Stem plot.

color sets both the stems and the marker at once, matching every other line/marker method’s own color= convention; linecolor/ markercolor override it independently where a stem plot’s two colorable parts need to differ (the same “a shared default, plus a more specific override” shape errorbar()’s ecolor has).

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.

fmt is matplotlib’s 5th positional argument here too (its own real signature is errorbar(x, y, yerr, xerr, fmt, ...)) – a format string like 'ro-' (see plot()/_parse_fmt()). This used to be plotpress’s own color slot, so a matplotlib caller’s 5th positional argument – almost always a fmt string – silently landed in color instead, rendering with whatever garbage color string that happened to be and no error anywhere. An explicit color=/marker=/linestyle= keyword still overrides whatever fmt says for that piece.

ecolor/elinewidth style the whiskers/caps independently of the connecting line and marker – each falls back to color (resolved the same way) / linewidth if not given, so nothing changes unless you pass them. capthick (the caps’ own width) falls back to elinewidth in turn.

errorevery draws 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=20 keeps 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 via set_aspect() (this axes’ own aspect, not per-image) – left alone by default, unlike matplotlib’s own imshow(), which forces 'equal' even without an explicit aspect= (see matshow(), 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.

whis sets the whisker reach in IQRs past q1/q3 (matching matplotlib’s own default of 1.5); points past that are drawn as fliers unless showfliers=False drops them instead.

vert is matplotlib’s older True/False spelling of orientation (True -> "vertical", False -> "horizontal") – an explicit orientation= 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 own positions entry, via set_xticks()/set_yticks().

showmeans adds 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).

cut extends each density past its data extremes by that many bandwidths (seaborn’s default is 2; 0 clips at the observed range). inner overlays 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), or None.

vert is matplotlib’s older True/False spelling of orientation. showmeans/showmedians each draw one solid/ dashed line across the violin at that value, independent of inner (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.

cut extends 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.

height is 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. scale maps (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 own head_width/head_length, measured in points).

A thin wrapper over quiver() with one vector and scale=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 length U (in Q’s own data units) looks like, for the Quiver Q returned by quiver().

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 own transform=ax.transAxes, the key arrow is a data-anchored Quiver under 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.

labelpos places label "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 encoding hypot(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/V therefore only set direction here, not shaft length; there is no scale= 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) or contour(x, y, Z).

Colors (when colors isn’t given explicitly) come from mapping each level’s own value through cmap, normalized by vmin/vmax (defaulting to Z’s own min/max) – the same normalization contourf() uses, so an explicit vmin/vmax colors both the same way, and non-uniform levels (e.g. [0, 1, 2, 10]) get each level’s true position on the scale, not just its rank among them.

linewidths/linestyles are a single value or one per level (matching levels’ own length). When colors is given explicitly (a single flat color, not a cmap gradient) and linestyles is left unset, negative levels default to negative_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) or contourf(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. levels is a band count or explicit boundaries.

clabel(CS, levels=None, fmt='%1.3g', fontsize=None, colors=None, inline=True, zorder=6)[source]

Label CS (the Contour contour() 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 – inline is accepted for signature compatibility but has no effect here; the label’s own contrast halo keeps it legible over the line regardless.

levels restricts labeling to a subset of CS’s own levels (default: every level). fmt is a %-style format string or a callable taking the level value. colors overrides the label color (default: matches each level’s own line color).

Returns the list of Text labels 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/vmax normalize the counts exactly as they do for pcolormesh and imshow. 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, so norm=LogNorm() is often the difference between a readable density map and two blobs.

edgecolors/linewidths outline 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. linewidths is one width for every hexagon – unlike matplotlib’s own hexbin, there is no per-hexagon outline width here (the collection this builds on has one shared edge width, the same as scatter’s own linewidths=).

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/vmax as hexbin(), and for the same reason: counts are rarely uniform enough for a linear ramp.

stackplot(x, *ys, colors=None, alpha=0.8, labels=None, zorder=0)[source]

Stacked area plot.

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 x and y over +-maxlags.

Returns (lags, c, lines, markers) where lines is the stem collection (usevlines) or connecting line, and markers is the dot at each lag. alpha/zorder apply to both.

acorr(x, **kwargs)[source]

Autocorrelation – xcorr() of x with itself.

set_xscale(scale)[source]

Set the x-axis scale: 'linear' or 'log'.

set_yscale(scale)[source]

Set the y-axis scale: 'linear' or 'log'.

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.

get_aspect()[source]

The current aspect: 'auto', or the y/x unit ratio (1.0 for 'equal').

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” strategy set_aspect() uses.

get_box_aspect()[source]
semilogx(*args, **kwargs)[source]
semilogy(*args, **kwargs)[source]
loglog(*args, **kwargs)[source]
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 s at data coordinates (x, y).

outline is 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; pass False to 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.

alpha fades the glyphs themselves, independent of bbox’s own alpha (the box’s fill can be more or less transparent than the text drawn over it).

bbox draws a filled/bordered box behind the text instead of (or as well as) the outline halo – matplotlib’s bbox= dict, a subset of its keys: facecolor/fc (default white), edgecolor/ec (default none), alpha (default 1.0), pad (pixels around the text, default 4.0), boxstyle ("square" or "round"), and linewidth. Pass {} for the defaults.

fontweight ("normal"/"bold", or any matplotlib weight name/ number – >= 600 counts as bold) and fontstyle ("normal"/ "italic"/"oblique") select the glyph face; both also feed the width measurement bbox sizes against and the leader in annotate() anchors to, so a bold or italic label still gets a tight box/leader rather than one sized for the regular face.

s may contain \n for a multi-line label – each line is independently aligned per ha (matplotlib’s default multialignment), and the block as a whole is placed per va ("top" anchors the block’s top edge, "bottom" its bottom edge, "center" its middle, "baseline" the first line’s baseline).

transform=ax.transAxes places (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 xy with text placed at xytext.

Pass arrowprops={"color": ...} (or {}) to draw an arrow from the text to xy. arrowprops also accepts alpha, applied to the arrow only – independent of the text’s own alpha. The leader starts at the edge of the text’s bounding box nearest xy – preferring the middle of an edge – so it never sets off across its own label; with bbox set, 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-line text all match text().

textcoords=ax.transAxes places xytext as an axes-fraction position – the label sits at a fixed spot on the axes frame while its arrow still points at the data coordinate xy, e.g. a callout pinned to a corner regardless of where the data it labels ends up after a pan or zoom. xy itself 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.

cellText is a list of rows, each a list of cell strings. rowLabels/colLabels add a labeled header column/row. cellColours/rowColours/colColours are 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_ANCHOR for the full set – or an explicit bbox=(x0, y0, w, h)), the same as text()’s own transform=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-column colWidths= sizing by content yet.

set_axis_off()[source]

Hide the spines, ticks, grid, and axis labels (keep the title).

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=value dispatches to this axes’ own set_foo(value) – matplotlib’s Axes.set() works the same way, generated from every set_* 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 like set_axis_off()). Raises on any keyword with no matching set_* method, naming all of them at once rather than stopping at the first.

set_facecolor(color)[source]

Set this axes’ own background color (independent of the figure).

get_facecolor()[source]
set_visible(visible)[source]

Show/hide this axes. A hidden axes still reserves its grid cell.

get_visible()[source]
set_pickable(pickable=True)[source]

Include or exclude this axes from Point Picking.

False makes 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 is set_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.

get_pickable()[source]
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.

get_pick_context()[source]
remove()[source]

Detach this axes from its figure.

Also drops it from any sharex/sharey group 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 (see set_id()) so another axes may reuse it, and drops it from any group() it belonged to – both that group’s flat axes list and, for a GroupLayout-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 later set_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 – see svg._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; call tight_layout() again for that. Neither this frozen box nor the whitespace this axes’ own removed grid cell leaves behind shrinks on its own – call fig.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 in figure.axes, still rendered, with its own _twin_of/_secondary_of now 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_parents of any colorbar (colorbar()) built against it – _layout_colorbar re-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/sharey group first, using the same in-place-removal trick as remove() (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’s set_xlim/set_ylim. Also releases this axes’ own id (see set_id()) first – the constructor resets self._id to None same 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 PolarAxes resets 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/sharey group first, using the same in-place-removal trick as remove() (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’s set_xlim/set_ylim. Also releases this axes’ own id (see set_id()) first – the constructor resets self._id to None same 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 PolarAxes resets 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).

x may be datetime-like or a string, the same as plot()’s own x/y – including on a categorical axis, where a string not already among this axis’ categories is added as a new one (matching plot()’s own first-seen-wins rule) rather than raising. That differs from set_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 (via slope or 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 at yrange.

yrange is (ystart, yheight). Handy for Gantt / timeline charts.

Each span’s xstart may be datetime-like or a string, the same as plot()’s own x – a task’s start date, say. xwidth stays a plain number in either case: it’s a duration, not a position, and on a date axis that duration is in days, the unit plot() 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 (len values + 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).

y may be datetime-like or a string, the same as plot()’s own x/y – see axvline()’s own docstring for what that means on a categorical axis (a new string is added as a category, unlike set_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)), or None on either side to autoscale just that end – set_xlim(0, None) pins the left edge and lets the data decide the right. Both None clears 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()’s x/y are, so set_xlim("2024-01-01", "2024-06-01") or set_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). axis selects "x", "y", or "both" (default) – each axis keeps its own override, so tick_params(axis='x', color='red') recolors only the x ticks. which selects "major", "minor", or "both"; minor ticks have no labels, so labelsize/labelcolor/ labelrotation only ever affect major ticks.

labelrotation angles the tick labels (degrees, counterclockwise – matching text()’s own rotation), 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 separate ha='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.

minorticks_on()[source]

Draw unlabeled minor tick marks between the major ones.

minorticks_off()[source]
tick_bottom()[source]

Draw x-axis ticks/labels along the bottom edge (the default).

tick_top()[source]

Draw x-axis ticks/labels along the top edge.

tick_left()[source]

Draw y-axis ticks/labels along the left edge (the default).

tick_right()[source]

Draw y-axis ticks/labels along the right edge.

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 of invert_xaxis() – unlike get_xlim(), which reports them in whatever direction they’re actually drawn.

get_ybound()[source]

The resolved y limits, always (low, high); see get_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 via x=/y=. This is a persistent setting – unlike a one-shot set_xlim nudge, 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/_ylim here.

set_xmargin(m)[source]
set_ymargin(m)[source]
get_xmargin()[source]
get_ymargin()[source]
autoscale(enable=True, axis='both', tight=None)[source]

Re-enable (or freeze) autoscaling on axis ('x'/'y'/'both').

enable=False freezes the axis at its current resolved limits. tight=True also zeroes that axis’ margin.

set_autoscalex_on(b)[source]

Enable/disable x autoscaling (shorthand for autoscale() with axis='x').

set_autoscaley_on(b)[source]

Enable/disable y autoscaling; see set_autoscalex_on().

get_autoscalex_on()[source]
get_autoscaley_on()[source]
set_xticks(ticks, labels=None, minor=False)[source]

Set explicit x tick locations. Pass [] to hide ticks.

ticks may also be datetime-like or a list of strings – resolved through the same coercion plot()’s x/y use (see its docstring), so set_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.

labels optionally sets the tick label strings in the same call (matplotlib’s combined set_xticks(ticks, labels) form) – ignored when minor=True, since minor ticks never carry labels here.

minor=True sets minor tick positions instead of major ones, and (matching matplotlib) implicitly turns minor ticks on – the same flag minorticks_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/minor match set_xticks(); ticks accepts 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.

spec is plain, JSON-serializable data – a dict naming a scheme (currently just "multiple", matplotlib’s MultipleLocator) – 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. Pass None to 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 – see resolve_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 callable value -> 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 None to go back to the default. See apply_tick_format() for the full spec grammar, and resolve_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 Text objects, 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 sharex column 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().

xaxis_inverted()[source]
yaxis_inverted()[source]
sharex(other)[source]

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.

sharey(other)[source]

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/subplots grid (_subplotspec is None).

twinx()[source]

Return an overlaid axes sharing this x-axis, y-axis drawn on the right.

twiny()[source]

Return an overlaid axes sharing this y-axis, x-axis drawn on the top.

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 along location ('top' or 'bottom'). Custom unit-conversion (matplotlib’s functions=) is not supported; use twiny() 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(); location is '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 later tight_layout/ subplots_adjust calls (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 an inset_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_ax is 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(), with bounds taken from inset_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 later tight_layout/subplots_adjust will 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 own get_position()/ apply_aspect() split).

set_xlabel(xlabel, visible=True, size=None, fontsize=None)[source]

Set the x-axis label. visible=False stores it without drawing it – get_xlabel(), the load_data() layout round-trip, and a picked point’s Extract record still report it, but it isn’t rendered and reserves no margin (see also set_xlabel_visible()). Passing text again with the default visible=True re-shows it.

size overrides the style’s label_size for this axes’ x label only, the same per-axes escape hatch set_title() already has – a small-multiples grid can want each panel’s own label a few points high without a whole Style copy per figure changing every other label too. fontsize is accepted as matplotlib spells it.

set_ylabel(ylabel, visible=True, size=None, fontsize=None)[source]

Set the y-axis label. visible=False stores it without drawing it – see set_xlabel() and set_ylabel_visible(). size/ fontsize override the style’s label_size for this axes’ y label only, same as set_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). False after set_xlabel(..., visible=False) or set_xlabel_visible(False).

get_ylabel_visible()[source]

Whether the y-axis label is drawn (default True).

set_title(label, size=None, fontsize=None)[source]

Set this axes’ title. size overrides 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 Style copy per figure – changes every other title too. fontsize is 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.

axis restricts 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. which draws lines at "major" tick positions (default), "minor" (see minorticks_on()), or "both".

alpha overrides this axes’ gridline opacity; None (the default) falls back to the figure style’s own grid_alpha, the same “override vs. style default” convention Spine and 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).

loc is a matplotlib-style corner/edge name (e.g. "upper left", "lower center", "center"; "best" maps to upper right). ncol lays the entries out in that many columns; title adds a heading row. fontsize overrides the entry/title text size (default: the style’s own tick label size). framealpha is the legend box’s own background opacity (matplotlib’s default is 0.8; 0.85 matches 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 the loc corner 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. Unlike loc alone, this can and often does place the box outside the axes’ own drawn area.

handles overrides which artists appear – any plotpress artist (from this axes, another, or never added to one at all), in the order given, regardless of their own label. Pair with labels to also override the text shown for each, positionally; without it, each handle’s own label is used.

Returns a Legend handle – also available later via get_legend() – for repositioning/restyling or hiding the legend after the fact without a full legend(...) call.

get_legend()[source]

The current Legend, or None if legend() was never called (or was hidden via Legend.remove()/ set_visible(False)).

get_legend_handles_labels()[source]

(handles, labels) for whatever legend() would currently draw – _legend_handles (from legend(handles=...)) if set, else every artist on this axes carrying a label, in call order. Mirrors plotpress.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.

get_xlim()[source]
get_ylim()[source]
get_xlabel()[source]
get_ylabel()[source]
get_title()[source]
set_id(id)[source]

Set a plain, undrawn identifier for this axes – for later retrieval via get_ax()/ get_ax(), distinct from set_title() (which is drawn on the plot, and may legitimately repeat across several axes). Unlike a title, id must be unique across this axes’ whole figure – raises if another axes already has it. Pass None to 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_id()[source]
get_xscale()[source]
get_yscale()[source]
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 of print_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 – see print_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: a numpy.datetime64 scalar/array, a datetime.date/datetime.datetime, or a sequence of those (what a plain Python list of dates, or a pandas Series/ DatetimeIndex converted through numpy.asarray, already is).

plotpress.dates.to_days(value) ndarray[source]

Convert date/time data to a plain float64 array 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 of NaN) maps to NaN, not a huge-but-finite float – NaT’s own int64 encoding is the minimum representable int64, 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 reads 14: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**exp as the correctly rounded double, for an integer exp.

Neither Python’s ** nor JavaScript’s Math.pow is required to give the correctly rounded power of ten, and both really do miss: 10.0**23 and 10.0**126 differ from the 1e23/1e126 literals here, and a CI runner’s V8 returned a different double for 10**-5 than the author’s machine did – which shifted a whole axis’ first tick, because ceil(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 own pow10) 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 (or 1e-3 if vmax itself isn’t positive either), never exactly zero.

Shared by log_ticks()/minor_ticks() here and by LogNorm, which used to independently floor at a fixed 1e-300 instead – for a LogNorm(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 pass vmin > 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 step nice_ticks chose. 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_tick alone 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 (plus offset) within [vmin, vmax] – matplotlib’s MultipleLocator. The standard way to force ticks at, say, every pi/2 on a trig plot’s axis, regardless of what nice_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 locator spec 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 explicit fmt spec 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 explicit apply_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 explicit apply_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 tick values.

spec is one of:

  • a callable value -> str (Python-only – see set_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 text in pixels, per this style’s font settings.

The measuring entry point layout should use: it carries font_family and measure_installed_fonts with it, so no caller has to remember to pass either.

copy(**overrides) Style[source]

Return a modified copy, leaving this instance untouched.

Mutable fields are duplicated so two figures never share a list.

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 nan is 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’s nanmin warns 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.