Skip to content
Open
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
6 changes: 6 additions & 0 deletions .changeset/bright-axis-titles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@tanstack/charts': minor
---

Allow Cartesian axis titles to configure font size, font weight, fill, and
opacity while retaining automatic guide layout.
28 changes: 27 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-26
Last updated: 2026-08-27

## Triage rule

Expand Down Expand Up @@ -331,6 +331,7 @@ Each entry records:
| 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 |
| F-295 | Axis titles could not carry authored typography or paint | API | resolved |

## Findings

Expand Down Expand Up @@ -8505,3 +8506,28 @@ Each entry records:
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.

### F-295 - Axis titles could not carry authored typography or paint

- Status: resolved
- Severity: medium
- Owner: API
- Observed in: migrating Rewardo dashboard charts to TanStack Charts on the
`experimental-tanstack-charts` branch and issue #93
- Friction: tick labels accepted font size, weight, and opacity, but
`axis.label` accepted only text, offset, and motion. Rewardo's 14-pixel chart
typography and foreground treatment therefore required a generated-node CSS
override that Canvas and React Native could not honor.
- Decision: extend the existing axis-label object with `fontSize`,
`fontWeight`, `fill`, and `opacity`. Keep the string form and every omitted
object field on the existing defaults. Resolve the options into the shared
scene label so automatic guide measurement and every renderer use one
contract.
- Verification: scene layout covers configured typography, paint, default
compatibility, automatic margins, and shared facet titles. SVG, Canvas,
React Native, and motion tests exercise the same public definition. The
composed weather catalog case uses independently styled titles on three y
axes without renderer callbacks or CSS selectors. The reviewed shared-path
delta is 169 minified bytes and 29 gzip bytes for the locked line plus static
SVG consumer. A dedicated styled-title fixture adds 0.06 KiB gzip and no
retained modules over that consumer.
4 changes: 4 additions & 0 deletions benchmarks/bundle-size/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ gzip ceiling. The locked shared-host entries record the reviewed integration
cost, while noninteractive consumers retain only the small scene-compiler
portion of that contract.

Cartesian axis-title styling is also part of the shared scene contract. Its
static SVG fixture adds no retained modules over the ordinary line consumer
and keeps authored title options under a 0.25 KiB incremental gzip ceiling.

Continuous viewports and the controlled interaction controller are also part
of the default scene and host contracts. Default static SVG consumes scene
clips and gradients. Their reviewed shared-path cost is recorded in the locked
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": 50813,
"gzip": 19070
"bytes": 50982,
"gzip": 19097
},
"D3-scale line + static SVG": {
"bytes": 55544,
"gzip": 20776
"bytes": 55713,
"gzip": 20805
},
"Representative marks": {
"bytes": 75617,
"gzip": 27582
"bytes": 75788,
"gzip": 27619
},
"TanStack DOM host": {
"bytes": 74947,
"gzip": 26053
"bytes": 75118,
"gzip": 26064
},
"React adapter": {
"bytes": 77130,
"gzip": 26831
"bytes": 77301,
"gzip": 26854
},
"React line consumer": {
"bytes": 100766,
"gzip": 36222
"bytes": 100937,
"gzip": 36262
},
"Compact-scale line scene": {
"bytes": 33265,
"gzip": 11935
"bytes": 33436,
"gzip": 11953
},
"React compact-scale line consumer": {
"bytes": 83266,
"gzip": 29144
"bytes": 83437,
"gzip": 29177
},
"Custom-scale line scene": {
"bytes": 31447,
"gzip": 11193
"bytes": 31618,
"gzip": 11219
},
"D3 linear-scale line scene": {
"bytes": 50745,
"gzip": 19032
"bytes": 50914,
"gzip": 19060
}
}
}
126 changes: 63 additions & 63 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-26T21:51:12.832Z",
"generatedAt": "2026-08-27T21:42:45.654Z",
"packageVersions": {
"tanstack": "0.15.0",
"tanstack": "0.16.0",
"chartjs": "4.5.1",
"echarts": "6.1.0",
"recharts": "3.10.1",
Expand All @@ -12,7 +12,7 @@
"tanstack": {
"kind": "workspace",
"revision": "3df87d71f0305e5a450c10b66940f76f3e14259a",
"inputDigest": "sha256:b1016e599e63d05e1647b9d27adabaaed7a38bb44dac8d6ba0fef06b8b8bd38e"
"inputDigest": "sha256:f6e6651fe8f30068b56208e243a35ea634593f85c7364b8403dfebfcb1e0d0f8"
},
"chartjs": {
"kind": "package",
Expand Down Expand Up @@ -45,88 +45,88 @@
},
"bundles": {
"tanstack-line-basic": {
"minifiedBytes": 110431,
"gzipBytes": 39891,
"brotliBytes": 35325,
"incrementalGzipBytes": 39891,
"incrementalBrotliBytes": 35325
"minifiedBytes": 110602,
"gzipBytes": 39906,
"brotliBytes": 35329,
"incrementalGzipBytes": 39906,
"incrementalBrotliBytes": 35329
},
"tanstack-line-interactive": {
"minifiedBytes": 115872,
"gzipBytes": 41648,
"brotliBytes": 36746,
"incrementalGzipBytes": 41648,
"incrementalBrotliBytes": 36746
"minifiedBytes": 116043,
"gzipBytes": 41675,
"brotliBytes": 36794,
"incrementalGzipBytes": 41675,
"incrementalBrotliBytes": 36794
},
"tanstack-line-advanced": {
"minifiedBytes": 123062,
"gzipBytes": 43974,
"brotliBytes": 38764,
"incrementalGzipBytes": 43974,
"incrementalBrotliBytes": 38764
"minifiedBytes": 123233,
"gzipBytes": 44002,
"brotliBytes": 38748,
"incrementalGzipBytes": 44002,
"incrementalBrotliBytes": 38748
},
"tanstack-bar-basic": {
"minifiedBytes": 119225,
"gzipBytes": 43210,
"brotliBytes": 38168,
"incrementalGzipBytes": 43210,
"incrementalBrotliBytes": 38168
"minifiedBytes": 119396,
"gzipBytes": 43218,
"brotliBytes": 38198,
"incrementalGzipBytes": 43218,
"incrementalBrotliBytes": 38198
},
"tanstack-bar-interactive": {
"minifiedBytes": 123521,
"gzipBytes": 44585,
"brotliBytes": 39223,
"incrementalGzipBytes": 44585,
"incrementalBrotliBytes": 39223
"minifiedBytes": 123692,
"gzipBytes": 44605,
"brotliBytes": 39267,
"incrementalGzipBytes": 44605,
"incrementalBrotliBytes": 39267
},
"tanstack-bar-advanced": {
"minifiedBytes": 123860,
"gzipBytes": 44735,
"brotliBytes": 39349,
"incrementalGzipBytes": 44735,
"incrementalBrotliBytes": 39349
"minifiedBytes": 124031,
"gzipBytes": 44749,
"brotliBytes": 39341,
"incrementalGzipBytes": 44749,
"incrementalBrotliBytes": 39341
},
"tanstack-area-basic": {
"minifiedBytes": 115640,
"gzipBytes": 41867,
"brotliBytes": 37073,
"incrementalGzipBytes": 41867,
"incrementalBrotliBytes": 37073
"minifiedBytes": 115811,
"gzipBytes": 41866,
"brotliBytes": 37072,
"incrementalGzipBytes": 41866,
"incrementalBrotliBytes": 37072
},
"tanstack-area-interactive": {
"minifiedBytes": 121085,
"gzipBytes": 43594,
"brotliBytes": 38493,
"incrementalGzipBytes": 43594,
"incrementalBrotliBytes": 38493
"minifiedBytes": 121256,
"gzipBytes": 43620,
"brotliBytes": 38585,
"incrementalGzipBytes": 43620,
"incrementalBrotliBytes": 38585
},
"tanstack-area-advanced": {
"minifiedBytes": 128457,
"gzipBytes": 46057,
"brotliBytes": 40527,
"incrementalGzipBytes": 46057,
"incrementalBrotliBytes": 40527
"minifiedBytes": 128628,
"gzipBytes": 46064,
"brotliBytes": 40628,
"incrementalGzipBytes": 46064,
"incrementalBrotliBytes": 40628
},
"tanstack-scatter-basic": {
"minifiedBytes": 111460,
"gzipBytes": 40266,
"brotliBytes": 35596,
"incrementalGzipBytes": 40266,
"incrementalBrotliBytes": 35596
"minifiedBytes": 111631,
"gzipBytes": 40284,
"brotliBytes": 35628,
"incrementalGzipBytes": 40284,
"incrementalBrotliBytes": 35628
},
"tanstack-scatter-interactive": {
"minifiedBytes": 116901,
"gzipBytes": 42015,
"brotliBytes": 36976,
"incrementalGzipBytes": 42015,
"incrementalBrotliBytes": 36976
"minifiedBytes": 117072,
"gzipBytes": 42044,
"brotliBytes": 37051,
"incrementalGzipBytes": 42044,
"incrementalBrotliBytes": 37051
},
"tanstack-scatter-advanced": {
"minifiedBytes": 116917,
"gzipBytes": 42020,
"brotliBytes": 37012,
"incrementalGzipBytes": 42020,
"incrementalBrotliBytes": 37012
"minifiedBytes": 117088,
"gzipBytes": 42050,
"brotliBytes": 37043,
"incrementalGzipBytes": 42050,
"incrementalBrotliBytes": 37043
},
"chartjs-line-basic": {
"minifiedBytes": 137909,
Expand Down
1 change: 1 addition & 0 deletions benchmarks/conformance/cases/70-composed-chart/case.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"mixed marks",
"independent y scales",
"left axis and stacked right axes",
"styled axis titles",
"daily observations",
"monotone curves",
"resolved maximum bar thickness"
Expand Down
24 changes: 21 additions & 3 deletions benchmarks/conformance/cases/70-composed-chart/example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,13 @@ export const createExampleChart = (input: ChartOptions) => {
scale: scaleLinear,
grid: true,
axis: {
label: 'Temperature (°C)',
label: {
text: 'Temperature (°C)',
fontSize: 12,
fontWeight: 700,
fill: '#8884d8',
opacity: 0.8,
},
ticks: { count: 5 },
},
},
Expand All @@ -72,7 +78,13 @@ export const createExampleChart = (input: ChartOptions) => {
side: 'right',
scale: scaleLinear,
axis: {
label: 'Precipitation (mm)',
label: {
text: 'Precipitation (mm)',
fontSize: 12,
fontWeight: 700,
fill: '#413ea0',
opacity: 0.8,
},
ticks: { count: 5 },
},
},
Expand All @@ -81,7 +93,13 @@ export const createExampleChart = (input: ChartOptions) => {
side: 'right',
scale: scaleLinear,
axis: {
label: 'Wind (m/s)',
label: {
text: 'Wind (m/s)',
fontSize: 12,
fontWeight: 700,
fill: '#ef4444',
opacity: 0.8,
},
ticks: { count: 5 },
},
},
Expand Down
12 changes: 12 additions & 0 deletions benchmarks/conformance/cases/70-composed-chart/recharts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ function chart(input: ConformanceInput) {
value: 'Temperature (°C)',
angle: -90,
position: 'insideLeft',
fill: '#8884d8',
fontSize: 12,
fontWeight: 700,
opacity: 0.8,
},
}),
createElement(YAxis, {
Expand All @@ -67,6 +71,10 @@ function chart(input: ConformanceInput) {
value: 'Precipitation (mm)',
angle: 90,
position: 'insideRight',
fill: '#413ea0',
fontSize: 12,
fontWeight: 700,
opacity: 0.8,
},
}),
createElement(YAxis, {
Expand All @@ -79,6 +87,10 @@ function chart(input: ConformanceInput) {
value: 'Wind (m/s)',
angle: 90,
position: 'insideRight',
fill: '#ef4444',
fontSize: 12,
fontWeight: 700,
opacity: 0.8,
},
}),
createElement(Area, {
Expand Down
Loading