|Kit
Chart

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 />
More in TanStack's own docs
This page is a sample, not the ceiling — TanStack Charts also covers node-link diagrams, geo projections, hexbin/contour density, sunburst/treemap hierarchies, and more. See its own example gallery for the rest.

On this page