# VerdictColumn

> Package `@umriss-ui/table`, version 0.40.0. Demo page: <https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/>. Every page in one line: [llms.txt](https://romanhaendler.github.io/umriss-ui/table/llms.txt); every page in full: [llms-full.txt](https://romanhaendler.github.io/umriss-ui/table/llms-full.txt).

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.

```ts
import { useTable } from "@umriss-ui/table";
```

Demo page: https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/

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](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0006-the-limit-model-lives-in-both-packages.md)).

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

```tsx
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.

```tsx
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.

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

### Show the worst in the footer

Set `aggregate="worst"` and the footer names the worst verdict among the rows – whether anyone in the team is overbooked.

```tsx
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

- A single reading in a tile or a bar → `Meter` from @umriss-ui/core

## Keyboard

The keys of [First table](https://romanhaendler.github.io/umriss-ui/table/first-table/#keyboard-first-table) apply here.

## API

### `VerdictBase`

| Prop | Type | Default | Description |
|---|---|---|---|
| `label` *required* | `string` | — | Names the column in its header, in the column menu and in the header row of the export.<br>Shown in: [Read latency against its objective](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#read-latency-against-its-objective), [Sort by verdict or by value](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#sort-by-verdict-or-by-value), [Judge a computed share](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#judge-a-computed-share) and 5 more |
| `limits` *required* | [`LimitSet`](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#type-LimitSet) | — | The limit set every value is read against.<br>Shown in: [Read latency against its objective](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#read-latency-against-its-objective), [Sort by verdict or by value](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#sort-by-verdict-or-by-value), [Judge a computed share](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#judge-a-computed-share) and 5 more |
| `format` | [`NumberFormat`](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#type-NumberFormat) | — | How the value and its excess are written.<br>Shown in: [Judge a computed share](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#judge-a-computed-share), [Show the worst in the footer](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#show-the-worst-in-the-footer), [Carry the worst verdict up (Aggregate)](https://romanhaendler.github.io/umriss-ui/table/aggregate/#worst-verdict) 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.<br>Shown in: [Sort by verdict or by value](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#sort-by-verdict-or-by-value) |
| `width` | `number` | the browser's | Initial width in pixels.<br>Shown in: [Pin columns to both sides (Width and pinning)](https://romanhaendler.github.io/umriss-ui/table/width-and-pinning/#pinned-both-sides) |
| `resizable` | `boolean` | — | Shows the drag grip on the header cell. |
| `pin` | [`Pin`](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#type-Pin)<br>`"start" \| "end"` | — | Keeps the column in view while the table scrolls sideways, before or after every other column.<br>Shown in: [Pin columns to both sides (Width and pinning)](https://romanhaendler.github.io/umriss-ui/table/width-and-pinning/#pinned-both-sides) |
| `aggregate` | `"worst"` | — | `"worst"`: the worst verdict among the rows – in the footer and in a group's header.<br>Shown in: [Show the worst in the footer](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#show-the-worst-in-the-footer), [Pin columns to both sides (Width and pinning)](https://romanhaendler.github.io/umriss-ui/table/width-and-pinning/#pinned-both-sides), [Read a day's deliveries by depot (Grouping)](https://romanhaendler.github.io/umriss-ui/table/grouping/#the-dispatch-report) 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.

| Prop | Type | Default | Description |
|---|---|---|---|
| `limits` | `readonly `[`Limit`](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#type-Limit)`[]` | — | In any order; none, one or several.<br>Shown in: [Read latency against its objective](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#read-latency-against-its-objective), [Sort by verdict or by value](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#sort-by-verdict-or-by-value), [Judge a computed share](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#judge-a-computed-share) and 5 more |
| `target` | `number` | — | <br>Shown in: [Pin columns to both sides (Width and pinning)](https://romanhaendler.github.io/umriss-ui/table/width-and-pinning/#pinned-both-sides), [Carry the worst verdict up (Aggregate)](https://romanhaendler.github.io/umriss-ui/table/aggregate/#worst-verdict), [Review cost centres against budget (Scenarios)](https://romanhaendler.github.io/umriss-ui/table/#review-cost-centres) |

#### `NumberFormat`

The names of the standard presentations of a number.

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

#### `Pin`

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

```ts
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.

| Prop | Type | Default | Description |
|---|---|---|---|
| `value` *required* | `number` | — | <br>Shown in: [Read latency against its objective](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#read-latency-against-its-objective), [Sort by verdict or by value](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#sort-by-verdict-or-by-value), [Judge a computed share](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#judge-a-computed-share) and 5 more |
| `side` *required* | [`Side`](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#type-Side)<br>`"upper" \| "lower"` | — | <br>Shown in: [Read latency against its objective](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#read-latency-against-its-objective), [Sort by verdict or by value](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#sort-by-verdict-or-by-value), [Judge a computed share](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#judge-a-computed-share) and 5 more |
| `severity` *required* | [`Severity`](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#type-Severity)<br>`"warning" \| "alarm"` | — | <br>Shown in: [Read latency against its objective](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#read-latency-against-its-objective), [Sort by verdict or by value](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#sort-by-verdict-or-by-value), [Judge a computed share](https://romanhaendler.github.io/umriss-ui/table/verdictcolumn/#judge-a-computed-share) and 5 more |

#### `Side`

From `@umriss-ui/core`.

Whether a limit is an upper or a lower bound.

```ts
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.

```ts
type Severity = "warning" | "alarm";
```

## Known limits

- No list filter: four verdicts are sorted, not filtered.
- It does not judge whether a value is stale; a stale value keeps its verdict ([ADR-0010](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0010-a-stale-value-keeps-its-verdict.md)).

What umriss deliberately does not build, and why: [ADR-0032](https://romanhaendler.github.io/umriss-ui/what-umriss-ui-is-not/).
