Skip to content

Charts

Five charts of plain series: <cae-column-chart> (vertical bars, stacked or side by side), <cae-line-chart>, <cae-heatmap>, <cae-state-grid> (a state per row and column) and <cae-bar-chart> (horizontal bars). Register them with @contraagent/elements/charts/define; the types and the arithmetic they share are in @contraagent/elements/charts.

docs/examples/charts.ts
// Charts: a CI pipeline's minutes by day and stage (stacked columns), its queue
// time (lines), its builds by weekday and hour (a heatmap), how its nightly builds
// ended (a state grid) and its test funnel (horizontal bars). The host gives each
// chart its unit's format.
import '@contraagent/elements/charts/define';
import type { ChartSeries } from '@contraagent/elements/charts';
const days = ['10-08', '10-09', '10-10', '10-11', '10-12', '10-13', '10-14'];
const minutes: ChartSeries[] = [
{ name: 'build', values: [42, 38, 51, 12, 9, 47, 44] },
{ name: 'test', values: [65, 70, 58, 20, null, 61, 66] },
{ name: 'deploy', values: [8, 8, 9, 2, 2, 10, 8] },
];
export function mount(host: HTMLElement): void {
const columns = document.createElement('cae-column-chart');
columns.label = 'Minutes by day';
columns.x = days;
columns.series = minutes;
columns.stacked = true;
columns.format = (v) => `${v} min`;
const lines = document.createElement('cae-line-chart');
lines.label = 'Queue time';
lines.x = days;
lines.series = [
{ name: 'median', values: [1.2, 1.4, 0.9, 0.3, 0.2, 1.8, 1.1] },
{ name: 'slowest', values: [6, 9, 4, 1, 1, 12, 7] },
];
lines.format = (v) => `${v} min`;
const heat = document.createElement('cae-heatmap');
heat.label = 'Builds by weekday and hour';
heat.x = ['9', '10', '11', '12', '13', '14', '15', '16', '17'];
heat.y = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'];
// A count per weekday and hour; Wednesday at 13 has no record (null: an empty cell).
heat.values = heat.y.map((_, r) => heat.x.map((_, c) => (r === 2 && c === 4 ? null : (r * 7 + c * 3) % 9)));
heat.strings = { scale: (low, high) => `${low} to ${high} builds` };
const bars = document.createElement('cae-bar-chart');
bars.label = 'Tests this week';
bars.bars = [
{ label: 'written', value: 1240 },
{ label: 'run', value: 1180 },
{ label: 'passed', value: 1162 },
{ label: 'flaky', value: 18, tone: 'attention' },
];
const nightly = document.createElement('cae-state-grid');
nightly.label = 'Nightly builds by weekday and hour';
nightly.x = ['0', '1', '2', '3'];
nightly.y = ['Mon', 'Tue'];
const passed = { label: 'passed', tone: 'done' as const };
// A state per weekday and hour: its word, its tone, and a fact for the tooltip; null: no build.
nightly.cells = [
[passed, { label: 'failed', tone: 'failed', tip: 'build 4127' }, passed, null],
[passed, passed, { label: 'skipped', tone: 'muted' }, passed],
];
host.replaceChildren(columns, lines, heat, nightly, bars);
}

A chart takes labels (x) and series, each a name and a value per label; a null value is a value the series has not: no bar, a break in a line, an empty cell. A heatmap takes its rows (y), its columns (x) and values[row][column]; a state grid the same rows and columns and cells[row][column], each a state’s label, its tone and an optional tip (null: no cell); a bar chart takes bars, each a label, a value and an optional tone.

src/charts/types.ts
/** One series: a value per label of the chart's `x`, in that order; null where it has none. */
export interface ChartSeries {
name: string;
values: readonly (number | null)[];
}
/** One horizontal bar: its label and value; `tone` gives it a status colour. */
export interface ChartBar {
label: string;
value: number;
tone?: Tone;
}
/** A number in words (`$1.20`, `42%`, `2m 05s`): the host's format for the chart's unit. */
export type ValueText = (value: number) => string;

format writes a value in words ($1.20, 42%, 2m 05s): on the axis, in the tooltip and in the table. It is the host’s, since the chart does not know the unit.

The series take the series colours (--ca-series-1 to --ca-series-6) in their order, so a series keeps its colour whatever its values and its rank. A chart never makes up a seventh colour: past six series, a column chart sums the sixth and later into one series, strings.other, in the neutral --ca-meter; a line chart draws them in that colour and names each in the legend and the tooltip. A heatmap uses one hue, light to dark in five steps between its lowest and highest value. A state grid colours a cell by its tone, so a failure and what needs attention stand out and a cell that ended well stays quiet; the colour is never alone: a cell wide enough holds its tone’s mark (✓ done, ✕ failed, ◌ attention; a done cell’s mark is quieter), the legend names each state once, failures and what needs attention first, and the tooltip and the table say the state in words. A bar with a tone takes that status colour; a status colour is never a series.

Two or more series have a legend; one series has none, since the chart’s title names it. Pointing at a label, or moving to it with the arrow keys once the chart has the focus (up and down by a row in a heatmap and a state grid, Home and End to the ends, Escape to close), shows its values in a tooltip. Every chart has a switch to a table of the same values (strings.showTable, strings.showChart), which is also how a screen reader reads it. A bar chart writes each value beside its bar.

height sets a column or line chart’s height in px (200 by default; the rows of a heatmap and of a state grid are 22px each). The parts are plot, table, switch, legend and empty.