Aggregate

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.

Examples

Total a column

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

Find the extremes

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

Show a range of dates

"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 values and distinct values

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

Show each group's share

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

Weight a rate by its rows

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

Write an aggregate of your own

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

Carry the worst verdict up

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

When to use something else

Keyboard

The keys of First table and Input apply here.

API

AggregateOptions<W, Z>

NameTypeDefaultDescription
aggregateAggregateFor<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

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

Types on this page

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;

Absent

A value that is not there: null or undefined.

type Absent = null | undefined;

Displayable

What can appear as text without children.

type Displayable = string | number | boolean | Date;

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