VerdictColumn

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).

Examples

Read latency against its objective

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>
  );
}

Sort by verdict or by value

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>
  );
}

Judge a computed share

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>
  );
}

When to use something else

Keyboard

The keys of First table apply here.

API

VerdictBase

NameTypeDefaultDescription
labelrequiredstring—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

limitsrequiredLimitSet—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

formatNumberFormat—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

widthnumberthe browser'sInitial width in pixels.

Shown in: Pin columns to both sides (Width and pinning)

resizableboolean—Shows the drag grip on the header cell.
pinPin
"start" | "end"
—Keeps the column in view while the table scrolls sideways, before or after every other column.

Shown in: Pin columns to both sides (Width and pinning)

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

Types on this page

LimitSet

From @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.

NumberFormat

The names of the standard presentations of a number.

type NumberFormat = "percent" | "count" | { decimals: number };

Pin

Where a pinned column stands: in the block before or after the others.

type Pin = "start" | "end";

Limit

From @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.

Side

From @umriss-ui/core.

Whether a limit is an upper or a lower bound.

type Side = "upper" | "lower";

Severity

From @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";

Known limits

What umriss deliberately does not build, and why: ADR-0032.

Every page of @umriss-ui/table

Getting started

Columns

Finding rows

Grouping

Rows

Around the table

Many rows

Editing

Limits and alarms

API index