Scatter Plot

Two measured variables plotted against each other, with an optional least-squares trend line and Pearson’s r. Points can carry a third dimension as area and a fourth as colour.

src/components/ui/scatter-plot.tsx

About

From the component's own source, where it was written.

Two measured variables plotted against each other.

**Different from Chart's scatter variant, which is why this exists.** That one takes number[] and spaces the points evenly along x — the x axis is an index, so it answers "how did this change in sequence". A scatter plot answers "are these two things related", and for that x must be a *measured value* with its own scale. Plotting spend against conversion on an index axis silently destroys the relationship you were looking for.

**The trend line and r are opt-in and honest.** trend fits an ordinary least-squares line and reports Pearson's r beside it. r measures *linear* association only: a perfect parabola scores near zero, and a single outlier can carry r from 0.1 to 0.8. It is shown to two decimal places and never as a claim about causation.

**Points can carry a third and fourth dimension** — size for magnitude and group for category. Area, not radius, is proportional to size, because scaling the radius makes a doubled value look four times bigger, which is the classic way a bubble chart lies.

Axes get real ticks from the data range rather than a fixed 0–100 grid, and both are drawn outside the stretched plot group so the labels are not distorted by the aspect fit.

Install

npx astralyx-ui add scatter-plot

Usage

import { ScatterPlot } from '@/components/ui/scatter-plot'

<ScatterPlot points={points} trend xLabel="Spend" yLabel="Conversion %" />

Composer

Change the props and the source below follows.

Scatter plot, r = -0.10
7.029999999999999%1.9700000000000002%
$0.4kSpend per campaign$5.3k
Conversion rateSearchSocialEmailDisplayr = -0.10
Props
tsx
<ScatterPlot
  points={points}
  trend={true}
  grid={true}
  xLabel="Spend per campaign"
  yLabel="Conversion rate"
/>

Examples

Spend against conversion, with a fit

API

Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.

ScatterPlot

Extends Omit<ComponentProps<'figure'>, 'height'>.

PropTypeDefaultDescription
pointsScatterPoint[]requiredThe plot is stretched to fill its box, which turns an SVG circle into an ellipse. Positioned elements stay round at any aspect ratio.
heightnumber240—
xLabelstring——
yLabelstring——
trendbooleanfalseOrdinary least squares, with Pearson’s r. r measures linear association only — a parabola scores near zero, and one outlier moves it a long way.
gridbooleantrue—
radiusRange[number, number][3, 14]Radius range in pixels, when points carry size.
xDomain[number, number]—Fixed bounds. Defaults to the data range with 5% padding so nothing sits on the axis.
yDomain[number, number]—Fixed bounds. Defaults to the data range with 5% padding so nothing sits on the axis.
valueFormat(value: number) => stringDEFAULT_FORMAT—
legendbooleantrue—
emptyLabelstring'No data.'—
labelstring'Scatter plot'—

Types

ScatterPoint

Two measured variables plotted against each other.

FieldTypeDescription
xnumber—
ynumber—
size?numberScaling the radius makes a doubled value look four times bigger. Area is proportional, so it reads honestly.
group?stringColours the point and builds the legend.
label?string—

Behaviour

TopicDetailDescription
vs Chart scattermeasured xChart’s scatter variant spaces number[] evenly along an index. Here x is a value, which is what makes the relationship visible at all.