A filled course (an area chart): down to zero for a quantity, between two edges for a range, stacked for parts of a whole.
import { useChart } from "@umriss-ui/charts";
Without baseline the foot is 0 and stays in the y extent. A range's lower edge is the baseline of the same series, not a second series - where either edge is missing, the fill has a hole.
Without baseline an area is filled down to 0, and 0 stays in the y extent - a quantity is read from its foot.
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: 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;
/** 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,
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 Filled() {
const { Chart, XAxis, YAxis, Area } = useChart(CHECKOUT);
return (
<Chart height={260} ariaLabel="Checkout's requests per minute today">
<XAxis value="t" time />
<YAxis label="Requests/min" />
<Area value="requests" name="Requests" fillOpacity={0.35} strokeWidth={2} />
<Tooltip mode="x" />
</Chart>
);
}
Give baseline the lower edge and the area becomes a range; where either edge is missing, the fill has a hole. dash breaks the outline, never the fill.
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;
/** 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: 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;
/** 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,
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 HOUR = 60 * MINUTE;
interface ExpectedPoint {
t: number;
/** The range the requests per minute are expected in; `null` where the
job that computes it did not run. */
low: number | null;
high: number | null;
}
/** The range a service's requests per minute are expected in today, every
five minutes - the day's usual curve, give or take 15 %. The job that
computes it skipped 03:00 to 04:00. */
function expected(serviceId: string): ExpectedPoint[] {
const service = SERVICES.find((one) => one.id === serviceId);
if (service === undefined) throw new Error(`No service "${serviceId}".`);
const points: ExpectedPoint[] = [];
for (let t = at(17, 0); t <= NOW; t += 5 * MINUTE) {
const hour = (t - at(17, 0)) / HOUR;
const usual = VOLUME[service.tier] * (0.35 + 0.65 * Math.max(0, Math.sin((Math.PI * (hour - 5)) / 16)));
const skipped = hour >= 3 && hour < 4;
points.push({ t, low: skipped ? null : Math.round(usual * 0.85), high: skipped ? null : Math.round(usual * 1.15) });
}
return points;
}
const CHECKOUT = metrics("checkout");
const EXPECTED = expected("checkout");
export default function Corridor() {
const { Chart, XAxis, YAxis, Area, Line } = useChart(CHECKOUT);
return (
<Chart height={280} ariaLabel="Checkout's requests inside the range they are expected in">
<XAxis value="t" time />
<YAxis label="Requests/min" />
{/* The outline runs along the upper edge; dashed, it reads as expected, not measured. */}
<Area
data={EXPECTED}
value="high"
baseline="low"
name="Expected range"
strokeWidth={1}
dash={[4, 3]}
/>
<Line value="requests" name="Requests" strokeWidth={1.75} />
<Legend placement="top" />
<Tooltip mode="x" />
</Chart>
);
}
Areas with the same stack stand on the ones before them: the top edge is the whole, each band one depot's part. Hide a depot from the legend, and the stack closes over the gap.
import { Legend, 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();
const HOUR = 60 * MINUTE;
interface DepotHour {
t: number;
/** Parcels loaded per hour at each depot. */
north: number;
river: number;
east: number;
}
/** Monday 06:00 to Tuesday 06:00, hour by hour. East Gate closes at night and
loads nothing - 0, not nothing. */
const PARCELS_PER_HOUR: readonly DepotHour[] = (() => {
const r = random(906);
return Array.from({ length: 25 }, (_, i) => {
const hour = (6 + i) % 24;
const night = hour >= 22 || hour < 6;
return {
t: on(3, 16, 6) + i * HOUR,
north: Math.round(120 + r() * 30),
river: Math.round((night ? 60 : 90) + r() * 25),
east: night ? 0 : Math.round(70 + r() * 30),
};
});
})();
export default function StackedAreas() {
const { Chart, XAxis, YAxis, Area } = useChart(PARCELS_PER_HOUR);
return (
<Chart height={280} ariaLabel="Parcels loaded per hour at three depots, stacked">
<XAxis value="t" time domain="data" />
<YAxis label="Parcels per hour" />
<Area value="north" name="North" stack="depots" fillOpacity={0.5} strokeWidth={1} />
<Area value="river" name="Riverside" stack="depots" fillOpacity={0.5} strokeWidth={1} />
<Area value="east" name="East Gate" stack="depots" fillOpacity={0.5} strokeWidth={1} />
<Legend placement="top" />
<Tooltip mode="x" />
</Chart>
);
}
normalize on a stack makes every hour sum to 100 %: the axis reads in per cent, and format still writes the tooltip's readings in tonnes.
import { Legend, 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 HOUR = 3_600_000;
const on = (month: number, day: number, hours = 0) => new Date(2026, month - 1, day, hours).getTime();
interface DepotLoad {
t: number;
/** Freight loaded per hour at each depot, in tonnes. */
north: number;
river: number;
east: number;
}
/** Monday 06:00 to Tuesday 06:00, hour by hour. East Gate closes at night, and
Riverside's night trucks take over its share. */
const TONNES_PER_HOUR: readonly DepotLoad[] = (() => {
const r = random(911);
return Array.from({ length: 25 }, (_, i) => {
const hour = (6 + i) % 24;
const night = hour >= 22 || hour < 6;
return {
t: on(3, 16, 6) + i * HOUR,
north: Math.round((6 + r() * 2) * 10) / 10,
river: Math.round(((night ? 7 : 4) + r() * 1.5) * 10) / 10,
east: night ? 0 : Math.round((3.5 + r() * 2) * 10) / 10,
};
});
})();
const tonnes = (v: number) => `${v.toFixed(1)} t`;
export default function Shares() {
const { Chart, XAxis, YAxis, Area } = useChart(TONNES_PER_HOUR);
return (
<Chart height={280} ariaLabel="Each depot's share of the freight loaded per hour">
<XAxis value="t" time domain="data" />
<YAxis label="Share of the hour" />
<Area value="north" name="North" stack="depots" normalize format={tonnes} fillOpacity={0.5} strokeWidth={1} />
<Area value="river" name="Riverside" stack="depots" normalize format={tonnes} fillOpacity={0.5} strokeWidth={1} />
<Area value="east" name="East Gate" stack="depots" normalize format={tonnes} fillOpacity={0.5} strokeWidth={1} />
<Legend placement="top" />
<Tooltip mode="x" />
</Chart>
);
}
The keys of Chart apply here.
AreaProps<T>| Name | Type | Default | Description |
|---|---|---|---|
valuerequired | Value<T> | — | Upper edge - a number field of the row or a function of it;
null/undefined/NaN/±Infinity means a gap. Shown in: Fill down to zero, Fill a range between two edges, Stack areas and 5 more |
baseline | Value<T> | a fixed baseline at 0 | Lower edge - a number field of the row or a function of it. In a stack the stack below is the lower edge, and this is
not read.Shown in: Fill a range between two edges, Mix series kinds (Chart), Mark every kind (Chart) |
xAxisId | string | "x" | Binding to an x axis. |
yAxisId | string | "y" | Binding to a y axis. |
data | readonly T[] | — | Series-own data; overrides the container data. Shown in: Fill a range between two edges, Bind every kind of series to a second axis (Axis) |
name | string | "Series n", after its place in the chart | The name in legend and tooltip. Without one a warning stands in DEV - an
unnamed series is a colour nobody can look up. Shown in: Fill down to zero, Fill a range between two edges, Stack areas and 5 more |
format | (value: number) => string | the y axis' tickFormat, else the built-in number format | The value as the tooltip writes it. Shown in: Show shares of the whole over time |
color | string | the palette's --uc-series-N | Any CSS colour value. |
tone | | "ok" | — | A role instead of a colour value; the theme resolves it. color beats
it. |
stack | string | — | The stack this series stands in: every Bar and Area with the same id on
the same x and y axis stands on the ones registered before it. A gap
stacks as zero; negative values stack downward from zero. The tooltip
names each series' own value and the stack's total. Shown in: Stack areas, Show shares of the whole over time |
normalize | boolean | — | On any member of a stack, every x of the stack sums to 100 %: each value
becomes its share, and the y axis reads in percent unless it has a
tickFormat. The total stays the readings' own sum, written in the
series' format.Shown in: Show shares of the whole over time |
fillOpacity | number | 0.18 | Opacity of the fill, from 0 to 1; the outline stays fully opaque. Shown in: Fill down to zero, Stack areas, Show shares of the whole over time |
strokeWidth | number | 1.5 | Width of the outline along the upper edge in CSS pixels; 0 leaves it
out. Shown in: Fill down to zero, Fill a range between two edges, Stack areas and 2 more |
dash | readonly number[] | a solid outline | Dash pattern of the outline as a run of lengths in CSS pixels. The fill
stays whole. Shown in: Fill a range between two edges |
Value<T>A value read from the row (ADR-0048): the name of a number field, compared
by its name, or a function, compared by its source text (ValueFunction).
type Value<T> = NumberField<T> | ValueFunction<T>;NumberField<T>The fields of a row whose value is a number, or absent.
type NumberField<T> = NoInfer<
{
[K in keyof T & string]-?: T[K] extends number | null | undefined ? K : never;
}[keyof T & string]
>;ValueFunction<T>A value computed from the row - the function form of Value.
Compared by its source text, not its identity - an inline function is new
on every render. Known limit: one that reads a
changed closure variable under the same text is not run again; pass a new
data reference then. A native or bound function is compared by identity.
type ValueFunction<T> = (d: T, index: number) => number | null | undefined;