caliper uidocs

Scatter chart

Free

Two 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.

Installs components/ui/scatter-chart.tsxDepends on d3-shapeMotion none in component source
pnpm dlx shadcn@latest add https://caliperui.site/r/scatter-chart.json
Accounts by seats and annual revenue

Accounts by seats and annual revenue
ItemSeatsRevenueOpen tickets
Halden 1412002
Ostra 241248059
Northwind 3783179016
Sprout 41156614323
Lumen 51526919130
Kestrel 6965077
Halden 7462682214
Ostra 8833680921
Northwind 91203920428
Sprout 10157817345
Lumen 11141305212
Kestrel 12512238519
Halden 13885087826
Ostra 14125562753
Northwind 151625407910
Sprout 16191244717
Lumen 17562478224
Kestrel 18932952531
Halden 19130661968
Ostra 201677510215
Northwind 21241359622
Sprout 22613622929
Lumen 2398439746
Kestrel 241354412713
Halden 251728897520
Ostra 26291337127
Northwind 2766204624
Sprout 281035127311
Lumen 291406252818
Kestrel 301775809025
Halden 3134221962
Ostra 3271322899
Northwind 331083479016
Sprout 341457377923
Lumen 351827234330
Kestrel 3639120157

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 #

terminal
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.

example.tsx
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
PropTypeDefault
label *string—
points *readonly ScatterDatum[]—
groupsreadonly ChartSeries[]—
xLabel *string—
yLabel *string—
sizeLabelstring—
formatX((value: number) => string)numberText
formatY((value: number) => string)numberText
trendbooleanfalse
heightnumber300
loadingboolean—
errorstring—
classNamestring—

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.