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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 53 additions & 2 deletions .github/workflows/chart-library-benchmarks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,17 @@ jobs:
- run: pnpm benchmark:check
- run: pnpm catalog:check
- run: pnpm catalog:build
- run: pnpm catalog:deploy:check
- run: git diff --check

- name: Upload production catalog
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: actions/upload-artifact@v4
with:
name: charts-catalog-${{ github.sha }}
path: examples/conformance/dist
if-no-files-found: error

compare:
needs: validate
runs-on: ubuntu-latest
Expand Down Expand Up @@ -72,7 +81,7 @@ jobs:
conformance:
needs: validate
runs-on: ubuntu-latest
timeout-minutes: 30
timeout-minutes: 45

steps:
- uses: actions/checkout@v4
Expand All @@ -85,7 +94,12 @@ jobs:
- run: corepack enable
- run: pnpm install --frozen-lockfile
- run: pnpm exec playwright install --with-deps chromium
- run: pnpm conformance:quick
- name: Run pull-request conformance
if: github.event_name == 'pull_request'
run: pnpm conformance:quick
- name: Run production conformance
if: github.event_name != 'pull_request'
run: pnpm conformance
- run: cat .benchmark-output/conformance/results/plot-catalog.md >> "$GITHUB_STEP_SUMMARY"

- name: Upload conformance
Expand Down Expand Up @@ -136,3 +150,40 @@ jobs:
name: chart-library-stress-${{ github.run_id }}
path: .benchmark-output/stress/results
if-no-files-found: error

deploy-catalog:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs:
- validate
- conformance
runs-on: ubuntu-latest
timeout-minutes: 10
environment:
name: charts-catalog-production
url: https://tanstack.com/charts/catalog/

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 24

- name: Download validated catalog
uses: actions/download-artifact@v4
with:
name: charts-catalog-${{ github.sha }}
path: examples/conformance/dist

- name: Stage catalog below its public path
run: node scripts/stage-conformance-deployment.mjs

- name: Deploy catalog Worker
uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3.15.0
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: deploy --config wrangler.catalog.jsonc
wranglerVersion: 4.103.0

- name: Verify production routes
run: node scripts/check-catalog-deployment.mjs
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ node_modules
dist
.bundle-output
.benchmark-output
.catalog-deploy
.catalog-worker
.wrangler
coverage
*.log
tanstack.com-parity/
Expand Down
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@
- Author public documentation only in the root `docs/` tree. The
`packages/charts-core/docs` tree and both `llms.txt` files are generated by
`pnpm docs:sync`; never edit those generated copies directly.
- In Codex on macOS, run Playwright/Chromium commands with escalated sandbox
permissions on the first attempt. The restricted sandbox blocks Chromium's
Mach bootstrap services; do not probe browser launches inside it. This
includes `benchmark`, `benchmark:stress:*`, and browser-backed `conformance`.
235 changes: 219 additions & 16 deletions API-FRICTION.md

Large diffs are not rendered by default.

222 changes: 162 additions & 60 deletions MARKETING.md

Large diffs are not rendered by default.

30 changes: 22 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ or dropping down to a separate API.
- **Build from common to custom.** Layer built-in marks or implement a custom
mark against the same public scene protocol.
- **Get the application runtime too.** Responsive layout, automatic guide
margins, themes, interaction, animation, accessibility, SVG SSR, hydration,
and export are part of the system.
margins, themes, interaction, animation, accessibility, SVG SSR, opt-in
Canvas painting, hydration, and export are part of the system.
- **Pay for what you import.** Marks, renderers, and chart-owned interactions
have independent TanStack subpaths; specialized algorithms come directly
from granular, tree-shakeable `d3-*` packages.
Expand Down Expand Up @@ -116,15 +116,29 @@ assigns their responsive pixel ranges, compiles a renderer-neutral keyed scene,
and hands that scene to the selected host.

Definitions are framework-independent. The same `revenueChart` can render
through React, Octane, the vanilla DOM host, or static SVG.
through React, Octane, the vanilla DOM host, static SVG, or the optional Canvas
renderer.

When SVG element count becomes the bottleneck, switch the adapter import and
keep the definition and interaction props:

```tsx
import { Chart } from '@tanstack/react-charts/canvas'
```

Canvas stays outside the default bundles. React and Octane also expose a
renderer-neutral `/core` entry when an application owns the surface. Canvas
removes per-mark DOM cost, not scene memory or dense nearest-point work, so
large interactive charts should still use a measured spatial index or a
bounded representation.

## Packages

| Package | Role |
| ----------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| [`@tanstack/charts`](./packages/charts-core) | Marks, channels, guides, scene compilation, static SVG, vanilla DOM lifecycle, and optional export |
| [`@tanstack/react-charts`](./packages/react-charts) | Thin React lifecycle adapter with SSR and hydration |
| [`@tanstack/octane-charts`](./packages/octane-charts) | Thin Octane lifecycle adapter with equivalent scene output |
| Package | Role |
| ----------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| [`@tanstack/charts`](./packages/charts-core) | Marks, channels, guides, scene compilation, SVG, optional Canvas, vanilla DOM lifecycle, and export |
| [`@tanstack/react-charts`](./packages/react-charts) | Thin React lifecycle adapter with SSR and hydration |
| [`@tanstack/octane-charts`](./packages/octane-charts) | Thin Octane lifecycle adapter with equivalent scene output |

The earlier host experiment remains under `@plot-poc/*` for migration evidence
and benchmark comparison. The private
Expand Down
7 changes: 7 additions & 0 deletions benchmarks/bundle-size/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ Every package entry in
New marks and optional capabilities need an isolated budgeted entry. They must
leave the locked entries byte-identical.

Non-cartesian capability subpaths follow the same policy. Polar and geographic
entries measure complete scenes through static SVG: a minimal arc, D3 pie,
full scale-backed gauge composition, a scale-backed polar line and scatter
composition, and projected GeoJSON. Atlas datasets remain application-owned
inputs rather than chart-package dependencies. The locked Cartesian entries
prove those D3 geometry modules remain opt-in.

The advanced custom-mark scale-value factory follows the same rule: its
subpath has a dedicated budget, while ordinary chart entries must retain zero
bytes from it.
Expand Down
32 changes: 16 additions & 16 deletions benchmarks/bundle-size/universal-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,36 @@
"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": 31163,
"gzip": 12478
"bytes": 31200,
"gzip": 12492
},
"D3-scale line + static SVG": {
"bytes": 34215,
"gzip": 13613
"bytes": 34252,
"gzip": 13624
},
"Representative marks": {
"bytes": 42525,
"gzip": 15876
"bytes": 42562,
"gzip": 15889
},
"TanStack DOM host": {
"bytes": 25595,
"gzip": 9952
"bytes": 28362,
"gzip": 10896
},
"React adapter": {
"bytes": 27600,
"gzip": 10595
"bytes": 30011,
"gzip": 11437
},
"React line consumer": {
"bytes": 49157,
"gzip": 19097
"bytes": 51559,
"gzip": 19955
},
"Custom-scale line scene": {
"bytes": 11937,
"gzip": 4795
"bytes": 11974,
"gzip": 4810
},
"D3 linear-scale line scene": {
"bytes": 31095,
"gzip": 12443
"bytes": 31132,
"gzip": 12458
}
}
}
3 changes: 3 additions & 0 deletions benchmarks/comparison/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ scatter charts.
## Commands

```sh
# Install the Playwright version's pinned headless browser
pnpm browser:install

# Bundle size and the standard browser matrix
pnpm benchmark

Expand Down
86 changes: 69 additions & 17 deletions benchmarks/conformance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ harness.
## Commands

```sh
# Install the Playwright version's pinned headless browser
pnpm browser:install

# Fast bundle, timing, visual, and type audit
pnpm conformance:quick

Expand Down Expand Up @@ -146,6 +149,42 @@ Adding or changing a case updates every catalog surface automatically.
invalid schemas, duplicate IDs or orders, and case IDs that drift from their
directory names.

## Production hosting

The catalog is deployed from this repository as the separate Cloudflare Static
Assets Worker `tanstack-charts-catalog`. Its
`tanstack.com/charts/catalog*` route takes precedence over the main
`tanstack-com` Worker and includes query strings on the bare catalog path. No
catalog source or generated asset is copied into the `tanstack.com`
repository.

Workers Static Assets matches the complete request path, so `catalog:stage`
mirrors the production build below `.catalog-deploy/charts/catalog/` before
deployment. The ignored staging directory is never committed. Fingerprinted
assets receive immutable caching; HTML and metadata retain Cloudflare's
revalidating default. Catalog pages deny framing, while the explicit embed
routes remain frameable.

```sh
# Build, mirror the public path, and validate the Worker bundle locally
pnpm catalog:build
pnpm catalog:deploy:check

# Deploy with an authenticated Wrangler session
pnpm catalog:deploy

# Verify root, detail, embed, asset, metadata, and 404 behavior
pnpm catalog:smoke
```

Main-branch CI deploys only after validation and the unfiltered standard
conformance matrix. The `charts-catalog-production` GitHub environment must
provide a `CLOUDFLARE_API_TOKEN` scoped to the catalog Worker and the
`tanstack.com` route; the non-secret account ID stays in the Wrangler config.
Rollbacks use the prior
`tanstack-charts-catalog` Worker version; the catalog has no mutable runtime
state.

## What is and is not equivalent

Both implementations must satisfy the same data and semantic contract. They do
Expand Down Expand Up @@ -197,8 +236,8 @@ comparison.

## Current scope

The executable corpus contains 79 paired cases: 61 sourced from Observable
Plot, nine from Recharts, and nine from Apache ECharts. It spans the common
The executable corpus contains 100 paired cases: 68 sourced from Observable
Plot, 21 from Recharts, and 11 from Apache ECharts. It spans the common
cartesian vocabulary plus the high-value catalog beyond it:

- lines, areas, bars, intervals, heatmaps, histograms, facets, and framed
Expand All @@ -209,7 +248,13 @@ cartesian vocabulary plus the high-value catalog beyond it:
rankings, indexed lines, marginal distributions, ridgelines, violins,
Marimekko layouts, and waffles;
- pointer, grouped, and Voronoi-nearest tooltips;
- pie, labeled pie, basic/centered/rounded/nested donuts, partial and needle
gauges, single/comparative radar, numeric polar line/scatter, rose, radial
bars, and sunburst layouts;
- trees, Delaunay links, force networks, vector fields, and GeoJSON maps.
- regional and world choropleths, proportional symbols, orthographic globe
and graticule layers, projected routes, 177 real country boundaries, 51 US
state/DC boundaries, and a four-projection atlas gallery.

Shared facet guides, guide suppression, Plot-style tooltip mapping, exact
difference crossings, and responsive fixed-pixel spatial preparation are
Expand All @@ -218,22 +263,29 @@ merely because it renders.

The Recharts tranche adds mixed composition, population pyramids,
adjacent-plus-stacked bars, a 1,000-point scatter pressure test, treemaps,
radar, an accessible interactive legend, chart/table selection, and a pinned
tooltip containing a real nested chart.

The ECharts tranche adds a snapped grouped axis pointer, native horizontal
resource scrolling, streaming-window preservation, synchronized multi-view
cursors, a free two-dimensional cursor, continuous brush selection, wheel
zoom and pan, timeline scrubbing, and editable event ranges. Focus-plus-context
uses Plot as its reference. Together with the original tooltip cases, 16 cases
carry executable interaction scenarios.

All declared interaction scenarios and the full 79-case standard visual
pie and donut variants, partial and needle gauges, single and comparative
radar, radial bars, rose, sunburst, an accessible interactive legend,
chart/table selection, and a pinned tooltip containing a real nested chart.

The ECharts tranche adds numeric polar line/scatter, a snapped grouped axis
pointer, native horizontal resource scrolling, streaming-window preservation,
synchronized multi-view cursors, a free two-dimensional cursor, continuous
brush selection, wheel zoom and pan, timeline scrubbing, and editable event
ranges. Focus-plus-context uses Plot as its reference. Together with the
original tooltip cases, 16 cases carry executable interaction scenarios.

All declared interaction scenarios and the prior 79-case full-corpus visual
matrix pass across both renderers, initial/revised data, 320/640/960 px, and
light/dark themes. That result proves the declared contracts, not production
interaction quality; the interaction UX audit preserves the original gaps and
their implementation follow-through. The full run covers all standard widths
and themes. The final quick verification measures 0.21× the
light/dark themes. The nine added polar cases pass their focused standard
matrix with 100.0% mean frame-relative geometry similarity; all four added
geographic cases pass the same matrix at 99.9% each. Those results prove the
declared contracts, not production interaction quality. The numeric polar
line/scatter additions pass their focused standard matrix at 100.0%; the
country and state atlas cases pass at 99.9%, and the four-pane projection
gallery passes at 99.8%, for 99.9% across the five-case expansion. The
interaction UX audit preserves the original gaps and their implementation
follow-through.
The prior full-corpus baseline measures 0.21× the
selected-reference gzip, 0.57× mount time, 0.66× update time, and 97.8% mean
diagnostic geometry similarity. The transitive authored-source ratio is 1.07×.
Strict case sources have zero diagnostics, unsafe assertions, or suppressions.
Expand Down
13 changes: 13 additions & 0 deletions benchmarks/conformance/cases/03-temperature-range-band/tanstack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,17 @@ const definition = defineChart<ConformanceInput>()(({ input }) => {
export const mount: ConformanceMount = tanstackMount(
definition,
'Weekly low-to-high temperature range',
{
format: ({ datum }) =>
`${datum.date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
})} · ${datum.low.toLocaleString('en-US', {
maximumFractionDigits: 1,
})}–${datum.high.toLocaleString('en-US', {
maximumFractionDigits: 1,
})} °F`,
},
)
11 changes: 11 additions & 0 deletions benchmarks/conformance/cases/04-stacked-time-area/tanstack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ const definition = defineChart<ConformanceInput>()(({ input }) => {
export const mount: ConformanceMount = tanstackMount(
definition,
'Three stacked time-series areas',
{
format: ({ datum }) =>
`${datum.series} · ${datum.date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
})} · ${datum.value.toLocaleString('en-US', {
maximumFractionDigits: 1,
})} index points`,
},
)

function stackRows(rows: readonly TimePoint[]): readonly StackedTimePoint[] {
Expand Down
Loading
Loading