Scatter Chart
Scatter and bubble chart for two numeric measures.
The chart is a standalone component. Import the styles once for the whole family.
import { KuiScatterChart } from '@kikita-labs/ui';Pass series of { x, y } points. There are no categories: both axes are numeric domains computed from the data extent.
<div class="chart-example">
<kui-scatter-chart ariaLabel="Age vs. income" [series]="series" [axes]="axes" />
</div>bubble reads r from each point as its radius in CSS pixels instead of a fixed dot size. It is a flag on the scatter chart and the radius is not clamped, so pass one that fits the plot.
<div class="chart-example">
<kui-scatter-chart
ariaLabel="Accounts by age, income and seats"
[series]="series"
[bubble]="true"
/>
</div>loading shows a loose cluster of shimmering dots; a series input without points shows the empty composition.
<div class="chart-example chart-example--stack">
<kui-scatter-chart ariaLabel="Loading accounts" [series]="series" [loading]="true" size="sm" />
<kui-scatter-chart ariaLabel="Empty accounts" [series]="[]" size="sm" />
</div>Charts normalize their input before drawing, so gaps and invalid values never become zeros.
series[].pointsholds{ x, y, r? }. A point with a non-finitex,yorris dropped.- Both axes are independent numeric domains computed from the data extent. Zero is not forced, and hiding a series keeps the scale.
- The identity of a series is
id ?? name. Two series or slices sharing an identity get distinct keys (a,a#1, ...), and in development mode the chart logs one warning for a duplicate explicitidand for adataarray whose length differs fromcategories. - A flat domain (every value equal) is expanded so the axis never collapses, and ticks are rounded to the precision their step needs.
series[].colororslices[].coloris optional; without it the marks use a round-robin over the eight--kui-chart-series-1..8tokens in the order given.
The legend shows automatically when there is more than one series or slice; the legend input overrides it. Clicking an item hides that series or slice.
- Hiding a series does not recompute the value-axis domain, so the remaining series keep a stable scale across toggles.
- Hovering a legend item highlights the matching mark, and hovering a mark highlights its legend entry. Hidden items stay interactive; "every series hidden" is a legend-only state and not the empty composition.
- A hidden item is marked by more than its pressed state: the label is struck through and the swatch becomes an outline.
Standalone legend
Every chart implements KuiChartLegendSource: a public legendItems() and hoveredLegendId() read surface plus toggleLegendItem(id) and setHoveredLegendId(id). Pass the chart through a template reference variable to kui-chart-legend to render the legend anywhere, and set [legend]="false" on the chart so it is not drawn twice. Project a kuiChartLegendItem template (context KuiChartLegendItemContext: $implicit is the KuiChartLegendEntry, hovered the cross-highlight state) to replace the default button markup; wiring the toggle and hover calls is the template's job.
<kui-line-chart
#chartRef
ariaLabel="Sessions per day"
[series]="series"
[categories]="categories"
[legend]="false"
/>
<kui-chart-legend [chart]="chartRef" />size sets the plot height in CSS pixels; the chart follows the width of its container.
sizeissm,mdorlg(defaultmd): a plot height of 200, 280 or 360px.- The chart measures its container with a
ResizeObserverand draws at one user unit per CSS pixel, so text, strokes and marks keep their pixel size at any width. The axis padding fits the widest measured tick label and the axis title. - Server rendering and the first client render use a nominal width (320, 480 or 640px) inside a graphic whose height is already final, so hydration does not shift the layout.
- Tick labels are thinned by their measured width plus an 8px gap so they never overlap, and a label wider than its slot is cut with an ellipsis (the full text stays in the element
<title>). Axis text defaults to--kui-text-sm-size; the--kui-chart-axis-text-font-sizetoken changes it. - Axis titles come from
axes.xTitleandaxes.yTitleand are drawn in the SVG. There is no built-in zoom, pan or decimation: aggregate very dense data before passing it in.
loading shows a skeleton shaped like the chart instead of a generic spinner. Missing data renders an empty composition.
- The scatter skeleton is a loose cluster of shimmering dots built from
[kuiSkeleton]. - An empty or missing input (no non-gap value anywhere) always renders the empty composition: a dashed border, a faded chart icon and the "No data" text. It is not the
kui-empty-statecomponent. - The loading and empty texts are
chartmessages.
The tooltip text and the numbers on axes and in the legend can be replaced.
protected formatTooltip(point: KuiChartPoint): string {
return `${point.categoryLabel}: $${point.value.toLocaleString('en-US')}`;
}tooltipoverrides the default text:<series>: (<x>, <y>), with the radius added for bubbles.valueFormatdefaults to the compact notation of the locale (1.2Kand3.4Min English,1,5 Mio.in German). It formats axis ticks and the default tooltip and legend numbers, and is never used for the alt-table, which shows exact values.- The tooltip is dismissible (
Escapecloses it without moving focus or the pointer until a new trigger) and persistent (it stays until hover or focus ends). It follows the pointer and ignores pointer events, so it is not hoverable (WCAG 1.4.13); see Known gaps. - Accessible names, role descriptions, the loading and empty texts, the table headers and the point text are
chartmessages. Every chart takes amessagesinput for one-off overrides.
The Table button switches the same data to a table representation with exact values.
- The alt-table is the accessible-charts recommendation of pairing a graphic with a tabular alternative, not a decorative duplicate. It lists the visible series with exact (not compact-formatted) numbers and has a caption with the chart name.
- It lists every point of a dense chart whatever the chart draws.
- The Table button keeps keyboard focus when the view switches.
- The columns are
Series,XandY(plusRwhenbubbleis on), not category-keyed rows.
Colour is never the only way to tell series apart (WCAG 1.4.1).
- With more than one series, marks use a marker shape per series index (circle, square, diamond, up and down triangle, cross, plus, star) and the legend swatch shows the same shape. A single series stays circles, and hiding another series never changes the shape.
- In forced colours (Windows High Contrast) the chart draws with the system colours
CanvasText,Canvas,GrayTextandHighlightonly. Series then differ by hatch pattern, by dash (lines) and by marker shape; patterns and dashes turn on automatically.
Tab enters a chart at the last focused mark and leaves with Tab; focus follows the arrows.
- Points are ordered by
xwithin a series. ArrowLeft and ArrowRight move along the series, ArrowUp and ArrowDown move between series, and Home and End jump to the first and last point of the series. - Escape dismisses the tooltip; Enter and Space on a legend item toggle it.
- The roving position is a key (series and category), not an index, so hiding a series moves it to the nearest remaining mark and re-focuses that mark if focus was inside the chart. Focus mirrors hover: the focused mark highlights its series and opens the tooltip.
A chart draws every shape it is given, up to a limit for line marks.
- Scatter charts draw every shape. The practical limit is a few thousand shapes; above that, aggregate before passing the data in.
- Library measurements in Chromium (a two-series line chart and a one-series scatter chart of the same point count): 500 points paint in 46 ms with 1,629 DOM nodes, 2,000 points in 115 ms with 6,123 nodes and 5,000 points in 259 ms with 15,123 nodes. Hiding a series on the 2,000-point line chart paints in 27 ms. Decimation is out of scope.
Set defaults.scatterChart once for the application or for a subtree. Each option resolves as local input > defaults.scatterChart.<option> > built-in default.
import { provideKikitaUi } from '@kikita-labs/ui';
export const appConfig: ApplicationConfig = {
providers: [
provideKikitaUi({
defaults: {
scatterChart: {
/* options below */
},
},
}),
],
};| Name | Type | Default | Description |
|---|---|---|---|
| size | 'sm' | 'md' | 'lg' | — | Chart size. |
| legend | boolean | — | Shows the chart legend. |
Set any of these on the component or an ancestor to restyle one part. Each token is optional: when it is not set, the part uses the role or scale token in the default column.
| Name | Type | Default | Description |
|---|---|---|---|
| --kui-chart-mark-stroke | CSS custom property | --kui-color-surface | Mark stroke |
| --kui-chart-pattern-ground-color | CSS custom property | --kui-color-surface | Ground under hatch patterns |
| --kui-chart-axis-title-color | CSS custom property | --kui-chart-axis-label-color | Axis title color |
| --kui-chart-empty-border | CSS custom property | --kui-color-border | Empty border color |
| --kui-chart-empty-color | CSS custom property | --kui-color-text-secondary | Empty color |
| --kui-chart-legend-item-bg-active | CSS custom property | --kui-color-surface-sunken | Legend item background, active |
| --kui-chart-legend-item-color-active | CSS custom property | --kui-color-text | Legend item color, active |
| Name | Type | Default | Description |
|---|---|---|---|
| --kui-chart-gap | CSS custom property | --kui-space-3 | Gap |
| --kui-chart-axis-text-font-size | CSS custom property | --kui-text-sm-size | Axis text font size |
| --kui-chart-loading-bars-gap | CSS custom property | --kui-space-3 | Loading bars gap |
| --kui-chart-loading-bars-padding | CSS custom property | --kui-space-4 | Loading bars padding |
| --kui-chart-empty-gap | CSS custom property | --kui-space-3 | Empty gap |
| --kui-chart-empty-radius | CSS custom property | --kui-radius-md | Empty corner radius |
| --kui-chart-empty-text-font-size | CSS custom property | --kui-text-sm-size | Empty text font size |
| --kui-chart-toolbar-gap | CSS custom property | --kui-space-3 | Toolbar gap |
| --kui-chart-legend-gap | CSS custom property | --kui-space-3 | Legend gap |
| --kui-chart-legend-item-gap | CSS custom property | --kui-space-2 | Legend item gap |
| --kui-chart-legend-item-radius | CSS custom property | --kui-radius-sm | Legend item corner radius |
| --kui-chart-legend-item-padding-inline | CSS custom property | --kui-space-1 | Legend item padding, inline |
| --kui-chart-legend-item-font-size | CSS custom property | --kui-text-sm-size | Legend item font size |
| --kui-chart-legend-swatch-size | CSS custom property | 12px | Legend swatch size |
Every text the component owns (accessible names, visible words, announcements) is a typed message with an English default. Override it for the application with provideKikitaUi({ messages }), or for a subtree with provideKuiMessages.
| Name | Type | Default | Description |
|---|---|---|---|
| loading | string | — | Accessible name of the loading state. Default Loading chart. |
| noData | string | — | Text shown when there is no data. Default No data. |
| viewChart | string | — | Button that shows the chart again from its data table. Default Chart. |
| viewTable | string | — | Button that shows the data table instead of the chart. Default Table. |
| columnCategory | string | — | Header of the category column in the data table. Default Category. |
| columnSeries | string | — | Header of the series column in the data table. Default Series. |
| columnLabel | string | — | Header of the label column in the data table. Default Label. |
| columnValue | string | — | Header of the value column in the data table. Default Value. |
| columnX | string | — | Header of the horizontal-axis column of a scatter chart's data table. Default X. |
| columnY | string | — | Header of the vertical-axis column of a scatter chart's data table. Default Y. |
| columnRadius | string | — | Header of the radius column of a bubble chart's data table. Default Radius. |
| legendLabel | string | — | Accessible name of the chart's inline legend. Default Legend. |
| lineLabel | string | — | Accessible name of a line chart. Default Line chart. |
| barLabel | string | — | Accessible name of a bar chart. Default Bar chart. |
| donutLabel | string | — | Accessible name of a donut chart. Default Donut chart. |
| scatterLabel | string | — | Accessible name of a scatter chart. Default Scatter chart. |
| lineRoleDescription | string | — | Role description of a line chart. Default line chart. |
| barRoleDescription | string | — | Role description of a bar chart. Default bar chart. |
| donutRoleDescription | string | — | Role description of a donut chart. Default donut chart. |
| scatterRoleDescription | string | — | Role description of a scatter chart. Default scatter chart. |
| pointWithCategory | KuiMessageFn<{ series: string; category: string; value: string; }> | — | Tooltip and name of a mark that has a category. Default {series} · {category}: {value}. |
| point | KuiMessageFn<{ series: string; value: string; }> | — | Tooltip and name of a mark without a category. Default {series}: {value}. |
| scatterPoint | KuiMessageFn<{ series: string; x: string; y: string; }> | — | Tooltip and name of a scatter point. Default {series}: ({x}, {y}). |
| bubblePoint | KuiMessageFn<{ series: string; x: string; y: string; radius: string; }> | — | Tooltip and name of a bubble. Default {series}: ({x}, {y}), radius {radius}. |
| slice | KuiMessageFn<{ label: string; value: string; percent: number; }> | — | Tooltip and name of a donut slice. Default {label}: {value} ({percent}%). |
Inputs verified against @kikita-labs/ui v2.0.0 public typings.
| Name | Type | Default | Description |
|---|---|---|---|
| series | readonly KuiChartScatterSeries[] | - | Required. { id?, name, color?, points: { x, y, r? }[] }. A point with a non-finite x, y or r is dropped. Without a point the empty composition renders. |
| bubble | boolean | false | Reads r from each point as the radius in CSS pixels. It is not clamped; the consumer owns a radius that fits the plot. |
| axes | KuiChartAxesOptions | {} | { x?, y?, gridLines?, xTitle?, yTitle? }: show or hide an axis, choose the grid lines and set the axis titles drawn in the SVG. |
| size | 'sm' | 'md' | 'lg' | undefined | undefined | Plot height of 200, 280 or 360px (a square of up to that size for the donut). Falls back to the chart defaults key, then md. |
| loading | boolean | false | Shows a skeleton placeholder shaped like the chart instead of the chart. |
| legend | boolean | undefined | undefined | Shows the inline legend. Falls back to the chart defaults key, then true when there is more than one series or slice. |
| valueFormat | (value: number) => string | compact notation | Axis tick, default tooltip and legend number formatting. The default is the compact notation of the locale; the alt-table always shows exact values. |
| tooltip | (point: KuiChartPoint) => string | undefined | built-in text | Overrides the default tooltip and point label text. |
| ariaLabel | string | undefined | undefined | Accessible name of the chart as a whole. Falls back to the chart message for this type (Scatter chart); prefer a content-specific name. |
| messages | Partial<KuiChartMessages> | undefined | undefined | Per-instance text overrides: names, role descriptions, loading and empty text, table headers and point text. They win over scoped and root messages. |
| legendItems() / hoveredLegendId() | KuiChartLegendSource | - | Public read surface of the chart legend, used by kui-chart-legend. |
| toggleLegendItem(id) / setHoveredLegendId(id) | KuiChartLegendSource | - | Hide or show a series or slice, and set the cross-highlighted legend item. |
| kui-chart-legend [chart] | KuiChartLegendSource | - | Renders the legend of any chart through a template reference variable. Project a kuiChartLegendItem template (KuiChartLegendItemContext) to replace the buttons. |
| KuiBarChartOptions / KuiLineChartOptions / KuiScatterChartOptions / KuiDonutChartOptions | interfaces | - | Shape of defaults.barChart, lineChart, scatterChart and donutChart: size and legend. |
The chart is an SVG graphic with an accessible name, labelled marks and a table alternative.
- The
<svg>is the graphic:role="graphics-document"with the name and the role description (aria-roledescription). The wrapper has no role, and axis text, gridlines and decorative shapes arearia-hidden. - Each point is
role="graphics-symbol img"with its ownaria-label, the same text as the tooltip, as an indivisible graphical unit per the WAI-ARIA Graphics Module. ariaLabelhas a chart-type fallback name from thechartmessages and should describe what the chart shows. It is distinct from each mark's own label (its value).- The marks use a roving tabindex (see Keyboard). The inline legend is a named group of native
<button aria-pressed>items at least 24px high. - One shared tooltip overlay is retargeted between marks, so hover and keyboard focus show the same text. On pointer hover it appears at the pointer and follows it; keyboard focus anchors it to the focused mark. On touch, a tap pins the tooltip until an outside tap, Escape or a tap on a different mark, and it always renders below 768px.
- Pointer hit targets: each point has an invisible hit circle of at least 12px radius (a 24px target, WCAG 2.5.8) or the bubble radius if larger, which carries the role, label and tabindex, and a visible
aria-hiddenmarker at the real size. - Text is at least 12px on screen; axis text defaults to 13px at any width.
prefers-reduced-motion: reducedisables the hover and dim transition and the donut sweep of sibling charts.- The alt-table is a real accessible alternative to the graphic.
The library checks keyboard, the accessibility tree, axe, forced-colours media emulation and reduced motion automatically. A session with a real screen reader has not been run and stays pending.
Documented limits of the installed package.
- The chart tooltip follows the pointer and is not hoverable, so it does not meet the hoverable part of WCAG 1.4.13 (it is dismissible and persistent). A tooltip that is hoverable and follows the cursor would catch the cursor, so anchoring it to the mark is an open design decision.
- The
bubbleradius is not clamped to any minimum or maximum; the consumer owns a radius that fits the plot area. - There is no zoom, pan or decimation, and bar and scatter charts draw every shape (see Performance).
- The marker shapes and hatch patterns are engineering decisions that the designer still has to confirm. A formal assistive-technology pass with a real screen reader and a real Windows forced-colours session is still pending.