What a value is measured against: extent, ticks and labels, categories, several axes per side, time on the local clock, and a working-time axis that leaves out the hours nobody works (a scale).
import { useChart, workingCalendar } from "@umriss-ui/charts";
Every scale is linear. An axis reads what its rows hold: a number is a position, a string a category, which gets a place, a tick and its name (ADR-0052); a working calendar maps time before it is drawn and marks every seam it took out (ADR-0001).
An x axis zooms where it is zoomable; domain is then where it starts. That has a page of its own, Zoom and pan.
A fixed domain holds the axis still, tickFormat writes the labels, and a typed Tooltip renders its own content.
import { Legend, Tooltip, useChart } from "@umriss-ui/charts";
import type { TooltipHit } from "@umriss-ui/charts";
interface Week {
week: number;
/** Share of the team's capacity booked on client work, in per cent. */
web: number;
apps: number;
}
const BOOKED: Week[] = [
{ week: 1, web: 62, apps: 71 },
{ week: 2, web: 74, apps: 78 },
{ week: 3, web: 81, apps: 76 },
{ week: 4, web: 79, apps: 69 },
{ week: 5, web: 86, apps: 72 },
{ week: 6, web: 91, apps: 80 },
{ week: 7, web: 88, apps: 84 },
{ week: 8, web: 70, apps: 88 },
{ week: 9, web: 77, apps: 93 },
{ week: 10, web: 83, apps: 90 },
{ week: 11, web: 85, apps: 82 },
{ week: 12, web: 80, apps: 86 },
];
const percent = (v: number) => `${v.toFixed(0)} %`;
function WeekTooltip({ hit }: { hit: TooltipHit<Week> }) {
const point = hit.points[0];
return (
<div className="custom-tooltip">
<strong>Week {hit.xValue}</strong>
{point === undefined ? null : (
<span>
{point.seriesName}: {percent(point.yValue)} booked
</span>
)}
</div>
);
}
export default function Configuration() {
const { Chart, XAxis, YAxis, Line } = useChart(BOOKED);
return (
<Chart height={280} padding={{ top: 12, right: 16, bottom: 8, left: 8 }} ariaLabel="Share of capacity booked per team and week">
<XAxis value="week" label="Week" tickCount={6} />
<YAxis domain={[0, 100]} tickFormat={percent} label="Booked" />
<Line value="web" name="Web" markers="always" strokeWidth={2} />
<Line value="apps" name="Apps" markers="always" color="#7c3aed" />
<Legend placement="bottom" />
{/* The type argument makes the hit fully typed in the render prop. */}
<Tooltip<Week> mode="nearest" render={(hit) => <WeekTooltip hit={hit} />} />
</Chart>
);
}
Several y axes stack outwards on their side, and a series picks its axes by yAxisId and xAxisId; only the first axis draws a grid.
import { Legend, Tooltip, useChart } from "@umriss-ui/charts";
/* Data from the operations world, written out here so the example runs on its own. */
/** A small LCG - the same numbers on every computer. */
function random(seed: number): () => number {
let s = seed >>> 0;
return () => {
s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
return s / 4294967296;
};
}
/** When this morning's load test of Search began. */
interface LoadTestPoint {
/** Minutes since the test began. */
minute: number;
/** CPU load of the busiest node, in per cent. */
cpu: number;
requestsPerHour: number;
/** 95th percentile latency, in ms. */
p95: number;
}
/** An hour of load test, minute by minute: three magnitudes far apart. */
const LOAD_TEST: readonly LoadTestPoint[] = (() => {
const r = random(99);
let cpu = 21;
let requestsPerHour = 128_000;
let p95 = 640;
return Array.from({ length: 60 }, (_, minute) => {
cpu += (r() - 0.5) * 0.9;
requestsPerHour += (r() - 0.5) * 9000;
p95 += (r() - 0.5) * 40;
return { minute, cpu, requestsPerHour, p95 };
});
})();
export default function Axes() {
const { Chart, XAxis, YAxis, Line } = useChart(LOAD_TEST);
return (
<Chart height={340} ariaLabel="A load test: CPU, throughput and latency on three y axes">
<XAxis value="minute" label="Minute of the test" />
<XAxis id="seconds" position="top" value={(d) => d.minute * 60} label="Seconds into the test" />
<YAxis id="cpu" position="left" label="CPU %" />
<YAxis id="requests" position="right" label="Requests/h" />
<YAxis id="latency" position="right" label="p95 ms" />
<Line value="cpu" yAxisId="cpu" name="CPU" />
<Line value="requestsPerHour" xAxisId="seconds" yAxisId="requests" name="Throughput" dash={[4, 4]} />
<Line value="p95" yAxisId="latency" name="p95" />
<Legend placement="top" />
<Tooltip mode="x" />
</Chart>
);
}
Pass the working hours as calendar and the axis drops nights and the weekend, marking every seam where time was taken out.
import { Tooltip, useChart } from "@umriss-ui/charts";
/* Data from the logistics world, written out here so the example runs on its own. */
/** A small LCG - the same numbers on every computer. */
function random(seed: number): () => number {
let s = seed >>> 0;
return () => {
s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
return s / 4294967296;
};
}
const MINUTE = 60_000;
const on = (month: number, day: number, hours = 0) => new Date(2026, month - 1, day, hours).getTime();
/** The North depot's sorting hours: Monday to Friday, 06:00 to 22:00. */
const SORTING_HOURS = Array.from({ length: 5 }, (_, day) => ({ from: on(3, 9 + day, 6), to: on(3, 9 + day, 22) }));
interface SortedPoint {
t: number;
/** Parcels sorted per hour. */
parcels: number;
}
/** Last week's sorting at the North depot, quarter hour by quarter hour -
only in its sorting hours. */
const SORTED: readonly SortedPoint[] = (() => {
const r = random(1963);
const points: SortedPoint[] = [];
let parcels = 420;
for (const { from } of SORTING_HOURS) {
for (let quarter = 0; quarter < 4 * 16; quarter++) {
parcels += (r() - 0.5) * 26;
points.push({ t: from + quarter * 15 * MINUTE, parcels: Math.round(parcels) });
}
}
return points;
})();
const weekdayAndTime = (v: number) =>
new Date(v).toLocaleString("en-GB", { weekday: "short", hour: "2-digit", minute: "2-digit" });
export default function WorkingTime() {
const { Chart, XAxis, YAxis, Line } = useChart(SORTED);
return (
<Chart height={280} ariaLabel="Parcels sorted per hour across last week's sorting hours">
<XAxis
value="t"
calendar={SORTING_HOURS}
tickFormat={weekdayAndTime}
tickCount={8}
label="Sorting hours"
/>
<YAxis label="Parcels/h" />
<Line value="parcels" name="Sorted" />
<Tooltip mode="x" />
</Chart>
);
}
Rows that hold a Date put the axis in time; time does the same for milliseconds and for ISO strings. Ticks stand on the local clock, labelled by hour, with the date where a day begins.
import { Tooltip, useChart } from "@umriss-ui/charts";
/* Data from the operations world, written out here so the example runs on its own. */
/** A small LCG - the same numbers on every computer. */
function random(seed: number): () => number {
let s = seed >>> 0;
return () => {
s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
return s / 4294967296;
};
}
const at = (day: number, hours: number, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime();
const MINUTE = 60_000;
/** Tuesday, 17 March 2026, 10:30 - the moment the screens are read at. */
const NOW = at(17, 10, 30);
interface Service {
id: string;
name: string;
team: string;
/** 1 is customer-facing and pages at night; 3 waits for the morning. */
tier: 1 | 2 | 3;
/** The latency objective: the 95th percentile stays below this, in ms. */
latencySlo: number;
/** The availability promised for a month, in per cent. */
availabilityTarget: number;
}
const SERVICES: readonly Service[] = [
{ id: "checkout", name: "Checkout", team: "Payments", tier: 1, latencySlo: 300, availabilityTarget: 99.95 },
{ id: "billing", name: "Billing", team: "Payments", tier: 1, latencySlo: 400, availabilityTarget: 99.9 },
{ id: "sign-in", name: "Sign-in", team: "Identity", tier: 1, latencySlo: 200, availabilityTarget: 99.95 },
{ id: "search", name: "Search", team: "Discovery", tier: 1, latencySlo: 250, availabilityTarget: 99.9 },
{ id: "images", name: "Image service", team: "Discovery", tier: 2, latencySlo: 500, availabilityTarget: 99.5 },
{ id: "notifications", name: "Notifications", team: "Messaging", tier: 2, latencySlo: 800, availabilityTarget: 99.5 },
{ id: "webhooks", name: "Webhooks", team: "Integrations", tier: 2, latencySlo: 1000, availabilityTarget: 99.5 },
{ id: "reports", name: "Reporting", team: "Insights", tier: 3, latencySlo: 2000, availabilityTarget: 99 },
];
interface MetricPoint {
t: Date;
/** Median and 95th percentile latency, in ms. */
p50: number;
p95: number;
/** Failed requests, in per cent. */
errorRate: number;
/** Requests per minute. */
requests: number;
}
/** Requests per minute at the busiest hour, by tier. */
const VOLUME = { 1: 900, 2: 400, 3: 120 } as const;
/** A service's latency and error rate every five minutes of today, from
midnight up to now. Busy by day, quiet at night; Checkout's 95th percentile
climbs well past its objective from 09:40 to 10:10, and its errors with it. */
function metrics(serviceId: string): MetricPoint[] {
const index = SERVICES.findIndex((one) => one.id === serviceId);
const service = SERVICES[index];
if (service === undefined) throw new Error(`No service "${serviceId}".`);
const r = random(1000 + index * 37);
const points: MetricPoint[] = [];
let drift = 0;
for (let t = at(17, 0); t <= NOW; t += 5 * MINUTE) {
const hour = (t - at(17, 0)) / (60 * MINUTE);
const load = 0.35 + 0.65 * Math.max(0, Math.sin((Math.PI * (hour - 5)) / 16));
drift = 0.8 * drift + (r() - 0.5) * 0.08;
const base = service.latencySlo * (0.45 + 0.2 * load + drift);
const surge = serviceId === "checkout" && t >= at(17, 9, 40) && t < at(17, 10, 10) ? 1.9 : 1;
const p95 = Math.round(base * surge * (1 + r() * 0.08));
points.push({
t: new Date(t),
p50: Math.round(p95 * (0.42 + r() * 0.06)),
p95,
errorRate: Math.round((0.05 + r() * 0.12 + (surge > 1 ? 2.4 + r() : 0)) * 100) / 100,
requests: Math.round(VOLUME[service.tier] * load * (1 + (r() - 0.5) * 0.1)),
});
}
return points;
}
const CHECKOUT = metrics("checkout");
export default function TimeAxis() {
const { Chart, XAxis, YAxis, Line } = useChart(CHECKOUT);
return (
<Chart height={260} ariaLabel="Checkout's error rate today">
<XAxis value="t" />
<YAxis label="Errors %" />
<Line value="errorRate" name="Error rate" />
<Tooltip mode="x" />
</Chart>
);
}
With alignTicks a further y axis widens its domain until its ticks, in 1-2-5 steps, fall on the first axis' grid lines.
import { Legend, Tooltip, useChart } from "@umriss-ui/charts";
/* Data from the operations world, written out here so the example runs on its own. */
/** A small LCG - the same numbers on every computer. */
function random(seed: number): () => number {
let s = seed >>> 0;
return () => {
s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
return s / 4294967296;
};
}
const at = (day: number, hours: number, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime();
const MINUTE = 60_000;
interface Service {
id: string;
name: string;
team: string;
/** 1 is customer-facing and pages at night; 3 waits for the morning. */
tier: 1 | 2 | 3;
/** The latency objective: the 95th percentile stays below this, in ms. */
latencySlo: number;
/** The availability promised for a month, in per cent. */
availabilityTarget: number;
}
const SERVICES: readonly Service[] = [
{ id: "checkout", name: "Checkout", team: "Payments", tier: 1, latencySlo: 300, availabilityTarget: 99.95 },
{ id: "billing", name: "Billing", team: "Payments", tier: 1, latencySlo: 400, availabilityTarget: 99.9 },
{ id: "sign-in", name: "Sign-in", team: "Identity", tier: 1, latencySlo: 200, availabilityTarget: 99.95 },
{ id: "search", name: "Search", team: "Discovery", tier: 1, latencySlo: 250, availabilityTarget: 99.9 },
{ id: "images", name: "Image service", team: "Discovery", tier: 2, latencySlo: 500, availabilityTarget: 99.5 },
{ id: "notifications", name: "Notifications", team: "Messaging", tier: 2, latencySlo: 800, availabilityTarget: 99.5 },
{ id: "webhooks", name: "Webhooks", team: "Integrations", tier: 2, latencySlo: 1000, availabilityTarget: 99.5 },
{ id: "reports", name: "Reporting", team: "Insights", tier: 3, latencySlo: 2000, availabilityTarget: 99 },
];
interface MetricPoint {
t: number;
/** Median and 95th percentile latency, in ms. */
p50: number;
p95: number;
/** Failed requests, in per cent. */
errorRate: number;
/** Requests per minute. */
requests: number;
}
/** Requests per minute at the busiest hour, by tier. */
const VOLUME = { 1: 900, 2: 400, 3: 120 } as const;
const HOUR = 60 * MINUTE;
const DAY = 24 * HOUR;
/** Monday, 9 March 2026, 00:00 - the start of last week. */
const LAST_WEEK = at(9, 0);
/** Search returned nothing for some regions (INC-1043): Saturday, 18:30 to 19:55. */
const SEARCH_OUTAGE = [at(14, 18, 30) - LAST_WEEK, at(14, 19, 55) - LAST_WEEK] as const;
/** A service's last week, Monday to Monday, one point every `step`
milliseconds: busy by day, quieter at the weekend. During INC-1043 Search's
95th percentile more than doubles and its errors climb. The hour is counted,
not asked of a Date - a week of seconds is 604,800 points. */
function week(serviceId: string, step: number): MetricPoint[] {
const index = SERVICES.findIndex((one) => one.id === serviceId);
const service = SERVICES[index];
if (service === undefined) throw new Error(`No service "${serviceId}".`);
const r = random(2000 + index * 37);
const n = Math.floor((7 * DAY) / step);
const points = new Array<MetricPoint>(n);
/* The drift wanders at the same pace whatever the step. */
const keep = Math.pow(0.8, step / (5 * MINUTE));
const kick = 0.08 * Math.sqrt(1 - keep * keep) / 0.6;
let drift = 0;
for (let i = 0; i < n; i++) {
const offset = i * step;
const day = Math.floor(offset / DAY);
const hour = (offset % DAY) / HOUR;
const load = (day >= 5 ? 0.85 : 1) * (0.35 + 0.65 * Math.max(0, Math.sin((Math.PI * (hour - 5)) / 16)));
drift = keep * drift + (r() - 0.5) * kick;
const outage = serviceId === "search" && offset >= SEARCH_OUTAGE[0] && offset < SEARCH_OUTAGE[1];
const p95 = Math.round(service.latencySlo * (0.45 + 0.2 * load + drift) * (outage ? 2.3 : 1) * (1 + r() * 0.08));
points[i] = {
t: LAST_WEEK + offset,
p50: Math.round(p95 * (0.42 + r() * 0.06)),
p95,
errorRate: Math.round((0.05 + r() * 0.12 + (outage ? 3 : 0)) * 100) / 100,
requests: Math.round(VOLUME[service.tier] * load * (1 + (r() - 0.5) * 0.1)),
};
}
return points;
}
const SEARCH_WEEK = week("search", 5 * 60_000);
export default function AlignedTicks() {
const { Chart, XAxis, YAxis, Line } = useChart(SEARCH_WEEK);
return (
<Chart height={260} ariaLabel="Search requests and latency over last week on one grid">
<XAxis value="t" time />
<YAxis label="Requests/min" />
<YAxis id="latency" position="right" label="ms" alignTicks />
<Line value="requests" name="Requests" />
<Line value="p95" yAxisId="latency" name="p95" />
<Legend placement="top" />
<Tooltip mode="x" />
</Chart>
);
}
In the plant: xAxisId and yAxisId work alike on every series kind. An area, bars, a line and a scatter read hourly counts on the right and top axes; the kiln's minutes keep the first two.
import { Legend, Tooltip, useChart } from "@umriss-ui/charts";
/* Data from the plant world, written out here so the example runs on its own. */
/** A small LCG - reproducible across runs and platforms. */
function random(seed: number): () => number {
let s = seed >>> 0;
return () => {
s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
return s / 4294967296;
};
}
/** The early shift, 06:00 to 14:00. */
const SHIFT_MINUTES = 480;
/** The kiln's zone 3, in °C: the numbers every part reads it against, once.
Between the two warnings lies the tolerance - a tile fired outside it is
scrap; `returnTo` is the alarm's dead band. */
const KILN = { target: 1200, tolerance: [1185, 1215], alarm: 1230, returnTo: 1222 } as const;
/** The kiln fires one tile every quarter of a minute when nothing holds it up -
the ideal cycle time of the OEE's performance. */
const IDEAL_CYCLE_MINUTES = 0.25;
/** One minute of the line. */
interface Reading {
/** Minutes since the start of the shift. */
minute: number;
/** Zone 3 of the kiln, in °C. */
kiln: number;
/** The exit pyrometer, in °C - `null` while it sends nothing. */
exit: number | null;
/** Whether the belt runs. */
running: boolean;
/** Tiles out of the kiln in this minute. */
fired: number;
/** Of them, the ones fired inside the tolerance. */
good: number;
}
/** A tile taken off the belt and measured - one every ten minutes while it runs. */
interface Sample {
minute: number;
/** The tile's length after firing, in mm (nominal 600). */
length: number;
}
/** A batch of tiles through the three stations. */
interface Batch {
id: string;
name: string;
/** How many tiles the batch is planned for. */
tiles: number;
/** Minutes since the start of the shift, per station. */
press: readonly [number, number];
dryer: readonly [number, number];
kiln: readonly [number, number];
}
interface Plant {
readings: readonly Reading[];
samples: readonly Sample[];
batches: readonly Batch[];
}
/* When the exit pyrometer goes silent, and for how long. Fixed rather than
seeded, so that a running control room shows its reading turn stale, then
lost, and come back. */
const SILENT_FROM = 288;
const SILENT_FOR = 50;
/** The shift, from a seed. */
function plant(seed: number): Plant {
const r = random(seed);
/* The one excursion: a burner overshoots somewhere between 02:30 and 03:30
into the shift, for half an hour, peaking near 1242 °C. The operator stops
the feed twenty-two minutes in, for eighteen minutes. The noise around it
is too small to reach a warning on its own, so this is the only crossing -
which is what the tests hold. */
const surgeFrom = 150 + Math.floor(r() * 60);
const surgeFor = 30;
const stopFrom = surgeFrom + 22;
const stopFor = 18;
/* The plan: batches through the kiln one after another, each pressed an
hour and dried half an hour before - the first ones during the night
shift. Tiles come out of the kiln only while a batch is in it, so the
OEE's count and the batches' counts are the same tiles. The plan is a
plan: a stop of the belt does not move it. */
const batches: Batch[] = [];
for (let at = 0, i = 0; at < SHIFT_MINUTES; i++) {
const length = 70 + Math.floor(r() * 25);
batches.push({
id: `b-${4121 + i}`,
name: `B-${4121 + i}`,
tiles: Math.round(length / IDEAL_CYCLE_MINUTES / 10) * 10,
press: [at - 60, at - 60 + length],
dryer: [at - 30, at - 30 + length],
kiln: [at, at + length],
});
at += length + 5;
}
const readings: Reading[] = [];
const samples: Sample[] = [];
let drift = 0;
for (let minute = 0; minute < SHIFT_MINUTES; minute++) {
drift = 0.9 * drift + (r() - 0.5) * 3;
const into = minute - surgeFrom;
const surge = into >= 0 && into <= surgeFor ? 42 * Math.sin((Math.PI * into) / surgeFor) : 0;
const kiln = Math.round((KILN.target + drift + surge) * 10) / 10;
const running = !(minute >= stopFrom && minute < stopFrom + stopFor);
const loaded = batches.some((batch) => batch.kiln[0] <= minute && minute < batch.kiln[1]);
const jam = r() < 0.12;
const fired = running && loaded ? (jam ? 3 : 4) : 0;
const inTolerance = kiln >= KILN.tolerance[0] && kiln <= KILN.tolerance[1];
const silent = minute >= SILENT_FROM && minute < SILENT_FROM + SILENT_FOR;
const exit = silent ? null : Math.round((kiln - 380 + (r() - 0.5) * 4) * 10) / 10;
readings.push({ minute, kiln, exit, running, fired, good: inTolerance ? fired : 0 });
if (running && minute % 10 === 0) {
/* Hotter shrinks more: the length follows the kiln, so the control
chart sees the excursion the trend sees. */
const length = 600 + (KILN.target - kiln) * 0.05 + (r() - 0.5) * 0.6;
samples.push({ minute, length: Math.round(length * 100) / 100 });
}
}
return { readings, samples, batches };
}
/** One hour of the shift, counted. */
interface HourCount {
/** The middle of the hour, in hours since the start of the shift. */
hour: number;
/** Tiles the plan expects while a batch is in the kiln. */
planned: number;
fired: number;
good: number;
/** Fired outside the tolerance. */
scrap: number;
}
/** The minutes of the shift, summed up hour by hour. */
function hourly(shift: Plant): HourCount[] {
return Array.from({ length: SHIFT_MINUTES / 60 }, (_, h) => {
const minutes = shift.readings.slice(h * 60, (h + 1) * 60);
const loaded = minutes.filter((m) => shift.batches.some((b) => b.kiln[0] <= m.minute && m.minute < b.kiln[1])).length;
const fired = minutes.reduce((sum, m) => sum + m.fired, 0);
const good = minutes.reduce((sum, m) => sum + m.good, 0);
return { hour: h + 0.5, planned: loaded / IDEAL_CYCLE_MINUTES, fired, good, scrap: fired - good };
});
}
const SHIFT = plant(7);
const HOURS = hourly(SHIFT);
export default function EveryKind() {
const { Chart, XAxis, YAxis, Line, Area, Bar, Scatter } = useChart(SHIFT.readings);
return (
<Chart height={340} ariaLabel="The kiln's temperature by the minute above the tiles it fired by the hour">
<XAxis value="minute" domain={[0, SHIFT_MINUTES]} label="Minute of the shift" />
{/* This axis reads the hourly rows its series bring, so it names their type. */}
<XAxis<HourCount>
id="hour"
position="top"
value="hour"
domain={[0, SHIFT_MINUTES / 60]}
ticks={HOURS.map((d) => d.hour)}
tickFormat={(v) => `Hour ${Math.ceil(v)}`}
/>
{/* The kiln above, the counts below: each axis keeps to its own half. */}
<YAxis domain={[1050, 1250]} label="°C" />
<YAxis id="tiles" position="right" domain={[0, 500]} label="Tiles per hour" />
<Line value="kiln" name="Zone 3" />
<Area data={HOURS} value="planned" xAxisId="hour" yAxisId="tiles" name="Planned" strokeWidth={0} />
<Bar data={HOURS} value="fired" xAxisId="hour" yAxisId="tiles" name="Fired" />
<Line data={HOURS} value="good" xAxisId="hour" yAxisId="tiles" name="Good" />
<Scatter data={HOURS} value="scrap" xAxisId="hour" yAxisId="tiles" name="Scrap" />
<Legend placement="top" />
<Tooltip mode="x" />
</Chart>
);
}
categories is the order and the extent: a day without a row keeps its place, and the rows may come as they like. categoryLabel writes a name where the rows hold a key.
import { Tooltip, useChart } from "@umriss-ui/charts";
/* Minutes late per working day, as the tour log sends them: only the days
that had a delay, the worst first. */
const LATE = [
{ day: "thu", minutes: 164 },
{ day: "mon", minutes: 71 },
{ day: "fri", minutes: 58 },
{ day: "tue", minutes: 43 },
];
const WEEK = ["mon", "tue", "wed", "thu", "fri"];
const NAMES: Record<string, string> = { mon: "Monday", tue: "Tuesday", wed: "Wednesday", thu: "Thursday", fri: "Friday" };
export default function Categories() {
const { Chart, XAxis, YAxis, Bar } = useChart(LATE);
return (
<Chart height={260} ariaLabel="Minutes late per working day">
<XAxis value="day" categories={WEEK} categoryLabel={(day) => NAMES[day] ?? day} />
<YAxis label="Minutes late" />
<Bar value="minutes" name="Late" />
<Tooltip mode="x" />
</Chart>
);
}
The keys of Chart apply here.
XAxisProps<T>| Name | Type | Default | Description |
|---|---|---|---|
valuerequired | AxisField<T> | ((d: T, index: number) => AxisValue) | — | Where a row lies on this axis: a field of the row, compared by its
name, or a function of it, compared by its source text as a series' is
(ValueFunction) - with the same closure limit. A number is a position,
a Date an instant, a string a category: every distinct one gets a
place of its own, in the order the rows bring them, a tick and its name
as the label (ADR-0052). The type decides, never the content - "2024"
is a category, 2024 a number - and a row without a value has no
place. The axis reads the rows
of every series bound to it; where those bring their own data, name
their row: <XAxis<HourCount> value="hour" />.Shown in: Fix the domain and name the ticks, Add axes for other magnitudes, Leave out the hours nobody works and 74 more |
domain | | "nice" | "nice" | "nice" widens the data's extent to ticks - to named ticks where
there are any, on a time axis to whole units of a step of hours or
longer -, "data" keeps it, a pair is fixed: numbers, on a time axis
milliseconds or two Dates, on a category axis places. On a zoomable
axis it is the start: what the axis shows while the chart's view names
no span for it.Shown in: Bind every kind of series to a second axis, Read it by keyboard and screen reader (Chart), Gestures and keys (Zoom and pan) and 11 more |
position | "bottom" | "top" | "bottom" | Which edge the axis stands at. A second x axis on the opposite edge is how
a series counts in a unit of its own. Shown in: Add axes for other magnitudes, Bind every kind of series to a second axis |
time | boolean | — | The values are instants: milliseconds since the epoch, or with time
said ISO 8601 strings as an API sends them, read by the platform's rule -
a date with a time and no offset is local time, a date alone is UTC; a
string that is none is not drawn. Rows that hold a Date put the axis
in time without time. Ticks on local boundaries from the minute to the
month, labels by level in en-GB with a 24-hour clock - 15:00, 17 Mar,
Mar 2026 - and the date on the first tick and on the first of a new
day (17 Mar 00:00); a day tick there carries the year. The tooltip's x
value carries the seconds where the readings lie less than a minute
apart. Another language is a tickFormat, which is handed the instant
in milliseconds. calendar implies it.Shown in: Align a second axis to the grid, Draw a first chart (Installation), Size a chart by its container (Installation) and 36 more |
calendar | readonly WorkingInterval[] | — | Working calendar: the intervals in which time counts. With it the axis
stands in working time - nights and weekends are out, and every
removed span gets a break mark. The scale stays affine; the mapping happens
in materialisation (ADR-0001). Shown in: Leave out the hours nobody works |
zoomable | boolean | false | Zoom and pan: Ctrl or ⌘ with the wheel - and a pinch - zoom around the
pointer, a drag, a horizontal wheel or Shift with the wheel pan, a double
click goes back to domain; with the focus on the chart, + and − zoom,
Shift with ← or → pans and 0 goes back. The span shown is part of the
chart's view, by this axis' id (useChart's initialView,
onViewChange, setView, setDomain). The plain wheel always scrolls the page.Shown in: Read it by keyboard and screen reader (Chart), Gestures and keys (Zoom and pan), Limit the zoom (Zoom and pan) and 6 more |
zoomLimits | ZoomLimits | at most the data's extent or this axis' own domain, whichever is wider, at least three data steps | The narrowest and the widest span zoom may reach, in the axis' units -
milliseconds on a time axis. Shown in: Limit the zoom (Zoom and pan) |
id | string | "x" | Axis id, through which series bind themselves. Shown in: Add axes for other magnitudes, Align a second axis to the grid, Bind every kind of series to a second axis and 7 more |
label | string | — | Axis title. Shown in: Fix the domain and name the ticks, Add axes for other magnitudes, Leave out the hours nobody works and 66 more |
tickCount | number | — | Target value; the 1-2-5 algorithm may deviate. Shown in: Fix the domain and name the ticks, Leave out the hours nobody works, Mix series kinds (Chart) and 8 more |
tickFormat | (v: number) => string | — | Label of a tick. Compared by its source text: one that reads a changed
closure variable under the same text does not relabel the axis - give it
a new text or remount. A bound Intl.NumberFormat#format is compared by
identity.Shown in: Fix the domain and name the ticks, Leave out the hours nobody works, Bind every kind of series to a second axis and 21 more |
grid | boolean | only on the first axis of its orientation | Grid lines. Shown in: One vehicle through the day (StateBand), Give each one a lane (StateBand), See where the vans stand idle (Scenarios) |
ticks | readonly number[] | — | Fixed tick values instead of the 1-2-5 algorithm, on a numeric axis: an
hour axis' named hours, a lane grid. An axis that reads categories needs
none - every category has its tick. Shown in: Bind every kind of series to a second axis, Mark bands, limits and cells (Chart), Compare progress with a plan (Line) and 7 more |
categories | readonly string[] | — | The categories of the axis, in their order. Without it they stand in
the order the rows first bring them. With it the list is the order and
the extent: a category no row carries keeps its empty place, and a row
whose category is not listed is not drawn - DEV names it. It makes the
axis a category axis whatever the rows hold (ADR-0052). Shown in: Fix the order and keep an empty place |
categoryLabel | (category: string) => string | the category itself | What is written for a category - under its tick, in the tooltip, the
readout and the data table - where the rows hold a key and not a name.
Two categories may share a label and keep two places. Compared by its
source text, as tickFormat is.Shown in: Fix the order and keep an empty place, Tell series apart without colour (Chart), Legend above or below (Legend) and 1 more |
YAxisProps| Name | Type | Default | Description |
|---|---|---|---|
domain | | "nice" | "nice" | As on the x axis, and "visible": what the series show inside their x
axis' domain - a zoomed hour gets the hour's range, not the week's -,
widened to ticks as "nice" is. Where the x domain is not fixed that is
every point.Shown in: Fix the domain and name the ticks, Bind every kind of series to a second axis, Mark bands, limits and cells (Chart) and 11 more |
alignTicks | boolean | — | On a further y axis: take the first y axis' tick count and widen this
axis' domain until its ticks fall on that one's grid lines, the steps
still 1-2-5 - one grid serves both. tickCount then does not apply; on
the first y axis it changes nothing.Shown in: Align a second axis to the grid |
position | "left" | "right" | "left" | Which edge the axis stands at. Several y axes per side are stacked
outwards, so that extents of clearly different magnitude stay readable. Shown in: Add axes for other magnitudes, Align a second axis to the grid, Bind every kind of series to a second axis and 7 more |
id | string | "y" | Axis id, through which series bind themselves. Shown in: Add axes for other magnitudes, Align a second axis to the grid, Bind every kind of series to a second axis and 7 more |
label | string | — | Axis title. Shown in: Fix the domain and name the ticks, Add axes for other magnitudes, Leave out the hours nobody works and 66 more |
tickCount | number | — | Target value; the 1-2-5 algorithm may deviate. Shown in: Fix the domain and name the ticks, Leave out the hours nobody works, Mix series kinds (Chart) and 8 more |
tickFormat | (v: number) => string | — | Label of a tick. Compared by its source text: one that reads a changed
closure variable under the same text does not relabel the axis - give it
a new text or remount. A bound Intl.NumberFormat#format is compared by
identity.Shown in: Fix the domain and name the ticks, Leave out the hours nobody works, Bind every kind of series to a second axis and 21 more |
grid | boolean | only on the first axis of its orientation | Grid lines. Shown in: One vehicle through the day (StateBand), Give each one a lane (StateBand), See where the vans stand idle (Scenarios) |
ticks | readonly number[] | — | Fixed tick values instead of the 1-2-5 algorithm, on a numeric axis: an
hour axis' named hours, a lane grid. An axis that reads categories needs
none - every category has its tick. Shown in: Bind every kind of series to a second axis, Mark bands, limits and cells (Chart), Compare progress with a plan (Line) and 7 more |
categories | readonly string[] | — | The categories of the axis, in their order. Without it they stand in
the order the rows first bring them. With it the list is the order and
the extent: a category no row carries keeps its empty place, and a row
whose category is not listed is not drawn - DEV names it. It makes the
axis a category axis whatever the rows hold (ADR-0052). Shown in: Fix the order and keep an empty place |
categoryLabel | (category: string) => string | the category itself | What is written for a category - under its tick, in the tooltip, the
readout and the data table - where the rows hold a key and not a name.
Two categories may share a label and keep two places. Compared by its
source text, as tickFormat is.Shown in: Fix the order and keep an empty place, Tell series apart without colour (Chart), Legend above or below (Legend) and 1 more |
AxisField<T, V = AxisValue>The fields of a row an axis can read - or, with V, those that hold only
some of what an axis reads: a matrix' row is a position or a category,
never an instant.
type AxisField<T, V = AxisValue> = NoInfer<
{
[K in keyof T & string]-?: T[K] extends V ? K : never;
}[keyof T & string]
>;AxisValueWhat an axis reads from a row (ADR-0052): a number - a position, or an
instant on a time axis -, a string, which is a category, or a Date,
which is an instant. The type of the value decides what kind the axis is,
never its content: "2024" is a category, 2024 a number. On an axis
that says time a string is an ISO 8601 instant. null/undefined is no
place.
type AxisValue = number | string | Date | null | undefined;WorkingIntervalAn interval in which time counts; thought of as half-open, but mappable from
both sides at its edges: both from and to have a working time.
ZoomLimitsThe narrowest and the widest span zoom may reach, in the axis' units - the schedule's shape.
| Name | Type | Default | Description |
|---|---|---|---|
minrequired | number | — | Shown in: Limit the zoom (Zoom and pan) |
maxrequired | number | — | Shown in: Limit the zoom (Zoom and pan) |
value; a category is a place on the linear scale (ADR-0032, ADR-0052).What umriss deliberately does not build, and why: ADR-0032.