From 56f5f402007ff1a542ebc9283d26051327daac98 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Thu, 27 Aug 2026 11:51:02 -0600 Subject: [PATCH] Enhance Charts landing page --- scripts/charts-landing/bundle-size-chart.ts | 86 +++++++ scripts/generate-charts-landing-svg.ts | 37 +++ .../landing/ChartsCatalogGallery.tsx | 14 +- src/components/landing/ChartsLanding.tsx | 224 ++++++++++++++++-- .../landing/ChartsLandingGraphics.tsx | 25 ++ src/components/landing/chartsBundleSizeSvg.ts | 7 + tests/charts-landing-ssr.test.ts | 84 +++++-- 7 files changed, 420 insertions(+), 57 deletions(-) create mode 100644 scripts/charts-landing/bundle-size-chart.ts create mode 100644 src/components/landing/chartsBundleSizeSvg.ts diff --git a/scripts/charts-landing/bundle-size-chart.ts b/scripts/charts-landing/bundle-size-chart.ts new file mode 100644 index 000000000..a52df7082 --- /dev/null +++ b/scripts/charts-landing/bundle-size-chart.ts @@ -0,0 +1,86 @@ +import { scaleBand } from '@tanstack/charts/scales/band' +import { scaleLinear } from '@tanstack/charts/scales/linear' +import { barX, defineChart, text } from '@tanstack/charts' + +export const bundleSizeSnapshot = [ + { library: 'TanStack Charts', size: 40 }, + { library: 'uPlot', size: 22 }, + { library: 'Chart.js', size: 46 }, + { library: 'visx', size: 49 }, + { library: 'Lightweight Charts', size: 60 }, + { library: 'Observable Plot', size: 85 }, + { library: 'Vega-Lite', size: 87 }, + { library: 'D3', size: 90 }, + { library: 'Recharts', size: 97 }, + { library: 'Highcharts', size: 100 }, + { library: 'Victory', size: 105 }, + { library: 'Nivo', size: 143 }, + { library: 'Apache ECharts', size: 157 }, + { library: 'ApexCharts', size: 164 }, + { library: 'Plotly.js partial', size: 250 }, + { library: 'AG Charts', size: 367 }, +] as const + +const libraryNames = bundleSizeSnapshot.map((row) => row.library) + +export function bundleSizeChart(compact = false) { + return defineChart({ + marks: [ + barX(bundleSizeSnapshot, { + id: 'bundle-bars', + x: 'size', + y: 'library', + key: 'library', + fill: (row) => + row.library === 'TanStack Charts' ? '#61e8ff' : '#315464', + inset: compact ? 3 : 4, + maxThickness: compact ? 25 : 28, + radius: 6, + }), + text(bundleSizeSnapshot, { + id: 'bundle-values', + x: 'size', + y: 'library', + text: (row) => `${row.size} kB`, + key: 'library', + fill: (row) => + row.library === 'TanStack Charts' ? '#f7ffcc' : '#d6e6eb', + fontSize: compact ? 15 : 14, + fontWeight: 700, + anchor: 'start', + dx: compact ? 8 : 10, + }), + ], + scales: { + x: { + scale: scaleLinear().domain([0, 425]), + axis: { + label: 'Minified + gzip (kB)', + ticks: { count: compact ? 3 : 5 }, + tickLabels: { fontSize: compact ? 13 : 12 }, + }, + grid: true, + }, + y: { + scale: scaleBand().domain(libraryNames).padding(0.12), + axis: { + line: false, + ticks: { padding: compact ? 8 : 12, size: 0 }, + tickLabels: { + fontSize: compact ? 15 : 14, + fontWeight: ({ value }) => + value === 'TanStack Charts' ? 800 : 500, + opacity: ({ value }) => (value === 'TanStack Charts' ? 1 : 0.76), + }, + }, + }, + }, + theme: { + background: 'transparent', + foreground: '#eef8f7', + grid: '#24404c', + muted: '#91aab3', + palette: ['#61e8ff'], + }, + }) +} diff --git a/scripts/generate-charts-landing-svg.ts b/scripts/generate-charts-landing-svg.ts index 6d48dc89c..87ca7933c 100644 --- a/scripts/generate-charts-landing-svg.ts +++ b/scripts/generate-charts-landing-svg.ts @@ -14,6 +14,10 @@ import { renderChartSvg, } from '@tanstack/charts' import { activationChart } from './charts-landing/activation-chart' +import { + bundleSizeChart, + bundleSizeSnapshot, +} from './charts-landing/bundle-size-chart' import { kineticBarChart } from './charts-landing/kinetic-bar-chart' import { kineticDonutChart } from './charts-landing/kinetic-donut-chart' import { kineticHeatmapChart } from './charts-landing/kinetic-heatmap-chart' @@ -33,11 +37,18 @@ const kineticOutputFile = resolve( process.cwd(), 'src/components/landing/chartsKineticSvg.ts', ) +const bundleSizeOutputFile = resolve( + process.cwd(), + 'src/components/landing/chartsBundleSizeSvg.ts', +) const checkOnly = process.argv.includes('--check') const activationAriaLabel = 'Weekly activation rate with expected range, 70 percent goal, and two product release events' const activationAriaDescription = 'Illustrative weekly activation data from January through May 2026. The actual rate rises from 48 to 78 percent, compared with an expected range and a 70 percent goal. Onboarding v2 and Invite flow are marked as release events.' +const bundleSizeAriaDescription = `A static August 2026 snapshot of minified and gzip-compressed browser bundle sizes. ${bundleSizeSnapshot + .map((row) => `${row.library}: ${row.size} kilobytes`) + .join('; ')}.` const themeBaseline = 40 const themeDomain: [number, number] = [40, 105] @@ -258,6 +269,25 @@ const activationCharts = { ), } +const bundleSizeCharts = { + chartsBundleSizeSvg: render( + bundleSizeChart(), + 'Bundle size comparison across 16 charting libraries', + 1200, + 820, + bundleSizeAriaDescription, + -1, + ), + chartsBundleSizeCompactSvg: render( + bundleSizeChart(true), + 'Bundle size comparison across 16 charting libraries', + 520, + 820, + bundleSizeAriaDescription, + -1, + ), +} + const kineticCharts = { chartsKineticBarSvg: render( kineticBarChart, @@ -322,6 +352,13 @@ const generatedModules = [ 'scripts/charts-landing/kinetic-*-chart.ts', ), }, + { + file: bundleSizeOutputFile, + source: createGeneratedModule( + bundleSizeCharts, + 'scripts/charts-landing/bundle-size-chart.ts', + ), + }, ] if (checkOnly) { diff --git a/src/components/landing/ChartsCatalogGallery.tsx b/src/components/landing/ChartsCatalogGallery.tsx index f374a0bce..87f7a2c7a 100644 --- a/src/components/landing/ChartsCatalogGallery.tsx +++ b/src/components/landing/ChartsCatalogGallery.tsx @@ -9,9 +9,9 @@ type ChartsLandingCatalog = Awaited> type CatalogCase = ChartsLandingCatalog['cases'][number] export const chartsLandingHeroCaseIds = [ - '03-temperature-range-band', - 'bar-grouped', - 'scatter-bubble', + '70-composed-chart', + '101-sunburst', + '127-shadcn-dashboard', ] as const export function CatalogChartsHero({ @@ -122,6 +122,10 @@ export function ChartsCatalogGallery({ ) } +function compareCatalogCases(left: CatalogCase, right: CatalogCase) { + return left.order - right.order +} + function CatalogChartCard({ catalogCase, revision, @@ -159,7 +163,3 @@ function CatalogChartCard({ ) } - -function compareCatalogCases(left: CatalogCase, right: CatalogCase) { - return left.order - right.order -} diff --git a/src/components/landing/ChartsLanding.tsx b/src/components/landing/ChartsLanding.tsx index 1c4750aba..b9b4cc27e 100644 --- a/src/components/landing/ChartsLanding.tsx +++ b/src/components/landing/ChartsLanding.tsx @@ -1,3 +1,5 @@ +import { ArrowRightIcon } from '@phosphor-icons/react/ArrowRight' + import { CodeBlock } from '~/components/markdown/CodeBlock' import { charts } from '~/libraries' import type { getChartsCatalogLanding } from '~/utils/charts-catalog.functions' @@ -7,6 +9,7 @@ import { LandingSection, LibraryLandingShell } from './LibraryLanding' import { AccountChart, ActivationChart, + BundleSizeChart, ThemeGallery, } from './ChartsLandingGraphics' import { CatalogChartsHero, ChartsCatalogGallery } from './ChartsCatalogGallery' @@ -36,25 +39,65 @@ export default function ChartsLanding({ tone="raised" className="py-12 lg:py-14" > -
-

- All mark, no chart. +
+

+ Your next chart, already working.

+
+ +
+

+ A lot of chart. Not a lot of bundle. +

+

+ TanStack Charts starts at 40 kB minified and gzip compressed in the + current matched suite, with its SVG renderer, scales, axes, and + styles included. +

+
+ +
+ +
+ + + See the full bundle comparison + +
+ -
-
+
+

- Type Safe & Declarative for both Humans & Agents + Types stay connected to the source row.

- Every example compiles under strict TypeScript. Fields, datum - types, inferred domains and keys, tooltips, and focus callbacks - stay connected to the source datum; the type suite rejects invalid - definitions. + Fields, datum types, inferred domains and keys, tooltips, and + focus callbacks all trace back to the data you passed in. Every + example compiles under strict TypeScript, and invalid definitions + fail before they reach the browser.

@@ -64,14 +107,43 @@ export default function ChartsLanding({
+ +
+
+

+ SVG by default. Canvas where it earns its weight. +

+

+ Move one paint-heavy mark to Canvas while axes, labels, focus, + tooltips, responsive layout, hydration, and export keep working + through one definition. Canvas and motion stay out of the default + bundle until you import them. +

+ + Rendering and export reference + +
+ + +
+
+

Make it look like your product.

- Same data and scales. CSS variables, themes, mark props, custom - tooltips, or your own renderer control the rest. + Same data, scales, and interactions. CSS variables, themes, mark + props, and custom tooltip content make the chart belong to your + product.

@@ -83,19 +155,13 @@ export default function ChartsLanding({

- Compose from marks to complete views. + Add ranges, goals, and events without switching APIs.

- Layer marks when they share a coordinate system. For compound - charts, composeViews places complete definitions with - fill, grid, layer, and inset utilities. Each view keeps its own - scales unless you share or align them.{' '} - - View composition reference. - + The area, goal rule, activation line, release points, and labels + each use the rows and channels they need, then share one coordinate + system. The same definition mixes a compact linear scale with + D3's UTC scale and monotone curve.

@@ -116,6 +182,29 @@ export default function ChartsLanding({ marks-and-channels API is most directly inspired by Observable Plot, but the runtime is an independent implementation.

+ +
) @@ -125,13 +214,100 @@ function TypedDotExample() { return ( {accountChartSource} ) } +function RendererSurfaceProof() { + return ( +
+
+

+ One definition, three surface choices +

+
+ +
+ + {mixedRendererSource} + + +
+ + + +
+
+ +

+ The same definition also feeds the web adapters, vanilla DOM, static + output, and the experimental React Native adapter. +

+
+ ) +} + +function RendererSurfaceRow({ + change, + detail, + title, +}: { + change: string + detail: string + title: string +}) { + return ( +
+
+

+ {title} +

+ + {change} + +
+

{detail}

+
+ ) +} + +const mixedRendererSource = `import { canvasChartRenderer } from '@tanstack/charts/canvas' +import { defineChart, dot, lineY } from '@tanstack/charts' + +const activity = defineChart({ + marks: [ + lineY(summary, { + x: 'time', + y: 'average', + }), + dot(events, { + x: 'time', + y: 'latency', + renderer: canvasChartRenderer, + }), + ], + scales, + tooltip, +})` + const accountChartSource = `import { scaleLinear } from '@tanstack/charts/scales/linear' import { defineChart, dot } from '@tanstack/charts' import { tooltip } from '@tanstack/charts/tooltip' diff --git a/src/components/landing/ChartsLandingGraphics.tsx b/src/components/landing/ChartsLandingGraphics.tsx index 12680e1b7..d8893fc25 100644 --- a/src/components/landing/ChartsLandingGraphics.tsx +++ b/src/components/landing/ChartsLandingGraphics.tsx @@ -29,6 +29,10 @@ import { chartsActivationCompactSvg, chartsActivationSvg, } from './chartsActivationSvg' +import { + chartsBundleSizeCompactSvg, + chartsBundleSizeSvg, +} from './chartsBundleSizeSvg' import { chartsKineticBarSvg, chartsKineticDonutSvg, @@ -599,6 +603,27 @@ export function AccountChart() { ) } +export function BundleSizeChart() { + return ( +
+
+ Bundle size snapshot + + August 2026 ยท rendered with TanStack Charts + +
+ + +
+ ) +} + export function ThemeGallery() { return (
diff --git a/src/components/landing/chartsBundleSizeSvg.ts b/src/components/landing/chartsBundleSizeSvg.ts new file mode 100644 index 000000000..7a74d102c --- /dev/null +++ b/src/components/landing/chartsBundleSizeSvg.ts @@ -0,0 +1,7 @@ +// Generated by pnpm charts:generate-landing-svg. +// Source: scripts/charts-landing/bundle-size-chart.ts + +export const chartsBundleSizeSvg = + 'A static August 2026 snapshot of minified and gzip-compressed browser bundle sizes. TanStack Charts: 40 kilobytes; uPlot: 22 kilobytes; Chart.js: 46 kilobytes; visx: 49 kilobytes; Lightweight Charts: 60 kilobytes; Observable Plot: 85 kilobytes; Vega-Lite: 87 kilobytes; D3: 90 kilobytes; Recharts: 97 kilobytes; Highcharts: 100 kilobytes; Victory: 105 kilobytes; Nivo: 143 kilobytes; Apache ECharts: 157 kilobytes; ApexCharts: 164 kilobytes; Plotly.js partial: 250 kilobytes; AG Charts: 367 kilobytes.' +export const chartsBundleSizeCompactSvg = + 'A static August 2026 snapshot of minified and gzip-compressed browser bundle sizes. TanStack Charts: 40 kilobytes; uPlot: 22 kilobytes; Chart.js: 46 kilobytes; visx: 49 kilobytes; Lightweight Charts: 60 kilobytes; Observable Plot: 85 kilobytes; Vega-Lite: 87 kilobytes; D3: 90 kilobytes; Recharts: 97 kilobytes; Highcharts: 100 kilobytes; Victory: 105 kilobytes; Nivo: 143 kilobytes; Apache ECharts: 157 kilobytes; ApexCharts: 164 kilobytes; Plotly.js partial: 250 kilobytes; AG Charts: 367 kilobytes.' diff --git a/tests/charts-landing-ssr.test.ts b/tests/charts-landing-ssr.test.ts index 0a36ad08b..2342fcc16 100644 --- a/tests/charts-landing-ssr.test.ts +++ b/tests/charts-landing-ssr.test.ts @@ -29,52 +29,76 @@ const catalog = { revision: 'a'.repeat(40), cases: [ { - id: '03-temperature-range-band', - family: 'range', + id: 'bar-grouped', + family: 'bar', order: 1, - title: 'Temperature Range Band', + title: 'Grouped bars', }, { - id: 'bar-grouped', - family: 'bar', + id: '14-error-bars', + family: 'uncertainty', order: 2, - title: 'Grouped Bars', + title: 'Point estimates with error bars', }, { - id: 'scatter-bubble', - family: 'relationship', + id: 'heatmap-labeled', + family: 'matrix', order: 3, - title: 'Bubble Scatter', + title: 'Labeled ordinal heatmap', }, { - id: '04-stacked-time-area', + id: '70-composed-chart', family: 'composition', order: 4, - title: 'Stacked Time Area', + title: 'Seattle weather with three y axes', }, { - id: '14-error-bars', - family: 'uncertainty', + id: '84-pinned-nested-chart-tooltip', + family: 'interaction', order: 5, - title: 'Error Bars', + title: 'Expanding pinned energy tooltip', }, { - id: 'heatmap-labeled', - family: 'matrix', + id: '90-zoomable-time-window', + family: 'interaction', order: 6, - title: 'Labeled Heatmap', + title: 'Wheel zoom and pan over AAPL closes', }, { - id: '36-hierarchy-tree', - family: 'hierarchy', + id: '99-comparative-radar', + family: 'polar', order: 7, - title: 'Hierarchy Tree', + title: 'Comparative radar chart', }, { - id: '76-pie', - family: 'polar', + id: '101-sunburst', + family: 'hierarchy', order: 8, - title: 'Pie', + title: 'Flare analytics sunburst', + }, + { + id: '103-bubble-map', + family: 'geography', + order: 9, + title: 'World population bubble map', + }, + { + id: '116-geometry-morph', + family: 'motion', + order: 10, + title: 'Cross-chart geometry morph', + }, + { + id: '127-shadcn-dashboard', + family: 'application', + order: 11, + title: 'shadcn dashboard', + }, + { + id: '131-shadcn-radial-text', + family: 'radial', + order: 12, + title: 'shadcn radial chart with text', }, ], } @@ -85,9 +109,9 @@ test('the landing hero uses a fixed set of distinct catalog cases', () => { chartsLandingHeroCaseIds.length, ) assert.deepEqual(chartsLandingHeroCaseIds, [ - '03-temperature-range-band', - 'bar-grouped', - 'scatter-bubble', + '70-composed-chart', + '101-sunburst', + '127-shadcn-dashboard', ]) }) @@ -166,6 +190,14 @@ test('the landing server-renders revision-pinned chart preview assets', () => { $('img[src^="/charts/catalog/previews/"]').length, catalog.cases.length + chartsLandingHeroCaseIds.length, ) + assert.equal( + $('img[data-catalog-preview-case][loading="lazy"]').length, + catalog.cases.length + chartsLandingHeroCaseIds.length, + ) + assert.equal( + $('img[data-catalog-preview-case][decoding="async"]').length, + catalog.cases.length + chartsLandingHeroCaseIds.length, + ) assert.equal($('.charts-catalog-chart').length, 0) assert.equal($('[data-chart-case]').length, 0) assert.equal($('[src*="/charts/catalog/assets/"]').length, 0)