ENG-10441: Complete Reflex events and cross-filtering#113
Conversation
Close the interaction loop for reflex-xy: point hover/click, select end, and view change now reach Reflex handlers as versioned, bounded JSON envelopes, and handler-driven state updates republish dependent figures behind their stable tokens without remounting or losing the source chart's viewport and selection. - Selection.rows() / interaction.selection_rows: deterministic, JSON-safe canonical row projection matching the pick-result shape. - channel.py: opt-in include_rows enrichment of selection replies (kind/mode/bounds/polygon/canonical_row_ids/rows/truncated) capped at SELECTION_EVENT_ROW_LIMIT=1000 rows and SELECTION_EVENT_ID_LIMIT=10000 ids; replies without the flag are unchanged. - reflex_xy.resolve_selection(): re-resolves a select_end event against the live figure for complete, unbounded server-side rows when the bounded payload reports truncation. - namespace.on_msg: clients echo the payload version as `v`; messages minted against a replaced figure version are dropped (mirrors the drill_seq staleness pattern; absent `v` stays compatible). - XYChart.jsx: v1 event envelopes, interaction-flag injection only for wired handlers, 120 ms latest-wins hover throttle, 200 ms view-change debounce with linked/republish sources suppressed, click screen coords + modifiers, and view/selection restore across republish that never redispatches handlers (no feedback loops); timers and latches cleaned up on unmount. - 53_interaction.js: keyboard Enter/Space activation emits the same click contract as pointer clicks (input parity); bundles regenerated. - Demo app, reflex-integration design doc, and README document the envelopes, limits, truncation fallback, and cross-filter patterns. - _axisgrid.py gains a type-only cast so the repo-wide ty gate is clean.
Merging this PR will not alter performance
Comparing Footnotes
|
masenf
left a comment
There was a problem hiding this comment.
it would be nice to have TypedDict declarations for the event payloads handled on the reflex side
…lete-reflex-events-and-cross-filtering-in-xy # Conflicts: # python/xy/interaction.py
Greptile SummaryThis PR completes semantic chart events and cross-filtering for
Confidence Score: 5/5This looks safe to merge. Selection restore replies use request-specific identities, and version checks prevent messages for replaced figures from reaching kernel handling. No blocking issue remains in the reviewed interaction paths.
What T-Rex did
Important Files Changed
|
…l swaps Selection state previously degraded around density drill transitions: the modebar Select trigger froze at whatever pickability held when the toolbar was built, and a pan/zoom re-drill blanked the highlight until the kernel's mask reply round-tripped. Pickability recomputes through ChartView._updatePickable() on every drill state change; the Select trigger shows/hides with it, closing its menu and dropping select drag modes when the capability is revoked. The client retains brush geometry in data space and re-derives a provisional mask locally when a re-drill ships a new subset, so the highlight never blinks out; the kernel reply remains the authoritative overwrite. The Reflex wrapper keeps the outgoing view as a pointer-inert ghost overlay during a republish until the replacement restores selection and drill tier (bounded by a 1200 ms timeout), making stable-token republishes pixel-steady. Demo app gains event counters reproducing the PR #113 interaction contract; specs updated (spec/api/interaction.md, spec/design/reflex-integration.md).
…lete-reflex-events-and-cross-filtering-in-xy Conflict resolutions beyond textual merges: - XYChart.jsx onPayload: adopt main's in-place ChartView.updatePayload fast path while keeping this branch's republish restore contract — on an in-place swap the wrapper pins the viewport (clearing the domain interpolation, as the kernel append path does) and re-requests the selection mask; the ghost overlay now covers only the rebuild path. - 56_animation.js updatePayload recomputed pickability with the old static formula; route it through _updatePickable() so the modebar Select trigger stays in sync (semantic conflict with this branch's dynamic-pickability work). - Accept main's removal of python/reflex-xy/examples/demo_app (#124) and port its unique pieces into examples/reflex: v1 event envelopes in the showcase handlers (data/canonical_row_id/modifiers, selection total_count/data_bounds, view x_domain — the flat pre-#113 shapes no longer arrive), the handler-revision republish stress test with event counters, and the README interaction contract checks. - Specs updated to match: reflex-integration.md documents the in-place republish restore; interaction.md adds the updatePayload call site to the _updatePickable funnel.
Selection replies now echo the request's `seq` (kernel-side, covered in test_channel.py), so the wrapper identifies its own republish restores exactly instead of via a restoringSelection flag that a concurrent user selection reply could consume — the flag suppressed the wrong event when replies raced. Restore tracking moves to a per-seq set. The republish ghost overlay is removed: when the in-place updatePayload swap is refused, the wrapper destroys the outgoing view and rebuilds immediately. Brush geometry retention still reconstructs the selection mask across re-drills. Spec updated to match.
Declare every envelope XYChart.jsx dispatches (PointHoverEvent, PointClickEvent, SelectEndEvent, SelectionPayload, ViewChangeEvent and their component pieces) as TypedDicts in reflex_xy.events, exported from the package root. Handlers still receive plain dicts; the declarations document the shapes and let type checkers verify field access. Annotate the demo handlers, document the contract in the reflex-integration spec, and pin the lasso resolution test to exact row indices.
IDE hovers show only a TypedDict's docstring, never its (often inherited) field declarations, so annotating a handler with e.g. PointHoverEvent surfaced nothing about the payload. Embed the full wire shape of each envelope in its class docstring so the hover documents exactly what XYChart.jsx sends.
…ontinuity The headless render smoke failed after the brush-continuity feature: the probe's earlier lasso leaves _lastBrush retained, so the drill swap legitimately restores a provisional mask before the deliberately stale drill_seq reply arrives — the sstale assertion misread that as the stale mask being applied. Clear the selection before that scenario so sstale/sfresh isolate exactly the drill_seq gate, and add a positive srestore bit asserting the retained brush re-derives its mask across a drill swap. Spec regression-coverage note updated.
Reconcile the updatePayload re-home fix with main's Reflex events / cross-filtering work (#113) and the box-zoom scatter-collapse fix (#101). - js/src/56_animation.js: auto-merged — the re-home block (clear view0 before deriving the new home) and main's `_updatePickable()` refactor are separate hunks of updatePayload and coexist. The full-payload path still routes through updatePayload; main's new XYChart.jsx onPayload re-pins a navigated chart via the restore contract, while a dependent (never-navigated) chart — the §4 detail histogram — takes the re-home, which is what fixes zoom-out. - python/xy/static/{index,standalone}.js: regenerated from merged js/src. - tests/reflex_adapter/test_assets.py: took main's assertion — the wrapper now fits the live spec via `eventSpec(data.spec, cbRef.current)`, superseding the stale-literal fix carried on this branch. - spec/design/reflex-integration.md §4: keep the re-home note, reworded to defer viewport preservation to main's restore contract (the home must be the new spec's extents in both cases).
… throttle (#201) PR #113 replaced the wrapper's immediate on_view_change dispatch with a 200 ms trailing-edge debounce. A continuous pan/zoom emits a view_change per animation frame, so the timer reset on every frame and the handler could only fire after the gesture went quiet — an on_view_change-computed detail chart froze until the interaction ended instead of tracking it live (demo section 4). Replace the debounce with a leading+trailing latest-wins throttle (VIEW_THROTTLE_MS = 120, matching hover): the first event of a gesture dispatches immediately, updates stream at most once per window while it is in progress, and the trailing flush guarantees the resting viewport always lands last. The envelope's phase field now reflects the client's gesture phase — "update" mid-gesture, "final" at rest — so handlers that only want settled views can filter on it; linked/republish suppression and unmount cleanup are unchanged. Verified against the running showcase app with a CDP probe: 24 wheel steps at 140 ms spacing produced 23 mid-gesture Reflex state updates (the old debounce produces none), with the exact resting view arriving as the final event. Spec (reflex-integration.md), ViewChangeEvent typing/docstring, README, demo comment, and the wrapper contract test updated to pin the new behavior.
Closes the P0 interaction loop for
reflex_xy: browser interaction → semantic event → Reflex handler → State update → dependent@reflex_xy.figurerecompute → republish behind stable tokens, with no remount and no feedback loops.What changed
Event envelopes (v1).
on_point_hover,on_point_click,on_select_end, andon_view_changenow deliver versioned, JSON-bounded envelopes carrying the stable figuretoken, trace identity, the canonical row id (never a shipped/decimated buffer index),data/datum, and — for clicks — canvas-relative screen coords and keyboard modifiers.Bounded selections + complete data (ENG-10461). Selection replies opt into
include_rows(sent only whenon_select_endis wired) and carrykind/mode/bounds/polygon/canonical_row_ids/rows/truncated, capped atSELECTION_EVENT_ROW_LIMIT = 1000rows /SELECTION_EVENT_ID_LIMIT = 10000ids withtotal_countalways exact. NewSelection.rows()gives a deterministic, JSON-safe canonical projection (pick-result shape); newreflex_xy.resolve_selection(event)re-resolves the geometry server-side against canonical f64 columns for the complete, unbounded selection — large selections never ride the browser as JSON.Event enablement, throttling, loop hygiene. The JSX wrapper injects
click/view_changeinteraction flags only when those handler props are wired (zero overhead otherwise), throttles hover dispatch (120 ms latest-wins), debounces view changes (200 ms trailing) and suppresses non-user sources. On a stable-token republish the wrapper preserves the user's viewport and re-issues the active selection without redispatching handlers, so cross-filter refreshes can't loop or reset zoom.Stale-event rejection. Clients echo the last payload version (
v) on every kernel message;XYNamespace.on_msgdrops messages minted against a replaced figure version (same pattern asdrill_seqstaleness). Clients that omitvremain compatible.Input parity. Keyboard Enter/Space on a focused datum emits the same click contract (screen + modifiers) as pointer activation; render-client bundles regenerated via
node js/build.mjs.Docs & demo.
docs/engineering/design/reflex-integration.md§5.1 documents the envelopes, limits, truncation fallback, static-srccaveats, and shared-handler/viewport-sync patterns; the demo app cross-filters by selection rows and demonstratesresolve_selection.Verification
uv run pytest— 1949 passed, 65 skipped (includes new unit + socket-E2E coverage: enriched selection round-trip, stale-version drop,resolve_selection, keyboard-activation contract, payload-bound tests)uv run ruff check ./ruff format --check ./uv run ty check— cleannode js/build.mjs— bundles freshuv run --with pre-commit pre-commit run --all-files— all hooks passNot covered: the browser smoke (
scripts/reflex_ws_smoke.py) needs a running demo app + headless Chromium; worth a manual pass before release.