Source code for anyplotlib.axes._axes

"""
axes/_axes.py
=============
Grid-cell container that owns a single plot panel.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import numpy as np

from anyplotlib.plot1d import Plot1D, PlotBar
from anyplotlib.plot2d import Plot2D, PlotMesh
from anyplotlib.plot3d import Plot3D

if TYPE_CHECKING:
    from anyplotlib.figure import Figure
    from anyplotlib.figure._gridspec import SubplotSpec


[docs] class Axes: """A single grid cell in a Figure. Returned by Figure.add_subplot() and Figure.subplots(). Call .imshow() or .plot() to attach a data plot and get back a Plot2D or Plot1D object. """ def __init__(self, fig: "Figure", spec: "SubplotSpec"): # noqa: F821 self._fig = fig self._spec = spec self._plot: "Plot1D | Plot2D | None" = None # ------------------------------------------------------------------
[docs] def imshow(self, data, axes: list | None = None, units: str = "px", cmap: str | None = None, vmin: float | None = None, vmax: float | None = None, origin: str = "upper", gpu: "str | bool" = "auto", tile: "str | bool" = "auto", integration_method: str = "mean", overview_method: str = "mean", tile_backend=None) -> "Plot2D": """Attach a 2-D image to this axes cell. Parameters ---------- data : np.ndarray, shape (H, W) or (H, W, 3|4) Image data. 2-D arrays are colormapped. ``(H, W, 3)`` / ``(H, W, 4)`` arrays render as true-colour RGB(A): uint8 values are used directly; floats are interpreted as 0–1 (or 0–255 when the max exceeds 1). ``cmap``/``vmin``/``vmax`` and the colorbar do not apply to RGB images. axes : [x_axis, y_axis], optional Physical coordinate arrays for each axis. units : str, optional Axis units label. Default ``"px"``. cmap : str, optional Colormap name (e.g. ``"viridis"``, ``"inferno"``). Defaults to ``"gray"``. vmin, vmax : float, optional Colormap clipping limits in data units. Values outside this range are clamped to the colormap endpoints. Defaults to the data min / max. origin : ``"upper"`` | ``"lower"``, optional Where row 0 of the array is placed. ``"upper"`` (default) puts row 0 at the top, matching the usual image convention. ``"lower"`` puts row 0 at the bottom, matching the matplotlib convention for matrices / scientific plots. Returns ------- Plot2D """ x_axis = axes[0] if axes and len(axes) > 0 else None y_axis = axes[1] if axes and len(axes) > 1 else None plot = Plot2D(data, x_axis=x_axis, y_axis=y_axis, units=units, cmap=cmap, vmin=vmin, vmax=vmax, origin=origin, gpu=gpu, tile=tile, integration_method=integration_method, overview_method=overview_method, tile_backend=tile_backend) self._attach(plot) return plot
[docs] def pcolormesh(self, data: np.ndarray, x_edges=None, y_edges=None, units: str = "") -> "PlotMesh": """Attach a 2-D mesh to this axes cell using edge coordinates. Follows the matplotlib pcolormesh convention: x_edges and y_edges are the cell *edge* coordinates, so they have length N+1 and M+1 respectively for an (M, N) data array. Parameters ---------- data : np.ndarray shape (M, N) x_edges : array-like, length N+1, optional Column edge coordinates. Defaults to ``np.arange(N+1)``. y_edges : array-like, length M+1, optional Row edge coordinates. Defaults to ``np.arange(M+1)``. units : str, optional Returns ------- PlotMesh """ plot = PlotMesh(data, x_edges=x_edges, y_edges=y_edges, units=units) self._attach(plot) return plot
[docs] def plot_surface(self, X, Y, Z, *, colormap: str = "viridis", x_label: str = "x", y_label: str = "y", z_label: str = "z", azimuth: float = -60.0, elevation: float = 30.0, zoom: float = 1.0, bounds=None, texture=None, gpu: "str | bool" = "auto") -> "Plot3D": """Attach a 3-D surface to this axes cell. Parameters ---------- X, Y, Z : array-like 2-D grid arrays of the same shape (e.g. from ``np.meshgrid``), or 1-D centre arrays for X/Y with a 2-D Z. colormap : str, optional Matplotlib colormap name. Default ``'viridis'``. x_label, y_label, z_label : str, optional Axis labels. azimuth, elevation : float, optional Initial camera angles in degrees. zoom : float, optional Initial zoom factor. bounds : ((xmin, xmax), (ymin, ymax), (zmin, zmax)), optional Fix the axes bounds instead of fitting them to the data. Pass ``((-1, 1),) * 3`` for a unit sphere so it projects as a true circle rather than being stretched to fill the panel. texture : array-like, bytes, or path, optional Image to wrap around the surface — shorthand for :meth:`~anyplotlib.Plot3D.set_texture`, which takes the mapping, shading, and culling options. gpu : ``"auto"`` | bool, optional WebGPU acceleration policy for a *textured* surface (a colormapped one always renders on Canvas2D). ``"auto"`` (default) uses the GPU when available above ~2k triangles; ``True`` always attempts it; ``False`` forces Canvas2D. Falls back silently when WebGPU is unavailable — check :attr:`Plot3D.gpu_active`. Returns ------- Plot3D """ plot = Plot3D("surface", X, Y, Z, colormap=colormap, x_label=x_label, y_label=y_label, z_label=z_label, azimuth=azimuth, elevation=elevation, zoom=zoom, bounds=bounds, texture=texture, gpu=gpu) self._attach(plot) return plot
[docs] def scatter3d(self, x, y, z, *, color: str = "#4fc3f7", colors=None, point_size: float = 4.0, x_label: str = "x", y_label: str = "y", z_label: str = "z", azimuth: float = -60.0, elevation: float = 30.0, zoom: float = 1.0, bounds=None, gpu: str | bool = "auto") -> "Plot3D": """Attach a 3-D scatter plot to this axes cell. Parameters ---------- x, y, z : array-like, shape (N,) Point coordinates. color : str, optional CSS colour for all points. colors : list of "#rrggbb" or (N, 3) array, optional Per-point colours (overrides *color*). Floats are 0–1. point_size : float, optional Radius of each point in pixels. x_label, y_label, z_label : str, optional Axis labels. azimuth, elevation : float, optional Initial camera angles in degrees. zoom : float, optional Initial zoom factor. bounds : ((xmin, xmax), (ymin, ymax), (zmin, zmax)), optional Fix the axes bounds instead of fitting them to the data — keeps the origin and scale stable, e.g. ``((-1, 1),) * 3`` for unit vectors on a sphere. gpu : ``"auto"`` | bool, optional WebGPU acceleration policy. ``"auto"`` (default) renders on the GPU when available and the cloud exceeds ~20k points, else Canvas2D; ``True`` always attempts GPU; ``False`` forces Canvas2D. Falls back silently when WebGPU is unavailable — check :attr:`Plot3D.gpu_active` for the actual path. Returns ------- Plot3D """ plot = Plot3D("scatter", x, y, z, color=color, colors=colors, point_size=point_size, x_label=x_label, y_label=y_label, z_label=z_label, azimuth=azimuth, elevation=elevation, zoom=zoom, bounds=bounds, gpu=gpu) self._attach(plot) return plot
[docs] def voxels(self, x, y, z, *, colors=None, color: str = "#4fc3f7", size: float = 1.0, alpha: float = 0.3, x_label: str = "x", y_label: str = "y", z_label: str = "z", azimuth: float = -60.0, elevation: float = 30.0, zoom: float = 1.0, bounds=None, gpu: str | bool = "auto") -> "Plot3D": """Attach a 3-D voxel plot: shaded translucent cubes at the centres. Designed for volumetric grain/label maps. Add draggable :class:`~anyplotlib.PlaneWidget` slice selectors with ``plot.add_widget("plane", axis=..., position=...)`` — voxels lying on a plane render at ``voxel_slice_alpha`` (more opaque) so the selected slice pops out of the translucent volume. **Large volumes** With WebGPU (``gpu="auto"``, the default, active above ~1k cubes when a GPU is present) hundreds of thousands of voxels render interactively via instancing. On the Canvas2D fallback the budget is ~20k cubes (~3–6 µs each); a warning is emitted above that *only when* ``gpu=False``. For volumes too large even for the GPU (e.g. a 512 × 512 × 300 tomogram = 78M voxels), downsample with stride slicing (``vol[::s, ::s, ::s]``) or draw only grain-boundary voxels, and pair the 3-D overview with linked full-resolution 2-D slice panels — the voxel grain explorer example demonstrates this pattern. Parameters ---------- x, y, z : array-like, shape (N,) Voxel centre coordinates. colors : list of "#rrggbb" or (N, 3) array, optional Per-voxel colours (overrides *color*). Floats are 0–1. color : str, optional Single CSS colour when *colors* is omitted. size : float, optional Cube edge length in data units. Default 1. alpha : float, optional Base voxel opacity (0–1). Default 0.3. See also :meth:`Plot3D.set_voxel_alpha`. x_label, y_label, z_label : str, optional Axis labels. azimuth, elevation : float, optional Initial camera angles in degrees. zoom : float, optional Initial zoom factor. bounds : ((xmin, xmax), (ymin, ymax), (zmin, zmax)), optional Fix the axes bounds instead of fitting them to the data. gpu : ``"auto"`` | bool, optional WebGPU acceleration policy. ``"auto"`` (default) renders cubes on the GPU when available and the set exceeds ~1k; ``True`` always attempts GPU; ``False`` forces Canvas2D. Falls back silently when WebGPU is unavailable — see :attr:`Plot3D.gpu_active`. Returns ------- Plot3D """ plot = Plot3D("voxels", x, y, z, color=color, colors=colors, voxel_size=size, alpha=alpha, x_label=x_label, y_label=y_label, z_label=z_label, azimuth=azimuth, elevation=elevation, zoom=zoom, bounds=bounds, gpu=gpu) self._attach(plot) return plot
[docs] def plot3d(self, x, y, z, *, color: str = "#4fc3f7", linewidth: float = 1.5, x_label: str = "x", y_label: str = "y", z_label: str = "z", azimuth: float = -60.0, elevation: float = 30.0, zoom: float = 1.0) -> "Plot3D": """Attach a 3-D line plot to this axes cell. Parameters ---------- x, y, z : array-like, shape (N,) Point coordinates along the line. color : str, optional CSS colour. linewidth : float, optional Stroke width in pixels. x_label, y_label, z_label : str, optional Axis labels. azimuth, elevation : float, optional Initial camera angles in degrees. zoom : float, optional Initial zoom factor. Returns ------- Plot3D """ plot = Plot3D("line", x, y, z, color=color, linewidth=linewidth, x_label=x_label, y_label=y_label, z_label=z_label, azimuth=azimuth, elevation=elevation, zoom=zoom) self._attach(plot) return plot
[docs] def plot(self, data: np.ndarray, axes: list | None = None, units: str = "px", y_units: str = "", color: str = "#4fc3f7", linewidth: float = 1.5, linestyle: str = "solid", ls: str | None = None, alpha: float = 1.0, marker: str = "none", markersize: float = 4.0, label: str = "", yscale: str = "linear") -> "Plot1D": """Attach a 1-D line to this axes cell. Parameters ---------- data : array-like, shape (N,) Y values. Must be 1-D. axes : list, optional ``[x_axis]`` — a one-element list containing the x-coordinates (shape ``(N,)``). If omitted the x-axis defaults to ``0, 1, …, N-1``. units : str, optional Label for the x-axis (e.g. ``"eV"``, ``"s"``). Default ``"px"``. y_units : str, optional Label for the y-axis. Default ``""`` (no label). color : str, optional CSS colour string for the line (hex, ``rgb()``, named colour, etc.). Default ``"#4fc3f7"``. linewidth : float, optional Stroke width in pixels. Default ``1.5``. linestyle : str, optional Dash pattern. Accepted values: ``"solid"`` (``"-"``), ``"dashed"`` (``"--"``), ``"dotted"`` (``":"``), ``"dashdot"`` (``"-."``), or ``"none"`` to draw no connecting line at all — pair it with *marker* for a scatter. Default ``"solid"``. ls : str, optional Short alias for *linestyle*. Takes precedence if both are given. alpha : float, optional Line opacity in the range 0–1. Default ``1.0`` (fully opaque). marker : str, optional Per-point marker symbol. Supported values: ``"o"`` (circle), ``"s"`` (square), ``"^"`` (triangle-up), ``"v"`` (triangle-down), ``"D"`` (diamond), ``"+"`` (plus), ``"x"`` (cross), ``"none"`` (no markers). Default ``"none"``. markersize : float, optional Marker radius / half-side in pixels. Default ``4.0``. label : str, optional Legend label. A legend is only drawn when at least one line has a non-empty label. Default ``""`` (no legend entry). Returns ------- Plot1D Live plot object. Call methods on it to update data, add overlays, register callbacks, etc. Examples -------- Basic sine wave with a physical x-axis:: import numpy as np import anyplotlib as apl x = np.linspace(0, 4 * np.pi, 512) fig, ax = apl.subplots(1, 1, figsize=(620, 320)) v = ax.plot(np.sin(x), axes=[x], units="rad", color="#ff7043", linewidth=2, label="sin") v # display in a Jupyter cell Dashed line with semi-transparent markers:: v = ax.plot(data, linestyle="dashed", alpha=0.7, marker="o", markersize=4) Overlay a second curve with :meth:`Plot1D.add_line`:: v.add_line(np.cos(x), x_axis=x, color="#aed581", label="cos") """ x_axis = axes[0] if axes and len(axes) > 0 else None plot = Plot1D(data, x_axis=x_axis, units=units, y_units=y_units, color=color, linewidth=linewidth, linestyle=ls if ls is not None else linestyle, alpha=alpha, marker=marker, markersize=markersize, label=label, yscale=yscale) self._attach(plot) return plot
[docs] def semilogy(self, data: np.ndarray, axes: list | None = None, **kwargs) -> "Plot1D": """Attach a 1-D line with a logarithmic y-axis.""" kwargs.setdefault("yscale", "log") return self.plot(data, axes=axes, **kwargs)
[docs] def axes2d(self, *, xlim=(0.0, 1.0), ylim=(0.0, 1.0), aspect=None, units: str = "", y_units: str = "") -> "PlotXY": # noqa: F821 """Attach a blank **data-coordinate 2-D axis** (`PlotXY`). Unlike :meth:`plot` (a curve over a monotonic x-axis), this is a coordinate canvas: set ``xlim`` / ``ylim`` (+ optional ``aspect="equal"``) and draw :meth:`~anyplotlib.plotxy.PlotXY.scatter` / ``plot`` / ``fill`` / ``text`` as collection-style artists in data coordinates — matplotlib's ``transData`` + ``PathCollection`` model. Suits stereographic / IPF / pole-figure style plots. Examples -------- >>> import anyplotlib as apl >>> fig, ax = apl.subplots() >>> xy = ax.axes2d(xlim=(-1, 1), ylim=(-1, 1), aspect="equal") >>> xy.fill([0, 1, 0.5], [0, 0, 0.9], facecolor="#eee") # triangle >>> xy.scatter([0.3], [0.3], c="#f00", s=10) >>> xy.text(0.5, 0.95, r"$[111]$") """ from anyplotlib.plotxy import PlotXY plot = PlotXY(xlim=xlim, ylim=ylim, aspect=aspect, units=units, y_units=y_units) self._attach(plot) return plot
[docs] def bar(self, x, height=None, width: float = 0.8, bottom: float = 0.0, *, align: str = "center", color: str = "#4fc3f7", colors=None, orient: str = "v", log_scale: bool = False, group_labels=None, group_colors=None, show_values: bool = False, units: str = "", y_units: str = "", # ── legacy backward-compat kwargs ────────────────────────────── x_labels=None, x_centers=None, bar_width=None, baseline=None, values=None) -> "PlotBar": """Attach a bar chart to this axes cell. Signature mirrors ``matplotlib.pyplot.bar``:: ax.bar(x, height, width=0.8, bottom=0.0, ...) Parameters ---------- x : array-like of str or numeric Bar positions. Strings become category labels with auto-numeric centres; numbers are used directly as bar centres. height : array-like, shape ``(N,)`` or ``(N, G)``, optional Bar heights. Pass a 2-D array to draw *G* grouped bars per category. If omitted *x* is treated as the heights and positions are generated automatically (backward-compatible call form). width : float, optional Bar width as a fraction of the category slot (0–1). Default ``0.8``. bottom : float, optional Value at which bars are rooted (baseline). Default ``0``. align : ``"center"`` | ``"edge"``, optional Alignment of the bar relative to its *x* position. Currently only ``"center"`` is rendered; stored for future use. color : str, optional Single CSS colour applied to every bar. Default ``"#4fc3f7"``. colors : list of str, optional Per-bar colour list (ungrouped) or ignored when *group_colors* is set. orient : ``"v"`` | ``"h"``, optional Vertical (default) or horizontal orientation. log_scale : bool, optional Use a logarithmic value axis. Non-positive values are clamped to ``1e-10`` for display. Default ``False``. group_labels : list of str, optional Legend labels for each group in a grouped bar chart. group_colors : list of str, optional CSS colours per group. Defaults to a built-in palette. show_values : bool, optional Draw the numeric value above / beside each bar. units : str, optional Label for the categorical axis. y_units : str, optional Label for the value axis. Backward-compatible keyword aliases ------------------------------------ ``values`` → ``height`` ``x_centers`` → ``x`` ``bar_width`` → ``width`` ``baseline`` → ``bottom`` ``x_labels`` → strings passed via ``x`` Returns ------- PlotBar """ # ── legacy backward-compat resolution ───────────────────────────── if height is None: if values is not None: height = values else: height = x x = None if baseline is not None: bottom = baseline if bar_width is not None: width = bar_width plot = PlotBar(x, height, width=width, bottom=bottom, align=align, color=color, colors=colors, orient=orient, log_scale=log_scale, group_labels=group_labels, group_colors=group_colors, show_values=show_values, units=units, y_units=y_units, x_labels=x_labels, x_centers=x_centers) self._attach(plot) return plot
def _panel_id_from_spec(self) -> str: """Derive a deterministic, position-based panel ID from the SubplotSpec. The ID is ``"p"`` followed by the first 7 hex characters of a SHA-256 hash of the row/col bounds, e.g. ``"p6a2f3b1"``. This is: * **Deterministic** – the same SubplotSpec always produces the same ID across Python processes and after code edits. * **Starts with "p"** – satisfies the JS naming convention and makes it easy to grep for panel traits (``panel_{id}_json``). * **Short** – 8 characters total; safe to embed in CSS selectors. """ import hashlib as _hl key = f"{self._spec.row_start},{self._spec.row_stop},{self._spec.col_start},{self._spec.col_stop}" return "p" + _hl.sha256(key.encode()).hexdigest()[:7] def _attach(self, plot: "Plot1D | Plot2D | PlotMesh | Plot3D | PlotBar") -> None: """Register a plot on this axes (replace any previous plot).""" # Allocate a panel id if needed; reuse if replacing if self._plot is not None: panel_id = self._plot._id else: panel_id = self._panel_id_from_spec() plot._id = panel_id plot._fig = self._fig self._plot = plot self._fig._register_panel(self, plot) def __repr__(self) -> str: kind = type(self._plot).__name__ if self._plot else "empty" return f"Axes(rows={self._spec.row_start}:{self._spec.row_stop}, cols={self._spec.col_start}:{self._spec.col_stop}, {kind})"