Skip to content

Figure & Axes

The core 2D/3D plotting API.

Entry points

subplots

subplots(
    nrows: int = 1,
    ncols: int = 1,
    *,
    figsize: tuple[float, float] = DEFAULT_FIGSIZE,
    sharex: bool = False,
    sharey: bool = False,
    projection: str | None = None,
    theme=None,
    units: str = "pt",
    width_ratios=None,
    height_ratios=None
)

Create a [Figure] with an nrows x ncols grid of axes.

figsize is the canvas (width, height) in points by default, so a plot is sized directly against its font scale; pass units="in", "cm" or "mm" for another unit. projection="3d" makes every axes an [Axes3D]. theme is a [Theme] (or a preset name — "default", "dark", "grayscale"); it flows to every axes. Returns (fig, ax) for a 1x1 grid, (fig, [ax, ...]) when one dimension is 1, and (fig, [[ax, ...], ...]) otherwise (row-major).

width_ratios / height_ratios give relative column widths and row heights, e.g. width_ratios=[2, 1] for a wide panel beside a narrow one. Only the proportions matter ([2, 1] and [0.5, 0.25] are the same), and the gutters stay a fixed size - weighting changes the panels, not the space between them.

Source code in python/pyplotrs/_figure.py
def subplots(nrows: int = 1, ncols: int = 1, *, figsize: tuple[float, float] = DEFAULT_FIGSIZE,
             sharex: bool = False, sharey: bool = False, projection: str | None = None,
             theme=None, units: str = "pt", width_ratios=None, height_ratios=None):
    """Create a [`Figure`] with an ``nrows`` x ``ncols`` grid of axes.

    ``figsize`` is the canvas ``(width, height)`` in **points** by default, so a
    plot is sized directly against its font scale; pass ``units="in"``, ``"cm"``
    or ``"mm"`` for another unit. ``projection="3d"`` makes every axes an
    [`Axes3D`]. ``theme`` is a [`Theme`] (or a preset name — ``"default"``,
    ``"dark"``, ``"grayscale"``); it flows to every axes. Returns
    ``(fig, ax)`` for a 1x1 grid, ``(fig, [ax, ...])`` when one dimension is 1,
    and ``(fig, [[ax, ...], ...])`` otherwise (row-major).

    ``width_ratios`` / ``height_ratios`` give relative column widths and row
    heights, e.g. ``width_ratios=[2, 1]`` for a wide panel beside a narrow one.
    Only the proportions matter (``[2, 1]`` and ``[0.5, 0.25]`` are the same),
    and the gutters stay a fixed size - weighting changes the panels, not the
    space between them.
    """
    fig = Figure(figsize=figsize, nrows=nrows, ncols=ncols, sharex=sharex, sharey=sharey,
                 projection=projection, theme=theme, units=units,
                 width_ratios=width_ratios, height_ratios=height_ratios)
    if nrows == 1 and ncols == 1:
        return fig, fig.axes[0]
    if nrows == 1 or ncols == 1:
        return fig, list(fig.axes)
    grid = [[fig.axes[r * ncols + c] for c in range(ncols)] for r in range(nrows)]
    return fig, grid

subplot_mosaic

subplot_mosaic(
    mosaic,
    *,
    figsize: tuple[float, float] = DEFAULT_FIGSIZE,
    theme=None,
    units: str = "pt"
)

Build a figure of spanning axes from an ASCII mosaic layout.

mosaic is a multi-line string (or a list of equal-length rows) whose repeated labels mark the cells each axes spans, e.g.::

"""
AB
AC
"""

gives A spanning both rows of column 0, with B/C stacked at the right. "." (or a space) marks an empty cell. Returns (fig, {label: axes}). Each label's cells must form a solid rectangle.

The string is dedented before it is read, so the indented triple-quoted form above - the way a mosaic is actually written inside a function - means what it looks like. Without that, the shared leading spaces are cells of their own and the layout gains a phantom panel wider than the real ones.

Source code in python/pyplotrs/_figure.py
def subplot_mosaic(mosaic, *, figsize: tuple[float, float] = DEFAULT_FIGSIZE,
                   theme=None, units: str = "pt"):
    """Build a figure of spanning axes from an ASCII ``mosaic`` layout.

    ``mosaic`` is a multi-line string (or a list of equal-length rows) whose
    repeated labels mark the cells each axes spans, e.g.::

        \"\"\"
        AB
        AC
        \"\"\"

    gives ``A`` spanning both rows of column 0, with ``B``/``C`` stacked at the
    right. ``"."`` (or a space) marks an empty cell. Returns
    ``(fig, {label: axes})``. Each label's cells must form a solid rectangle.

    The string is dedented before it is read, so the indented triple-quoted
    form above - the way a mosaic is actually written inside a function - means
    what it looks like. Without that, the shared leading spaces are cells of
    their own and the layout gains a phantom panel wider than the real ones.
    """
    if isinstance(mosaic, str):
        rows = [list(line) for line in textwrap.dedent(mosaic).splitlines()
                if line.strip()]
    else:
        rows = [list(r) for r in mosaic]
    nrows = len(rows)
    ncols = max((len(r) for r in rows), default=0)
    # Bounding box (min/max row & col) of each label's occupied cells.
    boxes: dict[str, list[int]] = {}
    order: list[str] = []
    for r, row in enumerate(rows):
        for c, label in enumerate(row):
            if label in (".", " "):
                continue
            if label not in boxes:
                boxes[label] = [r, c, r, c]
                order.append(label)
            else:
                b = boxes[label]
                b[0], b[1] = min(b[0], r), min(b[1], c)
                b[2], b[3] = max(b[2], r), max(b[3], c)

    fig = Figure(figsize=figsize, nrows=nrows, ncols=ncols, theme=theme, units=units)
    fig.axes = [Axes(fig.theme) for _ in order]
    spans = []
    for label in order:
        r0, c0, r1, c1 = boxes[label]
        spans.append((r0, c0, r1 - r0 + 1, c1 - c0 + 1))
    fig._spans = spans
    return fig, {label: fig.axes[i] for i, label in enumerate(order)}

figure

figure(
    figsize: tuple[float, float] = DEFAULT_FIGSIZE,
    *,
    theme=None,
    units: str = "pt"
) -> Figure

Create an empty Figure (no axes). Use Figure.add_gridspec + Figure.add_subplot to place spanning axes, or subplots / subplot_mosaic for the common cases.

Source code in python/pyplotrs/__init__.py
def figure(figsize: tuple[float, float] = DEFAULT_FIGSIZE, *, theme=None,
           units: str = "pt") -> Figure:
    """Create an empty ``Figure`` (no axes). Use
    ``Figure.add_gridspec`` + ``Figure.add_subplot`` to place spanning
    axes, or ``subplots`` / ``subplot_mosaic`` for the common cases."""
    fig = Figure(figsize=figsize, nrows=1, ncols=1, theme=theme, units=units)
    fig.axes = []
    fig._spans = []
    return fig

Figure

Figure

Figure(
    figsize: tuple[float, float] = DEFAULT_FIGSIZE,
    nrows: int = 1,
    ncols: int = 1,
    sharex: bool = False,
    sharey: bool = False,
    projection: str | None = None,
    theme=None,
    units: str = "pt",
    width_ratios=None,
    height_ratios=None,
)

A figure: an output canvas holding a grid of [Axes].

figsize is the (width, height) of the canvas in points by default (units="pt"). Sizing in points lets you reason about a plot directly against its font scale — e.g. the default 250x200 pt figure with a 10 pt font. That default is a single journal column wide (~3.5 in), so a figure comes out at publication size instead of needing to be scaled down to one. Pass units="in", "cm" or "mm" to give the size in another unit (Nature's widths are 89 mm / 183 mm).

Source code in python/pyplotrs/_figure.py
def __init__(self, figsize: tuple[float, float] = DEFAULT_FIGSIZE, nrows: int = 1,
             ncols: int = 1, sharex: bool = False, sharey: bool = False,
             projection: str | None = None, theme=None, units: str = "pt",
             width_ratios=None, height_ratios=None) -> None:
    self.figsize = figsize          # raw, as given (back-compat / repr)
    self.units = units
    self.size_pt = _figsize_to_points(figsize, units)  # resolved, canonical
    self.nrows = nrows
    self.ncols = ncols
    self.sharex = sharex
    self.sharey = sharey
    self.theme: Theme = _theme.get(theme)
    self.suptitle: str | None = None
    self._legend: dict | None = None
    # Relative column widths / row heights. Normalized in the Rust solver, so
    # only the proportions matter; `None` means an even grid.
    self._width_ratios = None if width_ratios is None else [float(v) for v in width_ratios]
    self._height_ratios = (
        None if height_ratios is None else [float(v) for v in height_ratios])
    make = _axes_class(projection)
    self.axes = [self._adopt(make(self.theme)) for _ in range(nrows * ncols)]
    # Spanning placement (GridSpec / subplot_mosaic): one
    # (row, col, rowspan, colspan) per axes, or None for a uniform grid.
    self._spans: list[tuple[int, int, int, int]] | None = None

set

set(*, suptitle: str | None = None) -> 'Figure'
Source code in python/pyplotrs/_figure.py
def set(self, *, suptitle: str | None = None) -> "Figure":
    if suptitle is not None:
        self.suptitle = suptitle
    return self

add_gridspec

add_gridspec(
    nrows: int,
    ncols: int,
    *,
    width_ratios=None,
    height_ratios=None
) -> "GridSpec"

Switch this figure to spanning-subplot mode over an nrows x ncols grid and return a GridSpec. Populate it with add_subplot; existing auto-created axes are cleared.

width_ratios/height_ratios weight the columns and rows (see subplots).

Source code in python/pyplotrs/_figure.py
def add_gridspec(self, nrows: int, ncols: int, *,
                 width_ratios=None, height_ratios=None) -> "GridSpec":
    """Switch this figure to spanning-subplot mode over an ``nrows`` x
    ``ncols`` grid and return a ``GridSpec``. Populate it with
    ``add_subplot``; existing auto-created axes are cleared.

    ``width_ratios``/``height_ratios`` weight the columns and rows (see
    ``subplots``)."""
    self.nrows = nrows
    self.ncols = ncols
    self.axes = []
    self._spans = []
    if width_ratios is not None:
        self._width_ratios = [float(v) for v in width_ratios]
    if height_ratios is not None:
        self._height_ratios = [float(v) for v in height_ratios]
    return GridSpec(nrows, ncols)

add_subplot

add_subplot(
    spec, *, projection: str | None = None
) -> "Axes"

Add an axes at a GridSpec slice (e.g. gs[0, :] or gs[1:, 0]). Returns the new axes.

Source code in python/pyplotrs/_figure.py
def add_subplot(self, spec, *, projection: str | None = None) -> "Axes":
    """Add an axes at a ``GridSpec`` slice (e.g. ``gs[0, :]`` or
    ``gs[1:, 0]``). Returns the new axes."""
    if self._spans is None:
        self._spans = []
    r0, c0, rs, cs = spec
    ax = self._adopt(_axes_class(projection)(self.theme))
    self.axes.append(ax)
    self._spans.append((r0, c0, rs, cs))
    return ax

legend

legend(
    *,
    loc: str = "right",
    ncol: int = 1,
    title: str | None = None,
    frameon: bool = True,
    fontsize: float | None = None
) -> "Figure"

Enable a single figure-level legend, collecting the labeled marks of every axes into one box placed in a reserved column to the right of the grid. Unlike Axes.legend, this is laid out as its own region and so can never overlap the data. loc currently supports "right".

ncol/title/frameon/fontsize work as on Axes.legend; the reserved column is measured from them, so a two-column figure legend takes a wider, shorter band.

Source code in python/pyplotrs/_figure.py
def legend(self, *, loc: str = "right", ncol: int = 1,
           title: str | None = None, frameon: bool = True,
           fontsize: float | None = None) -> "Figure":
    """Enable a single figure-level legend, collecting the labeled marks of
    every axes into one box placed in a reserved column to the right of the
    grid. Unlike ``Axes.legend``, this is laid out as its own region and
    so can never overlap the data. ``loc`` currently supports ``"right"``.

    ``ncol``/``title``/``frameon``/``fontsize`` work as on
    ``Axes.legend``; the reserved column is measured from them, so a
    two-column figure legend takes a wider, shorter band."""
    self._legend = {
        "loc": loc, "ncol": int(ncol), "title": title,
        "frameon": bool(frameon),
        "fontsize": None if fontsize is None else float(fontsize),
    }
    return self

colorbar

colorbar(
    mappable: "Mappable",
    *,
    label: str | None = None,
    orientation: str = "vertical",
    shrink: float = 1.0,
    ticks=None,
    format=None
) -> "Figure"

Attach a colorbar for mappable (from Axes.imshow or a colormapped Axes.scatter) in a reserved band beside its axes. The tick scale follows the mappable's norm (e.g. log ticks for a LogNorm).

orientation="horizontal" puts the bar beneath the plot instead, in its own reserved band below the x-axis label. shrink scales the strip's length as a fraction of the plot extent, centered. ticks pins the tick values and format accepts anything pyplotrs.ticker does - a formatter, a "{x:.2f}" template, or a callable.

Source code in python/pyplotrs/_figure.py
def colorbar(self, mappable: "Mappable", *, label: str | None = None,
             orientation: str = "vertical", shrink: float = 1.0,
             ticks=None, format=None) -> "Figure":
    """Attach a colorbar for ``mappable`` (from ``Axes.imshow`` or a
    colormapped ``Axes.scatter``) in a reserved band beside its axes.
    The tick scale follows the mappable's ``norm`` (e.g. log ticks for
    a [`LogNorm`][pyplotrs.norms.LogNorm]).

    ``orientation="horizontal"`` puts the bar beneath the plot instead, in
    its own reserved band below the x-axis label. ``shrink`` scales the
    strip's length as a fraction of the plot extent, centered. ``ticks``
    pins the tick values and ``format`` accepts anything
    [`pyplotrs.ticker`][pyplotrs.ticker] does - a formatter, a ``"{x:.2f}"`` template, or
    a callable."""
    if orientation not in ("vertical", "horizontal"):
        raise ValueError(
            f'orientation must be "vertical" or "horizontal", got {orientation!r}')
    mappable.ax._colorbar = {
        "cmap": mappable.cmap,
        "vmin": mappable.vmin,
        "vmax": mappable.vmax,
        "label": label,
        "norm": mappable.norm,
        "orientation": orientation,
        "shrink": float(shrink),
        "ticks": None if ticks is None else [float(v) for v in ticks],
        "format": format,
    }
    return self

save

save(
    path: str,
    *,
    dpi: float = 200.0,
    tagged: bool = False,
    transparent: bool = False,
    title: str | None = None,
    alt: str | None = None
) -> None

Save to path; the format is inferred from the extension (.pdf, .svg, .png, or .html/.htm).

transparent=True drops the page behind the figure, in favor of an alpha channel. That means two things at once: the white fill .png would otherwise paint, and — for a theme that states a page of its own, such as themes.dark — that fill too, in every format. A dark figure saved this way keeps its light text and rules but carries no background, ready to composite onto whatever is behind it. .pdf/.svg/.html under a theme with no stated page paint no background to begin with, so for them the flag changes nothing.

.html writes a single self-contained page with the figure inlined as vector SVG (real selectable text, embedded fonts, nothing fetched at view time) — handy for dropping a chart straight into a web page or report. If any label contains $...$ math, that math is re-rendered by an inlined copy of MathJax (SVG output) so it is selectable and copyable as LaTeX/MathML (right-click → Show Math As); the page stays fully offline. For a 3D figure the .html is instead a dependency-free Canvas2D viewer you can orbit (drag), zoom (scroll) and pan (shift-drag).

dpi controls the resolution of raster (.png) output and is recorded in the file's physical-size metadata. PDF, SVG and HTML pages are resolution-independent, but any image inside one - a heatmap, a filled contour - is not, and dpi sets its resolution there too. That matters for a journal: an image embedded at the 200 dpi default is below most submission minimums, and pinning it there regardless of what was asked for meant a 600-dpi PDF carried a third of the detail a 600-dpi PNG of the same figure did.

tagged=True (.pdf only) writes a tagged, accessible PDF: the whole chart becomes one Figure structure element with alt text (auto-derived from the titles/labels when omitted) so screen readers can announce it, plus a document title and language. For .html the same auto-derived title/alt label the page and the inline SVG (role="img"), and title/alt may be overridden here too.

Source code in python/pyplotrs/_figure.py
def save(self, path: str, *, dpi: float = 200.0, tagged: bool = False,
         transparent: bool = False, title: str | None = None,
         alt: str | None = None) -> None:
    """Save to ``path``; the format is inferred from the extension
    (``.pdf``, ``.svg``, ``.png``, or ``.html``/``.htm``).

    ``transparent=True`` drops the page behind the figure, in favor of an
    alpha channel. That means two things at once: the white fill ``.png``
    would otherwise paint, and — for a theme that states a page of its own,
    such as [`themes.dark`][pyplotrs.theme] — that fill too, in every
    format. A dark figure saved this way keeps its light text and rules but
    carries no background, ready to composite onto whatever is behind it.
    ``.pdf``/``.svg``/``.html`` under a theme with no stated page paint no
    background to begin with, so for them the flag changes nothing.

    ``.html`` writes a single self-contained page with the figure inlined as
    vector SVG (real selectable text, embedded fonts, nothing fetched at view
    time) — handy for dropping a chart straight into a web page or report.
    If any label contains ``$...$`` **math**, that math is re-rendered by an
    inlined copy of **MathJax** (SVG output) so it is selectable and copyable
    as LaTeX/MathML (right-click → *Show Math As*); the page stays fully
    offline. For a **3D figure** the ``.html`` is instead a dependency-free
    Canvas2D viewer you can orbit (drag), zoom (scroll) and pan (shift-drag).

    ``dpi`` controls the resolution of raster (``.png``) output and is
    recorded in the file's physical-size metadata. PDF, SVG and HTML pages
    are resolution-independent, but any **image** inside one - a heatmap,
    a filled contour - is not, and ``dpi`` sets its resolution there too.
    That matters for a journal: an image embedded at the 200 dpi default
    is below most submission minimums, and pinning it there regardless of
    what was asked for meant a 600-dpi PDF carried a third of the detail a
    600-dpi PNG of the same figure did.

    ``tagged=True`` (``.pdf`` only) writes a tagged, accessible PDF: the
    whole chart becomes one ``Figure`` structure element with ``alt`` text
    (auto-derived from the titles/labels when omitted) so screen readers can
    announce it, plus a document ``title`` and language. For ``.html`` the
    same auto-derived ``title``/``alt`` label the page and the inline SVG
    (``role="img"``), and ``title``/``alt`` may be overridden here too."""
    path_str = str(path)
    # `dpi=-5`, `dpi=0` and `dpi=nan` all rendered silently - the negative
    # and the zero fell through to a 72 dpi default deep in the raster
    # path, so you got a small figure and no indication the argument had
    # been discarded. The 4 GB upper bound was already guarded with a good
    # message, so only the bottom was open.
    dpi = float(dpi)
    if not dpi > 0.0 or dpi != dpi or dpi == float("inf"):
        raise ValueError(f"dpi must be a positive, finite number; got {dpi!r}")
    ext = path_str.rsplit(".", 1)[-1].lower() if "." in path_str else ""
    if ext in ("html", "htm"):
        auto_title, auto_alt = self._accessible_text()
        if self._has_3d():
            from ._html3d import figure_to_interactive_html
            doc = figure_to_interactive_html(self, title or auto_title, alt or auto_alt)
        else:
            # Build through the capture proxy: if any label carries $...$,
            # render the math with MathJax (selectable/copyable, offline);
            # otherwise inline the baked-vector SVG as before.
            placements: list = []
            svg = self._build_scene(capture=placements,
                                    transparent=transparent).to_svg(dpi)
            if placements:
                from ._htmlmath import figure_to_math_html
                doc = figure_to_math_html(svg, placements, self.size_pt,
                                          title or auto_title, alt or auto_alt,
                                          page=self._page_css())
            else:
                doc = _svg_to_html(svg, title or auto_title, alt or auto_alt,
                                   page=self._page_css())
        with open(path_str, "w", encoding="utf-8") as f:
            f.write(doc)
        return

    scene = self._build_scene(transparent=transparent)
    if ext == "pdf":
        if tagged:
            auto_title, auto_alt = self._accessible_text()
            data = scene.to_pdf(True, title or auto_title, alt or auto_alt, dpi)
        else:
            data = scene.to_pdf(dpi=dpi)
        with open(path_str, "wb") as f:
            f.write(data)
    elif ext == "svg":
        with open(path_str, "w", encoding="utf-8") as f:
            f.write(scene.to_svg(dpi))
    elif ext == "png":
        with open(path_str, "wb") as f:
            f.write(scene.to_png(dpi, transparent))
    else:
        raise ValueError(
            f"Unsupported file extension for {path_str!r}; "
            "expected .pdf, .svg, .png, or .html"
        )

GridSpec

GridSpec(nrows: int, ncols: int)

A lightweight grid geometry for spanning subplots. Create with a figure's row/column count, then slice it (NumPy-style) to place an axes across a range of rows/columns via Figure.add_subplot.

Source code in python/pyplotrs/_figure.py
def __init__(self, nrows: int, ncols: int) -> None:
    self.nrows = nrows
    self.ncols = ncols

Axes

Axes

Axes(theme: Theme | None = None)

Bases: _AxesBase

A single set of axes: a coordinate system plus a stack of marks.

Source code in python/pyplotrs/axes.py
def __init__(self, theme: Theme | None = None) -> None:
    self._init_common(theme)
    self._marks: list[dict] = []
    self._annotations: list[dict] = []
    self._colorbar: dict | None = None
    self._xlabel: str | None = None
    self._ylabel: str | None = None
    # View limits, populated during layout; None => auto from data.
    self._xlim: tuple[float, float] | None = None
    self._ylim: tuple[float, float] | None = None
    # Axis scales (linear by default; LogScale etc. set via ``set(xscale=)``).
    self._xscale: _scales.Scale = _scales.LinearScale()
    self._yscale: _scales.Scale = _scales.LinearScale()
    # Autoscale margin as a fraction of the data span; None => _DATA_PAD.
    self._xmargin: float | None = None
    self._ymargin: float | None = None
    # Descending view without pinning numbers (see ``_ranges``).
    self._xinverted = False
    self._yinverted = False
    # X tick label angle: "auto" rotates only on a collision (see
    # `_x_tick_angle`), a number forces it, 0 forces flat.
    self._xtickrotation = "auto"
    self._x_tick_deg = 0.0
    # Shared offset/multiplier lifted out of the tick labels, written once
    # at the end of the axis. Computed in `_bands` (which always runs
    # before `_draw`, as the layout depends on it) and rendered there.
    self._x_offset_text = ""
    self._y_offset_text = ""
    # Minor ticks. Non-linear scales subdivide on their own; on a linear
    # axis there is no canonical subdivision, so it is opt-in and the count
    # is how many minor intervals fill one major one.
    self._xminor: int = 0
    self._yminor: int = 0
    # Per-axes tick styling, overriding the theme when not None.
    self._tick_direction: str = "out"
    self._tick_length: float | None = None
    # Reference primitives (axhline/axvline/axspan/axline) and free-form
    # patches (rectangle/circle/...) live outside the data-mark stack: refs
    # never drive autoscaling, patches contribute a bbox. All default empty
    # so figures that use none stay byte-identical to before Phase D.
    self._refs: list[dict] = []
    self._patches: list[dict] = []
    # Axis/tick/grid overrides (all None/False => theme + scale defaults).
    self._grid_override: bool | None = None
    self._aspect: str | None = None
    self._frame_off: bool = False
    self._xticks_manual: list[float] | None = None
    self._yticks_manual: list[float] | None = None
    self._xticklabels_manual: list[str] | None = None
    self._yticklabels_manual: list[str] | None = None
    self._xformatter = None
    self._yformatter = None
    # Overlays sharing this axes' cell (Phase F): a twin y/x axis, inset
    # child axes (fractional sub-rects), and functional secondary axes.
    self._twinx: "Axes | None" = None
    self._twiny: "Axes | None" = None
    self._insets: list[tuple["Axes", tuple]] = []
    self._secondary: list[dict] = []
    # Width a secondary y axis on the right claimed out of the shared cbar
    # band, recomputed by `_bands` on every render.
    self._sec_right_w: float = 0.0
    self._is_twin: bool = False  # a twin skips its own facecolor/grid

line

line(
    xs,
    ys,
    *,
    label: str | None = None,
    color=None,
    linewidth: float | None = None,
    alpha: float = 1.0,
    linestyle: str = "solid",
    marker: str | None = None,
    markersize: float = 5.0,
    simplify: bool = True,
    zorder: float = 0.0
) -> "Axes"

Plot a polyline through (xs, ys).

color may be None (cycle the palette), "C0".."C7", or an (r, g, b[, a]) tuple. linestyle is one of solid/dashed/ dotted/dashdot (or none for markers only). An optional marker draws a glyph at each vertex.

simplify (default True) collapses runs of near-collinear vertices in device space - visually identical output, far smaller and faster vector export on dense data. Set False to keep every vertex exactly (e.g. when the polyline is the data being exported).

Source code in python/pyplotrs/axes.py
def line(self, xs, ys, *, label: str | None = None, color=None,
         linewidth: float | None = None, alpha: float = 1.0,
         linestyle: str = "solid", marker: str | None = None,
         markersize: float = 5.0, simplify: bool = True, zorder: float = 0.0) -> "Axes":
    """Plot a polyline through ``(xs, ys)``.

    ``color`` may be ``None`` (cycle the palette), ``"C0".."C7"``, or an
    ``(r, g, b[, a])`` tuple. ``linestyle`` is one of ``solid``/``dashed``/
    ``dotted``/``dashdot`` (or ``none`` for markers only). An optional
    ``marker`` draws a glyph at each vertex.

    ``simplify`` (default ``True``) collapses runs of near-collinear
    vertices in device space - visually identical output, far smaller and
    faster vector export on dense data. Set ``False`` to keep every vertex
    exactly (e.g. when the polyline *is* the data being exported).
    """
    _check_marker(marker)
    xs, ys = self._coords(xs, "x"), self._coords(ys, "y")
    _require_same_length("line", x=xs, y=ys)
    self._marks.append({
        "zorder": float(zorder),
        "kind": "line",
        "xs": xs,
        "ys": ys,
        "label": label,
        "color": self._mark_color(color, alpha),
        "linewidth": self._theme.line_width if linewidth is None else float(linewidth),
        "linestyle": linestyle,
        "marker": marker,
        "markersize": float(markersize),
        "simplify": bool(simplify),
    })
    return self

scatter

scatter(
    xs,
    ys,
    *,
    label: str | None = None,
    color=None,
    markersize: float | None = None,
    alpha: float = 1.0,
    marker: str = "o",
    edgecolor=None,
    edgewidth: float = 1.0,
    size: float | None = None,
    c=None,
    cmap="viridis",
    norm=None,
    vmin: float | None = None,
    vmax: float | None = None,
    zorder: float = 0.0
)

Scatter markers at (xs, ys).

markersize is the marker diameter in points, the same unit every other mark uses. size is accepted for matplotlib compatibility and means area in pt² (so size=36 and markersize=6 agree).

Pass c (a per-point array) to color markers by value through cmap and norm (vmin/vmax set the range; norm="log" or a pyplotrs.norms instance for non-linear). Returns a colorbar handle in that case, else self.

Source code in python/pyplotrs/axes.py
def scatter(self, xs, ys, *, label: str | None = None, color=None,
            markersize: float | None = None, alpha: float = 1.0,
            marker: str = "o", edgecolor=None, edgewidth: float = 1.0,
            size: float | None = None,
            c=None, cmap="viridis", norm=None, vmin: float | None = None,
            vmax: float | None = None, zorder: float = 0.0):
    """Scatter markers at ``(xs, ys)``.

    ``markersize`` is the marker **diameter in points**, the same unit every
    other mark uses. ``size`` is accepted for matplotlib compatibility and
    means *area* in pt² (so ``size=36`` and ``markersize=6`` agree).

    Pass ``c`` (a per-point array) to color markers by value through ``cmap``
    and ``norm`` (``vmin``/``vmax`` set the range; ``norm="log"`` or a
    [`pyplotrs.norms`][pyplotrs.norms] instance for non-linear). Returns a colorbar handle
    in that case, else ``self``."""
    _check_marker(marker)
    xs = self._coords(xs, "x")
    ys = self._coords(ys, "y")
    _require_same_length("scatter", x=xs, y=ys)
    mark = {
        "zorder": float(zorder),
        "kind": "scatter",
        "xs": xs,
        "ys": ys,
        "label": label,
        # A colormapped scatter's per-point colors replace this, but it is
        # still the legend swatch and the fallback, so it must follow the theme.
        "color": (self._mark_color(color, alpha) if c is None
                  else self._theme.text_color),
        "markersize": self._marker_diameter(markersize, size),
        "marker": marker,
        "edgecolor": None if edgecolor is None else self._theme.resolve(edgecolor),
        "edgewidth": float(edgewidth),
        "colors": None,
    }
    self._marks.append(mark)
    if c is None:
        return self
    # Colormapped scatter: precompute one RGBA per point and hand back a
    # mappable so ``fig.colorbar(sc)`` matches the color scale.
    #
    # `alpha` goes through here too. It used to be folded in only on the
    # `c is None` branch above, so `scatter(c=v, alpha=0.3)` - the standard
    # way to show density in an overplotted cloud - drew fully opaque
    # points, and a crowded region took the color of whichever point
    # happened to be drawn last instead of a blend. `hexbin`, `pcolormesh`
    # and `imshow` all already honored it; this was an omission.
    cvals = _to_f64(c)
    nrm = _norms.get(norm, vmin, vmax).autoscale(cvals)
    cm = _colormaps.get_cmap(cmap)
    mark["colors"] = _rgba_values(cvals, cm, nrm, alpha)
    return Mappable(self, cm, nrm.vmin, nrm.vmax, norm=nrm)

bar

bar(
    x,
    height,
    *,
    width: float = 0.5,
    bottom=0.0,
    color=None,
    alpha: float = 1.0,
    label: str | None = None,
    edgecolor=None,
    zorder: float = 0.0
) -> "Axes"

Draw vertical bars of the given height at positions x. x may be strings (categories), which set a categorical x-axis.

width is a data extent in x units, not a stroke width: at the default 0.5 a bar fills half the gap to its neighbor. This is narrower than matplotlib's 0.8 on purpose - the gap is what makes bars read as discrete categories at the default single-column figure size.

Source code in python/pyplotrs/axes.py
def bar(self, x, height, *, width: float = 0.5, bottom=0.0, color=None,
        alpha: float = 1.0, label: str | None = None, edgecolor=None, zorder: float = 0.0) -> "Axes":
    """Draw vertical bars of the given ``height`` at positions ``x``. ``x``
    may be strings (categories), which set a categorical x-axis.

    ``width`` is a **data extent** in x units, not a stroke width: at the
    default 0.5 a bar fills half the gap to its neighbor. This is narrower
    than matplotlib's 0.8 on purpose - the gap is what makes bars read as
    discrete categories at the default single-column figure size."""
    xs = self._coords(x, "x")
    heights = [float(v) for v in height]
    bottoms = _as_seq(bottom, len(xs))
    _require_same_length("bar", x=xs, height=heights)
    self._marks.append({
        "zorder": float(zorder),
        "kind": "bar",
        "xs": xs,
        "heights": heights,
        "bottoms": bottoms,
        "width": float(width),
        # Bars rest on their bases. Only the extreme bases can ever bind,
        # so two values stand in for all of them.
        "sticky_y": [min(bottoms), max(bottoms)] if bottoms else [],
        "color": self._mark_color(color, alpha),
        "label": label,
        "edgecolor": None if edgecolor is None else self._theme.resolve(edgecolor),
    })
    return self

barh

barh(
    y,
    width,
    *,
    height: float = 0.8,
    left=0.0,
    color=None,
    alpha: float = 1.0,
    label: str | None = None,
    edgecolor=None,
    zorder: float = 0.0
) -> "Axes"

Horizontal bars of the given width at vertical positions y. y may be strings (categories), which set a categorical y-axis.

Source code in python/pyplotrs/axes.py
def barh(self, y, width, *, height: float = 0.8, left=0.0, color=None,
         alpha: float = 1.0, label: str | None = None, edgecolor=None, zorder: float = 0.0) -> "Axes":
    """Horizontal bars of the given ``width`` at vertical positions ``y``.
    ``y`` may be strings (categories), which set a categorical y-axis."""
    ys = self._coords(y, "y")
    widths = [float(v) for v in width]
    lefts = _as_seq(left, len(ys))
    _require_same_length("barh", y=ys, width=widths)
    self._marks.append({
        "zorder": float(zorder),
        "kind": "barh", "ys": ys, "widths": widths,
        "lefts": lefts, "height": float(height),
        "sticky_x": [min(lefts), max(lefts)] if lefts else [],
        "color": self._mark_color(color, alpha), "label": label,
        "edgecolor": None if edgecolor is None else self._theme.resolve(edgecolor),
    })
    return self

hist

hist(
    data,
    *,
    bins: int = 10,
    color=None,
    alpha: float = 1.0,
    label: str | None = None,
    range=None,
    density: bool = False,
    zorder: float = 0.0
) -> "Axes"

Bin data into bins equal-width bins and draw the histogram.

The binning loop runs in Rust (_core.histogram), matching what hist2d already did.

Source code in python/pyplotrs/axes.py
def hist(self, data, *, bins: int = 10, color=None, alpha: float = 1.0,
         label: str | None = None, range=None, density: bool = False, zorder: float = 0.0) -> "Axes":
    """Bin ``data`` into ``bins`` equal-width bins and draw the histogram.

    The binning loop runs in Rust (``_core.histogram``), matching what
    ``hist2d`` already did."""
    # `max(int(bins), 1)` used to silently promote 0 and negatives to a
    # single bin, so `hist(data, bins=0)` drew one wide bar rather than
    # complaining about the argument.
    if int(bins) < 1:
        raise ValueError(f"hist needs at least one bin; got bins={bins!r}")
    vals = _to_f64(data)
    if not len(vals):
        vals = array("d", (0.0, 1.0))
    span = (float(range[0]), float(range[1])) if range else None
    edges, counts = _core.histogram(vals, int(bins), span, bool(density))
    self._marks.append({
        "zorder": float(zorder),
        "kind": "hist",
        "edges": edges,
        "counts": counts,
        "sticky_y": [0.0],
        "color": self._mark_color(color, alpha),
        "label": label,
    })
    return self

boxplot

boxplot(
    data,
    *,
    positions=None,
    widths: float = 0.5,
    color=None,
    showfliers: bool = True,
    alpha: float = 1.0,
    label: str | None = None,
    zorder: float = 0.0
) -> "Axes"

Box-and-whisker plot. data is a list of numeric arrays (one box each); positions default to 1..n.

Source code in python/pyplotrs/axes.py
def boxplot(self, data, *, positions=None, widths: float = 0.5, color=None,
            showfliers: bool = True, alpha: float = 1.0,
            label: str | None = None, zorder: float = 0.0) -> "Axes":
    """Box-and-whisker plot. ``data`` is a list of numeric arrays (one box
    each); ``positions`` default to ``1..n``."""
    groups = data if _is_2d(data) else [data]
    stats = [_boxstats([float(v) for v in g]) for g in groups]
    positions = ([float(p) for p in positions] if positions is not None
                 else [float(i + 1) for i in range(len(groups))])
    # The `max` keeps a box wider than one slot from clipping (see _BOX_SLOT).
    slot = max(_BOX_SLOT, float(widths) / 2.0)
    self._marks.append({
        "zorder": float(zorder),
        "kind": "boxplot", "stats": stats, "positions": positions,
        "width": float(widths), "color": self._mark_color(color, alpha),
        "showfliers": showfliers, "label": label,
        "slot": slot,
        "sticky_x": ([min(positions) - slot, max(positions) + slot]
                     if positions else []),
    })
    return self

violinplot

violinplot(
    data,
    *,
    positions=None,
    widths: float = 0.5,
    color=None,
    points: int = 128,
    alpha: float = 1.0,
    label: str | None = None,
    zorder: float = 0.0
) -> "Axes"

Violin plot: a mirrored Gaussian-KDE density for each array in data (KDE computed in Rust, no SciPy dependency).

Source code in python/pyplotrs/axes.py
def violinplot(self, data, *, positions=None, widths: float = 0.5, color=None,
               points: int = 128, alpha: float = 1.0,
               label: str | None = None, zorder: float = 0.0) -> "Axes":
    """Violin plot: a mirrored Gaussian-KDE density for each array in
    ``data`` (KDE computed in Rust, no SciPy dependency)."""
    groups = data if _is_2d(data) else [data]
    positions = ([float(p) for p in positions] if positions is not None
                 else [float(i + 1) for i in range(len(groups))])
    violins = []
    for g in groups:
        vals = [float(v) for v in g if math.isfinite(v)]
        if not vals:
            violins.append(([], []))
            continue
        # Over the data range exactly, as matplotlib does. Evaluating 15%
        # past each end drew KDE tails the sample never reached, and - the
        # whole grid fed autoscaling - padded 5% beyond those on top.
        lo, hi = _expand_degenerate(min(vals), max(vals))
        grid = [lo + (hi - lo) * i / (points - 1) for i in range(points)]
        dens = _core.gaussian_kde(vals, grid, 0.0)
        violins.append((grid, dens))
    self._marks.append({
        "zorder": float(zorder),
        "kind": "violin", "violins": violins, "positions": positions,
        "width": float(widths), "color": self._mark_color(color, alpha),
        "label": label,
    })
    return self

pie

pie(
    sizes,
    *,
    labels=None,
    colors=None,
    startangle: float = 90.0,
    radius: float = 1.0,
    alpha: float = 1.0,
    zorder: float = 0.0
) -> "Axes"

Pie chart of sizes (auto-normalized). Turns the frame off and fixes an equal aspect so wedges stay circular.

This is the one mark with no scalar label: its labels are per-wedge, so they come from labels - which is also what feeds legend.

Source code in python/pyplotrs/axes.py
def pie(self, sizes, *, labels=None, colors=None, startangle: float = 90.0,
        radius: float = 1.0, alpha: float = 1.0, zorder: float = 0.0) -> "Axes":
    """Pie chart of ``sizes`` (auto-normalized). Turns the frame off and fixes
    an equal aspect so wedges stay circular.

    This is the one mark with no scalar ``label``: its labels are per-wedge,
    so they come from ``labels`` - which is also what feeds ``legend``.
    """
    vals = [float(v) for v in sizes]
    total = sum(vals) or 1.0
    wedges = []
    ang = math.radians(startangle)
    for i, v in enumerate(vals):
        sweep = 2.0 * math.pi * v / total
        col = colors[i] if colors else None
        wedges.append({"a0": ang, "a1": ang + sweep,
                       "color": self._mark_color(col, alpha),
                       "label": labels[i] if labels else None})
        ang += sweep
    self._marks.append({"zorder": float(zorder), "kind": "pie", "wedges": wedges, "radius": float(radius)})
    self._frame_off = True
    self._aspect = "equal"
    # Limits are the pie's own bounding box, so the equal-aspect square *is*
    # the pie. Room for the slice labels is taken out of the drawn radius at
    # draw time, where they can be measured (`_pie_geometry`); padding the
    # limits by a guessed factor instead both wasted the cell and still
    # clipped labels the guess was too small for.
    self._xlim = (-radius, radius)
    self._ylim = (-radius, radius)
    return self

fill_between

fill_between(
    xs,
    y1,
    y2=0.0,
    *,
    color=None,
    alpha: float = 0.3,
    label: str | None = None,
    zorder: float = 0.0
) -> "Axes"

Fill the band between y1 and y2 across xs.

Source code in python/pyplotrs/axes.py
def fill_between(self, xs, y1, y2=0.0, *, color=None, alpha: float = 0.3,
                 label: str | None = None, zorder: float = 0.0) -> "Axes":
    """Fill the band between ``y1`` and ``y2`` across ``xs``."""
    xs = self._coords(xs, "x")
    y1 = _to_f64(y1)
    _require_same_length("fill_between", x=xs, y1=y1)
    self._marks.append({
        "zorder": float(zorder),
        "kind": "fill",
        "orient": "y",
        "xs": xs,
        "y1": y1,
        "y2": _to_f64(_as_seq(y2, len(xs))),
        "color": self._next_color(color),
        "alpha": float(alpha),
        "label": label,
    })
    return self

fill_betweenx

fill_betweenx(
    ys,
    x1,
    x2=0.0,
    *,
    color=None,
    alpha: float = 0.3,
    label: str | None = None,
    zorder: float = 0.0
) -> "Axes"

Fill the band between x1 and x2 across ys - the transpose of fill_between, for bands around a horizontal profile.

Source code in python/pyplotrs/axes.py
def fill_betweenx(self, ys, x1, x2=0.0, *, color=None, alpha: float = 0.3,
                  label: str | None = None, zorder: float = 0.0) -> "Axes":
    """Fill the band between ``x1`` and ``x2`` across ``ys`` - the transpose
    of ``fill_between``, for bands around a horizontal profile."""
    ys = self._coords(ys, "y")
    x1 = _to_f64(x1)
    _require_same_length("fill_betweenx", y=ys, x1=x1)
    self._marks.append({
        "zorder": float(zorder),
        "kind": "fill",
        "orient": "x",
        "ys": ys,
        "y1": x1,
        "y2": _to_f64(_as_seq(x2, len(ys))),
        "color": self._next_color(color),
        "alpha": float(alpha),
        "label": label,
    })
    return self

errorbar

errorbar(
    xs,
    ys,
    *,
    yerr=None,
    xerr=None,
    color=None,
    label: str | None = None,
    marker: str | None = "o",
    markersize: float = 5.0,
    linewidth: float | None = None,
    alpha: float = 1.0,
    capsize: float = 3.0,
    linestyle: str = "solid",
    zorder: float = 0.0
) -> "Axes"

Plot (xs, ys) with symmetric yerr/xerr error bars.

Source code in python/pyplotrs/axes.py
def errorbar(self, xs, ys, *, yerr=None, xerr=None, color=None, label: str | None = None,
             marker: str | None = "o", markersize: float = 5.0,
             linewidth: float | None = None, alpha: float = 1.0,
             capsize: float = 3.0, linestyle: str = "solid", zorder: float = 0.0) -> "Axes":
    """Plot ``(xs, ys)`` with symmetric ``yerr``/``xerr`` error bars."""
    _check_marker(marker)
    xs = [float(x) for x in xs]
    ys = [float(y) for y in ys]
    _require_same_length("errorbar", x=xs, y=ys)
    n = len(xs)
    self._marks.append({
        "zorder": float(zorder),
        "kind": "errorbar",
        "xs": xs,
        "ys": ys,
        "yerr": _as_seq(yerr, n) if yerr is not None else None,
        "xerr": _as_seq(xerr, n) if xerr is not None else None,
        "color": self._mark_color(color, alpha),
        "label": label,
        "marker": marker,
        "markersize": float(markersize),
        "linewidth": self._theme.line_width if linewidth is None else float(linewidth),
        "capsize": float(capsize),
        "linestyle": linestyle,
    })
    return self

step

step(
    xs,
    ys,
    *,
    where: str = "pre",
    color=None,
    linewidth: float | None = None,
    alpha: float = 1.0,
    linestyle: str = "solid",
    label: str | None = None,
    zorder: float = 0.0
) -> "Axes"

Step plot through (xs, ys); where is pre/post/mid.

Source code in python/pyplotrs/axes.py
def step(self, xs, ys, *, where: str = "pre", color=None,
         linewidth: float | None = None, alpha: float = 1.0,
         linestyle: str = "solid", label: str | None = None, zorder: float = 0.0) -> "Axes":
    """Step plot through ``(xs, ys)``; ``where`` is ``pre``/``post``/``mid``."""
    xs, ys = _to_f64(xs), _to_f64(ys)
    _require_same_length("step", x=xs, y=ys)
    px, py = _step_points(list(xs), list(ys), where)
    self._marks.append({
        "zorder": float(zorder),
        "kind": "line", "xs": _to_f64(px), "ys": _to_f64(py), "label": label,
        "color": self._mark_color(color, alpha),
        "linewidth": self._theme.line_width if linewidth is None else float(linewidth),
        "linestyle": linestyle, "marker": None, "markersize": 5.0, "simplify": False,
    })
    return self

stairs

stairs(
    values,
    edges=None,
    *,
    color=None,
    linewidth: float | None = None,
    alpha: float = 1.0,
    fill: bool = False,
    baseline: float = 0.0,
    label: str | None = None,
    zorder: float = 0.0
) -> "Axes"

Step outline of values over bin edges (len(values)+1 edges; defaults to 0..n). fill=True fills down to baseline.

Source code in python/pyplotrs/axes.py
def stairs(self, values, edges=None, *, color=None, linewidth: float | None = None,
           alpha: float = 1.0, fill: bool = False, baseline: float = 0.0,
           label: str | None = None, zorder: float = 0.0) -> "Axes":
    """Step outline of ``values`` over bin ``edges`` (``len(values)+1`` edges;
    defaults to ``0..n``). ``fill=True`` fills down to ``baseline``."""
    values = _to_f64(values)
    edges = (_to_f64(edges) if edges is not None
             else _to_f64(range(len(values) + 1)))
    xs = array("d")
    top = array("d")
    for i, v in enumerate(values):
        xs.extend((edges[i], edges[i + 1]))
        top.extend((v, v))
    if fill:
        self._marks.append({
            "zorder": float(zorder),
            "kind": "fill", "xs": xs, "y1": top,
            "y2": _to_f64(_as_seq(baseline, len(xs))),
            "sticky_y": [float(baseline)],
            "color": self._next_color(color), "alpha": 0.3, "label": label,
        })
    else:
        px = array("d", [edges[0]]) + xs + array("d", [edges[-1]])
        py = array("d", [baseline]) + top + array("d", [baseline])
        self._marks.append({
            "zorder": float(zorder),
            "kind": "line", "xs": px, "ys": py, "label": label,
            "sticky_y": [float(baseline)],
            "color": self._mark_color(color, alpha),
            "linewidth": self._theme.line_width if linewidth is None else float(linewidth),
            "linestyle": "solid", "marker": None, "markersize": 5.0, "simplify": False,
        })
    return self

stem

stem(
    xs,
    ys,
    *,
    bottom: float = 0.0,
    color=None,
    alpha: float = 1.0,
    marker: str = "o",
    markersize: float = 5.0,
    label: str | None = None,
    zorder: float = 0.0
) -> "Axes"

Stem plot: a vertical line from bottom to each (x, y) topped by a marker, with a baseline.

Source code in python/pyplotrs/axes.py
def stem(self, xs, ys, *, bottom: float = 0.0, color=None, alpha: float = 1.0,
         marker: str = "o", markersize: float = 5.0,
         label: str | None = None, zorder: float = 0.0) -> "Axes":
    """Stem plot: a vertical line from ``bottom`` to each ``(x, y)`` topped by
    a marker, with a baseline."""
    _check_marker(marker)
    xs, ys = self._coords(xs, "x"), self._coords(ys, "y")
    _require_same_length("stem", x=xs, y=ys)
    self._marks.append({
        "zorder": float(zorder),
        "kind": "stem", "xs": xs, "ys": ys,
        "bottom": float(bottom), "color": self._mark_color(color, alpha),
        "marker": marker, "markersize": float(markersize), "label": label,
    })
    return self

broken_barh

broken_barh(
    xranges,
    yrange,
    *,
    color=None,
    edgecolor=None,
    alpha: float = 1.0,
    label: str | None = None,
    zorder: float = 0.0
) -> "Axes"

Horizontal bars from (xstart, width) pairs, all spanning the vertical yrange = (ymin, height) (e.g. Gantt / interval plots).

Source code in python/pyplotrs/axes.py
def broken_barh(self, xranges, yrange, *, color=None, edgecolor=None,
                alpha: float = 1.0, label: str | None = None,
                zorder: float = 0.0) -> "Axes":
    """Horizontal bars from ``(xstart, width)`` pairs, all spanning the
    vertical ``yrange = (ymin, height)`` (e.g. Gantt / interval plots)."""
    self._marks.append({
        "zorder": float(zorder),
        "kind": "broken_barh",
        "bars": [(float(x0), float(w)) for x0, w in xranges],
        "y0": float(yrange[0]), "h": float(yrange[1]),
        "color": self._next_color(color), "alpha": float(alpha),
        "edgecolor": None if edgecolor is None else self._theme.resolve(edgecolor),
        "label": label,
    })
    return self

eventplot

eventplot(
    positions,
    *,
    orientation: str = "horizontal",
    lineoffsets: float = 1.0,
    linelengths: float = 0.8,
    color=None,
    linewidth: float | None = None,
    alpha: float = 1.0,
    label: str | None = None,
    zorder: float = 0.0
) -> "Axes"

Raster of event marks. positions is a 1D array or a list of rows; each row is offset by lineoffsets and drawn linelengths long (perpendicular to orientation).

Source code in python/pyplotrs/axes.py
def eventplot(self, positions, *, orientation: str = "horizontal",
              lineoffsets: float = 1.0, linelengths: float = 0.8,
              color=None, linewidth: float | None = None, alpha: float = 1.0,
              label: str | None = None, zorder: float = 0.0) -> "Axes":
    """Raster of event marks. ``positions`` is a 1D array or a list of rows;
    each row is offset by ``lineoffsets`` and drawn ``linelengths`` long
    (perpendicular to ``orientation``)."""
    rows = positions if len(positions) and _is_2d(positions) else [positions]
    rows = [_to_f64(r) for r in rows]
    self._marks.append({
        "zorder": float(zorder),
        "kind": "eventplot", "rows": rows, "orientation": orientation,
        "offset": float(lineoffsets), "length": float(linelengths),
        "color": self._mark_color(color, alpha), "label": label,
        "linewidth": self._theme.line_width if linewidth is None else float(linewidth),
    })
    return self

stackplot

stackplot(
    x,
    *ys,
    labels=None,
    colors=None,
    alpha: float = 1.0,
    baseline: float = 0.0,
    zorder: float = 0.0
) -> "Axes"

Stacked area plot: each series in ys is filled on top of the cumulative total of the ones before it.

ys may be passed as separate arrays or as one sequence of arrays, matching stackplot(x, a, b) and stackplot(x, [a, b]).

Source code in python/pyplotrs/axes.py
def stackplot(self, x, *ys, labels=None, colors=None, alpha: float = 1.0,
              baseline: float = 0.0, zorder: float = 0.0) -> "Axes":
    """Stacked area plot: each series in ``ys`` is filled on top of the
    cumulative total of the ones before it.

    ``ys`` may be passed as separate arrays or as one sequence of arrays,
    matching ``stackplot(x, a, b)`` and ``stackplot(x, [a, b])``."""
    if len(ys) == 1 and _is_2d(ys[0]):
        ys = tuple(ys[0])
    xs = _to_f64(x)
    n = len(xs)
    for i, series in enumerate(ys):
        if hasattr(series, "__len__") and len(series) != n:
            raise ValueError(
                f"stackplot needs x and every series of equal length; "
                f"got x={n}, series {i}={len(series)}"
            )
    lower = [float(baseline)] * n
    for i, series in enumerate(ys):
        vals = _as_seq(series, n)
        upper = [lo + v for lo, v in zip(lower, vals)]
        self.fill_between(xs, upper, lower, alpha=alpha, zorder=zorder,
                          color=(colors[i % len(colors)] if colors else None),
                          label=(labels[i] if labels and i < len(labels) else None))
        if i == 0:
            # Only the first band rests on the floor, and the floor is the
            # whole reading: a stack hovering above the spine misreports
            # where the total starts. Sticks at this stack's *own* baseline,
            # not the literal zero matplotlib uses (stackplot.py:135) -
            # `baseline=100` should sit on 100, which matplotlib gets wrong.
            self._marks[-1]["sticky_y"] = [float(baseline)]
        lower = upper
    return self

imshow

imshow(
    data,
    *,
    cmap="viridis",
    vmin: float | None = None,
    vmax: float | None = None,
    norm=None,
    extent=None,
    origin: str = "upper",
    alpha: float = 1.0,
    label: str | None = None,
    zorder: float = 0.0
) -> "Mappable"

Display 2D data as a colormapped image.

data is a sequence of equal-length rows. cmap is a colormap name (see pyplotrs.colormaps) or a Colormap. norm maps values onto the color axis (None linear, "log" for a LogNorm, or any Normalize); the per-pixel lookup runs in Rust. extent is (x0, x1, y0, y1) in data coordinates (default (0, ncols, 0, nrows)); origin is "upper" (row 0 at top) or "lower". Returns a handle for Figure.colorbar.

Source code in python/pyplotrs/axes.py
def imshow(self, data, *, cmap="viridis", vmin: float | None = None,
           vmax: float | None = None, norm=None, extent=None,
           origin: str = "upper", alpha: float = 1.0,
           label: str | None = None, zorder: float = 0.0) -> "Mappable":
    """Display 2D ``data`` as a colormapped image.

    ``data`` is a sequence of equal-length rows. ``cmap`` is a colormap
    name (see [`pyplotrs.colormaps`][pyplotrs.colormaps]) or a ``Colormap``. ``norm`` maps
    values onto the color axis (``None`` linear, ``"log"`` for a
    [`LogNorm`][pyplotrs.norms.LogNorm], or any [`Normalize`][pyplotrs.norms.Normalize]);
    the per-pixel lookup runs in Rust. ``extent`` is ``(x0, x1, y0, y1)`` in
    data coordinates (default ``(0, ncols, 0, nrows)``); ``origin`` is
    ``"upper"`` (row 0 at top) or ``"lower"``. Returns a handle for
    ``Figure.colorbar``.
    """
    # Flattened once here, row-major, and handed to Rust as a buffer; the
    # draw path used to re-flatten a nested list per pixel on every save.
    flat, h, w = _to_f64_grid(data)
    cm = _colormaps.get_cmap(cmap)
    # Resolve the norm to fill vmin/vmax from the data (log norms use only
    # positive samples) and to pick the Rust per-pixel transform code.
    nrm = _norms.get(norm, vmin, vmax)
    nrm.autoscale(flat)
    lo, hi = nrm.vmin, nrm.vmax
    if extent is None:
        extent = (0.0, float(w), 0.0, float(h))
    else:
        extent = (float(extent[0]), float(extent[1]), float(extent[2]), float(extent[3]))
    self._marks.append({
        "zorder": float(zorder),
        "kind": "image",
        "flat": flat,
        "w": w,
        "h": h,
        "cmap": cm,
        "vmin": lo,
        "vmax": hi,
        # The whole norm, not a code: `_draw._norm_lut` decides at draw
        # time whether Rust can run it directly or has to be handed a
        # table with the norm already folded in. Keeping only a code here
        # is what let a `TwoSlopeNorm` be quietly downgraded to linear.
        "norm": nrm,
        "extent": extent,
        # Covers its extent exactly and stops. Per-mark and per-axis, so a
        # line drawn beyond the image still gets its own margin.
        "sticky_x": [extent[0], extent[1]],
        "sticky_y": [extent[2], extent[3]],
        "origin": origin,
        "alpha": float(alpha),
        "label": label,
        # A colormapped mark has no single color, so its legend key is the
        # colormap's midpoint - the one swatch that reads as "this map".
        "color": _with_alpha(cm(0.5), alpha),
    })
    # The colorbar gets the real norm unconditionally - it places its ticks
    # by calling it, so handing it `None` for anything Rust could not run
    # per-pixel drew a linear scale beside a non-linear image.
    return Mappable(self, cm, lo, hi, norm=nrm)

matshow

matshow(data, **kwargs) -> 'Mappable'

Display a matrix with row 0 at the top and one cell per entry.

imshow with the conventions a matrix wants rather than the ones an image wants: origin at the top-left and an equal aspect, so cells stay square.

Source code in python/pyplotrs/axes.py
def matshow(self, data, **kwargs) -> "Mappable":
    """Display a matrix with row 0 at the top and one cell per entry.

    ``imshow`` with the conventions a *matrix* wants rather than the
    ones an *image* wants: origin at the top-left and an equal aspect, so
    cells stay square."""
    kwargs.setdefault("origin", "upper")
    self._aspect = "equal"
    return self.imshow(data, **kwargs)

spy

spy(
    data,
    *,
    markersize: float = 4.0,
    color=None,
    marker: str = "s",
    alpha: float = 1.0,
    label: str | None = None,
    zorder: float = 0.0
) -> "Axes"

Plot the sparsity pattern of data: a marker wherever an entry is nonzero, row 0 at the top.

Source code in python/pyplotrs/axes.py
def spy(self, data, *, markersize: float = 4.0, color=None,
        marker: str = "s", alpha: float = 1.0, label: str | None = None,
        zorder: float = 0.0) -> "Axes":
    """Plot the sparsity pattern of ``data``: a marker wherever an entry is
    nonzero, row 0 at the top."""
    rows = list(data)
    pts_x: list[float] = []
    pts_y: list[float] = []
    for i, row in enumerate(rows):
        for j, v in enumerate(row):
            if v:
                pts_x.append(float(j))
                pts_y.append(float(i))
    self.scatter(pts_x, pts_y, markersize=markersize, color=color,
                 marker=marker, alpha=alpha, label=label, zorder=zorder)
    # Matrix orientation: row 0 on top, and square cells.
    nrows = len(rows)
    ncols = max((len(r) for r in rows), default=0)
    self._xlim = (-0.5, ncols - 0.5)
    self._ylim = (nrows - 0.5, -0.5)
    self._aspect = "equal"
    return self

pcolormesh

pcolormesh(
    *args,
    cmap="viridis",
    norm=None,
    vmin: float | None = None,
    vmax: float | None = None,
    alpha: float = 1.0,
    label: str | None = None,
    zorder: float = 0.0
) -> "Mappable"

Pseudocolor plot of a 2D grid: pcolormesh(C) or pcolormesh(X, Y, C). Regular grids route to the fast Rust image path; irregular grids draw one colored quad per cell.

Source code in python/pyplotrs/axes.py
def pcolormesh(self, *args, cmap="viridis", norm=None, vmin: float | None = None,
               vmax: float | None = None, alpha: float = 1.0,
               label: str | None = None, zorder: float = 0.0) -> "Mappable":
    """Pseudocolor plot of a 2D grid: ``pcolormesh(C)`` or
    ``pcolormesh(X, Y, C)``. Regular grids route to the fast Rust image path;
    irregular grids draw one colored quad per cell."""
    xc, yc, Z = _field_args(args)
    h = len(Z)
    w = len(Z[0]) if Z else 0
    if _is_uniform(xc) and _is_uniform(yc):
        # Cell-centered image: extent spans half a cell beyond edge centers.
        dx = (xc[-1] - xc[0]) / (w - 1) if w > 1 else 1.0
        dy = (yc[-1] - yc[0]) / (h - 1) if h > 1 else 1.0
        extent = (xc[0] - dx / 2, xc[-1] + dx / 2, yc[0] - dy / 2, yc[-1] + dy / 2)
        return self.imshow(Z, cmap=cmap, norm=norm, vmin=vmin, vmax=vmax,
                           extent=extent, origin="lower",
                           alpha=alpha, label=label, zorder=zorder)
    cm, nrm, colors = self._map_colors(
        [v for row in Z for v in row], cmap, norm, vmin, vmax)
    if alpha < 1.0:
        colors = [_with_alpha(c, alpha) for c in colors]
    # Cell edges from coordinate midpoints (irregular quad mesh).
    xe = _edges_from_centers(xc)
    ye = _edges_from_centers(yc)
    quads = []
    for iy in _irange(h):
        for ix in _irange(w):
            quads.append((xe[ix], xe[ix + 1], ye[iy], ye[iy + 1],
                          colors[iy * w + ix]))
    self._marks.append({"zorder": float(zorder),
                        "kind": "quadmesh", "quads": quads, "label": label,
                        "color": _with_alpha(cm(0.5), alpha),
                        "extent": (xe[0], xe[-1], ye[0], ye[-1]),
                        "sticky_x": [xe[0], xe[-1]],
                        "sticky_y": [ye[0], ye[-1]]})
    return Mappable(self, cm, nrm.vmin, nrm.vmax)

pcolor

pcolor(*args, **kwargs) -> 'Mappable'

Alias of pcolormesh.

matplotlib distinguishes the two (pcolor returns a masked-aware PolyCollection, pcolormesh a faster QuadMesh); pyplotrs has only the fast path, and it already chooses per-cell quads over the image path whenever the grid is irregular, so the distinction has nothing left to express.

Source code in python/pyplotrs/axes.py
def pcolor(self, *args, **kwargs) -> "Mappable":
    """Alias of ``pcolormesh``.

    matplotlib distinguishes the two (``pcolor`` returns a masked-aware
    ``PolyCollection``, ``pcolormesh`` a faster ``QuadMesh``); pyplotrs has
    only the fast path, and it already chooses per-cell quads over the
    image path whenever the grid is irregular, so the distinction has
    nothing left to express."""
    return self.pcolormesh(*args, **kwargs)

hist2d

hist2d(
    xs,
    ys,
    *,
    bins=10,
    range=None,
    cmap="viridis",
    norm=None,
    vmin: float | None = None,
    vmax: float | None = None,
    alpha: float = 1.0,
    label: str | None = None,
    zorder: float = 0.0
) -> "Mappable"

2D histogram of (xs, ys) rendered as a colormapped image. bins is an int or (nx, ny); the count grid is built in Rust.

Source code in python/pyplotrs/axes.py
def hist2d(self, xs, ys, *, bins=10, range=None, cmap="viridis", norm=None,
           vmin: float | None = None, vmax: float | None = None,
           alpha: float = 1.0, label: str | None = None,
           zorder: float = 0.0) -> "Mappable":
    """2D histogram of ``(xs, ys)`` rendered as a colormapped image. ``bins``
    is an int or ``(nx, ny)``; the count grid is built in Rust."""
    xs = _to_f64(xs)
    ys = _to_f64(ys)
    nx, ny = (bins, bins) if isinstance(bins, int) else (int(bins[0]), int(bins[1]))
    # A non-positive count reached the kernel, which computes `nx - 1` in
    # `usize` - so 0 wrapped to 18446744073709551615 and indexed an empty
    # buffer, and a negative failed as a PyO3 conversion OverflowError.
    if nx < 1 or ny < 1:
        raise ValueError(
            f"hist2d needs at least one bin on each axis; got bins=({nx}, {ny})"
        )
    if range is not None:
        (xlo, xhi), (ylo, yhi) = range
    else:
        xlo, xhi = _core.data_range(xs) or (0.0, 1.0)
        ylo, yhi = _core.data_range(ys) or (0.0, 1.0)
    counts = _core.hist2d(xs, ys, nx, ny, xlo, xhi, ylo, yhi)
    rows = [counts[iy * nx:(iy + 1) * nx] for iy in _irange(ny)]
    return self.imshow(rows, cmap=cmap, norm=norm, vmin=vmin, vmax=vmax,
                       extent=(xlo, xhi, ylo, yhi), origin="lower",
                       alpha=alpha, label=label, zorder=zorder)

hexbin

hexbin(
    xs,
    ys,
    *,
    gridsize: int = 30,
    cmap="viridis",
    norm=None,
    vmin: float | None = None,
    vmax: float | None = None,
    alpha: float = 1.0,
    label: str | None = None,
    zorder: float = 0.0
) -> "Mappable"

Hexagonal binning of (xs, ys) colored by count (binning in Rust).

The whole lattice is drawn, as in matplotlib: a cell no point landed in is a count of zero, so it takes the bottom of the colormap rather than leaving the background showing through.

Source code in python/pyplotrs/axes.py
def hexbin(self, xs, ys, *, gridsize: int = 30, cmap="viridis", norm=None,
           vmin: float | None = None, vmax: float | None = None,
           alpha: float = 1.0, label: str | None = None,
           zorder: float = 0.0) -> "Mappable":
    """Hexagonal binning of ``(xs, ys)`` colored by count (binning in Rust).

    The whole lattice is drawn, as in matplotlib: a cell no point landed in
    is a count of zero, so it takes the bottom of the colormap rather than
    leaving the background showing through.
    """
    xs = _to_f64(xs)
    ys = _to_f64(ys)
    xlo, xhi = _core.data_range(xs) or (0.0, 1.0)
    ylo, yhi = _core.data_range(ys) or (0.0, 1.0)
    hexes, sx, sy = _core.hexbin(xs, ys, gridsize, xlo, xhi, ylo, yhi)
    counts = _to_f64([c for _, _, c in hexes])
    cm, nrm, colors = self._map_colors(counts, cmap, norm, vmin, vmax)
    # Pointy-top hexagon: the Voronoi cell of the binner's two interleaved
    # lattices, sx wide and 2/3 sy tall. Both scales come from the binner -
    # sy is set by the *y* range and the derived row count, so guessing it
    # from sx (as this once did) only tiles when the two happen to agree,
    # and shears into slivers or overlapping spikes when they don't.
    offs = [(0.0, sy / 3.0), (sx / 2.0, sy / 6.0), (sx / 2.0, -sy / 6.0),
            (0.0, -sy / 3.0), (-sx / 2.0, -sy / 6.0), (-sx / 2.0, sy / 6.0)]
    if alpha < 1.0:
        colors = [_with_alpha(c, alpha) for c in colors]
    self._marks.append({
        "zorder": float(zorder),
        "kind": "hexbin", "centers": [(cx, cy) for cx, cy, _ in hexes],
        "colors": colors, "offsets": offs, "label": label,
        "color": _with_alpha(cm(0.5), alpha),
    })
    return Mappable(self, cm, nrm.vmin, nrm.vmax,
                     norm=(nrm if type(nrm) is not _norms.Normalize else None))

contour

contour(
    *args,
    levels=None,
    colors=None,
    cmap=None,
    linewidth: float | None = None,
    alpha: float = 1.0,
    label: str | None = None,
    zorder: float = 0.0
) -> "Axes"

Contour lines of a 2D field: contour(Z) or contour(X, Y, Z). Marching squares runs in Rust; lines are colored per level from colors (a single color / list) or cmap (default palette C0).

levels is either the thresholds themselves, or an int asking for about that many: the levels then land on round numbers inside the data range, the way the axis locator picks ticks, so the count comes out near the hint rather than exactly on it.

Source code in python/pyplotrs/axes.py
def contour(self, *args, levels=None, colors=None, cmap=None,
            linewidth: float | None = None, alpha: float = 1.0,
            label: str | None = None, zorder: float = 0.0) -> "Axes":
    """Contour *lines* of a 2D field: ``contour(Z)`` or ``contour(X, Y, Z)``.
    Marching squares runs in Rust; lines are colored per level from
    ``colors`` (a single color / list) or ``cmap`` (default palette C0).

    ``levels`` is either the thresholds themselves, or an int asking for
    *about* that many: the levels then land on round numbers inside the data
    range, the way the axis locator picks ticks, so the count comes out near
    the hint rather than exactly on it.
    """
    xc, yc, Z = _field_args(args)
    h = len(Z)
    w = len(Z[0]) if Z else 0
    flat = _to_f64([v for row in Z for v in row])
    lvls = _auto_levels(flat, levels)
    lines = _core.contour_lines(flat, w, h, lvls)
    lcolors = self._level_colors(len(lvls), colors, cmap)
    if alpha < 1.0:
        lcolors = [_with_alpha(c, alpha) for c in lcolors]
    self._marks.append({
        "zorder": float(zorder),
        "kind": "contour", "lines": lines, "xcoords": xc, "ycoords": yc,
        "levels": lvls, "colors": lcolors, "label": label,
        "linewidth": self._theme.line_width if linewidth is None else float(linewidth),
        # Legend key: the middle level's color stands for the line set.
        "color": lcolors[len(lcolors) // 2] if lcolors else self._theme.palette[0],
        "extent": (min(xc), max(xc), min(yc), max(yc)),
        # The field is only defined on its grid, so the view stops there.
        "sticky_x": [min(xc), max(xc)],
        "sticky_y": [min(yc), max(yc)],
    })
    return self

contourf

contourf(
    *args,
    levels=None,
    cmap="viridis",
    norm=None,
    vmin: float | None = None,
    vmax: float | None = None,
    upsample: int = 6,
    alpha: float = 1.0,
    label: str | None = None,
    zorder: float = 0.0
) -> "Mappable"

Filled contour bands of a 2D field. The field is bilinearly upsampled and band-colored in Rust (a raster fill, like imshow).

levels is either the band edges themselves, or an int asking for about that many bands. Auto edges are the round numbers contour draws its lines on, extended out to bracket the data, so a contour overlay lands exactly on the band boundaries - and the colorbar spans those round numbers rather than the raw extrema.

Source code in python/pyplotrs/axes.py
def contourf(self, *args, levels=None, cmap="viridis", norm=None,
             vmin: float | None = None, vmax: float | None = None,
             upsample: int = 6, alpha: float = 1.0,
             label: str | None = None, zorder: float = 0.0) -> "Mappable":
    """Filled contour bands of a 2D field. The field is bilinearly upsampled
    and band-colored in Rust (a raster fill, like ``imshow``).

    ``levels`` is either the band edges themselves, or an int asking for
    *about* that many bands. Auto edges are the round numbers
    ``contour`` draws its lines on, extended out to bracket the data, so
    a contour overlay lands exactly on the band boundaries - and the
    colorbar spans those round numbers rather than the raw extrema.
    """
    xc, yc, Z = _field_args(args)
    h = len(Z)
    w = len(Z[0]) if Z else 0
    flat = _to_f64([v for row in Z for v in row])
    edges = _level_edges(flat, levels)
    nbands = len(edges) - 1
    cm = _colormaps.get_cmap(cmap)
    # `levels` fixes where the bands *are*; `vmin`/`vmax` fix how the
    # colormap is stretched across them. Both were accepted and then
    # overwritten with the level extremes, so passing `vmin`/`vmax` to
    # `contourf` did nothing at all - not even a warning - and two panels
    # asked to share a color scale silently each got their own.
    nrm = _norms.get(norm, vmin, vmax)
    nrm.vmin = edges[0] if nrm.vmin is None else nrm.vmin
    nrm.vmax = edges[-1] if nrm.vmax is None else nrm.vmax
    band_lut = bytes(b for k in _irange(nbands)
                     for b in _with_alpha(cm(nrm(0.5 * (edges[k] + edges[k + 1]))), alpha))
    img, uw, uh = _core.contourf_image(flat, w, h, edges, band_lut, upsample)
    self._marks.append({
        "zorder": float(zorder),
        "kind": "contourf", "img": bytes(img), "uw": uw, "uh": uh,
        "label": label, "color": _with_alpha(cm(0.5), alpha),
        "extent": (min(xc), max(xc), min(yc), max(yc)),
        "sticky_x": [min(xc), max(xc)],
        "sticky_y": [min(yc), max(yc)],
    })
    # The colorbar spans the norm, so an explicit vmin/vmax shows there too.
    return Mappable(self, cm, nrm.vmin, nrm.vmax, norm=nrm)

quiver

quiver(
    x,
    y,
    u,
    v,
    *,
    scale: float = 1.0,
    color=None,
    linewidth: float | None = None,
    alpha: float = 1.0,
    label: str | None = None,
    zorder: float = 0.0
) -> "Axes"

Arrow field: an arrow at each (x, y) pointing along (u, v).

scale multiplies the vectors in data space (so the arrow tip lands at x + u*scale), and the arrowheads are sized in points. x/y may be 1D lists or 2D grids, as long as all four agree in shape.

Source code in python/pyplotrs/axes.py
def quiver(self, x, y, u, v, *, scale: float = 1.0, color=None,
           linewidth: float | None = None, alpha: float = 1.0,
           label: str | None = None, zorder: float = 0.0) -> "Axes":
    """Arrow field: an arrow at each ``(x, y)`` pointing along ``(u, v)``.

    ``scale`` multiplies the vectors in data space (so the arrow tip lands
    at ``x + u*scale``), and the arrowheads are sized in points. ``x``/``y``
    may be 1D lists or 2D grids, as long as all four agree in shape."""
    xs, ys, us, vs = (_to_f64(_flatten2d(a)) for a in (x, y, u, v))
    if not (len(xs) == len(ys) == len(us) == len(vs)):
        raise ValueError(
            f"quiver needs x, y, u, v of equal length; got "
            f"{len(xs)}, {len(ys)}, {len(us)}, {len(vs)}"
        )
    self._marks.append({
        "zorder": float(zorder),
        "kind": "quiver", "xs": xs, "ys": ys, "us": us, "vs": vs,
        "scale": float(scale), "color": self._mark_color(color, alpha),
        "linewidth": self._theme.line_width if linewidth is None else float(linewidth),
        "label": label,
    })
    return self

streamplot

streamplot(
    x,
    y,
    u,
    v,
    *,
    density: float = 1.0,
    color=None,
    linewidth: float | None = None,
    alpha: float = 1.0,
    maxlength: float = 4.0,
    arrows: bool = True,
    label: str | None = None,
    zorder: float = 0.0
) -> "Axes"

Streamlines of the vector field (u, v) sampled on the grid (x, y).

x/y are the 1D coordinates of the grid columns/rows and u/v are 2D len(y) x len(x) grids. Seeds are laid on a density-scaled lattice and integrated both ways with RK4; maxlength caps each streamline's arc length in grid cells. arrows puts a direction head at each streamline's midpoint - a streamline is otherwise unsigned, and which way the flow runs is usually the point of drawing one.

Source code in python/pyplotrs/axes.py
def streamplot(self, x, y, u, v, *, density: float = 1.0, color=None,
               linewidth: float | None = None, alpha: float = 1.0,
               maxlength: float = 4.0, arrows: bool = True,
               label: str | None = None, zorder: float = 0.0) -> "Axes":
    """Streamlines of the vector field ``(u, v)`` sampled on the grid
    ``(x, y)``.

    ``x``/``y`` are the 1D coordinates of the grid columns/rows and
    ``u``/``v`` are 2D ``len(y) x len(x)`` grids. Seeds are laid on a
    ``density``-scaled lattice and integrated both ways with RK4;
    ``maxlength`` caps each streamline's arc length in grid cells.
    ``arrows`` puts a direction head at each streamline's midpoint - a
    streamline is otherwise unsigned, and which way the flow runs is
    usually the point of drawing one."""
    xc = [float(t) for t in x]
    yc = [float(t) for t in y]
    gu = [[float(t) for t in row] for row in u]
    gv = [[float(t) for t in row] for row in v]
    h, w = len(gu), len(gu[0]) if gu else 0
    if h < 2 or w < 2:
        raise ValueError("streamplot needs a grid of at least 2x2")
    col = self._mark_color(color, alpha)
    lw = self._theme.line_width if linewidth is None else float(linewidth)
    heads: list[tuple[float, float, float, float]] = []
    first = True
    for px, py in _streamlines(xc, yc, gu, gv, density, maxlength):
        if len(px) < 2:
            continue
        # Only the first streamline carries the label, so one legend key
        # stands for the whole field rather than one per line.
        self.line(px, py, color=col, linewidth=lw, simplify=False,
                  zorder=zorder, label=(label if first else None))
        first = False
        if arrows and len(px) >= 3:
            k = len(px) // 2
            dxs, dys = px[k] - px[k - 1], py[k] - py[k - 1]
            mag = math.hypot(dxs, dys)
            if mag > 0.0:
                heads.append((px[k - 1], py[k - 1], dxs / mag, dys / mag))
    if heads:
        # One quiver mark for every head, so the arrows cost a single mark
        # rather than one per streamline. The shaft is a hair long enough
        # to carry the head and no more - the line underneath is the path.
        span = max(abs(xc[-1] - xc[0]), abs(yc[-1] - yc[0])) or 1.0
        self._marks.append({
            "zorder": float(zorder),
            "kind": "quiver",
            "xs": array("d", [p[0] for p in heads]),
            "ys": array("d", [p[1] for p in heads]),
            "us": array("d", [p[2] for p in heads]),
            "vs": array("d", [p[3] for p in heads]),
            "scale": span * 0.012, "color": col, "linewidth": lw,
            "label": None,
        })
    return self

loglog

loglog(xs, ys, **kwargs) -> 'Axes'

line with both axes log-scaled - matplotlib's ax.loglog.

Source code in python/pyplotrs/axes.py
def loglog(self, xs, ys, **kwargs) -> "Axes":
    """``line`` with both axes log-scaled - matplotlib's ``ax.loglog``."""
    self.set(xscale="log", yscale="log")
    return self.line(xs, ys, **kwargs)

semilogx

semilogx(xs, ys, **kwargs) -> 'Axes'

line with the x-axis log-scaled - matplotlib's ax.semilogx.

A thin wrapper: ax.set(xscale="log") then ax.line(xs, ys, **kwargs).

Source code in python/pyplotrs/axes.py
def semilogx(self, xs, ys, **kwargs) -> "Axes":
    """``line`` with the x-axis log-scaled - matplotlib's ``ax.semilogx``.

    A thin wrapper: ``ax.set(xscale="log")`` then ``ax.line(xs, ys, **kwargs)``.
    """
    self.set(xscale="log")
    return self.line(xs, ys, **kwargs)

semilogy

semilogy(xs, ys, **kwargs) -> 'Axes'

line with the y-axis log-scaled - matplotlib's ax.semilogy.

Source code in python/pyplotrs/axes.py
def semilogy(self, xs, ys, **kwargs) -> "Axes":
    """``line`` with the y-axis log-scaled - matplotlib's ``ax.semilogy``."""
    self.set(yscale="log")
    return self.line(xs, ys, **kwargs)

hlines

hlines(
    y,
    xmin,
    xmax,
    *,
    color=None,
    linewidth: float | None = None,
    alpha: float = 1.0,
    linestyle: str = "solid",
    label: str | None = None,
    zorder: float = 0.0
) -> "Axes"

Horizontal line segments at each y, spanning xmin to xmax in data coordinates.

Unlike axhline, which spans a fraction of the axes and is a guide, these are data and participate in autoscaling. Each argument may be a scalar or a sequence; scalars broadcast.

Source code in python/pyplotrs/axes.py
def hlines(self, y, xmin, xmax, *, color=None, linewidth: float | None = None,
           alpha: float = 1.0, linestyle: str = "solid",
           label: str | None = None, zorder: float = 0.0) -> "Axes":
    """Horizontal line segments at each ``y``, spanning ``xmin`` to ``xmax``
    in **data** coordinates.

    Unlike ``axhline``, which spans a fraction of the axes and is a
    guide, these are data and participate in autoscaling. Each argument may
    be a scalar or a sequence; scalars broadcast."""
    return self._add_lines("h", y, xmin, xmax, color, linewidth, linestyle, label,
                           alpha, zorder)

vlines

vlines(
    x,
    ymin,
    ymax,
    *,
    color=None,
    linewidth: float | None = None,
    alpha: float = 1.0,
    linestyle: str = "solid",
    label: str | None = None,
    zorder: float = 0.0
) -> "Axes"

Vertical line segments at each x, spanning ymin to ymax in data coordinates (see hlines).

Source code in python/pyplotrs/axes.py
def vlines(self, x, ymin, ymax, *, color=None, linewidth: float | None = None,
           alpha: float = 1.0, linestyle: str = "solid",
           label: str | None = None, zorder: float = 0.0) -> "Axes":
    """Vertical line segments at each ``x``, spanning ``ymin`` to ``ymax`` in
    **data** coordinates (see ``hlines``)."""
    return self._add_lines("v", x, ymin, ymax, color, linewidth, linestyle, label,
                           alpha, zorder)

axhline

axhline(
    y: float = 0.0,
    *,
    xmin: float = 0.0,
    xmax: float = 1.0,
    color=None,
    linewidth: float | None = None,
    linestyle: str = "solid"
) -> "Axes"

Draw a horizontal reference line at data y spanning the axes fraction xmin..xmax (0 = left edge, 1 = right).

y is folded into the y limits so the guide cannot land outside the frame; xmin/xmax are axes fractions, not data, so x is untouched.

Source code in python/pyplotrs/axes.py
def axhline(self, y: float = 0.0, *, xmin: float = 0.0, xmax: float = 1.0,
            color=None, linewidth: float | None = None,
            linestyle: str = "solid") -> "Axes":
    """Draw a horizontal reference line at data ``y`` spanning the axes
    fraction ``xmin..xmax`` (0 = left edge, 1 = right).

    ``y`` is folded into the y limits so the guide cannot land outside the
    frame; ``xmin``/``xmax`` are axes fractions, not data, so x is untouched."""
    self._refs.append({
        "kind": "axhline", "y": float(y), "min": float(xmin), "max": float(xmax),
        "color": self._theme.resolve(color) if color is not None else self._theme.text_color,
        "linewidth": self._theme.line_width if linewidth is None else float(linewidth),
        "linestyle": linestyle,
    })
    return self

axvline

axvline(
    x: float = 0.0,
    *,
    ymin: float = 0.0,
    ymax: float = 1.0,
    color=None,
    linewidth: float | None = None,
    linestyle: str = "solid"
) -> "Axes"

Draw a vertical reference line at data x spanning the axes fraction ymin..ymax. x is folded into the x limits so the guide stays inside the frame; ymin/ymax are fractions. See axhline.

Source code in python/pyplotrs/axes.py
def axvline(self, x: float = 0.0, *, ymin: float = 0.0, ymax: float = 1.0,
            color=None, linewidth: float | None = None,
            linestyle: str = "solid") -> "Axes":
    """Draw a vertical reference line at data ``x`` spanning the axes
    fraction ``ymin..ymax``. ``x`` is folded into the x limits so the guide
    stays inside the frame; ``ymin``/``ymax`` are fractions. See ``axhline``."""
    self._refs.append({
        "kind": "axvline", "x": float(x), "min": float(ymin), "max": float(ymax),
        "color": self._theme.resolve(color) if color is not None else self._theme.text_color,
        "linewidth": self._theme.line_width if linewidth is None else float(linewidth),
        "linestyle": linestyle,
    })
    return self

axhspan

axhspan(
    ymin: float,
    ymax: float,
    *,
    xmin: float = 0.0,
    xmax: float = 1.0,
    color=None,
    alpha: float = 0.3
) -> "Axes"

Shade the horizontal band between data ymin and ymax (spanning the axes fraction xmin..xmax in x). Drawn behind the data; the band is folded into the y limits so it stays visible.

Source code in python/pyplotrs/axes.py
def axhspan(self, ymin: float, ymax: float, *, xmin: float = 0.0, xmax: float = 1.0,
            color=None, alpha: float = 0.3) -> "Axes":
    """Shade the horizontal band between data ``ymin`` and ``ymax`` (spanning
    the axes fraction ``xmin..xmax`` in x). Drawn behind the data; the band
    is folded into the y limits so it stays visible."""
    self._refs.append({
        "kind": "axhspan", "lo": float(ymin), "hi": float(ymax),
        "min": float(xmin), "max": float(xmax),
        "color": self._next_color(color), "alpha": float(alpha),
    })
    return self

axvspan

axvspan(
    xmin: float,
    xmax: float,
    *,
    ymin: float = 0.0,
    ymax: float = 1.0,
    color=None,
    alpha: float = 0.3
) -> "Axes"

Shade the vertical band between data xmin and xmax (spanning the axes fraction ymin..ymax in y). Drawn behind the data; the band is folded into the x limits so it stays visible.

Source code in python/pyplotrs/axes.py
def axvspan(self, xmin: float, xmax: float, *, ymin: float = 0.0, ymax: float = 1.0,
            color=None, alpha: float = 0.3) -> "Axes":
    """Shade the vertical band between data ``xmin`` and ``xmax`` (spanning
    the axes fraction ``ymin..ymax`` in y). Drawn behind the data; the band
    is folded into the x limits so it stays visible."""
    self._refs.append({
        "kind": "axvspan", "lo": float(xmin), "hi": float(xmax),
        "min": float(ymin), "max": float(ymax),
        "color": self._next_color(color), "alpha": float(alpha),
    })
    return self

axline

axline(
    xy1,
    *,
    xy2=None,
    slope: float | None = None,
    color=None,
    linewidth: float | None = None,
    linestyle: str = "solid"
) -> "Axes"

Draw an infinite line through xy1, defined by a second point xy2 or a slope. Clipped to the plot rect. Alone among the guides it contributes nothing to the limits - it is infinite, so it has no extent to contribute and is always on screen already.

Source code in python/pyplotrs/axes.py
def axline(self, xy1, *, xy2=None, slope: float | None = None, color=None,
           linewidth: float | None = None, linestyle: str = "solid") -> "Axes":
    """Draw an infinite line through ``xy1``, defined by a second point
    ``xy2`` or a ``slope``. Clipped to the plot rect. Alone among the
    guides it contributes nothing to the limits - it is infinite, so it has
    no extent to contribute and is always on screen already."""
    if (xy2 is None) == (slope is None):
        raise ValueError("axline requires exactly one of xy2= or slope=")
    self._refs.append({
        "kind": "axline", "p1": (float(xy1[0]), float(xy1[1])),
        "p2": None if xy2 is None else (float(xy2[0]), float(xy2[1])),
        "slope": None if slope is None else float(slope),
        "color": self._theme.resolve(color) if color is not None else self._theme.text_color,
        "linewidth": self._theme.line_width if linewidth is None else float(linewidth),
        "linestyle": linestyle,
    })
    return self

rectangle

rectangle(
    xy,
    width: float,
    height: float,
    *,
    angle: float = 0.0,
    facecolor=None,
    edgecolor=None,
    linewidth: float = 1.0,
    linestyle: str = "solid",
    alpha: float = 1.0,
    fill: bool = True,
    hatch: str | None = None
) -> "Axes"

Add an axis-aligned (or angle-rotated, degrees CCW) rectangle with lower-left corner xy and the given data-space width/height.

Source code in python/pyplotrs/axes.py
def rectangle(self, xy, width: float, height: float, *, angle: float = 0.0,
              facecolor=None, edgecolor=None, linewidth: float = 1.0,
              linestyle: str = "solid", alpha: float = 1.0,
              fill: bool = True, hatch: str | None = None) -> "Axes":
    """Add an axis-aligned (or ``angle``-rotated, degrees CCW) rectangle with
    lower-left corner ``xy`` and the given data-space ``width``/``height``."""
    self._patches.append(self._patch_style({
        "kind": "rectangle", "xy": (float(xy[0]), float(xy[1])),
        "w": float(width), "h": float(height), "angle": float(angle),
    }, facecolor, edgecolor, linewidth, linestyle, alpha, fill, hatch))
    return self

circle

circle(
    xy,
    radius: float,
    *,
    facecolor=None,
    edgecolor=None,
    linewidth: float = 1.0,
    linestyle: str = "solid",
    alpha: float = 1.0,
    fill: bool = True,
    hatch: str | None = None
) -> "Axes"

Add a circle of data-space radius centered at xy. Note it maps to an ellipse when the x/y scales differ (use set(aspect='equal')).

Source code in python/pyplotrs/axes.py
def circle(self, xy, radius: float, *, facecolor=None, edgecolor=None,
           linewidth: float = 1.0, linestyle: str = "solid", alpha: float = 1.0,
           fill: bool = True, hatch: str | None = None) -> "Axes":
    """Add a circle of data-space ``radius`` centered at ``xy``. Note it maps
    to an ellipse when the x/y scales differ (use ``set(aspect='equal')``)."""
    self._patches.append(self._patch_style({
        "kind": "ellipse", "xy": (float(xy[0]), float(xy[1])),
        "rx": float(radius), "ry": float(radius), "angle": 0.0,
    }, facecolor, edgecolor, linewidth, linestyle, alpha, fill, hatch))
    return self

ellipse

ellipse(
    xy,
    width: float,
    height: float,
    *,
    angle: float = 0.0,
    facecolor=None,
    edgecolor=None,
    linewidth: float = 1.0,
    linestyle: str = "solid",
    alpha: float = 1.0,
    fill: bool = True,
    hatch: str | None = None
) -> "Axes"

Add an ellipse of full data-space width/height (diameters) centered at xy, rotated angle degrees CCW.

Source code in python/pyplotrs/axes.py
def ellipse(self, xy, width: float, height: float, *, angle: float = 0.0,
            facecolor=None, edgecolor=None, linewidth: float = 1.0,
            linestyle: str = "solid", alpha: float = 1.0, fill: bool = True,
            hatch: str | None = None) -> "Axes":
    """Add an ellipse of full data-space ``width``/``height`` (diameters)
    centered at ``xy``, rotated ``angle`` degrees CCW."""
    self._patches.append(self._patch_style({
        "kind": "ellipse", "xy": (float(xy[0]), float(xy[1])),
        "rx": float(width) / 2.0, "ry": float(height) / 2.0, "angle": float(angle),
    }, facecolor, edgecolor, linewidth, linestyle, alpha, fill, hatch))
    return self

polygon

polygon(
    points,
    *,
    closed: bool = True,
    facecolor=None,
    edgecolor=None,
    linewidth: float = 1.0,
    linestyle: str = "solid",
    alpha: float = 1.0,
    fill: bool = True,
    hatch: str | None = None
) -> "Axes"

Add a polygon through the data-space vertices points.

Source code in python/pyplotrs/axes.py
def polygon(self, points, *, closed: bool = True, facecolor=None,
            edgecolor=None, linewidth: float = 1.0, linestyle: str = "solid",
            alpha: float = 1.0, fill: bool = True, hatch: str | None = None) -> "Axes":
    """Add a polygon through the data-space vertices ``points``."""
    self._patches.append(self._patch_style({
        "kind": "polygon", "pts": [(float(x), float(y)) for x, y in points],
        "closed": bool(closed),
    }, facecolor, edgecolor, linewidth, linestyle, alpha, fill, hatch))
    return self

fill

fill(
    x,
    y,
    *,
    facecolor=None,
    edgecolor=None,
    linewidth: float = 1.0,
    linestyle: str = "solid",
    alpha: float = 1.0,
    hatch: str | None = None
) -> "Axes"

Fill the closed polygon through (x, y) - matplotlib's ax.fill.

A thin wrapper over polygon taking parallel x/y arrays instead of a list of point pairs; facecolor cycles the palette like a data mark when omitted. It is a patch like polygon (drawn over the data, outside the zorder/legend contract the marks share) - call polygon directly for its other knobs.

Source code in python/pyplotrs/axes.py
def fill(self, x, y, *, facecolor=None, edgecolor=None, linewidth: float = 1.0,
         linestyle: str = "solid", alpha: float = 1.0,
         hatch: str | None = None) -> "Axes":
    """Fill the closed polygon through ``(x, y)`` - matplotlib's ``ax.fill``.

    A thin wrapper over ``polygon`` taking parallel ``x``/``y`` arrays
    instead of a list of point pairs; ``facecolor`` cycles the palette like
    a data mark when omitted. It is a patch like ``polygon`` (drawn
    over the data, outside the zorder/legend contract the marks share) -
    call ``polygon`` directly for its other knobs.
    """
    xs = _to_f64(x)
    ys = _to_f64(y)
    _require_same_length("fill", x=xs, y=ys)
    return self.polygon(list(zip(xs, ys)), facecolor=facecolor, edgecolor=edgecolor,
                        linewidth=linewidth, linestyle=linestyle, alpha=alpha, hatch=hatch)

arrow

arrow(
    x: float,
    y: float,
    dx: float,
    dy: float,
    *,
    color=None,
    linewidth: float = 1.5
) -> "Axes"

Draw an arrow from data (x, y) to (x + dx, y + dy).

Source code in python/pyplotrs/axes.py
def arrow(self, x: float, y: float, dx: float, dy: float, *, color=None,
          linewidth: float = 1.5) -> "Axes":
    """Draw an arrow from data ``(x, y)`` to ``(x + dx, y + dy)``."""
    self._patches.append({
        "kind": "arrow", "x": float(x), "y": float(y),
        "dx": float(dx), "dy": float(dy),
        "edgecolor": self._next_color(color), "linewidth": float(linewidth),
    })
    return self

text

text(
    x,
    y,
    s,
    *,
    color=None,
    fontsize: float | None = None,
    weight: str = "normal",
    style: str = "normal",
    ha: str = "left",
    va: str = "baseline",
    rotation: float = 0.0
) -> "Axes"

Draw s at data coordinates (x, y).

ha is left/center/right; va is baseline/bottom/center/top. s may contain $...$ math. color defaults to the theme text color. weight is normal or bold and style is normal or italic; both select a real face of the body family, so the glyphs are genuinely bold or italic rather than synthetically slanted.

rotation turns the text counter-clockwise by that many degrees about its anchor, and it stays selectable text in PDF/SVG - the rotation is a group transform in the IR, not baked-out paths.

Source code in python/pyplotrs/axes.py
def text(self, x, y, s, *, color=None, fontsize: float | None = None,
         weight: str = "normal", style: str = "normal",
         ha: str = "left", va: str = "baseline",
         rotation: float = 0.0) -> "Axes":
    """Draw ``s`` at data coordinates ``(x, y)``.

    ``ha`` is ``left``/``center``/``right``; ``va`` is
    ``baseline``/``bottom``/``center``/``top``. ``s`` may contain ``$...$``
    math. ``color`` defaults to the theme text color. ``weight`` is
    ``normal`` or ``bold`` and ``style`` is ``normal`` or ``italic``; both
    select a real face of the body family, so the glyphs are genuinely bold
    or italic rather than synthetically slanted.

    ``rotation`` turns the text counter-clockwise by that many degrees
    about its anchor, and it stays selectable text in PDF/SVG - the
    rotation is a group transform in the IR, not baked-out paths."""
    self._annotations.append({
        "kind": "text", "x": float(x), "y": float(y), "s": s,
        "color": self._theme.text_color if color is None else self._theme.resolve(color),
        "size": None if fontsize is None else float(fontsize),
        "font": _font(weight, style), "ha": ha, "va": va,
        "rotation": float(rotation),
    })
    return self

annotate

annotate(
    text,
    xy,
    *,
    xytext=None,
    color=None,
    fontsize: float | None = None,
    weight: str = "normal",
    style: str = "normal",
    arrow: bool = True,
    ha: str = "left",
    va: str = "bottom",
    rotation: float = 0.0
) -> "Axes"

Annotate the data point xy with text placed at xytext (defaults to xy), optionally drawing a callout arrow from the text to the point. All coordinates are in data space. weight/style select a bold and/or italic face (see text).

Source code in python/pyplotrs/axes.py
def annotate(self, text, xy, *, xytext=None, color=None, fontsize: float | None = None,
             weight: str = "normal", style: str = "normal",
             arrow: bool = True, ha: str = "left", va: str = "bottom",
             rotation: float = 0.0) -> "Axes":
    """Annotate the data point ``xy`` with ``text`` placed at ``xytext``
    (defaults to ``xy``), optionally drawing a callout arrow from the text to
    the point. All coordinates are in data space. ``weight``/``style`` select
    a bold and/or italic face (see ``text``)."""
    xy = (float(xy[0]), float(xy[1]))
    self._annotations.append({
        "kind": "annotate", "s": text, "xy": xy,
        "xytext": xy if xytext is None else (float(xytext[0]), float(xytext[1])),
        "color": self._theme.text_color if color is None else self._theme.resolve(color),
        "size": None if fontsize is None else float(fontsize),
        "font": _font(weight, style),
        "arrow": bool(arrow), "ha": ha, "va": va,
        "rotation": float(rotation),
    })
    return self

legend

legend(
    *,
    loc: str | None = None,
    ncol: int = 1,
    title: str | None = None,
    frameon: bool = True,
    fontsize: float | None = None
)

Enable an auto-legend over this axes' labeled marks.

loc is best / upper right / upper left / lower right / lower left / upper center / lower center; None uses this axes class's default. best picks the corner that overlaps the data least.

ncol lays the keys out in that many columns, filled down then across - the usual fix for a legend tall enough to crowd the data. title puts a heading above the keys, frameon=False drops the box and its background, and fontsize overrides theme.legend_size for this legend only.

Source code in python/pyplotrs/axes.py
def legend(self, *, loc: str | None = None, ncol: int = 1,
           title: str | None = None, frameon: bool = True,
           fontsize: float | None = None):
    """Enable an auto-legend over this axes' labeled marks.

    ``loc`` is ``best`` / ``upper right`` / ``upper left`` / ``lower right``
    / ``lower left`` / ``upper center`` / ``lower center``; ``None`` uses
    this axes class's default. ``best`` picks the corner that overlaps the
    data least.

    ``ncol`` lays the keys out in that many columns, filled down then
    across - the usual fix for a legend tall enough to crowd the data.
    ``title`` puts a heading above the keys, ``frameon=False`` drops the
    box and its background, and ``fontsize`` overrides ``theme.legend_size``
    for this legend only.
    """
    self._legend = {
        "loc": self._LEGEND_DEFAULT_LOC if loc is None else loc,
        "ncol": int(ncol), "title": title, "frameon": bool(frameon),
        "fontsize": None if fontsize is None else float(fontsize),
    }
    return self

axis

axis(arg: str) -> 'Axes'

Coarse axis control: "off"/"on" toggle the frame (spines, ticks, grid); "equal" requests an equal data-unit aspect.

Source code in python/pyplotrs/axes.py
def axis(self, arg: str) -> "Axes":
    """Coarse axis control: ``"off"``/``"on"`` toggle the frame (spines,
    ticks, grid); ``"equal"`` requests an equal data-unit aspect."""
    if arg == "off":
        self._frame_off = True
    elif arg == "on":
        self._frame_off = False
    elif arg == "equal":
        self._aspect = "equal"
    elif arg == "auto":
        self._aspect = None
    else:
        raise ValueError(f"unknown axis({arg!r}); expected 'off'/'on'/'equal'/'auto'")
    return self

twinx

twinx() -> 'Axes'

A second axes sharing this one's x-axis but with an independent y-axis drawn on the right (e.g. two series in different units). Plot on the returned axes; it overlays the same cell.

Source code in python/pyplotrs/axes.py
def twinx(self) -> "Axes":
    """A second axes sharing this one's x-axis but with an independent y-axis
    drawn on the right (e.g. two series in different units). Plot on the
    returned axes; it overlays the same cell."""
    tw = Axes(self._theme)
    tw._is_twin = True
    tw._cidx = self._cidx  # continue the palette so colors don't collide
    self._twinx = tw
    return tw

twiny

twiny() -> 'Axes'

A second axes sharing this one's y-axis with an independent x-axis drawn along the top.

Source code in python/pyplotrs/axes.py
def twiny(self) -> "Axes":
    """A second axes sharing this one's y-axis with an independent x-axis
    drawn along the top."""
    tw = Axes(self._theme)
    tw._is_twin = True
    tw._cidx = self._cidx
    self._twiny = tw
    return tw

inset_axes

inset_axes(bounds) -> 'Axes'

A child axes occupying bounds = (x0, y0, width, height) given as fractions of this axes' plot area ((0, 0) = lower-left). Returns the inset axes to plot on.

Source code in python/pyplotrs/axes.py
def inset_axes(self, bounds) -> "Axes":
    """A child axes occupying ``bounds = (x0, y0, width, height)`` given as
    fractions of this axes' plot area (``(0, 0)`` = lower-left). Returns the
    inset axes to plot on."""
    x0, y0, w, h = (float(v) for v in bounds)
    child = Axes(self._theme)
    self._insets.append((child, (x0, y0, w, h)))
    return child

secondary_xaxis

secondary_xaxis(
    location: str,
    *,
    functions=None,
    label: str | None = None
) -> "Axes"

A functional secondary x-axis at location ("top"/"bottom"). functions=(forward, inverse) maps primary→secondary data (e.g. Celsius↔Fahrenheit); omit for a plain duplicate axis. Returns self.

Source code in python/pyplotrs/axes.py
def secondary_xaxis(self, location: str, *, functions=None,
                    label: str | None = None) -> "Axes":
    """A functional secondary x-axis at ``location`` (``"top"``/``"bottom"``).
    ``functions=(forward, inverse)`` maps primary→secondary data (e.g.
    Celsius↔Fahrenheit); omit for a plain duplicate axis. Returns ``self``."""
    if location not in ("top", "bottom"):
        raise ValueError(
            f"secondary_xaxis location must be 'top' or 'bottom', got {location!r}"
        )
    self._secondary.append({"axis": "x", "loc": location, "functions": functions,
                            "label": label})
    return self

secondary_yaxis

secondary_yaxis(
    location: str,
    *,
    functions=None,
    label: str | None = None
) -> "Axes"

A functional secondary y-axis at location ("left"/"right").

Source code in python/pyplotrs/axes.py
def secondary_yaxis(self, location: str, *, functions=None,
                    label: str | None = None) -> "Axes":
    """A functional secondary y-axis at ``location`` (``"left"``/``"right"``)."""
    if location not in ("left", "right"):
        raise ValueError(
            f"secondary_yaxis location must be 'left' or 'right', got {location!r}"
        )
    self._secondary.append({"axis": "y", "loc": location, "functions": functions,
                            "label": label})
    return self

set

set(
    *,
    title=None,
    xlabel=None,
    ylabel=None,
    xlim=None,
    ylim=None,
    xscale=None,
    yscale=None,
    xticks=None,
    yticks=None,
    xticklabels=None,
    yticklabels=None,
    xformatter=None,
    yformatter=None,
    grid=None,
    aspect=None,
    xmargin=None,
    ymargin=None,
    margin=None,
    xinverted=None,
    yinverted=None,
    xminor=None,
    yminor=None,
    minor=None,
    tick_direction=None,
    tick_length=None,
    xtickrotation=None
) -> "Axes"

Set any combination of title, axis labels, view limits, axis scales, and tick/grid/aspect/margin controls.

xscale/yscale accept "linear" (default), "log", "symlog", "logit" or a pyplotrs.scales.Scale. xticks/yticks pin tick positions; xticklabels/yticklabels give matching label strings. xformatter/yformatter accept a pyplotrs.ticker.Formatter, a "{x:.2f}" template, or a callable. grid overrides the theme grid; aspect="equal" equalizes the data-unit scale on both axes.

Passing xlim="auto" (or ylim) clears a previously pinned limit and returns that axis to autoscaling - None means "leave alone", so it cannot double as a reset.

xmargin/ymargin (or margin for both) set the autoscale padding as a fraction of the data span, replacing the 5% default; 0 gives limits tight to the data. xinverted/yinverted make an axis descend without pinning numbers, which composes with autoscaling. xminor/yminor (or minor) put that many minor intervals inside each major one - non-linear scales already subdivide themselves, so this is for linear axes. tick_direction is "out" (default) or "in", and tick_length overrides the tick mark length in points.

xtickrotation is an angle in degrees for the x tick labels, or "auto" (the default) to rotate only when the labels would otherwise collide, or 0 to force them flat and accept the overlap. Long category names on a bar chart are the case this exists for: drawn flat they overprint each other, and the axis stops saying which bar is which.

Source code in python/pyplotrs/axes.py
def set(self, *, title=None, xlabel=None, ylabel=None, xlim=None, ylim=None,
        xscale=None, yscale=None, xticks=None, yticks=None,
        xticklabels=None, yticklabels=None, xformatter=None, yformatter=None,
        grid=None, aspect=None, xmargin=None, ymargin=None, margin=None,
        xinverted=None, yinverted=None, xminor=None, yminor=None, minor=None,
        tick_direction=None, tick_length=None, xtickrotation=None) -> "Axes":
    """Set any combination of title, axis labels, view limits, axis scales,
    and tick/grid/aspect/margin controls.

    ``xscale``/``yscale`` accept ``"linear"`` (default), ``"log"``,
    ``"symlog"``, ``"logit"`` or a [`pyplotrs.scales.Scale`][pyplotrs.scales.Scale].
    ``xticks``/``yticks`` pin tick positions; ``xticklabels``/``yticklabels``
    give matching label strings. ``xformatter``/``yformatter`` accept a
    [`pyplotrs.ticker.Formatter`][pyplotrs.ticker.Formatter], a ``"{x:.2f}"`` template, or a
    callable. ``grid`` overrides the theme grid; ``aspect="equal"`` equalizes
    the data-unit scale on both axes.

    Passing ``xlim="auto"`` (or ``ylim``) clears a previously pinned limit
    and returns that axis to autoscaling - ``None`` means "leave alone", so
    it cannot double as a reset.

    ``xmargin``/``ymargin`` (or ``margin`` for both) set the autoscale
    padding as a fraction of the data span, replacing the 5% default;
    ``0`` gives limits tight to the data. ``xinverted``/``yinverted``
    make an axis descend without pinning numbers, which composes with
    autoscaling. ``xminor``/``yminor`` (or ``minor``) put that many minor
    intervals inside each major one - non-linear scales already subdivide
    themselves, so this is for linear axes. ``tick_direction`` is ``"out"``
    (default) or ``"in"``, and ``tick_length`` overrides the tick mark
    length in points.

    ``xtickrotation`` is an angle in degrees for the x tick labels, or
    ``"auto"`` (the default) to rotate only when the labels would otherwise
    collide, or ``0`` to force them flat and accept the overlap. Long
    category names on a bar chart are the case this exists for: drawn flat
    they overprint each other, and the axis stops saying which bar is
    which."""
    if title is not None:
        self._title = title
    if xlabel is not None:
        self._xlabel = xlabel
    if ylabel is not None:
        self._ylabel = ylabel
    if xlim is not None:
        self._xlim = None if xlim == "auto" else (float(xlim[0]), float(xlim[1]))
    if ylim is not None:
        self._ylim = None if ylim == "auto" else (float(ylim[0]), float(ylim[1]))
    if xscale is not None:
        self._xscale = _scales.get(xscale)
    if yscale is not None:
        self._yscale = _scales.get(yscale)
    if xticks is not None:
        self._xticks_manual = [float(v) for v in xticks]
    if yticks is not None:
        self._yticks_manual = [float(v) for v in yticks]
    if xticklabels is not None:
        self._xticklabels_manual = list(xticklabels)
    if yticklabels is not None:
        self._yticklabels_manual = list(yticklabels)
    if xformatter is not None:
        self._xformatter = _ticker.get(xformatter)
    if yformatter is not None:
        self._yformatter = _ticker.get(yformatter)
    if grid is not None:
        self._grid_override = bool(grid)
    if aspect is not None:
        self._aspect = None if aspect == "auto" else str(aspect)
    if margin is not None:
        xmargin = margin if xmargin is None else xmargin
        ymargin = margin if ymargin is None else ymargin
    if xmargin is not None:
        self._xmargin = _check_margin("xmargin", xmargin)
    if ymargin is not None:
        self._ymargin = _check_margin("ymargin", ymargin)
    if xinverted is not None:
        self._xinverted = bool(xinverted)
    if yinverted is not None:
        self._yinverted = bool(yinverted)
    if minor is not None:
        xminor = minor if xminor is None else xminor
        yminor = minor if yminor is None else yminor
    if xminor is not None:
        self._xminor = int(xminor)
    if yminor is not None:
        self._yminor = int(yminor)
    if tick_direction is not None:
        if tick_direction not in ("in", "out"):
            raise ValueError(
                f'tick_direction must be "in" or "out", got {tick_direction!r}')
        self._tick_direction = tick_direction
    if tick_length is not None:
        self._tick_length = float(tick_length)
    if xtickrotation is not None:
        self._xtickrotation = ("auto" if xtickrotation == "auto"
                               else float(xtickrotation))
    return self

get_xlim

get_xlim() -> tuple[float, float]

Effective x limits: the explicit xlim if set, else autoscaled - and unified across the row when the figure was built sharex=True.

Source code in python/pyplotrs/axes.py
def get_xlim(self) -> tuple[float, float]:
    """Effective x limits: the explicit ``xlim`` if set, else autoscaled -
    and unified across the row when the figure was built ``sharex=True``."""
    return self._effective_ranges()[0]

get_ylim

get_ylim() -> tuple[float, float]

Effective y limits (see get_xlim; sharey unifies these).

Source code in python/pyplotrs/axes.py
def get_ylim(self) -> tuple[float, float]:
    """Effective y limits (see ``get_xlim``; ``sharey`` unifies these)."""
    return self._effective_ranges()[1]

get_xlabel

get_xlabel() -> str | None
Source code in python/pyplotrs/axes.py
def get_xlabel(self) -> str | None:
    return self._xlabel

get_ylabel

get_ylabel() -> str | None
Source code in python/pyplotrs/axes.py
def get_ylabel(self) -> str | None:
    return self._ylabel

get_title

get_title() -> str | None

This axes' title, or None. Lives on the base class because all three axes kinds have one - the rest of the getters are per-kind.

Source code in python/pyplotrs/axes.py
def get_title(self) -> str | None:
    """This axes' title, or ``None``. Lives on the base class because all
    three axes kinds have one - the rest of the getters are per-kind."""
    return self._title

get_xscale

get_xscale() -> str

The x scale's name ("linear", "log", "symlog", ...).

Source code in python/pyplotrs/axes.py
def get_xscale(self) -> str:
    """The x scale's name (``"linear"``, ``"log"``, ``"symlog"``, ...)."""
    return getattr(self._xscale, "name", self._xscale.code)

get_yscale

get_yscale() -> str
Source code in python/pyplotrs/axes.py
def get_yscale(self) -> str:
    return getattr(self._yscale, "name", self._yscale.code)

get_aspect

get_aspect() -> str
Source code in python/pyplotrs/axes.py
def get_aspect(self) -> str:
    return self._aspect or "auto"

get_xticks

get_xticks() -> list[float]

The x tick positions that will be drawn.

Source code in python/pyplotrs/axes.py
def get_xticks(self) -> list[float]:
    """The x tick positions that will be drawn."""
    return [v for v, _ in self._xtick_pairs()]

get_yticks

get_yticks() -> list[float]
Source code in python/pyplotrs/axes.py
def get_yticks(self) -> list[float]:
    return [v for v, _ in self._ytick_pairs()]

get_xticklabels

get_xticklabels() -> list[str]

The x tick label strings that will be drawn.

Source code in python/pyplotrs/axes.py
def get_xticklabels(self) -> list[str]:
    """The x tick label strings that will be drawn."""
    return [s for _, s in self._xtick_pairs()]

get_yticklabels

get_yticklabels() -> list[str]
Source code in python/pyplotrs/axes.py
def get_yticklabels(self) -> list[str]:
    return [s for _, s in self._ytick_pairs()]

get_legend_handles_labels

get_legend_handles_labels() -> tuple[list[dict], list[str]]

(handles, labels) for the labeled marks, in draw order.

A handle is the mark's own dict: pyplotrs has no Artist objects, and the mark is what the legend key gets drawn from.

Source code in python/pyplotrs/axes.py
def get_legend_handles_labels(self) -> tuple[list[dict], list[str]]:
    """``(handles, labels)`` for the labeled marks, in draw order.

    A handle is the mark's own dict: pyplotrs has no Artist objects, and
    the mark *is* what the legend key gets drawn from."""
    entries = self._legend_entries()
    return list(entries), [e["label"] for e in entries]

PolarAxes

PolarAxes

PolarAxes(theme: Theme | None = None)

Bases: _AxesBase

A polar axes: plot(theta, r) and scatter(theta, r).

Angles are in radians, measured counter-clockwise from the positive x-axis (East), matching matplotlib's default; change this with set(theta_zero_location=...) / set(theta_direction=...). Create one with subplots(projection="polar") or add_subplot(spec, projection="polar").

Source code in python/pyplotrs/polar.py
def __init__(self, theme: Theme | None = None) -> None:
    self._init_common(theme)
    self._marks: list[dict] = []
    self._xlabel: str | None = None  # kept for _accessible_text() compatibility
    self._ylabel: str | None = None
    self._rmin = 0.0
    self._rmax: float | None = None
    self._rticks: list[float] | None = None
    self._thetagrids_deg: list[float] | None = None  # spoke angles, degrees
    self._theta_offset = 0.0  # angle (rad) drawn at theta == 0
    self._theta_dir = 1  # +1 counter-clockwise (default), -1 clockwise
    self._rlabel_deg = 22.5  # angle (deg) along which radial labels sit

plot

plot(
    theta,
    r,
    *,
    label: str | None = None,
    color=None,
    linewidth: float | None = None,
    alpha: float = 1.0,
    linestyle: str = "solid",
    marker: str | None = None,
    markersize: float = 5.0,
    zorder: float = 0.0
) -> "PolarAxes"

Line through polar points (theta, r) (theta in radians).

Source code in python/pyplotrs/polar.py
def plot(self, theta, r, *, label: str | None = None, color=None,
         linewidth: float | None = None, alpha: float = 1.0,
         linestyle: str = "solid",
         marker: str | None = None, markersize: float = 5.0,
         zorder: float = 0.0) -> "PolarAxes":
    """Line through polar points ``(theta, r)`` (``theta`` in radians)."""
    _check_marker(marker)
    self._marks.append({
        "zorder": float(zorder),
        "kind": "line",
        "theta": [float(t) for t in theta],
        "r": [float(v) for v in r],
        "label": label,
        "color": self._mark_color(color, alpha),
        "linewidth": self._theme.line_width if linewidth is None else float(linewidth),
        "linestyle": linestyle,
        "marker": marker,
        "markersize": float(markersize),
    })
    return self

scatter

scatter(
    theta,
    r,
    *,
    label: str | None = None,
    color=None,
    markersize: float | None = None,
    alpha: float = 1.0,
    marker: str = "o",
    edgecolor=None,
    size: float | None = None,
    zorder: float = 0.0
) -> "PolarAxes"

Scatter polar points (theta, r) (theta in radians).

markersize is a diameter in points; size is the matplotlib-style area in pt² (see Axes.scatter).

Source code in python/pyplotrs/polar.py
def scatter(self, theta, r, *, label: str | None = None, color=None,
            markersize: float | None = None, alpha: float = 1.0,
            marker: str = "o", edgecolor=None,
            size: float | None = None, zorder: float = 0.0) -> "PolarAxes":
    """Scatter polar points ``(theta, r)`` (``theta`` in radians).

    ``markersize`` is a diameter in points; ``size`` is the matplotlib-style
    area in pt² (see ``Axes.scatter``)."""
    _check_marker(marker)
    self._marks.append({
        "zorder": float(zorder),
        "kind": "scatter",
        "theta": [float(t) for t in theta],
        "r": [float(v) for v in r],
        "label": label,
        "color": self._mark_color(color, alpha),
        "markersize": self._marker_diameter(markersize, size),
        "marker": marker,
        "edgecolor": None if edgecolor is None else self._theme.resolve(edgecolor),
    })
    return self

legend

legend(
    *,
    loc: str | None = None,
    ncol: int = 1,
    title: str | None = None,
    frameon: bool = True,
    fontsize: float | None = None
)

Enable an auto-legend over this axes' labeled marks.

loc is best / upper right / upper left / lower right / lower left / upper center / lower center; None uses this axes class's default. best picks the corner that overlaps the data least.

ncol lays the keys out in that many columns, filled down then across - the usual fix for a legend tall enough to crowd the data. title puts a heading above the keys, frameon=False drops the box and its background, and fontsize overrides theme.legend_size for this legend only.

Source code in python/pyplotrs/axes.py
def legend(self, *, loc: str | None = None, ncol: int = 1,
           title: str | None = None, frameon: bool = True,
           fontsize: float | None = None):
    """Enable an auto-legend over this axes' labeled marks.

    ``loc`` is ``best`` / ``upper right`` / ``upper left`` / ``lower right``
    / ``lower left`` / ``upper center`` / ``lower center``; ``None`` uses
    this axes class's default. ``best`` picks the corner that overlaps the
    data least.

    ``ncol`` lays the keys out in that many columns, filled down then
    across - the usual fix for a legend tall enough to crowd the data.
    ``title`` puts a heading above the keys, ``frameon=False`` drops the
    box and its background, and ``fontsize`` overrides ``theme.legend_size``
    for this legend only.
    """
    self._legend = {
        "loc": self._LEGEND_DEFAULT_LOC if loc is None else loc,
        "ncol": int(ncol), "title": title, "frameon": bool(frameon),
        "fontsize": None if fontsize is None else float(fontsize),
    }
    return self

set

set(
    *,
    title=None,
    rmin=None,
    rmax=None,
    rticks=None,
    thetagrids=None,
    theta_zero_location=None,
    theta_direction=None,
    rlabel_position=None
) -> "PolarAxes"

Set polar options: title; radial limits rmin/rmax; explicit rticks (radii) and thetagrids (spoke angles, degrees); the zero location ("E"/"N"/"W"/"S" or radians); the theta_direction (1 counter-clockwise or -1 clockwise); and rlabel_position (the angle in degrees along which radial tick labels are placed).

Source code in python/pyplotrs/polar.py
def set(self, *, title=None, rmin=None, rmax=None, rticks=None, thetagrids=None,
        theta_zero_location=None, theta_direction=None,
        rlabel_position=None) -> "PolarAxes":
    """Set polar options: ``title``; radial limits ``rmin``/``rmax``; explicit
    ``rticks`` (radii) and ``thetagrids`` (spoke angles, degrees); the zero
    location (``"E"``/``"N"``/``"W"``/``"S"`` or radians); the ``theta_direction``
    (``1`` counter-clockwise or ``-1`` clockwise); and ``rlabel_position`` (the
    angle in degrees along which radial tick labels are placed)."""
    if title is not None:
        self._title = title
    if rmin is not None:
        self._rmin = float(rmin)
    if rmax is not None:
        self._rmax = float(rmax)
    if rticks is not None:
        self._rticks = [float(v) for v in rticks]
    if thetagrids is not None:
        self._thetagrids_deg = [float(v) for v in thetagrids]
    if theta_zero_location is not None:
        self._theta_offset = _theta_zero(theta_zero_location)
    if theta_direction is not None:
        self._theta_dir = -1 if theta_direction in (-1, "clockwise", "cw") else 1
    if rlabel_position is not None:
        self._rlabel_deg = float(rlabel_position)
    return self

get_rlim

get_rlim() -> tuple[float, float]

Effective radial limits: explicit if set, else out to the data.

Source code in python/pyplotrs/polar.py
def get_rlim(self) -> tuple[float, float]:
    """Effective radial limits: explicit if set, else out to the data."""
    return self._rlimits()

get_rticks

get_rticks() -> list[float]
Source code in python/pyplotrs/polar.py
def get_rticks(self) -> list[float]:
    return list(self._rticks) if self._rticks else []

get_thetagrids

get_thetagrids() -> list[float]

Spoke angles in degrees.

Source code in python/pyplotrs/polar.py
def get_thetagrids(self) -> list[float]:
    """Spoke angles in degrees."""
    return list(self._thetagrids_deg) if self._thetagrids_deg else []

get_theta_direction

get_theta_direction() -> int
Source code in python/pyplotrs/polar.py
def get_theta_direction(self) -> int:
    return self._theta_dir

Axes3D

Axes3D

Axes3D(theme: Theme | None = None)

Bases: _AxesBase

A 3D axes. Marks (scatter/plot/surface) are projected to 2D paths by an orthographic camera and depth-sorted, then drawn through the normal IR.

Source code in python/pyplotrs/axes3d.py
def __init__(self, theme: Theme | None = None) -> None:
    self._init_common(theme)
    self._marks3: list[dict] = []
    self._xlabel: str | None = None
    self._ylabel: str | None = None
    self._zlabel: str | None = None
    self._xlim: tuple[float, float] | None = None
    self._ylim: tuple[float, float] | None = None
    self._zlim: tuple[float, float] | None = None
    self._elev = 30.0
    self._azim = -60.0

scatter

scatter(
    xs,
    ys,
    zs,
    *,
    label: str | None = None,
    color=None,
    markersize: float | None = None,
    alpha: float = 1.0,
    marker: str = "o",
    edgecolor=None,
    size: float | None = None
) -> "Axes3D"

Scatter 3D points at (xs, ys, zs).

markersize is a diameter in points; size is the matplotlib-style area in pt² (see Axes.scatter).

Source code in python/pyplotrs/axes3d.py
def scatter(self, xs, ys, zs, *, label: str | None = None, color=None,
            markersize: float | None = None, alpha: float = 1.0,
            marker: str = "o", edgecolor=None, size: float | None = None) -> "Axes3D":
    """Scatter 3D points at ``(xs, ys, zs)``.

    ``markersize`` is a diameter in points; ``size`` is the matplotlib-style
    area in pt² (see ``Axes.scatter``)."""
    _check_marker(marker)
    self._marks3.append({
        "kind": "scatter",
        "xs": [float(x) for x in xs],
        "ys": [float(y) for y in ys],
        "zs": [float(z) for z in zs],
        "label": label,
        "color": self._mark_color(color, alpha),
        "markersize": self._marker_diameter(markersize, size),
        "marker": marker,
        "edgecolor": None if edgecolor is None else self._theme.resolve(edgecolor),
    })
    return self

plot

plot(
    xs,
    ys,
    zs,
    *,
    label: str | None = None,
    color=None,
    linewidth: float = 1.5,
    alpha: float = 1.0,
    linestyle: str = "solid",
    depthsort: bool = True
) -> "Axes3D"

Draw a 3D polyline through (xs, ys, zs).

depthsort controls how the line takes part in the painter's-order pass. With it on (the default) each segment is sorted separately, so the line occludes itself and interleaves correctly with surfaces and points it passes through - which is what makes a knotted or spiraling curve read as 3D at all. That costs one stroked path per segment, and on a long line the rasterizer notices.

With it off the whole polyline is one path at a single depth: much faster on dense lines, and what matplotlib's mplot3d always does, at the cost of a line that cannot pass behind anything - including itself.

Source code in python/pyplotrs/axes3d.py
def plot(self, xs, ys, zs, *, label: str | None = None, color=None,
         linewidth: float = 1.5, alpha: float = 1.0,
         linestyle: str = "solid", depthsort: bool = True) -> "Axes3D":
    """Draw a 3D polyline through ``(xs, ys, zs)``.

    ``depthsort`` controls how the line takes part in the painter's-order
    pass. With it on (the default) each **segment** is sorted separately,
    so the line occludes itself and interleaves correctly with surfaces and
    points it passes through - which is what makes a knotted or spiraling
    curve read as 3D at all. That costs one stroked path per segment, and
    on a long line the rasterizer notices.

    With it off the whole polyline is one path at a single depth: much
    faster on dense lines, and what matplotlib's mplot3d always does, at
    the cost of a line that cannot pass behind anything - including itself.
    """
    self._marks3.append({
        "kind": "line",
        "xs": [float(x) for x in xs],
        "ys": [float(y) for y in ys],
        "zs": [float(z) for z in zs],
        "label": label,
        "color": self._mark_color(color, alpha),
        "linewidth": float(linewidth),
        "linestyle": linestyle,
        "depthsort": bool(depthsort),
    })
    return self

surface

surface(
    X,
    Y,
    Z,
    *,
    cmap="viridis",
    alpha: float = 1.0,
    label: str | None = None
) -> "Axes3D"

Draw a colormapped surface over the grid (X, Y, Z).

Source code in python/pyplotrs/axes3d.py
def surface(self, X, Y, Z, *, cmap="viridis", alpha: float = 1.0,
            label: str | None = None) -> "Axes3D":
    """Draw a colormapped surface over the grid ``(X, Y, Z)``."""
    gx, gy, gz, nr, nc = _grid_xyz(X, Y, Z)
    zflat = [v for row in gz for v in row]
    cm = _colormaps.get_cmap(cmap)
    self._marks3.append({
        "kind": "surface",
        "gx": gx,
        "gy": gy,
        "gz": gz,
        "nr": nr,
        "nc": nc,
        "xflat": [v for row in gx for v in row],
        "yflat": [v for row in gy for v in row],
        "zflat": zflat,
        "zmin": min(zflat) if zflat else 0.0,
        "zmax": max(zflat) if zflat else 1.0,
        "cmap": cm,
        "alpha": float(alpha),
        "label": label,
        # Legend fallback: the colormap's midpoint stands for the surface.
        "color": _with_alpha(cm(0.5), alpha),
    })
    return self

bar3d

bar3d(
    x,
    y,
    z,
    dx,
    dy,
    dz,
    *,
    color=None,
    alpha: float = 1.0,
    label: str | None = None
) -> "Axes3D"

Draw 3D bars (boxes): base corners (x, y, z) with sizes (dx, dy, dz) (each a scalar or per-bar array).

Source code in python/pyplotrs/axes3d.py
def bar3d(self, x, y, z, dx, dy, dz, *, color=None, alpha: float = 1.0,
          label: str | None = None) -> "Axes3D":
    """Draw 3D bars (boxes): base corners ``(x, y, z)`` with sizes
    ``(dx, dy, dz)`` (each a scalar or per-bar array)."""
    xs = [float(v) for v in x]
    n = len(xs)
    self._marks3.append({
        "kind": "bar3d", "xs": xs, "ys": [float(v) for v in y],
        "zs": [float(v) for v in z], "dx": _as_seq(dx, n), "dy": _as_seq(dy, n),
        "dz": _as_seq(dz, n), "color": self._mark_color(color, alpha), "label": label,
    })
    return self

plot_wireframe

plot_wireframe(
    X,
    Y,
    Z,
    *,
    color=None,
    linewidth: float = 0.8,
    alpha: float = 1.0,
    label: str | None = None
) -> "Axes3D"

Draw the grid (X, Y, Z) as a wireframe (row + column lines).

Source code in python/pyplotrs/axes3d.py
def plot_wireframe(self, X, Y, Z, *, color=None, linewidth: float = 0.8,
                   alpha: float = 1.0, label: str | None = None) -> "Axes3D":
    """Draw the grid ``(X, Y, Z)`` as a wireframe (row + column lines)."""
    gx, gy, gz, nr, nc = _grid_xyz(X, Y, Z)
    self._marks3.append({
        "kind": "wireframe", "gx": gx, "gy": gy, "gz": gz, "nr": nr, "nc": nc,
        "xflat": [v for row in gx for v in row],
        "yflat": [v for row in gy for v in row],
        "zflat": [v for row in gz for v in row],
        "color": self._mark_color(color, alpha), "linewidth": float(linewidth),
        "label": label,
    })
    return self

contour3d

contour3d(
    X,
    Y,
    Z,
    *,
    levels=None,
    cmap="viridis",
    linewidth: float = 1.5,
    alpha: float = 1.0,
    label: str | None = None
) -> "Axes3D"

Draw contour lines of the grid (X, Y, Z) at their z-heights (marching squares in Rust); each level colored from cmap.

Source code in python/pyplotrs/axes3d.py
def contour3d(self, X, Y, Z, *, levels=None, cmap="viridis",
              linewidth: float = 1.5, alpha: float = 1.0,
              label: str | None = None) -> "Axes3D":
    """Draw contour lines of the grid ``(X, Y, Z)`` at their z-heights
    (marching squares in Rust); each level colored from ``cmap``."""
    gx, gy, gz, nr, nc = _grid_xyz(X, Y, Z)
    flat = [v for row in gz for v in row]
    lvls = _auto_levels(flat, levels)
    lines = _core.contour_lines(flat, nc, nr, lvls)
    cm = _colormaps.get_cmap(cmap)
    lo, hi = (min(lvls), max(lvls)) if lvls else (0.0, 1.0)
    span = (hi - lo) or 1.0
    colors = [_with_alpha(cm((lv - lo) / span), alpha) for lv in lvls]
    self._marks3.append({
        "kind": "contour3d", "lines": lines, "gx": gx, "gy": gy, "levels": lvls,
        "colors": colors, "linewidth": float(linewidth), "label": label,
        # Legend fallback: the middle level's color stands for the line set.
        "color": colors[len(colors) // 2] if colors else _with_alpha((0, 0, 0, 255), alpha),
        "xflat": [v for row in gx for v in row],
        "yflat": [v for row in gy for v in row], "zflat": flat,
    })
    return self

plot_trisurf

plot_trisurf(
    x,
    y,
    z,
    *,
    triangles=None,
    cmap="viridis",
    alpha: float = 1.0,
    label: str | None = None
) -> "Axes3D"

Surface over scattered points (x, y, z): Delaunay-triangulate the (x, y) plane (unless triangles index-triples are given) and shade each facet by mean z.

Source code in python/pyplotrs/axes3d.py
def plot_trisurf(self, x, y, z, *, triangles=None, cmap="viridis",
                 alpha: float = 1.0, label: str | None = None) -> "Axes3D":
    """Surface over scattered points ``(x, y, z)``: Delaunay-triangulate the
    ``(x, y)`` plane (unless ``triangles`` index-triples are given) and shade
    each facet by mean z."""
    xs = [float(v) for v in x]
    ys = [float(v) for v in y]
    zs = [float(v) for v in z]
    tris = triangles if triangles is not None else _delaunay(list(zip(xs, ys)))
    cm = _colormaps.get_cmap(cmap)
    self._marks3.append({
        "kind": "trisurf", "xs": xs, "ys": ys, "zs": zs,
        "tris": [tuple(t) for t in tris], "cmap": cm, "alpha": float(alpha),
        "zmin": min(zs) if zs else 0.0, "zmax": max(zs) if zs else 1.0,
        "xflat": xs, "yflat": ys, "zflat": zs, "label": label,
        # Legend fallback: the colormap's midpoint stands for the surface.
        "color": _with_alpha(cm(0.5), alpha),
    })
    return self

quiver3d

quiver3d(
    x,
    y,
    z,
    u,
    v,
    w,
    *,
    length: float = 1.0,
    color=None,
    linewidth: float = 1.5,
    alpha: float = 1.0,
    label: str | None = None
) -> "Axes3D"

Draw 3D arrows (u, v, w) rooted at (x, y, z), scaled by length.

Source code in python/pyplotrs/axes3d.py
def quiver3d(self, x, y, z, u, v, w, *, length: float = 1.0, color=None,
             linewidth: float = 1.5, alpha: float = 1.0,
             label: str | None = None) -> "Axes3D":
    """Draw 3D arrows ``(u, v, w)`` rooted at ``(x, y, z)``, scaled by
    ``length``."""
    self._marks3.append({
        "kind": "quiver3d", "xs": [float(v) for v in x], "ys": [float(v) for v in y],
        "zs": [float(v) for v in z], "us": [float(v) for v in u],
        "vs": [float(v) for v in v], "ws": [float(v) for v in w],
        "length": float(length), "color": self._mark_color(color, alpha),
        "linewidth": float(linewidth), "label": label,
    })
    # Autoscale should include arrow tips.
    self._marks3[-1]["xflat"] = [px + length * uu for px, uu in
                                 zip(self._marks3[-1]["xs"], self._marks3[-1]["us"])] + self._marks3[-1]["xs"]
    self._marks3[-1]["yflat"] = [py + length * vv for py, vv in
                                 zip(self._marks3[-1]["ys"], self._marks3[-1]["vs"])] + self._marks3[-1]["ys"]
    self._marks3[-1]["zflat"] = [pz + length * ww for pz, ww in
                                 zip(self._marks3[-1]["zs"], self._marks3[-1]["ws"])] + self._marks3[-1]["zs"]
    return self

voxels

voxels(
    filled,
    *,
    color=None,
    edgecolor=None,
    alpha: float = 1.0,
    label: str | None = None
) -> "Axes3D"

Draw a 3D boolean occupancy grid filled[i][j][k] as unit cubes.

Source code in python/pyplotrs/axes3d.py
def voxels(self, filled, *, color=None, edgecolor=None, alpha: float = 1.0,
           label: str | None = None) -> "Axes3D":
    """Draw a 3D boolean occupancy grid ``filled[i][j][k]`` as unit cubes."""
    color = self._mark_color(color, alpha)
    cells = []
    for i, plane in enumerate(filled):
        for j, row in enumerate(plane):
            for k, on in enumerate(row):
                if on:
                    cells.append((i, j, k))
    self._marks3.append({
        "kind": "voxels", "cells": cells, "color": color, "label": label,
        "edgecolor": None if edgecolor is None else self._theme.resolve(edgecolor),
        "xflat": [0.0] + [c[0] + 1 for c in cells],
        "yflat": [0.0] + [c[1] + 1 for c in cells],
        "zflat": [0.0] + [c[2] + 1 for c in cells],
    })
    return self

legend

legend(
    *,
    loc: str | None = None,
    ncol: int = 1,
    title: str | None = None,
    frameon: bool = True,
    fontsize: float | None = None
)

Enable an auto-legend over this axes' labeled marks.

loc is best / upper right / upper left / lower right / lower left / upper center / lower center; None uses this axes class's default. best picks the corner that overlaps the data least.

ncol lays the keys out in that many columns, filled down then across - the usual fix for a legend tall enough to crowd the data. title puts a heading above the keys, frameon=False drops the box and its background, and fontsize overrides theme.legend_size for this legend only.

Source code in python/pyplotrs/axes.py
def legend(self, *, loc: str | None = None, ncol: int = 1,
           title: str | None = None, frameon: bool = True,
           fontsize: float | None = None):
    """Enable an auto-legend over this axes' labeled marks.

    ``loc`` is ``best`` / ``upper right`` / ``upper left`` / ``lower right``
    / ``lower left`` / ``upper center`` / ``lower center``; ``None`` uses
    this axes class's default. ``best`` picks the corner that overlaps the
    data least.

    ``ncol`` lays the keys out in that many columns, filled down then
    across - the usual fix for a legend tall enough to crowd the data.
    ``title`` puts a heading above the keys, ``frameon=False`` drops the
    box and its background, and ``fontsize`` overrides ``theme.legend_size``
    for this legend only.
    """
    self._legend = {
        "loc": self._LEGEND_DEFAULT_LOC if loc is None else loc,
        "ncol": int(ncol), "title": title, "frameon": bool(frameon),
        "fontsize": None if fontsize is None else float(fontsize),
    }
    return self

set

set(
    *,
    title=None,
    xlabel=None,
    ylabel=None,
    zlabel=None,
    xlim=None,
    ylim=None,
    zlim=None,
    elev=None,
    azim=None
) -> "Axes3D"
Source code in python/pyplotrs/axes3d.py
def set(self, *, title=None, xlabel=None, ylabel=None, zlabel=None,
        xlim=None, ylim=None, zlim=None, elev=None, azim=None) -> "Axes3D":
    if title is not None:
        self._title = title
    if xlabel is not None:
        self._xlabel = xlabel
    if ylabel is not None:
        self._ylabel = ylabel
    if zlabel is not None:
        self._zlabel = zlabel
    if xlim is not None:
        self._xlim = (float(xlim[0]), float(xlim[1]))
    if ylim is not None:
        self._ylim = (float(ylim[0]), float(ylim[1]))
    if zlim is not None:
        self._zlim = (float(zlim[0]), float(zlim[1]))
    if elev is not None:
        self._elev = float(elev)
    if azim is not None:
        self._azim = float(azim)
    return self

get_xlim

get_xlim() -> tuple[float, float]

Effective x limits: explicit if set, else the data cube's extent.

Source code in python/pyplotrs/axes3d.py
def get_xlim(self) -> tuple[float, float]:
    """Effective x limits: explicit if set, else the data cube's extent."""
    return self._limits()[0]

get_ylim

get_ylim() -> tuple[float, float]
Source code in python/pyplotrs/axes3d.py
def get_ylim(self) -> tuple[float, float]:
    return self._limits()[1]

get_zlim

get_zlim() -> tuple[float, float]
Source code in python/pyplotrs/axes3d.py
def get_zlim(self) -> tuple[float, float]:
    return self._limits()[2]

get_xlabel

get_xlabel() -> str | None
Source code in python/pyplotrs/axes3d.py
def get_xlabel(self) -> str | None:
    return self._xlabel

get_ylabel

get_ylabel() -> str | None
Source code in python/pyplotrs/axes3d.py
def get_ylabel(self) -> str | None:
    return self._ylabel

get_zlabel

get_zlabel() -> str | None
Source code in python/pyplotrs/axes3d.py
def get_zlabel(self) -> str | None:
    return self._zlabel

get_view

get_view() -> tuple[float, float]

The camera as (elev, azim) in degrees.

Source code in python/pyplotrs/axes3d.py
def get_view(self) -> tuple[float, float]:
    """The camera as ``(elev, azim)`` in degrees."""
    return (self._elev, self._azim)

Fonts

These module-level helpers configure body-font resolution, and which family $...$ math is drawn in.

set_font_family

set_font_family(*families) -> None

Set the preferred sans-serif family names for body text, tried in order.

pyplotrs' analog of matplotlib's rcParams["font.sans-serif"]. The default is Arial, Helvetica, Liberation Sans: the host's Arial is used if installed, else Helvetica, else the bundled Liberation Sans (Arial-metric-compatible). Whichever is chosen is embedded into every saved figure (PDF/SVG/PNG/HTML), so a saved file always looks identical when viewed on another machine.

Accepts either a single iterable or positional names::

pyplotrs.set_font_family("Calibri", "Arial")
pyplotrs.set_font_family(["Calibri", "Arial"])

Call with no arguments to restore the default. Arial and Helvetica are proprietary and never shipped with pyplotrs; they are only used if already present on the machine.

Source code in python/pyplotrs/__init__.py
def set_font_family(*families) -> None:
    """Set the preferred sans-serif family names for body text, tried in order.

    pyplotrs' analog of matplotlib's ``rcParams["font.sans-serif"]``. The
    default is ``Arial``, ``Helvetica``, ``Liberation Sans``: the host's Arial
    is used if installed, else Helvetica, else the bundled Liberation Sans
    (Arial-metric-compatible). Whichever is chosen is **embedded into every
    saved figure** (PDF/SVG/PNG/HTML), so a saved file always looks identical
    when viewed on another machine.

    Accepts either a single iterable or positional names::

        pyplotrs.set_font_family("Calibri", "Arial")
        pyplotrs.set_font_family(["Calibri", "Arial"])

    Call with no arguments to restore the default. Arial and Helvetica are
    proprietary and never shipped with pyplotrs; they are only used if already
    present on the machine.
    """
    if len(families) == 1 and not isinstance(families[0], str):
        families = tuple(families[0])
    _core.set_sans_serif([str(f) for f in families])

get_font_family

get_font_family() -> list[str]

The preferred sans-serif family names, in order. Defaults to ["Arial", "Helvetica", "Liberation Sans"].

Source code in python/pyplotrs/__init__.py
def get_font_family() -> list[str]:
    """The preferred sans-serif family names, in order. Defaults to
    ``["Arial", "Helvetica", "Liberation Sans"]``."""
    return _core.get_sans_serif()

resolved_font_name

resolved_font_name() -> str

The family name body text actually resolves to on this host right now (e.g. "Arial" if installed, otherwise "Liberation Sans").

Source code in python/pyplotrs/__init__.py
def resolved_font_name() -> str:
    """The family name body text actually resolves to on this host right now
    (e.g. ``"Arial"`` if installed, otherwise ``"Liberation Sans"``)."""
    return _core.resolved_font_name()

resolved_font_variants

resolved_font_variants() -> list[tuple[str, str]]

What each body face resolves to here, as [(selector, font name), ...] for body, body-bold, body-italic and body-bolditalic.

Font matching is approximate: a family with no italic face resolves to its regular one, so asking for italic can quietly give you upright text. This makes that visible - two selectors reporting the same name means the host has no distinct face for one of them::

pyplotrs.resolved_font_variants()
# [('body', 'ArialMT'), ('body-bold', 'Arial-BoldMT'), ...]
Source code in python/pyplotrs/__init__.py
def resolved_font_variants() -> list[tuple[str, str]]:
    """What each body face resolves to here, as ``[(selector, font name), ...]``
    for ``body``, ``body-bold``, ``body-italic`` and ``body-bolditalic``.

    Font matching is approximate: a family with no italic face resolves to its
    regular one, so asking for italic can quietly give you upright text. This
    makes that visible - two selectors reporting the same name means the host
    has no distinct face for one of them::

        pyplotrs.resolved_font_variants()
        # [('body', 'ArialMT'), ('body-bold', 'Arial-BoldMT'), ...]
    """
    return _core.resolved_font_variants()

set_mathtext_fontset

set_mathtext_fontset(name: str = 'sans') -> None

Which family $...$ math is drawn in - pyplotrs' analog of matplotlib's rcParams["mathtext.fontset"].

"sans" (the default) sets math in your own body family wherever it has the glyphs - variables in its italic, Greek, digits and the common operators - and leaves the bundled STIX Two Math only what a text face cannot draw: big operators, radicals, stretchy fences, the blackboard / script / Fraktur alphabets, and any symbol your family is missing. Math then matches the labels around it, which is what matplotlib does by default.

"stix" sets every atom in STIX Two Math, so a span is uniformly serif. Pair it with a serif body family or the math will not match the text beside it::

pyplotrs.set_mathtext_fontset("stix")
pyplotrs.set_font_family("STIX Two Text", "Times New Roman")
Source code in python/pyplotrs/__init__.py
def set_mathtext_fontset(name: str = "sans") -> None:
    """Which family ``$...$`` math is drawn in - pyplotrs' analog of
    matplotlib's ``rcParams["mathtext.fontset"]``.

    ``"sans"`` (the default) sets math in your own body family wherever it has
    the glyphs - variables in its italic, Greek, digits and the common
    operators - and leaves the bundled STIX Two Math only what a text face
    cannot draw: big operators, radicals, stretchy fences, the blackboard /
    script / Fraktur alphabets, and any symbol your family is missing. Math
    then matches the labels around it, which is what matplotlib does by
    default.

    ``"stix"`` sets *every* atom in STIX Two Math, so a span is uniformly
    serif. Pair it with a serif body family or the math will not match the text
    beside it::

        pyplotrs.set_mathtext_fontset("stix")
        pyplotrs.set_font_family("STIX Two Text", "Times New Roman")
    """
    _core.set_mathtext_fontset(str(name))

get_mathtext_fontset

get_mathtext_fontset() -> str

The active math font set, "sans" or "stix" (see set_mathtext_fontset). Defaults to "sans".

Source code in python/pyplotrs/__init__.py
def get_mathtext_fontset() -> str:
    """The active math font set, ``"sans"`` or ``"stix"`` (see
    ``set_mathtext_fontset``). Defaults to ``"sans"``."""
    return _core.get_mathtext_fontset()

Number formatting

set_unicode_minus

set_unicode_minus(on: bool = True) -> None

Whether negative numeric labels are signed with U+2212 MINUS SIGN.

pyplotrs' analog of matplotlib's rcParams["axes.unicode_minus"], and on by default for the same reason: the minus is drawn on the math axis at the width of a +, where the ASCII hyphen-minus is a short, low word-joiner that leaves a tick column looking ragged.

Turn it off with set_unicode_minus(False) if labels must survive being copied out of a saved SVG/PDF and parsed back as numbers, or if a font you have set lacks the glyph::

pyplotrs.set_unicode_minus(False)

This governs labels pyplotrs formats from a number - axis and colorbar ticks, and the numeric ticker formatters. Text you supply yourself is never rewritten, and $...$ math always uses a real minus.

Source code in python/pyplotrs/__init__.py
def set_unicode_minus(on: bool = True) -> None:
    """Whether negative numeric labels are signed with U+2212 MINUS SIGN.

    pyplotrs' analog of matplotlib's ``rcParams["axes.unicode_minus"]``, and
    on by default for the same reason: the minus is drawn on the math axis at
    the width of a ``+``, where the ASCII hyphen-minus is a short, low
    word-joiner that leaves a tick column looking ragged.

    Turn it off with ``set_unicode_minus(False)`` if labels must survive being
    copied out of a saved SVG/PDF and parsed back as numbers, or if a font you
    have set lacks the glyph::

        pyplotrs.set_unicode_minus(False)

    This governs labels pyplotrs formats from a number - axis and colorbar
    ticks, and the numeric [`ticker`][pyplotrs.ticker] formatters. Text you supply
    yourself is never rewritten, and ``$...$`` math always uses a real minus.
    """
    ticker._UNICODE_MINUS = bool(on)

get_unicode_minus

get_unicode_minus() -> bool

Whether negative numeric labels use U+2212 (see set_unicode_minus). Defaults to True.

Source code in python/pyplotrs/__init__.py
def get_unicode_minus() -> bool:
    """Whether negative numeric labels use U+2212 (see
    ``set_unicode_minus``). Defaults to ``True``."""
    return ticker._UNICODE_MINUS