Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-radial-focus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/charts': minor
---

Add angular grouped focus for radial charts and geometry-backed arc tooltips.
53 changes: 52 additions & 1 deletion API-FRICTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ observed difficulty from examples, production migrations, tests, and agent
evaluations so later API, documentation, and TanStack Intent skill work is
based on evidence.

Last updated: 2026-08-11
Last updated: 2026-08-12

## Triage rule

Expand Down Expand Up @@ -307,6 +307,8 @@ Each entry records:
| F-268 | Animated arc flags became invalid fractional path values | API | resolved |
| F-269 | Angular mounted its browser host during server rendering | API | resolved |
| F-270 | Catalog migration left generated release evidence stale | Tooling | resolved |
| F-271 | Radial focus collapsed angular cross-sections to centroids | API | resolved |
| F-272 | Pointer probes armed between transient inactive frames | Tooling | resolved |

## Findings

Expand Down Expand Up @@ -7954,3 +7956,52 @@ Each entry records:
- Verification: the focused roadmap test and catalog preview check pass. The
only rendered asset change normalizes synchronized-cursor point keys from
timestamp labels to the migrated ISO date labels; geometry is unchanged.

### F-271 — Radial focus collapsed angular cross-sections to centroids

- Status: resolved
- Severity: high
- Owner: API
- Observed in: radial tooltip interaction review
- Friction: polar lines, areas, and dots exposed only Cartesian centroid
anchors to the default nearest-point resolver. Moving around one semantic
angle could select a different radius or adjacent angle, and grouped radial
tooltips had no equivalent of `group-x`. `radialArc` also emitted a centroid
without attaching its already available painted boundary, so pie and donut
focus could disagree with the visible slice.
- Decision: export `focusGroupAngle` from the exact polar subpath. It selects
the nearest bounded radial ray, uses radius distance as the primary-point
tie-breaker, groups one point per series at the same semantic angle, and
orders keyboard tasks angularly. Keep the strategy out of ordinary and
universal barrels. Attach the existing D3-replayed interaction boundary to
every `radialArc`, including authored generators.
- Verification: focused polar tests resolve two series from a one-pixel
angular ray while both anchors are farther away, preserve the closest radius
as primary, and reduce keyboard navigation to one task per semantic angle.
Arc tests hit the painted annulus at zero fallback distance, reject its hole,
and reuse the exact point attached to the scene geometry. Polar, radial-bar,
sunburst, callable-surface, documentation, and root TypeScript checks pass.
The measured polar arc is 15.06 KiB gzip, gauge 24.08, radial labels 20.66,
radial bars 24.17, and polar line/scatter 25.28; their isolated ceilings now
record those reviewed interaction costs while the locked representative-mark
bundle remains unchanged at 25.59 KiB gzip.

### F-272 — Pointer probes armed between transient inactive frames

- Status: resolved
- Severity: high
- Owner: Tooling
- Observed in: radial-focus release PR stress partition 3
- Friction: the Chart.js grouped-pointer cell moved outside the chart and
accepted its first inactive frame as settled. Its activation timer then
observed the tooltip active again and failed before measuring the target.
The exact cell passed locally, confirming a scheduling race rather than a
radial-focus regression, but the correctness failure was intentionally not
retryable.
- Decision: require two consecutive inactive animation frames before arming
pointer activation timing. Keep renderer and correctness failures
non-retryable; this stabilizes the measured precondition instead of hiding a
failed sample with another attempt.
- Verification: the exact Chart.js grouped-pointer cell and the complete quick
partition 3 pass with trusted activation, exact grouped series values, and
zero recovered retries.
8 changes: 4 additions & 4 deletions benchmarks/comparison/bundle-baseline.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"schemaVersion": 4,
"generatedAt": "2026-08-11T23:49:36.793Z",
"generatedAt": "2026-08-12T23:00:29.524Z",
"packageVersions": {
"tanstack": "0.11.0",
"tanstack": "0.11.2",
"chartjs": "4.5.1",
"echarts": "6.1.0",
"recharts": "3.10.1",
Expand All @@ -11,8 +11,8 @@
"sources": {
"tanstack": {
"kind": "workspace",
"revision": "02e24867f1699fa918fd49eaedfad64bc3f4e9e5",
"inputDigest": "sha256:790fc8c0e954afbba943d637d43b665739d2438d2cc6c341e81613baad327508"
"revision": "b9d8c541ef6a94afdc1812555ee0e4459c81d140",
"inputDigest": "sha256:52b55a904317f891ec664992193770fd9468bddddeef1e8f9fe10ec113051b05"
},
"chartjs": {
"kind": "package",
Expand Down
3 changes: 2 additions & 1 deletion benchmarks/comparison/stress/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,8 @@ and streaming inputs are source-accounted independently.
Multi-series output gates exact ordered series identity, stable color
ownership, explicit x-domain changes, and per-series vertex counts. Trusted
pointer checks validate the exact focused x and every series value on the
initial chart and again after reorder, append, and visibility updates.
initial chart and again after reorder, append, and visibility updates. Pointer
activation timing starts only after two consecutive inactive animation frames.

Cross-library timing rank is informational. Browser timing, long tasks, and
retained-memory deltas are meaningful only within the same run, machine, and
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/conformance/previews/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"schemaVersion": 1,
"width": 288,
"height": 192,
"sourceHash": "e51640f5fc66d37a982722a282685167d084940132c429f7a2515f16f28714f3",
"sourceHash": "93452895c364c1dd0cc6b824137d35b8c43e0a022dca544e38dccea52b94fb88",
"assets": [
{
"id": "01-line-gaps",
Expand Down
6 changes: 3 additions & 3 deletions docs/comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ turning untested behavior into a checkmark.

| Library | Package | Measured source |
| -------------------------------------------------------------------------------------- | -------------------- | ------------------- |
| [TanStack Charts](./overview.md) | `@tanstack/charts` | workspace `02e2486` |
| [TanStack Charts](./overview.md) | `@tanstack/charts` | workspace `b9d8c54` |
| [Chart.js](https://www.chartjs.org/docs/latest/) | `chart.js` | npm `4.5.1` |
| [Apache ECharts](https://echarts.apache.org/handbook/en/best-practices/canvas-vs-svg/) | `echarts` | npm `6.1.0` |
| [Recharts](https://recharts.github.io/en-US/) | `recharts` | npm `3.10.1` |
| [Observable Plot](https://observablehq.com/plot/features/plots) | `@observablehq/plot` | npm `0.6.17` |

The competitor versions are exact package pins, not latest versions inferred
at page render time. The measured TanStack workspace revision is `02e2486`.
at page render time. The measured TanStack workspace revision is `b9d8c54`.

## Capability matrix

Expand Down Expand Up @@ -90,7 +90,7 @@ output model.

## Bundle snapshot

Baseline date: `2026-08-11`.
Baseline date: `2026-08-12`.

Controlled ranges cover 12 independently built, minified browser consumers:
line, bar, area, and scatter at basic, interactive, and advanced tiers. Only
Expand Down
23 changes: 23 additions & 0 deletions docs/guides/tooltips-and-focus.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,29 @@ category. A sparse snapped cursor can opt into
`maxFocusDistance: Number.POSITIVE_INFINITY`; keep the finite default when
empty space should mean no focus.

## Angular focus

Use `focusGroupAngle` for the radial equivalent of `group-x`:

```ts
import { defineChart, type ChartDefinition } from '@tanstack/charts'
import { focusGroupAngle } from '@tanstack/charts/polar'
import { tooltip } from '@tanstack/charts/tooltip'

declare const radialDefinition: ChartDefinition

const interactiveDefinition = defineChart(radialDefinition, {
focus: focusGroupAngle,
tooltip,
})
```

The nearest radial ray selects the semantic angle, the closest radius becomes
primary, and the tooltip receives one point per series at that angle. The
strategy uses the same finite `maxFocusDistance` policy as axis grouping.
Ordinary pie and donut charts can keep default nearest focus: `radialArc`
attaches the exact painted slice geometry, including the donut hole.

Default `primary` and `group` presentation follows the canonical focused scene
points. Equal x/y/series values in another facet do not implicitly paint a
second focus marker. To synchronize a visual cursor across facets without
Expand Down
6 changes: 6 additions & 0 deletions docs/reference/focus-and-interaction.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@ or direct strategy use. The exact exported objects receive the same host-level
containment behavior as their presets. A strategy that wraps or copies one of
them is custom and owns its complete pointer resolution.

`focusGroupAngle` is available from `@tanstack/charts/polar`. It resolves the
nearest radial ray, groups points with the same semantic angle value, and
orders keyboard tasks by angle. Use it for grouped radar, polar-line, and
radial-dot tooltips. Painted `radialArc` geometry already participates in
default nearest focus.

## Crosshair guides

`crosshair` is a data-less presentation mark. It follows the chart's resolved
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ aggregate `/scales` export.
| `@tanstack/charts/motion` | `motion`, `ChartMotionOptions`, and renderer-neutral motion types |
| `@tanstack/charts/network/force` | `forceLayout`, built-in descriptors, named D3-compatible force factories, private working-clone context, settled node/link result, and lineage types |
| `@tanstack/charts/network/sankey` | `sankeyDiagram`, shorthand and callable alignment types, responsive layout options, immutable node/link values, comparator contexts, and lineage types |
| `@tanstack/charts/polar` | `pie`, `polar`, `radialArc`, `radialBarRadius`, `radialBarAngle`, other radial marks, and radial/angle guides |
| `@tanstack/charts/polar` | `pie`, `polar`, `focusGroupAngle`, `radialArc`, `radialBarRadius`, `radialBarAngle`, other radial marks, and radial/angle guides |
| `@tanstack/charts/universal` | Common root authoring, runtime, scene, and static SVG values without browser hosts or adapters |
| `@tanstack/charts/reconcile` | `reconcileChartSvg`, `reconcileChartSvgFragment` |
| `@tanstack/charts/rect` | `rect`, `cell` |
Expand Down
28 changes: 28 additions & 0 deletions docs/reference/marks/polar.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Polar marks are available only from the capability subpath:
```ts
import {
angleGrid,
focusGroupAngle,
pie,
polar,
radialArc,
Expand Down Expand Up @@ -82,6 +83,28 @@ resize.
The outer chart omits `x` and `y`. Cartesian axes do not participate in the
internal polar scales.

## `focusGroupAngle`

```ts
import { defineChart, type ChartDefinition } from '@tanstack/charts'
import { focusGroupAngle } from '@tanstack/charts/polar'
import { tooltip } from '@tanstack/charts/tooltip'

declare const definition: ChartDefinition

const interactiveDefinition = defineChart(definition, {
focus: focusGroupAngle,
tooltip,
})
```

`focusGroupAngle` is the polar equivalent of `group-x`. Pointer resolution
uses the nearest radial ray instead of the nearest point anchor, then returns
one point per series with the same semantic angle value. The closest radius is
primary. Keyboard navigation visits one representative per angle in angular
order. `maxFocusDistance` is the scene-pixel distance from the pointer to the
ray; set it to `Number.POSITIVE_INFINITY` for continuous angular snapping.

## `pie`

```ts
Expand Down Expand Up @@ -154,6 +177,11 @@ function radialArc<TDatum>(
| `strokeDasharray` | Boundary dash array |
| `opacity` | Whole-arc opacity |

Each arc attaches its sampled painted boundary to its interaction point.
Default nearest focus therefore follows the visible slice, including holes,
rounded corners, reversed sweeps, and custom D3 generators, instead of using
only the centroid anchor.

Use the native `pie` transform for flat typed rows with source lineage. D3
`pie` output remains valid interoperability input because its `startAngle`,
`endAngle`, and `padAngle` fields are also the channels this mark needs. A pie,
Expand Down
6 changes: 3 additions & 3 deletions packages/charts-core/docs/comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ turning untested behavior into a checkmark.

| Library | Package | Measured source |
| -------------------------------------------------------------------------------------- | -------------------- | ------------------- |
| [TanStack Charts](./overview.md) | `@tanstack/charts` | workspace `02e2486` |
| [TanStack Charts](./overview.md) | `@tanstack/charts` | workspace `b9d8c54` |
| [Chart.js](https://www.chartjs.org/docs/latest/) | `chart.js` | npm `4.5.1` |
| [Apache ECharts](https://echarts.apache.org/handbook/en/best-practices/canvas-vs-svg/) | `echarts` | npm `6.1.0` |
| [Recharts](https://recharts.github.io/en-US/) | `recharts` | npm `3.10.1` |
| [Observable Plot](https://observablehq.com/plot/features/plots) | `@observablehq/plot` | npm `0.6.17` |

The competitor versions are exact package pins, not latest versions inferred
at page render time. The measured TanStack workspace revision is `02e2486`.
at page render time. The measured TanStack workspace revision is `b9d8c54`.

## Capability matrix

Expand Down Expand Up @@ -90,7 +90,7 @@ output model.

## Bundle snapshot

Baseline date: `2026-08-11`.
Baseline date: `2026-08-12`.

Controlled ranges cover 12 independently built, minified browser consumers:
line, bar, area, and scatter at basic, interactive, and advanced tiers. Only
Expand Down
23 changes: 23 additions & 0 deletions packages/charts-core/docs/guides/tooltips-and-focus.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,29 @@ category. A sparse snapped cursor can opt into
`maxFocusDistance: Number.POSITIVE_INFINITY`; keep the finite default when
empty space should mean no focus.

## Angular focus

Use `focusGroupAngle` for the radial equivalent of `group-x`:

```ts
import { defineChart, type ChartDefinition } from '@tanstack/charts'
import { focusGroupAngle } from '@tanstack/charts/polar'
import { tooltip } from '@tanstack/charts/tooltip'

declare const radialDefinition: ChartDefinition

const interactiveDefinition = defineChart(radialDefinition, {
focus: focusGroupAngle,
tooltip,
})
```

The nearest radial ray selects the semantic angle, the closest radius becomes
primary, and the tooltip receives one point per series at that angle. The
strategy uses the same finite `maxFocusDistance` policy as axis grouping.
Ordinary pie and donut charts can keep default nearest focus: `radialArc`
attaches the exact painted slice geometry, including the donut hole.

Default `primary` and `group` presentation follows the canonical focused scene
points. Equal x/y/series values in another facet do not implicitly paint a
second focus marker. To synchronize a visual cursor across facets without
Expand Down
6 changes: 6 additions & 0 deletions packages/charts-core/docs/reference/focus-and-interaction.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@ or direct strategy use. The exact exported objects receive the same host-level
containment behavior as their presets. A strategy that wraps or copies one of
them is custom and owns its complete pointer resolution.

`focusGroupAngle` is available from `@tanstack/charts/polar`. It resolves the
nearest radial ray, groups points with the same semantic angle value, and
orders keyboard tasks by angle. Use it for grouped radar, polar-line, and
radial-dot tooltips. Painted `radialArc` geometry already participates in
default nearest focus.

## Crosshair guides

`crosshair` is a data-less presentation mark. It follows the chart's resolved
Expand Down
2 changes: 1 addition & 1 deletion packages/charts-core/docs/reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ aggregate `/scales` export.
| `@tanstack/charts/motion` | `motion`, `ChartMotionOptions`, and renderer-neutral motion types |
| `@tanstack/charts/network/force` | `forceLayout`, built-in descriptors, named D3-compatible force factories, private working-clone context, settled node/link result, and lineage types |
| `@tanstack/charts/network/sankey` | `sankeyDiagram`, shorthand and callable alignment types, responsive layout options, immutable node/link values, comparator contexts, and lineage types |
| `@tanstack/charts/polar` | `pie`, `polar`, `radialArc`, `radialBarRadius`, `radialBarAngle`, other radial marks, and radial/angle guides |
| `@tanstack/charts/polar` | `pie`, `polar`, `focusGroupAngle`, `radialArc`, `radialBarRadius`, `radialBarAngle`, other radial marks, and radial/angle guides |
| `@tanstack/charts/universal` | Common root authoring, runtime, scene, and static SVG values without browser hosts or adapters |
| `@tanstack/charts/reconcile` | `reconcileChartSvg`, `reconcileChartSvgFragment` |
| `@tanstack/charts/rect` | `rect`, `cell` |
Expand Down
Loading