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/sunburst-path-projection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/charts': patch
---

Avoid rebuilding sunburst hit-test boundaries when projecting animation frames.
6 changes: 3 additions & 3 deletions benchmarks/comparison/bundle-baseline.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"schemaVersion": 4,
"generatedAt": "2026-10-13T23:35:26.997Z",
"generatedAt": "2026-10-14T02:03:21.704Z",
"packageVersions": {
"tanstack": "1.1.0",
"chartjs": "4.5.1",
Expand All @@ -11,8 +11,8 @@
"sources": {
"tanstack": {
"kind": "workspace",
"revision": "a691036b07655adda97aa0dc3052428989e2b811",
"inputDigest": "sha256:390854b69e6ac7c4453ba8bcc75dfd5240f70bbd9f69efdc8d25c7fba528d951"
"revision": "cd2a8a7fcab201ca32db60ca6d6c8e92c4dd47b8",
"inputDigest": "sha256:6c5dc96470c53874d724ca231e9e5a552800d04834e584260cc2eac4b504974e"
},
"chartjs": {
"kind": "package",
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": "82eecd6c1fbdca5e857b965e09bb53c550520b2e596fbc4e9850266f03fdb9b4",
"sourceHash": "9bbb0210cda37cf492ff11d793f0e23d493f9aebc849609518fa4abc1ce7a26a",
"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 `a691036` |
| [TanStack Charts](./overview.md) | `@tanstack/charts` | workspace `cd2a8a7` |
| [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 `a691036`.
at page render time. The measured TanStack workspace revision is `cd2a8a7`.

## Capability matrix

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

## Bundle snapshot

Baseline date: `2026-10-13`.
Baseline date: `2026-10-14`.

Controlled ranges cover 12 independently built, minified browser consumers:
line, bar, area, and scatter at basic, interactive, and advanced tiers. Only
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 `a691036` |
| [TanStack Charts](./overview.md) | `@tanstack/charts` | workspace `cd2a8a7` |
| [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 `a691036`.
at page render time. The measured TanStack workspace revision is `cd2a8a7`.

## Capability matrix

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

## Bundle snapshot

Baseline date: `2026-10-13`.
Baseline date: `2026-10-14`.

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: 7 additions & 16 deletions packages/charts-core/src/hierarchy-sunburst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from './hierarchy-flat-internal'
import { channelValues, isChartKey, isFiniteNumber, visualValue } from './mark'
import { createPolarMark } from './polar-mark-internal'
import { resolvePolarSector } from './polar-sector-internal'
import { projectPolarSector, resolvePolarSector } from './polar-sector-internal'
import {
sceneMotionNode,
type SceneMotionMetadata,
Expand Down Expand Up @@ -340,22 +340,13 @@ export function sunburst<TDatum>(
}

function projectSunburstSectorPath(values: readonly number[]) {
const [startAngle, endAngle, innerRadius, outerRadius] = values
if (
startAngle === undefined ||
endAngle === undefined ||
innerRadius === undefined ||
outerRadius === undefined
) {
return undefined
}
return resolvePolarSector({
startAngle,
endAngle,
innerRadius,
outerRadius,
return projectPolarSector({
startAngle: values[0]!,
endAngle: values[1]!,
innerRadius: values[2]!,
outerRadius: values[3]!,
cornerRadius: 0,
})?.path
})
}

function sunburstNodeContext<TDatum>(
Expand Down
56 changes: 56 additions & 0 deletions packages/charts-core/src/polar-sector-internal.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { describe, expect, it, vi } from 'vitest'
import { projectPolarSector, resolvePolarSector } from './polar-sector-internal'

describe('polar sector projection', () => {
it.each([
[0, 2, 20, 60, 0],
[2, -3, 80, 40, 8],
[0, Math.PI * 2, 0, 80, 6],
[0, 0.012, 60, 80, 6],
])(
'keeps the rendered path without sampling its boundary: %j',
(...values) => {
const [startAngle, endAngle, innerRadius, outerRadius, cornerRadius] =
values
const sector = {
startAngle: startAngle!,
endAngle: endAngle!,
innerRadius: innerRadius!,
outerRadius: outerRadius!,
cornerRadius: cornerRadius!,
}
const cosine = vi.spyOn(Math, 'cos')
try {
const projected = projectPolarSector(sector)
const projectionCalls = cosine.mock.calls.length
cosine.mockClear()
const resolved = resolvePolarSector(sector)
expect(resolved?.path).toBe(projected)
expect(resolved!.points.length).toBeGreaterThan(0)
expect(cosine.mock.calls.length).toBeGreaterThan(projectionCalls)
expect(projectPolarSector(sector)).toBe(projected)
} finally {
cosine.mockRestore()
}
},
)

it.each([
'startAngle',
'endAngle',
'innerRadius',
'outerRadius',
'cornerRadius',
] as const)('still rejects nonfinite %s', (field) => {
const sector = {
startAngle: 0,
endAngle: 2,
innerRadius: 20,
outerRadius: 60,
cornerRadius: 0,
[field]: Number.NaN,
}
expect(projectPolarSector(sector)).toBeUndefined()
expect(resolvePolarSector(sector)).toBeUndefined()
})
})
28 changes: 15 additions & 13 deletions packages/charts-core/src/polar-sector-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,7 @@ export interface ResolvedPolarSector {

let polarSectorArc: Arc<any, PolarSectorGeometry> | undefined

/** Resolves one D3 sector and samples its painted interaction boundary. */
export function resolvePolarSector(
sector: PolarSectorGeometry,
): ResolvedPolarSector | undefined {
export function projectPolarSector(sector: PolarSectorGeometry) {
if (
!isFiniteNumber(sector.startAngle) ||
!isFiniteNumber(sector.endAngle) ||
Expand All @@ -32,17 +29,22 @@ export function resolvePolarSector(
) {
return undefined
}
const generator = (polarSectorArc ??= createArc<PolarSectorGeometry>()
.startAngle((value) => value.startAngle)
.endAngle((value) => value.endAngle)
.innerRadius((value) => value.innerRadius)
.outerRadius((value) => value.outerRadius)
.cornerRadius((value) => value.cornerRadius))
const path = generator(sector)
if (typeof path !== 'string' || !path) return undefined
const generator = (polarSectorArc ??=
createArc<PolarSectorGeometry>().cornerRadius(
(value) => value.cornerRadius,
))
return generator(sector) || undefined
}

/** Resolves one D3 sector and samples its painted interaction boundary. */
export function resolvePolarSector(
sector: PolarSectorGeometry,
): ResolvedPolarSector | undefined {
const path = projectPolarSector(sector)
if (!path) return undefined
return {
path,
points: tracePolarArcBoundary(generator, sector, 0, [sector]),
points: tracePolarArcBoundary(polarSectorArc!, sector, 0, [sector]),
}
}

Expand Down