-
Notifications
You must be signed in to change notification settings - Fork 954
Conversation
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 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
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
# 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
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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.
[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>
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
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 |
|-------------|---------|
|
/> |
/> |
dmadisetti
left a comment
There was a problem hiding this 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
|
Failing test is relevant but lgtm otherwise |
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