Skip to content

Add crosshair and shared cursor primitives - #55

Merged
tannerlinsley merged 15 commits into
mainfrom
taren/add-crosshair
Aug 5, 2026
Merged

Add crosshair and shared cursor primitives#55
tannerlinsley merged 15 commits into
mainfrom
taren/add-crosshair

Conversation

@tannerlinsley

@tannerlinsley tannerlinsley commented Aug 4, 2026

Copy link
Copy Markdown
Member

Summary

  • add renderer-native crosshair x/y rules, labels, markers, and categorical cursor bands with clipping, ordering, facets, motion, live-viewport projection, and public type exports
  • add shared focus and free-cursor controllers with explicit cursorHost bindings for browser and React Native synchronization, pinning, ownership, and accessibility
  • expose resolveCrosshairGuide from @tanstack/charts/crosshair for custom focus-guide renderers
  • make point-free rule marks focusable through semantic anchors without adding pointer hit targets
  • retain active keyed guide layers through scene updates so cursors animate continuously while focus callbacks receive current data
  • center categorical cursor bands on parent scale values, including grouped x and y bars
  • migrate the axis-pointer, synchronized-cursor, free-cursor, and cursor-motion catalog cases to the new primitives
  • publish a stacked-bar example with labeled animated x/y cursors and a working keyed data update

Behavior

Case 119 uses an x band that follows the focused period and extends 4 px beyond each bar edge, for 8 px additional width. Its x label names the period. A dotted y rule follows the focused stacked-segment endpoint, and its label shows that endpoint rather than the segment delta.

Both guides use spring motion across pointer focus and keyed data updates. Retained presentation points animate from their prior geometry while carrying the destination datum and semantic values, so the visible guide, focus callbacks, and labels remain consistent throughout an update. Semantic cursors and crosshairs project through ResolvedScale.viewport.map and remain aligned during transient live-viewport translation.

Matching controlled cursor values preserve the focused mark's plotted subgroup and stacked-endpoint geometry; genuinely different controlled values still override local focus. Pointer and keyboard hosts publish through ownership-safe cursor sessions, and leaving either interaction path clears only transient state owned by that host.

Issue #32 exposed the underlying gap: focus filters could reveal pre-authored marks, but they could not express one stable renderer-native cursor that follows focus. The new crosshair and cursor primitives provide that ownership model without application-authored SVG or rectangle overlays.

Validation

  • pnpm validate — 148 test files and 983 tests, plus TypeScript, formatting, docs, packages, adapters, SSR, catalog, and bundle gates
  • pnpm bundle:check and pnpm benchmark:check
  • pnpm canvas:check
  • quick conformance for cases 80, 87, 88, 117, and 119: 5/5 interaction cases passed across both renderers, revisions, and 320/640 px
  • full case 119 matrix: 1/1 interaction case passed at 320, 640, and 960 px in light and dark themes, both revisions, and both renderers
  • React catalog SSR/preview coverage and generated catalog sync
  • documentation contract: 83 pages, 83 catalog embeds, and 17 executable examples
  • catalog artifact: 110 cases across 483 allowlisted modules
  • independent post-rebase review found no remaining correctness blockers

Bundle impact

All locked bundle ceilings and comparison checks pass. Isolated gzip sizes are 353 B for the application cursor controller, 3,936 B for the cursor host policy, and 2,993 B for the crosshair extension.

The shared default host capability adds 2,655–2,827 gzip bytes, or 7.43–8.35%, across the 12 controlled TanStack comparison bundles versus the rebased main. The resulting controlled range is 34.03–39.56 KiB gzip. The baseline records that shared renderer capability explicitly; all 48 external-library rows remain unchanged.

Fixes #32

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 155 files, which is 55 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4c3324a1-9064-446f-82df-5e0939fc7689

📥 Commits

Reviewing files that changed from the base of the PR and between 1e8c048 and 7aab62c.

📒 Files selected for processing (155)
  • .changeset/brave-tooltips-expand.md
  • .changeset/bright-cursors-cross.md
  • API-FRICTION.md
  • benchmarks/bundle-size/universal-baseline.json
  • benchmarks/comparison/bundle-baseline.json
  • benchmarks/conformance/README.md
  • benchmarks/conformance/cases/117-focus-cursor-motion/case.json
  • benchmarks/conformance/cases/117-focus-cursor-motion/tanstack.ts
  • benchmarks/conformance/cases/119-stacked-bar-band-cursor/case.json
  • benchmarks/conformance/cases/119-stacked-bar-band-cursor/chart.ts
  • benchmarks/conformance/cases/119-stacked-bar-band-cursor/model.test.ts
  • benchmarks/conformance/cases/119-stacked-bar-band-cursor/model.ts
  • benchmarks/conformance/cases/119-stacked-bar-band-cursor/plot.ts
  • benchmarks/conformance/cases/119-stacked-bar-band-cursor/tanstack.ts
  • benchmarks/conformance/cases/119-stacked-bar-band-cursor/view.tsx
  • benchmarks/conformance/cases/80-echarts-axis-pointer/view.tsx
  • benchmarks/conformance/cases/87-echarts-synchronized-cursors/case.json
  • benchmarks/conformance/cases/87-echarts-synchronized-cursors/view.tsx
  • benchmarks/conformance/cases/88-echarts-free-cursor/case.json
  • benchmarks/conformance/cases/88-echarts-free-cursor/view.tsx
  • benchmarks/entries/charts-crosshair-kernel.ts
  • benchmarks/entries/charts-cursor-controller.ts
  • benchmarks/entries/charts-cursor-host-policy.ts
  • docs/comparison.md
  • docs/examples/facets-and-multiple-views.md
  • docs/examples/interactive-charts.md
  • docs/framework/alpine/reference/chart.md
  • docs/framework/angular/reference/chart.md
  • docs/framework/lit/reference/chart.md
  • docs/framework/octane/adapter.md
  • docs/framework/octane/reference/chart.md
  • docs/framework/preact/reference/chart.md
  • docs/framework/react/adapter.md
  • docs/framework/react/reference/chart.md
  • docs/framework/solid/reference/chart.md
  • docs/framework/svelte/reference/chart.md
  • docs/framework/vue/reference/chart.md
  • docs/guides/accessibility.md
  • docs/guides/custom-marks-and-renderers.md
  • docs/guides/dynamic-data-and-animation.md
  • docs/guides/exporting.md
  • docs/guides/interactions-and-selections.md
  • docs/guides/ssr-and-hydration.md
  • docs/guides/tooltips-and-focus.md
  • docs/installation.md
  • docs/reference/chart-definitions.md
  • docs/reference/chart-spec.md
  • docs/reference/custom-extensions.md
  • docs/reference/dom-host.md
  • docs/reference/focus-and-interaction.md
  • docs/reference/index.md
  • docs/reference/motion.md
  • docs/reference/rendering-and-export.md
  • docs/reference/runtime-and-scene.md
  • docs/reference/types.md
  • llms.txt
  • packages/charts-core/docs/comparison.md
  • packages/charts-core/docs/examples/facets-and-multiple-views.md
  • packages/charts-core/docs/examples/interactive-charts.md
  • packages/charts-core/docs/framework/alpine/reference/chart.md
  • packages/charts-core/docs/framework/angular/reference/chart.md
  • packages/charts-core/docs/framework/lit/reference/chart.md
  • packages/charts-core/docs/framework/octane/adapter.md
  • packages/charts-core/docs/framework/octane/reference/chart.md
  • packages/charts-core/docs/framework/preact/reference/chart.md
  • packages/charts-core/docs/framework/react/adapter.md
  • packages/charts-core/docs/framework/react/reference/chart.md
  • packages/charts-core/docs/framework/solid/reference/chart.md
  • packages/charts-core/docs/framework/svelte/reference/chart.md
  • packages/charts-core/docs/framework/vue/reference/chart.md
  • packages/charts-core/docs/guides/accessibility.md
  • packages/charts-core/docs/guides/custom-marks-and-renderers.md
  • packages/charts-core/docs/guides/dynamic-data-and-animation.md
  • packages/charts-core/docs/guides/exporting.md
  • packages/charts-core/docs/guides/interactions-and-selections.md
  • packages/charts-core/docs/guides/ssr-and-hydration.md
  • packages/charts-core/docs/guides/tooltips-and-focus.md
  • packages/charts-core/docs/installation.md
  • packages/charts-core/docs/reference/chart-definitions.md
  • packages/charts-core/docs/reference/chart-spec.md
  • packages/charts-core/docs/reference/custom-extensions.md
  • packages/charts-core/docs/reference/dom-host.md
  • packages/charts-core/docs/reference/focus-and-interaction.md
  • packages/charts-core/docs/reference/index.md
  • packages/charts-core/docs/reference/motion.md
  • packages/charts-core/docs/reference/rendering-and-export.md
  • packages/charts-core/docs/reference/runtime-and-scene.md
  • packages/charts-core/docs/reference/types.md
  • packages/charts-core/llms.txt
  • packages/charts-core/package.json
  • packages/charts-core/src/canvas.test.ts
  • packages/charts-core/src/canvas.ts
  • packages/charts-core/src/crosshair-resolver.ts
  • packages/charts-core/src/crosshair.test.ts
  • packages/charts-core/src/crosshair.ts
  • packages/charts-core/src/cursor-host-contract.ts
  • packages/charts-core/src/cursor-host.ts
  • packages/charts-core/src/cursor-public.ts
  • packages/charts-core/src/cursor.test.ts
  • packages/charts-core/src/cursor.ts
  • packages/charts-core/src/dom-types.ts
  • packages/charts-core/src/export.test.ts
  • packages/charts-core/src/export.ts
  • packages/charts-core/src/exports.test.ts
  • packages/charts-core/src/facet.test.ts
  • packages/charts-core/src/facet.ts
  • packages/charts-core/src/focus-layer.ts
  • packages/charts-core/src/focus-mark.test.ts
  • packages/charts-core/src/focus-presentation.ts
  • packages/charts-core/src/index.ts
  • packages/charts-core/src/interaction.ts
  • packages/charts-core/src/mark-state.ts
  • packages/charts-core/src/motion.test.ts
  • packages/charts-core/src/motion.ts
  • packages/charts-core/src/nearest.test.ts
  • packages/charts-core/src/nearest.ts
  • packages/charts-core/src/reconcile.test.ts
  • packages/charts-core/src/reconcile.ts
  • packages/charts-core/src/renderer.test.ts
  • packages/charts-core/src/renderer.ts
  • packages/charts-core/src/rule.ts
  • packages/charts-core/src/scene.ts
  • packages/charts-core/src/svg-focus-guide-layer.ts
  • packages/charts-core/src/svg-focus-guide-serializer.ts
  • packages/charts-core/src/svg-renderer.ts
  • packages/charts-core/src/svg-surface.test.ts
  • packages/charts-core/src/svg-surface.ts
  • packages/charts-core/src/type-contract.test.ts
  • packages/charts-core/src/types.ts
  • packages/charts-core/src/universal-types.ts
  • packages/charts-core/src/universal.ts
  • packages/octane-charts/src/CanvasChart.client.test.tsrx
  • packages/octane-charts/src/CanvasChart.test.tsrx
  • packages/react-charts-catalog/package.json
  • packages/react-charts-catalog/src/cases/119-stacked-bar-band-cursor.ts
  • packages/react-charts-catalog/src/catalog.test.tsx
  • packages/react-charts-catalog/src/index.ts
  • packages/react-charts-catalog/src/preview.test.tsx
  • packages/react-charts/src/CanvasChart.test.tsx
  • packages/react-native-charts/src/Chart.test.tsx
  • packages/react-native-charts/src/Chart.tsx
  • packages/react-native-charts/src/FocusOverlay.tsx
  • packages/react-native-charts/src/SvgScene.test.tsx
  • packages/react-native-charts/src/SvgScene.tsx
  • packages/react-native-charts/src/interaction.test.ts
  • packages/react-native-charts/src/interaction.ts
  • packages/react-native-charts/src/paint.test.ts
  • packages/react-native-charts/src/paint.ts
  • scripts/catalog-artifact.mjs
  • scripts/catalog-definition-shapes.test.mjs
  • scripts/check-canvas-renderer.mjs
  • scripts/check-packed-consumers.mjs
  • scripts/measure-bundles.mjs
  • scripts/public-callback-contract.mjs
  • scripts/sync-react-charts-catalog.mjs

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@tannerlinsley
tannerlinsley marked this pull request as ready for review August 4, 2026 23:50
@nx-cloud

nx-cloud Bot commented Aug 5, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 7aab62c

Command Status Duration Result
nx run charts-workspace:ci ✅ Succeeded 1m 53s View ↗

☁️ Nx Cloud last updated this comment at 2026-08-05 18:39:43 UTC

@tannerlinsley
tannerlinsley merged commit 16585da into main Aug 5, 2026
19 checks passed
@github-actions github-actions Bot mentioned this pull request Aug 5, 2026
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.

whenFocused silently never renders marks that emit no interaction points (ruleX/ruleY)

1 participant