Charts

ScatterChart

A scatter chart — and, the moment any point carries `r`, a bubble chart. One component for both because they are one picture: position encodes the relationship, and `r` optionally encodes a third measure.

Server safePure SVG0 dependenciesSource

Import

Charts live at their own subpath with their own stylesheet, so an app that never draws one pays nothing for them.

import { ScatterChart } from '@the_viveksingh/vivek-ui/charts'
import '@the_viveksingh/vivek-ui/charts.css'

A relationship, not a trend

Both axes are measures. The chart answers "do these two things move together" - which a line chart, with its ordered category axis, cannot.

Ad spend vs signups354045505560651520253035404550
Ad spend vs signups
Spend (k)Signups
1234
1841
2438
3155
3851
4568
5262
<ScatterChart
  title="Ad spend vs signups"
  xLabel="Spend (k)"
  yLabel="Signups"
  data={[
    { x: 12, y: 34 },
    { x: 18, y: 41 },
    { x: 31, y: 55 },
    { x: 45, y: 68 },
  ]}
/>

Bubbles: a third measure, scaled honestly

Any point carrying r turns the series into a bubble chart. r maps to the AREA of the mark, not its radius - radius scaling (the Chart.js default) squares every visual ratio and quietly exaggerates the data.

Spend vs revenue, sized by headcount40455055606570101520253035404550
Spend vs revenue, sized by headcount
Spend (k)Revenue (k)Size
Meridian4012
Halcyon5845
Fieldwork4520
Northgate7280
Overtone6033
<ScatterChart
  title="Spend vs revenue, sized by headcount"
  data={[
    { x: 10, y: 40, r: 12, label: 'Meridian' },
    { x: 41, y: 72, r: 80, label: 'Northgate' },
  ]}
/>

// r is any positive measure; the largest bubble gets maxBubbleSize px.

Accessibility

A chart is not an image

Every chart carries role="img" with a generated accessible name, and renders a real <table> alongside it — visually hidden, never display: none — so a screen-reader user gets the actual numbers instead of "chart". Set accessibleTable={false} only if you have provided the data in a table elsewhere on the page.

No series is ever encoded by colour alone: each carries a distinct dash pattern and marker shape on top of its colour, so the chart survives greyscale printing and every common form of colour blindness.

The palette is verified rather than asserted. Each theme has its own six colours, because one set cannot clear the 3:1 non-text contrast threshold against both a white and a near-black plot surface. Separation is measured under simulated protanopia, deuteranopia and tritanopia: for one to four series — which is nearly every chart — the closest pair sits at ΔE 17, slightly better than the unmodified Okabe-Ito palette it is derived from. Past four series the dash patterns and marker shapes carry the distinction.

Hostile data

Real series contain gaps and rubbish. Every chart is tested against empty arrays, a single point, all-equal values, negatives crossing zero, NaN, ±Infinity, 1e308 and 1e-320, with an assertion that no rendered SVG attribute ever contains NaN. A bad row degrades the chart; it does not blank your page.


Props

Generated from the package's own type declarations, so this table cannot drift from the code.

Props for ScatterChart
PropTypeDefaultDescription
datareadonly ScatterDatum[]—Single series.
seriesreadonly ScatterSeries[]—Multiple series. Takes precedence over `data` when both are given.
heightnumber—Height of the `viewBox`, in px. The chart scales to its container width.
showGridboolean——
showAxesboolean——
showLegendboolean—Defaults to on for more than one series.
interactiveLegendboolean—Each legend entry is a checkbox that shows and hides its series, with a fade — the way every charting library's users expect a legend to behave. On by default whenever the legend shows; pass `false` for a figure in a report, where the legend is a key and not a control. No JavaScript and no client boundary: the entries are real checkboxes and the chart reacts with `:has()`.
markSizenumber—Mark radius in px when no point carries `r`.
maxBubbleSizenumber—Largest bubble radius in px, reached by the largest `r` in the data.