Chart types
A tour of TanStack Charts' grammar through the same Chart wrapper, grouped by the job the data needs done — magnitude, trend, relationship, distribution, composition, and comparison.
Every example below is still Chart — TanStack's real grammar, plus the kit's theming. Grouped by what the data needs to say, not by how simple the code is: pick the section that matches your data's job, then adapt the mark. All of it wires TanStack's own tooltip extension (@tanstack/charts/tooltip).
Magnitude by category
barY compares categories vertically; barX swaps the axes for a ranking with labels too long to fit under vertical bars.
Commits per month
Code
const chart = defineChart({
marks: [barY(commitsByMonth, { x: 'month', y: 'commits', inset: 2 })],
scales: {
x: { scale: () => scaleBand().padding(0.24) },
y: { scale: scaleLinear, nice: true, grid: true, axis: { label: 'Commits' } },
},
tooltip,
});
<Chart definition={chart} ariaLabel="Commits per month" height={280} color="blue" />Lines changed by contributor
Code
const chart = defineChart({
marks: [barX(linesChangedByContributor, { y: 'contributor', x: 'lines', inset: 2 })],
scales: {
y: { scale: () => scaleBand().padding(0.24) },
x: { scale: scaleLinear, nice: true, grid: true, axis: { label: 'Lines changed' } },
},
tooltip,
});Change over time
lineY for a trend, areaY when the magnitude building up matters, and areaY's y1/y2 (instead of y) for an interval band — with a second mark composed on top, since a chart is just marks layered in one defineChart.
Weekly active users
Code
const chart = defineChart({
marks: [lineY(weeklyActiveUsers, { x: 'week', y: 'users', points: true })],
scales: {
x: { scale: () => scalePoint().padding(0.5) },
y: { scale: scaleLinear, nice: true, grid: true, axis: { label: 'Weekly active users' } },
},
tooltip,
});Cumulative signups this quarter
Code
const chart = defineChart({
marks: [
areaY(cumulativeSignups, { x: 'week', y: 'total', fillOpacity: 0.35, strokeWidth: 2 }),
ruleY([0]),
],
scales: {
x: { scale: () => scalePoint().padding(0.5) },
y: { scale: scaleLinear, nice: true, grid: true, axis: { label: 'Cumulative signups' } },
},
tooltip,
});Response time, P50 line inside a P50-P99 band
Code
const chart = defineChart({
marks: [
areaY(responseTimeBand, { x: 'day', y1: 'p50', y2: 'p99', fillOpacity: 0.28 }),
lineY(responseTimeBand, { x: 'day', y: 'p50', strokeWidth: 2 }),
],
scales: {
x: { scale: () => scalePoint().padding(0.5) },
y: { scale: scaleLinear, nice: true, grid: true, axis: { label: 'Response time (ms)' } },
},
tooltip,
});Relationship
dot plots raw x/y pairs — a scatter for two quantitative variables, or a bubble once a third joins as a r (radius) channel.
Review time vs. PR size
Code
const chart = defineChart({
marks: [dot(prSamples, { x: 'linesChanged', y: 'reviewHours' })],
scales: {
x: { scale: scaleLinear, nice: true, grid: true, axis: { label: 'Lines changed' } },
y: { scale: scaleLinear, nice: true, grid: true, axis: { label: 'Review time (hours)' } },
},
tooltip,
});The same data, with comment count as a third dimension
Code
dot(prSamples, { x: 'linesChanged', y: 'reviewHours', r: 'comments', fillOpacity: 0.7 })Distribution
binX groups raw observations into bins (thresholds picks roughly how many) for rect to draw as a histogram — no dedicated histogram mark, a transform plus a mark. boxY takes raw per-observation rows directly and summarizes each category into a Tukey box (quartiles, whiskers, outliers) itself, no pre-aggregation.
PR size distribution
Code
const bins = binX(prSizes.map((size) => ({ size })), { value: 'size', thresholds: 10 });
const chart = defineChart({
marks: [rect(bins, { x1: 'x1', x2: 'x2', y1: () => 0, y2: 'value', inset: 1 })],
scales: {
x: { scale: scaleLinear, nice: true, grid: true, axis: { label: 'Lines changed' } },
y: { scale: scaleLinear, nice: true, grid: true, axis: { label: 'PRs' } },
},
tooltip,
});Review turnaround by team
Code
const chart = defineChart({
marks: [boxY(reviewTurnaround, { x: 'team', y: 'hours' })],
scales: {
x: { scale: () => scaleBand().domain(['Platform', 'Web', 'Mobile']).padding(0.35) },
y: { scale: scaleLinear, nice: true, grid: true, axis: { label: 'Hours' } },
},
tooltip,
});Composition
Multiple series first, then stacked, then stacked and normalized. A color channel on a mark gets TanStack's own implicit categorical scale — Chart's colors prop themes that via --ts-chart-N, with zero change to how the chart itself is built. For a specific series order or a legend, build the color scale explicitly instead: chartColors() resolves the kit's palette to literal values for color.range, paired with colorLegend() (TanStack's own).
Commits by contributor
Code
const chart = defineChart({
marks: [
lineY(commitsByContributor, { x: 'month', y: 'commits', color: 'contributor', points: true }),
],
scales: {
x: { scale: () => scalePoint().padding(0.5) },
y: { scale: scaleLinear, nice: true, grid: true, axis: { label: 'Commits' } },
},
tooltip,
});
<Chart definition={chart} ariaLabel="Commits by contributor" height={320} colors={['blue', 'green', 'red']} />Issues by status, per month (stacked)
Code
const chart = defineChart({
marks: [
areaY(issuesByStatus, {
x: 'month',
y: 'count',
color: 'status',
layout: stack({ order: ['Open', 'In progress', 'Closed'] }),
fillOpacity: 0.85,
}),
ruleY([0]),
],
scales: {
x: { scale: () => scalePoint().padding(0.15) },
y: { scale: scaleLinear, nice: true, grid: true, axis: { label: 'Issues' } },
},
color: {
domain: ['Open', 'In progress', 'Closed'],
range: chartColors(['orange', 'blue', 'green']),
legend: colorLegend({ label: 'Status' }),
},
tooltip,
});Traffic source share by quarter (normalized)
Code
const chart = defineChart({
marks: [
barY(trafficShareByQuarter, {
x: 'quarter',
y: 'sessions',
color: 'channel',
layout: stack({ order: ['Search', 'Direct', 'Referral', 'Social'], offset: 'normalize' }),
inset: 2,
}),
],
scales: {
x: { scale: () => scaleBand().padding(0.24) },
y: {
scale: scaleLinear,
grid: true,
axis: { label: 'Share of sessions', ticks: { format: (v) => `${Math.round(v * 100)}%` } },
},
},
color: {
domain: ['Search', 'Direct', 'Referral', 'Social'],
range: chartColors(['blue', 'green', 'orange', 'purple']),
legend: colorLegend({ label: 'Channel' }),
},
tooltip,
});Comparison across groups
TanStack has its own facet mark for small multiples; the simpler, always-safe option is just rendering N ordinary Charts side by side — no new API to learn, and each panel is an ordinary chart you could style or interact with independently.
PRs merged per month, per team
Platform
Web
Mobile
Code
{Object.entries(teamChartDefinitions).map(([team, definition]) => (
<div key={team}>
<p>{team}</p>
<Chart definition={definition} ariaLabel={`PRs merged per month — ${team}`} height={160} />
</div>
))}Putting it together
A single chart rarely stays inside one "job" — a real dashboard layers several. This one composes four banded areaY regions (an expected-pace corridor), a lineY split into a solid historical segment and a dashed projected one, dot for per-point pacing color, and ruleX/text for milestone and "now" annotations — eleven ordinary marks in one defineChart, nothing dashboard-specific about any of them.
Goal progress against a target corridor
Code
<MilestoneProgressDemo />Chart
A light theme wrapper around TanStack Charts — the grammar and rendering are theirs, the light/dark chrome, tooltip surface, and categorical palette are the kit's.
Interactions
Wiring a chart's own interaction callbacks into your UI, and an animated hover marker built on TanStack's own motion system.