Conversation
Route plotly `FigureWidget` through the anywidget formatter instead of the static plotly formatter. `FigureWidget` is a subclass of `anywidget.AnyWidget`, but marimo's `PlotlyFormatter` explicitly registered a formatter for it. Since `FigureWidget` appears before `anywidget.AnyWidget` in the MRO, the static plotly formatter always won, rendering FigureWidget as a non-interactive `marimo-plotly` HTML component. This broke interactive widget features like plotly-resampler's dynamic resampling, which requires the widget comm to be initialized. The fix removes the `FigureWidget` registration from `PlotlyFormatter`, allowing the MRO walk to reach the `anywidget.AnyWidget` formatter instead. This properly initializes the widget comm and enables interactive features. Closes #4091
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR fixes plotly-resampler (and other FigureWidget subclasses) not working in marimo by addressing two fundamental issues: (1) FigureWidget was being incorrectly routed through the static Plotly formatter instead of the anywidget formatter, and (2) Plotly's FigureWidget maintains a split internal model that requires syncing via _repr_mimebundle_() before the widget comm is initialized.
Changes:
- Removed FigureWidget registration from PlotlyFormatter to allow it to fall through to AnyWidgetFormatter via MRO
- Added
_sync_widget_state()helper infrom_anywidget.pyto call_repr_mimebundle_()before creating widget wrappers - Added comprehensive tests for both the formatter routing and end-to-end rendering behavior
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| marimo/_output/formatters/plotly_formatters.py | Removed FigureWidget formatter registration so it falls through to anywidget formatter |
| marimo/_plugins/ui/_impl/from_anywidget.py | Added widget state synchronization via _repr_mimebundle_() before wrapping |
| tests/_output/formatters/test_plotly_formatters.py | Added unit test verifying FigureWidget uses anywidget formatter |
| tests/_plugins/ui/_impl/test_anywidget.py | Added integration test verifying FigureWidget renders as marimo-anywidget |
| marimo/_smoke_tests/plotly/_plotly_resampler.py | Updated smoke test structure and __generated_with version |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Some widgets (e.g. plotly FigureWidget, plotly-resampler) only sync | ||
| # their internal data to widget traits during _repr_mimebundle_(). | ||
| # Without this, the comm's initial state may be stale/empty. | ||
| _sync_widget_state(widget) |
There was a problem hiding this comment.
@manzt, thoughts on special casing this for plotly? or i wonder if this is other things we may see/hit with other anywidgets.
calling _repr_mimebundle_ should be cheap anyways
There was a problem hiding this comment.
I think this is a fine fix, but want to call out that it might be a partial solution.
Calling _repr_mimebundle_() eagerly at creation time works for plotly's trait-syncing case, but I could imagine a widget that does something meaningful at display time, e.g.:
widget = mo.ui.anywidget(FooWidget(...)) # _repr_mimebundle_ fires here now
widget.update_something()
widget.update_something()
widget # but it should really fire hereanywidget will always use _repr_mimebundle_ because that's the protocol/convention for rendering.
manzt
left a comment
There was a problem hiding this comment.
I think this is fine fix, just a comment.
| # Some widgets (e.g. plotly FigureWidget, plotly-resampler) only sync | ||
| # their internal data to widget traits during _repr_mimebundle_(). | ||
| # Without this, the comm's initial state may be stale/empty. | ||
| _sync_widget_state(widget) |
There was a problem hiding this comment.
I think this is a fine fix, but want to call out that it might be a partial solution.
Calling _repr_mimebundle_() eagerly at creation time works for plotly's trait-syncing case, but I could imagine a widget that does something meaningful at display time, e.g.:
widget = mo.ui.anywidget(FooWidget(...)) # _repr_mimebundle_ fires here now
widget.update_something()
widget.update_something()
widget # but it should really fire hereanywidget will always use _repr_mimebundle_ because that's the protocol/convention for rendering.
Fix plotly-resampler (and other FigureWidget subclasses) not working in marimo by routing
FigureWidgetthrough the anywidget formatter and syncing widget state via_repr_mimebundle_().Two problems, two fixes:
Wrong formatter:
PlotlyFormatterregistered a static HTML formatter forFigureWidget. SinceFigureWidgetappears beforeanywidget.AnyWidgetin the MRO, the static formatter always won — rendering FigureWidget as a non-interactivemarimo-plotlycomponent. The widget comm was never initialized, so interactive features like plotly-resampler's dynamic resampling never worked. Fix: Remove theFigureWidgetregistration fromPlotlyFormatterso it falls through to theAnyWidgetFormatter.Stale widget state: Plotly's
FigureWidgetmaintains a split internal model — figure data lives in_data/_layout_obj, but widget traits (_widget_data,_widget_layout) are only synced during_repr_mimebundle_(). Without this call, the widget comm sends stale/empty state to the frontend. This is critical for plotly-resampler, which populates downsampled data during this sync. Fix: Call_repr_mimebundle_()infrom_anywidget()before creating the wrapper, ensuring widget traits reflect the current figure state.Closes #4091