Skip to content

Add geometry-aware point selection - #29

Merged
tannerlinsley merged 5 commits into
TanStack:mainfrom
gillkyle:codex/interaction-geometry
Aug 2, 2026
Merged

Add geometry-aware point selection#29
tannerlinsley merged 5 commits into
TanStack:mainfrom
gillkyle:codex/interaction-geometry

Conversation

@gillkyle

@gillkyle gillkyle commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Problem

The production resolver measures maxFocusDistance from a mark's interaction anchor. For a tall bar, the anchor can be hundreds of pixels from a pointer that is visibly inside the painted bar, so a closer neighboring anchor wins. Chart-wide nearest-x fixes that case but makes unrelated off-bar targets too permissive; a pure x tie also selects the wrong stacked segment above or below a stack.

I built a little gallery of different chart types exposing the problem, and a possible solution (with some tradeoffs):

CleanShot.2026-07-31.at.21.13.25.mp4
CleanShot.2026-07-31.at.23.39.25.mp4
MAIN

pointer
   │
   ▼
measure distance to every ChartPoint anchor
   │
   ▼
choose nearest anchor within 48 px


THIS PR

pointer
   │
   ├── Is it inside any rendered shape?
   │      └── yes: choose the topmost painted shape
   │
   └── no: measure distance from each shape boundary
          ├── vertical bars/lines/areas prefer x
          ├── horizontal bars/areas prefer y
          └── dots/rects/hexagons use xy

The proposed solution is a combo of Vega's traversal of points in reverse order + ideas from Observable Plot–style axis affinity.

Built-in marks would describe their natural geometry: vertical bars, lines, areas, and vertical bands prefer x; horizontal bars and bands prefer y; rects, dots, and hexagons use two-dimensional geometry. Facets translate hit regions with their points.

The sandbox has a 14-case before/after lab with stacked and horizontal bars, line/area, bubbles, cellstimelines, pie, radar, maps, and Sankey geometry. Run it with:

pnpm dev:interaction-geometry

Correctness, speed, and bundle-size trade-offs

The current system checks only a mark’s representative point, so it can select the wrong mark when the pointer is inside a large bar or shape.

The proposed system checks the painted shape first, then falls back to the mark’s natural x/y behavior. This improves correctness while keeping the implementation simple, but point-only lookups are about 1.8× slower in the benchmark.
D3’s quadtree and Delaunay utilities use an indexed approach: they organize points spatially so pointer lookups can skip most of the dataset. That can be much faster for huge point clouds, but it increases bundle size and memory usage and requires rebuilding when data changes.

Approach Main trade-off
Current Smallest and fastest, but can select the wrong large mark
Proposed Correct large-mark behavior with modest runtime and bundle costs
D3-style index Faster for huge datasets, but larger bundle, more memory, and rebuild work

For ordinary charts, the proposed approach trades some larger bundle size for more accurate pointer selection. D3-style indexing could maybe remain a lazy loaded optimization later for very large datasets?


More Context for AI review/Appendix

The following is a bunch of AI authored stuff and tests I ran and looked over while trying to find a good way to do this.

Agent-run pointer benchmark

Measured on an Apple M4 Pro (48 GB), macOS arm64, Node 24.12.0 with
pnpm performance:pointer. Each value is median / p95 microseconds per
pointer query. The command rotates implementation order, exposes GC, and first
asserts that the POC and optimized implementation return the same target.

Fixture Current anchor Original geometry POC Optimized geometry
10k ordinary points 14.2 / 14.5 116.7 / 118.1 23.8 / 26.5
10k contained rectangles 18.8 / 19.9† 62.2 / 64.4 27.1 / 29.1
10k stacked rectangles, x fallback 18.7 / 20.7† 210.5 / 214.4 127.0 / 132.1
10k contained circles 18.5 / 19.0† 64.5 / 68.3 25.7 / 28.2
2k polygon fallbacks 4.0 / 4.2† 132.9 / 134.4 79.5 / 80.9

† Production is a speed baseline on geometry rows, not a correctness
baseline; it cannot return the intended geometry behavior.

Current production vs optimized geometry — median/query
common scale: one # ~= 4 microseconds; shorter is faster

ordinary points       prod ####                              14.2
                      new  ######                            23.8
rect containment      prod #####                             18.8 †
                      new  #######                           27.1
stacked x fallback    prod #####                             18.7 †
                      new  ################################ 127.0
circle containment    prod #####                             18.5 †
                      new  ######                            25.7
polygons (2k)         prod #                                  4.0 †
                      new  ####################              79.5

Point-only comparison with prior implementations

This uses identical 10k point targets. These are query kernels, not end-to-end
library benchmarks: Plot excludes DOM/render/event work, and D3 excludes index
construction. The result does not claim TanStack Charts is globally faster
than Plot, D3, or Vega.

Point-only resolver Median / query p95 / query
Current production linear scan 13.9 µs 14.7 µs
Proposed geometry resolver 25.2 µs 27.4 µs
Observable Plot 0.6.17 pointer kernel 41.4 µs 47.1 µs
D3 quadtree 3.0.1, indexed 2.8 µs 3.3 µs
D3 Delaunay 6.0.4, cold start 8.6 µs 10.2 µs
D3 Delaunay 6.0.4, coherent start 2.8 µs 3.6 µs
Point-only median/query — one # ~= 1 microsecond; shorter is faster

D3 quadtree       ###                                       2.8
D3 Delaunay warm  ###                                       2.8
D3 Delaunay cold  #########                                 8.6
current prod      ##############                           13.9
new geometry      #########################                25.2
Observable Plot   ######################################### 41.4
One-time 10k index construction Median p95
D3 quadtree 1.76 ms 2.32 ms
D3 Delaunay 2.47 ms 2.88 ms

Measured retained index structure was 9,643 quadtree internal arrays plus
10,000 leaf objects, or at least 624.6 KiB of public Delaunay typed arrays.
Both exclude the original points and unobservable object overhead.

For contained rectangles, the proposed generic resolver measured 23.8 / 25.0
µs. A source-equivalent Vega cached-bounds pass measured 9.8 / 25.0 µs, but
that is deliberately a lower bound: Vega subsequently performs the actual
Canvas path test. It supports learning from cached bounds, not a claim about
full Vega interaction speed.

Bundle measurements

Isolated resolver/kernel Minified Gzip
Current anchor-only resolver 157 B 153 B
Proposed geometry resolver 1,955 B 869 B
D3 quadtree 4,951 B 1,971 B
D3 Delaunay 19,620 B 7,308 B
Isolated gzip — one # ~= 250 bytes; shorter is smaller

anchor-only  #                                153 B
geometry     ###                              869 B
quadtree     ########                       1,971 B
Delaunay     #############################  7,308 B

Integrated retained-input deltas:

Consumer Minified delta Gzip delta
Representative marks +261 B +83 B
TanStack DOM host +1,885 B +685 B
React adapter +1,885 B +707 B
React line consumer +1,903 B +720 B

pnpm bundle:check is intentionally red and no lock was updated. It reports
the deltas above and the branch is over the React compact-scale line
(17.17 > 16.80 kB), Stats (40.81 > 40.50 kB), and React Stats
(41.65 > 41.50 kB) ceilings. That keeps acceptance of the size cost explicit.

Prior art reviewed

The established pattern is “cheap candidate rejection/lookup, then
mark-specific exact testing.” This PR adopts the exact-testing and paint-order
parts without taking an index dependency by default.

Agentic verification

  • pnpm performance:pointer — completed; semantic equivalence guards passed
  • focused interaction tests — 45 passed
  • pnpm test — 678 passed across 123 files
  • pnpm typecheck — passed
  • pnpm docs:check — passed (82 pages, 84 catalog embeds, 17 executable examples)
  • pnpm package:check — passed (packed consumers and 7 adapters)
  • pnpm format:check — passed
  • sandbox production build — passed
  • pnpm bundle:check — expected failure; measurements retained above, lock unchanged

Questions for review

  1. Is mark-level focusAffinity plus point-level hitRegion the right
    ownership boundary?
  2. Is +685 B gzip in the integrated DOM host acceptable for the default
    correctness behavior?
  3. Should cached bounds be part of the first implementation, or remain a
    profile-driven follow-up?
  4. Is the comprehensive sandbox lab worth keeping beside the smaller core
    invariants and benchmark harness?

Summary by CodeRabbit

  • New Features

    • Pointer interactions now resolve against rendered shapes, improving selection of bars, areas, bubbles, polygons, and other complex marks.
    • Added support for interaction affinity, facet-aware targeting, clipping, transforms, and animated focus scenes.
    • Custom marks and renderers can provide semantic interaction points and resolved focus geometry.
    • Added an interaction-geometry lab and pointer-performance tooling.
  • Bug Fixes

    • Improved focus marker sizing during bar inset transitions.
    • Synchronized explicit x/y cursors across facet views.
    • Larger overlapping bubbles are now targeted consistently.
  • Documentation

    • Expanded guidance for custom interactions, focus behavior, spatial indexes, and rendering.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8cba10ff-a5ba-4e37-bff8-33f96ed30bbb

📥 Commits

Reviewing files that changed from the base of the PR and between 2e1f7ee and 76bcdbb.

📒 Files selected for processing (17)
  • API-FRICTION.md
  • benchmarks/bundle-size/README.md
  • benchmarks/bundle-size/universal-baseline.json
  • benchmarks/interaction/README.md
  • docs/guides/bundle-size-and-performance.md
  • examples/sandbox/package.json
  • examples/sandbox/src/InteractionGeometryLab.test.ts
  • examples/sandbox/src/InteractionGeometryLab.tsx
  • examples/sandbox/src/main.tsx
  • examples/sandbox/src/styles.css
  • packages/charts-core/docs/guides/bundle-size-and-performance.md
  • packages/charts-core/src/canvas.ts
  • packages/charts-core/src/dom-types.ts
  • packages/charts-core/src/facet.test.ts
  • packages/charts-core/src/nearest.test.ts
  • packages/charts-core/src/nearest.ts
  • packages/charts-core/src/renderer.ts
🚧 Files skipped from review as they are similar to previous changes (12)
  • examples/sandbox/src/main.tsx
  • packages/charts-core/docs/guides/bundle-size-and-performance.md
  • examples/sandbox/package.json
  • benchmarks/interaction/README.md
  • packages/charts-core/src/facet.test.ts
  • benchmarks/bundle-size/universal-baseline.json
  • packages/charts-core/src/dom-types.ts
  • benchmarks/bundle-size/README.md
  • packages/charts-core/src/renderer.ts
  • examples/sandbox/src/styles.css
  • docs/guides/bundle-size-and-performance.md
  • API-FRICTION.md

📝 Walkthrough

Walkthrough

This change adds geometry-aware pointer resolution from rendered scene primitives. It adds interaction metadata, facet and transition handling, benchmark coverage, an interaction geometry lab, documentation, and bundle-budget updates.

Changes

Scene-based interaction geometry

Layer / File(s) Summary
Interaction contracts and mark metadata
packages/charts-core/src/types.ts, packages/charts-core/src/rect.ts, packages/charts-core/src/bar.ts, packages/charts-core/src/area*.ts, packages/charts-core/src/dot.ts, packages/charts-core/src/band.ts, packages/charts-core/src/line.ts, packages/charts-core/src/hexagon.ts
Scene primitives now carry semantic points and focus affinity. Built-in marks attach interaction data directly to rendered geometry.
Scene resolution and host integration
packages/charts-core/src/nearest.ts, packages/charts-core/src/scene.ts, packages/charts-core/src/renderer.ts, packages/react-native-charts/src/interaction.ts
Pointer lookup uses painted containment, geometry, transforms, clipping, affinity, spatial indexes, and returned focus scenes.
Facet, focus, and inset behavior
packages/charts-core/src/facet.ts, packages/charts-core/src/focus-layer.ts, packages/charts-core/src/mark-state.ts, packages/charts-core/src/facet.test.ts, packages/charts-core/src/focus-mark.test.ts
Facet scenes remap interaction references. Focus matching uses stable identity. Bar insets change along the categorical axis.
Interaction geometry lab
examples/sandbox/src/InteractionGeometryLab.tsx, examples/sandbox/src/InteractionGeometryLab.test.ts, examples/sandbox/src/main.tsx, examples/sandbox/src/styles.css
The sandbox adds rendered-geometry proofs for marks, facets, animation, transforms, clipping, and stress cases.

Measurement and supporting updates

Layer / File(s) Summary
Resolver and conformance benchmarks
benchmarks/interaction/*, benchmarks/entries/*, scripts/measure-pointer-resolution.mjs
Benchmarks compare anchor, geometry, Plot, D3, and Vega selection, validate equivalent results, and report timing and storage metrics.
Bubble ordering and bundle baselines
benchmarks/conformance/cases/scatter-bubble/*, benchmarks/bundle-size/*, scripts/measure-bundles.mjs
Bubble data is shared and sorted by descending body mass. Bundle baselines and budgets reflect the new interaction resolver.
Guides, references, and release notes
docs/*, packages/charts-core/docs/*, .changeset/bright-hit-regions.md, API-FRICTION.md
Documentation describes scene interaction metadata, spatial-index inputs, focus-painted scenes, facet synchronization, and geometry-aware pointer resolution.
Commands and project notes
README.md, examples/sandbox/README.md, examples/sandbox/package.json, package.json, PLAN.md
Development and pointer-performance commands are documented. Bundle labels are updated.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • TanStack/charts#11: Both changes update native chart interaction and spatial-index handling.
  • TanStack/charts#16: Both changes modify Sankey conformance implementations.
  • TanStack/charts#25: Both changes modify packages/charts-core/src/nearest.ts and nearest-point logic.

Suggested reviewers: tannerlinsley

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.20% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the PR's main change: geometry-aware point selection for chart interactions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@gillkyle
gillkyle force-pushed the codex/interaction-geometry branch from 8322499 to 054d23b Compare August 2, 2026 04:09
@tannerlinsley
tannerlinsley marked this pull request as ready for review August 2, 2026 04:28

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/charts-core/src/focus-layer.ts (1)

78-107: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Align default focus matching with restoreFocusedPoint’s stability rules.

restoreFocusedPoint can return a point whose key differs when it falls back to chartValueEqual(xValue, yValue) + markId or datumIndex. sameFocusedPoint then rejects that point, so focus state can disappear after re-render. Match the same selection fallback in sameFocusedPoint, or make the re-render restore path guarantee a stable point the focus matcher accepts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/charts-core/src/focus-layer.ts` around lines 78 - 107, Update
sameFocusedPoint, used by matchesFocusPoint, to accept the same fallback
identity rules as restoreFocusedPoint: after reference, key, or datum matching,
compare chartValueEqual(xValue, yValue) together with markId and support
datumIndex matching. Ensure re-rendered points restored through either fallback
remain recognized by the focus matcher.
🧹 Nitpick comments (9)
examples/sandbox/src/styles.css (1)

222-225: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer a stable selector over > div:first-child.

These rules set the chart minimum height through the renderer's DOM shape. If the Canvas renderer adds or reorders a wrapper element, the rule stops applying and the cards collapse. Target a renderer-owned class such as .ts-chart-canvas__scene, which examples/sandbox/src/InteractionGeometryLab.test.ts line 39 already asserts.

Also applies to: 283-286

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/sandbox/src/styles.css` around lines 222 - 225, The stress-case
chart rule currently depends on the renderer’s first child DOM position; replace
both `> div:first-child` selectors in the stress chart min-height rules with the
stable renderer-owned `.ts-chart-canvas__scene` selector, preserving the
existing 320px minimum height.
examples/sandbox/src/InteractionGeometryLab.tsx (1)

535-556: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the duplicated probe state and marker.

ProofChart and GroupedProofChart repeat the same probe state, the same onRender callback, and the same marker markup. Extract one useProbe(proof) hook and one ProbeMarker component. This keeps the two cards in sync when the probe contract changes.

♻️ Proposed refactor
+function useProbe(proof: ProofCase) {
+  const [probe, setProbe] = React.useState<{ x: number; y: number } | null>(
+    null,
+  )
+  const onRender = React.useCallback(
+    (context: { scene: ChartScene<ProofDatum, number, number> }) => {
+      const next = proof.probe(context.scene)
+      if (!next) return
+      setProbe((current) =>
+        current && current.x === next.x && current.y === next.y
+          ? current
+          : next,
+      )
+    },
+    [proof],
+  )
+  return { probe, onRender }
+}
+
+function ProbeMarker({ id }: { id: string }) {
+  // render the positioned crosshair for a resolved probe
+}

Also applies to: 618-648

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/sandbox/src/InteractionGeometryLab.tsx` around lines 535 - 556,
Extract the duplicated probe logic from ProofChart and GroupedProofChart into a
shared useProbe(proof) hook containing probe state and the onRender callback,
plus a shared ProbeMarker component for the marker markup. Update both chart
components to use these shared symbols while preserving the existing probe
behavior and rendering.
examples/sandbox/src/InteractionGeometryLab.test.ts (2)

196-204: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Reuse the lab's own legacy resolver in the gallery test.

legacyNearest reimplements legacyPointFocus.resolve from examples/sandbox/src/InteractionGeometryLab.tsx lines 94-115. The test states that it locks each case's before-and-after outcome, but it never exercises proof.before. If legacyPointFocus changes, beforeExpected stays green while the rendered "before" card changes. Export legacyPointFocus from the lab module and call legacyPointFocus.resolve(scene.points, probe.x, probe.y, 48)[0] instead.

Also applies to: 317-333

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/sandbox/src/InteractionGeometryLab.test.ts` around lines 196 - 204,
Replace the duplicated legacyNearest resolver in InteractionGeometryLab.test.ts
with the lab module’s exported legacyPointFocus resolver. Export
legacyPointFocus from InteractionGeometryLab.tsx, then derive the before label
from legacyPointFocus.resolve(scene.points, probe.x, probe.y, 48)[0] in both
affected test sections while preserving the existing after assertion.

4-9: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Resolve charts-core through one specifier.

Import createChartScene and findNearestPoint from @tanstack/charts instead of ../../../packages/charts-core/src/scene). @tanstack/charts exports both helpers, and this module uses deep paths alongside the package specifiers without an alias. Import the helpers from @tanstack/charts/scene if the test needs that entry point to match the package export boundary.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/sandbox/src/InteractionGeometryLab.test.ts` around lines 4 - 9,
Update the imports in InteractionGeometryLab.test.ts to resolve createChartScene
and findNearestPoint through the `@tanstack/charts` package export, using
`@tanstack/charts/scene` only if that is the defined export boundary; remove the
relative packages/charts-core/src/scene import while preserving the existing
type and focus imports.
examples/sandbox/src/main.tsx (1)

4-14: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

The lab module runs its stress fixtures on every sandbox route. examples/sandbox/src/InteractionGeometryLab.tsx calls createProofCases() at module scope, and examples/sandbox/src/main.tsx imports that module unconditionally. The default dashboard route therefore builds 4,096 rectangles, 2,048 polygons, and a 1,024-vertex contour that it never renders, and it bundles the lab code.

  • examples/sandbox/src/main.tsx#L4-L14: replace the static import with a dynamic import('./InteractionGeometryLab') inside the showInteractionGeometryLab branch, or use React.lazy with a Suspense boundary.
  • examples/sandbox/src/InteractionGeometryLab.tsx#L271-L271: if the dynamic import is not adopted, convert proofCases into a memoized accessor so the fixtures build on first use instead of at module evaluation.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/sandbox/src/main.tsx` around lines 4 - 14, The default sandbox route
eagerly imports InteractionGeometryLab, causing its module-scope stress fixtures
to build and bundling lab code unnecessarily. In examples/sandbox/src/main.tsx
lines 4-14, replace the static InteractionGeometryLab import with a dynamic
import used only inside the showInteractionGeometryLab branch, and render the
loaded component through an appropriate loading boundary;
examples/sandbox/src/InteractionGeometryLab.tsx line 271 requires no direct
change when this dynamic-import fix is applied.
packages/charts-core/src/dom-types.ts (1)

41-44: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Preserve the ChartSurface type parameters.

ChartSurface.paintFocus returns ChartScene<any, any, any>, so a renderer can return a scene whose datum or axis-value types differ from the surface generics. Return ChartScene<TDatum, TXValue, TYValue> | void instead.

Proposed fix
-  ) => ChartScene<any, any, any> | void
+  ) => ChartScene<TDatum, TXValue, TYValue> | void
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/charts-core/src/dom-types.ts` around lines 41 - 44, Update the
paintFocus method type in ChartSurface to return ChartScene<TDatum, TXValue,
TYValue> | void instead of ChartScene<any, any, any> | void, preserving the
surface’s datum and axis-value generic parameters.
packages/charts-core/src/nearest.test.ts (1)

399-402: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Assert point identity, not structural equality.

toEqual compares interaction.point structurally. The assertion passes for a structurally identical clone.

This PR makes focus matching depend on stable point identity. Assert that the rendered node references the same object as scene.points[0].

♻️ Proposed stronger assertion
-  expect(rectNode.interaction).toEqual({
-    point,
-    affinity: expectedAffinity,
-  })
+  expect(rectNode.interaction?.point).toBe(point)
+  expect(rectNode.interaction?.affinity).toBe(expectedAffinity)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/charts-core/src/nearest.test.ts` around lines 399 - 402, Update the
assertion in the nearest-point interaction test to verify that interaction.point
is the identical object as scene.points[0], while continuing to validate the
expected affinity. Replace structural equality for the point with an identity
assertion so cloned but equivalent points do not pass.
packages/charts-core/src/nearest.ts (1)

478-559: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider moving the shared geometry helpers to one internal module.

nearest.ts and canvas.ts both define boundsForNode and boundsFromPoints with overlapping node shape handling. Centralizing these helpers ensures hit-boxes stay aligned with painted node bounds.

This is optional for this PR.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/charts-core/src/nearest.ts` around lines 478 - 559, Optionally
centralize the shared boundsForNode and boundsFromPoints geometry helpers
currently duplicated across nearest.ts and canvas.ts into one internal geometry
module, then update both callers to reuse them while preserving existing
node-shape handling and painted-versus-hit bounds alignment.
packages/charts-core/src/renderer.ts (1)

220-233: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Type paintFocus and surface the destination-scene layout contract.

ChartSurface.paintFocus returns ChartScene<any, any, any> | void, so the cast in renderer.ts is needed even though the implementations preserve the chart generics. Change the signature to a generic return type such as ChartScene<TDatum, TXValue, TYValue> | void. If implementations may return a focus-transition scene, document that it must preserve the rendered scene’s layout (width, height, margin, chart) used by clientToScene(scene, ...) and tooltip layout code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/charts-core/src/renderer.ts` around lines 220 - 233, Update
ChartSurface.paintFocus to return ChartScene<TDatum, TXValue, TYValue> | void,
preserving the chart generics and eliminating the any-based contract. Document
that any focus-transition scene must preserve the rendered scene layout fields
width, height, margin, and chart required by clientToScene and tooltip layout.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@benchmarks/bundle-size/README.md`:
- Around line 28-29: Update the bundle-size policy wording in the README to
identify the 18.6 KiB ceiling as applying to the “React compact-scale line
consumer” entry, matching the name used by the baseline; do not alter the
ceiling or unrelated bundle entries.

In `@docs/guides/bundle-size-and-performance.md`:
- Line 69: Update the locked compact React line consumer size limit in the
documented policy to state 18.6 KiB gzip instead of 18.6 kB gzip, matching the
unit used by scripts/measure-bundles.mjs and benchmarks/bundle-size/README.md.

In `@examples/sandbox/package.json`:
- Line 9: Update the dev:interaction-geometry script’s --open argument to remove
the surrounding single quotes, leaving the /?lab=interaction-geometry value
unchanged.

In `@packages/charts-core/docs/guides/bundle-size-and-performance.md`:
- Line 69: Revert the direct change in the generated documentation copy and
update the corresponding source content in the root
docs/guides/bundle-size-and-performance.md instead. Then run pnpm docs:sync to
regenerate the packages/charts-core copy, preserving the locked compact React
line consumer requirement.

In `@packages/charts-core/src/facet.test.ts`:
- Around line 455-457: Update both assertions in the facet tests that wrap
visibleBands x-attributes in a Set to check the Set’s size explicitly instead of
using toHaveLength; preserve the expected value of 1.

In `@packages/charts-core/src/facet.ts`:
- Around line 736-842: Update createFacetScene and the final facetScene result
to re-add a single default focus layer after merging the offset sub-scenes,
using the merged points and nodes returned by offsetScene. Preserve the existing
removal of per-cell default focus layers in withoutDefaultFocusLayers, and
ensure the resulting facet scene exposes the default focus layer for pointer
hover highlighting.

In `@packages/charts-core/src/nearest.ts`:
- Around line 420-438: Update squaredDistanceToPolyline to handle exactly one
point by returning its squared distance to (x, y), including for open polylines.
Preserve the existing Infinity result for empty input and segment-based behavior
for polylines with multiple points.

In `@scripts/measure-bundles.mjs`:
- Around line 747-755: Update the documented ceiling in
benchmarks/interaction/README.md from “2 kB gzip ceiling” to “2 KiB gzip
ceiling,” keeping it consistent with the 2 KiB budget configured by the budgeted
call for “Geometry pointer resolver kernel” and the generated documentation
copy.

---

Outside diff comments:
In `@packages/charts-core/src/focus-layer.ts`:
- Around line 78-107: Update sameFocusedPoint, used by matchesFocusPoint, to
accept the same fallback identity rules as restoreFocusedPoint: after reference,
key, or datum matching, compare chartValueEqual(xValue, yValue) together with
markId and support datumIndex matching. Ensure re-rendered points restored
through either fallback remain recognized by the focus matcher.

---

Nitpick comments:
In `@examples/sandbox/src/InteractionGeometryLab.test.ts`:
- Around line 196-204: Replace the duplicated legacyNearest resolver in
InteractionGeometryLab.test.ts with the lab module’s exported legacyPointFocus
resolver. Export legacyPointFocus from InteractionGeometryLab.tsx, then derive
the before label from legacyPointFocus.resolve(scene.points, probe.x, probe.y,
48)[0] in both affected test sections while preserving the existing after
assertion.
- Around line 4-9: Update the imports in InteractionGeometryLab.test.ts to
resolve createChartScene and findNearestPoint through the `@tanstack/charts`
package export, using `@tanstack/charts/scene` only if that is the defined export
boundary; remove the relative packages/charts-core/src/scene import while
preserving the existing type and focus imports.

In `@examples/sandbox/src/InteractionGeometryLab.tsx`:
- Around line 535-556: Extract the duplicated probe logic from ProofChart and
GroupedProofChart into a shared useProbe(proof) hook containing probe state and
the onRender callback, plus a shared ProbeMarker component for the marker
markup. Update both chart components to use these shared symbols while
preserving the existing probe behavior and rendering.

In `@examples/sandbox/src/main.tsx`:
- Around line 4-14: The default sandbox route eagerly imports
InteractionGeometryLab, causing its module-scope stress fixtures to build and
bundling lab code unnecessarily. In examples/sandbox/src/main.tsx lines 4-14,
replace the static InteractionGeometryLab import with a dynamic import used only
inside the showInteractionGeometryLab branch, and render the loaded component
through an appropriate loading boundary;
examples/sandbox/src/InteractionGeometryLab.tsx line 271 requires no direct
change when this dynamic-import fix is applied.

In `@examples/sandbox/src/styles.css`:
- Around line 222-225: The stress-case chart rule currently depends on the
renderer’s first child DOM position; replace both `> div:first-child` selectors
in the stress chart min-height rules with the stable renderer-owned
`.ts-chart-canvas__scene` selector, preserving the existing 320px minimum
height.

In `@packages/charts-core/src/dom-types.ts`:
- Around line 41-44: Update the paintFocus method type in ChartSurface to return
ChartScene<TDatum, TXValue, TYValue> | void instead of ChartScene<any, any, any>
| void, preserving the surface’s datum and axis-value generic parameters.

In `@packages/charts-core/src/nearest.test.ts`:
- Around line 399-402: Update the assertion in the nearest-point interaction
test to verify that interaction.point is the identical object as
scene.points[0], while continuing to validate the expected affinity. Replace
structural equality for the point with an identity assertion so cloned but
equivalent points do not pass.

In `@packages/charts-core/src/nearest.ts`:
- Around line 478-559: Optionally centralize the shared boundsForNode and
boundsFromPoints geometry helpers currently duplicated across nearest.ts and
canvas.ts into one internal geometry module, then update both callers to reuse
them while preserving existing node-shape handling and painted-versus-hit bounds
alignment.

In `@packages/charts-core/src/renderer.ts`:
- Around line 220-233: Update ChartSurface.paintFocus to return
ChartScene<TDatum, TXValue, TYValue> | void, preserving the chart generics and
eliminating the any-based contract. Document that any focus-transition scene
must preserve the rendered scene layout fields width, height, margin, and chart
required by clientToScene and tooltip layout.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: dad5e3a6-21cb-4922-8177-62ddf1f25b17

📥 Commits

Reviewing files that changed from the base of the PR and between e08e12c and 2e1f7ee.

📒 Files selected for processing (67)
  • .changeset/bright-hit-regions.md
  • API-FRICTION.md
  • PLAN.md
  • README.md
  • benchmarks/bundle-size/README.md
  • benchmarks/bundle-size/universal-baseline.json
  • benchmarks/conformance/cases/scatter-bubble/model.test.ts
  • benchmarks/conformance/cases/scatter-bubble/model.ts
  • benchmarks/conformance/cases/scatter-bubble/plot.ts
  • benchmarks/conformance/cases/scatter-bubble/tanstack.ts
  • benchmarks/entries/charts-pointer-anchor-kernel.ts
  • benchmarks/entries/charts-pointer-geometry-kernel.ts
  • benchmarks/interaction/README.md
  • benchmarks/interaction/nearest.ts
  • docs/guides/bundle-size-and-performance.md
  • docs/guides/custom-marks-and-renderers.md
  • docs/guides/tooltips-and-focus.md
  • docs/reference/custom-extensions.md
  • docs/reference/focus-and-interaction.md
  • docs/reference/rendering-and-export.md
  • docs/reference/runtime-and-scene.md
  • docs/reference/types.md
  • examples/sandbox/README.md
  • examples/sandbox/package.json
  • examples/sandbox/src/InteractionGeometryLab.test.ts
  • examples/sandbox/src/InteractionGeometryLab.tsx
  • examples/sandbox/src/main.tsx
  • examples/sandbox/src/styles.css
  • package.json
  • packages/charts-core/docs/guides/bundle-size-and-performance.md
  • packages/charts-core/docs/guides/custom-marks-and-renderers.md
  • packages/charts-core/docs/guides/tooltips-and-focus.md
  • packages/charts-core/docs/reference/custom-extensions.md
  • packages/charts-core/docs/reference/focus-and-interaction.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/src/area-x.ts
  • packages/charts-core/src/area.ts
  • packages/charts-core/src/band.ts
  • packages/charts-core/src/bar.ts
  • packages/charts-core/src/canvas.ts
  • packages/charts-core/src/dom-types.ts
  • packages/charts-core/src/dot.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/hexagon.ts
  • packages/charts-core/src/index.ts
  • packages/charts-core/src/line.ts
  • packages/charts-core/src/mark-state.ts
  • packages/charts-core/src/marks.test.ts
  • packages/charts-core/src/nearest.test.ts
  • packages/charts-core/src/nearest.ts
  • packages/charts-core/src/rect.ts
  • packages/charts-core/src/renderer.test.ts
  • packages/charts-core/src/renderer.ts
  • packages/charts-core/src/scene.test.ts
  • packages/charts-core/src/scene.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/react-native-charts/src/interaction.ts
  • scripts/measure-bundles.mjs
  • scripts/measure-pointer-resolution.mjs

Comment thread benchmarks/bundle-size/README.md Outdated
Comment thread docs/guides/bundle-size-and-performance.md Outdated
Comment thread examples/sandbox/package.json Outdated
Comment thread packages/charts-core/docs/guides/bundle-size-and-performance.md Outdated
Comment thread packages/charts-core/src/facet.test.ts Outdated
Comment thread packages/charts-core/src/facet.ts
Comment thread packages/charts-core/src/nearest.ts
Comment thread scripts/measure-bundles.mjs
@tannerlinsley
tannerlinsley merged commit f08fbd7 into TanStack:main Aug 2, 2026
3 checks passed
@github-actions github-actions Bot mentioned this pull request Aug 2, 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.

2 participants