Repository navigation
Conversation
… state accessors A plotly selection is state on the graph div (trace selectedpoints, plus the region on the last layout selection), so when the event bridge binds plotly_selected on a graph div it now also reports the current selection as a synthetic plotly_selected message (a no-data one when nothing is selected, the same message plotly.js sends for an empty selection). That lets selected()/brushed() (and so selected_rows()) become state accessors: they attach at render, and a box/lasso selection made before the accessor's first read -- or before the bind request reaches the browser -- is reported instead of silently lost. clicked() stays an event. State accessors don't reset on re-render, so selected()/brushed() now keep their value until the new plot reports its own (initially empty) selection; documented in the render_plotly docstring.
…te accessors Covers the selection-seed scenario end to end: a box selection made before anything reads selected()/brushed() (gated behind an action button), then a gated first read -- which must report the pre-read selection -- and a deselect (double-click on the plot background) after the read, which must clear the accessors to None. plotly.js emits plotly_selected a few hundred ms after mouseup, so a button click right after the drag can overtake the selection message; the fixture follows the sentinel pattern (an ungated text output reading the same accessor state, like ipyleaflet_eager_attach's synced_zoom) so the test waits for the browser-to-server sync before triggering the gated read.
There was a problem hiding this comment.
🟡 Changes recommended
Selection seeding drops valid empty regions, misparses coordinates with negative exponents, and omits implicit coordinates, while the tests do not verify selection before bridge binding.
4 open issues
1 file not reviewed
shinywidgets/static/output.js: Generated file
What changed in this PR
Makes Plotly selection accessors state-based, building on #412’s eager attachment so selections made before the first accessor read can be recovered.
Changes:
- Seeds the current selection and brush region when the browser bridge first binds.
- Changes
selected()andbrushed()to state accessors and documents their re-render behavior. - Adds unit and browser tests for selection retention, gated reads, and deselection.
| File | Description |
|---|---|
tests/unit/test_plotly_accessors.py |
Tests eager attachment, early selection messages, and re-render behavior. |
tests/playwright/plotly_selection_seed/test_plotly_selection_seed.py |
Adds gated-read and deselection browser tests. |
tests/playwright/plotly_selection_seed/app.py |
Adds the browser-test fixture and synchronization output. |
shinywidgets/static/output.js |
Updates the bundled browser bridge. |
shinywidgets/_render_widget.py |
Documents selection state across re-renders. |
shinywidgets/_plotly.py |
Declares selection and brush accessors as state-based. |
js/src/plotly.ts |
Reconstructs and sends selection state on initial binding. |
CHANGELOG.md |
Records the accessor and selection-seeding changes. |
Review effort: Balanced
…ure read - Coordinates the trace doesn't carry explicitly (implicit x, bar x0/dx) fall back to calcdata, matching what live event point objects supply. - An empty drag selection seeds points: [] plus the region, the same message plotly.js sends for one; only no-points-and-no-region remains the no-data case. - The lasso vertex regex accepts signed exponents (1e-7), which it previously failed to match or misparsed. - The fixture's ungated synced_selection read attached the accessor at startup, so the gated reads weren't genuinely late and the test could pass pre-change via live forwarding. Removed; tests now wait on graph-div selection state plus plotly.js's event-emission delay, and would fail pre-change.
… spike/plotly-selection-state
There was a problem hiding this comment.
🟡 Changes recommended
Selection seeding can assign unrelated bin counts as histogram y values and drops valid typed-array selections, producing incorrect or missing selected-row data.
3 open issues
3 resolved since last review
1 file not reviewed
shinywidgets/static/output.js: Generated file
Review effort: Balanced
- A selection preconfigured from plotly.py arrives as a typed array, which Array.isArray rejected, dropping the seed; normalize with jsonable() first. - Histogram selectedpoints are sample indices but histogram calcdata is bins, so the calcdata fallback seeded bin counts as y; the live event reads only the original arrays for aggregated traces, so skip the fallback for histograms to match.
There was a problem hiding this comment.
🟡 Changes recommended
Selection seeding misreports Scattergl coordinates, corrupts date-axis lasso vertices, and ignores subplot axis references, producing results inconsistent with live events.
4 open issues
2 resolved since last review
1 file not reviewed
shinywidgets/static/output.js: Generated file
Review effort: Balanced
- The calcdata coordinate fallback is restricted to verified layouts and moved into calcCoord(): scatter and bar align per data index; scattergl keeps resolved coords in typed arrays on the single calc entry's .t (its top-level x/y are false sentinels); other types are skipped, so unverified layouts omit keys rather than seed wrong values. - Lasso vertices are parsed generically (M/L segments split on the comma), which also restores date-axis values: plotly stores them as date strings with '_' for the date/time separator, live lassoPoints use a space. - The seeded region is keyed by the saved selection's xref/yref (x2/y2 on later subplots), matching live range/lassoPoints keys. Each verified against the compiled bridge: scattergl y-only seeds x=0,1; a date-axis lasso seeds vertices identical to the live event; a second-subplot selection seeds range keyed x2/y2 with the subplot trace's points.
There was a problem hiding this comment.
🟡 Changes recommended
The selection-path regex includes the closing Z/z command in the final coordinate, corrupting regions returned by brushed(); the capture must be corrected and the bundled output regenerated.
2 open issues
3 resolved since last review
1 file not reviewed
shinywidgets/static/output.js: Generated file
Review effort: Balanced
A preconfigured closed path (M0,0L1,0L1,1Z) captured '1Z' as the final coordinate, which Number() rejects and the date fallback returned as the string '1Z'.
There was a problem hiding this comment.
🟡 Changes recommended
Existing selections report incorrect multicategory coordinates, and seeded date-axis brush coordinates retain SVG whitespace, requiring source fixes and bundle regeneration.
3 open issues
1 resolved since last review
1 file not reviewed
shinywidgets/static/output.js: Generated file
Review effort: Balanced
- Multicategory axes store one array per category level, so trace[key][i] is a level array, not the coordinate; reconstruct it level-wise (verified: seed x becomes ['A', 'two'], identical to the live event). - Lasso vertex tokens are trimmed before conversion, so SVG formatting whitespace doesn't survive into date-valued region coordinates.
There was a problem hiding this comment.
🟡 Changes recommended
The multicategory selection logic skips points beyond the number of category levels, producing an incorrect coordinate; the source fix and regenerated output.js are still needed.
1 open issue
3 resolved since last review
1 file not reviewed
shinywidgets/static/output.js: Generated file
Review effort: Balanced
A multicategory trace array's length is the category level count, so for a selected index beyond it trace[key][i] is undefined and the per-point check fell through to the wrong path; detect array-of-arrays on the trace attribute itself (verified: index 2 seeds ['B', 'one']).
There was a problem hiding this comment.
🟡 Changes recommended
The selection seed incorrectly treats selectedpoints=[] as absent, causing selected() to return None instead of an empty DataFrame; preserve the empty selection and regenerate the bundled output.
1 open issue
1 resolved since last review
1 file not reviewed
shinywidgets/static/output.js: Generated file
Review effort: Balanced
plotly treats selectedpoints: [] as an active empty selection (every point gets unselected styling), distinct from an absent selectedpoints; the seed now emits points: [] for it (empty DataFrame, matching the empty-drag case) while a plot with no selection arrays anywhere still seeds the no-data message (None).
There was a problem hiding this comment.
🟢 Approval recommended
No active review issues were identified for the changes preserving Plotly selections made before the first accessor read.
No open issues 🎉
1 resolved since last review
1 file not reviewed
shinywidgets/static/output.js: Generated file
Review effort: Balanced

Stacked on #412.
Summary
A plotly selection is state on the graph div (
trace.selectedpoints, plus the region ongd._fullLayout.selections), not just an event. The event bridge (js/src/plotly.ts) now rebuilds and sends the current selection when it bindsplotly_selected— as a syntheticplotly_selectedmessage (a no-data one when empty, the same message plotly.js sends for an empty selection; deliberately notplotly_deselect, which would wrongly clearclicked()).That lets
selected()/brushed()(and soselected_rows()) becomekind="state"accessors: they attach at render via #412's mechanism, and a box/lasso selection made before the accessor's first read — for example, before a chat handler or LLM tool first reads it — is reported instead of silently lost. This was the original motivating failure behind #412, which #412 alone could not fix (these accessors were event-kind, and events can't seed).clicked()stayskind="event"; so doselecting()/brushing()(drag streams) andrelayout()(autoranged axes have no explicit range, so the empty state isn't seedable).Trade-off: state accessors don't reset on re-render, so
selected()/brushed()keep their value until the new plot reports its own (initially empty) selection, at which point they becomeNone. Documented in therender_plotlydocstring and CHANGELOG.Evidence
selectedpointspost-lasso and clears on deselect, verified empirically (Playwright-driven standalone pages) for scatter, scattergl, bar, and histogram; the state-rebuilt payload matches the realplotly_selectedpayload point-for-point (includingcustomdataand histogrampointNumbersflattening; lasso → parsedlassoPoints; box → ascending range).Verification
tests/playwright/plotly_selection_seed/): box-select before a button-gated first read →selected()/selected_rows(df)/brushed()reflect it; double-click deselect clears toNone. 2 passed × 4 runs, no flakes; existing plotly Playwright suites green (plotly_accessors,plotly_accessors_rerender,plotly_config,plotly_rerender_cleanup).make check: ruff ✓ pyright ✓ unit tests 842 passed (the onetest_geolibrefailure is pre-existing on this base and already fixed on main by feat(geolibre): use geolibre's async command methods when available #360).Note: plotly.js emits
plotly_selecteda few hundred ms after mouseup (held back for double-click disambiguation), so the fixture waits on a sentinel output for browser→server sync before triggering the gated read.Related hygiene issues found during this investigation: #417, #418.