What a hover reports (a hover card), and the values as a table for a reader who wants them all at once.
import { Tooltip, DataTable } from "@umriss-ui/charts";
The crosshair snaps to a reading, never between two. The data table's key stands at the end of the legend.
useChart(rows, { onSelect }) hears a click or tap on a hit and Enter on the Active point, with the hit a custom render receives - once, never at the end of a drag, and for a double click only on its first click. The chart keeps and draws no selection; it needs a Tooltip for its hits and does not send the hit over syncId.
mode="x" lists every series at the pointer's x; mode="nearest" names only the closest point - the question when lines cross.
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 SEARCH = metrics("search");
const SIGN_IN = metrics("sign-in");
function Latencies({ mode }: { mode: "x" | "nearest" }) {
const { Chart, XAxis, YAxis, Line } = useChart(SEARCH);
return (
<Chart height={220} ariaLabel={`Search and sign-in latency, tooltip mode ${mode}`}>
<XAxis value="t" time tickCount={4} />
<YAxis label="ms" />
<Line value="p95" name="Search" />
<Line data={SIGN_IN} value="p95" name="Sign-in" />
<Tooltip mode={mode} />
</Chart>
);
}
export default function XOrNearest() {
return (
<div className="pair">
<div>
<p className="pair-caption">mode="x" - every series at the x position</p>
<Latencies mode="x" />
</div>
<div>
<p className="pair-caption">mode="nearest" - only the nearest point</p>
<Latencies mode="nearest" />
</div>
</div>
);
}
render receives the hit - the x value and each series' point with its row - and returns the box's content, in your screen's language.
import { Legend, Tooltip, useChart } from "@umriss-ui/charts";
import type { TooltipHit } 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");
const BILLING = metrics("billing");
const OBJECTIVE: Record<string, number> = Object.fromEntries(SERVICES.map((one) => [one.name, one.latencySlo]));
const timeOfDay = (v: number) =>
new Date(v).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
function AgainstObjective({ hit }: { hit: TooltipHit<MetricPoint> }) {
return (
<div className="custom-tooltip">
<strong>{timeOfDay(hit.xValue)}</strong>
{hit.points.map((point) => {
const off = point.datum.p95 - (OBJECTIVE[point.seriesName] ?? 0);
return (
<span key={point.seriesName}>
{point.seriesName} - {point.datum.p95} ms, {Math.abs(off)} ms {off > 0 ? "over" : "under"} objective
</span>
);
})}
</div>
);
}
export default function OwnContent() {
const { Chart, XAxis, YAxis, Line } = useChart(CHECKOUT);
return (
<Chart height={280} ariaLabel="Checkout and billing latency, the tooltip against their objectives">
<XAxis value="t" time />
<YAxis label="ms" />
<Line value="p95" name="Checkout" />
<Line data={BILLING} value="p95" name="Billing" />
<Legend placement="top" />
<Tooltip<MetricPoint> mode="x" render={(hit) => <AgainstObjective hit={hit} />} />
</Chart>
);
}
format on a series writes its value in the tooltip; without it the y axis' tickFormat does, and without that a bare number.
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 CHECKOUT = metrics("checkout");
const milliseconds = (v: number) => `${v.toFixed(0)} ms`;
const percent = (v: number) => `${v.toFixed(2)} %`;
export default function ValueFormat() {
const { Chart, XAxis, YAxis, Line } = useChart(CHECKOUT);
return (
<Chart height={260} ariaLabel="Checkout's latency and error rate with their units">
<XAxis value="t" time />
<YAxis label="ms" />
<YAxis id="errors" position="right" label="Errors %" />
<Line value="p95" name="p95" format={milliseconds} />
<Line value="errorRate" yAxisId="errors" name="Error rate" format={percent} />
<Legend />
<Tooltip mode="x" />
</Chart>
);
}
DataTable adds a key to the legend that lays a table of what the chart shows over the plot, and takes it back.
import { DataTable, Legend, Tooltip, useChart } from "@umriss-ui/charts";
/* Data from the planning world, written out here so the example runs on its own. */
const at = (day: number, hours = 9, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime();
/** A work item, shaped as `@umriss-ui/schedule`'s `Subtask`. */
interface WorkItem {
id: string;
/** The project. */
task: string;
/** The person. */
lane: string;
from: number;
to: number;
name: string;
sprint: string;
/** Hours, as estimated at planning. */
estimate: number;
status: "to do" | "in progress" | "in review" | "done";
}
const item = (id: string, task: string, lane: string, from: number, to: number, name: string, estimate: number, status: WorkItem["status"]): WorkItem =>
({ id, task, lane, from, to, name, sprint: "sprint-14", estimate, status });
const WORK: readonly WorkItem[] = [
item("w-101", "portal", "arjun", at(9), at(11, 17), "Sign-in with e-mail code", 18, "done"),
item("w-102", "portal", "arjun", at(12), at(17, 17), "Profile page", 26, "in progress"),
item("w-103", "portal", "chloe", at(9), at(10, 17), "Session handling", 12, "done"),
item("w-104", "shop", "chloe", at(11), at(13, 17), "Basket keeps items across devices", 20, "in review"),
item("w-105", "portal", "chloe", at(16), at(19, 17), "Change of address form", 24, "in progress"),
item("w-106", "portal", "noah", at(9), at(12, 13), "Profile page design", 14, "done"),
item("w-107", "shop", "noah", at(16), at(18, 17), "Search results layout", 12, "to do"),
item("w-108", "portal", "eva", at(12), at(13, 17), "Test plan for sign-in", 10, "done"),
item("w-109", "portal", "eva", at(17), at(20, 17), "Regression run", 20, "to do"),
item("w-110", "booking", "hana", at(9), at(13, 17), "Reminder scheduling service", 32, "done"),
item("w-111", "booking", "hana", at(16), at(20, 17), "Push notifications", 30, "in progress"),
item("w-112", "booking", "kofi", at(10), at(12, 17), "Calendar sync", 18, "done"),
item("w-113", "intranet", "kofi", at(16), at(18, 17), "News feed", 16, "in progress"),
item("w-114", "booking", "freya", at(9), at(11, 17), "Reminder settings screen", 16, "done"),
item("w-115", "booking", "freya", at(18), at(20, 17), "Store screenshots", 12, "to do"),
item("w-116", "booking", "david", at(16), at(19, 13), "Device test matrix", 14, "to do"),
];
interface BurndownPoint {
/** The working day, at 17:00. */
t: number;
/** Where the line would be, burning evenly. */
ideal: number;
/** Hours left, `null` for the days still to come. */
remaining: number | null;
}
/** Sprint 14's ten working days. The team fell behind in the first week, when
two were away, and is catching up. */
const BURNDOWN: readonly BurndownPoint[] = (() => {
const total = WORK.reduce((sum, one) => sum + one.estimate, 0);
const days = [9, 10, 11, 12, 13, 16, 17, 18, 19, 20];
const burnt = [0, 22, 38, 60, 71, 98];
return days.map((day, i) => ({
t: at(day, 17),
ideal: Math.round(total * (1 - i / (days.length - 1))),
remaining: burnt[i] === undefined ? null : total - burnt[i]!,
}));
})();
const day = (v: number) => new Date(v).toLocaleDateString("en-GB", { weekday: "short", day: "numeric" });
const hours = (v: number) => `${v.toFixed(0)} h`;
export default function ValuesAsTable() {
const { Chart, XAxis, YAxis, Line } = useChart(BURNDOWN);
return (
<Chart height={260} ariaLabel="Sprint 14: hours left against the ideal">
<XAxis value="t" ticks={BURNDOWN.map((d) => d.t)} tickFormat={day} />
<YAxis label="Hours left" />
<Line value="ideal" name="Ideal" format={hours} />
<Line value="remaining" name="Remaining" format={hours} />
<Legend />
<Tooltip mode="x" />
{/* The table lists what the chart shows: hide a series and it follows. */}
<DataTable />
</Chart>
);
}
Above 500 rows the table lists each stretch's first, lowest, highest and last value and says so; zoom in and every reading returns.
import { DataTable, 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", 60_000);
const milliseconds = (v: number) => `${v.toFixed(0)} ms`;
export default function WeekAsTable() {
const { Chart, XAxis, YAxis, Line } = useChart(SEARCH_WEEK);
return (
<Chart height={240} ariaLabel="Search latency over last week, a reading a minute">
<XAxis value="t" time domain="data" zoomable label="Time" />
<YAxis label="ms" />
<Line value="p95" name="p95" format={milliseconds} />
<Tooltip mode="x" />
{/* Without a legend the key stands on a line of its own. */}
<DataTable />
</Chart>
);
}
limits adds a row for every labelled limit on the hit's y axis, its label and value in the axis' format; a key reads the same out, and the chart's summary names them without it.
import { LimitBand, LimitLine, Tooltip, useChart } from "@umriss-ui/charts";
/* Checkout's 95th percentile latency every ten minutes of a morning, in ms, as the monitor exports it. */
const LATENCY = [
{ time: "09:00", p95: 212 }, { time: "09:10", p95: 224 }, { time: "09:20", p95: 231 },
{ time: "09:30", p95: 246 }, { time: "09:40", p95: 318 }, { time: "09:50", p95: 402 },
{ time: "10:00", p95: 387 }, { time: "10:10", p95: 291 }, { time: "10:20", p95: 238 },
{ time: "10:30", p95: 229 },
].map((row, x) => ({ ...row, x }));
export default function Limits() {
const { Chart, XAxis, YAxis, Line } = useChart(LATENCY);
return (
<Chart height={260} ariaLabel="Checkout's latency against its objective and its warning band">
<XAxis value="x" tickFormat={(v) => LATENCY[Math.round(v)]?.time ?? ""} />
<YAxis label="ms" tickFormat={(v) => `${v} ms`} />
<LimitBand from={250} to={300} severity="warning" label="Warning band" />
<LimitLine value={300} severity="alarm" label="Objective" />
<Line value="p95" name="p95" />
<Tooltip mode="x" limits />
</Chart>
);
}
onSelect hears a click on a hit, a tap, or Enter on the Active point, with the same hit a custom tooltip receives. The chart keeps no choice and draws none: it is your state, here shown below the chart.
import { useState } from "react";
import { Tooltip, useChart, type TooltipHit } from "@umriss-ui/charts";
/* Orders taken per hour of a shop's day, written out here so the example runs on its own. */
const ORDERS = [
{ hour: 8, web: 14, phone: 6 }, { hour: 9, web: 22, phone: 9 }, { hour: 10, web: 31, phone: 11 },
{ hour: 11, web: 38, phone: 8 }, { hour: 12, web: 52, phone: 14 }, { hour: 13, web: 47, phone: 10 },
{ hour: 14, web: 35, phone: 7 }, { hour: 15, web: 29, phone: 9 },
];
type Order = (typeof ORDERS)[number];
export default function ChooseAHit() {
const [chosen, setChosen] = useState<TooltipHit<Order> | null>(null);
const { Chart, XAxis, YAxis, Line } = useChart(ORDERS, { onSelect: setChosen });
return (
<>
<Chart height={260} ariaLabel="Orders per hour, by channel">
<XAxis value="hour" tickFormat={(v) => `${v}:00`} />
<YAxis label="orders" />
<Line value="web" name="Web" />
<Line value="phone" name="Phone" />
<Tooltip mode="x" />
</Chart>
<dl className="metrics">
<div>
<dt>Chosen</dt>
<dd>{chosen === null ? "nothing yet - click a point, or press Enter on one" : `${chosen.xValue}:00`}</dd>
</div>
{chosen?.points.map((p) => (
<div key={p.seriesName}>
<dt>{p.seriesName}</dt>
<dd>{p.yValue} orders</dd>
</div>
))}
</dl>
</>
);
}
The table of a stacked chart ends the stack's members with a Total column - the sum the tooltip shows at the same place, so nobody adds the row up by hand.
import { DataTable, Legend, Tooltip, useChart } from "@umriss-ui/charts";
/* Support tickets opened per working day, by channel - one week, 2 to 6 March. */
const TICKETS = [
{ day: "Mon 2", mail: 34, chat: 21, phone: 9 },
{ day: "Tue 3", mail: 29, chat: 25, phone: 12 },
{ day: "Wed 4", mail: 31, chat: 18, phone: 7 },
{ day: "Thu 5", mail: 27, chat: 30, phone: 11 },
{ day: "Fri 6", mail: 22, chat: 16, phone: 6 },
];
export default function TotalOfAStack() {
const { Chart, XAxis, YAxis, Bar } = useChart(TICKETS);
return (
<Chart height={260} ariaLabel="Tickets opened per working day, by channel">
<XAxis value="day" />
<YAxis label="Tickets" />
<Bar value="mail" name="Mail" stack="tickets" />
<Bar value="chat" name="Chat" stack="tickets" />
<Bar value="phone" name="Phone" stack="tickets" />
<Legend placement="top" />
<Tooltip mode="x" />
<DataTable />
</Chart>
);
}
The keys of Chart apply here.
TooltipProps<T>| Name | Type | Default | Description |
|---|---|---|---|
mode | "x" | "nearest" | "x" | "x": every series at the x position; "nearest": only the nearest point. Shown in: Report every series or the nearest, Write the tooltip yourself, Write each value in its unit and 61 more |
limits | boolean | false | Adds a row for every labelled LimitLine and LimitBand on the hit's y
axis - its label and value in that axis' format - to the built-in
tooltip, and the same to what a key reads out. Limits are not series and
stay out of the tooltip unless asked for.Shown in: Read the limits at the hit |
render | (hit: TooltipHit<T>) => ReactNode | the built-in tooltip | Render prop for content of one's own. Compared by its source text, like
tickFormat - with the same closure limit.Shown in: Write the tooltip yourself, Fix the domain and name the ticks (Axis) |
ChartOptions<Z = unknown>| Name | Type | Default | Description |
|---|---|---|---|
initialView | ChartView | — | The view to start from, read once when the hook first runs; a different
value on a later render is ignored, as React ignores a changed
defaultValue. To go to a view later, call setView. The chart
remembers none: where a view is kept is the application's decision.Shown in: Fit the y axis to what is visible (Zoom and pan), Hide a series from the legend (Legend) |
| Name | Type | Default | Description |
|---|---|---|---|
onViewChange | (view: ChartView) => void | — | Every change the reader makes, once, always the whole view - the one to
keep, or to hand another chart's setView. A pan or zoom is reported at
most once per frame. Not called for the view the chart starts with, nor
for one setView applies.Shown in: Keep charts in step (Zoom and pan), Read it by keyboard and screen reader (Chart) |
onSelect | (hit: TooltipHit<Z>) => void | — | The reader chose a point: a click on a hit (a tap too), or Enter on the
Active point - the same hit the tooltip's render receives. Once per
click; the end of a drag does not report, and the second click of a
double click does not either. The chart holds no selection and draws
none: what is chosen is the application's state. Needs a <Tooltip>,
which makes the hits; the hit does not travel over syncId.Shown in: Choose a hit |
TooltipHit<T = unknown>What the pointer hit: the x value under it and one point per series there.
A custom tooltip's render receives it.
| Name | Type | Default | Description |
|---|---|---|---|
xValuerequired | number | — | Shown in: Write the tooltip yourself, Choose a hit, Fix the domain and name the ticks (Axis) |
pointsrequired | readonly TooltipPoint<T>[] | — | Shown in: Write the tooltip yourself, Choose a hit, Fix the domain and name the ticks (Axis) |
xPxrequired | number | — | Pixel position of the crosshair (snapped to the data point). |
yPxrequired | number | — | Pixel position of the primary hit. |
TooltipPoint<T = unknown>One hit per series in the tooltip.
| Name | Type | Default | Description |
|---|---|---|---|
seriesNamerequired | string | — | Shown in: Write the tooltip yourself, Choose a hit, Fix the domain and name the ticks (Axis) |
colorrequired | string | — | |
xValuerequired | number | — | The point's x value on its own x axis - with several x axes it can differ from the hit's. |
yValuerequired | number | — | The point's y value; in a stack its own value, not its top - in a
normalised stack its share in percent. Shown in: Choose a hit, Fix the domain and name the ticks (Axis) |
datumrequired | T | — | Shown in: Write the tooltip yourself |
indexrequired | number | — | |
level | number | — | Only for the matrix: its level, out of the level channel. It stands here and not in yValue, because yValue is the position on the y axis - for a matrix therefore the row. Putting both into one field would be exactly the overloading ADR-0011 is written against. |
segment | { from: number; to: number; label: string } | — | Only for state series: the section being pointed at. A duration does not stand here - it is the difference of two numbers the entry already carries, and formatting it would presuppose knowing what the x axis means. The caller knows that, not this library. |
box | BoxNumbers & Partial<BoxExtras> & { /** As the caller gave them; only where the series has `outliers`. */ outliers?: readonly number[]; } | — | Only for a box: its further numbers; yValue is its median. Fields of
their own for the reason level has one (ADR-0011). |
BoxNumbers<V = number>A box's further numbers beside its median, by name.
| Name | Type | Default | Description |
|---|---|---|---|
lowerQuartilerequired | V | — | |
upperQuartilerequired | V | — | |
lowerWhiskerrequired | V | — | |
upperWhiskerrequired | V | — |
BoxExtras<V = number>The optional numbers of a box by name.
| Name | Type | Default | Description |
|---|---|---|---|
meanrequired | V | — | |
notchLowerrequired | V | — | |
notchUpperrequired | V | — | |
countrequired | V | — |