One value per cell across two axes (a heatmap), coloured by its limits or along a gradient. Reach for it to see whether a bad hour hits everyone or one row.
import { useChart, DEFAULT_GRADIENT } from "@umriss-ui/charts";
value places a cell's row, as it places every series along y - a string names the row, and the y axis lists one per name, the first at the top (ADR-0052); level decides its colour. A cell without a level stays a hole, not a zero. The tooltip carries the level as text, since colour alone carries no number.
With coloring of kind assessment a cell takes the tone its limits give it: a bad hour everywhere reads differently from one bad service. value names the row - a string is a category, and the y axis lists one per service, the first at the top; format writes the level in the tooltip.
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;
};
}
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 SuccessCell {
hour: number;
/** The row: the service's name. */
service: string;
/** Requests that succeeded, in per cent; `null` where there were none. */
success: number | null;
}
/** Yesterday, per service and hour: the share of requests that succeeded.
Webhooks has a bad late morning, 03:00 is bad everywhere (a DNS change),
and Reporting takes no requests before 06:00 - no requests, no share. */
const SUCCESS_BY_HOUR: readonly SuccessCell[] = (() => {
const r = random(23);
const cells: SuccessCell[] = [];
SERVICES.forEach((one) => {
for (let hour = 0; hour < 24; hour++) {
let success: number | null = 99.2 + r() * 0.8;
if (one.id === "webhooks" && hour >= 9 && hour < 14) success = 96 + r() * 1.5;
if (hour === 3) success = 97.4 + r() * 1;
if (one.id === "reports" && hour < 6) success = null;
cells.push({ hour, service: one.name, success: success === null ? null : Math.round(success * 100) / 100 });
}
});
return cells;
})();
export default function ByLimits() {
const { Chart, XAxis, YAxis, Matrix } = useChart(SUCCESS_BY_HOUR);
return (
<Chart height={280} ariaLabel="Successful requests per service and hour yesterday, by limits">
<XAxis value="hour" ticks={[0, 6, 12, 18]} tickFormat={(v) => `${v}:00`} label="Hour" />
<YAxis />
<Matrix
value="service"
level="success"
coloring={{
kind: "assessment",
limits: {
limits: [
{ value: 99, side: "lower", severity: "warning" },
{ value: 98, side: "lower", severity: "alarm" },
],
},
}}
name="Success rate"
format={(v) => `${v.toFixed(2)} %`}
/>
<Tooltip mode="nearest" />
</Chart>
);
}
Without coloring the cells run along DEFAULT_GRADIENT from the lowest value to the highest; a cell without a value stays a hole.
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;
};
}
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 SuccessCell {
hour: number;
/** The row: the service's name. */
service: string;
/** Requests that succeeded, in per cent; `null` where there were none. */
success: number | null;
}
/** Yesterday, per service and hour: the share of requests that succeeded.
Webhooks has a bad late morning, 03:00 is bad everywhere (a DNS change),
and Reporting takes no requests before 06:00 - no requests, no share. */
const SUCCESS_BY_HOUR: readonly SuccessCell[] = (() => {
const r = random(23);
const cells: SuccessCell[] = [];
SERVICES.forEach((one) => {
for (let hour = 0; hour < 24; hour++) {
let success: number | null = 99.2 + r() * 0.8;
if (one.id === "webhooks" && hour >= 9 && hour < 14) success = 96 + r() * 1.5;
if (hour === 3) success = 97.4 + r() * 1;
if (one.id === "reports" && hour < 6) success = null;
cells.push({ hour, service: one.name, success: success === null ? null : Math.round(success * 100) / 100 });
}
});
return cells;
})();
export default function Gradient() {
const { Chart, XAxis, YAxis, Matrix } = useChart(SUCCESS_BY_HOUR);
return (
<Chart height={280} ariaLabel="Successful requests per service and hour yesterday, as a gradient">
<XAxis value="hour" ticks={[0, 6, 12, 18]} tickFormat={(v) => `${v}:00`} label="Hour" />
<YAxis />
<Matrix value="service" level="success" name="Success rate" />
<Tooltip mode="nearest" />
</Chart>
);
}
The keys of Chart apply here.
MatrixProps<T>| Name | Type | Default | Description |
|---|---|---|---|
valuerequired | AxisField<T, number | string | null | undefined> | ((d: T, index: number) => number | string | null | undefined) | — | The row on the y axis - a field of the data's row or a function of it.
A number is a position, as every series' y is; a string is a category:
the y axis then has a named row per distinct one, the first at the top,
in the order the data bring them or the y axis' categories
(ADR-0052). Never an instant: a Date is refused.Shown in: Colour cells by their limits, Colour cells along a gradient, Mark bands, limits and cells (Chart) and 1 more |
levelrequired | Value<T> | — | The level that decides the colour - a number field of the row or a
function of it. A missing level is a hole, not a zero. Shown in: Colour cells by their limits, Colour cells along a gradient, Mark bands, limits and cells (Chart) and 1 more |
coloring | MatrixColoring | { kind: "gradient", stops: DEFAULT_GRADIENT } | How the level colours a cell: by assessment against limits, or across
a gradient. Shown in: Colour cells by their limits, Mark bands, limits and cells (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. |
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: Colour cells by their limits, Colour cells along a gradient, Mark bands, limits and cells (Chart) and 1 more |
format | (value: number) => string | the built-in number format | The level as the tooltip writes it. The y axis' tickFormat writes the
row, not the level.Shown in: Colour cells by their limits |
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]
>;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>;MatrixColoringHow a matrix colours its cells. Discretely by assessment - the same limits that colour a tile - or across a gradient.
type MatrixColoring =
| { kind: "assessment"; limits: LimitSet }
| {
kind: "gradient";
/** Colour stops from small to large; at least two. The stops ARE the
steps - the library does not interpolate between them, because that
would mean a colour parser for arbitrary CSS colour values, which this
package does not have and is not to get (ADR-0011). Whoever wants a
finer gradation names more colours. */
stops: readonly string[];
/** Value range of the gradient.
@default the range of the data */
range?: readonly [number, number];
};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;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;LimitSetThe set a value is read against: its bounds and, where there is one, its target. The target is not a limit. It is missed, never violated, and it is never the edge of a region. Conflating the two is how a tile turns red for being above target.
| Name | Type | Default | Description |
|---|---|---|---|
limits | readonly Limit[] | — | In any order; none, one or several. Shown in: Colour cells by their limits, Mark bands, limits and cells (Chart) |
target | number | — |
LimitA bound: a value, a side and a severity. No more - a tolerance band is
derived and never an input (two limits of equal severity on opposite sides
enclose one). That keeps the one-sided case, which is the more common one,
free of a null on the other edge.
| Name | Type | Default | Description |
|---|---|---|---|
valuerequired | number | — | Shown in: Colour cells by their limits, Mark bands, limits and cells (Chart) |
siderequired | Side"upper" | "lower" | — | Shown in: Colour cells by their limits, Mark bands, limits and cells (Chart) |
severityrequired | Severity"warning" | "alarm" | — | Shown in: Colour cells by their limits, Mark bands, limits and cells (Chart) |
SideWhether a limit is an upper or a lower bound.
type Side = "upper" | "lower";SeverityHow seriously a violation weighs. Exactly two: one severity that advises and one that calls for action. Five severities would be five colours, and five colours on a dense screen are noise. Whoever needs a third distinction sets a second limit.
type Severity = "warning" | "alarm";