Skip to content
ShineUI
Install

Scatter chart

Points and bubbles.

<sui-chart-scatter>ChartsStable

About

Scatter and bubble chart on two numeric axes, for one measure against another: invoice size against days to pay. Series draw from the brand ramp and one may take the accent; a point with r becomes a bubble. Hovering a mark outlines it and shows its name with both coordinates. Round legend swatches toggle their series.

Usage

<sui-chart-scatter heading="Invoice size vs. days to pay" subtitle="Bubble area is the open invoice count" x-label="Days to pay" y-label="Avg invoice" caption="Municipal payers run two to three times slower, flagged orange"></sui-chart-scatter> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-chart-scatter"); el.series = [ { label: "Commercial", points: [ { x: 12, y: 4200, r: 7, name: "Meridian Fabrication" }, { x: 18, y: 6400, r: 9, name: "Silver Hills Realty" }, { x: 24, y: 12400, r: 11, name: "Riverbend Dental" }, { x: 31, y: 8200, r: 8, name: "Kye's Catering" }, ], }, { label: "Municipal", points: [{ x: 38, y: 18900, r: 12, name: "Clark County Schools" }, { x: 44, y: 11050, r: 9, name: "New Albany Parks" }], accent: true }, ]; /* SuiScatterSeries[] */ el.addEventListener("suiHover", (e) => console.log(e.detail)); </script>
import { SuiChartScatter } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { const series = [ { label: "Commercial", points: [ { x: 12, y: 4200, r: 7, name: "Meridian Fabrication" }, { x: 18, y: 6400, r: 9, name: "Silver Hills Realty" }, { x: 24, y: 12400, r: 11, name: "Riverbend Dental" }, { x: 31, y: 8200, r: 8, name: "Kye's Catering" }, ], }, { label: "Municipal", points: [{ x: 38, y: 18900, r: 12, name: "Clark County Schools" }, { x: 44, y: 11050, r: 9, name: "New Albany Parks" }], accent: true }, ]; /* SuiScatterSeries[] */ return <SuiChartScatter heading="Invoice size vs. days to pay" subtitle="Bubble area is the open invoice count" xLabel="Days to pay" yLabel="Avg invoice" caption="Municipal payers run two to three times slower, flagged orange" series={series} onSuiHover={(e) => console.log(e.detail)} />; }
<script setup> import { SuiChartScatter } from "@shineui/vue"; import "@shineui/core/shineui.css"; const series = [ { label: "Commercial", points: [ { x: 12, y: 4200, r: 7, name: "Meridian Fabrication" }, { x: 18, y: 6400, r: 9, name: "Silver Hills Realty" }, { x: 24, y: 12400, r: 11, name: "Riverbend Dental" }, { x: 31, y: 8200, r: 8, name: "Kye's Catering" }, ], }, { label: "Municipal", points: [{ x: 38, y: 18900, r: 12, name: "Clark County Schools" }, { x: 44, y: 11050, r: 9, name: "New Albany Parks" }], accent: true }, ]; // SuiScatterSeries[] const onChange = (e) => console.log(e.detail); </script> <template> <SuiChartScatter heading="Invoice size vs. days to pay" subtitle="Bubble area is the open invoice count" x-label="Days to pay" y-label="Avg invoice" caption="Municipal payers run two to three times slower, flagged orange" :series="series" @sui-hover="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiChartScatter } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiChartScatter], template: `<sui-chart-scatter heading="Invoice size vs. days to pay" subtitle="Bubble area is the open invoice count" x-label="Days to pay" y-label="Avg invoice" caption="Municipal payers run two to three times slower, flagged orange" [series]="series" (suiHover)="onChange($event)"></sui-chart-scatter>`, }) export class Example { series = [ { label: "Commercial", points: [ { x: 12, y: 4200, r: 7, name: "Meridian Fabrication" }, { x: 18, y: 6400, r: 9, name: "Silver Hills Realty" }, { x: 24, y: 12400, r: 11, name: "Riverbend Dental" }, { x: 31, y: 8200, r: 8, name: "Kye's Catering" }, ], }, { label: "Municipal", points: [{ x: 38, y: 18900, r: 12, name: "Clark County Schools" }, { x: 44, y: 11050, r: 9, name: "New Albany Parks" }], accent: true }, ]; // SuiScatterSeries[] onChange(e: CustomEvent) { console.log(e.detail); } }

Bubble chart

series is a list of { label, points }, each point { x, y, r, name }; formatX and formatY word the ticks and tip. The plot is one tab stop named by an aria-label on the host, else by the heading, the visible series and both measures, and the arrow keys walk the marks from left to right. Legend keys toggle a series.

<sui-chart-scatter heading="Invoice size vs. days to pay" subtitle="Bubble area is the open invoice count" x-label="Days to pay" y-label="Avg invoice" caption="Municipal payers run two to three times slower, flagged orange"></sui-chart-scatter>
// setup(root): properties and events setup(root) { const el = root.querySelector('sui-chart-scatter'); el.formatX = days; el.formatY = money; el.series = pick(['Commercial', 'Municipal'], 'Municipal'); }

Points only

Drop r and every mark is a 4px point at 85% alpha. Bubbles fill at 55% so overlapping areas stay readable.

<sui-chart-scatter heading="Invoice size vs. days to pay" subtitle="Points only, no third measure" x-label="Days to pay" y-label="Avg invoice"></sui-chart-scatter>
// setup(root): properties and events setup(root) { const el = root.querySelector('sui-chart-scatter'); el.formatX = days; el.formatY = money; el.series = asPoints(pick(['Commercial', 'Municipal'], 'Municipal')); }

Four series

Series take --viz-1 to --viz-4 in order, and one marked accent takes --accent. Toggling a key off removes its marks and its name from the accessible summary, and the axes rescale to what is left.

<sui-chart-scatter heading="Payment behavior by segment" subtitle="Four segments · toggle a legend key to isolate" x-label="Days to pay" y-label="Avg invoice" caption="Axes rescale to the visible series"></sui-chart-scatter>
// setup(root): properties and events setup(root) { const el = root.querySelector('sui-chart-scatter'); el.formatX = days; el.formatY = money; el.series = pick(['Commercial', 'Municipal', 'Healthcare', 'Education']); }

Compact with a control

width and height set the viewBox and the card is 34px wider than width. The header-end slot puts a control beside the heading.

<sui-chart-scatter heading="Days to pay" subtitle="Commercial book" width="320" height="150" x-label="Days to pay" y-label="Avg invoice"> <sui-button slot="header-end" size="sm" variant="ghost" label="Export"></sui-button> </sui-chart-scatter>
// setup(root): properties and events setup(root) { const el = root.querySelector('sui-chart-scatter'); el.formatX = days; el.formatY = money; el.series = pick(['Commercial']); }

Events

suiHover fires when the hovered mark changes and reports { index, series, point }: index counts every point of every series in order, and all three are -1 on leave. hoverIndex takes that same index, so a write from outside outlines the mark and shows its tooltip without emitting.

Idle.
<div style="display:flex;flex-direction:column;gap:12px"> <sui-chart-scatter id="sc-events" heading="Invoice size vs. days to pay" subtitle="Hover a bubble" x-label="Days to pay" y-label="Avg invoice"></sui-chart-scatter> <span id="sc-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Idle.</span> </div>
// setup(root): properties and events setup(root) { const el = root.querySelector('#sc-events'); const log = root.querySelector('#sc-log'); const series = pick(['Commercial', 'Municipal'], 'Municipal'); el.formatX = days; el.formatY = money; el.series = series; el.addEventListener('suiHover', (e) => { const { index, series: s, point: p } = e.detail; log.textContent = index < 0 ? 'Idle.' : `Point ${index}: ${series[s].points[p].name} (${series[s].label}, point ${p}).`; }); }

Empty and loading

loading stands a shimmer in for the plot and marks the host aria-busy while the figures are on their way; the frame and its heading stay. A chart with nothing to plot keeps its frame and says so, and emptyMessage is the sentence. Both are the same on every chart.

<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px"> <sui-chart-scatter heading="Invoice size vs. days to pay" subtitle="Fetching the period" height="150" loading></sui-chart-scatter> <sui-chart-scatter heading="Invoice size vs. days to pay" subtitle="This period" height="150" empty-message="No invoices were paid this period."></sui-chart-scatter> </div>

Keyboard

KeyAction
TabMoves to the plot, then through the legend keys, one button per series.
ArrowRight ArrowLeftMoves to the next mark to the right, or to the left, in the series and shows its tooltip; a reader hears its name, its series and both measures.
ArrowDown ArrowUpMoves to the mark of the next or the previous series nearest along x.
Home EndMoves to the leftmost or the rightmost mark of the series.
Ctrl+Home Ctrl+EndMoves to the first mark of the first series or the last mark of the last.
EscapeLeaves the point and hides the tooltip; the plot keeps focus.
Enter SpaceOn a legend key, hides its series and redraws the chart without it; press again to bring it back.

API

<sui-chart-scatter>

Properties

PropertyAttributeTypeDefaultDescription
captioncaptionstringstring | undefinedFootnote under the legend.
emptyMessageempty-messagestringstring | undefinedSentence drawn over the plot when there are no series or no series holds a point. Defaults to "Nothing to plot yet."; the frame, the axes and the legend stay. The chart filters nothing, so this is its only empty string.
everyeverySuiChartEvery"auto" | number'auto'Draw every nth x tick label, so a narrow plot or a long formatX stays legible. auto, the default, measures the widest tick against the space between two ticks and thins only where the labels would collide; where they fit it draws all of them and the axis is what it always was. Set it to 1 to draw every tick at any width, or to a larger number for the stride to start from: where its ticks would run into each other it widens to the next multiple that clears them.
fixedSizefixed-sizebooleanfalsePin the plot to width, which is what a fixed export wants, or a row of charts that have to measure the same whatever the space around them. Without it the plot grows to the width the container gives it and the coordinate space tracks the pixel width, so one unit stays one pixel: the marks spread out over more x space and the axis text keeps the size it was drawn at. Reflected.
formatXproperty only((v: number) => string) | undefinedFormats x ticks and tooltip values. Defaults to the figure with every decimal it has, grouped in the page's locale (1,234.5, 1.234,5). Property, not attribute.
formatYproperty only((v: number) => string) | undefinedFormats y ticks and tooltip values, with the same default as formatX. The y axis widens to its widest tick. Property, not attribute.
headingheadingstringstring | undefinedChart heading (the reference title prop, renamed because title is a native attribute). With what the chart shows, it names the plot for a reader; an aria-label or aria-labelledby on the host names the plot instead, and this summary then describes it.
heightheightnumber210ViewBox height of the plot in px.
hoverIndexhover-indexnumber-1Hovered point; -1 when idle. The index counts every point of every series in order, so with one point per row it lines up with a table or a bar chart of the same rows. The pointer and the arrow keys set it, the arrows walking a series's points from left to right; a write from outside outlines the mark and shows its tooltip and emits nothing, which is how sui-chart-synced mirrors a sibling. A point of a hidden series draws nothing.
loadingloadingbooleanfalseThe figures are still on their way: a shimmer stands in for the plot, the frame and its heading stay, and the host is aria-busy. Reflected, so a page can style the wait.
seriesproperty onlySuiScatterSeries[][]Series of points. A point with r draws as a bubble at 55% alpha; without it, a 4px point at 85%. name titles the tooltip. Legend keys are buttons that toggle a series. Property, not attribute.
subtitlesubtitlestringstring | undefinedOne-line context under the title.
widthwidthnumber440Width of the plot in px. The plot grows to the width its container gives it, and this is the starting figure, used until the container has been measured and wherever it measures zero; with fixedSize it is the plot's exact width. The card adds 34px of padding and border on top of it.
xLabelx-labelstringstring | undefinedName of the x measure: the tooltip row label. With yLabel set too, it joins the accessible name, because tick numbers alone do not say what a scatter plots.
yLabely-labelstringstring | undefinedName of the y measure: the tooltip row label, and with xLabel part of the accessible name.

Events

EventDetailReactVueDescription
suiHoverSuiChartScatterHoverDetailonSuiHover@sui-hoverFires when the pointer or the keyboard moves the hover to another point: { index, series, point }, all -1 when the pointer leaves the marks, or Escape or a blur takes the keyboard's point away.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves focus to the plot, the chart's one tab stop, from which the arrow keys reach the marks. Does nothing while there is nothing to plot.

Slots

SlotDescription
header-endControls at the end of the head row, after the title.

Types

SuiChartEvery typeHow the x labels thin: a fixed stride, or auto to measure the widest label against the room one point is given and draw only as many as fit.
No members.
SuiChartScatterHoverDetail interfaceDetail of the scatter's suiHover. index counts every point of every series in order, the value hoverIndex takes; series and point locate the same mark in the series prop. All three are -1 when nothing is hovered.
indexnumber
seriesnumber
pointnumber
SuiScatterPoint interfaceOne mark. r turns the point into a bubble (area encodes a third measure); name titles its tooltip.
xnumber
ynumber
r?number
name?string
SuiScatterSeries interfaceA scatter series. accent claims the single accent highlight; color overrides the ramp.
labelstring
pointsSuiScatterPoint[]
accent?boolean
color?string

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

See also