DocumentationComponentsTheme Creator
Theme CreatorGitHubIntroduction
InstallationUsageThemingMotion
Components
ChartArea ChartBar ChartLine ChartLive Line ChartComposed ChartPie ChartFunnel ChartRadar ChartRadial ChartSankey ChartScatter ChartCandlestick ChartSunburst ChartChoropleth ChartActivity Heatmap
AI agentsContributing
  1. Components
  2. Scatter Chart

Scatter Chart

Token-driven scatter and bubble chart built on Recharts v3 — points on two measured axes, a marker shape per series, optional size as a third measure, and a number, time or category x-axis.

$ pnpm add @tessinaui/ui

When to use

  • To show how two measures relate. Spend against seats, load time against page weight: each point is one thing, placed by two of its numbers.
  • To find clusters, gaps and outliers. A scatter keeps every row visible, so the one account that does not fit the pattern stays on screen.
  • To add a third measure as size. Set sizeKey and each point becomes a bubble (Bubbles).
  • For individual readings over time. Glucose readings, build times, sales: events that happened when they happened, with no line implied between them (Time axis).
  • To sort things into four groups. Two reference lines turn the plot into quadrants (Quadrants).

When not to use

  • For a trend over time. If the points are evenly spaced and the eye should follow them, use LineChart. A scatter draws no line because it claims none.
  • To compare totals across categories. Use BarChart; length is easier to compare than position on two axes.
  • For activity by day and hour. Use ActivityHeatmap.
  • For more than four series. Markers and tones stop being tellable apart. Split the chart into small ones, or fold the smallest series into "Other".
  • For more than a few hundred points. Points start to hide each other and the keyboard walk gets long. Group the rows into bins first, or show a sample.
  • When the exact values matter more than the pattern. Put the numbers in a Table and keep the chart as the summary.

Installation

Terminal
pnpm add @tessinaui/ui recharts

Usage

example.tsx
import { ScatterChart } from "@tessinaui/ui";
import type { ChartConfig } from "@tessinaui/ui";

const config: ChartConfig = {
  starter: { label: "Starter", color: "var(--chart-1)" },
  pro: { label: "Pro", color: "var(--chart-2)" },
};

// One row per point. `plan` names the series each row belongs to.
const data = [
  { seats: 5, spend: 140, plan: "starter" },
  { seats: 14, spend: 340, plan: "starter" },
  { seats: 18, spend: 710, plan: "pro" },
  { seats: 38, spend: 1350, plan: "pro" },
];

<ScatterChart
  data={data}
  config={config}
  xKey="seats"
  yKey="spend"
  seriesKey="plan"
  xAxisLabel="Seats"
  yAxisLabel="Monthly spend"
  label="Pro accounts spend more per seat than Starter accounts"
/>

The data is one flat array. xKey and yKey name the two measures; seriesKey names the field that says which series a row belongs to, and its values are the keys of config. Leave seriesKey out and every row belongs to the first series.

Examples

Default

One series on two measured axes. Hover a point, or Tab to the chart and use the arrow keys: the point takes a ring and the others step back.

Multiple series

Three plans in three tones of one colour. Each series also takes its own marker, so the chart reads the same in greyscale.

Markers

A series takes the next marker by its place in config: circle, square, triangle, diamond, cross, star. shapes overrides one series, and outlinedSeries draws a series as a ring instead of a fill.

Bubbles

sizeKey sizes each point by a third measure, and nameKey gives each point its name in the tooltip.

Time axis

xScale="time" places points on a continuous time scale. Pass epoch milliseconds, a Date or a date string.

Category axis

xScale="category" gives each distinct value a slot, in row order, so two series can be compared category by category.

Axes

Titles, tick formatters, pinned ends and a right-hand value axis.

Point labels

showLabels prints each point's nameKey under its marker.

Quadrants

Two ReferenceLine children split the plot, and each quadrant is named in its corner.

Target range

A ReferenceArea child draws the range, and readings outside it take a second series with its own colour and marker. The range gets a legend entry of its own.

Trend line

A fitted line is a ReferenceLine segment between two points you compute.

Sizes

Three aspect ratios: sm (short and wide), md, and lg (tall).

Series toggling

onSeriesToggle turns the legend items into toggle buttons. The hidden list is yours to keep.

In a card

The heading, the insight and the range control belong to the card, not to the chart.

Accessible name and data table

label states the insight, description carries the numbers, and a table holds the same rows for anyone who cannot read the plot.

Choosing a point

With selectionMode="single" a click, a tap, or Enter or Space on the point the arrow keys are on chooses it, and choosing it again clears it. A point is chosen by its nameKey value, so give every point its own name; without nameKey each point is keyed by its place in its series and a development warning says so. The chosen point wears a ring in the page's ink and the others step back to a wash inside an outline; while a point is chosen, the one under the pointer only steps forward, without a ring of its own. Here the card below names the chosen account, and its close button clears it.

On a coloured plate

Inside a coloured Surface the chart's chrome takes the plate's ink with no prop: tick text, axis titles, grid lines, the legend, printed labels and the focus ring. Series colours are yours. The --chart-N tokens are measured against the page, not a plate, so give each series the plate's ink (hsl(var(--surface-ink))) and tell series apart by marker and by outlinedSeries. The tooltip is a page panel, so it keeps the page's look.

Right-to-left

The chart reads the direction around it. In a right-to-left page the x-axis runs from right to left and the value axis moves to the right-hand side; the legend and the tooltip follow the text. Set dir on the chart to choose a direction yourself, and yAxisPosition to keep the value axis on one side.

States

Loading skeleton, empty state, error, and a chart with a single reading.

API Reference

PropTypeDefaultDescription
dataRecord<string, string | number | Date | null>[]—One row per point.
configChartConfig—Series metadata (label, color).
xKey / yKeystring—Datum keys for the horizontal and vertical position.
sizeKeystring—Datum key for the marker's size. Makes the chart a bubble chart.
seriesKeystring—Datum key whose value names the row's series. Without it every row belongs to the first series.
nameKeystring—Datum key naming the point: the tooltip heading, the printed label, and its key while selecting.
seriesstring[]all config keysWhich series to plot, in order.
shapesRecord<string, "circle" | "square" | "triangle" | "diamond" | "cross" | "star">by config positionMarker per series.
outlinedSeriesstring[][]Series drawn as an outline instead of a fill.
sizeRange[number, number][8, 40]The bubble drawn for zero and for the largest value, in pixels across.
sizeLabelstringsizeKeyNames the size in the tooltip.
sizeFormatter(value: number) => string—Format the size in the tooltip.
fillOpacitynumber1, or 0.3 for bubblesMarker fill opacity.
xScale"linear" | "time" | "category""linear"How x positions are read.
xDomain / yDomain[number | "auto", number | "auto"]fit the dataPin either end of an axis.
xAxisLabel / yAxisLabelstring—Axis titles. They also name the values in the tooltip.
xTickFormatter / yTickFormatter(value, index) => string—Format tick text and the tooltip's values.
yAxisPosition"left" | "right"follows dirWhich side the y-axis sits on.
size"sm" | "md" | "lg""md"Aspect ratio.
showGrid / showTooltip / showXAxis / showYAxisbooleantrueToggle chart chrome.
showLegendbooleanmore than one seriesShow the legend.
showLabelsbooleanfalsePrint each point's nameKey under its marker.
hiddenSeriesstring[]—Series currently hidden.
onSeriesToggle(key: string) => void—Makes the legend items toggle buttons.
animatebooleantrueAnimate the points when they enter. Reduced motion turns it off regardless.
selectionMode"none" | "single""none""single" lets a reader choose one point by click, tap, Enter or Space; choosing it again clears it.
selectedstring | null—nameKey value of the chosen point, controlled. null chooses nothing.
defaultSelectedstring | nullnullnameKey value of the point chosen at first render, uncontrolled.
onSelectedChange(key, { datum, index, source }) => void—Called on every choice and clear; source is "pointer" or "keyboard".
activestring | null—nameKey value of the lit point, controlled: opens the tooltip on it.
onActiveChange(key: string | null) => void—Called when the chart's own pointer or arrow keys move to another point.
stringsPartial<ScatterChartStrings>English, Arabic under dir="rtl"Selection's words per entry: selected, cleared, items, point.
childrenReactNode—Extra Recharts children (ReferenceLine, ReferenceArea, …).
dir"ltr" | "rtl"inheritedReading direction. Unset, read off the chart's position in the page once it mounts.
labelstring—Accessible name; renders the chart as a role="figure".
descriptionstring—Visually hidden summary for assistive tech.
loadingbooleanfalseRender the skeleton.
errorReactNode—Render an error message instead of the chart.
emptyStateReactNode"No data to display"Shown when no row can be plotted.

Markers and colour

Colour is never the only thing that tells two series apart. Every series takes its own marker, and the legend draws that marker instead of a colour square, so the chart holds up in greyscale, in print and for a reader who cannot tell the hues apart.

  • A marker belongs to its series, not to its rank. It is read from the series' place in config, so hiding or reordering series never reshuffles the rest.
  • Every marker is drawn at the same area. A triangle carries the same ink as a circle, so no series looks more important than another.
  • Tones of one colour keep a chart calm, and markers do the telling apart. The examples use three tones of --chart-1:
example.tsx
const config: ChartConfig = {
  starter: { label: "Starter", color: "oklch(from var(--chart-1) calc(l + 0.16) c h)" },
  pro: { label: "Pro", color: "var(--chart-1)" },
  team: { label: "Team", color: "oklch(from var(--chart-1) calc(l - 0.12) c h)" },
};
  • Use outlinedSeries for a series that is a reference rather than a category: an estimate, a previous period, a reading that was not measured.

Bubble size

sizeKey scales each marker's area, starting from zero, so a value twice as large is drawn with twice the ink. Scaling the radius instead would make it look four times as large.

  • sizeRange is the bubble drawn for zero and for the largest value, in pixels across.
  • A bubble is a see-through fill inside a full-strength outline, so overlapping bubbles stay readable through each other.
  • A row with no size takes the smallest marker.
  • There is no size legend. Name the measure with sizeLabel; the tooltip states each point's value, and description should state the range.

Missing and sparse data

A row that cannot be placed is left out. A row whose x or y is missing or not a number is dropped. It is never drawn at zero, because zero is a value.

A chart with a single reading still draws a real axis around it. When no row can be placed at all, the chart shows emptyState.

Accessibility

  • Give every chart a label. It names the figure to assistive tech. State the insight, not the anatomy: "Spend per seat steps up with each plan", not "Scatter chart of spend by seats".
  • Put the numbers in description. A static read should carry the message without anyone stepping through the points.
  • Offer the rows as a table when the exact values matter (Accessible name and data table).
  • Keyboard. Tab moves focus to the chart, which draws the focus ring. The Left and Right arrow keys step through every point of every series from the start of the x-axis to its end, and each point is announced through the tooltip: its name or series, then each value with its axis title. Escape dismisses the tooltip without leaving the chart. With onSeriesToggle, the legend items are buttons that come before the chart in the tab order.
  • Pointer. Each point has a target larger than its marker: 24px with a mouse, 44px under a finger.
  • Motion. Points animate in once. Under prefers-reduced-motion they do not, and the dimming of the other points is instant.
  • Screen readers. Not yet verified with VoiceOver, NVDA or JAWS, which is why the component is in beta.
  • With selectionMode="single" the points become a list to choose from. The chart's one tab stop is a named listbox ("Spend by seats: Points"), each point an option with aria-selected, its place in the list, and a name that carries its readings, and the point the arrow keys are on is its aria-activedescendant. Enter or Space chooses that point, so Enter no longer toggles the tooltip; Escape still hides it. A polite status region says "Acme selected" or "Selection cleared" once per change. The screen reader pass of this mode is still to be run.

Composition

children are rendered inside the Recharts chart, so annotations stay Recharts' own API instead of growing a prop each:

example.tsx
import { ReferenceArea, ReferenceLine } from "recharts";

<ScatterChart data={data} config={config} xKey="at" yKey="level" xScale="time">
  <ReferenceArea y1={3.9} y2={10} fill="var(--chart-5)" fillOpacity={0.12} />
  <ReferenceLine y={10} stroke="var(--chart-3)" strokeDasharray="6 4" />
</ScatterChart>

A reference line left at its default ink is painted as a hairline to match the grid; a stroke you pass is kept. A label is text, so give it a text ink (hsl(var(--muted-foreground))), not a series colour: the --chart-N tokens clear 3:1 for marks, not 4.5:1 for 12px type.

Boundaries

  • No fitted line of its own. The chart draws the line it is handed (Trend line). The fit is your data's.
  • No second y-axis. Two scales on one plot invite a comparison the numbers do not support. Draw two charts.
  • No joined points. Points joined in order are a LineChart.
  • No click or selection yet. A point shows its values on hover and focus; it is not a control.

Theming

Series colours come from config, as --chart-1 to --chart-6 or any colour you pass; both themes are handled by the tokens. Axis text, grid lines, the legend and the tooltip use the page's text, border and surface tokens, and follow a coloured Surface without a prop.

Sankey ChartCandlestick Chart

On this page

When to useWhen not to useInstallationUsageExamplesDefaultMultiple seriesMarkersBubblesTime axisCategory axisAxesPoint labelsQuadrantsTarget rangeTrend lineSizesSeries togglingIn a cardAccessible name and data tableChoosing a pointOn a coloured plateRight-to-leftStatesAPI ReferenceMarkers and colourBubble sizeMissing and sparse dataAccessibilityCompositionBoundariesTheming