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.
<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.
Options
One block per prop — what it does, and what it looks like set.
Data
requireddata 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
requiredxKey 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
requiredyKey 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
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
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
requiredariaLabel 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.
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
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 tick size
xTickSize is the box the x-tick slot draws into, and the room reserved for it below the plot. Real tick marks are the one thing the default axis leaves out, and they're a line and a label in the slot — box starts at the plot's edge, so a mark drawn from it points back at the axis.
- Search
- Social
Y tick size
yTickSize is the same for the y-tick slot, taken out of the left margin — and there it replaces the estimate from the label text entirely, so it has to cover everything the slot draws. Height costs nothing on this axis: only the width is reserved.
- Search
- Social
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.
X min
xMin, xMax, yMin and yMax hold an axis open to values the data needn't reach. Small multiples are what they're for: fitted to its own extent, each channel below gets a different scale and the three panels stop being comparable — the same dot lands somewhere else in each.
The four bounds are what make small multiples honest: a scatter fitted to its own extent puts the same point in a different place in every panel. Set the same xMin/xMax/yMin/yMax across a facet grid and the panels become comparable. None of them clamps the data — a point outside a bound widens that axis instead of being drawn past the plot's edge.
X max
xMax is the top of the x axis — a budget ceiling to read the spend against, whether or not a campaign got near it.
Y min
yMin is the floor of the y axis. Zero is the usual choice: it makes the dots' heights readable as counts, rather than as positions inside whatever range this batch happens to span.
Y max
yMax is the ceiling of the y axis, on the same terms. A value outside any of these bounds still widens the axis — a dot drawn past the plot's edge would be a worse answer than a scale that grew.
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.
Animate
animate is on by default: the dots grow in on the first draw and glide to their new positions. Roll new values to compare it with the chart on the right, which has it off. Only the marks move — axes, tooltips and labels always read the real numbers — and a reader who has asked their system for reduced motion gets no motion either way.
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
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
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
- Search
- Social
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
Legend group
legendGroup names a group that a standalone UiChartLegend with the same group belongs to, so the legend can sit anywhere — here above the plot — and still dim the other dots while you hover an entry. The chart hands the legend its entries, colors included, so it needs no items. Switch the chart's own legend off if you don't want two.
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.
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.
API Reference
Generated from the component's source — props, slots and emits as the component actually declares them.
Props
datarequiredRecord<string, unknown>[]stringariaLabelrequiredstringChartColorName'primary'ChartColorName[]number24numberbooleantruestringSlots
defaultx-ticky-tickTypes
interface ScatterPoint {
key: string;
x: number;
y: number;
size: number | null;
group: string;
color: ChartColorName;
}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;
}type ChartColorName = ChartColorFamily | `${ChartColorFamily}-${ChartColorShade}` | `[${string}]`;