# HeatmapSelect API


`HeatmapSelect` renders a 2D array as a dense grid — one image pixel per cell, in the spirit of the parameter spaces in Bret Victor's *Up and Down the Ladder of Abstraction*. Hover or click a cell in the body to pick one point of the sweep, or grab the left or bottom gutter to pin a whole row or column. The three pins are independent and coexist, so you can hold a cell, a row and a column at once. The values behind the picture never cross the wire: the widget reports indices and you do the slicing, which is what lets a 14 641-cell field stay a single PNG. Reach for it when a sweep has two knobs and you want to see the runs behind any slice of it.


See also: ChartSelect for box and lasso selection over a matplotlib figure, ChartPuck for dragging a single point across a chart, and Slider2D for picking a continuous `(x, y)` pair rather than a grid cell.


 Bases: `AnyWidget`


A dense parameter-space grid where you pick one cell or a whole row/column.


Modelled on the grid in Bret Victor's *Up and Down the Ladder of Abstraction*. The body picks a single cell; the left gutter picks a whole row (a horizontal band); the bottom gutter picks a whole column (a vertical band).


Hovering previews, clicking *pins*. The three pins are independent and coexist — you can hold a cell, a row and a column at once, and clicking one region only replaces that region's pin. Double-clicking a region drops just that pin. What to make of a combination is the caller's business.


One pixel of `image` is one grid cell, and the values behind the picture never cross the wire — the widget hands back indices, and you do the slicing yourself with `values[widget.pinned_row]`. Use `x_at`/`y_at` to turn an index back into a data coordinate.



```
from wigglystuff import HeatmapSelect

import numpy as np
import marimo as mo
from wigglystuff import HeatmapSelect

steps = np.random.rand(100, 91)
widget = mo.ui.anywidget(
    HeatmapSelect(
        steps,
        x_range=(0, 90),
        y_range=(0.1, 10.0),
        x_label="bend angle",
        y_label="turning rate",
        x_suffix="°",
        y_suffix="°",
    )
)
widget
```


Then react to the pins in another cell. They are independent, so this is three `if`s rather than a branch:


```
if widget.pinned_row is not None:
    row_sweep = steps[widget.pinned_row, :]
if widget.pinned_col is not None:
    col_sweep = steps[:, widget.pinned_col]
if widget.pinned_cell is not None:
    row, col = widget.pinned_cell
    value = steps[row, col]
    x, y = widget.x_at(col), widget.y_at(row)
```


Coloring follows matplotlib, so a Bret-Victor-style crash region is just a masked array plus a "bad" color — the widget has no concept of one:


```
import matplotlib

HeatmapSelect(
    np.ma.masked_where(crashed, distance),
    cmap=matplotlib.colormaps["gray"].with_extremes(bad="red"),
)
```


Create a HeatmapSelect widget.


  Source code in `wigglystuff/heatmap_select.py`

```
def __init__(
    self,
    image: Any,
    *,
    x_range: Tuple[float, float] = (0.0, 1.0),
    y_range: Tuple[float, float] = (0.0, 1.0),
    x_label: str = "",
    y_label: str = "",
    x_suffix: str = "",
    y_suffix: str = "",
    origin: str = "lower",
    cell_width: int = 4,
    cell_height: int = 4,
    row_color: str = "",
    col_color: str = "",
    cmap: Any = "gray",
    norm: Any = None,
    vmin: Optional[float] = None,
    vmax: Optional[float] = None,
    throttle: Union[int, str] = 50,
    **kwargs: Any,
):
    """Create a HeatmapSelect widget.

    Args:
        image: The grid bitmap — one pixel per cell. Accepts a base64/data-URI
            PNG string, a path, a PIL ``Image``, an ``(rows, cols, 3|4)`` uint8
            array, or a ``(rows, cols)`` numeric array colormapped via
            ``cmap``/``norm``/``vmin``/``vmax``.
        x_range: Data coordinates of the first and last *column* centers.
        y_range: Data coordinates of the first and last *row* centers.
        x_label: Label drawn under the bottom gutter.
        y_label: Label drawn beside the left gutter.
        x_suffix: Suffix appended to x tick labels, e.g. ``"°"``.
        y_suffix: Suffix appended to y tick labels.
        origin: ``"lower"`` puts image row 0 at ``y_range[0]`` (bottom),
            ``"upper"`` puts it at the top, matching matplotlib's ``imshow``.
        cell_width: Screen pixels per cell horizontally.
        cell_height: Screen pixels per cell vertically.
        row_color: Tint for the row band grabbed from the left (y) axis, e.g.
            ``"#1f4fd8"``. Empty uses the ``--hs-row-color`` CSS variable.
        col_color: Tint for the column band grabbed from the bottom (x) axis.
            Empty uses the ``--hs-col-color`` CSS variable.
        cmap: Colormap name or ``matplotlib.colors.Colormap``, for 2D numeric
            arrays. Grayscale by default. Mask cells (or make them NaN) and
            use ``cmap.with_extremes(bad="red")`` to color a crash region.
        norm: Optional ``matplotlib.colors.Normalize`` instance, e.g.
            ``LogNorm()``. Mutually exclusive with ``vmin``/``vmax``.
        vmin: Optional lower end of the color scale.
        vmax: Optional upper end of the color scale.
        throttle: How often hover updates reach Python — ``0`` for every
            mouse move, an int for milliseconds, or ``"dragend"`` to send
            hover only on release. Pin changes always sync immediately,
            whatever this is set to.
        **kwargs: Forwarded to ``anywidget.AnyWidget``.
    """
    if origin not in ("lower", "upper"):
        raise ValueError(f"origin must be 'lower' or 'upper', got {origin!r}")
    if cell_width < 1 or cell_height < 1:
        raise ValueError("cell_width and cell_height must be at least 1")
    for name, value in (("x_range", x_range), ("y_range", y_range)):
        if len(tuple(value)) != 2:
            raise ValueError(f"{name} must be a (min, max) pair, got {value!r}")

    # Remembered so set_image() recolors the same way without repeating them.
    self._color_kwargs = {"cmap": cmap, "norm": norm, "vmin": vmin, "vmax": vmax}
    image_base64, n_rows, n_cols = _image_to_png_base64(
        image, **self._color_kwargs
    )

    super().__init__(
        image_base64=image_base64,
        n_rows=n_rows,
        n_cols=n_cols,
        x_range=tuple(float(v) for v in x_range),
        y_range=tuple(float(v) for v in y_range),
        x_label=x_label,
        y_label=y_label,
        x_suffix=x_suffix,
        y_suffix=y_suffix,
        origin=origin,
        cell_width=cell_width,
        cell_height=cell_height,
        row_color=row_color,
        col_color=col_color,
        throttle=throttle,
        **kwargs,
    )
```


## height `property`


```
height: int
```


Height of the grid area in screen pixels (excludes the gutters).


## selection `property`


```
selection: dict
```


All six selection traits in one dict, handy for one-shot reads.


## width `property`


```
width: int
```


Width of the grid area in screen pixels (excludes the gutters).


## clear


```
clear() -> None
```


Drop all three pins and the current hover.

 Source code in `wigglystuff/heatmap_select.py`

```
def clear(self) -> None:
    """Drop all three pins and the current hover."""
    with self.hold_sync():
        self.pinned_cell = None
        self.pinned_row = None
        self.pinned_col = None
        self.hover_cell = None
        self.hover_row = None
        self.hover_col = None
```


## set_image


```
set_image(image: Any, **color_kwargs: Any) -> None
```


Swap the grid bitmap, re-deriving the grid shape from its pixels.


  Source code in `wigglystuff/heatmap_select.py`

```
def set_image(self, image: Any, **color_kwargs: Any) -> None:
    """Swap the grid bitmap, re-deriving the grid shape from its pixels.

    Args:
        image: A new image, in any of the forms the constructor accepts.
        **color_kwargs: Optional ``cmap``/``norm``/``vmin``/``vmax`` overrides;
            anything omitted reuses what the constructor was given.
    """
    self._color_kwargs.update(color_kwargs)
    image_base64, n_rows, n_cols = _image_to_png_base64(
        image, **self._color_kwargs
    )
    with self.hold_sync():
        self.image_base64 = image_base64
        self.n_rows = n_rows
        self.n_cols = n_cols
```


## x_at


```
x_at(col: Optional[int]) -> Optional[float]
```


Data coordinate at the center of a column, or `None` for `None`.




| Type | Description |
| --- | --- |
| `Optional[float]` | float \| None: The x coordinate, interpolated across `x_range`. |

 Source code in `wigglystuff/heatmap_select.py`

```
def x_at(self, col: Optional[int]) -> Optional[float]:
    """Data coordinate at the center of a column, or ``None`` for ``None``.

    Args:
        col: Column index, or ``None``.

    Returns:
        float | None: The x coordinate, interpolated across ``x_range``.
    """
    if col is None:
        return None
    lo, hi = self.x_range
    if self.n_cols <= 1:
        return lo
    return lo + (col / (self.n_cols - 1)) * (hi - lo)
```


## y_at


```
y_at(row: Optional[int]) -> Optional[float]
```


Data coordinate at the center of a row, or `None` for `None`.




| Type | Description |
| --- | --- |
| `Optional[float]` | float \| None: The y coordinate, interpolated across `y_range`. |

 Source code in `wigglystuff/heatmap_select.py`

```
def y_at(self, row: Optional[int]) -> Optional[float]:
    """Data coordinate at the center of a row, or ``None`` for ``None``.

    Args:
        row: Row index, or ``None``.

    Returns:
        float | None: The y coordinate, interpolated across ``y_range``.
    """
    if row is None:
        return None
    lo, hi = self.y_range
    if self.n_rows <= 1:
        return lo
    return lo + (row / (self.n_rows - 1)) * (hi - lo)
```


## Synced traitlets


| Traitlet | Type | Notes |
| --- | --- | --- |
| `image_base64` | `str` | The grid bitmap as a PNG data URI. One image pixel is one cell. |
| `n_rows` | `int` | Grid rows, derived from the image height. |
| `n_cols` | `int` | Grid columns, derived from the image width. |
| `x_range` | `tuple[float, float]` | Data coordinates of the first and last column centers. |
| `y_range` | `tuple[float, float]` | Data coordinates of the first and last row centers. |
| `x_label` | `str` | Label under the bottom gutter. May contain `\n` to stack lines. |
| `y_label` | `str` | Label beside the left gutter. May contain `\n` to stack lines. |
| `x_suffix` | `str` | Appended to x tick labels, e.g. `"°"`. |
| `y_suffix` | `str` | Appended to y tick labels. |
| `origin` | `str` | `"lower"` puts image row 0 at the bottom (like `imshow`), `"upper"` at the top. |
| `cell_width` | `int` | Screen pixels per cell horizontally. |
| `cell_height` | `int` | Screen pixels per cell vertically. |
| `row_color` | `str` | Tint for the row band from the left (y) axis. Empty uses `--hs-row-color`. |
| `col_color` | `str` | Tint for the column band from the bottom (x) axis. Empty uses `--hs-col-color`. |
| `pinned_cell` | `tuple[int, int] \\| None` | `(row, col)` of the pinned cell. |
| `pinned_row` | `int \\| None` | Row index pinned from the left axis. |
| `pinned_col` | `int \\| None` | Column index pinned from the bottom axis. |
| `hover_cell` | `tuple[int, int] \\| None` | `(row, col)` under the cursor, when it is over the grid body. |
| `hover_row` | `int \\| None` | Row under the cursor, when it is over the left gutter. |
| `hover_col` | `int \\| None` | Column under the cursor, when it is over the bottom gutter. |
| `throttle` | `int \\| str` | Hover sync rate. `0` = every move, int = ms, `"dragend"` = on release. Pin changes always sync immediately. |

****

## Interaction


| Gesture | Result |
| --- | --- |
| Hover the body | Sets `hover_cell`. |
| Hover the left gutter | Sets `hover_row` — a horizontal band. |
| Hover the bottom gutter | Sets `hover_col` — a vertical band. |
| Click (anywhere, including a gutter) | Pins that region. Only that region's pin is replaced. |
| Drag | Keeps moving that region's pin. |
| Double-click a region | Drops only that region's pin. |
| Mouse out | Clears the hover traits; pins are untouched. |


The three pins are independent, so a cell, a row and a column can all be held at once. Hovering never disturbs a pin — it draws a faint ghost instead.


## Coloring


Pass a 2D numeric array and the widget colormaps it with matplotlib's own conventions: `cmap` (a name or a `Colormap`), `norm`, `vmin`, `vmax`. The default is grayscale. Cells that are **masked or non-finite** take the colormap's "bad" color, which is all you need for a crash region:


```
import matplotlib
import numpy as np
from wigglystuff import HeatmapSelect

HeatmapSelect(
    np.ma.masked_where(crashed, distance),
    cmap=matplotlib.colormaps["gray"].with_extremes(bad="red"),
)
```


Autoscaling is relative, exactly as with `imshow`: uniformly rescaling the data produces an identical picture. Pin `vmin`/`vmax` if you need an absolute scale across successive `set_image` calls.


You can also skip colormapping entirely and hand over a finished picture — a PIL image, an `(rows, cols, 3|4)` uint8 array, a path, or a base64 PNG.


## Sizing


The plot size is *derived*: `n_cols * cell_width` by `n_rows * cell_height`. Cells are therefore always whole pixel blocks and never shimmer, which is why there is no `width`/`height` argument — both are read-only properties.
