Scatter chart
FreeTwo measures per item, with an optional third as point size. Points grow in, hidden groups shrink away, and a trend line with its fit draws after them.
pnpm dlx shadcn@latest add https://caliperui.site/r/scatter-chart.json| Item | Seats | Revenue | Open tickets |
|---|---|---|---|
| Halden 1 | 4 | 1200 | 2 |
| Ostra 2 | 41 | 24805 | 9 |
| Northwind 3 | 78 | 31790 | 16 |
| Sprout 4 | 115 | 66143 | 23 |
| Lumen 5 | 152 | 69191 | 30 |
| Kestrel 6 | 9 | 6507 | 7 |
| Halden 7 | 46 | 26822 | 14 |
| Ostra 8 | 83 | 36809 | 21 |
| Northwind 9 | 120 | 39204 | 28 |
| Sprout 10 | 157 | 81734 | 5 |
| Lumen 11 | 14 | 13052 | 12 |
| Kestrel 12 | 51 | 22385 | 19 |
| Halden 13 | 88 | 50878 | 26 |
| Ostra 14 | 125 | 56275 | 3 |
| Northwind 15 | 162 | 54079 | 10 |
| Sprout 16 | 19 | 12447 | 17 |
| Lumen 17 | 56 | 24782 | 24 |
| Kestrel 18 | 93 | 29525 | 31 |
| Halden 19 | 130 | 66196 | 8 |
| Ostra 20 | 167 | 75102 | 15 |
| Northwind 21 | 24 | 13596 | 22 |
| Sprout 22 | 61 | 36229 | 29 |
| Lumen 23 | 98 | 43974 | 6 |
| Kestrel 24 | 135 | 44127 | 13 |
| Halden 25 | 172 | 88975 | 20 |
| Ostra 26 | 29 | 13371 | 27 |
| Northwind 27 | 66 | 20462 | 4 |
| Sprout 28 | 103 | 51273 | 11 |
| Lumen 29 | 140 | 62528 | 18 |
| Kestrel 30 | 177 | 58090 | 25 |
| Halden 31 | 34 | 22196 | 2 |
| Ostra 32 | 71 | 32289 | 9 |
| Northwind 33 | 108 | 34790 | 16 |
| Sprout 34 | 145 | 73779 | 23 |
| Lumen 35 | 182 | 72343 | 30 |
| Kestrel 36 | 39 | 12015 | 7 |
When to use #
Use it for
- Accounts by seats and revenue, endpoints by traffic and latency.
- Finding outliers and clusters.
Skip it when
- One measure over time; use the line chart.
- Thousands of points; aggregate into a heatmap matrix first.
Installation #
pnpm dlx shadcn@latest add https://caliperui.site/r/scatter-chart.json
Adds its 6 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage #
This is the code behind the preview above. It is built from trendLine and ScatterChart.
import { ScatterChart } from "@/components/ui/scatter-chart" const plans = [ { id: "starter", label: "Starter" }, { id: "team", label: "Team" }, { id: "enterprise", label: "Enterprise" }, ]; const accounts = Array.from({ length: 36 }, (_, i) => { const seats = 4 + ((i * 37) % 180); const segment = seats > 120 ? "enterprise" : seats > 40 ? "team" : "starter"; return { id: `a${i}`, label: ["Halden", "Ostra", "Northwind", "Sprout", "Lumen", "Kestrel"][i % 6] + ` ${i + 1}`, x: seats, y: Math.max(1200, Math.round(seats * 380 * (0.6 + ((i * 53) % 80) / 100) + ((i * 71) % 9) * 900)), size: 2 + ((i * 7) % 30), group: segment }; }); export function ScatterChartDemo() { return ( <ScatterChart label="Accounts by seats and annual revenue" points={accounts} groups={plans} xLabel="Seats" yLabel="Revenue" sizeLabel="Open tickets" formatY={(v) => `$${Math.round(v / 1000)}k`} trend className="w-full" /> ); }
API reference #
trendLine
component
No props of its own; it accepts the native props of its root element.
ScatterChart
component
Accessibility #
It exposes group and status semantics, with aria-label, aria-busy and aria-hidden kept in step with state. Changes that matter are announced through a live region.
No custom key handling; Tab and the native activation keys do the work.
Motion #
It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.