Scatter Chart

Plot points as dots to show how two values relate, or size each dot for a bubble chart, with a touch tooltip, pan and zoom, and dark mode built in.

Installation

npx @blotch/dolan add scatter-chart
import { UiScatterChart } from '../components/ui/scatter-chart';

This also adds Chart, which draws the preset, and installs the npm packages @tanstack/charts and d3-shape.

Usage

import { Component } from '@angular/core';
import { UiScatterChart } from '../components/ui/scatter-chart';

@Component({
  selector: 'app-dose-response',
  imports: [UiScatterChart],
  template: `<ui-scatter-chart
    [data]="measurements"
    label="Dose and response"
  />`,
})
export class DoseResponse {
  protected readonly measurements = [
    { x: 1, y: 3 },
    { x: 2, y: 4 },
    { x: 4, y: 6 },
    { x: 8, y: 10 },
  ];
}
  • data is an array of { x, y } points, both numbers. The axes fit rounded ranges to the data.
  • Each side is inset by 4%, so dots at the edges aren't cut off.
  • Touch the chart to see a point's values in a tooltip. Tap to pin it. Set label so a screen reader can say what the chart shows.

Customize

Colors

color fills every dot (default var(--primary)). A theme color such as var(--chart-2) switches with dark mode. A fixed color stays the same. Pass colors to give each dot its own color. The list repeats if it's shorter than the data.

<ui-scatter-chart [data]="clusters" [colors]="clusterColors" />
// The first two points are group A, the rest group B.
protected readonly clusterColors = [
  'var(--chart-1)',
  'var(--chart-1)',
  'var(--chart-2)',
  'var(--chart-2)',
];

For groups with a legend and named tooltips, use <ui-chart> with a z channel instead.

Shape

radius sets every dot's radius in px (default 4). Pass sizes to give each dot its own radius, which shows a third value as the dot's size. Points past the end of sizes use radius.

<ui-scatter-chart [data]="measurements" [sizes]="[4, 6, 5, 9]" />

Axes and range

xAxisLabel and yAxisLabel add axis titles. Horizontal gridlines are on by default: showGrid turns them off, and showXGrid adds vertical ones.

<ui-scatter-chart
  [data]="measurements"
  xAxisLabel="Dose"
  yAxisLabel="Response"
  [yMin]="0"
/>
  • xMin, xMax, yMin and yMax fix a bound instead of fitting the data, for example to compare two charts on one scale.
  • xTickFormat and yTickFormat turn tick values into labels.
  • tickCount sets roughly how many ticks each axis gets. The axis picks round values near it.

Padding

padding insets the plot, each side as a fraction (0 to 1) of its size. Sides you leave out keep the 4% default.

<ui-scatter-chart [data]="measurements" [padding]="{ top: 0.1 }" />

Size

The chart fills its container's width and is 200px tall. Set height for another height and width for a fixed width, both in px. Use height rather than a height class, because the preset always sets a height.

<ui-scatter-chart [data]="measurements" [height]="240" />

Interaction

Taps

pointTap emits the tapped { x, y } point. A tap on empty space emits nothing. Set tooltip to false to turn the tooltip off.

<ui-scatter-chart [data]="measurements" (pointTap)="selected.set($event)" />

pointTap fires inside the touch, so wrap router navigation in setTimeout (see the setTimeout rule).

Pan and zoom

Set zoomable to let users drag to pan, with +, − and 1:1 buttons. zoomAxes limits it to one axis.

<ui-scatter-chart [data]="measurements" [zoomable]="true" />

To zoom from your own controls, set showZoomControls to false and call zoomIn(), zoomOut() or resetZoom() on the component. See Pan and zoom for how it behaves.

API

UiScatterChart

InputTypeDefaultDescription
datareadonly XYDatum[]RequiredPoints to plot, each { x, y }.
colorstring'var(--primary)'Fill for every dot (any CSS color).
colorsreadonly string[][]One fill per dot, repeating. Overrides color when not empty.
radiusnumber4Dot radius in px.
sizesreadonly number[][]One radius per dot, for a bubble chart. Extra points use radius.
widthnumberfills containerFixed width in px.
heightnumber200Height in px.
tickCountnumber5Target number of ticks per axis.
showGridbooleantrueHorizontal gridlines at the y ticks.
showXGridbooleanfalseVertical gridlines at the x ticks.
xAxisLabelstring''X-axis title.
yAxisLabelstring''Y-axis title.
paddingChartPadding{} (4% each side)Inner inset, each side a 0–1 fraction of the plot.
xMinnumberfits the dataFixed x-axis lower bound.
xMaxnumberfits the dataFixed x-axis upper bound.
yMinnumberfits the dataFixed y-axis lower bound.
yMaxnumberfits the dataFixed y-axis upper bound.
xTickFormat(value: number) => stringautomaticFormats x-axis tick labels.
yTickFormat(value: number) => stringautomaticFormats y-axis tick labels.
tooltipbooleantrueShow a tooltip for the touched point. A tap pins it.
labelstring'Chart'What a screen reader announces for the chart.
colorScheme'light' | 'dark' | 'system'from provideChartThemeWhich color table the marks use.
zoomablebooleanfalseDrag to pan, with zoom buttons.
zoomAxes'x' | 'y' | 'xy''xy'Which axes pan and zoom.
showZoomControlsbooleantrueShow the +/−/1:1 buttons when zoomable.
minZoomnumber1Smallest zoom. 1 shows all the data.
maxZoomnumber8Largest zoom.
zoomStepnumber1.4Zoom factor per button press or method call.
classstring''Extra Tailwind classes for the chart's root view.
OutputTypeDescription
pointTapXYDatumThe point a tap landed on.
MethodDescription
zoomIn()Zooms in by zoomStep.
zoomOut()Zooms out by zoomStep.
resetZoom()Shows all the data again.

XYDatum is { x: number; y: number }. ChartPadding is { top?, right?, bottom?, left? }, each a fraction (0–1) of the plot's size. Both are exported from ../components/ui/chart.

scatterChartDefinition

function scatterChartDefinition(options: ScatterChartOptions): ChartDefinition;

Returns the TanStack Charts definition <ui-scatter-chart> renders: one dot mark. The options are the inputs that shape the chart, with the same defaults: data, color, colors, radius, sizes, tickCount, showGrid, showXGrid, xAxisLabel, yAxisLabel, padding, xMin, xMax, yMin, yMax, xTickFormat, yTickFormat and tooltip. Size, label, color scheme and zoom are inputs of <ui-chart> instead.

Start from it to add marks of your own, such as a trend line, and render the result with <ui-chart>. Put the trend line first so the dots draw over it. ci: 0 leaves out the confidence band.

import { defineChart, linearRegressionY } from '@tanstack/charts/universal';
import { scatterChartDefinition } from '../components/ui/scatter-chart';

const base = scatterChartDefinition({ data: measurements });

protected readonly withTrend = defineChart({
  ...base,
  marks: [
    linearRegressionY(measurements, { x: 'x', y: 'y', ci: 0, stroke: 'var(--chart-2)' }),
    ...base.marks,
  ],
});