From e9421b60b01067eda5366538efd4dfb25b9c5551 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 1 Jun 2026 12:32:26 -0700 Subject: [PATCH] Update docs --- miscplot/__init__.py | 2 + miscplot/variability.py | 106 ++++++++++++++++++++++++++++++---------- miscplot/wmap.py | 29 +++++++++-- 3 files changed, 107 insertions(+), 30 deletions(-) diff --git a/miscplot/__init__.py b/miscplot/__init__.py index bd10ef1..0b8ac40 100644 --- a/miscplot/__init__.py +++ b/miscplot/__init__.py @@ -1,3 +1,5 @@ +"""Miscellaneous matplotlib plots with small, focused public APIs.""" + from .variability import variability_plot as variability_plot from .wmap import wafermap as wafermap diff --git a/miscplot/variability.py b/miscplot/variability.py index 1c7af73..e3c8506 100644 --- a/miscplot/variability.py +++ b/miscplot/variability.py @@ -1,3 +1,5 @@ +"""Hierarchical categorical variability charts for matplotlib.""" + from typing import Sequence, Callable, Any, TYPE_CHECKING import threading import textwrap @@ -16,6 +18,12 @@ if TYPE_CHECKING: def twrap(text: str, **kwargs) -> str: + """Wrap label text while preserving underscores in the displayed output. + + Underscores are temporarily treated as wrap-friendly separators, then + restored after wrapping. Keyword arguments are forwarded to + `textwrap.fill`; `width` defaults to 15 characters. + """ kwargs.setdefault('width', 15) intxt = text.replace('_', '-') wrapped = textwrap.fill(intxt, **kwargs) @@ -35,27 +43,38 @@ def variability_plot( boxprops: dict[str, Any] | bool = True, meanprops: dict[str, Any] | bool = True, ) -> tuple[Figure, Axes, list[Axes], list[Axes]]: - """ - Create a variability plot (categorical box & scatter plot) + """Create a hierarchical categorical variability plot. + + The main axes show jittered scatter points, optional box plots, and optional + per-category means for `data_col`. Additional axes beneath the main plot + show the category labels for each grouping level, with coarser groups + spanning multiple finer groups. Args: - data_table: Dataset to plot. Passed directly to `polars.DataFrame()`. - data_col: Column to use for box/scatterplot value. - groups: Columns to group by. Coarsest grouping should be first, and will appear - furthest from the scatterplot, at the bottom of the figure. - vert_groups: Labels for these column names will be rotated (text will run vertically). - wrap_fn: Function called to wrap label text (i.e. insert newlines). + data_table: Dataset to plot. Passed directly to + `polars.DataFrame`. + data_col: Numeric column to plot on the y-axis. + groups: Columns to group by. The coarsest grouping should be first and + appears furthest from the scatter plot, at the bottom of the figure. + The finest grouping should be last and appears closest to the data. + vert_groups: Group names whose labels should be rotated vertically. + wrap_fn: Function called to wrap label text by inserting newlines. Default wraps at 15 characters, preferentially on underscores or whitespace. - mainplot_ratios: Scale factors setting the size of the main axes, relative to the size - of other axes. Default is (10, 10). - ylim: Y-limits for the scatter/box plot. Points which fall outside the limits are drawn - as red triangles at the edges. - dotprops: Passed as kwargs to scatterplot. - boxprops: Passed as kwargs to boxplot. - meanprops: Passed as kwargs to lineplot of means. + mainplot_ratios: Width and height scale factors for the main data axes, + relative to the label/header axes. + ylim: Optional y-limits for the scatter and box plot. Points outside + the limits are drawn as red triangles at the corresponding edge. + dotprops: `False` disables jittered scatter points. A dictionary is + passed as keyword arguments to `matplotlib.axes.Axes.scatter`. + boxprops: `False` disables box plots. A dictionary is passed as + keyword arguments to `matplotlib.axes.Axes.boxplot`. + meanprops: `False` disables the mean trace. A dictionary is passed as + keyword arguments to `matplotlib.axes.Axes.plot`. Returns: - figure, data axes, label axes, header axes + Tuple of `(figure, data_axes, label_axes, header_axes)`. The label and + header axes are ordered from the finest displayed row nearest the data + to the coarsest displayed row at the bottom. """ vert_groups = set(vert_groups) @@ -261,6 +280,12 @@ def variability_plot( label_stack_t = list[list[tuple[int, int, str]]] def debounce(func: Callable, delay_s: float = 0.05) -> Callable: + """Return a wrapper that calls `func` after resize events settle. + + Repeated calls within `delay_s` cancel the previous pending call. This + keeps expensive text-size recalculation from running continuously while a + matplotlib window is being resized. + """ timer = None def debounced_func(*args, **kwargs) -> None: nonlocal timer @@ -272,8 +297,18 @@ def debounce(func: Callable, delay_s: float = 0.05) -> Callable: def get_label_stack(df_groups: polars.DataFrame, groups: Sequence[str], wrap_fn: Callable) -> label_stack_t: - """ - For each level, get (xmin_inclusive, xmax_inclusive, wrapped_text_for_label) for all the labels + """Build label text and x-span metadata for each grouping level. + + Args: + df_groups: DataFrame containing one row per plotted category + combination and an `x_pos` column with integer plot positions. + groups: Grouping columns ordered from coarsest to finest. + wrap_fn: Function used to convert each label value to display text. + + Returns: + Nested list in grouping order. Each row contains + `(xmin_inclusive, xmax_inclusive, wrapped_label_text)` tuples for the + contiguous spans at that grouping level. """ label_stack = [] for ll, level in enumerate(groups): @@ -295,8 +330,14 @@ def get_label_stack(df_groups: polars.DataFrame, groups: Sequence[str], wrap_fn: def get_text_sizes(label_stack: label_stack_t) -> list[NDArray[numpy.float64]]: - """ - Transform the label stack (see `get_label_stack` into a stack of (allocated x-span, unrotated x-size, unrotated y-size) + """Measure unrotated label dimensions for layout calculations. + + Args: + label_stack: Label span metadata returned by `get_label_stack`. + + Returns: + One array per label level. Each row contains + `(allocated_x_span, unrotated_width_px, unrotated_height_px)`. """ fig, ax = pyplot.subplots() text_obj = ax.text(0, 0, 'placeholder') @@ -315,10 +356,21 @@ def get_text_sizes(label_stack: label_stack_t) -> list[NDArray[numpy.float64]]: def get_label_y_ratios(groups: Sequence[str], vert_groups: set[str], size_lists: list[NDArray[numpy.float64]]) -> list[float]: - """ - For each level, figure out max(rotated_x_size / x_span) and max(rotated_y_size). - Normalize so that the sum of y-values is equal to the number of levels. - Output order is reversed so that the bottom labels (most general) come last. + """Calculate relative GridSpec heights for hierarchical label rows. + + The calculation accounts for vertically rotated label groups, normalizes the + label rows so their total height equals the number of grouping levels, and + reverses the output for the figure layout so the coarsest labels appear at + the bottom. + + Args: + groups: Grouping columns ordered from coarsest to finest. + vert_groups: Set of group names whose labels are rotated vertically. + size_lists: Text measurement arrays returned by + `get_text_sizes`. + + Returns: + Height ratios ordered for the label rows in the matplotlib GridSpec. """ grouping_rotated = numpy.array([grouping in vert_groups for grouping in groups], dtype=bool) level_dims = [] @@ -335,8 +387,10 @@ def get_label_y_ratios(groups: Sequence[str], vert_groups: set[str], size_lists: def _mk_data(filename: str) -> None: - """ - Make some dummy data and write it to a csv file + """Create synthetic variability-chart data for the module demo. + + Args: + filename: Path where the generated CSV data should be written. """ rows = [] rng = numpy.random.default_rng(seed=0) diff --git a/miscplot/wmap.py b/miscplot/wmap.py index 4aba0f8..1b8d4e4 100644 --- a/miscplot/wmap.py +++ b/miscplot/wmap.py @@ -1,3 +1,5 @@ +"""Wafer-style tiled heat maps for matplotlib.""" + from typing import Any, Callable from matplotlib import pyplot @@ -17,12 +19,30 @@ def wafermap( xy2sn: Callable[[int, int], str] | None = None, aspect_ratio: float = 1, ) -> tuple[Figure, Axes]: - """ - Wafer map plot + """Plot sparse `(x, y, z)` samples as a tiled wafer-style heat map. - This is effectively a pcolor plot which does a clearer job showing that data may come - from anywhere within a tile, and correctly shows missing data. + The plot bins each sample into integer-spaced x/y tiles, draws one color + per populated tile, and leaves unpopulated tiles transparent. This is useful + for spatial measurements where each value belongs to a die, cell, or other + tile rather than to an exact point location. + Args: + xyz: Array-like object with three columns: x coordinate, y coordinate, + and z value to color. Coordinates are binned with unit spacing based + on their floored values. + pcoloropts: Optional keyword arguments passed to + `matplotlib.axes.Axes.pcolor`. Defaults are added for + `shading`, `edgecolors`, and `cmap` when they are not already + present. + zlabel: Label for the colorbar. + xy2sn: Optional callback that receives the binned x/y indices and + returns text to draw over populated tiles. + aspect_ratio: Scale factor applied to x coordinates before plotting. + Use values below 1 for horizontally compressed layouts and values + above 1 for horizontally stretched layouts. + + Returns: + The created matplotlib `(figure, axes)` pair. """ fig, ax = pyplot.subplots() @@ -62,6 +82,7 @@ def wafermap( def example() -> None: + """Run an interactive wafer map example with synthetic radial data.""" rng = numpy.random.default_rng(0) ar = 0.5 ny = 10