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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
8 changes: 8 additions & 0 deletions .changeset/named-scales-canvas-layers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@tanstack/charts': minor
---

Add named Cartesian and polar scale registries with multiple axes, guided
legacy scale migration, and opt-in per-mark Canvas rendering that composes with
the default renderer across DOM framework adapters, SSR, focus, and image
export.
57 changes: 56 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-15
Last updated: 2026-08-26

## Triage rule

Expand Down Expand Up @@ -329,6 +329,8 @@ Each entry records:
| F-290 | Public examples imported a private workspace package | Tooling | resolved |
| F-291 | Renderer capability injection depended on module identity | API/Tooling | resolved |
| F-292 | Fixed preview paints ignored the selected site theme | Tooling | resolved |
| F-293 | Root scale slots blocked named axes | API | resolved |
| F-294 | Automatic mark renderers imposed shared host plumbing | API | resolved |

## Findings

Expand Down Expand Up @@ -8429,3 +8431,56 @@ Each entry records:
- Follow-up verification: the generator contract covers all four tokens in both
palettes, and regenerated ShadCN donut, radial, radar, and authored-label
previews render readable dark text and theme-matched separators.

### F-293 - Root scale slots blocked named axes

- Status: resolved
- Severity: high
- Owner: API
- Observed in: auditing the claimed TanStack Charts gaps against ECharts,
Observable Plot, and the existing React chart implementation
- Friction: one root `x` slot and one root `y` slot could not describe multiple
unit-specific axes or bind different marks to independent scales. Polar marks
repeated the same limitation with one `angle` and one `radius` slot. Adding a
registry alone was not enough because composite marks could hide positional
identity, named callbacks could not see the resolved polar registry, and a
misspelled binding otherwise failed later with an undefined scale.
- Decision: make `scales` the canonical Cartesian and polar registry, reserve
`x`, `y`, `angle`, and `radius` as readable defaults, and let marks bind to
named entries. Each named scale declares its positional channel and guide
side. Axes support all four sides and stack when they share one side. Keep the
old root options only as temporary compatibility, warn once with exact
migration instructions, and remove the warning text from production bundles.
Preserve positional identity through composite channel namespacing and expose
the public resolved polar registry to length callbacks.
- Verification: named scale, axis, grid, focus, facet, motion, composite, box,
regression, polar, and type-contract regressions pass. All authored examples,
benchmarks, fixtures, adapters, and docs use the canonical registry, while
focused compatibility tests are the only intentional legacy syntax. The full
1,920-test suite, workspace typecheck, documentation contract, and packed
package gate pass.

### F-294 - Automatic mark renderers imposed shared host plumbing

- Status: resolved
- Severity: high
- Owner: API
- Observed in: adding Canvas rendering for dense marks while retaining SVG
guides, labels, focus, and accessibility
- Friction: renderer choice belonged to the whole chart, and every DOM adapter
assumed one surface. A dense mark could not opt into Canvas without moving
axes and labels too, while nested compositions, SSR adoption, focus updates,
pointer geometry, and image export all depended on the original single-root
contract.
- Decision: add a small universal renderer token to mark options and keep the
DOM compositor contract in the DOM layer. The compositor preserves authored
source order across nested SVG and Canvas layers, retains a default surface
for existing adapter callbacks, rebuilds complete interaction geometry after
focus presentation, and supports SSR, responsive updates, accessibility, and
mixed image export without importing Canvas into SVG-only bundles.
- Verification: Canvas unit coverage includes nested ordering, remounting,
focus-guide layers, and a second geometry-only pointer resolution after a
mixed focus paint. The browser Canvas gate, packed declarations and runtime,
React Native Metro gates, and framework package checks pass. Bundle boundary
checks keep SVG-only entries free of Canvas and measure the opt-in mixed
representative and React consumers at 35.68 KiB and 41.63 KiB gzip.
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,15 +90,18 @@ const revenueChart = defineChart({
y: 'value',
}),
],
x: {
scale: () => scaleBand().padding(0.2),
},
y: {
scale: scaleLinear,
nice: true,
grid: true,
axis: { label: 'Revenue' },
scales: {
x: {
scale: () => scaleBand().padding(0.2),
},
y: {
scale: scaleLinear,
nice: true,
grid: true,
axis: { label: 'Revenue' },
},
},

tooltip,
})

Expand Down
40 changes: 20 additions & 20 deletions benchmarks/bundle-size/universal-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,44 @@
"policy": "Exact minified and gzip output for entries that optional features must not affect. Review every change before updating.",
"bundles": {
"D3-scale line scene": {
"bytes": 47151,
"gzip": 17902
"bytes": 50871,
"gzip": 19096
},
"D3-scale line + static SVG": {
"bytes": 51884,
"gzip": 19619
"bytes": 55602,
"gzip": 20795
},
"Representative marks": {
"bytes": 71572,
"gzip": 26228
"bytes": 75552,
"gzip": 27551
},
"TanStack DOM host": {
"bytes": 70791,
"gzip": 24832
"bytes": 74740,
"gzip": 26000
},
"React adapter": {
"bytes": 72950,
"gzip": 25604
"bytes": 76923,
"gzip": 26780
},
"React line consumer": {
"bytes": 96180,
"gzip": 34832
"bytes": 100559,
"gzip": 36186
},
"Compact-scale line scene": {
"bytes": 29608,
"gzip": 10773
"bytes": 33323,
"gzip": 11958
},
"React compact-scale line consumer": {
"bytes": 78681,
"gzip": 27762
"bytes": 83059,
"gzip": 29082
},
"Custom-scale line scene": {
"bytes": 27792,
"gzip": 10035
"bytes": 31505,
"gzip": 11222
},
"D3 linear-scale line scene": {
"bytes": 47083,
"gzip": 17866
"bytes": 50803,
"gzip": 19059
}
}
}
122 changes: 61 additions & 61 deletions benchmarks/comparison/bundle-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"tanstack": {
"kind": "workspace",
"revision": "91e2eef1772dc06118ce5aa1e0b388d12bf91918",
"inputDigest": "sha256:07856952e86ead8b51b7517ecd2bcfd8af9fb47ff152125855b752cd51f2f94c"
"inputDigest": "sha256:08292488d4f7f01b2135f740b2047727a5711134fe05af96612f90bc73a63cee"
},
"chartjs": {
"kind": "package",
Expand Down Expand Up @@ -45,88 +45,88 @@
},
"bundles": {
"tanstack-line-basic": {
"minifiedBytes": 105868,
"gzipBytes": 38506,
"brotliBytes": 34131,
"incrementalGzipBytes": 38506,
"incrementalBrotliBytes": 34131
"minifiedBytes": 110224,
"gzipBytes": 39836,
"brotliBytes": 35281,
"incrementalGzipBytes": 39836,
"incrementalBrotliBytes": 35281
},
"tanstack-line-interactive": {
"minifiedBytes": 111309,
"gzipBytes": 40288,
"brotliBytes": 35520,
"incrementalGzipBytes": 40288,
"incrementalBrotliBytes": 35520
"minifiedBytes": 115665,
"gzipBytes": 41593,
"brotliBytes": 36763,
"incrementalGzipBytes": 41593,
"incrementalBrotliBytes": 36763
},
"tanstack-line-advanced": {
"minifiedBytes": 118499,
"gzipBytes": 42603,
"brotliBytes": 37538,
"incrementalGzipBytes": 42603,
"incrementalBrotliBytes": 37538
"minifiedBytes": 122855,
"gzipBytes": 43923,
"brotliBytes": 38701,
"incrementalGzipBytes": 43923,
"incrementalBrotliBytes": 38701
},
"tanstack-bar-basic": {
"minifiedBytes": 114536,
"gzipBytes": 41765,
"brotliBytes": 36861,
"incrementalGzipBytes": 41765,
"incrementalBrotliBytes": 36861
"minifiedBytes": 118895,
"gzipBytes": 43111,
"brotliBytes": 38052,
"incrementalGzipBytes": 43111,
"incrementalBrotliBytes": 38052
},
"tanstack-bar-interactive": {
"minifiedBytes": 118828,
"gzipBytes": 43107,
"brotliBytes": 37909,
"incrementalGzipBytes": 43107,
"incrementalBrotliBytes": 37909
"minifiedBytes": 123191,
"gzipBytes": 44478,
"brotliBytes": 39092,
"incrementalGzipBytes": 44478,
"incrementalBrotliBytes": 39092
},
"tanstack-bar-advanced": {
"minifiedBytes": 119167,
"gzipBytes": 43235,
"brotliBytes": 38037,
"incrementalGzipBytes": 43235,
"incrementalBrotliBytes": 38037
"minifiedBytes": 123530,
"gzipBytes": 44634,
"brotliBytes": 39245,
"incrementalGzipBytes": 44634,
"incrementalBrotliBytes": 39245
},
"tanstack-area-basic": {
"minifiedBytes": 110952,
"gzipBytes": 40437,
"brotliBytes": 35806,
"incrementalGzipBytes": 40437,
"incrementalBrotliBytes": 35806
"minifiedBytes": 115310,
"gzipBytes": 41751,
"brotliBytes": 36954,
"incrementalGzipBytes": 41751,
"incrementalBrotliBytes": 36954
},
"tanstack-area-interactive": {
"minifiedBytes": 116393,
"gzipBytes": 42169,
"brotliBytes": 37237,
"incrementalGzipBytes": 42169,
"incrementalBrotliBytes": 37237
"minifiedBytes": 120755,
"gzipBytes": 43491,
"brotliBytes": 38370,
"incrementalGzipBytes": 43491,
"incrementalBrotliBytes": 38370
},
"tanstack-area-advanced": {
"minifiedBytes": 123765,
"gzipBytes": 44603,
"brotliBytes": 39264,
"incrementalGzipBytes": 44603,
"incrementalBrotliBytes": 39264
"minifiedBytes": 128127,
"gzipBytes": 45958,
"brotliBytes": 40454,
"incrementalGzipBytes": 45958,
"incrementalBrotliBytes": 40454
},
"tanstack-scatter-basic": {
"minifiedBytes": 106907,
"gzipBytes": 38897,
"brotliBytes": 34396,
"incrementalGzipBytes": 38897,
"incrementalBrotliBytes": 34396
"minifiedBytes": 111253,
"gzipBytes": 40212,
"brotliBytes": 35564,
"incrementalGzipBytes": 40212,
"incrementalBrotliBytes": 35564
},
"tanstack-scatter-interactive": {
"minifiedBytes": 112348,
"gzipBytes": 40640,
"brotliBytes": 35797,
"incrementalGzipBytes": 40640,
"incrementalBrotliBytes": 35797
"minifiedBytes": 116694,
"gzipBytes": 41969,
"brotliBytes": 37033,
"incrementalGzipBytes": 41969,
"incrementalBrotliBytes": 37033
},
"tanstack-scatter-advanced": {
"minifiedBytes": 112364,
"gzipBytes": 40646,
"brotliBytes": 35846,
"incrementalGzipBytes": 40646,
"incrementalBrotliBytes": 35846
"minifiedBytes": 116710,
"gzipBytes": 41975,
"brotliBytes": 37003,
"incrementalGzipBytes": 41975,
"incrementalBrotliBytes": 37003
},
"chartjs-line-basic": {
"minifiedBytes": 137909,
Expand Down
29 changes: 16 additions & 13 deletions benchmarks/comparison/libraries/tanstack/area.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,23 @@ const definition = (input: BenchmarkInput) =>
stroke: BENCHMARK_INTERACTIVE ? undefined : color,
}),
],
x: {
scale: scaleLinear().domain(
BENCHMARK_STRESS
? [xMinimum(input), xMaximum(input)]
: [0, Math.max(1, input.rows.length - 1)],
),
grid: true,
axis: { ticks: { count: 6 } },
},
y: {
scale: scaleLinear().domain([0, BENCHMARK_ADVANCED ? 200 : 100]),
grid: true,
axis: { ticks: { count: 5 } },
scales: {
x: {
scale: scaleLinear().domain(
BENCHMARK_STRESS
? [xMinimum(input), xMaximum(input)]
: [0, Math.max(1, input.rows.length - 1)],
),
grid: true,
axis: { ticks: { count: 6 } },
},
y: {
scale: scaleLinear().domain([0, BENCHMARK_ADVANCED ? 200 : 100]),
grid: true,
axis: { ticks: { count: 5 } },
},
},

color: BENCHMARK_INTERACTIVE
? {
scale: scaleOrdinal<string, string>()
Expand Down
19 changes: 11 additions & 8 deletions benchmarks/comparison/libraries/tanstack/bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,18 @@ const definition = (input: BenchmarkInput) =>
fill: BENCHMARK_INTERACTIVE ? undefined : color,
}),
],
x: {
scale: () => scaleBand<string>().padding(0.1),
grid: true,
},
y: {
scale: scaleLinear().domain([0, BENCHMARK_ADVANCED ? 200 : 100]),
grid: true,
axis: { ticks: { count: 5 } },
scales: {
x: {
scale: () => scaleBand<string>().padding(0.1),
grid: true,
},
y: {
scale: scaleLinear().domain([0, BENCHMARK_ADVANCED ? 200 : 100]),
grid: true,
axis: { ticks: { count: 5 } },
},
},

color: BENCHMARK_INTERACTIVE
? {
scale: scaleOrdinal<string, string>()
Expand Down
Loading