Distributions

Distribution charts show how the values of one measure spread out, where most of them fall and how groups differ in shape.

LLMs

Code

Distribution charts are drawn with TanStack Charts, through the kit's Chart components. They paint the chart with the theme's colors, grid and type, and add three palette props. The chart itself is a TanStack definition, written the way TanStack documents it.

Install

npx antuikit@latest init
npx antuikit@latest add tanstack-chart
The first command installs AntuikitProvider and the theme tokens. The second adds the chart components and TanStack Charts itself.

Your first histogram

Count the rows into bins with binX, then draw each bin with a rect mark from zero to its count. Each bin carries its edges as x1 and x2, plus every output you name.
import { binX, defineChart, rect } from "@tanstack/charts"
import { scaleLinear } from "@tanstack/charts/scales/linear"
import { tooltip } from "@tanstack/charts/tooltip"
import { Chart, chartColors } from "@/components/ui/tanstack-chart"
const bins = binX(cars, {
value: "economy",
thresholds: [5, 10, 15, 20, 25, 30, 35, 40, 45, 50],
outputs: { count: { reduce: "count" } },
})
const definition = defineChart(
{
marks: [
rect(bins, {
x1: "x1",
x2: "x2",
y1: () => 0,
y2: "count",
fill: chartColors.series[0],
inset: 1,
}),
],
scales: {
x: { scale: scaleLinear, axis: { label: "Fuel economy (mpg)" } },
y: { scale: scaleLinear, grid: true, axis: { label: "Cars" } },
},
},
{
tooltip: {
use: tooltip,
content: ([point]) => ({
title: `${point?.datum.x1} to ${point?.datum.x2} mpg`,
rows: [{ label: "Cars", value: String(point?.datum.count), color: point?.color }],
}),
},
},
)
export function EconomyHistogram() {
return (
<Chart
definition={definition}
ariaLabel="Number of cars by fuel economy, in steps of 5 mpg"
height={240}
/>
)
}
The chart fills its container's width, so give it a height. ariaLabel is required.

Which component

All three take the same definition and the same palette props.
Chart components

Bins

thresholds decides the bins. Pass the full list of edges for bins the reader can name, a count for a rough number of bins, or a rule such as thresholdScott from d3-array.
import { thresholdScott } from "d3-array"
binX(rows, { value: "weight", thresholds: [40, 60, 80, 100, 120] })
binX(rows, { value: "weight", thresholds: 20 })
binX(rows, { value: "weight", thresholds: thresholdScott })
  • Use the same list of edges in every histogram you compare. A count or a rule picks new edges for each data set.
  • by bins each group on its own, with the same edges, and adds the group's field to each bin.
  • binY does the same along the vertical axis, with y1 and y2, for violins and sideways histograms.
  • When your code imports from d3-array, d3-shape or d3-scale, add that package and its @types package to your project.

Counts or shares

To compare groups of different sizes, turn counts into shares with normalize. basis: "sum" divides by the group's total, so each group's bins add up to 1. basis: "max" divides by the tallest bin, which is what a violin or a ridgeline needs.
import { binX, normalize } from "@tanstack/charts"
const shares = normalize(
binX(penguins, {
value: "body_mass",
by: "species",
thresholds: [2500, 3000, 3500, 4000, 4500, 5000, 5500, 6000, 6500],
outputs: { count: { reduce: "count" } },
}),
{ value: "count", by: "species", basis: "sum", as: "proportion" },
)

Box plots and summaries

boxY takes the raw rows, one per observation, and computes the quartiles, the whiskers and the outliers for each group itself. Do not summarize the rows first.
import { boxY } from "@tanstack/charts"
import { scaleBand } from "@tanstack/charts/scales/band"
boxY(runs, {
x: "experiment",
y: "speed",
key: (row) => `${row.experiment}:${row.run}`,
fill: chartColors.series[0],
fillOpacity: 0.25,
stroke: chartColors.series[0],
})
scales: { x: { scale: () => scaleBand<number>().padding(0.22) } }
For a mean with error bars, reduce each group with groupBy, then draw the range with link and the caps with tickY. The reducers include "mean", median, deviation and quantile(p).
import { deviation, groupBy } from "@tanstack/charts"
const rows = groupBy(penguins, {
by: "species",
outputs: {
mean: { value: "body_mass", reduce: "mean" },
spread: { value: "body_mass", reduce: deviation },
},
})

Violins and ridgelines

A violin is a binned profile drawn on both sides of a center line. Bin with binY, scale each group to its tallest bin with normalize, and pass the result to violinY.
import { binY, d3AreaXCurve, normalize, violinY } from "@tanstack/charts"
import { curveBasis } from "d3-shape"
const profiles = normalize(
binY(penguins, {
value: "body_mass",
by: "species",
thresholds: massBoundaries,
outputs: { count: { reduce: "count" } },
}),
{ value: "count", by: "species", basis: "max", as: "width" },
)
violinY(profiles, {
x: "species",
y: "y",
width: "width",
span: 0.76,
color: "species",
curve: d3AreaXCurve(curveBasis),
})
ridgelineY takes the same kind of profile, with height in place of width, and stacks one ridge per group. overlap sets how far each ridge reaches into the row above. Keep it below 1 so the lower ridges stay readable.

Cumulative

Run the bins through cumulative to add a running total, or rank the raw rows with rank and divide by the count for a cumulative distribution with no bins.
import { cumulative, rank } from "@tanstack/charts"
const totals = cumulative(bins, {
orderBy: "x1",
outputs: { total: { value: "count", reduce: "sum" } },
})
const shares = rank(cars, { value: "economy", order: "ascending" })
.map((row) => ({ ...row, share: row.rank / cars.length }))

Every value

For a beeswarm, give dot a dodgeY layout. It pushes overlapping points apart vertically, so every row stays visible along one axis. Set the y scale to null, since the vertical position carries no value.
import { dodgeY, dot } from "@tanstack/charts"
dot(cars, {
x: "economy",
r: 4,
layout: dodgeY({ anchor: "middle", padding: 1 }),
})
scales: { x: { scale: scaleLinear }, y: null }

Colors

The chart takes the theme's chart colors without any setup. Change the palette on the chart itself, or paint one mark with fill and chartColors. The Color guideline covers which palette fits which data.
<Chart definition={definition} ariaLabel="..." palette="multi" />
// One color for every bin, from the active palette.
rect(bins, { x1: "x1", x2: "x2", y1: () => 0, y2: "count", fill: chartColors.series[0] })
Palette props
Taken by Chart, RendererChart and CanvasChart. Rows marked antuikit in the Version column are added by the kit.

Animation

Use RendererChart with TanStack's motion() renderer to animate marks in and between data updates. It skips the animation when the reader has asked for reduced motion.
import { motion } from "@tanstack/charts/motion"
import { RendererChart } from "@/components/ui/tanstack-chart"
const renderer = motion({ initial: "always" })
<RendererChart definition={definition} renderer={renderer} ariaLabel="..." />

API

The transforms and marks distribution charts use most. TanStack Charts' documentation has the full reference.
binX and binY
boxY
violinY