Skip to content

feat: add renderer-neutral pattern resources - #166

Open
greglum wants to merge 16 commits into
TanStack:mainfrom
greglum:greg/pattern-resources
Open

greglum wants to merge 16 commits into
TanStack:mainfrom
greglum:greg/pattern-resources

Conversation

@greglum

@greglum greglum commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Resolves API-FRICTION F-259 and covers the hatch and dot requests in #128.

What: charts can declare patterns next to gradients and use them as fill or stroke through url(#id). linePattern() and dotPattern() from @tanstack/charts/pattern build hatch and dot tiles whose colors accept CSS variables. Gradient and pattern IDs share the url(#id) namespace and must be unique; behavior on a collision is undefined. Animated patterns are not included.

Why: authors had to render their own <pattern> outside the chart, which broke export, Canvas, React Native and idPrefix scoping. Patterns also give charts a non-colour cue. We need one in a production app because our amber and red status fills look alike to readers with deuteranopia.

How: a pattern is plain data: tile size, optional angle, and ordinary scene nodes (nodes: SceneNode[]). The two factories are the vocabulary.

We chose nodes over a kind descriptor such as { type: 'lines' } on purpose. With a descriptor, the line and dot drawing code would live in three renderers and grow every default bundle. With nodes, each renderer reuses the node code it already has, the vocabulary tree-shakes away unless imported, and custom tiles need no new renderer code.

  • SVG emits a scoped userSpaceOnUse pattern and scopes resource paints inside tiles too, so a tile can use a gradient.
  • Canvas draws the tile at device resolution and repeats it with the same rotation, including on labels. Like gradients, Canvas patterns are rebuilt on each paint, so a theme switch re-resolves CSS variables. Active resource IDs stop direct and indirect cycles independently of raster resolution. Canvas bounds each tile to 4096 device pixels per side, total tile allocation to 16,777,216 pixels per paint, and dependency depth to 64; excess allocation or depth throws a RangeError.
  • React Native emits a Pattern (react-native-svg 15).
  • Standalone export inlines computed tile colors.
  • Embedded views reject child patterns, as they do gradients.

Proposed bundle cost (gzip):

Matched production builds compare upstream 5a34b7e14d85ff8c7b7248fedb2fd9731cacbd67 with pattern source ec695a8b993f4c22d4210df39b0d75c56ceab3a2, using Node 24.18.0, pnpm 11.15.1, and esbuild 0.27.7.

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

These increases and the corresponding ceilings/locked-baseline updates remain proposed, pending maintainer approval. Decimal kB uses 1000 bytes; source ceilings use KiB (1024 bytes). The existing upstream headroom is retained. Scene-only consumers add 7 to 8 bytes. The factories add 280 bytes (about 0.273 KiB) under a proposed 0.3 KiB incremental cap; the ordinary line consumer forbids their module.

Default path performance:

Both variants use matched production builds and identical scene fixtures in Chromium 151.0.7922.34. A trial creates one 960 by 540 scene with 1000 solid-color rectangles, each 6 by 6 at (i % 100 * 8, Math.floor(i / 100) * 8), filled with #2563eb, with no patterns. It renders that scene 100 times through surface.render(scene, { ariaLabel: 'Bench' }) at pixel ratio one.

Each of two fresh browser processes reuses its two variant contexts for three warmups and seven paired trials. Execution order alternates per trial; the second process reverses context creation order.

Browser repetition Context creation order Main median PR median Paired median PR minus main
1 Main, PR 51.8 ms 48.0 ms -0.7 ms
2 PR, main 48.7 ms 47.9 ms -0.1 ms

No consistent default-path slowdown was reproduced in this bounded fixture. Solid-color paints return before the pattern lookup; the painter still creates its per-paint pattern cache and budget state. These timings did not justify a runtime optimization. Other workloads and devices need their own measurements.

Verified:

  • Unit tests cover SVG with idPrefix (including a tile that uses a gradient), the Canvas paint path for fill, stroke and text, CSS-variable resolution, export inlining, and React Native markup.
  • Earlier pnpm canvas:check verification compared Canvas output for rotated hatch and dot tiles against rasterized exported SVG: mean difference about 1 in 255 per channel, with zero divergent pixels. Reversing the Canvas rotation makes it fail.
  • Matched complete bundle entries were rebuilt for both variants. pnpm run validate passes all 19 targets at ad427019503aa65c469bc047b998ecc015dbf0ea, including 2233 unit tests, strict packed consumers, all seven framework adapters, types, docs, bundles, comparison and catalog checks.
  • On-device React Native rendering and browser comparison/stress/conformance matrices were not rerun in this follow-up. Package validation uses an isolated pinned Corepack home so temporary consumers select pnpm 11.15.1; the prior React 18 peer error came from selecting pnpm 12.6.0. Strict peer checks remain enabled.

Open question: do you want a duplicate resource ID check at scene compilation? It would cover gradient–gradient and gradient–pattern collisions. The measured cost is about 60 B gzip on every default bundle, and it would newly reject duplicate gradient IDs that work today, so we left it out.

Summary by CodeRabbit

  • New Features
    • Add repeating line and dot patterns for chart fills and strokes, with options for color, spacing, angle, and background.
    • Render patterns in SVG, Canvas, standalone exports, and React Native. Pattern colors support CSS variables.
  • Bug Fixes
    • Scope pattern IDs to each chart to help prevent collisions.
    • Guard Canvas rendering against oversized pattern tiles and excessive pattern dependencies.
  • Documentation
    • Add guidance on creating, styling, and rendering patterns, including their static behavior.

Declare repeating tiles in a chart's patterns list and reference them
from fill or stroke as url(#id). SVG emits scoped userSpaceOnUse
patterns, Canvas rasterizes the tile at device resolution and repeats it
with the same rotation, and React Native emits react-native-svg Pattern.
linePattern() and dotPattern() at @tanstack/charts/pattern build hatch
and dot tiles with CSS-variable paint.
Pass the SVG paint hooks into pattern tiles so a tile that paints with a
gradient gets the scoped id. Reject gradient and pattern ids that collide,
since SVG emits both while Canvas picks one. Make ChartScene.patterns
required like gradients while renderers still tolerate untyped scenes
without it.
Keep ChartScene.patterns optional and leave gradient and pattern id
collisions undefined, as documented in the reference. The check cost
about 60 gzip bytes in every default bundle and newly rejected duplicate
gradient ids. Restore bundle budgets and baselines to measured values.
@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 25433c2a-32ef-43f0-92c5-d0c75e7b453e
📥 Commits

Reviewing files that changed from the base of the PR and between c2c0ca1 and 281c0f3.

📒 Files selected for processing: 23
  • .changeset/add-pattern-resources.md
  • API-FRICTION.md
  • benchmarks/bundle-size/README.md
  • benchmarks/bundle-size/universal-baseline.json
  • benchmarks/comparison/bundle-baseline.json
  • benchmarks/conformance/previews/manifest.json
  • docs/comparison.md
  • docs/reference/rendering-and-export.md
  • docs/reference/scales-guides-and-color.md
  • docs/reference/types.md
  • llms.txt
  • packages/charts-core/docs/comparison.md
  • packages/charts-core/docs/reference/rendering-and-export.md
  • packages/charts-core/docs/reference/scales-guides-and-color.md
  • packages/charts-core/docs/reference/types.md
  • packages/charts-core/llms.txt
  • packages/charts-core/src/canvas.test.ts
  • packages/charts-core/src/canvas.ts
  • packages/charts-core/src/index.ts
  • packages/charts-core/src/scene.ts
  • packages/charts-core/src/types.ts
  • packages/charts-core/src/universal-types.ts
  • scripts/measure-bundles.mjs
🚧 Files skipped from review as they are similar to previous changes: 7
  • docs/comparison.md
  • packages/charts-core/docs/comparison.md
  • benchmarks/bundle-size/README.md
  • docs/reference/types.md
  • .changeset/add-pattern-resources.md
  • API-FRICTION.md
  • docs/reference/scales-guides-and-color.md

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 0 remain after this review.

📝 Walkthrough

The change adds repeating pattern resources to chart definitions and scenes. It adds line and dot pattern builders and rendering support for SVG, Canvas, React Native, and standalone SVG export. It also updates examples, documentation, tests, and bundle measurements.

Changes: Chart pattern resources

Layer / File(s) Summary
Pattern contract and scene wiring
packages/charts-core/src/types.ts, packages/charts-core/src/pattern.ts, packages/charts-core/src/scene.ts, packages/charts-core/src/index.ts, packages/charts-core/src/universal.ts, packages/charts-core/src/universal-types.ts, packages/charts-core/src/view.ts, packages/charts-core/package.json, packages/charts-core/src/exports.test.ts, docs/reference/*, packages/charts-core/docs/reference/*, docs/guides/themes-and-styling.md, packages/charts-core/docs/guides/themes-and-styling.md, docs/examples/themes-and-motion.md, packages/charts-core/docs/examples/themes-and-motion.md, llms.txt, packages/charts-core/llms.txt, .changeset/add-pattern-resources.md, API-FRICTION.md
Chart specs and scenes now support pattern resources. The linePattern() and dotPattern() builders and their types are exported. Documentation describes pattern configuration and use. Embedded static child definitions reject nonempty patterns.
SVG and React Native rendering
packages/charts-core/src/svg.ts, packages/charts-core/src/svg-renderer.test.ts, packages/react-native-charts/src/SvgScene.tsx, packages/react-native-charts/src/SvgScene.test.tsx, packages/charts-core/src/export.test.ts, docs/reference/rendering-and-export.md, packages/charts-core/docs/reference/rendering-and-export.md, docs/guides/custom-marks-and-renderers.md, packages/charts-core/docs/guides/custom-marks-and-renderers.md
SVG and React Native output now emits pattern definitions and scopes matching paint references. SVG tests cover pattern serialization and nested resource references. Standalone export tests cover computed tile paints and prefixed IDs.
Canvas pattern rasterization
packages/charts-core/src/canvas.ts, packages/charts-core/src/canvas.test.ts, scripts/check-canvas-renderer.mjs
Canvas resolves pattern fills and strokes, rasterizes and caches tiles, and applies scale, rotation, and origin transforms. Tests cover tiles, labels, and recursive references. The browser check compares Canvas pattern pixels with rasterized SVG output.
Pattern examples and bundle measurements
benchmarks/entries/charts-pattern-svg.ts, scripts/measure-bundles.mjs, benchmarks/bundle-size/*, benchmarks/comparison/bundle-baseline.json, benchmarks/conformance/previews/manifest.json, docs/comparison.md, packages/charts-core/docs/comparison.md, docs/guides/bundle-size-and-performance.md, packages/charts-core/docs/guides/bundle-size-and-performance.md
A benchmark entry renders patterned SVG. Bundle measurement rules add a pattern-specific entry and update gzip limits. Bundle baselines, comparison records, and documented measurements are refreshed.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant ChartDefinition
  participant SceneBuilder
  participant SVGRenderer
  participant CanvasRenderer
  participant ReactNativeRenderer
  ChartDefinition->>SceneBuilder: pass pattern definitions
  SceneBuilder->>SVGRenderer: provide scene patterns
  SceneBuilder->>CanvasRenderer: provide scene patterns
  SceneBuilder->>ReactNativeRenderer: provide scene patterns
  SVGRenderer->>SVGRenderer: emit pattern definitions and resolve paint references
  CanvasRenderer->>CanvasRenderer: rasterize tiles and resolve pattern paints
  ReactNativeRenderer->>ReactNativeRenderer: emit Pattern elements and resolve paint references
Loading

Suggested reviewers: tannerlinsley

Merge Risk: ⚪ Minimal · up to 281c0

No actionable issue is established that should delay merging after normal checks.

Security architecture risk: 🟡 Moderate · up to c2c0c

The new Canvas rendering path can amplify circular pattern references or oversized tiles into excessive allocation and rendering work. Security exposure depends on who can supply pattern definitions. Existing text escaping, resource scoping, and drawing-state restoration provide useful controls but do not bound this new resource-consumption path.

Retained concerns

  • Medium · security · inferred: Canvas pattern dependency cycles can evade the size-dependent recursion sentinel as nested rasterization changes resolution. Combined with unbounded tile allocation, this creates a potential availability attack when a consumer accepts externally influenced pattern definitions; small cyclic declarations can trigger repeated, progressively larger allocations.
Security review details

Security Blast Radius

  • inferred — The identified mechanism affects the rendering page or process and potentially colocated charts through synchronous work and memory pressure. It requires influence over pattern declarations; evidence does not establish that ordinary chart data alone grants this capability or demonstrate backend, credential, or cross-tenant compromise.

Security Findings and Attack Paths

  • inferred — A consumer accepting externally influenced pattern definitions could receive two tiles whose paints reference each other. Resolution-dependent cache keys allow reentry into an already active resource under a different key, driving repeated allocation. Oversized finite tile dimensions also reach allocation without a pixel budget. Browser failure thresholds and an actual external ingress path were not verified.

Trust Boundaries and Controls

  • observed — SVG pattern IDs and paint attributes use existing attribute escaping, and label text uses text escaping. Pattern children pass through the existing explicit scene-node renderer, including its group clipping hook, rather than an arbitrary markup insertion path.
  • observed — Known gradient and pattern references are rewritten with the supplied resource prefix in SVG and React Native. Unrecognized paints continue through the existing handling. Prefixing is an identity convention, not evidence of authorization or external-URL rejection; no newly introduced cross-chart authority gain was demonstrated.

Resilience and Maintainability Implications

  • observed — The null cache sentinel contains direct self-reference when derived dimensions remain unchanged, and a supplied test exercises that case. Temporary tile contexts and per-node restoration contain ordinary drawing-state mutation, but these controls do not establish containment for indirect cycles with changing dimensions.

Hardening Proposals

  • proposed — Separate active dependency identity from the resolution-keyed result cache, remove active entries in finally, and define per-paint limits for device-pixel tile dimensions, aggregate allocation, and dependency depth. Consumers accepting untrusted definitions should validate them before rendering.
Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 13.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 30 functions across 18 files. (16 skipped… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: adding renderer-neutral pattern resources across the chart renderers.
Full details: Docstring Coverage

Explanation

Docstring coverage is 13.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 30 functions across 18 files. (16 skipped: 16 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@tannerlinsley

Copy link
Copy Markdown
Member

Thanks @greglum. I checked 281c0f31779ffbf9fb26f800f994babc6945b5cd in an isolated Chromium context. The fractional two-pattern cycle stops, a 65-resource chain and a 4097-pixel tile side throw the expected RangeError, and changing a CSS variable repaints the tile. Nine SVG/Canvas comparisons, including fractional rectangular tiles and 0/30/90-degree rotations, stayed within the proposed pixel tolerance.

Two things still need attention before we can merge:

  1. The size summary in the PR body and F-259 still says roughly 0.320 kB gzip for Canvas, but the current implementation costs more. My paired builds against current main show +0.516 kB for the Canvas host, +0.646 kB for representative marks with Canvas, and +0.620 kB for the React line/Canvas fixture. Static SVG is +0.119 kB, DOM +0.124 kB, and compact React +0.136 kB. These are decimal kB. The budget increases are not approved yet, so please keep them visible as proposed increases and update the stale summary.

  2. Please include default-path performance measurements. For 100 renders of a 960x540 scene with 1,000 solid-color rectangles and no patterns, seven paired trials after three warmups gave median times of 44.0 ms on main versus 46.7 ms with this PR. A fresh-browser repeat gave 46.3 versus 49.0 ms. Reversing browser-context creation order narrowed that to 48.1 versus 48.7 ms, so this is a signal to investigate, not proof of a consistent regression. The smaller 24/300-rectangle cases were mixed. We need confidence that the default path does not pay a rendering penalty for an unused feature.

The benchmark creates one scene per trial, uses pixelRatio: 1, alternates baseline/PR execution order, and calls surface.render(scene, { ariaLabel: 'Bench' }) 100 times. Rectangles are 6x6, placed at (i % 100 * 8, Math.floor(i / 100) * 8), with fill: '#2563eb'. Both variants run in fresh contexts in the same Chromium process, with the same production source except the reviewed pattern changes.

Full repository checks, the complete bundle matrix, and on-device React Native rendering are still pending. This does not approve the budget changes or mark the PR ready to merge.

@changeset-bot

changeset-bot Bot commented Oct 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ad42701

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 12 packages
Name Type
@tanstack/charts Minor
@tanstack/react-native-charts Minor
@tanstack/alpine-charts Minor
@tanstack/angular-charts Minor
@tanstack/lit-charts Minor
@tanstack/octane-charts Minor
@tanstack/preact-charts Minor
@tanstack/react-charts Minor
@tanstack/solid-charts Minor
@tanstack/svelte-charts Minor
@tanstack/vue-charts Minor
@tanstack/charts-scales Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@tannerlinsley

tannerlinsley commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Thanks @greglum, full isolated validation now passes 2,233 DOM tests, all six additional framework targets, TypeScript, docs and the full packed-package gate, including bare React Native, Expo and seven adapters. Bundle checks pass against the PR’s proposed limits, which is not approval of those increases.

The final packed artifact exposed one additional browser difference worth resolving before claiming fractional SVG/Canvas parity. With pixelRatio 1, a 96 × 96 scene, and a repeating 1.1 × 1.3 tile containing a white background and a black top half, WebKit paints visible Canvas stripes while the rasterized SVG is nearly solid gray. The 80 × 80 interior comparison has mean maximum-channel delta 78.825 and 22.5% of pixels above delta 96. A 6 × 6 integer tile matches exactly. The same fractional fixture passes in Chromium and Firefox.

The SVG reference has explicit 96 × 96 dimensions, not percentage intrinsic sizing. I repeated the packed check and inspected the rendered comparison. Cycle, 65-level dependency and 4097-pixel side guards, plus CSS-variable repainting, pass in all three engines. This is a rendering-parity finding, not evidence of a new security problem. It needs a focused regression and a decision about consistent fractional sampling, without just widening the pixel tolerance.

Update: the broader packed pattern/export fixture passes in all three engines. Its initial WebKit SVG-decoding failure came from the harness blocking local blob images. Allowing only local blob/data images, while continuing to block network requests, resolved it. The fractional-tile mismatch above remains reproducible. Existing budget, default-path performance and required-CI holds remain.

Native-browser isolation: the same 1.1 × 1.3 mismatch reproduces using only CanvasPattern and SVG, with no Charts code. Smoothing on/off does not change WebKit’s result, and increasing tile resolution is not a consistent fix across engines. An independent area-coverage calculation also shows that rasterized SVG is not an exact reference for these subpixel stripes. Repeating multiple tiles over an integer-sized period improves coverage accuracy but still does not match native SVG, so I have not adopted that as a renderer fix or relaxed the test threshold. This narrows the finding to native sampling differences; it does not establish a Charts geometry bug.

Verified head: ad427019503aa65c469bc047b998ecc015dbf0ea.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants