Look closer at a stretch of a long series and move along it (zoom, pan, a viewport), by wheel, pinch, drag or key, and keep several charts on the same stretch.
import { useChart } from "@umriss-ui/charts";
zoomable on an x axis turns the gestures and keys on; nothing else is needed. The span shown is part of the chart's view (ADR-0047): useChart(rows, { initialView, onViewChange }) takes a start and reports what the reader does, whole, at most once per frame; setView(view), domains and setDomain(axisId, span | null) stand on what the hook returns. A lone x axis is "x".
Once an axis is zoomed, the chart offers 'Show all' over a corner of its plot - a button of its own, a tab stop beside the plot's, that brings every zoomed axis back and goes. Its words are the wording's showAll.
Charts stay in step by sending each other what the reader did: each chart's onViewChange calls the other's setView. setView is not reported, so two charts linked both ways do not answer each other, and a pan does not jump back. syncId shares only the pointer.
A zoom stops at zoomLimits; without them at three readings and at the data's extent. A y axis with domain="visible" follows the stretch shown.
With zoomable the x axis zooms on Ctrl or ⌘ with the wheel, a pinch or + and −, pans on a drag or Shift with the arrows, and goes back on a double click, 0 or 'Show all' - without a line of state of yours.
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;
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);
export default function GesturesAndKeys() {
const { Chart, XAxis, YAxis, Line } = useChart(SEARCH_WEEK);
return (
<Chart height={260} ariaLabel="Search latency over last week, a reading a minute, zoomable">
{/* The span shown is the chart's own: `domain` is where it starts. */}
<XAxis value="t" time domain="data" zoomable />
<YAxis label="ms" />
<Line value="p95" name="p95" />
<Tooltip mode="x" />
</Chart>
);
}
zoomLimits keeps the span between a narrowest and a widest; without it a zoom stops at three readings and at the data's extent.
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;
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);
export default function ZoomLimits() {
const { Chart, XAxis, YAxis, Line } = useChart(SEARCH_WEEK);
return (
<Chart height={260} ariaLabel="Search latency over last week, zoomable between an hour and two days">
{/* Never closer than an hour, never wider than two days - the week
shows at the start and once more after a double click. */}
<XAxis value="t" time domain="data" zoomable zoomLimits={{ min: HOUR, max: 2 * DAY }} />
<YAxis label="ms" />
<Line value="p95" name="p95" />
<Tooltip mode="x" />
</Chart>
);
}
With domain="visible" the y axis fits the zoomed stretch - here Saturday evening's outage, handed in as initialView - and follows when you zoom out.
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;
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 SATURDAY = new Date(2026, 2, 14).getTime();
export default function VisibleDomain() {
const { Chart, XAxis, YAxis, Line } = useChart(SEARCH_WEEK, {
// The lone x axis is "x": its span is where the chart starts.
initialView: { domains: { x: [SATURDAY + 16 * HOUR, SATURDAY + 22 * HOUR] } },
});
return (
<Chart height={260} ariaLabel="Search latency on Saturday evening, the y axis fitted to it">
<XAxis value="t" time domain="data" zoomable />
<YAxis label="ms" domain="visible" />
<Line value="p95" name="p95" />
<Tooltip mode="x" />
</Chart>
);
}
Charts with one syncId share the pointer; send each one's onViewChange to the others' setView, and a zoom in any of them zooms all.
import { useEffect, useState } from "react";
import { Tooltip, useChart, type ChartView } 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 IMAGES_WEEK = week("images", 5 * 60_000);
const CHANNELS = [
{ name: "Requests", unit: "/min", value: "requests" },
{ name: "p95", unit: "ms", value: "p95" },
{ name: "p50", unit: "ms", value: "p50" },
] as const;
export default function CursorSync() {
// The three share a view: each sends the others what the reader did to it.
const [peers] = useState(() => new Map<string, (view: ChartView) => void>());
return (
<div>
{/* Each plot begins where its y axis ends: labels of one width - three
figures here - keep the crosshairs in one column. */}
{CHANNELS.map((c) => (
<Channel key={c.name} channel={c} peers={peers} />
))}
</div>
);
}
function Channel({ channel: c, peers }: { channel: (typeof CHANNELS)[number]; peers: Map<string, (view: ChartView) => void> }) {
const { Chart, XAxis, YAxis, Line, setView } = useChart(IMAGES_WEEK, {
onViewChange: (view) => {
for (const [name, goTo] of peers) if (name !== c.name) goTo(view);
},
});
useEffect(() => {
peers.set(c.name, setView);
return () => void peers.delete(c.name);
}, [peers, c.name, setView]);
return (
<Chart height={140} syncId="images" ariaLabel={`Image service ${c.name} over last week`}>
<XAxis value="t" time domain="data" zoomable />
<YAxis label={c.unit} domain="visible" tickCount={4} />
<Line value={c.value} name={c.name} />
<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: Gestures and keys, Limit the zoom, Fit the y axis to what is visible 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: Gestures and keys, Limit the zoom, Fit the y axis to what is visible 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 (Axis), Bind every kind of series to a second axis (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: Gestures and keys, Limit the zoom, Fit the y axis to what is visible 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 (Axis) |
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: Gestures and keys, Limit the zoom, Fit the y axis to what is visible 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 |
id | string | "x" | Axis id, through which series bind themselves. Shown in: Add axes for other magnitudes (Axis), Align a second axis to the grid (Axis), Bind every kind of series to a second axis (Axis) and 7 more |
label | string | — | Axis title. Shown in: Gestures and keys, Limit the zoom, Fit the y axis to what is visible and 66 more |
tickCount | number | — | Target value; the 1-2-5 algorithm may deviate. Shown in: Keep charts in step, Fix the domain and name the ticks (Axis), Leave out the hours nobody works (Axis) 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 (Axis), Leave out the hours nobody works (Axis), Bind every kind of series to a second axis (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 (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). |
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 (Axis), Tell series apart without colour (Chart), Legend above or below (Legend) and 1 more |
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, 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, 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 (Tooltip) |
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 |
maxrequired | number | — | Shown in: Limit the zoom |
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: Fix the domain and name the ticks (Axis), Write the tooltip yourself (Tooltip), Choose a hit (Tooltip) |
pointsrequired | readonly TooltipPoint<T>[] | — | Shown in: Fix the domain and name the ticks (Axis), Write the tooltip yourself (Tooltip), Choose a hit (Tooltip) |
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: Fix the domain and name the ticks (Axis), Write the tooltip yourself (Tooltip), Choose a hit (Tooltip) |
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: Fix the domain and name the ticks (Axis), Choose a hit (Tooltip) |
datumrequired | T | — | Shown in: Write the tooltip yourself (Tooltip) |
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 | — |
What umriss deliberately does not build, and why: ADR-0032.