A column that judges each value against limits – ok, warning, alarm or unknown – and shows by how much it is out (also called a threshold or status column). Use it wherever a number has a line it must not cross.
import { useTable } from "@umriss-ui/table";
A missing value reads “unknown”: a fourth verdict, not a blank cell. Each verdict carries a shape and a word as well as a colour, and a value past a limit shows its excess beside it.
It sorts by the verdict's weight – ok, unknown, warning, alarm – so values far out on either side meet at one end; sortBy="value" sorts by the number. It exports the value, not the verdict. The limit set is the one @umriss-ui/core and @umriss-ui/charts read (ADR-0006).
Give a VerdictColumn the value and a limits set; each cell shows ok, warning, alarm or unknown, and how far past the limit.
import type { LimitSet } from "@umriss-ui/core";
import { useTable } from "@umriss-ui/table";
interface Endpoint {
path: string;
p95: number | null;
}
/* Checkout's latency objective: the 95th percentile stays below 300 ms. */
const OBJECTIVE: LimitSet = {
limits: [
{ value: 250, side: "upper", severity: "warning" },
{ value: 300, side: "upper", severity: "alarm" },
],
};
const ENDPOINTS: Endpoint[] = [
{ path: "/cart", p95: 182 },
{ path: "/payment", p95: 271 },
{ path: "/payment/confirm", p95: 346 },
{ path: "/receipt", p95: null },
];
export default function ReadLatency() {
const { Table, Column, VerdictColumn } = useTable(ENDPOINTS, { rowKey: (e) => e.path });
return (
<Table ariaLabel="Checkout endpoints">
<Column value="path" label="Endpoint" rowHeader />
<VerdictColumn value="p95" label="p95 latency (ms)" limits={OBJECTIVE} />
</Table>
);
}
A verdict column sorts by the verdict's weight, so both overloaded and idle vans stand on top; sortBy="value" sorts by the number.
import { Stack, Text } from "@umriss-ui/core";
import type { LimitSet } from "@umriss-ui/core";
import { useTable } from "@umriss-ui/table";
interface Tour {
tour: string;
load: number | null;
}
/* The share of the vehicle's capacity a tour loads, in per cent. */
const LOAD: LimitSet = {
limits: [
{ value: 90, side: "upper", severity: "warning" },
{ value: 100, side: "upper", severity: "alarm" },
{ value: 40, side: "lower", severity: "warning" },
{ value: 25, side: "lower", severity: "alarm" },
],
};
const TOURS: Tour[] = [
{ tour: "T-01", load: 72 },
{ tour: "T-02", load: 104 },
{ tour: "T-03", load: 18 },
{ tour: "T-04", load: 93 },
{ tour: "T-05", load: null },
];
function Tours({ sortBy }: { sortBy: "verdict" | "value" }) {
const { Table, Column, VerdictColumn } = useTable(TOURS, {
rowKey: (t) => t.tour,
defaultSort: { column: "load", direction: "desc" },
});
return (
<Table ariaLabel={sortBy === "verdict" ? "Tours by verdict" : "Tours by load"}>
<Column value="tour" label="Tour" rowHeader />
<VerdictColumn value="load" label="Load (%)" limits={LOAD} sortBy={sortBy} />
</Table>
);
}
export default function SortByVerdictOrByValue() {
return (
<Stack gap={4}>
<Stack gap={2}>
<Text size="xs" tone="muted">
By verdict (the default)
</Text>
<Tours sortBy="verdict" />
</Stack>
<Stack gap={2}>
<Text size="xs" tone="muted">
By value
</Text>
<Tours sortBy="value" />
</Stack>
</Stack>
);
}
Compute the value in value and give the column an id; format writes value and excess alike, here as a percentage.
import type { LimitSet } from "@umriss-ui/core";
import { useTable } from "@umriss-ui/table";
interface CostCentre {
name: string;
budget: number;
actual: number;
}
/* Spent against the month's budget. */
const SPEND: LimitSet = {
limits: [
{ value: 1, side: "upper", severity: "warning" },
{ value: 1.1, side: "upper", severity: "alarm" },
],
};
const CENTRES: CostCentre[] = [
{ name: "Sales", budget: 142_000, actual: 138_400 },
{ name: "Marketing", budget: 68_000, actual: 79_300 },
{ name: "IT", budget: 83_000, actual: 84_100 },
];
export default function JudgeAComputedShare() {
const { Table, Column, VerdictColumn } = useTable(CENTRES, { rowKey: (c) => c.name });
return (
<Table ariaLabel="Budget use in March">
<Column value="name" label="Cost centre" rowHeader />
<Column value="actual" label="Actual (€)" format="count" />
<VerdictColumn id="used" label="Budget used" value={(c) => c.actual / c.budget} limits={SPEND} format="percent" />
</Table>
);
}
Set aggregate="worst" and the footer names the worst verdict among the rows – whether anyone in the team is overbooked.
import type { LimitSet } from "@umriss-ui/core";
import { useTable } from "@umriss-ui/table";
/* Data from the planning world, written out here so the example runs on its own. */
interface Person {
id: string;
name: string;
role: "Developer" | "Designer" | "Product manager" | "QA engineer";
team: "Web" | "Apps";
/** Hours a week they can be planned for. */
capacity: number;
}
const PEOPLE: readonly Person[] = [
{ id: "maya", name: "Maya Lindgren", role: "Product manager", team: "Web", capacity: 32 },
{ id: "arjun", name: "Arjun Mehta", role: "Developer", team: "Web", capacity: 40 },
{ id: "chloe", name: "Chloe Durand", role: "Developer", team: "Web", capacity: 40 },
{ id: "noah", name: "Noah Fischer", role: "Designer", team: "Web", capacity: 24 },
{ id: "eva", name: "Eva Novak", role: "QA engineer", team: "Web", capacity: 40 },
{ id: "luis", name: "Luis Moreno", role: "Product manager", team: "Apps", capacity: 40 },
{ id: "hana", name: "Hana Sato", role: "Developer", team: "Apps", capacity: 40 },
{ id: "kofi", name: "Kofi Mensah", role: "Developer", team: "Apps", capacity: 32 },
{ id: "freya", name: "Freya Olsen", role: "Designer", team: "Apps", capacity: 40 },
{ id: "david", name: "David Kowalski", role: "QA engineer", team: "Apps", capacity: 20 },
];
/* Hours booked this week, per person. */
const BOOKED: Record<string, number> = {
maya: 30, arjun: 44, chloe: 38, noah: 26, eva: 36, luis: 40, hana: 39, kofi: 35, freya: 22, david: 20,
};
/* Booked hours against the person's capacity. */
const BOOKING: LimitSet = {
limits: [
{ value: 1, side: "upper", severity: "warning" },
{ value: 1.1, side: "upper", severity: "alarm" },
],
};
export default function ShowTheWorst() {
const { Table, Column, VerdictColumn } = useTable(PEOPLE, { rowKey: (p) => p.id });
return (
<Table ariaLabel="Booked hours this week">
<Column value="name" label="Name" rowHeader />
<Column value="team" label="Team" />
<Column value="capacity" label="Capacity (h)" />
<VerdictColumn
id="booked"
label="Booked"
value={(p) => (BOOKED[p.id] ?? 0) / p.capacity}
limits={BOOKING}
format="percent"
aggregate="worst"
/>
</Table>
);
}
Meter from @umriss-ui/coreThe keys of First table apply here.
VerdictBase| Name | Type | Default | Description |
|---|---|---|---|
labelrequired | string | — | Names the column in its header, in the column menu and in the header row of the export. Shown in: Read latency against its objective, Sort by verdict or by value, Judge a computed share and 5 more |
limitsrequired | LimitSet | — | The limit set every value is read against. Shown in: Read latency against its objective, Sort by verdict or by value, Judge a computed share and 5 more |
format | NumberFormat | — | How the value and its excess are written. Shown in: Judge a computed share, Show the worst in the footer, Carry the worst verdict up (Aggregate) and 1 more |
sortBy | "verdict" | "value" | "verdict" | What the column sorts by: by the weight of the verdict (with equal
weight the existing order stays) – or by the value. Shown in: Sort by verdict or by value |
width | number | the browser's | Initial width in pixels. |
resizable | boolean | — | Shows the drag grip on the header cell. |
pin | Pin"start" | "end" | — | Keeps the column in view while the table scrolls sideways, before or after every other column. |
aggregate | "worst" | — | "worst": the worst verdict among the rows – in the footer and in a
group's header.Shown in: Show the worst in the footer, Pin columns to both sides (Width and pinning), Read a day's deliveries by depot (Grouping) and 2 more |
LimitSetFrom @umriss-ui/core.
The set a value is read against. The target stands beside the limits and is not one: it is missed, never violated, and it is never the edge of a region.
| Name | Type | Default | Description |
|---|---|---|---|
limits | readonly Limit[] | — | In any order; none, one or several. Shown in: Read latency against its objective, Sort by verdict or by value, Judge a computed share and 5 more |
target | number | — | Shown in: Pin columns to both sides (Width and pinning), Carry the worst verdict up (Aggregate), Review cost centres against budget (Scenarios) |
NumberFormatThe names of the standard presentations of a number.
type NumberFormat = "percent" | "count" | { decimals: number };PinWhere a pinned column stands: in the block before or after the others.
type Pin = "start" | "end";LimitFrom @umriss-ui/core.
A bound: a value, a side, a severity.
No tolerance band as input. A band is derived - two limits of equal
severity on opposite sides enclose one - and as an input type it would
force a null on one edge in the one-sided case.
| Name | Type | Default | Description |
|---|---|---|---|
valuerequired | number | — | Shown in: Read latency against its objective, Sort by verdict or by value, Judge a computed share and 5 more |
siderequired | Side"upper" | "lower" | — | Shown in: Read latency against its objective, Sort by verdict or by value, Judge a computed share and 5 more |
severityrequired | Severity"warning" | "alarm" | — | Shown in: Read latency against its objective, Sort by verdict or by value, Judge a computed share and 5 more |
SideFrom @umriss-ui/core.
Whether a limit is an upper or a lower bound.
type Side = "upper" | "lower";SeverityFrom @umriss-ui/core.
How seriously a violation weighs: one severity that advises and one that calls for action. Exactly two; for a third distinction one sets a second limit.
type Severity = "warning" | "alarm";What umriss deliberately does not build, and why: ADR-0032.