Scatter

Scatterplots place each observation at two values, to show how one measure moves with another and where clusters and outliers sit.

LLMs

Code

Scatterplots 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 scatterplot

Describe the chart with defineChart, outside the component, and pass it to Chart. A point is a dot mark: x and y name the two numeric fields, and both axes take a linear scale.
import { defineChart, dot } 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 cars = [
{ name: "Civic", power: 67, economy: 33 },
{ name: "Corolla", power: 75, economy: 29 },
{ name: "Mustang", power: 140, economy: 19 },
{ name: "Impala", power: 165, economy: 14 },
]
const definition = defineChart(
{
marks: [
dot(cars, {
x: "power",
y: "economy",
key: "name",
r: 3,
fill: chartColors.series[0],
fillOpacity: 0.3,
stroke: chartColors.series[0],
}),
],
scales: {
x: { scale: scaleLinear, grid: true, axis: { label: "Power (hp)" } },
y: { scale: scaleLinear, grid: true, axis: { label: "Fuel economy (mpg)" } },
},
},
{
tooltip: {
use: tooltip,
content: ([point]) => ({
title: point?.datum.name ?? "",
rows: [
{ label: "Power", value: `${point?.xValue} hp` },
{ label: "Economy", value: `${point?.yValue} mpg`, color: point?.color },
],
}),
},
},
)
export function PowerChart() {
return (
<Chart
definition={definition}
ariaLabel="Car power against fuel economy"
height={260}
/>
)
}
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

Many points

Chart draws one SVG element per point, which slows down past a few thousand points. Swap it for CanvasChart: it takes the same definition and draws on a canvas.
import { CanvasChart } from "@/components/ui/tanstack-chart"
<CanvasChart
definition={definition}
ariaLabel="Car weight against acceleration, colored by cylinder count"
height={280}
/>
  • Give every point a key that is unique, such as an id or a few fields joined. Without one, points that share a position share an identity.
  • Lower fillOpacity and r so dense areas show as darker, instead of one solid shape.
  • Canvas removes the cost of the SVG elements, not of the data. Past tens of thousands of points, count them into cells with a hexbin or a binned grid. The Heatmaps page shows both.

Groups

Set color to a category field to color the points by group. List the groups in color.domain on the definition, so each group keeps its color when one is filtered out.
defineChart({
marks: [dot(penguins, { x: "bill_length", y: "bill_depth", color: "species" })],
color: { domain: ["Adelie", "Chinstrap", "Gentoo"] },
})

Point size

r takes a fixed radius in pixels, or a field. With a field, pass rScale a square-root scale, so the area of each bubble, not its radius, follows the value. The scale's range is the smallest and the largest radius in pixels.
import { scaleSqrt } from "d3-scale"
// Draw the largest first, so the small bubbles stay on top.
const rows = [...penguins].sort((a, b) => b.body_mass - a.body_mass)
dot(rows, {
x: "bill_length",
y: "bill_depth",
color: "species",
r: "body_mass",
rScale: { scale: () => scaleSqrt().range([3, 11]) },
fillOpacity: 0.78,
stroke: chartColors.surface,
strokeWidth: 0.75,
})
TanStack ships compact linear, band, point and ordinal scales. For square-root, log and time scales, add d3-scale to your project: npm install d3-scale and npm install -D @types/d3-scale.

Trend line

Add linearRegressionY on the same rows and fields to draw a least-squares line through the points. It draws a 95% confidence band around the line by default. ci: 0 hides it.
import { dot, linearRegressionY } from "@tanstack/charts"
marks: [
dot(cars, { x: "power", y: "economy", fill: chartColors.series[0] }),
linearRegressionY(cars, {
x: "power",
y: "economy",
ci: 0,
stroke: chartColors.series[1],
strokeWidth: 2,
}),
]
Set z to a group field to fit one line per group. Both marks show in the tooltip, so check in your content whether the focused point is a car or the line.

Log scale

When values span several orders of magnitude, pass a log scale from d3-scale. Give it a fixed domain, since a log scale cannot include zero. Filter out rows at zero or below first.
import { scaleLog } from "d3-scale"
scales: {
x: { scale: scaleLog().domain([200, 30_000]), grid: true, axis: { label: "Class size" } },
}

Small multiples

Wrap the marks in facet to draw one scatterplot per group. Fix the domains of both scales, so every panel shares the same axes and the panels can be compared.
import { defineChart, dot, facet } from "@tanstack/charts"
defineChart({
marks: [
facet(rows, {
by: "series",
columns: 4,
label: (series) => `Set ${series}`,
chart: (group) => ({
marks: [dot(group, { x: "x", y: "y" })],
scales: {
x: { scale: scaleLinear().domain([3, 20]), grid: true },
y: { scale: scaleLinear().domain([2, 14]), grid: true },
},
}),
}),
],
scales: { x: null, 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 point, from the active palette.
dot(cars, { x: "power", y: "economy", 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 options a scatterplot uses most. TanStack Charts' documentation has the full reference.
dot
linearRegressionY