Skip to content
Draft
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/grid-focus-strategy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/charts': minor
---

Add `focusGrid` from `@tanstack/charts/focus/grid` for row-and-column keyboard focus in heatmaps, calendars, and matrices, and an optional `step` method on `ChartFocusStrategy` for directional keyboard moves. Strategies without `step` keep their existing navigation order.
32 changes: 32 additions & 0 deletions API-FRICTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ Each entry records:
| F-312 | Tick stubs could not be styled independently | API | resolved |
| F-313 | Initial observer precision cancelled entrance motion | Tooling | resolved |
| F-314 | Framework ticks and immediate focus layers were unclear | Documentation | resolved |
| F-315 | Heatmap keyboard focus moved through one flat list | API | open |

## Findings

Expand Down Expand Up @@ -9317,3 +9318,34 @@ Each entry records:
direction changes. A focus dot appears during entrance while the data path
and its running animation remain intact. Concurrent inline states remain a
separate feature; this addresses the documented alternatives requested.

### F-315 - Heatmap keyboard focus moved through one flat list

- Status: open
- Severity: medium
- Owner: API
- Observed in: PMDB, a consumer application, which renders its person-by-week
capacity heatmap as an HTML table with one button per cell instead of a
chart. Chart keyboard focus was one reason: every strategy exposes one
ordered `navigation` list, and the DOM host maps ArrowDown to ArrowRight and
ArrowUp to ArrowLeft over it.
- Friction: in a matrix, a keyboard reader could not move to the same week for
the next person, or to the start of a row, without stepping through every
cell between. Reproducing grid movement meant leaving chart focus and
building an application-owned widget.
- Decision (draft): add an optional `step(points, context)` method to
`ChartFocusStrategy` for directional keys, with `navigation` as the fallback
and the default for existing strategies. Ship `focusGrid` from the separate
`@tanstack/charts/focus/grid` entry point so charts that do not import it do
not grow. The rejected alternative was letting `navigation` return rows,
which changes a return type every host consumes. Two choices stay open for
maintainer review: (a) `key` is the raw `KeyboardEvent.key`, so only the DOM
host dispatches `step`; the D3 companion host and React Native call
`navigation` only and fall back to row-major linear order. A host-neutral
direction vocabulary (`right`, `left`, `up`, `down`, `rowStart`, `rowEnd`,
`first`, `last`) would let every host dispatch it. (b) `modifier` is one
boolean for Control or Command instead of explicit `ctrlKey` and `metaKey`.
- Verification: unit tests cover row and column moves, sparse skips, row and
grid extremes, the row-major fallback order, and a mounted heatmap whose
tooltip names both the row and column after keyboard moves. The built-in
axis strategies have no `step` and keep their existing tests.
16 changes: 8 additions & 8 deletions benchmarks/bundle-size/universal-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,24 @@
"gzip": 30414
},
"TanStack DOM host": {
"bytes": 82961,
"gzip": 29116
"bytes": 83027,
"gzip": 29160
},
"React adapter": {
"bytes": 85286,
"gzip": 30070
"bytes": 85352,
"gzip": 30114
},
"React line consumer": {
"bytes": 108945,
"gzip": 39420
"bytes": 109011,
"gzip": 39464
},
"Compact-scale line scene": {
"bytes": 35859,
"gzip": 12797
},
"React compact-scale line consumer": {
"bytes": 91443,
"gzip": 32305
"bytes": 91509,
"gzip": 32345
},
"Custom-scale line scene": {
"bytes": 34043,
Expand Down
126 changes: 63 additions & 63 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-14T00:00:31.426Z",
"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": "1d8892d8c8ca617f2ffc94e086ccd1db7669e336",
"inputDigest": "sha256:b4bdc595e1ba77a8875e12287431cb3fa921537829d4865870272bbf5c9fc501"
},
"chartjs": {
"kind": "package",
Expand Down Expand Up @@ -45,88 +45,88 @@
},
"bundles": {
"tanstack-line-basic": {
"minifiedBytes": 119233,
"gzipBytes": 43211,
"brotliBytes": 38200,
"incrementalGzipBytes": 43211,
"incrementalBrotliBytes": 38200
"minifiedBytes": 119299,
"gzipBytes": 43261,
"brotliBytes": 38290,
"incrementalGzipBytes": 43261,
"incrementalBrotliBytes": 38290
},
"tanstack-line-interactive": {
"minifiedBytes": 124980,
"gzipBytes": 45122,
"brotliBytes": 39727,
"incrementalGzipBytes": 45122,
"incrementalBrotliBytes": 39727
"minifiedBytes": 125046,
"gzipBytes": 45171,
"brotliBytes": 39826,
"incrementalGzipBytes": 45171,
"incrementalBrotliBytes": 39826
},
"tanstack-line-advanced": {
"minifiedBytes": 132176,
"gzipBytes": 47454,
"brotliBytes": 41771,
"incrementalGzipBytes": 47454,
"incrementalBrotliBytes": 41771
"minifiedBytes": 132242,
"gzipBytes": 47502,
"brotliBytes": 41813,
"incrementalGzipBytes": 47502,
"incrementalBrotliBytes": 41813
},
"tanstack-bar-basic": {
"minifiedBytes": 131670,
"gzipBytes": 47913,
"brotliBytes": 42147,
"incrementalGzipBytes": 47913,
"incrementalBrotliBytes": 42147
"minifiedBytes": 131736,
"gzipBytes": 47956,
"brotliBytes": 42154,
"incrementalGzipBytes": 47956,
"incrementalBrotliBytes": 42154
},
"tanstack-bar-interactive": {
"minifiedBytes": 136276,
"gzipBytes": 49307,
"brotliBytes": 43250,
"incrementalGzipBytes": 49307,
"incrementalBrotliBytes": 43250
"minifiedBytes": 136342,
"gzipBytes": 49357,
"brotliBytes": 43357,
"incrementalGzipBytes": 49357,
"incrementalBrotliBytes": 43357
},
"tanstack-bar-advanced": {
"minifiedBytes": 136615,
"gzipBytes": 49453,
"brotliBytes": 43349,
"incrementalGzipBytes": 49453,
"incrementalBrotliBytes": 43349
"minifiedBytes": 136681,
"gzipBytes": 49504,
"brotliBytes": 43425,
"incrementalGzipBytes": 49504,
"incrementalBrotliBytes": 43425
},
"tanstack-area-basic": {
"minifiedBytes": 124413,
"gzipBytes": 45239,
"brotliBytes": 39996,
"incrementalGzipBytes": 45239,
"incrementalBrotliBytes": 39996
"minifiedBytes": 124479,
"gzipBytes": 45290,
"brotliBytes": 40044,
"incrementalGzipBytes": 45290,
"incrementalBrotliBytes": 40044
},
"tanstack-area-interactive": {
"minifiedBytes": 130168,
"gzipBytes": 47088,
"brotliBytes": 41480,
"incrementalGzipBytes": 47088,
"incrementalBrotliBytes": 41480
"minifiedBytes": 130234,
"gzipBytes": 47137,
"brotliBytes": 41521,
"incrementalGzipBytes": 47137,
"incrementalBrotliBytes": 41521
},
"tanstack-area-advanced": {
"minifiedBytes": 137540,
"gzipBytes": 49496,
"brotliBytes": 43606,
"incrementalGzipBytes": 49496,
"incrementalBrotliBytes": 43606
"minifiedBytes": 137606,
"gzipBytes": 49547,
"brotliBytes": 43616,
"incrementalGzipBytes": 49547,
"incrementalBrotliBytes": 43616
},
"tanstack-scatter-basic": {
"minifiedBytes": 120286,
"gzipBytes": 43572,
"brotliBytes": 38499,
"incrementalGzipBytes": 43572,
"incrementalBrotliBytes": 38499
"minifiedBytes": 120352,
"gzipBytes": 43622,
"brotliBytes": 38568,
"incrementalGzipBytes": 43622,
"incrementalBrotliBytes": 38568
},
"tanstack-scatter-interactive": {
"minifiedBytes": 126037,
"gzipBytes": 45469,
"brotliBytes": 40055,
"incrementalGzipBytes": 45469,
"incrementalBrotliBytes": 40055
"minifiedBytes": 126103,
"gzipBytes": 45527,
"brotliBytes": 40085,
"incrementalGzipBytes": 45527,
"incrementalBrotliBytes": 40085
},
"tanstack-scatter-advanced": {
"minifiedBytes": 126053,
"gzipBytes": 45476,
"brotliBytes": 40107,
"incrementalGzipBytes": 45476,
"incrementalBrotliBytes": 40107
"minifiedBytes": 126119,
"gzipBytes": 45533,
"brotliBytes": 40125,
"incrementalGzipBytes": 45533,
"incrementalBrotliBytes": 40125
},
"chartjs-line-basic": {
"minifiedBytes": 137909,
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": "119950fe81201e2e5072fcef7350597cf289da3948cd78880e0051cc34df2711",
"assets": [
{
"id": "01-line-gaps",
Expand Down
14 changes: 7 additions & 7 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 `1d8892d` |
| [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 `1d8892d`.

## 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 All @@ -106,10 +106,10 @@ Vega-Lite, AG Charts, and uPlot main exports were read from Bundlephobia on July

| Library | Bundle size | React externalized | Evidence |
| ------------------ | -------------------------------------- | -------------------: | ---------------------------------------------------------- |
| TanStack Charts | 43.21–49.50 kB gzip | Not applicable | Controlled suite |
| TanStack Charts | 43.26–49.55 kB gzip | Not applicable | Controlled suite |
| D3 | 90 KB gzip | — | External main export |
| Chart.js | 45.78–59.61 kB gzip | — | Controlled suite |
| Apache ECharts | 156.77–177.34 kB gzip | — | Controlled suite |
| Chart.js | 45.78–59.61 kB gzip | Not applicable | Controlled suite |
| Apache ECharts | 156.77–177.34 kB gzip | Not applicable | Controlled suite |
| Recharts | 156.76–172.30 kB gzip | 97.24–112.60 kB gzip | Controlled suite |
| visx | 49 KB gzip | — | External `@visx/xychart` main export |
| Plotly.js | ~250 kB partial; ~3.6 MB full min+gzip | — | [Vendor distribution figures](https://plotly.com/graphs/) |
Expand All @@ -120,7 +120,7 @@ Vega-Lite, AG Charts, and uPlot main exports were read from Bundlephobia on July
| Victory | 105 KB gzip | — | External main export |
| uPlot | 22 KB gzip | — | External `uplot@1.6.32` main export |
| Vega-Lite | 87 KB gzip | — | External `vega-lite@6.4.3`; excludes the peer Vega runtime |
| Observable Plot | 85.34–94.15 kB gzip | — | Controlled suite |
| Observable Plot | 85.34–94.15 kB gzip | Not applicable | Controlled suite |
| Bklit UI | — | — | Registry-installed source; no fixed package bundle |
| AG Charts | 367 KB gzip | — | External `ag-charts-community@14.0.2` main export |

Expand Down
Loading