Skip to content

ChartSelect API#

Bases: AnyWidget

Region selection overlay for matplotlib charts.

Allows interactive box or lasso (freehand) selection on a static matplotlib chart. Returns selection coordinates in data space for user-side filtering.

Examples:

Basic usage:

import matplotlib.pyplot as plt
from wigglystuff import ChartSelect

fig, ax = plt.subplots()
ax.scatter(x_data, y_data)

select = ChartSelect(fig)
# select.selection contains the selection bounds/vertices
# select.has_selection is True when a selection exists

Filtering with a mask:

mask = select.get_mask(x_data, y_data)
selected_x = x_data[mask]

Filtering a DataFrame:

indices = select.get_indices(df["x"], df["y"])
selected_df = df.iloc[indices]

Create a ChartSelect widget from a matplotlib figure.

Parameters:

Name Type Description Default
fig

A matplotlib figure to overlay selection on.

required
mode str

Selection mode ("box" or "lasso").

'box'
modes list[str] | None

List of available modes. Defaults to ["box", "lasso"]. Pass a single-item list to lock to one mode.

None
selection_color str

Fill color for selection region.

'#3b82f6'
selection_opacity float

Opacity of selection fill (0-1).

0.3
**kwargs Any

Forwarded to AnyWidget.

{}
Source code in wigglystuff/chart_select.py
def __init__(
    self,
    fig,
    mode: str = "box",
    modes: list[str] | None = None,
    selection_color: str = "#3b82f6",
    selection_opacity: float = 0.3,
    **kwargs: Any,
) -> None:
    """Create a ChartSelect widget from a matplotlib figure.

    Args:
        fig: A matplotlib figure to overlay selection on.
        mode: Selection mode ("box" or "lasso").
        modes: List of available modes. Defaults to ["box", "lasso"].
               Pass a single-item list to lock to one mode.
        selection_color: Fill color for selection region.
        selection_opacity: Opacity of selection fill (0-1).
        **kwargs: Forwarded to ``AnyWidget``.
    """
    x_bounds, y_bounds, axes_pixel_bounds, width_px, height_px = extract_axes_info(
        fig
    )
    chart_base64 = fig_to_base64(fig)

    if modes is None:
        modes = ["box", "lasso"]

    super().__init__(
        mode=mode,
        modes=modes,
        x_bounds=x_bounds,
        y_bounds=y_bounds,
        axes_pixel_bounds=axes_pixel_bounds,
        width=width_px,
        height=height_px,
        chart_base64=chart_base64,
        selection_color=selection_color,
        selection_opacity=selection_opacity,
        **kwargs,
    )

clear #

clear() -> None

Clear the current selection.

Source code in wigglystuff/chart_select.py
def clear(self) -> None:
    """Clear the current selection."""
    self.selection = {}
    self.has_selection = False

contains_point #

contains_point(x: float, y: float) -> bool

Check if a point is inside the selection region.

Uses matplotlib's Path.contains_point for lasso selections.

Parameters:

Name Type Description Default
x float

X coordinate in data space.

required
y float

Y coordinate in data space.

required

Returns:

Type Description
bool

True if the point is inside the selection, False otherwise.

Source code in wigglystuff/chart_select.py
def contains_point(self, x: float, y: float) -> bool:
    """Check if a point is inside the selection region.

    Uses matplotlib's Path.contains_point for lasso selections.

    Args:
        x: X coordinate in data space.
        y: Y coordinate in data space.

    Returns:
        True if the point is inside the selection, False otherwise.
    """
    if not self.has_selection:
        return False

    if self._is_box_selection():
        bounds = self.get_bounds()
        if not bounds:
            return False
        return bounds[0] <= x <= bounds[2] and bounds[1] <= y <= bounds[3]
    else:
        from matplotlib.path import Path

        vertices = self.get_vertices()
        if len(vertices) < 3:
            return False
        path = Path(vertices)
        return path.contains_point((x, y))

from_callback classmethod #

from_callback(
    draw_fn,
    x_bounds: tuple[float, float],
    y_bounds: tuple[float, float],
    figsize: tuple[float, float] = (6, 6),
    mode: str = "box",
    modes: list[str] | None = None,
    **kwargs: Any
) -> "ChartSelect"

Create a ChartSelect that auto-updates when selection changes.

The callback function is called on init and whenever the selection changes. Use redraw() to manually trigger a re-render.

Parameters:

Name Type Description Default
draw_fn

A function(ax, widget) that draws onto the axes. Receives the axes and the widget instance, allowing access to widget.selection and widget.has_selection. The axes is pre-cleared and bounds are pre-set.

required
x_bounds tuple[float, float]

(min, max) for x-axis - fixed for lifetime of widget.

required
y_bounds tuple[float, float]

(min, max) for y-axis - fixed for lifetime of widget.

required
figsize tuple[float, float]

Figure size in inches.

(6, 6)
mode str

Selection mode ("box" or "lasso").

'box'
modes list[str] | None

List of available modes. Defaults to ["box", "lasso"].

None
**kwargs Any

Passed to ChartSelect (selection_color, etc.)

{}

Returns:

Type Description
'ChartSelect'

A ChartSelect instance with auto-update behavior and redraw() method.

Examples:

def draw_chart(ax, widget):
    ax.scatter(data_x, data_y, alpha=0.6)
    if widget.has_selection:
        idx = widget.get_indices(data_x, data_y)
        ax.scatter(data_x[idx], data_y[idx], color='red')

select = ChartSelect.from_callback(
    draw_fn=draw_chart,
    x_bounds=(-3, 3),
    y_bounds=(-3, 3),
)
Source code in wigglystuff/chart_select.py
@classmethod
def from_callback(
    cls,
    draw_fn,
    x_bounds: tuple[float, float],
    y_bounds: tuple[float, float],
    figsize: tuple[float, float] = (6, 6),
    mode: str = "box",
    modes: list[str] | None = None,
    **kwargs: Any,
) -> "ChartSelect":
    """Create a ChartSelect that auto-updates when selection changes.

    The callback function is called on init and whenever the selection
    changes. Use ``redraw()`` to manually trigger a re-render.

    Args:
        draw_fn: A function(ax, widget) that draws onto the axes.
                 Receives the axes and the widget instance, allowing access
                 to widget.selection and widget.has_selection.
                 The axes is pre-cleared and bounds are pre-set.
        x_bounds: (min, max) for x-axis - fixed for lifetime of widget.
        y_bounds: (min, max) for y-axis - fixed for lifetime of widget.
        figsize: Figure size in inches.
        mode: Selection mode ("box" or "lasso").
        modes: List of available modes. Defaults to ["box", "lasso"].
        **kwargs: Passed to ChartSelect (selection_color, etc.)

    Returns:
        A ChartSelect instance with auto-update behavior and redraw() method.

    Examples:
        ```python
        def draw_chart(ax, widget):
            ax.scatter(data_x, data_y, alpha=0.6)
            if widget.has_selection:
                idx = widget.get_indices(data_x, data_y)
                ax.scatter(data_x[idx], data_y[idx], color='red')

        select = ChartSelect.from_callback(
            draw_fn=draw_chart,
            x_bounds=(-3, 3),
            y_bounds=(-3, 3),
        )
        ```
    """
    import matplotlib.pyplot as plt

    fig, ax = plt.subplots(figsize=figsize)

    ax.set_xlim(x_bounds)
    ax.set_ylim(y_bounds)
    widget = cls(fig, mode=mode, modes=modes, **kwargs)

    def render():
        ax.clear()
        ax.set_xlim(x_bounds)
        ax.set_ylim(y_bounds)
        draw_fn(ax, widget)
        return fig_to_base64(fig)

    widget._render = render
    widget.chart_base64 = render()

    def on_change(change):
        widget.chart_base64 = render()

    widget.observe(on_change, names=["selection", "has_selection"])

    return widget

get_bounds #

get_bounds() -> tuple[float, float, float, float] | None

Get bounding box of selection in data coordinates.

Returns:

Type Description
tuple[float, float, float, float] | None

(x_min, y_min, x_max, y_max) or None if no selection.

Source code in wigglystuff/chart_select.py
def get_bounds(self) -> tuple[float, float, float, float] | None:
    """Get bounding box of selection in data coordinates.

    Returns:
        (x_min, y_min, x_max, y_max) or None if no selection.
    """
    if not self.has_selection or not self.selection:
        return None

    if self._is_box_selection():
        return (
            self.selection["x_min"],
            self.selection["y_min"],
            self.selection["x_max"],
            self.selection["y_max"],
        )
    else:  # lasso or polygon
        vertices = self.selection.get("vertices", [])
        if not vertices:
            return None
        xs = [v[0] for v in vertices]
        ys = [v[1] for v in vertices]
        return (min(xs), min(ys), max(xs), max(ys))

get_indices #

get_indices(x_arr, y_arr)

Return indices of points inside the selection.

Useful for filtering dataframes with df.iloc[indices].

Parameters:

Name Type Description Default
x_arr

Array-like of x coordinates.

required
y_arr

Array-like of y coordinates.

required

Returns:

Type Description

Numpy array of integer indices for points inside the selection.

Source code in wigglystuff/chart_select.py
def get_indices(self, x_arr, y_arr):
    """Return indices of points inside the selection.

    Useful for filtering dataframes with ``df.iloc[indices]``.

    Args:
        x_arr: Array-like of x coordinates.
        y_arr: Array-like of y coordinates.

    Returns:
        Numpy array of integer indices for points inside the selection.
    """
    import numpy as np

    return np.where(self.get_mask(x_arr, y_arr))[0]

get_mask #

get_mask(x_arr, y_arr)

Return boolean mask for points inside the selection.

Parameters:

Name Type Description Default
x_arr

Array-like of x coordinates.

required
y_arr

Array-like of y coordinates.

required

Returns:

Type Description

Boolean numpy array where True means point is inside selection.

Source code in wigglystuff/chart_select.py
def get_mask(self, x_arr, y_arr):
    """Return boolean mask for points inside the selection.

    Args:
        x_arr: Array-like of x coordinates.
        y_arr: Array-like of y coordinates.

    Returns:
        Boolean numpy array where True means point is inside selection.
    """
    import numpy as np

    x_arr = np.asarray(x_arr)
    y_arr = np.asarray(y_arr)

    if not self.has_selection:
        return np.zeros(len(x_arr), dtype=bool)

    if self._is_box_selection():
        bounds = self.get_bounds()
        if not bounds:
            return np.zeros(len(x_arr), dtype=bool)
        return (
            (x_arr >= bounds[0])
            & (x_arr <= bounds[2])
            & (y_arr >= bounds[1])
            & (y_arr <= bounds[3])
        )
    else:
        from matplotlib.path import Path

        vertices = self.get_vertices()
        if len(vertices) < 3:
            return np.zeros(len(x_arr), dtype=bool)
        path = Path(vertices)
        points = np.column_stack([x_arr, y_arr])
        return path.contains_points(points)

get_vertices #

get_vertices() -> list[tuple[float, float]]

Get selection vertices in data coordinates.

For box mode, returns the 4 corners (clockwise from bottom-left). For lasso mode, returns the path vertices.

Returns:

Type Description
list[tuple[float, float]]

List of (x, y) tuples, or empty list if no selection.

Source code in wigglystuff/chart_select.py
def get_vertices(self) -> list[tuple[float, float]]:
    """Get selection vertices in data coordinates.

    For box mode, returns the 4 corners (clockwise from bottom-left).
    For lasso mode, returns the path vertices.

    Returns:
        List of (x, y) tuples, or empty list if no selection.
    """
    if not self.has_selection or not self.selection:
        return []

    if self._is_box_selection():
        x_min = self.selection["x_min"]
        y_min = self.selection["y_min"]
        x_max = self.selection["x_max"]
        y_max = self.selection["y_max"]
        return [(x_min, y_min), (x_max, y_min), (x_max, y_max), (x_min, y_max)]
    else:
        return [tuple(v) for v in self.selection.get("vertices", [])]

redraw #

redraw() -> None

Re-render the chart using the stored callback.

Only available for widgets created via from_callback(). Call this when external state that affects the chart has changed.

Source code in wigglystuff/chart_select.py
def redraw(self) -> None:
    """Re-render the chart using the stored callback.

    Only available for widgets created via ``from_callback()``. Call this
    when external state that affects the chart has changed.
    """
    if hasattr(self, "_render"):
        self.chart_base64 = self._render()

Synced traitlets#

Traitlet Type Notes
mode str Selection mode: "box" or "lasso".
modes list[str] Available modes (controls which buttons are shown).
selection dict Selection data in data coordinates. Box: {x_min, y_min, x_max, y_max}. Lasso: {vertices: [[x, y], ...]}.
has_selection bool Whether a selection is currently active.
x_bounds tuple[float, float] Min/max x-axis bounds from matplotlib.
y_bounds tuple[float, float] Min/max y-axis bounds from matplotlib.
axes_pixel_bounds tuple[float, float, float, float] Axes position in pixels (left, top, right, bottom).
width int Canvas width in pixels.
height int Canvas height in pixels.
chart_base64 str Base64-encoded PNG of the matplotlib figure.
selection_color str CSS color for selection fill and stroke.
selection_opacity float Opacity of selection fill (0-1).
stroke_width int Width of selection border in pixels.

Helper methods#

Method Returns Description
clear() None Clear the current selection.
get_bounds() tuple or None Bounding box (x_min, y_min, x_max, y_max) in data coordinates.
get_vertices() list[tuple] Selection vertices as (x, y) tuples.
contains_point(x, y) bool Check if a point is inside the selection.
get_mask(x_arr, y_arr) ndarray[bool] Boolean mask for points inside selection.
get_indices(x_arr, y_arr) ndarray[int] Indices of points inside selection.
redraw() None Re-render chart (only for from_callback widgets).