Skip to content

ENG-10441: Complete Reflex events and cross-filtering#113

Merged
masenf merged 8 commits into
mainfrom
farhan/eng-10441-complete-reflex-events-and-cross-filtering-in-xy
Jul 21, 2026
Merged

ENG-10441: Complete Reflex events and cross-filtering#113
masenf merged 8 commits into
mainfrom
farhan/eng-10441-complete-reflex-events-and-cross-filtering-in-xy

Conversation

@FarhanAliRaza

Copy link
Copy Markdown
Contributor

Closes the P0 interaction loop for reflex_xy: browser interaction → semantic event → Reflex handler → State update → dependent @reflex_xy.figure recompute → 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, and on_view_change now deliver versioned, JSON-bounded envelopes carrying the stable figure token, 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 when on_select_end is wired) and carry kind/mode/bounds/polygon/canonical_row_ids/rows/truncated, capped at SELECTION_EVENT_ROW_LIMIT = 1000 rows / SELECTION_EVENT_ID_LIMIT = 10000 ids with total_count always exact. New Selection.rows() gives a deterministic, JSON-safe canonical projection (pick-result shape); new reflex_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_change interaction 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_msg drops messages minted against a replaced figure version (same pattern as drill_seq staleness). Clients that omit v remain 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-src caveats, and shared-handler/viewport-sync patterns; the demo app cross-filters by selection rows and demonstrates resolve_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 — clean
  • node js/build.mjs — bundles fresh
  • uv run --with pre-commit pre-commit run --all-files — all hooks pass

Not covered: the browser smoke (scripts/reflex_ws_smoke.py) needs a running demo app + headless Chromium; worth a manual pass before release.

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.
@linear-code

linear-code Bot commented Jul 20, 2026

Copy link
Copy Markdown

ENG-10441

@FarhanAliRaza
FarhanAliRaza marked this pull request as draft July 20, 2026 18:53
@codspeed-hq

codspeed-hq Bot commented Jul 20, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 97 untouched benchmarks
⏩ 1 skipped benchmark1


Comparing farhan/eng-10441-complete-reflex-events-and-cross-filtering-in-xy (6317ef5) with main (09b5873)

Open in CodSpeed

Footnotes

  1. 1 benchmark was skipped, so the baseline result was used instead. If it was deleted from the codebase, click here and archive it to remove it from the performance reports.

@masenf masenf left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

it would be nice to have TypedDict declarations for the event payloads handled on the reflex side

Comment thread tests/reflex_adapter/test_selections.py Outdated
Comment thread tests/reflex_adapter/test_socket_data_plane.py
…lete-reflex-events-and-cross-filtering-in-xy

# Conflicts:
#	python/xy/interaction.py
@greptile-apps

greptile-apps Bot commented Jul 21, 2026

Copy link
Copy Markdown

Greptile Summary

This PR completes semantic chart events and cross-filtering for reflex_xy. The main changes are:

  • Versioned, bounded envelopes for hover, click, selection, and view events.
  • Canonical selection rows with server-side resolution for truncated results.
  • Stable-token republishing that preserves viewport and selection state.
  • Request identities and payload versions for stale-reply rejection.
  • Keyboard click parity, updated clients, tests, docs, and demo behavior.

Confidence Score: 5/5

This 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.

T-Rex T-Rex Logs

What T-Rex did

  • T-Rex produced proof for the posted P1 finding in review comment 0.
  • T-Rex produced proof for the posted P1 finding in review comment 1.
  • T-Rex performed general-contract-validation-proof analysis, capturing the baseline showcase, smoke setup, viewport issue, drill, and separate Playwright run observations.
  • T-Rex linked and inspected 11 artifacts from the Playwright run to assess drill effects and hover behavior.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
python/reflex-xy/reflex_xy/assets/XYChart.jsx Adds semantic event envelopes, throttling, republish restoration, and request-specific click and selection reply tracking.
python/reflex-xy/reflex_xy/namespace.py Rejects versioned client messages when their figure payload has been replaced.
python/xy/channel.py Returns bounded selection details and preserves selection request identities in replies.
python/reflex-xy/reflex_xy/selections.py Adds server-side resolution of complete selections from event geometry.
js/src/53_interaction.js Adds keyboard activation details and retains selection geometry across drill changes.

Comments Outside Diff (2)

  1. General comment

    P1 Browser smoke sends drill input outside Chromium's viewport

    • Bug
      • The documented scripts/reflex_ws_smoke.py flow consistently times out at density drill even though the rendered chart and socket transport are healthy. Headless Chromium reports a 780×437 viewport, while the cloud center used for wheel input is y=517. CDP input at that off-screen coordinate does not reach the chart. Scrolling the cloud into view immediately allows the same 16-wheel sequence to drill.
    • Cause
      • The smoke deliberately removed device-metrics emulation but computes viewport-relative getBoundingClientRect() coordinates without ensuring #cloud is visible before dispatching input.
    • Fix
      • Call probe.scroll_to("cloud") before obtaining its rect and dispatching drill/hover input, or otherwise clamp/scroll the target into the active viewport. Retain the no-emulation workaround if pointer movement requires it.

    T-Rex Ran code and verified through T-Rex

  2. General comment

    P1 Hovering a rendered drilled point does not close the Reflex state loop

    • Bug
      • After scrolling the real cloud into view, the density tier drills successfully, but the smoke’s hover traversal never produces the expected x=… state readout. A separate Playwright execution scanned the rendered pick framebuffer, found a valid drilled point (pickable=true, 195 points), moved to that exact rendered coordinate, and still observed no Reflex readout. This contradicts the claimed hover → kernel pick → Reflex event → state delta path.
    • Cause
      • The failure is downstream of rendering and drill: valid drilled pick pixels exist, but pointer hover does not result in the expected Reflex DOM state update. The precise break between browser hover dispatch, socket pick response, wrapper event conversion, and state handler requires further tracing.
    • Fix
      • Trace the hover/pick frames and browser event callbacks for a known pick-buffer coordinate. Verify XYChart.jsx emits the v1 hover envelope, the namespace resolves the drilled subset using drill_seq, and Demo.on_hover receives and publishes the state delta. Add the exact rendered-point traversal as a deterministic smoke assertion.

    T-Rex Ran code and verified through T-Rex

Reviews (4): Last reviewed commit: "test(smoke): isolate stale-mask gate fro..." | Re-trigger Greptile

Comment thread python/reflex-xy/reflex_xy/assets/XYChart.jsx
Comment thread python/reflex-xy/reflex_xy/assets/XYChart.jsx
…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).
Comment thread python/reflex-xy/reflex_xy/assets/XYChart.jsx Outdated
…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.
@FarhanAliRaza
FarhanAliRaza marked this pull request as ready for review July 21, 2026 21:14
Comment thread python/reflex-xy/reflex_xy/assets/XYChart.jsx
@masenf
masenf merged commit 245ab57 into main Jul 21, 2026
29 checks passed
masenf added a commit that referenced this pull request Jul 21, 2026
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).
masenf pushed a commit that referenced this pull request Jul 22, 2026
… 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants