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
12 changes: 12 additions & 0 deletions .changeset/add-pattern-resources.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@tanstack/charts': minor
'@tanstack/react-native-charts': minor
---

Add renderer-neutral pattern resources. Declare repeating tiles in a chart's
`patterns` list and reference them from `fill` or `stroke` as `url(#id)`.
`linePattern()` and `dotPattern()` build hatch and dot tiles whose paint
accepts CSS variables. SVG, Canvas, standalone export, and React Native draw
the same tile, with resource IDs scoped by `idPrefix`.
Canvas stops cyclic pattern dependencies and rejects tiles that exceed its
documented allocation limits.
74 changes: 65 additions & 9 deletions API-FRICTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ Each entry records:
| F-256 | Shared host policy retained browser-only modules | API/Tooling | resolved |
| F-257 | The release package graph leaked into application setup | API/Docs/Tooling | resolved |
| F-258 | Tooltip chrome required specificity overrides | API/Documentation | resolved |
| F-259 | Chart resources cannot declare patterns | API | open |
| F-259 | Chart resources cannot declare patterns | API | resolved |
| F-260 | Static guides cannot express stroke treatment | API | resolved |
| F-261 | Cartesian bars cannot round only exposed corners | API | resolved |
| F-262 | Mark inference accepted an unsupported style option | API | resolved |
Expand Down Expand Up @@ -8084,19 +8084,75 @@ Each entry records:

### F-259 — Chart resources cannot declare patterns

- Status: open
- Status: resolved
- Severity: medium
- Owner: API
- Observed in: the theme-treatment audit for catalog cases 120–124 and the
Bklit-derived card references
Bklit-derived card references; restated by GitHub issue #128 for funnel,
Sankey, candlestick, gauge, and heatmap hatching
- Friction: `ChartSpec.gradients` keeps CSS-variable linear and radial
gradients inside the renderer-neutral definition. The same resource model
cannot declare a hatch, dot, or line pattern. An author must omit that
treatment or leave the native definition and renderer resource boundary.
- Current decision: case 124 uses the same declared linear gradient across its
palette treatments. Do not inject an application-owned SVG pattern as a
catalog workaround. Keep a native pattern resource open until one contract
covers SVG, Canvas, export, CSS-variable paint, and `idPrefix` scoping.
could not declare a hatch, dot, or line pattern. An author had to omit that
treatment or render an application-owned `<pattern>` beside the chart and
reference it by ID, which broke `serializeChartSvg`, `renderChartImage`,
Canvas, React Native, and `idPrefix` scoping.
- Decision: add a sibling `patterns` spec and scene key that mirrors
`gradients` and is referenced from `fill` or `stroke` as `url(#id)`. A
`ChartPattern` is plain data: an ID, a tile `width` and `height` in pixels,
an optional counterclockwise `angle`, and ordinary `SceneNode` tile content.
Renderers reuse their existing node serializers and painters for the tile,
which keeps the shared path small. The small vocabulary lives in
tree-shakeable `linePattern()` and `dotPattern()` factories at
`@tanstack/charts/pattern`; they own defaults, clamping, and hundredth
rounding, and their `color` and `background` accept CSS variables. SVG emits
a scoped `userSpaceOnUse` `<pattern>` and rewrites matching paints. Canvas
rasterizes the tile at device resolution and maps it back through
`CanvasPattern.setTransform` with the same rotation. Text labels get an
origin correction because SVG text keeps the scene origin while Canvas
translates to the label. React Native emits `react-native-svg` `Pattern`,
which version 15 supports. Embedded views reject child patterns, as they do
child gradients. SVG rewrites resource paints inside tiles, so a tile may
reference a gradient under `idPrefix`. Gradient and pattern IDs share one
namespace and must be unique; collisions are undefined. A scene-time
duplicate check measured about 60 gzip bytes on every default bundle and
would newly reject duplicate gradient IDs, so it is left for maintainers to
decide. Animated patterns stay out of scope; a pattern is static
until the definition changes.
- Safety: Canvas tracks active resource IDs separately from raster resolution
caches. Fractional cyclic tiles stop without repeated allocation. Per-paint
tile dimensions, pixel allocation, and dependency depth are bounded, with
focused cycle and allocation regressions.
- Follow-up: gradients and patterns are now two sibling resource lists with
one paint reference syntax. A later unified paint-resource model could merge
them, but this entry keeps the existing gradient contract unchanged.
- Verification: SVG unit coverage checks scoped IDs, rotation, factory
clamping, empty tiles, fill and stroke rewriting, a tile that references a
gradient, and untouched foreign references. Canvas unit coverage checks the device-resolution tile, CSS
variable resolution through the paint resolver, background-then-foreground
order, cached reuse across fill, stroke, and label paint, the rotation
matrix, and the label origin. Export coverage proves computed tile paint
replaces `var()` in standalone SVG. React Native coverage checks scoped
`Pattern` resources and resolved native paint. The browser Canvas gate
rasterizes exported SVG and compares rotated hatch and dot fills with
Canvas output: the mean channel delta is about 1 of 255 with no divergent
pixels, and reversing the Canvas rotation fails the gate.
- Proposed bundle increases: matched production builds against main
`5a34b7e1` measure 516 gzip bytes for the Canvas host, 646 for representative
marks with Canvas, and 619 for React line with Canvas. Static SVG adds 120
bytes, DOM 124, compact React 136, and scene-only consumers 7 to 8. These
are proposed increases pending maintainer approval. Decimal kB divides
bytes by 1000; the source ceilings use KiB (1024 bytes). The factories add
about 0.27 KiB under an isolated 0.3 KiB proposed budget.
- Default-path performance: matched Chromium 151.0.7922.34 production builds
compare main `5a34b7e1` with pattern source `ec695a8b`. Each trial renders
one 960 by 540 scene of 1000 solid-color 6 by 6 rectangles 100 times at
pixel ratio one, with no patterns. Each of two fresh browsers reuses its
variant contexts for three warmups and seven paired trials. Execution order
alternates; the second browser reverses context creation order. Main/PR
medians are 51.8/48.0 ms and 48.7/47.9 ms; paired median PR-minus-main
differences are -0.7 and -0.1 ms. This bounded benchmark reproduces no
consistent slowdown. Other workloads and on-device React Native rendering
remain unverified.

### F-260 — Static guides cannot express stroke treatment

Expand Down
30 changes: 29 additions & 1 deletion benchmarks/bundle-size/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ compact-scale, and tooltip kernels also reject all transform modules.

The compact linear scene and React consumer are both locked and budgeted. The
scene has a 12.46 KiB gzip ceiling. The React compact-scale line consumer has a
31.03 KiB ceiling with React and React DOM external. `d3-array` tick helpers are
31.16 KiB ceiling with React and React DOM external. `d3-array` tick helpers are
allowed only in the compact linear path; categorical compact-scale kernels
reject every D3 runtime input. All compact fixtures reject `d3-scale`,
`d3-format`, `d3-interpolate`, `d3-color`, and `internmap`.
Expand Down Expand Up @@ -60,6 +60,34 @@ Radial resources add 112
gzip bytes to the static SVG consumer and 124 bytes to the DOM host. Scene-only
consumers remain unchanged apart from one byte of compression variation.

Pattern resources are part of the default scene and SVG contracts. Matched
production builds against main `5a34b7e1` measure the following proposed gzip
increases, pending maintainer approval:

| Consumer | Added bytes | Decimal kB |
| -------------------------------- | ----------: | ---------: |
| Canvas host | 516 | 0.516 |
| Representative marks with Canvas | 646 | 0.646 |
| React line with Canvas | 619 | 0.619 |
| Static SVG line | 120 | 0.120 |
| DOM host | 124 | 0.124 |
| Compact React line | 136 | 0.136 |

Scene-only consumers add 7 to 8 bytes. Decimal kB uses 1000 bytes; the source
ceilings use KiB (1024 bytes). Proposed ceilings retain the existing upstream
headroom. The `linePattern()` and `dotPattern()` vocabulary stays in its own
subpath: its static SVG fixture adds about 0.27 KiB under a proposed 0.3 KiB
incremental cap, and the ordinary line consumer forbids it.

For the no-pattern default path, matched Chromium 151.0.7922.34 production
builds rendered 1000 solid-color rectangles in a 960 by 540 scene 100 times
per trial at pixel ratio one. Two fresh browsers each ran three warmups and
seven paired trials in reused variant contexts. Trial execution alternated;
context creation order reversed between browsers. Main/PR medians were
51.8/48.0 ms and 48.7/47.9 ms. Paired median PR-minus-main differences were
-0.7 and -0.1 ms. No consistent slowdown was reproduced in this fixture; it
does not establish performance for other workloads or devices.

Every public transform family has an isolated budget and retained-input
allowlist. Numeric and 2D bins may retain `d3-array`, and row stacks may retain
`d3-shape`. Other transform entries reject those dependencies, while every
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": 53407,
"gzip": 19966
"bytes": 53427,
"gzip": 19973
},
"D3-scale line + static SVG": {
"bytes": 59562,
"gzip": 22278
"bytes": 59969,
"gzip": 22398
},
"Representative marks": {
"bytes": 83349,
"gzip": 30414
"bytes": 83756,
"gzip": 30550
},
"TanStack DOM host": {
"bytes": 82961,
"gzip": 29116
"bytes": 83368,
"gzip": 29240
},
"React adapter": {
"bytes": 85286,
"gzip": 30070
"bytes": 85693,
"gzip": 30206
},
"React line consumer": {
"bytes": 108945,
"gzip": 39420
"bytes": 109352,
"gzip": 39544
},
"Compact-scale line scene": {
"bytes": 35859,
"gzip": 12797
"bytes": 35879,
"gzip": 12805
},
"React compact-scale line consumer": {
"bytes": 91443,
"gzip": 32305
"bytes": 91850,
"gzip": 32441
},
"Custom-scale line scene": {
"bytes": 34043,
"gzip": 12064
"bytes": 34063,
"gzip": 12072
},
"D3 linear-scale line scene": {
"bytes": 53339,
"gzip": 19928
"bytes": 53359,
"gzip": 19936
}
}
}
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:03:22.732Z",
"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": "c5ad730c9bb380e2c61dd2889969c29eac96d5f8",
"inputDigest": "sha256:47a9f12f28d46c4540ddf478a478e8778985922a42dd8668b1ca871cc4ce6347"
},
"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": 119640,
"gzipBytes": 43345,
"brotliBytes": 38299,
"incrementalGzipBytes": 43345,
"incrementalBrotliBytes": 38299
},
"tanstack-line-interactive": {
"minifiedBytes": 124980,
"gzipBytes": 45122,
"brotliBytes": 39727,
"incrementalGzipBytes": 45122,
"incrementalBrotliBytes": 39727
"minifiedBytes": 125387,
"gzipBytes": 45247,
"brotliBytes": 39873,
"incrementalGzipBytes": 45247,
"incrementalBrotliBytes": 39873
},
"tanstack-line-advanced": {
"minifiedBytes": 132176,
"gzipBytes": 47454,
"brotliBytes": 41771,
"incrementalGzipBytes": 47454,
"incrementalBrotliBytes": 41771
"minifiedBytes": 132583,
"gzipBytes": 47577,
"brotliBytes": 41851,
"incrementalGzipBytes": 47577,
"incrementalBrotliBytes": 41851
},
"tanstack-bar-basic": {
"minifiedBytes": 131670,
"gzipBytes": 47913,
"brotliBytes": 42147,
"incrementalGzipBytes": 47913,
"incrementalBrotliBytes": 42147
"minifiedBytes": 132079,
"gzipBytes": 48055,
"brotliBytes": 42278,
"incrementalGzipBytes": 48055,
"incrementalBrotliBytes": 42278
},
"tanstack-bar-interactive": {
"minifiedBytes": 136276,
"gzipBytes": 49307,
"brotliBytes": 43250,
"incrementalGzipBytes": 49307,
"incrementalBrotliBytes": 43250
"minifiedBytes": 136685,
"gzipBytes": 49446,
"brotliBytes": 43351,
"incrementalGzipBytes": 49446,
"incrementalBrotliBytes": 43351
},
"tanstack-bar-advanced": {
"minifiedBytes": 136615,
"gzipBytes": 49453,
"brotliBytes": 43349,
"incrementalGzipBytes": 49453,
"incrementalBrotliBytes": 43349
"minifiedBytes": 137024,
"gzipBytes": 49582,
"brotliBytes": 43561,
"incrementalGzipBytes": 49582,
"incrementalBrotliBytes": 43561
},
"tanstack-area-basic": {
"minifiedBytes": 124413,
"gzipBytes": 45239,
"brotliBytes": 39996,
"incrementalGzipBytes": 45239,
"incrementalBrotliBytes": 39996
"minifiedBytes": 124820,
"gzipBytes": 45366,
"brotliBytes": 40113,
"incrementalGzipBytes": 45366,
"incrementalBrotliBytes": 40113
},
"tanstack-area-interactive": {
"minifiedBytes": 130168,
"gzipBytes": 47088,
"brotliBytes": 41480,
"incrementalGzipBytes": 47088,
"incrementalBrotliBytes": 41480
"minifiedBytes": 130575,
"gzipBytes": 47214,
"brotliBytes": 41633,
"incrementalGzipBytes": 47214,
"incrementalBrotliBytes": 41633
},
"tanstack-area-advanced": {
"minifiedBytes": 137540,
"gzipBytes": 49496,
"brotliBytes": 43606,
"incrementalGzipBytes": 49496,
"incrementalBrotliBytes": 43606
"minifiedBytes": 137950,
"gzipBytes": 49624,
"brotliBytes": 43648,
"incrementalGzipBytes": 49624,
"incrementalBrotliBytes": 43648
},
"tanstack-scatter-basic": {
"minifiedBytes": 120286,
"gzipBytes": 43572,
"brotliBytes": 38499,
"incrementalGzipBytes": 43572,
"incrementalBrotliBytes": 38499
"minifiedBytes": 120693,
"gzipBytes": 43712,
"brotliBytes": 38562,
"incrementalGzipBytes": 43712,
"incrementalBrotliBytes": 38562
},
"tanstack-scatter-interactive": {
"minifiedBytes": 126037,
"gzipBytes": 45469,
"brotliBytes": 40055,
"incrementalGzipBytes": 45469,
"incrementalBrotliBytes": 40055
"minifiedBytes": 126444,
"gzipBytes": 45609,
"brotliBytes": 40205,
"incrementalGzipBytes": 45609,
"incrementalBrotliBytes": 40205
},
"tanstack-scatter-advanced": {
"minifiedBytes": 126053,
"gzipBytes": 45476,
"brotliBytes": 40107,
"incrementalGzipBytes": 45476,
"incrementalBrotliBytes": 40107
"minifiedBytes": 126460,
"gzipBytes": 45616,
"brotliBytes": 40176,
"incrementalGzipBytes": 45616,
"incrementalBrotliBytes": 40176
},
"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": "e3eda18a6f6deb804ab9ac1585736b71976df83ad42289f58943aaa2e78e7bda",
"assets": [
{
"id": "01-line-gaps",
Expand Down
Loading