Dark Mode

Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: mo.ui.matplotlib()#8342

Merged
akshayka merged 35 commits intomainfrom
aka/ui-matplotlib
Feb 19, 2026
Merged

feat: mo.ui.matplotlib()#8342
akshayka merged 35 commits intomainfrom
aka/ui-matplotlib

Conversation

Copy link
Contributor

akshayka commented Feb 17, 2026 *
edited
Loading

This PR adds a new public API, mo.ui.matplotlib(axis: plt.Axes, *, debounce: bool=False.), which adds reactive selection to a matplotlib Axes. This API is designed for scatter plots and scatter plot selections, but should be extensible enough to support other plot types and interactions (such as span selections)/geometries in the future.

This feature was inspired by and based on @koaning's Wigglystuff ChartSelect anywidget for matplotlib.

Selections

Two types of selections are supported: box and lasso. Box is the default selection, with Shift+click triggering lasso selection.

Python usage

import matplotlib.pyplot as plt
import marimo as mo
import numpy as np

x = np.arange(5)
y = x**2
plt.scatter(x=x, y=y)
fig = mo.ui.matplotlib(plt.gca())
fig
# Filter data using the selection
mask = fig.value.get_mask(x, y)
selected_x, selected_y = x[mask], y[mask]

If debounce=True is passed to the constructor, data is only sent back on mouse up.

value type

The element's value is a frozen dataclass containing information about the interaction and selection. Each dataclass exposes a get_mask(x: np.typing.ArrayLike, y: np.typing.ArrayLike) method which returns a boolean mask for indexing into the scattered data.

An empty selection returns an EmptySelection sentinel object which is False-y, so users can write code like

if fig.value:
# do something with selection
...

EmptySelection.get_mask(x, y) does return an all-False mask, so users can index into the original data without checking if the selection is empty.

The dataclass type is not returned as part of the public API. We could extend the class of interactions/geometries handled by adding new classes in the future.

Smoke test

This PR includes a smoke test that exercises linear and log scale axes

Media

mo-ui-matplotlib.mp4

dmadisetti reacted with heart emoji
akshayka added 12 commits February 14, 2026 08:39
Shift+drag draws a freehand polygon selection in addition to the
existing box selection. Both selection types can be dragged after
creation and cleared by clicking outside. The value format is now
a tagged union: {"type": "box"|"lasso", "data": ...} for
extensibility. All helper methods (get_bounds, get_vertices,
contains_point, get_mask, get_indices) handle both types.
akshayka requested a review from dmadisetti February 17, 2026 18:04
akshayka requested review from Light2Dark and manzt as code owners February 17, 2026 18:04
Copy link

vercel bot commented Feb 17, 2026 *
edited
Loading

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
marimo-docs Ready Preview, Comment Feb 19, 2026 6:49pm

akshayka removed the request for review from Light2Dark February 17, 2026 18:04
github-actions bot added the documentation Improvements or additions to documentation label Feb 17, 2026
vercel bot deployed to Preview February 17, 2026 18:06 View deployment
akshayka added the enhancement New feature or request label Feb 17, 2026
vercel bot deployed to Preview February 17, 2026 18:14 View deployment
akshayka added 2 commits February 17, 2026 10:31
vercel bot deployed to Preview February 17, 2026 18:34 View deployment
...ents

Replace useState-driven interaction state with useRef + requestAnimationFrame
to eliminate React re-renders on every mouse move during drawing/dragging.
Add DataPoint type to distinguish data coordinates from pixel coordinates,
Escape key to cancel in-progress selections, cursor feedback (crosshair/move),
touch-action:none for mobile support, and smarter onMouseLeave behavior that
doesn't finalize selections unexpectedly.
vercel bot deployed to Preview February 17, 2026 21:41 View deployment
vercel bot deployed to Preview February 17, 2026 21:42 View deployment
vercel bot deployed to Preview February 18, 2026 17:58 View deployment
Implements a version of the suggestion in
[review](#8342 (comment)).
Since all interaction state already lives in refs and React does no
rendering work (it's all canvas), separating the DOM/canvas logic into
its own class gives a cleaner boundary between React lifecycle and
imperative drawing code.

These chnages extract into an imperative `MatplotlibRenderer` class
without React. The class uses `AbortSignal` for cleanup (all
`addEventListener` calls pass `{ signal }` for automatic removal) and a
generation counter for stale image load guards. The React component
becomes a thin lifecycle shell that creates the renderer on mount, syncs
props via `update()` each render, and aborts the controller on unmount.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
vercel bot deployed to Preview February 19, 2026 02:33 View deployment
Adds d3-brush-style resize behavior: hovering box edges/corners shows
directional cursors, and dragging them resizes the selection using an
anchor + axis-lock approach.

These changes required refactoring interaction state to a nested state
machine. Replaces flat InteractionMode/InteractionState with a nested
discriminated union (`Interaction = idle | box | lasso`) where each
selection type carries its own typed action (drawing, dragging,
resizing):

| Interaction | Actions |
|-------------|---------|
| `idle` | |
| `box` | `null` * `drawing` * `dragging` * `resizing` |
| `lasso` | `null` * `drawing` * `dragging` |


https://github.com/user-attachments/assets/d7a41e87-00d1-41c0-8240-839e73f2035b
vercel bot deployed to Preview February 19, 2026 02:39 View deployment
On HiDPI/Retina displays the selection box and lasso overlays rendered
blurry because the canvas backing store matched the logical pixel
dimensions rather than the physical ones. The browser stretched the 1x
canvas to fill the CSS size, producing soft edges on every fillRect,
strokeRect, and lineTo call.

| before | after |
|-------------|---------|
| src="https://github.com/user-attachments/assets/ac089535-65b5-417b-9736-af6f6cdaf9b4"
/> | src="https://github.com/user-attachments/assets/4d9499a6-47aa-4e61-b26f-d0159b92cc01"
/> |
vercel bot deployed to Preview February 19, 2026 02:41 View deployment
akshayka added 2 commits February 18, 2026 19:52
vercel bot deployed to Preview February 19, 2026 03:53 View deployment
vercel bot deployed to Preview February 19, 2026 04:04 View deployment
akshayka added 2 commits February 18, 2026 20:06
vercel bot deployed to Preview February 19, 2026 04:08 View deployment
dmadisetti reviewed Feb 19, 2026
Copy link
Collaborator

dmadisetti left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left general comment on the api. Super fun addition, I don't think my comments are a blocker- just wanted to raise some thoughts on misuse / cumbersome nature of calling get_mask

manzt previously approved these changes Feb 19, 2026
akshayka dismissed manzt's stale review via dc0fb3c February 19, 2026 16:05
vercel bot deployed to Preview February 19, 2026 16:06 View deployment
Copy link
Collaborator

dmadisetti commented Feb 19, 2026

Failing test is relevant but lgtm otherwise

akshayka reacted with thumbs up emoji

vercel bot deployed to Preview February 19, 2026 17:27 View deployment
dmadisetti previously approved these changes Feb 19, 2026
akshayka dismissed dmadisetti's stale review via a170d89 February 19, 2026 18:47
vercel bot deployed to Preview February 19, 2026 18:49 View deployment
akshayka merged commit 4fe9ebe into main Feb 19, 2026
28 of 43 checks passed
akshayka deleted the aka/ui-matplotlib branch February 19, 2026 18:57
LiquidGunay pushed a commit to LiquidGunay/marimo that referenced this pull request Feb 21, 2026
This PR adds a new public API, `mo.ui.matplotlib(axis: plt.Axes, *,
debounce: bool=False.)`, which adds reactive selection to a matplotlib
Axes. This API is designed for scatter plots and scatter plot
selections, but should be extensible enough to support other plot types
and interactions (such as span selections)/geometries in the future.

This feature was inspired by and based on @koaning's `Wigglystuff`
`ChartSelect` anywidget for matplotlib.

## Selections

Two types of selections are supported: box and lasso. Box is the default
selection, with Shift+click triggering lasso selection.

## Python usage

```python
import matplotlib.pyplot as plt
import marimo as mo
import numpy as np

x = np.arange(5)
y = x**2
plt.scatter(x=x, y=y)
fig = mo.ui.matplotlib(plt.gca())
fig
```

```python
# Filter data using the selection
mask = fig.value.get_mask(x, y)
selected_x, selected_y = x[mask], y[mask]
```

If `debounce=True` is passed to the constructor, data is only sent back
on mouse up.

## `value` type

The element's `value` is a frozen dataclass containing information about
the interaction and selection. Each dataclass exposes a `get_mask(x:
np.typing.ArrayLike, y: np.typing.ArrayLike)` method which returns a
boolean mask for indexing into the scattered data.

An empty selection returns an `EmptySelection` sentinel object which is
`False-y`, so users can write code like

```python
if fig.value:
# do something with selection
...
```

`EmptySelection.get_mask(x, y)` does return an all-`False` mask, so
users can index into the original data without checking if the selection
is empty.

The dataclass type is not returned as part of the public API. We could
extend the class of interactions/geometries handled by adding new
classes in the future.

## Smoke test

This PR includes a smoke test that exercises linear and log scale axes

## Media


https://github.com/user-attachments/assets/e085f4d0-d17b-4d44-854c-7c3f2f156b4d

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Trevor Manz
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

dmadisetti dmadisetti left review comments

manzt manzt left review comments

Assignees

No one assigned

Labels

documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

3 participants