ScatterChart

Two quantities against each other as points, optionally grouped by colour and sized by a third value for a bubble chart. Keep colour groups to three — any two points can end up adjacent, which is a harder test than a bar chart's neighbours.

html
<UiScatterChart :data="..." :xKey="..." :yKey="..." :ariaLabel="..." />

Needs the charts module

This component ships in @jgastager/bundy-ui/charts, a separate module so projects that never draw a chart don't carry the paint classes these need. Nothing extra to install — just add the module entry. See Charts.


Data

required

data is one object per point, and unlike the series-based charts the rows stay in exactly the shape you have them — nothing needs reshaping into series first. The props below name which fields to read, so the same array can drive a plain scatter, a grouped one and a bubble chart without being touched.

Rows stay in their own shape here, unlike the series-based charts — x-key/y-key name the fields, and series-key/size-key are optional.

X key

required

xKey names the field holding each point's horizontal value. It has to be numeric — a scatter's whole job is placing two quantities against each other, so a categorical x belongs on a bar chart instead. Grid rules stay on by default here, because reading a point means recovering two coordinates rather than one.

Y key

required

yKey names the numeric field for the vertical position. Which of the two quantities goes here is a real choice: convention puts the thing you think is caused on the y axis, so a reader scanning left to right sees the effect follow the cause.

Series key

seriesKey groups the points by a field, one palette slot each. Keep this to three groups: a bar chart only ever puts neighbouring colors side by side, but a scatter can place any two points together, and past three the palette can no longer guarantee two arbitrary groups are distinguishable. Beyond that, facet into small multiples instead.

  • Search
  • Social
  • Email

Keep series-key to three groups. Colours only have to survive against their neighbours in a bar or line chart, but a scatter can place any two points side by side; past three, the palette can't guarantee two arbitrary groups are distinguishable. Facet into small multiples instead of adding a fourth.

Size key

sizeKey turns it into a bubble chart. The value maps to bubble area , not radius — mapping it to radius makes a doubled value look four times as large. fill-opacity sits below 1 by default so overlapping bubbles still show what's underneath.

  • Search
  • Social
  • Email

size-key maps to bubble *area*, not radius — mapping a value to radius directly makes a doubled value look four times as large.

ARIA label

required

ariaLabel is required rather than optional: a cloud of points is completely opaque to a screen reader, so this is the only description of the chart that exists. Name the relationship the scatter shows — that's the finding — rather than the geometry.

ARIA description

ariaDescription carries the detail the label shouldn't — the spread, an outlier, where the relationship breaks down. It's read after the label, so write the two as a headline and a paragraph rather than repeating one inside the other.

Email campaigns sit well above the trend, returning 180 to 310 signups on under $1,000 of spend, while social campaigns return the fewest signups per dollar despite the largest budgets.

Color

color is the paint for the dots when there's no seriesKey to group them: without a grouping field every point means the same thing, so they all take one colour, and this is how that colour stops being primary. It's ignored entirely once seriesKey is set, where colors takes over.

Colors are the library's own semantic ones — primary, secondary, success, warning, error, info, pending, neutral — and any shade of them, primary-400 through primary-950, exactly as the bg-* utilities spell them. Set color per series (or per item) to pin one; leave it unset to take the next slot of the default sequence. neutral and secondary are never handed out automatically, so an unlabelled series never lands on them.

Colors

colors pins a paint per group, matched positionally to the order the groups first appear in data — so reordering your rows reorders the palette. Reach for it when a group has an identity the reader already ties to a colour; groups past the end of the list fall back to the default sequence.

  • Search
  • Social
  • Email

Radius

radius is the dot size in pixels for a plain scatter, four by default. Shrink it when the cloud is dense enough that dots start merging, and grow it when there are only a handful of points and each one is worth pointing at. It's ignored once sizeKey turns the chart into a bubble chart.

Radius range

radiusRange sets the smallest and largest bubble in pixels; everything between is interpolated on area. Widen it when the values span orders of magnitude, narrow it when the points are dense enough that big bubbles start swallowing their neighbours.

Fill opacity

fillOpacity sits below 1 by default because overlap is the normal state of a scatter — it's how you see through a crowded cloud instead of losing whatever is underneath. It's also what hovering fades *relative to*, so the highlight keeps working at any setting.

fill-opacity sits below 1 by default because overlap is the normal state of a scatter. Solid dots hide how many points share a spot.

X label

xLabel titles the horizontal axis. It matters more on a scatter than on most charts: the ticks are bare numbers, and without a title nothing on the plot says what quantity they measure.

Y label

yLabel titles the vertical axis. Set both titles on a scatter as a rule — the pair is what turns two anonymous number lines into a stated relationship.

X format

xFormat formats the horizontal ticks and the x value in the tooltip together, so a unit written once shows up in both. Both axes are numeric here, which is why each gets its own formatter rather than sharing one.

Y format

yFormat does the same for the vertical axis. A scatter's tooltip names both fields it read, so formatted units carry straight into it — worth setting even when the axis ticks are short enough to read bare.

Grid

grid draws rules on both axes and is on by default here, unlike some of the other charts — reading a point means recovering two coordinates, and without rules the eye has to travel to both axes unaided. Drop it only on a thumbnail where nobody is reading values off the plot.

Tooltip

tooltip is the panel that follows the pointer, on by default, and the only place a point's exact pair of values appears. Turn it off for a decorative distribution thumbnail where the shape is the whole reading.

Hovering dims every dot except the nearest one, to 0.7 of whatever fill-opacity is set to. It's a factor rather than a fixed value because these dots don't rest at 1 — a flat target equal to fill-opacity would make hovering do nothing, and one above it would make the unfocused dots brighter than the focused one. The hit test is nearest-point within 40px in both axes rather than a bounding box, so a dot stays reachable however small size-key made it.

The hover fade is a CSS transition over the library's own 250ms duration, so it costs nothing and honours prefers-reduced-motion. There's no animate prop and nothing tweens on a data change — the marks are plain SVG elements Vue patches directly.

Legend

legend defaults to on whenever seriesKey is set and off when it isn't, which is almost always right: without grouping there are no identities to name. Pass it explicitly to drop the legend from a grouped chart whose colours are already explained by the surrounding copy.

  • Search
  • Social
  • Email

Legend position

legendPosition moves the legend above the plot instead of below it. Top works when the group identities are what a reader needs before the dots mean anything.

  • Search
  • Social
  • Email

Legend align

legendAlign places the legend horizontally — start by default, so it lines up with the plot's left edge and the axis title beneath it. Centre it when the chart is the only thing in its card and the symmetry reads better.

  • Search
  • Social
  • Email
  • Search
  • Social
  • Email

Legend orientation

legendOrientation flows the entries in a row or a column. Vertical costs height but reads as a list, which is the better shape beside a narrow chart or when the group names are long enough to wrap awkwardly in a row.

  • Search
  • Social
  • Email

Guides

guides draws the axes and reserves the margins they need. Turning it off drops all of that — ticks, labels, grid and gutters — leaving the points to fill the box, which is what turns a scatter into a distribution thumbnail inside a card.

Spend vs signups12 campaigns

guides="false" drops the axes, ticks, labels and grid *and* the margins they reserve, so the marks fill the whole box. That plus a small height is the inline-sparkline shape — there's no separate component for it.

Height

height is the plot's height in pixels — width always comes from the container. It's ignored once aspectRatio is set, and it's held through the loading and empty states so a dashboard doesn't reflow as its panels resolve.

Aspect ratio

aspectRatio sizes the chart from its container's width instead of a fixed height. It's worth more on a scatter than on most charts: the shape of a cloud depends on the ratio of the two axes, so a chart that squashes on narrow screens can make a relationship look steeper than it is.

Loading

loading replaces the plot with a skeleton of exactly the same height, so the page doesn't jump when the data lands. An empty data array gets the empty state instead — two separate conditions, worth driving separately.

No data to display

API Reference

Generated from the component's source — props, slots and emits as the component actually declares them.

Props

Prop
Type
Default
Description
datarequired
Record<string, unknown>[]
One object per point. Unlike the series charts, rows stay in their own shape.
xKeyrequired
string
Field holding each point's x value.
yKeyrequired
string
Field holding each point's y value.
string
Field grouping points into series, each taking a color. Keep this to **three** groups. A bar chart only ever puts neighbouring colors side by side, but a scatter can place any two points together, and past three the palette can't guarantee two arbitrary groups stay distinguishable. Facet into small multiples instead.
string
Field holding a numeric size, turning the chart into a bubble chart.
ariaLabelrequired
string
What the chart shows, for screen readers. Required: an unlabelled chart is unreadable.
string
A longer accessible description, for detail the label shouldn't carry.
ChartColorName
'primary'
Paint for the dots when there's no `seriesKey` to group them. Without a grouping field every point means the same thing, so they all take one color — this is how that color stops being `primary`. Ignored once `seriesKey` is set, where `colors` takes over.
ChartColorName[]
Per-group paints, positionally matched to the order groups first appear in `data`. Same escape hatch the other charts give: reach for it when a group has an identity the reader already ties to a color. Groups past the end of the list fall back to the default sequence.
number
4
Dot radius in pixels. Ignored when `sizeKey` is set.
[number, number]
[4, 18]
Smallest and largest radius, for the `sizeKey` mapping.
number
0.75
Fill opacity. Below 1 because overlap is the normal state of a scatter.
string
Title for the x axis.
string
Title for the y axis.
(value: number) => string
Formats x-axis ticks and tooltip values.
(value: number) => string
Formats y-axis ticks and tooltip values.
boolean
true
Draws grid rules on both axes — reading a point means recovering two coordinates.
boolean
true
Shows the tooltip on hover.
boolean
Shows the legend. Defaults to on whenever `seriesKey` is set.
"top" | "bottom"
'bottom'
Where the legend sits relative to the plot.
"start" | "center" | "end"
'start'
Horizontal placement of the legend.
"horizontal" | "vertical"
'horizontal'
How the legend entries flow.
boolean
true
Draws the axes and reserves their margins.
number
320
Height of the plot in pixels. Ignored when `aspectRatio` is set.
number
Width-to-height ratio, sizing the chart from its container's width.
boolean
false
Replaces the plot with a skeleton of the same height.

Types

ts
interface ScatterPoint {
    key: string;
    x: number;
    y: number;
    size: number | null;
    group: string;
    color: ChartColorName;
}
ts
interface ChartPlot {
    x: number;
    y: number;
    width: number;
    height: number;
    /** Right edge, since marks need it more often than the width. */
    right: number;
    /** Bottom edge, which is also the x axis' baseline. */
    bottom: number;
}
ts
type ChartColorName = ChartColorFamily | `${ChartColorFamily}-${ChartColorShade}`;