Worked examples

Whole figures as a screen shows them: the cost per stop of a day's tours, invoices with discount and VAT, and the equipment effectiveness of a hall, each built from data.

import { Calculation, Chain, Given, Ref } from "@umriss-ui/calculation";

Examples

The cost per stop of a day's tours

Eight tours from data, some sixty quantities: the sheet stands open, every tree in it folded, and the stale diesel price says so.

import { Calculation, Chain, DividedBy, Given, Interim, Plus, Product, Sum } from "@umriss-ui/calculation";

/* Data from the logistics world, written out here so the example runs on its own. */

/** A small LCG - the same numbers on every computer. */
function random(seed: number): () => number {
  let s = seed >>> 0;
  return () => {
    s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
    return s / 4294967296;
  };
}

const at = (hours: number, minutes = 0) => new Date(2026, 2, 17, hours, minutes).getTime();
const MINUTE = 60_000;

interface Vehicle {
  id: string;
  plate: string;
  type: "van" | "e-van" | "truck";
  /** Payload, in kg. */
  capacity: number;
  depot: string;
}

const VEHICLES: readonly Vehicle[] = [
  { id: "v1", plate: "FP 214 K", type: "van", capacity: 1200, depot: "north" },
  { id: "v2", plate: "FP 377 K", type: "e-van", capacity: 900, depot: "north" },
  { id: "v3", plate: "FP 118 R", type: "truck", capacity: 7500, depot: "north" },
  { id: "v4", plate: "FP 402 R", type: "van", capacity: 1200, depot: "river" },
  { id: "v5", plate: "FP 455 R", type: "e-van", capacity: 900, depot: "river" },
  { id: "v6", plate: "FP 290 E", type: "van", capacity: 1200, depot: "east" },
  { id: "v7", plate: "FP 311 E", type: "e-van", capacity: 900, depot: "east" },
  { id: "v8", plate: "FP 520 E", type: "truck", capacity: 7500, depot: "east" },
];

const DRIVERS = [
  { id: "d1", name: "Martin Hale", depot: "north" },
  { id: "d2", name: "Nadia Petrova", depot: "north" },
  { id: "d3", name: "Owen Carter", depot: "north" },
  { id: "d4", name: "Lucia Romero", depot: "river" },
  { id: "d5", name: "Ben Adeyemi", depot: "river" },
  { id: "d6", name: "Hanna Berg", depot: "east" },
  { id: "d7", name: "Yusuf Demir", depot: "east" },
  { id: "d8", name: "Clara Wendt", depot: "east" },
] as const;

const CUSTOMERS = [
  "Holloway Garden Supplies",
  "Marlow & Finch Books",
  "Oakridge Pharmacy",
  "Brixley Cycles",
  "Tamsin's Bakery",
  "Northfold Office",
  "Pellham Hardware",
  "Greywick Studio",
  "Juniper Lane Florist",
  "Ashcombe Dental",
] as const;

interface Stop {
  id: string;
  shipment: string;
  customer: string;
  /** The window the customer was promised, `[from, to]`. */
  window: readonly [number, number];
  /** When the tour plans to be there. */
  arrival: number;
}

interface Tour {
  id: string;
  vehicle: string;
  driver: string;
  depot: string;
  /** Leaves the depot, loaded; back at the depot. */
  from: number;
  to: number;
  /** Minutes of loading before `from`. */
  loading: number;
  km: number;
  stops: readonly Stop[];
}

/** One tour per vehicle. Stops every half hour or so, each with a two-hour
    window; every seventh arrival misses its window - the late ones a
    dispatcher looks for. */
function tour(index: number): Tour {
  const r = random(700 + index);
  const vehicle = VEHICLES[index]!;
  const truck = vehicle.type === "truck";
  const from = at(truck ? 6 : 7, index % 2 === 0 ? 0 : 30);
  const count = truck ? 5 : 8 + Math.floor(r() * 4);
  const stops: Stop[] = [];
  let arrival = from + (20 + Math.floor(r() * 15)) * MINUTE;
  for (let i = 0; i < count; i++) {
    const n = index * 20 + i;
    const slot = Math.floor((arrival - at(0)) / (2 * 60 * MINUTE)) * 2;
    /* A late stop's window closed half an hour or more before the arrival. */
    const late = i >= 2 && n % 7 === 3;
    const halfHour = 30 * MINUTE;
    const windowFrom = late ? Math.floor(arrival / halfHour) * halfHour - 5 * halfHour : at(slot);
    stops.push({
      id: `s-${index + 1}-${i + 1}`,
      shipment: `FP-${(1_004_210 + n * 13).toString()}`,
      customer: CUSTOMERS[(n * 3) % CUSTOMERS.length]!,
      window: [windowFrom, windowFrom + 2 * 60 * MINUTE],
      arrival,
    });
    arrival += (truck ? 45 : 22) * MINUTE + Math.floor(r() * 18) * MINUTE;
  }
  return {
    id: `T-${String(index + 1).padStart(2, "0")}`,
    vehicle: vehicle.id,
    driver: DRIVERS[index]!.id,
    depot: vehicle.depot,
    from,
    to: arrival + 30 * MINUTE,
    loading: truck ? 45 : 30,
    km: Math.round((truck ? 140 : 70) + r() * 40),
    stops,
  };
}

const TOURS: readonly Tour[] = VEHICLES.map((_, index) => tour(index));

/** What a tour costs, per unit - the inputs of a cost per tour. */
const COST_RATES = {
  /** Diesel, per litre; electricity, per kWh. */
  dieselPerLitre: 1.68,
  electricityPerKwh: 0.31,
  /** Consumption per 100 km. */
  per100Km: { van: 9.5, truck: 24, "e-van": 22 },
  /** A driver's hour, all in. */
  driverPerHour: 34.5,
  /** Lease, insurance and upkeep, per day. */
  vehiclePerDay: { van: 58, truck: 145, "e-van": 72 },
} as const;

const typeOf = (vehicleId: string) => VEHICLES.find((vehicle) => vehicle.id === vehicleId)!.type;
const hours = (tour: (typeof TOURS)[number]) => (tour.to - tour.from) / 3_600_000 + tour.loading / 60;
const DIESEL = TOURS.filter((tour) => typeOf(tour.vehicle) !== "e-van");
const ELECTRIC = TOURS.filter((tour) => typeOf(tour.vehicle) === "e-van");
const STOPS = TOURS.reduce((sum, tour) => sum + tour.stops.length, 0);

const AGES = { stale: 24 * 3_600_000, lost: 14 * 24 * 3_600_000 };
/* The price was read on Saturday at 06:00, seen from Tuesday 10:30; its
   freshness counts from page load, the tours keep their Tuesday. */
const LOADED = Date.now();
const DIESEL_READ = LOADED - (at(10, 30) - new Date(2026, 2, 14, 6, 0).getTime());

export default function CostPerTour() {
  return (
    <Calculation aria-label="Cost per stop, all tours, Tuesday">
      <Chain>
        <Product label="Drivers" unit="€" decimals={2}>
          <Sum label="Driver hours" unit="h" decimals={1}>
            {TOURS.map((tour) => (
              <Given key={tour.id} label={`Hours, ${tour.id}`} value={hours(tour)} unit="h" decimals={1} />
            ))}
          </Sum>
          <Given label="Driver rate, all in" value={COST_RATES.driverPerHour} unit="€/h" decimals={2} />
        </Product>
        <Plus>
          <Product label="Diesel" unit="€" decimals={2}>
            <Sum label="Diesel used" unit="l" decimals={1}>
              {DIESEL.map((tour) => (
                <Product key={tour.id} label={`Diesel, ${tour.id}`} unit="l" decimals={1}>
                  <Given label={`Distance, ${tour.id}`} value={tour.km} unit="km" />
                  <Given label={`Consumption, ${tour.id}`} value={COST_RATES.per100Km[typeOf(tour.vehicle)] / 100} unit="l/km" />
                </Product>
              ))}
            </Sum>
            <Given
              label="Diesel price"
              value={COST_RATES.dieselPerLitre}
              unit="€/l"
              decimals={2}
              source="Fuel card, weekly price"
              asOf={DIESEL_READ}
              ages={AGES}
            />
          </Product>
        </Plus>
        <Plus>
          <Product label="Electricity" unit="€" decimals={2}>
            <Sum label="Electricity used" unit="kWh" decimals={1}>
              {ELECTRIC.map((tour) => (
                <Product key={tour.id} label={`Electricity, ${tour.id}`} unit="kWh" decimals={1}>
                  <Given label={`Distance, ${tour.id}`} value={tour.km} unit="km" />
                  <Given label={`Consumption, ${tour.id}`} value={COST_RATES.per100Km["e-van"] / 100} unit="kWh/km" />
                </Product>
              ))}
            </Sum>
            <Given label="Electricity price" value={COST_RATES.electricityPerKwh} unit="€/kWh" decimals={2} source="Depot tariff 2026" />
          </Product>
        </Plus>
        <Plus>
          <Sum label="Vehicle days" unit="€" decimals={2}>
            {TOURS.map((tour) => (
              <Given key={tour.id} label={`Vehicle day, ${tour.id}`} value={COST_RATES.vehiclePerDay[typeOf(tour.vehicle)]} unit="€" decimals={2} />
            ))}
          </Sum>
        </Plus>
        <Interim label="Cost of the day's tours" unit="€" decimals={2} />
        <DividedBy label="Stops" value={STOPS} unit="stops" />
        <Interim
          label="Cost per stop"
          unit="€/stop"
          decimals={2}
          limits={[{ value: 30, side: "upper", severity: "warning" }]}
        />
      </Chain>
    </Calculation>
  );
}

Invoices with discount and VAT

Each invoice is a chain from its lines, discounts and VAT by rate; as operands of one sum they fold to their totals.

import type { ReactElement } from "react";
import { Calculation, Chain, Difference, Given, Interim, Plus, Product, Ref, Sum } from "@umriss-ui/calculation";

/* Data from the controlling world, written out here so the example runs on its own. */

const on = (month: number, day: number) => new Date(2026, month - 1, day).getTime();

interface InvoiceLine {
  description: string;
  quantity: number;
  unitPrice: number;
  /** A fraction: 0.1 is ten per cent off. */
  discount: number;
  /** A fraction: 0.19 or 0.07, or 0 where no VAT is charged. */
  vatRate: number;
}

interface Invoice {
  id: string;
  supplier: string;
  costCentre: string;
  received: number;
  due: number;
  status: "awaiting approval" | "approved" | "paid" | "rejected";
  lines: readonly InvoiceLine[];
}

const INVOICES: readonly Invoice[] = [
  {
    id: "INV-26-0318", supplier: "Brandlow Office Supply", costCentre: "CC-4400", received: on(3, 16), due: on(4, 15), status: "awaiting approval",
    lines: [
      { description: "Desk lamps, LED", quantity: 24, unitPrice: 48.9, discount: 0.1, vatRate: 0.19 },
      { description: "Printer paper, box of 5 reams", quantity: 40, unitPrice: 21.5, discount: 0, vatRate: 0.19 },
      { description: "Delivery", quantity: 1, unitPrice: 35, discount: 0, vatRate: 0.19 },
    ],
  },
  {
    id: "INV-26-0317", supplier: "Kettering & Shaw Events", costCentre: "CC-1200", received: on(3, 13), due: on(4, 12), status: "awaiting approval",
    lines: [
      { description: "Trade fair stand, 3 days", quantity: 1, unitPrice: 12_400, discount: 0.05, vatRate: 0.19 },
      { description: "Catering, per guest", quantity: 180, unitPrice: 18.5, discount: 0, vatRate: 0.07 },
    ],
  },
  {
    id: "INV-26-0309", supplier: "Nimbrel Software", costCentre: "CC-4300", received: on(3, 9), due: on(4, 8), status: "approved",
    lines: [
      { description: "Design tool licences, annual", quantity: 12, unitPrice: 540, discount: 0.15, vatRate: 0.19 },
      { description: "Onboarding session", quantity: 2, unitPrice: 890, discount: 0, vatRate: 0.19 },
    ],
  },
  {
    id: "INV-26-0302", supplier: "Fenwright Legal", costCentre: "CC-4100", received: on(3, 2), due: on(3, 16), status: "paid",
    lines: [{ description: "Contract review, hours", quantity: 14.5, unitPrice: 260, discount: 0, vatRate: 0.19 }],
  },
  {
    id: "INV-26-0226", supplier: "Corrin Travel", costCentre: "CC-1100", received: on(2, 26), due: on(3, 12), status: "paid",
    lines: [
      { description: "Rail tickets, sales conference", quantity: 22, unitPrice: 139, discount: 0, vatRate: 0.07 },
      { description: "Hotel, nights", quantity: 44, unitPrice: 112, discount: 0.08, vatRate: 0.07 },
    ],
  },
  {
    id: "INV-26-0221", supplier: "Stellbrook Consulting", costCentre: "CC-2100", received: on(2, 21), due: on(3, 23), status: "rejected",
    lines: [{ description: "Architecture review, days", quantity: 6, unitPrice: 1450, discount: 0, vatRate: 0 }],
  },
];

const OPEN = INVOICES.filter((invoice) => invoice.status === "awaiting approval" || invoice.status === "approved");
const percent = (rate: number) => `${Math.round(rate * 100)} %`;

/* A sum needs two operands; one stands for itself. */
const sumOf = (label: string, operands: ReactElement[]) =>
  operands.length === 1 ? operands[0] : (
    <Sum label={label} unit="€" decimals={2}>
      {operands}
    </Sum>
  );

function invoiceChain(invoice: Invoice) {
  const lineId = (index: number) => `${invoice.id}-line-${index}`;
  const rates = [...new Set(invoice.lines.map((line) => line.vatRate))];
  return (
    <Chain key={invoice.id}>
      {sumOf(
        `Net, ${invoice.id}`,
        invoice.lines.map((line, index) => {
          const amount = (
            <Product key={index} id={line.discount === 0 ? lineId(index) : `${lineId(index)}-list`} label={line.description} unit="€" decimals={2}>
              <Given label={`Quantity, ${line.description}`} value={line.quantity} />
              <Given label={`Unit price, ${line.description}`} value={line.unitPrice} unit="€" decimals={2} />
            </Product>
          );
          if (line.discount === 0) return amount;
          return (
            <Difference key={index} id={lineId(index)} label={`${line.description}, after discount`} unit="€" decimals={2}>
              {amount}
              <Product label={`Discount, ${line.description}`} unit="€" decimals={2}>
                <Given label={`Discount rate, ${line.description}`} value={line.discount} format="percent" />
                <Ref to={`${lineId(index)}-list`} />
              </Product>
            </Difference>
          );
        }),
      )}
      <Plus>
        {sumOf(
          `VAT, ${invoice.id}`,
          rates.map((rate) => {
            const lines = invoice.lines.flatMap((line, index) => (line.vatRate === rate ? [<Ref key={index} to={lineId(index)} />] : []));
            return (
              <Product key={rate} label={`VAT at ${percent(rate)}, ${invoice.id}`} unit="€" decimals={2}>
                <Given label={`VAT rate ${percent(rate)}`} value={rate} format="percent" />
                {sumOf(`Net at ${percent(rate)}, ${invoice.id}`, lines)}
              </Product>
            );
          }),
        )}
      </Plus>
      <Interim label={`${invoice.id}, ${invoice.supplier}`} unit="€" decimals={2} />
    </Chain>
  );
}

export default function Invoices() {
  return (
    <Calculation aria-label="Invoices to be paid, March">
      <Sum label="To be paid" unit="€" decimals={2}>
        {OPEN.map(invoiceChain)}
      </Sum>
    </Calculation>
  );
}

OEE of a hall, weighted by planned time

Three lines from data, three levels deep; line C's downtime is null, so its OEE and the hall's are missing while A and B stand.

import { Calculation, Difference, Given, Product, Quotient, Ref, Sum } from "@umriss-ui/calculation";

/* Data from the plant world, written out here so the example runs on its own. */

/** The early shift, 06:00 to 14:00. */
const SHIFT_MINUTES = 480;

interface Line {
  name: string;
  planned: number;
  downtime: number | null;
  idealCycle: number;
  total: number;
  good: number;
}

const BREAKS = 30;

const LINES: Line[] = [
  { name: "A", planned: SHIFT_MINUTES - BREAKS, downtime: 38, idealCycle: 0.8, total: 480, good: 461 },
  { name: "B", planned: SHIFT_MINUTES - BREAKS, downtime: 62, idealCycle: 1.2, total: 300, good: 291 },
  { name: "C", planned: SHIFT_MINUTES / 2, downtime: null, idealCycle: 0.5, total: 402, good: 395 },
];

function lineOee(line: Line) {
  const n = line.name;
  return (
    <Product key={n} label={`Weighted OEE, line ${n}`} unit="min">
      <Product label={`OEE, line ${n}`} format="percent">
        <Quotient label={`Availability, line ${n}`} format="percent">
          <Difference id={`runtime-${n}`} label={`Run time, line ${n}`} unit="min">
            <Given id={`planned-${n}`} label={`Planned time, line ${n}`} value={line.planned} unit="min" />
            <Given label={`Downtime, line ${n}`} value={line.downtime} unit="min" source="Shift log" />
          </Difference>
          <Ref to={`planned-${n}`} />
        </Quotient>
        <Quotient label={`Performance, line ${n}`} format="percent">
          <Product label={`Ideal run time, line ${n}`} unit="min">
            <Given label={`Ideal cycle time, line ${n}`} value={line.idealCycle} unit="min/pc" />
            <Given id={`total-${n}`} label={`Total count, line ${n}`} value={line.total} unit="pcs" />
          </Product>
          <Ref to={`runtime-${n}`} />
        </Quotient>
        <Quotient label={`Quality, line ${n}`} format="percent">
          <Given label={`Good count, line ${n}`} value={line.good} unit="pcs" />
          <Ref to={`total-${n}`} />
        </Quotient>
      </Product>
      <Ref to={`planned-${n}`} />
    </Product>
  );
}

export default function HallOee() {
  return (
    <Calculation aria-label="OEE of hall 2, early shift">
      <Quotient label="OEE, hall 2" format="percent" target={0.8}>
        <Sum label="Weighted OEE, all lines" unit="min">
          {LINES.map(lineOee)}
        </Sum>
        <Sum label="Planned time, all lines" unit="min">
          {LINES.map((line) => (
            <Ref key={line.name} to={`planned-${line.name}`} />
          ))}
        </Sum>
      </Quotient>
    </Calculation>
  );
}

Keyboard

The keys of Calculation apply here.

Props on this page

From CalculationProps — the full table stands on Calculation.

NameTypeDefaultDescription
childrenrequiredReactNode—The result: exactly one quantity - a tree (<Sum>, <Difference>, <Product>, <Quotient> with their operands, <Given> as leaves) or a <Chain>, the two mixing freely; <Ref> for a quantity defined elsewhere.

From OperatorProps — the full table stands on Tree.

NameTypeDefaultDescription
childrenReactNode—The operands, in order: quantities, givens and references.
labelrequiredstring—What the quantity is called. It stands on its line, wherever it is an operand, and in the accessible sentence. from QuantityProps
idstring—Makes the quantity referable: <Ref to> stands for it elsewhere. Needed only for a quantity used more than once. from QuantityProps
unitstring—A label written after the number - "min", "pcs", "€". Never converted and never checked; the calculation does no unit algebra. from QuantityProps
format"percent"—percent presents a plain ratio as per cent: 0.916 is shown as 91.6 %. The quantity stays a ratio in every operation. from QuantityProps
decimalsnumberone digit for a percentage, at most two for a derived number, a given as it was givenFixed number of fraction digits as shown. Rounding happens only in presentation; every operation uses the full value. from QuantityProps
targetnumber—The value this quantity is meant to reach - missed, never violated. from QuantityProps
limitsreadonly Limit[]—Bounds the quantity is assessed against, through core's assess(). from QuantityProps

From RefProps — the full table stands on Tree.

NameTypeDefaultDescription
torequiredstring—The id of the quantity this reference stands for.

From ChainProps — the full table stands on Chain.

NameTypeDefaultDescription
childrenReactNode—The operands, top to bottom: a first quantity with no operator, then Plus, Minus, Times and DividedBy with their operands and Interims naming the value where they stand. The last line is an Interim; a Times or DividedBy stands alone between two named values.

From ChainOperandProps — the full table stands on Chain.

NameTypeDefaultDescription
labelstring—The operand's name, where the line is a given written in place. Left out where the line holds a quantity as its child.
value| number
| null
| MetricValues
—The operand's number, where the line is a given written in place. Absent as for a Given: the line and every interim after it are absent, with the reason. With metrics, an object by metric id, as on a Given.
childrenReactNode—Instead of label and value: exactly one quantity - a tree, a <Ref> or a <Chain>.
sourcestring—Where the number came from: a system, a report, a person. from GivenProps
asOfDate | number—When the number was true - not when it was fetched. from GivenProps
agesFreshnessAges—The ages at which asOf turns stale and lost. With both, the given carries a freshness through core; with asOf alone, only the time. from GivenProps
idstring—Makes the quantity referable: <Ref to> stands for it elsewhere. Needed only for a quantity used more than once. from QuantityProps
unitstring—A label written after the number - "min", "pcs", "€". Never converted and never checked; the calculation does no unit algebra. from QuantityProps
format"percent"—percent presents a plain ratio as per cent: 0.916 is shown as 91.6 %. The quantity stays a ratio in every operation. from QuantityProps
decimalsnumberone digit for a percentage, at most two for a derived number, a given as it was givenFixed number of fraction digits as shown. Rounding happens only in presentation; every operation uses the full value. from QuantityProps
targetnumber—The value this quantity is meant to reach - missed, never violated. from QuantityProps
limitsreadonly Limit[]—Bounds the quantity is assessed against, through core's assess(). from QuantityProps

From QuantityProps — the full table stands on Chain.

NameTypeDefaultDescription
labelrequiredstring—What the quantity is called. It stands on its line, wherever it is an operand, and in the accessible sentence.
idstring—Makes the quantity referable: <Ref to> stands for it elsewhere. Needed only for a quantity used more than once.
unitstring—A label written after the number - "min", "pcs", "€". Never converted and never checked; the calculation does no unit algebra.
format"percent"—percent presents a plain ratio as per cent: 0.916 is shown as 91.6 %. The quantity stays a ratio in every operation.
decimalsnumberone digit for a percentage, at most two for a derived number, a given as it was givenFixed number of fraction digits as shown. Rounding happens only in presentation; every operation uses the full value.
targetnumber—The value this quantity is meant to reach - missed, never violated.
limitsreadonly Limit[]—Bounds the quantity is assessed against, through core's assess().

From GivenProps — the full table stands on Given.

NameTypeDefaultDescription
valuerequired| number
| null
| undefined
| MetricValues
—The number the calculation starts from. Absent (null, undefined, not finite) makes every quantity that depends on it absent, with the reason - never zero. Where the calculation has metrics, an object with a number for each metric id instead; absence then stays in its metric.
sourcestring—Where the number came from: a system, a report, a person.
asOfDate | number—When the number was true - not when it was fetched.
agesFreshnessAges—The ages at which asOf turns stale and lost. With both, the given carries a freshness through core; with asOf alone, only the time.
labelrequiredstring—What the quantity is called. It stands on its line, wherever it is an operand, and in the accessible sentence. from QuantityProps
idstring—Makes the quantity referable: <Ref to> stands for it elsewhere. Needed only for a quantity used more than once. from QuantityProps
unitstring—A label written after the number - "min", "pcs", "€". Never converted and never checked; the calculation does no unit algebra. from QuantityProps
format"percent"—percent presents a plain ratio as per cent: 0.916 is shown as 91.6 %. The quantity stays a ratio in every operation. from QuantityProps
decimalsnumberone digit for a percentage, at most two for a derived number, a given as it was givenFixed number of fraction digits as shown. Rounding happens only in presentation; every operation uses the full value. from QuantityProps
targetnumber—The value this quantity is meant to reach - missed, never violated. from QuantityProps
limitsreadonly Limit[]—Bounds the quantity is assessed against, through core's assess(). from QuantityProps

Every page of @umriss-ui/calculation

Getting started

Writing a calculation

In practice

API index