Add geometry-aware point selection - #29
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (17)
🚧 Files skipped from review as they are similar to previous changes (12)
📝 WalkthroughWalkthroughThis 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. ChangesScene-based interaction geometry
Measurement and supporting updates
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
8322499 to
054d23b
Compare
There was a problem hiding this comment.
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 winAlign default focus matching with
restoreFocusedPoint’s stability rules.
restoreFocusedPointcan return a point whosekeydiffers when it falls back tochartValueEqual(xValue, yValue) + markIdordatumIndex.sameFocusedPointthen rejects that point, so focus state can disappear after re-render. Match the same selection fallback insameFocusedPoint, 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 valuePrefer 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, whichexamples/sandbox/src/InteractionGeometryLab.test.tsline 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 valueExtract the duplicated probe state and marker.
ProofChartandGroupedProofChartrepeat the sameprobestate, the sameonRendercallback, and the same marker markup. Extract oneuseProbe(proof)hook and oneProbeMarkercomponent. 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 winReuse the lab's own legacy resolver in the gallery test.
legacyNearestreimplementslegacyPointFocus.resolvefromexamples/sandbox/src/InteractionGeometryLab.tsxlines 94-115. The test states that it locks each case's before-and-after outcome, but it never exercisesproof.before. IflegacyPointFocuschanges,beforeExpectedstays green while the rendered "before" card changes. ExportlegacyPointFocusfrom the lab module and calllegacyPointFocus.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 winResolve charts-core through one specifier.
Import
createChartSceneandfindNearestPointfrom@tanstack/chartsinstead of../../../packages/charts-core/src/scene).@tanstack/chartsexports both helpers, and this module uses deep paths alongside the package specifiers without an alias. Import the helpers from@tanstack/charts/sceneif 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 winThe lab module runs its stress fixtures on every sandbox route.
examples/sandbox/src/InteractionGeometryLab.tsxcallscreateProofCases()at module scope, andexamples/sandbox/src/main.tsximports 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 dynamicimport('./InteractionGeometryLab')inside theshowInteractionGeometryLabbranch, or useReact.lazywith aSuspenseboundary.examples/sandbox/src/InteractionGeometryLab.tsx#L271-L271: if the dynamic import is not adopted, convertproofCasesinto 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 winPreserve the
ChartSurfacetype parameters.
ChartSurface.paintFocusreturnsChartScene<any, any, any>, so a renderer can return a scene whose datum or axis-value types differ from the surface generics. ReturnChartScene<TDatum, TXValue, TYValue> | voidinstead.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 valueAssert point identity, not structural equality.
toEqualcomparesinteraction.pointstructurally. 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 valueConsider moving the shared geometry helpers to one internal module.
nearest.tsandcanvas.tsboth defineboundsForNodeandboundsFromPointswith 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 winType
paintFocusand surface the destination-scene layout contract.
ChartSurface.paintFocusreturnsChartScene<any, any, any> | void, so the cast inrenderer.tsis needed even though the implementations preserve the chart generics. Change the signature to a generic return type such asChartScene<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 byclientToScene(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
📒 Files selected for processing (67)
.changeset/bright-hit-regions.mdAPI-FRICTION.mdPLAN.mdREADME.mdbenchmarks/bundle-size/README.mdbenchmarks/bundle-size/universal-baseline.jsonbenchmarks/conformance/cases/scatter-bubble/model.test.tsbenchmarks/conformance/cases/scatter-bubble/model.tsbenchmarks/conformance/cases/scatter-bubble/plot.tsbenchmarks/conformance/cases/scatter-bubble/tanstack.tsbenchmarks/entries/charts-pointer-anchor-kernel.tsbenchmarks/entries/charts-pointer-geometry-kernel.tsbenchmarks/interaction/README.mdbenchmarks/interaction/nearest.tsdocs/guides/bundle-size-and-performance.mddocs/guides/custom-marks-and-renderers.mddocs/guides/tooltips-and-focus.mddocs/reference/custom-extensions.mddocs/reference/focus-and-interaction.mddocs/reference/rendering-and-export.mddocs/reference/runtime-and-scene.mddocs/reference/types.mdexamples/sandbox/README.mdexamples/sandbox/package.jsonexamples/sandbox/src/InteractionGeometryLab.test.tsexamples/sandbox/src/InteractionGeometryLab.tsxexamples/sandbox/src/main.tsxexamples/sandbox/src/styles.csspackage.jsonpackages/charts-core/docs/guides/bundle-size-and-performance.mdpackages/charts-core/docs/guides/custom-marks-and-renderers.mdpackages/charts-core/docs/guides/tooltips-and-focus.mdpackages/charts-core/docs/reference/custom-extensions.mdpackages/charts-core/docs/reference/focus-and-interaction.mdpackages/charts-core/docs/reference/rendering-and-export.mdpackages/charts-core/docs/reference/runtime-and-scene.mdpackages/charts-core/docs/reference/types.mdpackages/charts-core/src/area-x.tspackages/charts-core/src/area.tspackages/charts-core/src/band.tspackages/charts-core/src/bar.tspackages/charts-core/src/canvas.tspackages/charts-core/src/dom-types.tspackages/charts-core/src/dot.tspackages/charts-core/src/facet.test.tspackages/charts-core/src/facet.tspackages/charts-core/src/focus-layer.tspackages/charts-core/src/focus-mark.test.tspackages/charts-core/src/hexagon.tspackages/charts-core/src/index.tspackages/charts-core/src/line.tspackages/charts-core/src/mark-state.tspackages/charts-core/src/marks.test.tspackages/charts-core/src/nearest.test.tspackages/charts-core/src/nearest.tspackages/charts-core/src/rect.tspackages/charts-core/src/renderer.test.tspackages/charts-core/src/renderer.tspackages/charts-core/src/scene.test.tspackages/charts-core/src/scene.tspackages/charts-core/src/svg-surface.tspackages/charts-core/src/type-contract.test.tspackages/charts-core/src/types.tspackages/charts-core/src/universal-types.tspackages/react-native-charts/src/interaction.tsscripts/measure-bundles.mjsscripts/measure-pointer-resolution.mjs
Problem
The production resolver measures
maxFocusDistancefrom 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
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:
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.
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 perpointer query. The command rotates implementation order, exposes GC, and first
asserts that the POC and optimized implementation return the same target.
† Production is a speed baseline on geometry rows, not a correctness
baseline; it cannot return the intended geometry behavior.
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.
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
Integrated retained-input deltas:
pnpm bundle:checkis intentionally red and no lock was updated. It reportsthe 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
and the 0.6.17 pointer implementation
and 3.0.1 implementation
and 6.0.4 implementation
and bounds-first Canvas picker
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 passedpnpm test— 678 passed across 123 filespnpm typecheck— passedpnpm docs:check— passed (82 pages, 84 catalog embeds, 17 executable examples)pnpm package:check— passed (packed consumers and 7 adapters)pnpm format:check— passedpnpm bundle:check— expected failure; measurements retained above, lock unchangedQuestions for review
focusAffinityplus point-levelhitRegionthe rightownership boundary?
correctness behavior?
profile-driven follow-up?
invariants and benchmark harness?
Summary by CodeRabbit
New Features
Bug Fixes
Documentation