What a column's values come to: a sum, an average, the extremes, a count. It stands in the footer over the filtered rows and in each group header over the group's rows.
import { useTable } from "@umriss-ui/table";
An aggregate is always computed from rows: a group's average is over its rows, not over the averages of the groups inside it. Absent values do not count.
A rate or a weighted average is the application's to say, with an aggregate of its own.
aggregate="sum" and "avg" fill the footer over what search and filters leave; an average counts only the values present.
import { Search, Toolbar, useTable } from "@umriss-ui/table";
interface Tour {
id: string;
depot: string;
km: number;
stops: number;
load: number | null;
}
const TOURS: Tour[] = [
{ id: "T-01", depot: "North depot", km: 84, stops: 11, load: 0.72 },
{ id: "T-02", depot: "North depot", km: 91, stops: 9, load: 0.64 },
{ id: "T-03", depot: "North depot", km: 152, stops: 5, load: 0.88 },
{ id: "T-04", depot: "Riverside depot", km: 77, stops: 10, load: null },
{ id: "T-05", depot: "Riverside depot", km: 102, stops: 8, load: 0.58 },
];
export default function SumAndAverage() {
const { Table, Column } = useTable(TOURS, { rowKey: (t) => t.id });
return (
<Table ariaLabel="Tours">
<Toolbar>
<Search placeholder="Search depot" />
</Toolbar>
<Column value="id" label="Tour" rowHeader />
<Column value="depot" label="Depot" />
<Column value="km" label="Distance (km)" aggregate="sum" />
<Column value="stops" label="Stops" aggregate="sum" />
<Column value="load" label="Load" format="percent" aggregate="avg" />
</Table>
);
}
"min" and "max" take the extremes of numbers and dates alike, written in the column's format.
import { useTable } from "@umriss-ui/table";
interface Incident {
id: string;
toAcknowledge: number;
opened: Date;
}
const at = (day: number, hours: number, minutes: number) => new Date(2026, 2, day, hours, minutes);
const INCIDENTS: Incident[] = [
{ id: "INC-1048", toAcknowledge: 4, opened: at(17, 9, 42) },
{ id: "INC-1047", toAcknowledge: 47, opened: at(17, 7, 15) },
{ id: "INC-1046", toAcknowledge: 6, opened: at(16, 22, 5) },
{ id: "INC-1045", toAcknowledge: 4, opened: at(16, 14, 20) },
{ id: "INC-1044", toAcknowledge: 1392, opened: at(15, 10, 0) },
];
export default function MinimumAndMaximum() {
const { Table, Column } = useTable(INCIDENTS, { rowKey: (i) => i.id });
return (
<Table ariaLabel="Time to acknowledge">
<Column value="id" label="Incident" rowHeader />
<Column value="toAcknowledge" label="Minutes to acknowledge" aggregate="max" />
<Column value="opened" label="Opened" format="dateTime" aggregate="min" />
</Table>
);
}
"range" gives the earliest and latest date of each group, so a project's header says from when to when its work runs.
import { useTable } from "@umriss-ui/table";
interface WorkItem {
id: string;
project: string;
person: string;
name: string;
starts: Date;
}
const day = (d: number) => new Date(2026, 2, d);
const WORK: WorkItem[] = [
{ id: "W-101", project: "Member portal", person: "Arjun Mehta", name: "Sign-in with e-mail code", starts: day(9) },
{ id: "W-102", project: "Member portal", person: "Arjun Mehta", name: "Profile page", starts: day(12) },
{ id: "W-105", project: "Member portal", person: "Chloe Durand", name: "Change of address form", starts: day(16) },
{ id: "W-109", project: "Member portal", person: "Eva Novak", name: "Regression run", starts: day(17) },
{ id: "W-104", project: "Online shop relaunch", person: "Chloe Durand", name: "Basket keeps items across devices", starts: day(11) },
{ id: "W-107", project: "Online shop relaunch", person: "Noah Fischer", name: "Search results layout", starts: day(16) },
{ id: "W-110", project: "Booking app", person: "Hana Sato", name: "Reminder scheduling service", starts: day(9) },
{ id: "W-115", project: "Booking app", person: "Freya Olsen", name: "Store screenshots", starts: day(18) },
];
export default function RangeOfDates() {
const { Table, Column } = useTable(WORK, { rowKey: (w) => w.id, defaultGrouping: ["project", "person"] });
return (
<Table ariaLabel="Work items by project and person">
<Column value="person" label="Person" />
<Column value="project" label="Project" />
<Column value="id" label="Item" rowHeader />
<Column value="name" label="Title" />
<Column value="starts" label="Starts" format="date" aggregate="range" />
</Table>
);
}
"count" counts the values present, "distinct" the different ones: eight alerts, but three services and two people assigned.
import { useTable } from "@umriss-ui/table";
interface Alert {
id: string;
service: string;
assignee: string | null;
minutes: number;
}
const ALERTS: Alert[] = [
{ id: "A-2201", service: "Checkout", assignee: "Jonas Keller", minutes: 18 },
{ id: "A-2202", service: "Checkout", assignee: "Jonas Keller", minutes: 42 },
{ id: "A-2203", service: "Search", assignee: "Leila Haddad", minutes: 21 },
{ id: "A-2204", service: "Search", assignee: null, minutes: 7 },
{ id: "A-2205", service: "Webhooks", assignee: null, minutes: 35 },
{ id: "A-2206", service: "Checkout", assignee: "Leila Haddad", minutes: 16 },
{ id: "A-2207", service: "Webhooks", assignee: null, minutes: 25 },
{ id: "A-2208", service: "Search", assignee: "Jonas Keller", minutes: 4 },
];
export default function CountAndDistinct() {
const { Table, Column } = useTable(ALERTS, { rowKey: (a) => a.id });
return (
<Table ariaLabel="Alerts this morning">
<Column value="id" label="Alert" rowHeader aggregate="count" />
<Column value="service" label="Service" aggregate="distinct" />
<Column value="assignee" label="Assignee" aggregate="distinct" />
<Column value="minutes" label="Minutes open" aggregate="sum" />
</Table>
);
}
Under a group's sum stands a thin bar for its share of the whole; share={false} takes it away where a share says nothing.
import { useTable } from "@umriss-ui/table";
interface Invoice {
id: string;
costCentre: string;
supplier: string;
amount: number;
lines: number;
}
const INVOICES: Invoice[] = [
{ id: "INV-26-0318", costCentre: "Facilities", supplier: "Brandlow Office Supply", amount: 1951.24, lines: 3 },
{ id: "INV-26-0312", costCentre: "Facilities", supplier: "Hartwell Cleaning", amount: 2400, lines: 1 },
{ id: "INV-26-0301", costCentre: "Facilities", supplier: "Brandlow Office Supply", amount: 860, lines: 1 },
{ id: "INV-26-0317", costCentre: "Marketing", supplier: "Kettering & Shaw Events", amount: 15_110, lines: 2 },
{ id: "INV-26-0296", costCentre: "Marketing", supplier: "Lumen Print", amount: 1940, lines: 4 },
{ id: "INV-26-0309", costCentre: "IT", supplier: "Nimbrel Software", amount: 7288, lines: 2 },
];
export default function ShareBar() {
const { Table, Column } = useTable(INVOICES, { rowKey: (i) => i.id, defaultGrouping: ["costCentre", "supplier"] });
return (
<Table ariaLabel="Invoices by cost centre and supplier">
<Column value="supplier" label="Supplier" />
<Column value="costCentre" label="Cost centre" />
{/* The amount with its bar right beside the groups: after the invoice
it stood out of view on a phone. */}
<Column value="amount" label="Amount (€)" format={{ decimals: 2 }} aggregate="sum" />
<Column value="id" label="Invoice" rowHeader />
<Column value="lines" label="Lines" aggregate="sum" share={false} />
</Table>
);
}
Averaging rates weighs a small tour like a large one; divide the sums with an aggregate of your own, and header and footer are right.
import { useTable } from "@umriss-ui/table";
interface Tour {
id: string;
depot: string;
parcels: number;
failed: number;
}
const TOURS: Tour[] = [
{ id: "T-01", depot: "North depot", parcels: 410, failed: 5 },
{ id: "T-02", depot: "North depot", parcels: 380, failed: 3 },
{ id: "T-03", depot: "North depot", parcels: 12, failed: 3 },
{ id: "T-04", depot: "Riverside depot", parcels: 520, failed: 6 },
{ id: "T-05", depot: "Riverside depot", parcels: 460, failed: 4 },
];
const rate = (t: Tour) => t.failed / t.parcels;
export default function NeverAnAverageOfAverages() {
const { Table, Column } = useTable(TOURS, { rowKey: (t) => t.id, defaultGrouping: "depot" });
return (
<Table ariaLabel="Failed deliveries by depot">
<Column value="depot" label="Depot" />
<Column value="id" label="Tour" rowHeader />
<Column value="parcels" label="Parcels" format="count" aggregate="sum" share={false} />
<Column value="failed" label="Failed" aggregate="sum" share={false} />
<Column id="rateAveraged" value={rate} label="Rate, averaged" format="percent" aggregate="avg" />
<Column
id="rate"
value={rate}
label="Rate, weighted"
format="percent"
aggregate={(_, tours) => tours.reduce((s, t) => s + t.failed, 0) / tours.reduce((s, t) => s + t.parcels, 0)}
/>
</Table>
);
}
A function gets the values present and the group's rows and returns a value of the column's type; here a unit price weighted by quantity.
import { useTable } from "@umriss-ui/table";
interface Line {
id: string;
costCentre: string;
description: string;
quantity: number;
unitPrice: number;
}
const LINES: Line[] = [
{ id: "L-1", costCentre: "Facilities", description: "Desk lamps, LED", quantity: 24, unitPrice: 48.9 },
{ id: "L-2", costCentre: "Facilities", description: "Printer paper, box", quantity: 40, unitPrice: 21.5 },
{ id: "L-3", costCentre: "Facilities", description: "Delivery", quantity: 1, unitPrice: 35 },
{ id: "L-4", costCentre: "IT", description: "Design tool licences", quantity: 12, unitPrice: 459 },
{ id: "L-5", costCentre: "IT", description: "Onboarding session", quantity: 2, unitPrice: 890 },
];
export default function OwnAggregate() {
const { Table, Column } = useTable(LINES, { rowKey: (l) => l.id, defaultGrouping: "costCentre" });
return (
<Table ariaLabel="Invoice lines by cost centre">
<Column value="costCentre" label="Cost centre" />
<Column value="description" label="Description" rowHeader />
<Column value="quantity" label="Quantity" aggregate="sum" />
<Column
value="unitPrice"
label="Unit price (€)"
format={{ decimals: 2 }}
aggregate={(_, lines) => lines.reduce((s, l) => s + l.unitPrice * l.quantity, 0) / lines.reduce((s, l) => s + l.quantity, 0)}
/>
</Table>
);
}
On a VerdictColumn, aggregate="worst" shows the worst verdict among a group's rows; fold a zone and its line still shows the alarm.
import type { LimitSet } from "@umriss-ui/core";
import { useTable } from "@umriss-ui/table";
/* Data from the plant world, written out here so the example runs on its own. */
/** The kiln's zone 3, in °C: the numbers every part reads it against, once.
Between the two warnings lies the tolerance - a tile fired outside it is
scrap; `returnTo` is the alarm's dead band. */
const KILN = { target: 1200, tolerance: [1185, 1215], alarm: 1230, returnTo: 1222 } as const;
/** The same numbers as the limit set the tile and the verdict read. */
const KILN_LIMITS: LimitSet = {
target: KILN.target,
limits: [
{ value: KILN.tolerance[1], side: "upper", severity: "warning" },
{ value: KILN.alarm, side: "upper", severity: "alarm" },
{ value: KILN.tolerance[0], side: "lower", severity: "warning" },
],
};
interface Reading {
id: string;
kiln: string;
zone: string;
at: string;
temperature: number | null;
}
const READINGS: Reading[] = [
{ id: "Z1-0600", kiln: "Kiln K1", zone: "Zone 1", at: "06:00", temperature: 1198.4 },
{ id: "Z1-0700", kiln: "Kiln K1", zone: "Zone 1", at: "07:00", temperature: 1201.2 },
{ id: "Z1-0800", kiln: "Kiln K1", zone: "Zone 1", at: "08:00", temperature: 1203.9 },
{ id: "Z2-0600", kiln: "Kiln K1", zone: "Zone 2", at: "06:00", temperature: 1209.5 },
{ id: "Z2-0700", kiln: "Kiln K1", zone: "Zone 2", at: "07:00", temperature: 1217.8 },
{ id: "Z2-0800", kiln: "Kiln K1", zone: "Zone 2", at: "08:00", temperature: 1212 },
{ id: "Z3-0600", kiln: "Kiln K1", zone: "Zone 3", at: "06:00", temperature: 1224.1 },
{ id: "Z3-0700", kiln: "Kiln K1", zone: "Zone 3", at: "07:00", temperature: 1233.6 },
{ id: "Z3-0800", kiln: "Kiln K1", zone: "Zone 3", at: "08:00", temperature: null },
];
export default function WorstVerdict() {
const { Table, Column, VerdictColumn } = useTable(READINGS, { rowKey: (r) => r.id, defaultGrouping: ["kiln", "zone"] });
return (
<Table ariaLabel="Kiln temperature by zone">
<Column value="zone" label="Zone" />
<Column value="kiln" label="Kiln" />
<Column value="at" label="Hour" rowHeader />
<VerdictColumn value="temperature" label="Temperature (°C)" limits={KILN_LIMITS} format={{ decimals: 1 }} aggregate="worst" />
</Table>
);
}
Stat from @umriss-ui/coreThe keys of First table and Input apply here.
AggregateOptions<W, Z>| Name | Type | Default | Description |
|---|---|---|---|
aggregate | AggregateFor<W, Z> | — | What the column's values come to: in the footer over the filtered set,
in a group's header over its rows. "sum", "avg" for numbers; "min",
"max" for numbers and points in time; "range" for points in time;
"count", "distinct" for every value with a text form - or a function
of one's own. Always from the values, never from other aggregates; absent
values count towards nothing.Shown in: Total a column, Find the extremes, Show a range of dates and 34 more |
share | boolean | — | The share bar in a group's header – the group's share of the filtered
set's sum. It stands under the first column with a sum, one per header;
false passes it on to the next.Shown in: Show each group's share, Weight a rate by its rows, Share a column between tables (Presets) and 2 more |
AggregateFor<W, Z>What a column accepts as aggregate: sums and averages for numbers, the
extremes for numbers and points in time, the range for points in time,
counts for every value with a text form - or a function of one's own.
type AggregateFor<W, Z> =
| ([Present<W>] extends [number] ? "sum" | "avg" | "min" | "max" : never)
| ([Present<W>] extends [Date] ? "min" | "max" | "range" : never)
| (IsDisplayable<W> extends true ? "count" | "distinct" : never)
| AggregateFunction<W, Z>;Present<W>The value without the absence - that is what children gets.
type Present<W> = Exclude<W, Absent>;IsDisplayable<W>true where the value present can appear as text without children
(Displayable), false otherwise - what decides the filters and
aggregates a column offers.
type IsDisplayable<W> = [Present<W>] extends [never]
? false
: [Present<W>] extends [Displayable]
? true
: false;AggregateFunction<W, Z>An aggregate of one's own: the values present and the rows of the group -
or of the filtered set, in the footer. Its result runs through the column's
presentation; undefined means there is none.
type AggregateFunction<W, Z> = (values: readonly Present<NoInfer<W>>[], rows: readonly Z[]) => Present<NoInfer<W>> | undefined;AbsentA value that is not there: null or undefined.
type Absent = null | undefined;DisplayableWhat can appear as text without children.
type Displayable = string | number | boolean | Date;What umriss deliberately does not build, and why: ADR-0032.