# @umriss-ui/calculation 0.40.0 > React calculation view for data-dense applications - a derivation written as it is shown, evaluated by the library, folded and read line by line. Install with `npm install @umriss-ui/calculation @umriss-ui/core`. This text is generated from the package's demo (https://romanhaendler.github.io/umriss-ui/calculation/): every page with its import line, its examples - the source exactly as it runs, with the package name where the demo imports its own source - its props tables generated from the code, and what it deliberately does not do. The pages index stands in https://romanhaendler.github.io/umriss-ui/calculation/llms.txt. ## Scenarios Composed, realistic screens built from the package. A numbered mark on the screen is an element with `data-callout`. ### Check a service's availability for the month A team lead at Quillmere checks, before the monthly review, whether a service keeps its availability promise and how much error budget is left. 1. The service is chosen here; every figure on the screen follows the choice. 2. The tiles give the answer at a glance, read against the service's target. 3. The calculation shows where the availability comes from: the month so far, minus the downtime, divided by the month. The verdict beside it is the same one the tile shows. 4. The error budget is the downtime the target allows; what is left can turn negative, and then it is marked. 5. The incidents of the week that touched the service stand beside the figures, so the downtime has names. Built from: Calculation, Tree, Given, Select, Stat, Card, Badge. ```tsx import { useState } from "react"; import { Badge, Card, CardBody, CardHeader, FormField, Grid, Select, Stack, Stat, Text } from "@umriss-ui/core"; import { Calculation, Difference, Given, Product, Quotient, Ref } from "@umriss-ui/calculation"; /* Data from the operations world, written out here so the example runs on its own. */ const at = (day: number, hours: number, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime(); const MINUTE = 60_000; /** Tuesday, 17 March 2026, 10:30 - the moment the screens are read at. */ const NOW = at(17, 10, 30); interface Service { id: string; name: string; team: string; /** 1 is customer-facing and pages at night; 3 waits for the morning. */ tier: 1 | 2 | 3; /** The latency objective: the 95th percentile stays below this, in ms. */ latencySlo: number; /** The availability promised for a month, in per cent. */ availabilityTarget: number; } const SERVICES: readonly Service[] = [ { id: "checkout", name: "Checkout", team: "Payments", tier: 1, latencySlo: 300, availabilityTarget: 99.95 }, { id: "billing", name: "Billing", team: "Payments", tier: 1, latencySlo: 400, availabilityTarget: 99.9 }, { id: "sign-in", name: "Sign-in", team: "Identity", tier: 1, latencySlo: 200, availabilityTarget: 99.95 }, { id: "search", name: "Search", team: "Discovery", tier: 1, latencySlo: 250, availabilityTarget: 99.9 }, { id: "images", name: "Image service", team: "Discovery", tier: 2, latencySlo: 500, availabilityTarget: 99.5 }, { id: "notifications", name: "Notifications", team: "Messaging", tier: 2, latencySlo: 800, availabilityTarget: 99.5 }, { id: "webhooks", name: "Webhooks", team: "Integrations", tier: 2, latencySlo: 1000, availabilityTarget: 99.5 }, { id: "reports", name: "Reporting", team: "Insights", tier: 3, latencySlo: 2000, availabilityTarget: 99 }, ]; interface Incident { id: string; title: string; service: string; severity: "SEV1" | "SEV2" | "SEV3"; opened: number; acknowledged?: number; resolved?: number; /** An engineer's id. */ assignee: string; } const INCIDENTS: readonly Incident[] = [ { id: "INC-1048", title: "Checkout slow, card payments time out", service: "checkout", severity: "SEV1", opened: at(17, 9, 42), acknowledged: at(17, 9, 46), assignee: "jonas" }, { id: "INC-1047", title: "Webhook deliveries delayed", service: "webhooks", severity: "SEV3", opened: at(17, 7, 15), acknowledged: at(17, 8, 2), assignee: "ines" }, { id: "INC-1046", title: "Thumbnails missing for new uploads", service: "images", severity: "SEV2", opened: at(16, 22, 5), acknowledged: at(16, 22, 11), resolved: at(17, 0, 40), assignee: "tomasz" }, { id: "INC-1045", title: "Sign-in codes arrive late", service: "sign-in", severity: "SEV2", opened: at(16, 14, 20), acknowledged: at(16, 14, 24), resolved: at(16, 15, 5), assignee: "ada" }, { id: "INC-1044", title: "Monthly report export fails", service: "reports", severity: "SEV3", opened: at(15, 10, 0), acknowledged: at(16, 9, 12), resolved: at(16, 11, 30), assignee: "felix" }, { id: "INC-1043", title: "Search returns no results for some regions", service: "search", severity: "SEV1", opened: at(14, 18, 30), acknowledged: at(14, 18, 33), resolved: at(14, 19, 55), assignee: "leila" }, { id: "INC-1042", title: "Duplicate reminder e-mails", service: "notifications", severity: "SEV3", opened: at(13, 8, 45), acknowledged: at(13, 9, 30), resolved: at(13, 13, 0), assignee: "sam" }, { id: "INC-1041", title: "Invoices generated twice", service: "billing", severity: "SEV2", opened: at(12, 16, 10), acknowledged: at(12, 16, 18), resolved: at(12, 18, 40), assignee: "priya" }, ]; /** The month so far: 1 March 00:00 to now, in minutes. */ const MONTH_MINUTES = (NOW - at(1, 0)) / MINUTE; /** Minutes each service was down this month. With `MONTH_MINUTES` and the service's target, what an availability is worked out from. */ const DOWNTIME_MINUTES: Readonly> = { checkout: 34, billing: 18, "sign-in": 41, search: 85, images: 120, notifications: 12, webhooks: 210, reports: 95, }; const SEVERITY_TONE = { SEV1: "danger", SEV2: "warning", SEV3: "neutral" } as const; export default function ServiceAvailability() { const [serviceId, setServiceId] = useState("checkout"); const service = SERVICES.find((one) => one.id === serviceId) ?? SERVICES[0]!; const downtime = DOWNTIME_MINUTES[service.id] ?? 0; /* Targets are stated in per cent; the calculation works in ratios. */ const target = service.availabilityTarget / 100; const availability = (MONTH_MINUTES - downtime) / MONTH_MINUTES; const allowed = MONTH_MINUTES * (1 - target); const incidents = INCIDENTS.filter((one) => one.service === service.id); return ( {service.team} · tier {service.tier} · promised {service.availabilityTarget} % · 1 March to now {incidents.length === 0 ? ( No incident touched {service.name} this week. ) : ( {incidents.map((one) => ( {one.severity} {one.id} {one.title} {one.resolved === undefined ? "still open" : "resolved"} ))} )} ); } ``` ### Price a tour A dispatcher at Ferrow Parcel prices a day's tour - energy, driver and vehicle - to see what each stop costs before quoting a customer. 1. The tour is chosen here; vehicle, driver and depot come with it. 2. The hours run from the start of loading to the return to the depot - the figure the driver's cost starts from. 3. Late stops are counted beside the stops: a cheap tour that misses its windows is no bargain. 4. The cost per stop, worked out in full: the tour's cost divided by its stops. Energy, driver and vehicle each fold open to the numbers they come from, and each number names its source. 5. The rates the calculation uses, as the fleet office keeps them; a changed price is found here and in the line where it enters. Built from: Calculation, Tree, Given, Select, Stat, Card, Badge. ```tsx import { useState } from "react"; import { Badge, Card, CardBody, CardHeader, FormField, Grid, Select, Stack, Stat, Text } from "@umriss-ui/core"; import { Calculation, Given, Product, Quotient, 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; const DEPOTS = [ { id: "north", name: "North depot" }, { id: "river", name: "Riverside depot" }, { id: "east", name: "East Gate depot" }, ] as const; 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 TYPE_NAME = { van: "Van", "e-van": "Electric van", truck: "Truck" } as const; const time = (at: number) => new Date(at).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" }); export default function PriceATour() { const [tourId, setTourId] = useState(TOURS[0]!.id); const tour = TOURS.find((one) => one.id === tourId) ?? TOURS[0]!; const vehicle = VEHICLES.find((one) => one.id === tour.vehicle)!; const driver = DRIVERS.find((one) => one.id === tour.driver)!; const depot = DEPOTS.find((one) => one.id === tour.depot)!; const electric = vehicle.type === "e-van"; const hours = Math.round(((tour.to - tour.from) / 3_600_000 + tour.loading / 60) * 100) / 100; const late = tour.stops.filter((stop) => stop.arrival > stop.window[1]).length; return ( {TYPE_NAME[vehicle.type]} {vehicle.plate} · {driver.name} · {depot.name} · {time(tour.from)} to {time(tour.to)} {late > 0 && ( {late} {late === 1 ? "stop" : "stops"} outside the window )} Diesel {COST_RATES.dieselPerLitre.toFixed(2)} €/l · electricity {COST_RATES.electricityPerKwh.toFixed(2)} €/kWh Per 100 km: van {COST_RATES.per100Km.van} l · truck {COST_RATES.per100Km.truck} l · electric van{" "} {COST_RATES.per100Km["e-van"]} kWh Driver's hour, all in: {COST_RATES.driverPerHour.toFixed(2)} € Vehicle per day: van {COST_RATES.vehiclePerDay.van} € · truck {COST_RATES.vehiclePerDay.truck} € · electric van{" "} {COST_RATES.vehiclePerDay["e-van"]} € ); } ``` ### Check an invoice before approving it A cost-centre owner or the finance team at Carrow & Lisle checks what an invoice adds up to - discounts, VAT per rate - before signing it off. 1. The invoices waiting for a signature; the header names the supplier, the cost centre and the due date. 2. Every line is worked out from quantity, unit price and discount; a discounted line folds open to the share that is paid. 3. VAT is charged per rate, on the net of the lines that carry it - never on the gross amount. 4. Who has signed already and who is still to sign. 5. The decision is taken beside the working, not on a total read somewhere else. Built from: Calculation, Tree, Given, Select, Card, Badge, Button, Alert. ```tsx import { useState } from "react"; import { Alert, Badge, Button, Card, CardBody, CardHeader, FormField, Grid, Select, Stack, Text } from "@umriss-ui/core"; import { Calculation, Difference, Given, 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 CostCentre { id: string; name: string; owner: string; /** The budget of one month. */ monthlyBudget: number; } const COST_CENTRES: readonly CostCentre[] = [ { id: "CC-1100", name: "Sales", owner: "Helen Marsh", monthlyBudget: 142_000 }, { id: "CC-1200", name: "Marketing", owner: "Rafael Ortiz", monthlyBudget: 68_000 }, { id: "CC-2100", name: "Engineering", owner: "Anika Sørensen", monthlyBudget: 188_000 }, { id: "CC-2200", name: "Design", owner: "Paul Whitaker", monthlyBudget: 54_000 }, { id: "CC-3100", name: "Customer service", owner: "Grace Obi", monthlyBudget: 61_000 }, { id: "CC-4100", name: "Finance", owner: "Martina Vogel", monthlyBudget: 47_000 }, { id: "CC-4200", name: "People", owner: "Daniel Frost", monthlyBudget: 39_000 }, { id: "CC-4300", name: "IT", owner: "Kenji Arai", monthlyBudget: 83_000 }, { id: "CC-4400", name: "Facilities", owner: "Olga Ivanova", monthlyBudget: 72_000 }, ]; 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 }], }, ]; interface Approval { invoice: string; step: "cost centre" | "finance"; approver: string; decision: "approved" | "rejected" | "pending"; at?: number; comment?: string; } /** Two signatures an invoice needs: its cost centre's owner, then finance. */ const APPROVALS: readonly Approval[] = [ { invoice: "INV-26-0318", step: "cost centre", approver: "Olga Ivanova", decision: "pending" }, { invoice: "INV-26-0317", step: "cost centre", approver: "Rafael Ortiz", decision: "approved", at: on(3, 16) }, { invoice: "INV-26-0317", step: "finance", approver: "Martina Vogel", decision: "pending" }, { invoice: "INV-26-0309", step: "cost centre", approver: "Kenji Arai", decision: "approved", at: on(3, 10) }, { invoice: "INV-26-0309", step: "finance", approver: "Martina Vogel", decision: "approved", at: on(3, 11) }, { invoice: "INV-26-0302", step: "cost centre", approver: "Martina Vogel", decision: "approved", at: on(3, 3) }, { invoice: "INV-26-0302", step: "finance", approver: "Martina Vogel", decision: "approved", at: on(3, 3) }, { invoice: "INV-26-0226", step: "cost centre", approver: "Helen Marsh", decision: "approved", at: on(2, 27) }, { invoice: "INV-26-0226", step: "finance", approver: "Martina Vogel", decision: "approved", at: on(3, 2) }, { invoice: "INV-26-0221", step: "cost centre", approver: "Anika Sørensen", decision: "rejected", at: on(2, 24), comment: "Not ordered - the review was cancelled in January." }, ]; const WAITING = INVOICES.filter((one) => one.status === "awaiting approval"); const DECISION_TONE = { approved: "success", rejected: "danger", pending: "neutral" } as const; const date = (at: number) => new Date(at).toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" }); const percent = (rate: number) => `${Math.round(rate * 100)}\u00a0%`; /** One line's net amount: quantity × unit price, times the share paid after a discount. A function and not a component: the calculation reads its elements by their type, so a wrapper of one's own would not be recognised. */ function lineNet(line: InvoiceLine, id: string) { return ( {line.discount > 0 && ( )} ); } export default function CheckAnInvoice() { const [invoiceId, setInvoiceId] = useState(WAITING[0]!.id); const [decided, setDecided] = useState>({}); const invoice = WAITING.find((one) => one.id === invoiceId) ?? WAITING[0]!; const centre = COST_CENTRES.find((one) => one.id === invoice.costCentre)!; const approvals = APPROVALS.filter((one) => one.invoice === invoice.id); const next = approvals.find((one) => one.decision === "pending"); const decision = decided[invoice.id]; const lineIds = invoice.lines.map((_, i) => `line-${i}`); const rates = [...new Set(invoice.lines.map((line) => line.vatRate))].filter((rate) => rate > 0); /* The net a rate is charged on: the whole net where every line carries it, one line on its own, or the sum of the lines that do. */ const netAt = (rate: number) => { const ids = lineIds.filter((_, i) => invoice.lines[i]!.vatRate === rate); if (ids.length === lineIds.length) return ; if (ids.length === 1) return ; return ( {ids.map((id) => ( ))} ); }; const vatAt = (rate: number) => ( {netAt(rate)} ); return ( {invoice.supplier} · {centre.id} {centre.name} · received {date(invoice.received)} · due {date(invoice.due)} Each line: quantity × unit price × the share paid after its discount. {invoice.lines.length === 1 ? ( lineNet(invoice.lines[0]!, "net") ) : ( {invoice.lines.map((line, i) => lineNet(line, lineIds[i]!))} )} {rates.length === 1 ? ( vatAt(rates[0]!) ) : ( {rates.map(vatAt)} )} VAT {rates.map(percent).join(" and ")}, each on the lines that carry it. {approvals.map((one) => ( {one.decision} {one.step === "finance" ? "Finance" : "Cost centre"} · {one.approver} {one.at !== undefined && ` · ${date(one.at)}`} ))} {decision === undefined ? ( ) : ( {decision === "approved" ? "It moves on to the next signature." : "The supplier is told why."} )} ); } ``` ### Explain the shift's OEE A shift lead at Brenholt Tile Works explains at the 10:30 handover why the kiln line's OEE stands where it stands. 1. The tiles give the three counts the OEE is made of, as the line reports them. 2. The stop that cost availability, taken from the same readings - so the downtime in the calculation has a cause. 3. The OEE worked out as availability × performance × quality, each folded with its formula under its name, and read against the 85 % target. 4. Planned time, run time and total count are each used twice; they are defined once and referred to elsewhere. Built from: Calculation, Tree, Given, Stat, Card, Alert. ```tsx import { Alert, Card, CardBody, CardHeader, Grid, Stack, Stat, Text } from "@umriss-ui/core"; import { Calculation, Difference, Given, Product, Quotient, Ref } from "@umriss-ui/calculation"; /* Data from the plant world, written out here so the example runs on its own. */ /** A small LCG - reproducible across runs and platforms. */ function random(seed: number): () => number { let s = seed >>> 0; return () => { s = (Math.imul(s, 1664525) + 1013904223) >>> 0; return s / 4294967296; }; } /** The early shift, 06:00 to 14:00. */ const SHIFT_MINUTES = 480; /** 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 kiln fires one tile every quarter of a minute when nothing holds it up - the ideal cycle time of the OEE's performance. */ const IDEAL_CYCLE_MINUTES = 0.25; /** One minute of the line. */ interface Reading { /** Minutes since the start of the shift. */ minute: number; /** Zone 3 of the kiln, in °C. */ kiln: number; /** The exit pyrometer, in °C - `null` while it sends nothing. */ exit: number | null; /** Whether the belt runs. */ running: boolean; /** Tiles out of the kiln in this minute. */ fired: number; /** Of them, the ones fired inside the tolerance. */ good: number; } /** A tile taken off the belt and measured - one every ten minutes while it runs. */ interface Sample { minute: number; /** The tile's length after firing, in mm (nominal 600). */ length: number; } /** A batch of tiles through the three stations. */ interface Batch { id: string; name: string; /** How many tiles the batch is planned for. */ tiles: number; /** Minutes since the start of the shift, per station. */ press: readonly [number, number]; dryer: readonly [number, number]; kiln: readonly [number, number]; } interface Plant { readings: readonly Reading[]; samples: readonly Sample[]; batches: readonly Batch[]; } /* When the exit pyrometer goes silent, and for how long. Fixed rather than seeded, so that a running control room shows its reading turn stale, then lost, and come back. */ const SILENT_FROM = 288; const SILENT_FOR = 50; /** The shift, from a seed. */ function plant(seed: number): Plant { const r = random(seed); /* The one excursion: a burner overshoots somewhere between 02:30 and 03:30 into the shift, for half an hour, peaking near 1242 °C. The operator stops the feed twenty-two minutes in, for eighteen minutes. The noise around it is too small to reach a warning on its own, so this is the only crossing - which is what the tests hold. */ const surgeFrom = 150 + Math.floor(r() * 60); const surgeFor = 30; const stopFrom = surgeFrom + 22; const stopFor = 18; /* The plan: batches through the kiln one after another, each pressed an hour and dried half an hour before - the first ones during the night shift. Tiles come out of the kiln only while a batch is in it, so the OEE's count and the batches' counts are the same tiles. The plan is a plan: a stop of the belt does not move it. */ const batches: Batch[] = []; for (let at = 0, i = 0; at < SHIFT_MINUTES; i++) { const length = 70 + Math.floor(r() * 25); batches.push({ id: `b-${4121 + i}`, name: `B-${4121 + i}`, tiles: Math.round(length / IDEAL_CYCLE_MINUTES / 10) * 10, press: [at - 60, at - 60 + length], dryer: [at - 30, at - 30 + length], kiln: [at, at + length], }); at += length + 5; } const readings: Reading[] = []; const samples: Sample[] = []; let drift = 0; for (let minute = 0; minute < SHIFT_MINUTES; minute++) { drift = 0.9 * drift + (r() - 0.5) * 3; const into = minute - surgeFrom; const surge = into >= 0 && into <= surgeFor ? 42 * Math.sin((Math.PI * into) / surgeFor) : 0; const kiln = Math.round((KILN.target + drift + surge) * 10) / 10; const running = !(minute >= stopFrom && minute < stopFrom + stopFor); const loaded = batches.some((batch) => batch.kiln[0] <= minute && minute < batch.kiln[1]); const jam = r() < 0.12; const fired = running && loaded ? (jam ? 3 : 4) : 0; const inTolerance = kiln >= KILN.tolerance[0] && kiln <= KILN.tolerance[1]; const silent = minute >= SILENT_FROM && minute < SILENT_FROM + SILENT_FOR; const exit = silent ? null : Math.round((kiln - 380 + (r() - 0.5) * 4) * 10) / 10; readings.push({ minute, kiln, exit, running, fired, good: inTolerance ? fired : 0 }); if (running && minute % 10 === 0) { /* Hotter shrinks more: the length follows the kiln, so the control chart sees the excursion the trend sees. */ const length = 600 + (KILN.target - kiln) * 0.05 + (r() - 0.5) * 0.6; samples.push({ minute, length: Math.round(length * 100) / 100 }); } } return { readings, samples, batches }; } /** The readings up to and including the minute. */ function upTo(series: readonly T[], minute: number): readonly T[] { return series.filter((one) => one.minute <= minute); } /** The shift's figures for the OEE, up to and including the minute. */ interface ShiftCount { /** Minutes of the shift so far. */ planned: number; /** Minutes the belt stood. */ downtime: number; /** Tiles out of the kiln. */ total: number; /** Of them, inside the tolerance. */ good: number; } function countAt(p: Plant, minute: number): ShiftCount { const so = upTo(p.readings, minute); return { planned: so.length, downtime: so.filter((one) => !one.running).length, total: so.reduce((sum, one) => sum + one.fired, 0), good: so.reduce((sum, one) => sum + one.good, 0), }; } /* The early shift starts at 06:00; 10:30 is minute 270 into it, so the minutes 0 to 269 have passed. */ const SHIFT = plant(17); const MINUTE = 270; const count = countAt(SHIFT, MINUTE - 1); const stopped = SHIFT.readings.filter((one) => one.minute < MINUTE && !one.running).map((one) => one.minute); const clock = (minute: number) => `${String(6 + Math.floor(minute / 60)).padStart(2, "0")}:${String(minute % 60).padStart(2, "0")}`; const OEE_TARGET = 0.85; const OEE_LIMITS = [ { value: 0.75, side: "lower", severity: "warning" }, { value: 0.65, side: "lower", severity: "alarm" }, ] as const; export default function ExplainTheOee() { return ( {stopped.length > 0 && ( The kiln's zone 3 went above {KILN.alarm} °C; tiles fired outside {KILN.tolerance[0]} to {KILN.tolerance[1]} °C count as scrap. )}
Hover a line: its operands and every place it is used light up.
); } ``` ## Getting started What a calculation is for, and what an application sets up before the first one. ### Installation Show a figure together with how it came about (a derivation, a statement of account): the operator before each number, the result above a double rule, and every figure opening into the numbers it came from. ```ts import { Calculation, Given, Difference, Quotient, Ref } from "@umriss-ui/calculation"; ``` ```sh npm install @umriss-ui/calculation @umriss-ui/core ``` Demo page: https://romanhaendler.github.io/umriss-ui/calculation/installation/ The command brings the core package along, a peer dependency: it gives the calculation its formats, its wording and the assessment of targets and limits. React 19 or newer is a peer as well and stays the application's own. The stylesheet loads itself: the package imports it and marks CSS as a side effect, so a bundler keeps it. `@umriss-ui/calculation/styles.css` stays exported for setups that link stylesheets by hand. You write the figure as elements and hand in only the givens; the calculation performs every operation it shows, so the screen cannot disagree with the number. Start with [Calculation](#/calculation). #### Examples ##### The availability of a service Write the figure as elements; the calculation computes every number on the screen and assesses the result against its `target`. ```tsx import { Calculation, Difference, Given, Quotient, Ref } from "@umriss-ui/calculation"; export default function Availability() { return ( ); } ``` #### Keyboard The keys of [Calculation](#/calculation/keyboard-calculation) apply here. #### Props on this page From `CalculationProps` — the full table stands on [Calculation](#/calculation/type-CalculationProps). | Prop | Type | Default | Description | |---|---|---|---| | [`children`](#/calculation/CalculationProps-children) *required* | `ReactNode` | — | The result: exactly one quantity - a tree (``, ``, ``, `` with their operands, `` as leaves) or a ``, the two mixing freely; `` for a quantity defined elsewhere. | From `OperatorProps` — the full table stands on [Tree](#/tree/type-OperatorProps). | Prop | Type | Default | Description | |---|---|---|---| | [`children`](#/tree/OperatorProps-children) | `ReactNode` | — | The operands, in order: quantities, givens and references. | | [`label`](#/tree/OperatorProps-label) *required* | `string` | — | What the quantity is called. It stands on its line, wherever it is an operand, and in the accessible sentence. From `QuantityProps`. | | [`id`](#/tree/OperatorProps-id) | `string` | — | Makes the quantity referable: `` stands for it elsewhere. Needed only for a quantity used more than once. From `QuantityProps`. | | [`unit`](#/tree/OperatorProps-unit) | `string` | — | A label written after the number - "min", "pcs", "€". Never converted and never checked; the calculation does no unit algebra. From `QuantityProps`. | | [`format`](#/tree/OperatorProps-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`. | | [`decimals`](#/tree/OperatorProps-decimals) | `number` | one digit for a percentage, at most two for a derived number, a given as it was given | Fixed number of fraction digits as shown. Rounding happens only in presentation; every operation uses the full value. From `QuantityProps`. | | [`target`](#/tree/OperatorProps-target) | `number` | — | The value this quantity is meant to reach - missed, never violated. From `QuantityProps`. | From `RefProps` — the full table stands on [Tree](#/tree/type-RefProps). | Prop | Type | Default | Description | |---|---|---|---| | [`to`](#/tree/RefProps-to) *required* | `string` | — | The `id` of the quantity this reference stands for. | From `ChainOperandProps` — the full table stands on [Chain](#/chain/type-ChainOperandProps). | Prop | Type | Default | Description | |---|---|---|---| | [`id`](#/chain/ChainOperandProps-id) | `string` | — | Makes the quantity referable: `` stands for it elsewhere. Needed only for a quantity used more than once. From `QuantityProps`. | | [`unit`](#/chain/ChainOperandProps-unit) | `string` | — | A label written after the number - "min", "pcs", "€". Never converted and never checked; the calculation does no unit algebra. From `QuantityProps`. | | [`format`](#/chain/ChainOperandProps-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`. | | [`decimals`](#/chain/ChainOperandProps-decimals) | `number` | one digit for a percentage, at most two for a derived number, a given as it was given | Fixed number of fraction digits as shown. Rounding happens only in presentation; every operation uses the full value. From `QuantityProps`. | | [`target`](#/chain/ChainOperandProps-target) | `number` | — | The value this quantity is meant to reach - missed, never violated. From `QuantityProps`. | From `QuantityProps` — the full table stands on [Chain](#/chain/type-QuantityProps). | Prop | Type | Default | Description | |---|---|---|---| | [`label`](#/chain/QuantityProps-label) *required* | `string` | — | What the quantity is called. It stands on its line, wherever it is an operand, and in the accessible sentence. | | [`id`](#/chain/QuantityProps-id) | `string` | — | Makes the quantity referable: `` stands for it elsewhere. Needed only for a quantity used more than once. | | [`unit`](#/chain/QuantityProps-unit) | `string` | — | A label written after the number - "min", "pcs", "€". Never converted and never checked; the calculation does no unit algebra. | | [`format`](#/chain/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. | | [`decimals`](#/chain/QuantityProps-decimals) | `number` | one digit for a percentage, at most two for a derived number, a given as it was given | Fixed number of fraction digits as shown. Rounding happens only in presentation; every operation uses the full value. | | [`target`](#/chain/QuantityProps-target) | `number` | — | The value this quantity is meant to reach - missed, never violated. | From `GivenProps` — the full table stands on [Given](#/given/type-GivenProps). | Prop | Type | Default | Description | |---|---|---|---| | [`value`](#/given/GivenProps-value) *required* | `number \| null \| undefined \| `[`MetricValues`](#/given/type-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. | | [`label`](#/given/GivenProps-label) *required* | `string` | — | What the quantity is called. It stands on its line, wherever it is an operand, and in the accessible sentence. From `QuantityProps`. | | [`id`](#/given/GivenProps-id) | `string` | — | Makes the quantity referable: `` stands for it elsewhere. Needed only for a quantity used more than once. From `QuantityProps`. | | [`unit`](#/given/GivenProps-unit) | `string` | — | A label written after the number - "min", "pcs", "€". Never converted and never checked; the calculation does no unit algebra. From `QuantityProps`. | | [`format`](#/given/GivenProps-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`. | | [`decimals`](#/given/GivenProps-decimals) | `number` | one digit for a percentage, at most two for a derived number, a given as it was given | Fixed number of fraction digits as shown. Rounding happens only in presentation; every operation uses the full value. From `QuantityProps`. | | [`target`](#/given/GivenProps-target) | `number` | — | The value this quantity is meant to reach - missed, never violated. From `QuantityProps`. | ## Writing a calculation Two forms written as elements: a tree for a figure put together from factors, a chain for a sheet read top to bottom. ### Calculation Lets a reader follow and redo a figure (also called a KPI tree or calculation tree): each line a quantity with its operator, derived lines folded until opened beneath themselves. Reach for it where a number on the screen gets questioned: a cost, an availability, an invoice total. ```ts import { Calculation, Given, Sum, Product, Quotient, Ref, Chain, Plus, Interim } from "@umriss-ui/calculation"; ``` Demo page: https://romanhaendler.github.io/umriss-ui/calculation/calculation/ There is no prop for a result. Every derived number is computed with the operator its line names, at full precision; only the screen rounds. The nesting is the fold structure and the child order is the operand order. What TypeScript cannot check – a `Ref` to nothing, a circle through references, a wrong operand count, a duplicate id, a component of your own wrapping `Given`, `emphasis` or `rule` on the Result – fails on the first render with a message saying which ([ADR-0027](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0027-a-calculation-is-written-as-it-is-shown.md)). `emphasis` and `rule` set any other line apart without CSS ([ADR-0049](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0049-a-sum-takes-rows-from-data.md)). Every line is read as one sentence by a screen reader, and every derivation is a disclosure: its button says whether it shows or hides how the line is derived. #### Examples ##### Two numbers and a sum Two givens and the operator that joins them; the result stands beneath its operands, above a double rule. ```tsx import { Calculation, Given, Sum } from "@umriss-ui/calculation"; export default function FirstSum() { return ( ); } ``` ##### Write a tree Nest operators for a figure made of factors: each folds with its formula under its name, and `Ref` stands for a quantity used twice. ```tsx import { Calculation, Difference, Given, Product, Quotient, Ref } from "@umriss-ui/calculation"; /* Data from the plant world, written out here so the example runs on its own. */ /** A small LCG - reproducible across runs and platforms. */ function random(seed: number): () => number { let s = seed >>> 0; return () => { s = (Math.imul(s, 1664525) + 1013904223) >>> 0; return s / 4294967296; }; } /** The early shift, 06:00 to 14:00. */ const SHIFT_MINUTES = 480; /** 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 kiln fires one tile every quarter of a minute when nothing holds it up - the ideal cycle time of the OEE's performance. */ const IDEAL_CYCLE_MINUTES = 0.25; /** One minute of the line. */ interface Reading { /** Minutes since the start of the shift. */ minute: number; /** Zone 3 of the kiln, in °C. */ kiln: number; /** The exit pyrometer, in °C - `null` while it sends nothing. */ exit: number | null; /** Whether the belt runs. */ running: boolean; /** Tiles out of the kiln in this minute. */ fired: number; /** Of them, the ones fired inside the tolerance. */ good: number; } /** A tile taken off the belt and measured - one every ten minutes while it runs. */ interface Sample { minute: number; /** The tile's length after firing, in mm (nominal 600). */ length: number; } /** A batch of tiles through the three stations. */ interface Batch { id: string; name: string; /** How many tiles the batch is planned for. */ tiles: number; /** Minutes since the start of the shift, per station. */ press: readonly [number, number]; dryer: readonly [number, number]; kiln: readonly [number, number]; } interface Plant { readings: readonly Reading[]; samples: readonly Sample[]; batches: readonly Batch[]; } /* When the exit pyrometer goes silent, and for how long. Fixed rather than seeded, so that a running control room shows its reading turn stale, then lost, and come back. */ const SILENT_FROM = 288; const SILENT_FOR = 50; /** The shift, from a seed. */ function plant(seed: number): Plant { const r = random(seed); /* The one excursion: a burner overshoots somewhere between 02:30 and 03:30 into the shift, for half an hour, peaking near 1242 °C. The operator stops the feed twenty-two minutes in, for eighteen minutes. The noise around it is too small to reach a warning on its own, so this is the only crossing - which is what the tests hold. */ const surgeFrom = 150 + Math.floor(r() * 60); const surgeFor = 30; const stopFrom = surgeFrom + 22; const stopFor = 18; /* The plan: batches through the kiln one after another, each pressed an hour and dried half an hour before - the first ones during the night shift. Tiles come out of the kiln only while a batch is in it, so the OEE's count and the batches' counts are the same tiles. The plan is a plan: a stop of the belt does not move it. */ const batches: Batch[] = []; for (let at = 0, i = 0; at < SHIFT_MINUTES; i++) { const length = 70 + Math.floor(r() * 25); batches.push({ id: `b-${4121 + i}`, name: `B-${4121 + i}`, tiles: Math.round(length / IDEAL_CYCLE_MINUTES / 10) * 10, press: [at - 60, at - 60 + length], dryer: [at - 30, at - 30 + length], kiln: [at, at + length], }); at += length + 5; } const readings: Reading[] = []; const samples: Sample[] = []; let drift = 0; for (let minute = 0; minute < SHIFT_MINUTES; minute++) { drift = 0.9 * drift + (r() - 0.5) * 3; const into = minute - surgeFrom; const surge = into >= 0 && into <= surgeFor ? 42 * Math.sin((Math.PI * into) / surgeFor) : 0; const kiln = Math.round((KILN.target + drift + surge) * 10) / 10; const running = !(minute >= stopFrom && minute < stopFrom + stopFor); const loaded = batches.some((batch) => batch.kiln[0] <= minute && minute < batch.kiln[1]); const jam = r() < 0.12; const fired = running && loaded ? (jam ? 3 : 4) : 0; const inTolerance = kiln >= KILN.tolerance[0] && kiln <= KILN.tolerance[1]; const silent = minute >= SILENT_FROM && minute < SILENT_FROM + SILENT_FOR; const exit = silent ? null : Math.round((kiln - 380 + (r() - 0.5) * 4) * 10) / 10; readings.push({ minute, kiln, exit, running, fired, good: inTolerance ? fired : 0 }); if (running && minute % 10 === 0) { /* Hotter shrinks more: the length follows the kiln, so the control chart sees the excursion the trend sees. */ const length = 600 + (KILN.target - kiln) * 0.05 + (r() - 0.5) * 0.6; samples.push({ minute, length: Math.round(length * 100) / 100 }); } } return { readings, samples, batches }; } /** The readings up to and including the minute. */ function upTo(series: readonly T[], minute: number): readonly T[] { return series.filter((one) => one.minute <= minute); } /** The shift's figures for the OEE, up to and including the minute. */ interface ShiftCount { /** Minutes of the shift so far. */ planned: number; /** Minutes the belt stood. */ downtime: number; /** Tiles out of the kiln. */ total: number; /** Of them, inside the tolerance. */ good: number; } function countAt(p: Plant, minute: number): ShiftCount { const so = upTo(p.readings, minute); return { planned: so.length, downtime: so.filter((one) => !one.running).length, total: so.reduce((sum, one) => sum + one.fired, 0), good: so.reduce((sum, one) => sum + one.good, 0), }; } const COUNT = countAt(plant(17), SHIFT_MINUTES); export default function Oee() { return ( ); } ``` ##### Write a chain Put lines in a `Chain` for a sheet read top to bottom; each works into the value before it, and an `Interim` names that value. ```tsx import { Calculation, Chain, Given, Interim, Minus, Plus, Product, Ref } from "@umriss-ui/calculation"; export default function Invoice() { return ( ); } ``` ##### A stronger line `emphasis="strong"` sets a line apart to be read first – here the production cost, the figure a costing sheet is usually questioned on. It changes how the line looks, never what it says: its sentence for a screen reader stays the same. Any quantity takes it but the Result, which is already the heaviest line; there it fails on the first render. ```tsx import { Calculation, Chain, Given, Interim, Minus, Plus, Times } from "@umriss-ui/calculation"; export default function AStrongerLine() { return ( ); } ``` ##### A quieter line `emphasis="muted"` lets a line recede – a small position the reader may skip. It still counts in full: the interim below includes it, and a reader who redoes the sum needs it. ```tsx import { Calculation, Chain, Given, Interim, Plus } from "@umriss-ui/calculation"; export default function AQuieterLine() { return ( ); } ``` ##### A rule above `rule="above"` rules a line off from the ones before it, where a new section starts that needs no interim of its own: here the deductions after the earnings. An interim draws its rule across the figures only; this one runs across the whole row. ```tsx import { Calculation, Chain, Given, Interim, Minus, Plus } from "@umriss-ui/calculation"; export default function ARuleAbove() { return ( ); } ``` ##### Emphasis inside a group Emphasis and rule work inside an opened derivation too. Its numbers already recede, so a muted line there recedes one step further; a strong one stands out from the group, and a rule parts the group where its rows do not. ```tsx import { Calculation, Chain, Given, Interim, Plus, Sum } from "@umriss-ui/calculation"; /* Cost centres from the controlling system. */ const CENTRES = [ { id: "4100", name: "4100 Assembly", amount: 18400, emphasis: "strong" as const }, { id: "4200", name: "4200 Paint shop", amount: 9200 }, { id: "4300", name: "4300 Quality", amount: 6100 }, { id: "9900", name: "9900 Allocations", amount: 740, emphasis: "muted" as const, rule: "above" as const }, ]; export default function EmphasisInsideAGroup() { return ( {CENTRES.map((centre) => ( ))} ); } ``` ##### Density `density="compact"` sets one calculation as close as a compact table; an application that wants it dense everywhere says so once at the `UmrissProvider`, and a calculation's own `density` wins over it. ```tsx import { Stack, Text, UmrissProvider } from "@umriss-ui/core"; import { Calculation, Given, Sum } from "@umriss-ui/calculation"; function Downtime({ caption, density }: { caption: string; density?: "regular" | "compact" }) { return ( {caption} ); } export default function Density() { return ( ); } ``` #### When to use something else - A single figure with its verdict, without the working → `Stat` from @umriss-ui/core - Many figures side by side, summed per group → `Table` with grouping from @umriss-ui/table #### Keyboard | Key | Action | |---|---| | `Tab` | Moves to the next line that can be opened. | | `Enter or Space` | Opens or closes the derivation beneath the focused line. | #### Accessibility The calculation is a nested list: every line an item, every derivation a list inside it. A screen reader reads each line as one sentence - "Availability equals Run time divided by Planned production time, equals 412 min divided by 450 min, equals 91.6 percent, above target 90 percent" - and the drawn operator, label and figures are hidden from it. A line that opens is a button named "Show how Availability is derived", or "Hide …" while open, with `aria-expanded`; opening it moves no focus and announces nothing beyond that state. The sentence's words are core's wording, so under `@umriss-ui/core/wording/de` the same line reads "Verfügbarkeit gleich Laufzeit geteilt durch Planbelegungszeit, gleich 412 min geteilt durch 450 min, gleich 91,6 Prozent, über Ziel 90 Prozent" and its button "Herleitung von Verfügbarkeit zeigen" ([Given](#/given/in-german) shows one). The labels are yours and are read as you write them. Name the calculation with `aria-label`, which lands on the list; without it a reader meets an unnamed list. Under forced colours the band that couples a line with its operands is drawn as an outline in the system's selection colour; with reduced motion a derivation opens without its movement. #### API ##### `CalculationProps` | Prop | Type | Default | Description | |---|---|---|---| | `children` *required* | `ReactNode` | — | The result: exactly one quantity - a tree (``, ``, ``, `` with their operands, `` as leaves) or a ``, the two mixing freely; `` for a quantity defined elsewhere.
Shown in: [Two numbers and a sum](#/calculation/first-sum), [Write a tree](#/calculation/oee), [Write a chain](#/calculation/invoice) and 50 more | | `metrics` | `readonly `[`Metric`](#/metrics/type-Metric)`[]` | — | Several numbers per quantity, side by side - headcount and full-time equivalents of the same teams ([ADR-0038](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0038-a-calculation-can-carry-several-metrics.md)). Every `value` is then an object with a number for each metric id; unit and places are the metric's. A calculation with metrics only adds and subtracts, and assesses no target or limits.
Shown in: [Rows of both signs with metrics (Rows from data)](#/rows-from-data/rows-of-both-signs-with-metrics), [Headcount and full-time equivalents (Metrics)](#/metrics/two-teams), [A number one metric does not have (Metrics)](#/metrics/not-recorded) and 5 more | | `density` | `"regular" \| "compact"` | the density of the `UmrissProvider`, else `"regular"` | `"compact"` sets the lines as close as a compact table's rows: smaller type and less room above and below, under a finger still tall enough to tap. Columns, folding and wording stay as they are.
Shown in: [Density](#/calculation/density), [A dense staff report (Metrics)](#/metrics/compact-report) | Also takes every attribute of the rendered element – without `children`. #### Known limits - No result, formula text or operator of your own: a written-out operation the library did not perform could say something the number does not ([ADR-0027](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0027-a-calculation-is-written-as-it-is-shown.md)). - No input: a calculation shows how a figure came about; changing a given is your application's form. What umriss deliberately does not build, and why: [ADR-0032](https://romanhaendler.github.io/umriss-ui/what-umriss-ui-is-not/). ### Tree Sum, difference, product and quotient, each holding its operands: the form for a figure put together from factors, folded to its formula until a reader opens it. ```ts import { Sum, Difference, Product, Quotient, Ref } from "@umriss-ui/calculation"; ``` Demo page: https://romanhaendler.github.io/umriss-ui/calculation/tree/ `Product` and `Difference` (a − b − c) take two or more operands, `Quotient` exactly two, and `Sum` any number – rows from data, one or none included (see [Rows from data](#/rows-from-data)). A quantity used twice is defined once with an `id` and stands elsewhere as a `Ref`, which shows its name and number but never its derivation again. #### Examples ##### Show a share against a target `format="percent"` shows a ratio as per cent while it stays a ratio in every operation; a `target` is missed, never violated. ```tsx import { Calculation, Given, Quotient } from "@umriss-ui/calculation"; export default function ShareWithTarget() { return ( ); } ``` ##### Take several amounts away `Difference` takes every operand after the first away from it, each with its minus in the operator column. ```tsx import { Calculation, Difference, Given } from "@umriss-ui/calculation"; export default function DifferenceOfThree() { return ( ); } ``` ##### Multiply quantities with units A `unit` is a label you write, never converted or checked; `decimals` fixes the places shown while every operation uses the full value. ```tsx import { Calculation, Given, Product } from "@umriss-ui/calculation"; export default function ProductWithUnits() { return ( ); } ``` ##### Use a quantity twice Give a quantity an `id` where it first belongs and write `Ref` wherever it is used again; hover either place and both light up. ```tsx import { Calculation, Difference, Given, Quotient, Ref } from "@umriss-ui/calculation"; export default function UsedTwice() { return ( ); } ``` ##### Build operands from data `.map` inside an operator works; folded, a line with more than four operands says how many it holds instead of a long formula. ```tsx import { Calculation, Difference, Given, Product, Quotient } from "@umriss-ui/calculation"; /* Data from the operations world, written out here so the example runs on its own. */ const at = (day: number, hours: number, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime(); const MINUTE = 60_000; /** Tuesday, 17 March 2026, 10:30 - the moment the screens are read at. */ const NOW = at(17, 10, 30); interface Service { id: string; name: string; team: string; /** 1 is customer-facing and pages at night; 3 waits for the morning. */ tier: 1 | 2 | 3; /** The latency objective: the 95th percentile stays below this, in ms. */ latencySlo: number; /** The availability promised for a month, in per cent. */ availabilityTarget: number; } const SERVICES: readonly Service[] = [ { id: "checkout", name: "Checkout", team: "Payments", tier: 1, latencySlo: 300, availabilityTarget: 99.95 }, { id: "billing", name: "Billing", team: "Payments", tier: 1, latencySlo: 400, availabilityTarget: 99.9 }, { id: "sign-in", name: "Sign-in", team: "Identity", tier: 1, latencySlo: 200, availabilityTarget: 99.95 }, { id: "search", name: "Search", team: "Discovery", tier: 1, latencySlo: 250, availabilityTarget: 99.9 }, { id: "images", name: "Image service", team: "Discovery", tier: 2, latencySlo: 500, availabilityTarget: 99.5 }, { id: "notifications", name: "Notifications", team: "Messaging", tier: 2, latencySlo: 800, availabilityTarget: 99.5 }, { id: "webhooks", name: "Webhooks", team: "Integrations", tier: 2, latencySlo: 1000, availabilityTarget: 99.5 }, { id: "reports", name: "Reporting", team: "Insights", tier: 3, latencySlo: 2000, availabilityTarget: 99 }, ]; /** The month so far: 1 March 00:00 to now, in minutes. */ const MONTH_MINUTES = (NOW - at(1, 0)) / MINUTE; /** Minutes each service was down this month. With `MONTH_MINUTES` and the service's target, what an availability is worked out from. */ const DOWNTIME_MINUTES: Readonly> = { checkout: 34, billing: 18, "sign-in": 41, search: 85, images: 120, notifications: 12, webhooks: 210, reports: 95, }; /* The services a booking passes through, in order. */ const PATH = ["sign-in", "search", "checkout", "billing", "notifications"]; export default function FromData() { return ( {SERVICES.filter((service) => PATH.includes(service.id)).map((service) => ( ))} ); } ``` ##### Hold a figure to its limits `limits` are assessed through core's `assess()`; the verdict stands as a word beneath the number, never as a colour alone. ```tsx import { Calculation, Given, Quotient, Sum } from "@umriss-ui/calculation"; export default function Limits() { return ( ); } ``` #### When to use something else - A sheet read top to bottom, line by line → Chain #### Keyboard The keys of [Calculation](#/calculation/keyboard-calculation) apply here. #### API ##### `OperatorProps` | Prop | Type | Default | Description | |---|---|---|---| | `children` | `ReactNode` | — | The operands, in order: quantities, givens and references.
Shown in: [Show a share against a target](#/tree/share-with-target), [Take several amounts away](#/tree/difference-of-three), [Multiply quantities with units](#/tree/product-with-units) and 42 more | | `label` *required* | `string` | — | What the quantity is called. It stands on its line, wherever it is an operand, and in the accessible sentence. From `QuantityProps`.
Shown in: [Show a share against a target](#/tree/share-with-target), [Take several amounts away](#/tree/difference-of-three), [Multiply quantities with units](#/tree/product-with-units) and 50 more | | `id` | `string` | — | Makes the quantity referable: `` stands for it elsewhere. Needed only for a quantity used more than once. From `QuantityProps`.
Shown in: [Use a quantity twice](#/tree/used-twice), [The availability of a service (Installation)](#/installation/availability), [Write a tree (Calculation)](#/calculation/oee) and 10 more | | `unit` | `string` | — | A label written after the number - "min", "pcs", "€". Never converted and never checked; the calculation does no unit algebra. From `QuantityProps`.
Shown in: [Show a share against a target](#/tree/share-with-target), [Take several amounts away](#/tree/difference-of-three), [Multiply quantities with units](#/tree/product-with-units) and 42 more | | `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`.
Shown in: [Show a share against a target](#/tree/share-with-target), [Use a quantity twice](#/tree/used-twice), [Build operands from data](#/tree/from-data) and 17 more | | `decimals` | `number` | one digit for a percentage, at most two for a derived number, a given as it was given | Fixed number of fraction digits as shown. Rounding happens only in presentation; every operation uses the full value. From `QuantityProps`.
Shown in: [Multiply quantities with units](#/tree/product-with-units), [Build operands from data](#/tree/from-data), [Hold a figure to its limits](#/tree/limits) and 29 more | | `target` | `number` | — | The value this quantity is meant to reach - missed, never violated. From `QuantityProps`.
Shown in: [Show a share against a target](#/tree/share-with-target), [The availability of a service (Installation)](#/installation/availability), [Write a tree (Calculation)](#/calculation/oee) and 6 more | | `limits` | `readonly `[`Limit`](#type-Limit)`[]` | — | Bounds the quantity is assessed against, through core's `assess()`. From `QuantityProps`.
Shown in: [Hold a figure to its limits](#/tree/limits), [Density (Calculation)](#/calculation/density), [Hold an interim to its limits (Chain)](#/chain/error-budget) and 5 more | | `explanation` | `string` | — | A sentence of plain text beside the line: why the quantity is here, or what it means. From `QuantityProps`.
Shown in: [Say where a number came from (Given)](#/given/where-it-came-from), [A number one metric does not have (Metrics)](#/metrics/not-recorded), [Staff movement as a chain (Metrics)](#/metrics/staff-movement) and 2 more | | `aside` | `ReactNode` | — | Content of the caller's own beside the line - a sparkline, a link. It never takes part in the calculation. From `QuantityProps`.
Shown in: [Say where a number came from (Given)](#/given/where-it-came-from) | | `emphasis` | `"strong" \| "muted"` | — | How much the line stands out among the others ([ADR-0049](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0049-a-sum-takes-rows-from-data.md)): `strong` to be read first, `muted` to recede - a muted line still counts in full. Not on the Result, which is already the heaviest line. From `QuantityProps`.
Shown in: [A stronger line (Calculation)](#/calculation/a-stronger-line), [A quieter line (Calculation)](#/calculation/a-quieter-line), [Emphasis inside a group (Calculation)](#/calculation/emphasis-inside-a-group) | | `rule` | `"above"` | — | `above` rules the line off from the ones before it, where a section starts that is not an interim. Not on the Result. From `QuantityProps`.
Shown in: [A rule above (Calculation)](#/calculation/a-rule-above), [Emphasis inside a group (Calculation)](#/calculation/emphasis-inside-a-group) | ##### `RefProps` | Prop | Type | Default | Description | |---|---|---|---| | `to` *required* | `string` | — | The `id` of the quantity this reference stands for.
Shown in: [Use a quantity twice](#/tree/used-twice), [The availability of a service (Installation)](#/installation/availability), [Write a tree (Calculation)](#/calculation/oee) and 10 more | ##### Types on this page ###### `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` | — |
Shown in: [Hold a figure to its limits](#/tree/limits), [Density (Calculation)](#/calculation/density), [Hold an interim to its limits (Chain)](#/chain/error-budget) and 3 more | | `side` *required* | [`Side`](#type-Side)
`"upper" \| "lower"` | — |
Shown in: [Hold a figure to its limits](#/tree/limits), [Density (Calculation)](#/calculation/density), [Hold an interim to its limits (Chain)](#/chain/error-budget) and 3 more | | `severity` *required* | [`Severity`](#type-Severity)
`"warning" \| "alarm"` | — |
Shown in: [Hold a figure to its limits](#/tree/limits), [Density (Calculation)](#/calculation/density), [Hold an interim to its limits (Chain)](#/chain/error-budget) and 3 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 - Four operators and nothing else – no powers, roots or functions; compute those before and hand them in as givens. - A unit is a label and is never converted or checked; the calculation does no unit algebra. What umriss deliberately does not build, and why: [ADR-0032](https://romanhaendler.github.io/umriss-ui/what-umriss-ui-is-not/). ### Chain A sheet read top to bottom (a running calculation): each line worked into the value before it, and interims naming the value where they stand. Reach for it for invoices, budgets and cost sheets. ```ts import { Chain, Plus, Minus, Times, DividedBy, Interim } from "@umriss-ui/calculation"; ``` Demo page: https://romanhaendler.github.io/umriss-ui/calculation/chain/ A chain has no precedence: every line works into the value before it, strictly in order. So `Times` and `DividedBy` stand alone between two named values; anything else fails on the first render. A chain in view stands open – it is the working itself. What should show only on request goes into one line as a tree, which folds. Only an `Interim` shows the running value ([ADR-0028](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0028-a-calculation-is-a-tree-or-a-chain.md)). #### Examples ##### Take amounts away from a first quantity A chain starts with a quantity without operator; `Minus` and `Plus` take a label and a value directly, and an `Interim` ends it. ```tsx import { Calculation, Chain, Given, Interim, Minus } from "@umriss-ui/calculation"; export default function NetWeight() { return ( ); } ``` ##### Mix additions and deductions Where a figure is a list of items, write each as a line of its own; a credit is a `Minus` where it stands. ```tsx import { Calculation, Chain, Given, Interim, Minus, Plus } from "@umriss-ui/calculation"; export default function TourCost() { return ( ); } ``` ##### Multiply between two interims A chain has no precedence, so `Times` and `DividedBy` stand alone between two interims; after a `Plus` they fail on the first render. ```tsx import { Calculation, Chain, Given, Interim, Plus, Times } from "@umriss-ui/calculation"; export default function MarkUpAndVat() { return ( ); } ``` ##### Fold a group of lines into one A chain in view never folds; to show items only on request, put them into one line as a tree: a `Sum` held by a `Minus` – a `Minus`, because contributions are positive by nature. Rows of either sign from data go into a `Plus` instead; the page Rows from data shows every case. ```tsx import { Calculation, Chain, Given, Interim, Minus, Plus, Product, Ref, Sum } from "@umriss-ui/calculation"; const CONTRIBUTIONS = [ { name: "Pension insurance", rate: 0.093 }, { name: "Unemployment insurance", rate: 0.013 }, { name: "Health insurance", rate: 0.082 }, { name: "Long-term care insurance", rate: 0.017 }, ]; export default function Payslip() { return ( {CONTRIBUTIONS.map((contribution) => ( ))} ); } ``` ##### Build the lines from data `.map` inside a chain makes each item a `Plus`, and all stand in view above their interim. ```tsx import { Calculation, Chain, Given, Interim, Plus } from "@umriss-ui/calculation"; const ITEMS = [ ["Compute, booking API", 4128.4], ["Compute, search", 2864.1], ["Compute, workers", 1741.9], ["Database, primary", 2310], ["Database, replicas", 1540], ["Object storage", 386.2], ["Block storage", 512.8], ["Data transfer out", 948.6], ["Load balancers", 264], ["Queues", 118.3], ["Cache cluster", 690], ["Log storage", 431.7], ["Metrics and traces", 822.5], ["Backups", 207.4], ["Support plan", 1200], ] as const; export default function FifteenItems() { const [[firstName, firstAmount], ...rest] = ITEMS; return ( {rest.map(([name, amount]) => ( ))} ); } ``` ##### Put a chain inside a tree As an operand a chain folds and stands for its last interim; opened, it stands whole beneath its line. ```tsx import { Calculation, Chain, Given, Interim, Minus, Plus, Quotient, Ref } from "@umriss-ui/calculation"; export default function ChainInATree() { return ( ); } ``` ##### Hold an interim to its limits An `Interim` takes `target` and `limits` like any quantity; here the error budget left is assessed against two lower limits. ```tsx import { Calculation, Chain, Given, Interim, Minus, Product } from "@umriss-ui/calculation"; export default function ErrorBudget() { return ( ); } ``` #### When to use something else - A figure made of factors, folded to its formula → Tree #### Keyboard The keys of [Calculation](#/calculation/keyboard-calculation) apply here. #### API ##### `ChainProps` | Prop | Type | Default | Description | |---|---|---|---| | `children` | `ReactNode` | — | The operands, top to bottom: a first quantity with no operator, then `Plus`, `Minus`, `Times` and `DividedBy` with their operands and `Interim`s naming the value where they stand. The last line is an `Interim`; a `Times` or `DividedBy` stands alone between two named values.
Shown in: [Take amounts away from a first quantity](#/chain/net-weight), [Mix additions and deductions](#/chain/tour-cost), [Multiply between two interims](#/chain/mark-up-and-vat) and 22 more | ##### `ChainOperandProps` | Prop | Type | Default | Description | |---|---|---|---| | `label` | `string` | — | The operand's name, where the line is a given written in place. Left out where the line holds a quantity as its child.
Shown in: [Take amounts away from a first quantity](#/chain/net-weight), [Mix additions and deductions](#/chain/tour-cost), [Multiply between two interims](#/chain/mark-up-and-vat) and 17 more | | `value` | `number \| null \| `[`MetricValues`](#type-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`.
Shown in: [Take amounts away from a first quantity](#/chain/net-weight), [Mix additions and deductions](#/chain/tour-cost), [Multiply between two interims](#/chain/mark-up-and-vat) and 17 more | | `children` | `ReactNode` | — | Instead of `label` and `value`: exactly one quantity - a tree, a `` or a ``.
Shown in: [Fold a group of lines into one](#/chain/payslip), [Write a chain (Calculation)](#/calculation/invoice), [Emphasis inside a group (Calculation)](#/calculation/emphasis-inside-a-group) and 12 more | | `source` | `string` | — | Where the number came from: a system, a report, a person. From `GivenProps`.
Shown in: [Say where a number came from (Given)](#/given/where-it-came-from), [State when a number was true (Given)](#/given/as-of), [Staff movement as a chain (Metrics)](#/metrics/staff-movement) and 8 more | | `asOf` | `Date \| number` | — | When the number was true - not when it was fetched. From `GivenProps`.
Shown in: [Say where a number came from (Given)](#/given/where-it-came-from), [State when a number was true (Given)](#/given/as-of), [The cost per stop of a day's tours (Worked examples)](#/worked-examples/cost-per-tour) | | `ages` | [`FreshnessAges`](#type-FreshnessAges) | — | 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`.
Shown in: [Say where a number came from (Given)](#/given/where-it-came-from), [The cost per stop of a day's tours (Worked examples)](#/worked-examples/cost-per-tour) | | `id` | `string` | — | Makes the quantity referable: `` stands for it elsewhere. Needed only for a quantity used more than once. From `QuantityProps`.
Shown in: [Fold a group of lines into one](#/chain/payslip), [Put a chain inside a tree](#/chain/chain-in-a-tree), [The availability of a service (Installation)](#/installation/availability) and 10 more | | `unit` | `string` | — | A label written after the number - "min", "pcs", "€". Never converted and never checked; the calculation does no unit algebra. From `QuantityProps`.
Shown in: [Take amounts away from a first quantity](#/chain/net-weight), [Mix additions and deductions](#/chain/tour-cost), [Multiply between two interims](#/chain/mark-up-and-vat) and 42 more | | `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`.
Shown in: [Fold a group of lines into one](#/chain/payslip), [Put a chain inside a tree](#/chain/chain-in-a-tree), [Hold an interim to its limits](#/chain/error-budget) and 17 more | | `decimals` | `number` | one digit for a percentage, at most two for a derived number, a given as it was given | Fixed number of fraction digits as shown. Rounding happens only in presentation; every operation uses the full value. From `QuantityProps`.
Shown in: [Take amounts away from a first quantity](#/chain/net-weight), [Mix additions and deductions](#/chain/tour-cost), [Multiply between two interims](#/chain/mark-up-and-vat) and 29 more | | `target` | `number` | — | The value this quantity is meant to reach - missed, never violated. From `QuantityProps`.
Shown in: [Put a chain inside a tree](#/chain/chain-in-a-tree), [The availability of a service (Installation)](#/installation/availability), [Write a tree (Calculation)](#/calculation/oee) and 6 more | | `limits` | `readonly `[`Limit`](#type-Limit)`[]` | — | Bounds the quantity is assessed against, through core's `assess()`. From `QuantityProps`.
Shown in: [Hold an interim to its limits](#/chain/error-budget), [Density (Calculation)](#/calculation/density), [Hold a figure to its limits (Tree)](#/tree/limits) and 5 more | | `explanation` | `string` | — | A sentence of plain text beside the line: why the quantity is here, or what it means. From `QuantityProps`.
Shown in: [Say where a number came from (Given)](#/given/where-it-came-from), [A number one metric does not have (Metrics)](#/metrics/not-recorded), [Staff movement as a chain (Metrics)](#/metrics/staff-movement) and 2 more | | `aside` | `ReactNode` | — | Content of the caller's own beside the line - a sparkline, a link. It never takes part in the calculation. From `QuantityProps`.
Shown in: [Say where a number came from (Given)](#/given/where-it-came-from) | | `emphasis` | `"strong" \| "muted"` | — | How much the line stands out among the others ([ADR-0049](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0049-a-sum-takes-rows-from-data.md)): `strong` to be read first, `muted` to recede - a muted line still counts in full. Not on the Result, which is already the heaviest line. From `QuantityProps`.
Shown in: [A stronger line (Calculation)](#/calculation/a-stronger-line), [A quieter line (Calculation)](#/calculation/a-quieter-line), [Emphasis inside a group (Calculation)](#/calculation/emphasis-inside-a-group) | | `rule` | `"above"` | — | `above` rules the line off from the ones before it, where a section starts that is not an interim. Not on the Result. From `QuantityProps`.
Shown in: [A rule above (Calculation)](#/calculation/a-rule-above), [Emphasis inside a group (Calculation)](#/calculation/emphasis-inside-a-group) | ##### `QuantityProps` | Prop | Type | Default | Description | |---|---|---|---| | `label` *required* | `string` | — | What the quantity is called. It stands on its line, wherever it is an operand, and in the accessible sentence.
Shown in: [Take amounts away from a first quantity](#/chain/net-weight), [Mix additions and deductions](#/chain/tour-cost), [Multiply between two interims](#/chain/mark-up-and-vat) and 50 more | | `id` | `string` | — | Makes the quantity referable: `` stands for it elsewhere. Needed only for a quantity used more than once.
Shown in: [Fold a group of lines into one](#/chain/payslip), [Put a chain inside a tree](#/chain/chain-in-a-tree), [The availability of a service (Installation)](#/installation/availability) and 10 more | | `unit` | `string` | — | A label written after the number - "min", "pcs", "€". Never converted and never checked; the calculation does no unit algebra.
Shown in: [Take amounts away from a first quantity](#/chain/net-weight), [Mix additions and deductions](#/chain/tour-cost), [Multiply between two interims](#/chain/mark-up-and-vat) and 42 more | | `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.
Shown in: [Fold a group of lines into one](#/chain/payslip), [Put a chain inside a tree](#/chain/chain-in-a-tree), [Hold an interim to its limits](#/chain/error-budget) and 17 more | | `decimals` | `number` | one digit for a percentage, at most two for a derived number, a given as it was given | Fixed number of fraction digits as shown. Rounding happens only in presentation; every operation uses the full value.
Shown in: [Take amounts away from a first quantity](#/chain/net-weight), [Mix additions and deductions](#/chain/tour-cost), [Multiply between two interims](#/chain/mark-up-and-vat) and 29 more | | `target` | `number` | — | The value this quantity is meant to reach - missed, never violated.
Shown in: [Put a chain inside a tree](#/chain/chain-in-a-tree), [The availability of a service (Installation)](#/installation/availability), [Write a tree (Calculation)](#/calculation/oee) and 6 more | | `limits` | `readonly `[`Limit`](#type-Limit)`[]` | — | Bounds the quantity is assessed against, through core's `assess()`.
Shown in: [Hold an interim to its limits](#/chain/error-budget), [Density (Calculation)](#/calculation/density), [Hold a figure to its limits (Tree)](#/tree/limits) and 5 more | | `explanation` | `string` | — | A sentence of plain text beside the line: why the quantity is here, or what it means.
Shown in: [Say where a number came from (Given)](#/given/where-it-came-from), [A number one metric does not have (Metrics)](#/metrics/not-recorded), [Staff movement as a chain (Metrics)](#/metrics/staff-movement) and 2 more | | `aside` | `ReactNode` | — | Content of the caller's own beside the line - a sparkline, a link. It never takes part in the calculation.
Shown in: [Say where a number came from (Given)](#/given/where-it-came-from) | | `emphasis` | `"strong" \| "muted"` | — | How much the line stands out among the others ([ADR-0049](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0049-a-sum-takes-rows-from-data.md)): `strong` to be read first, `muted` to recede - a muted line still counts in full. Not on the Result, which is already the heaviest line.
Shown in: [A stronger line (Calculation)](#/calculation/a-stronger-line), [A quieter line (Calculation)](#/calculation/a-quieter-line), [Emphasis inside a group (Calculation)](#/calculation/emphasis-inside-a-group) | | `rule` | `"above"` | — | `above` rules the line off from the ones before it, where a section starts that is not an interim. Not on the Result.
Shown in: [A rule above (Calculation)](#/calculation/a-rule-above), [Emphasis inside a group (Calculation)](#/calculation/emphasis-inside-a-group) | ##### Types on this page ###### `MetricValues` A given's numbers where the calculation has metrics: one per metric id, `null` where the number is absent. ```ts type MetricValues = Readonly>; ``` ###### `FreshnessAges` From `@umriss-ui/core`. The two ages at which a value changes its state, in milliseconds. Both bounds belong to the older state: exactly five minutes old is already `stale`. `alt <= lost` is the sensible order. Stated the other way round, the harder statement still wins - the middle state simply falls away. | Prop | Type | Default | Description | |---|---|---|---| | `stale` *required* | `number` | — | From this age on the value counts as stale, but keeps its verdict. | | `lost` *required* | `number` | — | From this age on the connection counts as disconnected. | ###### `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` | — |
Shown in: [Hold an interim to its limits](#/chain/error-budget), [Density (Calculation)](#/calculation/density), [Hold a figure to its limits (Tree)](#/tree/limits) and 3 more | | `side` *required* | [`Side`](#type-Side)
`"upper" \| "lower"` | — |
Shown in: [Hold an interim to its limits](#/chain/error-budget), [Density (Calculation)](#/calculation/density), [Hold a figure to its limits (Tree)](#/tree/limits) and 3 more | | `severity` *required* | [`Severity`](#type-Severity)
`"warning" \| "alarm"` | — |
Shown in: [Hold an interim to its limits](#/chain/error-budget), [Density (Calculation)](#/calculation/density), [Hold a figure to its limits (Tree)](#/tree/limits) and 3 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 - A chain in view does not fold; as an operand of a tree it folds to its last interim. What umriss deliberately does not build, and why: [ADR-0032](https://romanhaendler.github.io/umriss-ui/what-umriss-ui-is-not/). ### Rows from data Rows that come from data (line items, corrections, allowances), however many there are, as one line that folds: a payslip's corrections, an invoice's surcharges, a team's movements. ```ts import { Sum, Plus, Minus } from "@umriss-ui/calculation"; ``` Demo page: https://romanhaendler.github.io/umriss-ui/calculation/rows-from-data/ A `Sum` takes the rows of an array as they come: many, one, or none. With one it still folds and opens onto that one; with none it is worth zero, has nothing to open, and says "no entries" where its formula would stand. The line keeps its place either way, so the statement does not change shape with the data ([ADR-0049](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0049-a-sum-takes-rows-from-data.md)). Rows of both signs go into the sum as they come. A line that adds or takes away shows its contribution: the direction as its operator, the number without a sign – "− Overpaid travel 120.00", never "+ -120.00". The quantity itself keeps its sign where its derivation closes, as an interim, as the result and wherever it is referenced. A factor keeps its sign; a line worth zero or missing keeps the operator it is written with. `Plus` and `Minus` say how a quantity enters, the number its sign. Signed rows from data go into `Plus`; a quantity that is positive by nature and taken away – income tax, a sum of contributions – into `Minus`, so that it is never quoted as negative. #### Examples ##### Rows of any count Hand a `Sum` the rows of an array, as they come from the data, and hold it in a `Plus` or `Minus`: the rows fold into one line, and the interim after it takes their total. ```tsx import { Calculation, Chain, Given, Interim, Minus, Plus, Sum } from "@umriss-ui/calculation"; /* Data from a payroll system, written out here so the example runs on its own. */ const ALLOWANCES = [ { id: "night", name: "Night work bonus", amount: 84 }, { id: "holiday", name: "Holiday pay", amount: 120 }, { id: "on-call", name: "On-call allowance", amount: 45 }, ]; export default function RowsOfAnyCount() { return ( {ALLOWANCES.map((allowance) => ( ))} ); } ``` ##### No rows When the data holds no rows, the sum is zero and its line stays where it was: it has nothing to open and says "no entries" where its formula would stand, so a reader sees that there were none rather than wondering whether a line is missing. ```tsx import { Calculation, Chain, Given, Interim, Minus, Plus, Sum } from "@umriss-ui/calculation"; /* A month without allowances: the payroll system returns an empty list. */ const ALLOWANCES: readonly { id: string; name: string; amount: number }[] = []; export default function NoRows() { return ( {ALLOWANCES.map((allowance) => ( ))} ); } ``` ##### One row A sum of one row is still a group: its line keeps its name and folds, and opens onto that one row, closing with "= Allowances". The statement keeps its shape as the data changes, and an opened group stays open. ```tsx import { Calculation, Chain, Given, Interim, Minus, Plus, Sum } from "@umriss-ui/calculation"; /* A month with a single allowance. */ const ALLOWANCES = [{ id: "night", name: "Night work bonus", amount: 84 }]; export default function OneRow() { return ( {ALLOWANCES.map((allowance) => ( ))} ); } ``` ##### Rows of both signs Rows that add and rows that take away go into one `Sum` as they come, each with its own sign. Every line shows its contribution: the direction as its operator, the number without a sign – a correction of −120 stands as "− 120.00", never as "+ -120.00". ```tsx import { Calculation, Chain, Given, Interim, Minus, Plus, Sum } from "@umriss-ui/calculation"; /* Corrections from a payroll system: positive adds to the pay, negative takes from it. */ const CORRECTIONS = [ { id: "night", name: "Night work bonus, February", amount: 84 }, { id: "travel", name: "Overpaid travel, February", amount: -120 }, { id: "rounding", name: "Rounding", amount: 0.03 }, ]; export default function RowsOfBothSigns() { return ( {CORRECTIONS.map((correction) => ( ))} ); } ``` ##### A group that turns negative Held by a `Plus`, a group whose total is negative stands as a minus with its number unsigned: "− Corrections 36.00". Opened, its derivation closes with the quantity itself, sign and all: "= Corrections -36.00". The line says what it does to the pay, the closing row what the corrections amount to. ```tsx import { Calculation, Chain, Given, Interim, Minus, Plus, Sum } from "@umriss-ui/calculation"; const CORRECTIONS = [ { id: "night", name: "Night work bonus, February", amount: 84 }, { id: "travel", name: "Overpaid travel, February", amount: -120 }, ]; export default function AGroupThatTurnsNegative() { return ( {CORRECTIONS.map((correction) => ( ))} ); } ``` ##### Plus or Minus `Plus` and `Minus` say how a quantity enters, the number says its sign. Signed rows from data go into `Plus`: their signs do the rest. A quantity that is positive by nature and taken away here – income tax, a sum of contributions – goes into `Minus`, so that it keeps its sign where it closes and wherever it is referenced. ```tsx import { Calculation, Chain, Given, Interim, Minus, Plus, Product, Ref, Sum } from "@umriss-ui/calculation"; const CONTRIBUTIONS = [ { name: "Pension insurance", rate: 0.093 }, { name: "Health insurance", rate: 0.082 }, ]; const CORRECTIONS = [ { id: "night", name: "Night work bonus, February", amount: 84 }, { id: "travel", name: "Overpaid travel, February", amount: -120 }, ]; export default function PlusOrMinus() { return ( {CONTRIBUTIONS.map((contribution) => ( ))} {CORRECTIONS.map((correction) => ( ))} ); } ``` ##### A deduction first The first operand of a sum stands without an operator – unless it lowers the sum: then it carries a minus like every other deduction. A folded line's formula starts with that minus too – "A sum from data in a tree" below shows one. ```tsx import { Calculation, Given, Sum } from "@umriss-ui/calculation"; const ROWS = [ { id: "refund", name: "Refund, order 1184", amount: -30 }, { id: "bonus", name: "Loyalty bonus", amount: 12 }, { id: "voucher", name: "Voucher", amount: 5 }, ]; export default function ADeductionFirst() { return ( {ROWS.map((row) => ( ))} ); } ``` ##### Zero and missing keep their sign A row worth zero has no direction, and a missing row has no number: both keep the operator they are written with. The missing row makes the group and everything after it absent, with the reason – never zero. ```tsx import { Calculation, Chain, Given, Interim, Minus, Plus, Sum } from "@umriss-ui/calculation"; const CORRECTIONS: readonly { id: string; name: string; amount: number | null }[] = [ { id: "night", name: "Night work bonus, February", amount: 84 }, { id: "travel", name: "Travel, February", amount: 0 }, { id: "overtime", name: "Overtime, February", amount: null }, ]; export default function ZeroAndMissingKeepTheirSign() { return ( {CORRECTIONS.map((correction) => ( ))} ); } ``` ##### A sum from data in a tree Rows from data work in a tree as in a chain: here the stock movements of a day, whatever their count and sign, stand as one operand of a `Difference`. Taken away, a negative total turns into a plus – "+ Movements 7" – and the derivation closes with its own sign. ```tsx import { Calculation, Difference, Given, Sum } from "@umriss-ui/calculation"; const MOVEMENTS = [ { id: "m1", name: "Picked, order 5512", amount: -12 }, { id: "m2", name: "Received, delivery 881", amount: 40 }, { id: "m3", name: "Picked, order 5517", amount: -35 }, ]; export default function ASumFromDataInATree() { return ( {MOVEMENTS.map((movement) => ( ))} ); } ``` ##### Rows of both signs with metrics With metrics a line has one operator and a number per metric. Where they all point the same way, the line shows its contribution as everywhere else; a metric that is zero or missing does not count. Where they point in different directions – a part-timer joins: one more head, half a position less – the line keeps its written operator and every number its sign. ```tsx import { Calculation, Chain, Given, Interim, Plus, Sum } from "@umriss-ui/calculation"; import type { Metric } from "@umriss-ui/calculation"; const STAFF: readonly Metric[] = [ { id: "heads", label: "Headcount", unit: "HC", decimals: 0 }, { id: "fte", label: "Full-time equivalents", unit: "FTE", decimals: 1 }, ]; /* The quarter's movements from the HR system. */ const MOVEMENTS = [ { id: "sales", name: "Transferred to sales", heads: -4, fte: -3.5 }, { id: "hours", name: "Hours reduced", heads: 0, fte: -1.5 }, { id: "part-time", name: "Full-timer replaced by two part-timers", heads: 1, fte: -0.5 }, { id: "joiners", name: "Joiners", heads: 6, fte: 5.8 }, ]; export default function RowsOfBothSignsWithMetrics() { return ( {MOVEMENTS.map((movement) => ( ))} ); } ``` #### When to use something else - A fixed set of lines that is always in view → Chain #### Keyboard The keys of [Calculation](#/calculation/keyboard-calculation) apply here. #### Props on this page From `CalculationProps` — the full table stands on [Calculation](#/calculation/type-CalculationProps). | Prop | Type | Default | Description | |---|---|---|---| | [`children`](#/calculation/CalculationProps-children) *required* | `ReactNode` | — | The result: exactly one quantity - a tree (``, ``, ``, `` with their operands, `` as leaves) or a ``, the two mixing freely; `` for a quantity defined elsewhere. | | [`metrics`](#/calculation/CalculationProps-metrics) | `readonly `[`Metric`](#/metrics/type-Metric)`[]` | — | Several numbers per quantity, side by side - headcount and full-time equivalents of the same teams ([ADR-0038](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0038-a-calculation-can-carry-several-metrics.md)). Every `value` is then an object with a number for each metric id; unit and places are the metric's. A calculation with metrics only adds and subtracts, and assesses no target or limits. | From `OperatorProps` — the full table stands on [Tree](#/tree/type-OperatorProps). | Prop | Type | Default | Description | |---|---|---|---| | [`children`](#/tree/OperatorProps-children) | `ReactNode` | — | The operands, in order: quantities, givens and references. | | [`label`](#/tree/OperatorProps-label) *required* | `string` | — | What the quantity is called. It stands on its line, wherever it is an operand, and in the accessible sentence. From `QuantityProps`. | | [`id`](#/tree/OperatorProps-id) | `string` | — | Makes the quantity referable: `` stands for it elsewhere. Needed only for a quantity used more than once. From `QuantityProps`. | | [`unit`](#/tree/OperatorProps-unit) | `string` | — | A label written after the number - "min", "pcs", "€". Never converted and never checked; the calculation does no unit algebra. From `QuantityProps`. | | [`format`](#/tree/OperatorProps-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`. | | [`decimals`](#/tree/OperatorProps-decimals) | `number` | one digit for a percentage, at most two for a derived number, a given as it was given | Fixed number of fraction digits as shown. Rounding happens only in presentation; every operation uses the full value. From `QuantityProps`. | From `RefProps` — the full table stands on [Tree](#/tree/type-RefProps). | Prop | Type | Default | Description | |---|---|---|---| | [`to`](#/tree/RefProps-to) *required* | `string` | — | The `id` of the quantity this reference stands for. | From `ChainProps` — the full table stands on [Chain](#/chain/type-ChainProps). | Prop | Type | Default | Description | |---|---|---|---| | [`children`](#/chain/ChainProps-children) | `ReactNode` | — | The operands, top to bottom: a first quantity with no operator, then `Plus`, `Minus`, `Times` and `DividedBy` with their operands and `Interim`s 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](#/chain/type-ChainOperandProps). | Prop | Type | Default | Description | |---|---|---|---| | [`label`](#/chain/ChainOperandProps-label) | `string` | — | 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`](#/chain/ChainOperandProps-value) | `number \| null \| `[`MetricValues`](#/chain/type-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`. | | [`children`](#/chain/ChainOperandProps-children) | `ReactNode` | — | Instead of `label` and `value`: exactly one quantity - a tree, a `` or a ``. | | [`id`](#/chain/ChainOperandProps-id) | `string` | — | Makes the quantity referable: `` stands for it elsewhere. Needed only for a quantity used more than once. From `QuantityProps`. | | [`unit`](#/chain/ChainOperandProps-unit) | `string` | — | A label written after the number - "min", "pcs", "€". Never converted and never checked; the calculation does no unit algebra. From `QuantityProps`. | | [`format`](#/chain/ChainOperandProps-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`. | | [`decimals`](#/chain/ChainOperandProps-decimals) | `number` | one digit for a percentage, at most two for a derived number, a given as it was given | Fixed number of fraction digits as shown. Rounding happens only in presentation; every operation uses the full value. From `QuantityProps`. | From `QuantityProps` — the full table stands on [Chain](#/chain/type-QuantityProps). | Prop | Type | Default | Description | |---|---|---|---| | [`label`](#/chain/QuantityProps-label) *required* | `string` | — | What the quantity is called. It stands on its line, wherever it is an operand, and in the accessible sentence. | | [`id`](#/chain/QuantityProps-id) | `string` | — | Makes the quantity referable: `` stands for it elsewhere. Needed only for a quantity used more than once. | | [`unit`](#/chain/QuantityProps-unit) | `string` | — | A label written after the number - "min", "pcs", "€". Never converted and never checked; the calculation does no unit algebra. | | [`format`](#/chain/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. | | [`decimals`](#/chain/QuantityProps-decimals) | `number` | one digit for a percentage, at most two for a derived number, a given as it was given | Fixed number of fraction digits as shown. Rounding happens only in presentation; every operation uses the full value. | From `GivenProps` — the full table stands on [Given](#/given/type-GivenProps). | Prop | Type | Default | Description | |---|---|---|---| | [`value`](#/given/GivenProps-value) *required* | `number \| null \| undefined \| `[`MetricValues`](#/given/type-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. | | [`label`](#/given/GivenProps-label) *required* | `string` | — | What the quantity is called. It stands on its line, wherever it is an operand, and in the accessible sentence. From `QuantityProps`. | | [`id`](#/given/GivenProps-id) | `string` | — | Makes the quantity referable: `` stands for it elsewhere. Needed only for a quantity used more than once. From `QuantityProps`. | | [`unit`](#/given/GivenProps-unit) | `string` | — | A label written after the number - "min", "pcs", "€". Never converted and never checked; the calculation does no unit algebra. From `QuantityProps`. | | [`format`](#/given/GivenProps-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`. | | [`decimals`](#/given/GivenProps-decimals) | `number` | one digit for a percentage, at most two for a derived number, a given as it was given | Fixed number of fraction digits as shown. Rounding happens only in presentation; every operation uses the full value. From `QuantityProps`. | From `Metric` — the full table stands on [Metrics](#/metrics/type-Metric). | Prop | Type | Default | Description | |---|---|---|---| | [`id`](#/metrics/Metric-id) *required* | `string` | — | The key of this metric's number in every `value` object. | | [`label`](#/metrics/Metric-label) *required* | `string` | — | The metric's name, in the head above its column and in each row's sentence. | | [`unit`](#/metrics/Metric-unit) | `string` | — | A label written after the number where a result closes, and beneath the metric's name in the head. Never converted and never checked. | | [`decimals`](#/metrics/Metric-decimals) | `number` | a given as it was given, at most two for a derived number | Fixed number of fraction digits as shown, for every quantity of this metric. | #### Known limits - Only a sum takes any count: an empty product, a difference of one and a quotient of anything but two still fail on the first render. What umriss deliberately does not build, and why: [ADR-0032](https://romanhaendler.github.io/umriss-ui/what-umriss-ui-is-not/). ### Given The numbers a calculation starts from (inputs, leaves): where they came from, when they were true, and whether they are still fresh. ```ts import { Given } from "@umriss-ui/calculation"; ``` Demo page: https://romanhaendler.github.io/umriss-ui/calculation/given/ A given with `asOf` and `ages` carries a freshness through core, as a `Stat` does – beside the number, never instead of its verdict. There are no default ages. The operator words, the reasons for absence and the number formats are core's wording; German comes from `@umriss-ui/core/wording/de` ([ADR-0019](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0019-two-wordings-ship-english-is-the-default.md)). #### Examples ##### Say where a number came from `source`, `asOf` and `ages` give a given its origin and freshness; `explanation` and `aside` stand beside a line without taking part. ```tsx import { useState } from "react"; import { Sparkline } from "@umriss-ui/core"; import { Calculation, Given, Quotient } from "@umriss-ui/calculation"; const AGES = { stale: 15 * 60_000, lost: 60 * 60_000 }; export default function WhereItCameFrom() { const [now] = useState(() => Date.now()); return ( ); } ``` ##### State when a number was true `asOf` alone shows the time a number held; there are no default `ages`, because stale means different things on different screens. ```tsx import { Calculation, Given, Product } from "@umriss-ui/calculation"; export default function AsOf() { return ( ); } ``` ##### Read in German Operator words, reasons and number formats come from core's `LanguageProvider` ([ADR-0019](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0019-two-wordings-ship-english-is-the-default.md)); your labels stay as you write them. ```tsx import { LanguageProvider } from "@umriss-ui/core"; import { GERMAN_FORMATS, GERMAN_WORDING } from "@umriss-ui/core/wording/de"; import { Calculation, Chain, Given, Interim, Minus, Product, Ref } from "@umriss-ui/calculation"; export default function InGerman() { return ( ); } ``` #### Keyboard The keys of [Calculation](#/calculation/keyboard-calculation) apply here. #### API ##### `GivenProps` | Prop | Type | Default | Description | |---|---|---|---| | `value` *required* | `number \| null \| undefined \| `[`MetricValues`](#type-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.
Shown in: [Say where a number came from](#/given/where-it-came-from), [State when a number was true](#/given/as-of), [Read in German](#/given/in-german) and 50 more | | `source` | `string` | — | Where the number came from: a system, a report, a person.
Shown in: [Say where a number came from](#/given/where-it-came-from), [State when a number was true](#/given/as-of), [Staff movement as a chain (Metrics)](#/metrics/staff-movement) and 8 more | | `asOf` | `Date \| number` | — | When the number was true - not when it was fetched.
Shown in: [Say where a number came from](#/given/where-it-came-from), [State when a number was true](#/given/as-of), [The cost per stop of a day's tours (Worked examples)](#/worked-examples/cost-per-tour) | | `ages` | [`FreshnessAges`](#type-FreshnessAges) | — | The ages at which `asOf` turns stale and lost. With both, the given carries a freshness through core; with `asOf` alone, only the time.
Shown in: [Say where a number came from](#/given/where-it-came-from), [The cost per stop of a day's tours (Worked examples)](#/worked-examples/cost-per-tour) | | `label` *required* | `string` | — | What the quantity is called. It stands on its line, wherever it is an operand, and in the accessible sentence. From `QuantityProps`.
Shown in: [Say where a number came from](#/given/where-it-came-from), [State when a number was true](#/given/as-of), [Read in German](#/given/in-german) and 50 more | | `id` | `string` | — | Makes the quantity referable: `` stands for it elsewhere. Needed only for a quantity used more than once. From `QuantityProps`.
Shown in: [Read in German](#/given/in-german), [The availability of a service (Installation)](#/installation/availability), [Write a tree (Calculation)](#/calculation/oee) and 10 more | | `unit` | `string` | — | A label written after the number - "min", "pcs", "€". Never converted and never checked; the calculation does no unit algebra. From `QuantityProps`.
Shown in: [Say where a number came from](#/given/where-it-came-from), [State when a number was true](#/given/as-of), [Read in German](#/given/in-german) and 42 more | | `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`.
Shown in: [Say where a number came from](#/given/where-it-came-from), [Read in German](#/given/in-german), [The availability of a service (Installation)](#/installation/availability) and 17 more | | `decimals` | `number` | one digit for a percentage, at most two for a derived number, a given as it was given | Fixed number of fraction digits as shown. Rounding happens only in presentation; every operation uses the full value. From `QuantityProps`.
Shown in: [Say where a number came from](#/given/where-it-came-from), [State when a number was true](#/given/as-of), [Read in German](#/given/in-german) and 29 more | | `target` | `number` | — | The value this quantity is meant to reach - missed, never violated. From `QuantityProps`.
Shown in: [The availability of a service (Installation)](#/installation/availability), [Write a tree (Calculation)](#/calculation/oee), [Density (Calculation)](#/calculation/density) and 6 more | | `limits` | `readonly `[`Limit`](#type-Limit)`[]` | — | Bounds the quantity is assessed against, through core's `assess()`. From `QuantityProps`.
Shown in: [Say where a number came from](#/given/where-it-came-from), [Density (Calculation)](#/calculation/density), [Hold a figure to its limits (Tree)](#/tree/limits) and 5 more | | `explanation` | `string` | — | A sentence of plain text beside the line: why the quantity is here, or what it means. From `QuantityProps`.
Shown in: [Say where a number came from](#/given/where-it-came-from), [A number one metric does not have (Metrics)](#/metrics/not-recorded), [Staff movement as a chain (Metrics)](#/metrics/staff-movement) and 2 more | | `aside` | `ReactNode` | — | Content of the caller's own beside the line - a sparkline, a link. It never takes part in the calculation. From `QuantityProps`.
Shown in: [Say where a number came from](#/given/where-it-came-from) | | `emphasis` | `"strong" \| "muted"` | — | How much the line stands out among the others ([ADR-0049](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0049-a-sum-takes-rows-from-data.md)): `strong` to be read first, `muted` to recede - a muted line still counts in full. Not on the Result, which is already the heaviest line. From `QuantityProps`.
Shown in: [A stronger line (Calculation)](#/calculation/a-stronger-line), [A quieter line (Calculation)](#/calculation/a-quieter-line), [Emphasis inside a group (Calculation)](#/calculation/emphasis-inside-a-group) | | `rule` | `"above"` | — | `above` rules the line off from the ones before it, where a section starts that is not an interim. Not on the Result. From `QuantityProps`.
Shown in: [A rule above (Calculation)](#/calculation/a-rule-above), [Emphasis inside a group (Calculation)](#/calculation/emphasis-inside-a-group) | ##### Types on this page ###### `MetricValues` A given's numbers where the calculation has metrics: one per metric id, `null` where the number is absent. ```ts type MetricValues = Readonly>; ``` ###### `FreshnessAges` From `@umriss-ui/core`. The two ages at which a value changes its state, in milliseconds. Both bounds belong to the older state: exactly five minutes old is already `stale`. `alt <= lost` is the sensible order. Stated the other way round, the harder statement still wins - the middle state simply falls away. | Prop | Type | Default | Description | |---|---|---|---| | `stale` *required* | `number` | — | From this age on the value counts as stale, but keeps its verdict. | | `lost` *required* | `number` | — | From this age on the connection counts as disconnected. | ###### `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` | — |
Shown in: [Say where a number came from](#/given/where-it-came-from), [Density (Calculation)](#/calculation/density), [Hold a figure to its limits (Tree)](#/tree/limits) and 3 more | | `side` *required* | [`Side`](#type-Side)
`"upper" \| "lower"` | — |
Shown in: [Say where a number came from](#/given/where-it-came-from), [Density (Calculation)](#/calculation/density), [Hold a figure to its limits (Tree)](#/tree/limits) and 3 more | | `severity` *required* | [`Severity`](#type-Severity)
`"warning" \| "alarm"` | — |
Shown in: [Say where a number came from](#/given/where-it-came-from), [Density (Calculation)](#/calculation/density), [Hold a figure to its limits (Tree)](#/tree/limits) and 3 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"; ``` ### Metrics Several numbers on every line, side by side (columns, measures): headcount and full-time equivalents of the same teams, summed by the same derivation. Reach for it where one sheet has to answer in more than one unit. ```ts import { Calculation, Given, Sum, Chain, Plus, Minus, Interim } from "@umriss-ui/calculation"; ``` Demo page: https://romanhaendler.github.io/umriss-ui/calculation/metrics/ `metrics` on the calculation names each metric once, with its unit and places; every `value` is then an object with a number for each metric id. Each metric is worked on its own - nothing ever takes numbers from two of them ([ADR-0038](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0038-a-calculation-can-carry-several-metrics.md)). The head names each metric above its column. The unit stands again only where a result closes: the Result, an interim, the line that closes an open derivation. A number one metric does not have is `null`, and makes absent only what depends on it in that metric; the badge says which metric, the sentence the full reason. Where the figures leave the labels less room than they need, and less than 10 rem, each line puts them on a line beneath its label, still in their columns. The component measures this: it depends on the labels, the number of metrics and their digits, not on a breakpoint. #### Examples ##### Headcount and full-time equivalents `metrics` puts several numbers on every line, each in its own column; every `value` is then an object with a number per metric. ```tsx import { Calculation, Given, Sum } from "@umriss-ui/calculation"; import type { Metric } from "@umriss-ui/calculation"; /* 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 }, ]; const METRICS: Metric[] = [ { id: "heads", label: "Headcount", unit: "HC" }, { id: "fte", label: "Full-time equivalents", unit: "FTE", decimals: 1 }, ]; /* A full-time week is 40 hours. */ const staffOf = (team: "Web" | "Apps") => { const people = PEOPLE.filter((person) => person.team === team); return { heads: people.length, fte: people.reduce((sum, person) => sum + person.capacity, 0) / 40 }; }; export default function TwoTeams() { return ( ); } ``` ##### A number one metric does not have `null` for one metric makes absent only what depends on it in that metric, with the reason; the other metric sums on. ```tsx import { Calculation, Given, Sum } from "@umriss-ui/calculation"; import type { Metric } from "@umriss-ui/calculation"; const METRICS: Metric[] = [ { id: "heads", label: "Headcount", unit: "HC" }, { id: "fte", label: "Full-time equivalents", unit: "FTE", decimals: 1 }, ]; export default function NotRecorded() { return ( ); } ``` ##### Team, cost centre, business line A tree of sums, built from data: every cost centre folds to its total and opens into its teams, each metric summed on its own. ```tsx import { Calculation, Given, Sum } from "@umriss-ui/calculation"; import type { Metric } from "@umriss-ui/calculation"; /* Data from the controlling world, written out here so the example runs on its own. */ interface CostCentre { id: string; name: string; owner: string; /** The budget of one month. */ monthlyBudget: number; } const COST_CENTRES: readonly CostCentre[] = [ { id: "CC-1100", name: "Sales", owner: "Helen Marsh", monthlyBudget: 142_000 }, { id: "CC-1200", name: "Marketing", owner: "Rafael Ortiz", monthlyBudget: 68_000 }, { id: "CC-2100", name: "Engineering", owner: "Anika Sørensen", monthlyBudget: 188_000 }, { id: "CC-2200", name: "Design", owner: "Paul Whitaker", monthlyBudget: 54_000 }, { id: "CC-3100", name: "Customer service", owner: "Grace Obi", monthlyBudget: 61_000 }, { id: "CC-4100", name: "Finance", owner: "Martina Vogel", monthlyBudget: 47_000 }, { id: "CC-4200", name: "People", owner: "Daniel Frost", monthlyBudget: 39_000 }, { id: "CC-4300", name: "IT", owner: "Kenji Arai", monthlyBudget: 83_000 }, { id: "CC-4400", name: "Facilities", owner: "Olga Ivanova", monthlyBudget: 72_000 }, ]; interface Team { /** The cost centre the team books to. */ costCentre: string; name: string; /** People employed, whatever their hours. */ headcount: number; /** Full-time equivalents: contracted hours over 40 a week. */ fte: number; /** Staff cost of a month, in thousand euros. */ monthlyCost: number; } /** Every team, on 17 March: 102 people, 91.5 full-time equivalents. */ const TEAMS: readonly Team[] = [ { costCentre: "CC-1100", name: "Field sales North", headcount: 9, fte: 8.6, monthlyCost: 61.2 }, { costCentre: "CC-1100", name: "Field sales South", headcount: 8, fte: 7.5, monthlyCost: 53.9 }, { costCentre: "CC-1100", name: "Inside sales", headcount: 6, fte: 5.2, monthlyCost: 29.8 }, { costCentre: "CC-1100", name: "Key accounts", headcount: 4, fte: 4.0, monthlyCost: 33.6 }, { costCentre: "CC-1200", name: "Brand", headcount: 3, fte: 2.8, monthlyCost: 19.4 }, { costCentre: "CC-1200", name: "Online marketing", headcount: 4, fte: 3.5, monthlyCost: 24.1 }, { costCentre: "CC-1200", name: "Trade fairs", headcount: 2, fte: 1.5, monthlyCost: 10.2 }, { costCentre: "CC-3100", name: "Service desk", headcount: 7, fte: 5.9, monthlyCost: 31.3 }, { costCentre: "CC-3100", name: "Returns", headcount: 3, fte: 2.5, monthlyCost: 12.6 }, { costCentre: "CC-2100", name: "Seating", headcount: 6, fte: 5.8, monthlyCost: 43.5 }, { costCentre: "CC-2100", name: "Desks", headcount: 7, fte: 6.5, monthlyCost: 48.1 }, { costCentre: "CC-2100", name: "Storage", headcount: 5, fte: 4.6, monthlyCost: 33.9 }, { costCentre: "CC-2100", name: "Test lab", headcount: 3, fte: 2.5, monthlyCost: 16.8 }, { costCentre: "CC-2200", name: "Industrial design", headcount: 4, fte: 3.6, monthlyCost: 27.2 }, { costCentre: "CC-2200", name: "Colour and material", headcount: 2, fte: 1.8, monthlyCost: 12.9 }, { costCentre: "CC-4100", name: "Accounting", headcount: 5, fte: 4.4, monthlyCost: 27.5 }, { costCentre: "CC-4100", name: "Controlling", headcount: 3, fte: 3.0, monthlyCost: 21.3 }, { costCentre: "CC-4200", name: "Recruiting", headcount: 2, fte: 1.8, monthlyCost: 12.4 }, { costCentre: "CC-4200", name: "Payroll", headcount: 2, fte: 1.5, monthlyCost: 9.6 }, { costCentre: "CC-4300", name: "Workplace", headcount: 4, fte: 3.8, monthlyCost: 24.2 }, { costCentre: "CC-4300", name: "Applications", headcount: 5, fte: 4.5, monthlyCost: 34.8 }, { costCentre: "CC-4400", name: "Buildings", headcount: 3, fte: 3.0, monthlyCost: 16.9 }, { costCentre: "CC-4400", name: "Canteen", headcount: 5, fte: 3.2, monthlyCost: 14.7 }, ]; const METRICS: Metric[] = [ { id: "heads", label: "Headcount", unit: "HC" }, { id: "fte", label: "Full-time equivalents", unit: "FTE", decimals: 1 }, ]; const COMMERCIAL = ["CC-1100", "CC-1200", "CC-3100"]; export default function BusinessLine() { return ( {COMMERCIAL.map((id) => ( centre.id === id)!.name}> {TEAMS.filter((team) => team.costCentre === id).map((team) => ( ))} ))} ); } ``` ##### Staff movement as a chain The chain works per metric too: joiners added, leavers taken away, and every interim closes with its units. ```tsx import { Calculation, Chain, Given, Interim, Minus, Plus } from "@umriss-ui/calculation"; import type { Metric } from "@umriss-ui/calculation"; const METRICS: Metric[] = [ { id: "heads", label: "Headcount", unit: "HC" }, { id: "fte", label: "Full-time equivalents", unit: "FTE", decimals: 1 }, ]; export default function StaffMovement() { return ( ); } ``` ##### A third metric: staff cost Any number of metrics, each with its unit and places. Narrow, where the figures leave the label too little room, each line puts them beneath its label. ```tsx import { Calculation, Given, Sum } from "@umriss-ui/calculation"; import type { Metric } from "@umriss-ui/calculation"; /* Data from the controlling world, written out here so the example runs on its own. */ interface CostCentre { id: string; name: string; owner: string; /** The budget of one month. */ monthlyBudget: number; } const COST_CENTRES: readonly CostCentre[] = [ { id: "CC-1100", name: "Sales", owner: "Helen Marsh", monthlyBudget: 142_000 }, { id: "CC-1200", name: "Marketing", owner: "Rafael Ortiz", monthlyBudget: 68_000 }, { id: "CC-2100", name: "Engineering", owner: "Anika Sørensen", monthlyBudget: 188_000 }, { id: "CC-2200", name: "Design", owner: "Paul Whitaker", monthlyBudget: 54_000 }, { id: "CC-3100", name: "Customer service", owner: "Grace Obi", monthlyBudget: 61_000 }, { id: "CC-4100", name: "Finance", owner: "Martina Vogel", monthlyBudget: 47_000 }, { id: "CC-4200", name: "People", owner: "Daniel Frost", monthlyBudget: 39_000 }, { id: "CC-4300", name: "IT", owner: "Kenji Arai", monthlyBudget: 83_000 }, { id: "CC-4400", name: "Facilities", owner: "Olga Ivanova", monthlyBudget: 72_000 }, ]; interface Team { /** The cost centre the team books to. */ costCentre: string; name: string; /** People employed, whatever their hours. */ headcount: number; /** Full-time equivalents: contracted hours over 40 a week. */ fte: number; /** Staff cost of a month, in thousand euros. */ monthlyCost: number; } /** Every team, on 17 March: 102 people, 91.5 full-time equivalents. */ const TEAMS: readonly Team[] = [ { costCentre: "CC-1100", name: "Field sales North", headcount: 9, fte: 8.6, monthlyCost: 61.2 }, { costCentre: "CC-1100", name: "Field sales South", headcount: 8, fte: 7.5, monthlyCost: 53.9 }, { costCentre: "CC-1100", name: "Inside sales", headcount: 6, fte: 5.2, monthlyCost: 29.8 }, { costCentre: "CC-1100", name: "Key accounts", headcount: 4, fte: 4.0, monthlyCost: 33.6 }, { costCentre: "CC-1200", name: "Brand", headcount: 3, fte: 2.8, monthlyCost: 19.4 }, { costCentre: "CC-1200", name: "Online marketing", headcount: 4, fte: 3.5, monthlyCost: 24.1 }, { costCentre: "CC-1200", name: "Trade fairs", headcount: 2, fte: 1.5, monthlyCost: 10.2 }, { costCentre: "CC-3100", name: "Service desk", headcount: 7, fte: 5.9, monthlyCost: 31.3 }, { costCentre: "CC-3100", name: "Returns", headcount: 3, fte: 2.5, monthlyCost: 12.6 }, { costCentre: "CC-2100", name: "Seating", headcount: 6, fte: 5.8, monthlyCost: 43.5 }, { costCentre: "CC-2100", name: "Desks", headcount: 7, fte: 6.5, monthlyCost: 48.1 }, { costCentre: "CC-2100", name: "Storage", headcount: 5, fte: 4.6, monthlyCost: 33.9 }, { costCentre: "CC-2100", name: "Test lab", headcount: 3, fte: 2.5, monthlyCost: 16.8 }, { costCentre: "CC-2200", name: "Industrial design", headcount: 4, fte: 3.6, monthlyCost: 27.2 }, { costCentre: "CC-2200", name: "Colour and material", headcount: 2, fte: 1.8, monthlyCost: 12.9 }, { costCentre: "CC-4100", name: "Accounting", headcount: 5, fte: 4.4, monthlyCost: 27.5 }, { costCentre: "CC-4100", name: "Controlling", headcount: 3, fte: 3.0, monthlyCost: 21.3 }, { costCentre: "CC-4200", name: "Recruiting", headcount: 2, fte: 1.8, monthlyCost: 12.4 }, { costCentre: "CC-4200", name: "Payroll", headcount: 2, fte: 1.5, monthlyCost: 9.6 }, { costCentre: "CC-4300", name: "Workplace", headcount: 4, fte: 3.8, monthlyCost: 24.2 }, { costCentre: "CC-4300", name: "Applications", headcount: 5, fte: 4.5, monthlyCost: 34.8 }, { costCentre: "CC-4400", name: "Buildings", headcount: 3, fte: 3.0, monthlyCost: 16.9 }, { costCentre: "CC-4400", name: "Canteen", headcount: 5, fte: 3.2, monthlyCost: 14.7 }, ]; const METRICS: Metric[] = [ { id: "heads", label: "Headcount", unit: "HC" }, { id: "fte", label: "Full-time equivalents", unit: "FTE", decimals: 1 }, { id: "cost", label: "Staff cost a month", unit: "k€", decimals: 1 }, ]; const PRODUCT = ["CC-2100", "CC-2200"]; export default function StaffCost() { return ( {PRODUCT.map((id) => ( centre.id === id)!.name}> {TEAMS.filter((team) => team.costCentre === id).map((team) => ( ))} ))} ); } ``` ##### The whole company Three levels from data - business line, cost centre, team - and three metrics: 23 teams folded to three lines a reader opens one by one. ```tsx import { Calculation, Given, Sum } from "@umriss-ui/calculation"; import type { Metric } from "@umriss-ui/calculation"; /* Data from the controlling world, written out here so the example runs on its own. */ interface CostCentre { id: string; name: string; owner: string; /** The budget of one month. */ monthlyBudget: number; } const COST_CENTRES: readonly CostCentre[] = [ { id: "CC-1100", name: "Sales", owner: "Helen Marsh", monthlyBudget: 142_000 }, { id: "CC-1200", name: "Marketing", owner: "Rafael Ortiz", monthlyBudget: 68_000 }, { id: "CC-2100", name: "Engineering", owner: "Anika Sørensen", monthlyBudget: 188_000 }, { id: "CC-2200", name: "Design", owner: "Paul Whitaker", monthlyBudget: 54_000 }, { id: "CC-3100", name: "Customer service", owner: "Grace Obi", monthlyBudget: 61_000 }, { id: "CC-4100", name: "Finance", owner: "Martina Vogel", monthlyBudget: 47_000 }, { id: "CC-4200", name: "People", owner: "Daniel Frost", monthlyBudget: 39_000 }, { id: "CC-4300", name: "IT", owner: "Kenji Arai", monthlyBudget: 83_000 }, { id: "CC-4400", name: "Facilities", owner: "Olga Ivanova", monthlyBudget: 72_000 }, ]; interface Team { /** The cost centre the team books to. */ costCentre: string; name: string; /** People employed, whatever their hours. */ headcount: number; /** Full-time equivalents: contracted hours over 40 a week. */ fte: number; /** Staff cost of a month, in thousand euros. */ monthlyCost: number; } /** Every team, on 17 March: 102 people, 91.5 full-time equivalents. */ const TEAMS: readonly Team[] = [ { costCentre: "CC-1100", name: "Field sales North", headcount: 9, fte: 8.6, monthlyCost: 61.2 }, { costCentre: "CC-1100", name: "Field sales South", headcount: 8, fte: 7.5, monthlyCost: 53.9 }, { costCentre: "CC-1100", name: "Inside sales", headcount: 6, fte: 5.2, monthlyCost: 29.8 }, { costCentre: "CC-1100", name: "Key accounts", headcount: 4, fte: 4.0, monthlyCost: 33.6 }, { costCentre: "CC-1200", name: "Brand", headcount: 3, fte: 2.8, monthlyCost: 19.4 }, { costCentre: "CC-1200", name: "Online marketing", headcount: 4, fte: 3.5, monthlyCost: 24.1 }, { costCentre: "CC-1200", name: "Trade fairs", headcount: 2, fte: 1.5, monthlyCost: 10.2 }, { costCentre: "CC-3100", name: "Service desk", headcount: 7, fte: 5.9, monthlyCost: 31.3 }, { costCentre: "CC-3100", name: "Returns", headcount: 3, fte: 2.5, monthlyCost: 12.6 }, { costCentre: "CC-2100", name: "Seating", headcount: 6, fte: 5.8, monthlyCost: 43.5 }, { costCentre: "CC-2100", name: "Desks", headcount: 7, fte: 6.5, monthlyCost: 48.1 }, { costCentre: "CC-2100", name: "Storage", headcount: 5, fte: 4.6, monthlyCost: 33.9 }, { costCentre: "CC-2100", name: "Test lab", headcount: 3, fte: 2.5, monthlyCost: 16.8 }, { costCentre: "CC-2200", name: "Industrial design", headcount: 4, fte: 3.6, monthlyCost: 27.2 }, { costCentre: "CC-2200", name: "Colour and material", headcount: 2, fte: 1.8, monthlyCost: 12.9 }, { costCentre: "CC-4100", name: "Accounting", headcount: 5, fte: 4.4, monthlyCost: 27.5 }, { costCentre: "CC-4100", name: "Controlling", headcount: 3, fte: 3.0, monthlyCost: 21.3 }, { costCentre: "CC-4200", name: "Recruiting", headcount: 2, fte: 1.8, monthlyCost: 12.4 }, { costCentre: "CC-4200", name: "Payroll", headcount: 2, fte: 1.5, monthlyCost: 9.6 }, { costCentre: "CC-4300", name: "Workplace", headcount: 4, fte: 3.8, monthlyCost: 24.2 }, { costCentre: "CC-4300", name: "Applications", headcount: 5, fte: 4.5, monthlyCost: 34.8 }, { costCentre: "CC-4400", name: "Buildings", headcount: 3, fte: 3.0, monthlyCost: 16.9 }, { costCentre: "CC-4400", name: "Canteen", headcount: 5, fte: 3.2, monthlyCost: 14.7 }, ]; interface BusinessLine { name: string; /** Its cost centres, in the order a report lists them. */ costCentres: readonly string[]; } /** How the company reports: three business lines over the nine cost centres. */ const BUSINESS_LINES: readonly BusinessLine[] = [ { name: "Commercial", costCentres: ["CC-1100", "CC-1200", "CC-3100"] }, { name: "Product", costCentres: ["CC-2100", "CC-2200"] }, { name: "Corporate services", costCentres: ["CC-4100", "CC-4200", "CC-4300", "CC-4400"] }, ]; const METRICS: Metric[] = [ { id: "heads", label: "Headcount", unit: "HC" }, { id: "fte", label: "Full-time equivalents", unit: "FTE", decimals: 1 }, { id: "cost", label: "Staff cost a month", unit: "k€", decimals: 1 }, ]; const centreName = (id: string) => COST_CENTRES.find((centre) => centre.id === id)!.name; export default function WholeCompany() { return ( {BUSINESS_LINES.map((line) => ( {line.costCentres.map((id) => ( {TEAMS.filter((team) => team.costCentre === id).map((team) => ( ))} ))} ))} ); } ``` ##### A dense staff report `density="compact"` closes up the head and every metric's column with the lines; a note stays beneath its label, and the figures move beneath the labels by the same measure as in a regular report. ```tsx import { Calculation, Given, Sum } from "@umriss-ui/calculation"; import type { Metric } from "@umriss-ui/calculation"; const METRICS: Metric[] = [ { id: "heads", label: "Headcount", unit: "HC" }, { id: "fte", label: "Full-time equivalents", unit: "FTE", decimals: 1 }, ]; export default function CompactReport() { return ( ); } ``` #### When to use something else - A ratio across two metrics, such as FTE per head → a calculation of its own, a `Quotient` of two givens - Many figures per row, sorted and filtered → `Table` from @umriss-ui/table #### Keyboard The keys of [Calculation](#/calculation/keyboard-calculation) apply here. #### API ##### `Metric` | Prop | Type | Default | Description | |---|---|---|---| | `id` *required* | `string` | — | The key of this metric's number in every `value` object.
Shown in: [Headcount and full-time equivalents](#/metrics/two-teams), [A number one metric does not have](#/metrics/not-recorded), [Team, cost centre, business line](#/metrics/business-line) and 5 more | | `label` *required* | `string` | — | The metric's name, in the head above its column and in each row's sentence.
Shown in: [Headcount and full-time equivalents](#/metrics/two-teams), [A number one metric does not have](#/metrics/not-recorded), [Team, cost centre, business line](#/metrics/business-line) and 5 more | | `unit` | `string` | — | A label written after the number where a result closes, and beneath the metric's name in the head. Never converted and never checked.
Shown in: [Headcount and full-time equivalents](#/metrics/two-teams), [A number one metric does not have](#/metrics/not-recorded), [Team, cost centre, business line](#/metrics/business-line) and 5 more | | `decimals` | `number` | a given as it was given, at most two for a derived number | Fixed number of fraction digits as shown, for every quantity of this metric.
Shown in: [Headcount and full-time equivalents](#/metrics/two-teams), [A number one metric does not have](#/metrics/not-recorded), [Team, cost centre, business line](#/metrics/business-line) and 5 more | #### Known limits - A calculation with metrics only adds and subtracts: `Product`, `Quotient`, `Times` and `DividedBy` fail on the first render, since a factor would need a unit in every metric. - Unit, format and places belong to the metric; on a quantity they fail on the first render, and so does a missing or unknown metric key in a `value`. - No target or limits with metrics: an assessment per cell is not there yet. What umriss deliberately does not build, and why: [ADR-0032](https://romanhaendler.github.io/umriss-ui/what-umriss-ui-is-not/). ## In practice What a calculation does when the numbers are not what they should be, and whole figures worked in full. ### What can go wrong A number not there yet, a division by zero, rounded figures that do not add up, and a limit crossed inside a folded line: what a calculation shows in each case. ```ts import { Calculation } from "@umriss-ui/calculation"; ``` Demo page: https://romanhaendler.github.io/umriss-ui/calculation/what-can-go-wrong/ An absent given is never zero: every quantity that depends on it is absent too, with the reason, up to the result. A quotient by zero is absent with its own reason. #### Examples ##### A number that is not there yet A given with `value={null}` shows as missing, and so does every figure that depends on it – never carried on as zero. ```tsx import { Calculation, Given, Quotient, Sum } from "@umriss-ui/calculation"; const BOOKED = [ { name: "Luis Moreno", hours: 34 }, { name: "Hana Sato", hours: 38 }, { name: "Kofi Mensah", hours: null }, { name: "Freya Olsen", hours: 36 }, { name: "David Kowalski", hours: 17 }, ]; export default function Missing() { return ( {BOOKED.map((person) => ( ))} ); } ``` ##### A division by zero A tour that delivered nothing has no cost per parcel – not zero, not infinity; the quotient is absent with its own reason. ```tsx import { Calculation, Given, Product, Quotient, Sum } from "@umriss-ui/calculation"; export default function DivisionByZero() { return ( ); } ``` ##### Rounded figures that do not add up Operations use full values and only the screen rounds; where the rounded operands miss the rounded result, the line says so with ≈. ```tsx import { Calculation, Given, Quotient, Sum } from "@umriss-ui/calculation"; const PEOPLE = ["Arjun Mehta", "Chloe Durand", "Eva Novak"]; export default function Rounding() { return ( {PEOPLE.map((person) => ( ))} ); } ``` ##### A limit violated deep inside A folded line holding a worse verdict says so quietly – without taking the colour itself, since its own number is not the one in alarm. ```tsx import { Calculation, Chain, Given, Interim, Plus, Product } from "@umriss-ui/calculation"; const ERROR_LIMITS = [ { value: 0.01, side: "upper" as const, severity: "warning" as const }, { value: 0.02, side: "upper" as const, severity: "alarm" as const }, ]; export default function LimitInside() { return ( ); } ``` #### Keyboard The keys of [Calculation](#/calculation/keyboard-calculation) apply here. #### Props on this page From `CalculationProps` — the full table stands on [Calculation](#/calculation/type-CalculationProps). | Prop | Type | Default | Description | |---|---|---|---| | [`children`](#/calculation/CalculationProps-children) *required* | `ReactNode` | — | The result: exactly one quantity - a tree (``, ``, ``, `` with their operands, `` as leaves) or a ``, the two mixing freely; `` for a quantity defined elsewhere. | From `OperatorProps` — the full table stands on [Tree](#/tree/type-OperatorProps). | Prop | Type | Default | Description | |---|---|---|---| | [`children`](#/tree/OperatorProps-children) | `ReactNode` | — | The operands, in order: quantities, givens and references. | | [`label`](#/tree/OperatorProps-label) *required* | `string` | — | What the quantity is called. It stands on its line, wherever it is an operand, and in the accessible sentence. From `QuantityProps`. | | [`unit`](#/tree/OperatorProps-unit) | `string` | — | A label written after the number - "min", "pcs", "€". Never converted and never checked; the calculation does no unit algebra. From `QuantityProps`. | | [`format`](#/tree/OperatorProps-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`. | | [`decimals`](#/tree/OperatorProps-decimals) | `number` | one digit for a percentage, at most two for a derived number, a given as it was given | Fixed number of fraction digits as shown. Rounding happens only in presentation; every operation uses the full value. From `QuantityProps`. | | [`target`](#/tree/OperatorProps-target) | `number` | — | The value this quantity is meant to reach - missed, never violated. From `QuantityProps`. | | [`limits`](#/tree/OperatorProps-limits) | `readonly `[`Limit`](#/tree/type-Limit)`[]` | — | Bounds the quantity is assessed against, through core's `assess()`. From `QuantityProps`. | From `ChainProps` — the full table stands on [Chain](#/chain/type-ChainProps). | Prop | Type | Default | Description | |---|---|---|---| | [`children`](#/chain/ChainProps-children) | `ReactNode` | — | The operands, top to bottom: a first quantity with no operator, then `Plus`, `Minus`, `Times` and `DividedBy` with their operands and `Interim`s 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](#/chain/type-ChainOperandProps). | Prop | Type | Default | Description | |---|---|---|---| | [`children`](#/chain/ChainOperandProps-children) | `ReactNode` | — | Instead of `label` and `value`: exactly one quantity - a tree, a `` or a ``. | | [`source`](#/chain/ChainOperandProps-source) | `string` | — | Where the number came from: a system, a report, a person. From `GivenProps`. | | [`unit`](#/chain/ChainOperandProps-unit) | `string` | — | A label written after the number - "min", "pcs", "€". Never converted and never checked; the calculation does no unit algebra. From `QuantityProps`. | | [`format`](#/chain/ChainOperandProps-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`. | | [`decimals`](#/chain/ChainOperandProps-decimals) | `number` | one digit for a percentage, at most two for a derived number, a given as it was given | Fixed number of fraction digits as shown. Rounding happens only in presentation; every operation uses the full value. From `QuantityProps`. | | [`target`](#/chain/ChainOperandProps-target) | `number` | — | The value this quantity is meant to reach - missed, never violated. From `QuantityProps`. | | [`limits`](#/chain/ChainOperandProps-limits) | `readonly `[`Limit`](#/chain/type-Limit)`[]` | — | Bounds the quantity is assessed against, through core's `assess()`. From `QuantityProps`. | From `QuantityProps` — the full table stands on [Chain](#/chain/type-QuantityProps). | Prop | Type | Default | Description | |---|---|---|---| | [`label`](#/chain/QuantityProps-label) *required* | `string` | — | What the quantity is called. It stands on its line, wherever it is an operand, and in the accessible sentence. | | [`unit`](#/chain/QuantityProps-unit) | `string` | — | A label written after the number - "min", "pcs", "€". Never converted and never checked; the calculation does no unit algebra. | | [`format`](#/chain/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. | | [`decimals`](#/chain/QuantityProps-decimals) | `number` | one digit for a percentage, at most two for a derived number, a given as it was given | Fixed number of fraction digits as shown. Rounding happens only in presentation; every operation uses the full value. | | [`target`](#/chain/QuantityProps-target) | `number` | — | The value this quantity is meant to reach - missed, never violated. | | [`limits`](#/chain/QuantityProps-limits) | `readonly `[`Limit`](#/chain/type-Limit)`[]` | — | Bounds the quantity is assessed against, through core's `assess()`. | From `GivenProps` — the full table stands on [Given](#/given/type-GivenProps). | Prop | Type | Default | Description | |---|---|---|---| | [`value`](#/given/GivenProps-value) *required* | `number \| null \| undefined \| `[`MetricValues`](#/given/type-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. | | [`source`](#/given/GivenProps-source) | `string` | — | Where the number came from: a system, a report, a person. | | [`label`](#/given/GivenProps-label) *required* | `string` | — | What the quantity is called. It stands on its line, wherever it is an operand, and in the accessible sentence. From `QuantityProps`. | | [`unit`](#/given/GivenProps-unit) | `string` | — | A label written after the number - "min", "pcs", "€". Never converted and never checked; the calculation does no unit algebra. From `QuantityProps`. | | [`format`](#/given/GivenProps-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`. | | [`decimals`](#/given/GivenProps-decimals) | `number` | one digit for a percentage, at most two for a derived number, a given as it was given | Fixed number of fraction digits as shown. Rounding happens only in presentation; every operation uses the full value. From `QuantityProps`. | | [`target`](#/given/GivenProps-target) | `number` | — | The value this quantity is meant to reach - missed, never violated. From `QuantityProps`. | | [`limits`](#/given/GivenProps-limits) | `readonly `[`Limit`](#/given/type-Limit)`[]` | — | Bounds the quantity is assessed against, through core's `assess()`. From `QuantityProps`. | ### 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. ```ts import { Calculation, Chain, Given, Ref } from "@umriss-ui/calculation"; ``` Demo page: https://romanhaendler.github.io/umriss-ui/calculation/worked-examples/ #### 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. ```tsx 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 ( {TOURS.map((tour) => ( ))} {DIESEL.map((tour) => ( ))} {ELECTRIC.map((tour) => ( ))} {TOURS.map((tour) => ( ))} ); } ``` ##### 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. ```tsx 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] : ( {operands} ); function invoiceChain(invoice: Invoice) { const lineId = (index: number) => `${invoice.id}-line-${index}`; const rates = [...new Set(invoice.lines.map((line) => line.vatRate))]; return ( {sumOf( `Net, ${invoice.id}`, invoice.lines.map((line, index) => { const amount = ( ); if (line.discount === 0) return amount; return ( {amount} ); }), )} {sumOf( `VAT, ${invoice.id}`, rates.map((rate) => { const lines = invoice.lines.flatMap((line, index) => (line.vatRate === rate ? [] : [])); return ( {sumOf(`Net at ${percent(rate)}, ${invoice.id}`, lines)} ); }), )} ); } export default function Invoices() { return ( {OPEN.map(invoiceChain)} ); } ``` ##### 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. ```tsx 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 ( ); } export default function HallOee() { return ( {LINES.map(lineOee)} {LINES.map((line) => ( ))} ); } ``` #### Keyboard The keys of [Calculation](#/calculation/keyboard-calculation) apply here. #### Props on this page From `CalculationProps` — the full table stands on [Calculation](#/calculation/type-CalculationProps). | Prop | Type | Default | Description | |---|---|---|---| | [`children`](#/calculation/CalculationProps-children) *required* | `ReactNode` | — | The result: exactly one quantity - a tree (``, ``, ``, `` with their operands, `` as leaves) or a ``, the two mixing freely; `` for a quantity defined elsewhere. | From `OperatorProps` — the full table stands on [Tree](#/tree/type-OperatorProps). | Prop | Type | Default | Description | |---|---|---|---| | [`children`](#/tree/OperatorProps-children) | `ReactNode` | — | The operands, in order: quantities, givens and references. | | [`label`](#/tree/OperatorProps-label) *required* | `string` | — | What the quantity is called. It stands on its line, wherever it is an operand, and in the accessible sentence. From `QuantityProps`. | | [`id`](#/tree/OperatorProps-id) | `string` | — | Makes the quantity referable: `` stands for it elsewhere. Needed only for a quantity used more than once. From `QuantityProps`. | | [`unit`](#/tree/OperatorProps-unit) | `string` | — | A label written after the number - "min", "pcs", "€". Never converted and never checked; the calculation does no unit algebra. From `QuantityProps`. | | [`format`](#/tree/OperatorProps-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`. | | [`decimals`](#/tree/OperatorProps-decimals) | `number` | one digit for a percentage, at most two for a derived number, a given as it was given | Fixed number of fraction digits as shown. Rounding happens only in presentation; every operation uses the full value. From `QuantityProps`. | | [`target`](#/tree/OperatorProps-target) | `number` | — | The value this quantity is meant to reach - missed, never violated. From `QuantityProps`. | | [`limits`](#/tree/OperatorProps-limits) | `readonly `[`Limit`](#/tree/type-Limit)`[]` | — | Bounds the quantity is assessed against, through core's `assess()`. From `QuantityProps`. | From `RefProps` — the full table stands on [Tree](#/tree/type-RefProps). | Prop | Type | Default | Description | |---|---|---|---| | [`to`](#/tree/RefProps-to) *required* | `string` | — | The `id` of the quantity this reference stands for. | From `ChainProps` — the full table stands on [Chain](#/chain/type-ChainProps). | Prop | Type | Default | Description | |---|---|---|---| | [`children`](#/chain/ChainProps-children) | `ReactNode` | — | The operands, top to bottom: a first quantity with no operator, then `Plus`, `Minus`, `Times` and `DividedBy` with their operands and `Interim`s 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](#/chain/type-ChainOperandProps). | Prop | Type | Default | Description | |---|---|---|---| | [`label`](#/chain/ChainOperandProps-label) | `string` | — | 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`](#/chain/ChainOperandProps-value) | `number \| null \| `[`MetricValues`](#/chain/type-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`. | | [`children`](#/chain/ChainOperandProps-children) | `ReactNode` | — | Instead of `label` and `value`: exactly one quantity - a tree, a `` or a ``. | | [`source`](#/chain/ChainOperandProps-source) | `string` | — | Where the number came from: a system, a report, a person. From `GivenProps`. | | [`asOf`](#/chain/ChainOperandProps-asOf) | `Date \| number` | — | When the number was true - not when it was fetched. From `GivenProps`. | | [`ages`](#/chain/ChainOperandProps-ages) | [`FreshnessAges`](#/chain/type-FreshnessAges) | — | 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`. | | [`id`](#/chain/ChainOperandProps-id) | `string` | — | Makes the quantity referable: `` stands for it elsewhere. Needed only for a quantity used more than once. From `QuantityProps`. | | [`unit`](#/chain/ChainOperandProps-unit) | `string` | — | A label written after the number - "min", "pcs", "€". Never converted and never checked; the calculation does no unit algebra. From `QuantityProps`. | | [`format`](#/chain/ChainOperandProps-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`. | | [`decimals`](#/chain/ChainOperandProps-decimals) | `number` | one digit for a percentage, at most two for a derived number, a given as it was given | Fixed number of fraction digits as shown. Rounding happens only in presentation; every operation uses the full value. From `QuantityProps`. | | [`target`](#/chain/ChainOperandProps-target) | `number` | — | The value this quantity is meant to reach - missed, never violated. From `QuantityProps`. | | [`limits`](#/chain/ChainOperandProps-limits) | `readonly `[`Limit`](#/chain/type-Limit)`[]` | — | Bounds the quantity is assessed against, through core's `assess()`. From `QuantityProps`. | From `QuantityProps` — the full table stands on [Chain](#/chain/type-QuantityProps). | Prop | Type | Default | Description | |---|---|---|---| | [`label`](#/chain/QuantityProps-label) *required* | `string` | — | What the quantity is called. It stands on its line, wherever it is an operand, and in the accessible sentence. | | [`id`](#/chain/QuantityProps-id) | `string` | — | Makes the quantity referable: `` stands for it elsewhere. Needed only for a quantity used more than once. | | [`unit`](#/chain/QuantityProps-unit) | `string` | — | A label written after the number - "min", "pcs", "€". Never converted and never checked; the calculation does no unit algebra. | | [`format`](#/chain/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. | | [`decimals`](#/chain/QuantityProps-decimals) | `number` | one digit for a percentage, at most two for a derived number, a given as it was given | Fixed number of fraction digits as shown. Rounding happens only in presentation; every operation uses the full value. | | [`target`](#/chain/QuantityProps-target) | `number` | — | The value this quantity is meant to reach - missed, never violated. | | [`limits`](#/chain/QuantityProps-limits) | `readonly `[`Limit`](#/chain/type-Limit)`[]` | — | Bounds the quantity is assessed against, through core's `assess()`. | From `GivenProps` — the full table stands on [Given](#/given/type-GivenProps). | Prop | Type | Default | Description | |---|---|---|---| | [`value`](#/given/GivenProps-value) *required* | `number \| null \| undefined \| `[`MetricValues`](#/given/type-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. | | [`source`](#/given/GivenProps-source) | `string` | — | Where the number came from: a system, a report, a person. | | [`asOf`](#/given/GivenProps-asOf) | `Date \| number` | — | When the number was true - not when it was fetched. | | [`ages`](#/given/GivenProps-ages) | [`FreshnessAges`](#/given/type-FreshnessAges) | — | The ages at which `asOf` turns stale and lost. With both, the given carries a freshness through core; with `asOf` alone, only the time. | | [`label`](#/given/GivenProps-label) *required* | `string` | — | What the quantity is called. It stands on its line, wherever it is an operand, and in the accessible sentence. From `QuantityProps`. | | [`id`](#/given/GivenProps-id) | `string` | — | Makes the quantity referable: `` stands for it elsewhere. Needed only for a quantity used more than once. From `QuantityProps`. | | [`unit`](#/given/GivenProps-unit) | `string` | — | A label written after the number - "min", "pcs", "€". Never converted and never checked; the calculation does no unit algebra. From `QuantityProps`. | | [`format`](#/given/GivenProps-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`. | | [`decimals`](#/given/GivenProps-decimals) | `number` | one digit for a percentage, at most two for a derived number, a given as it was given | Fixed number of fraction digits as shown. Rounding happens only in presentation; every operation uses the full value. From `QuantityProps`. | | [`target`](#/given/GivenProps-target) | `number` | — | The value this quantity is meant to reach - missed, never violated. From `QuantityProps`. | | [`limits`](#/given/GivenProps-limits) | `readonly `[`Limit`](#/given/type-Limit)`[]` | — | Bounds the quantity is assessed against, through core's `assess()`. From `QuantityProps`. | ## API index Every name the package exports, in one place. ### API index Everything `@umriss-ui/calculation` exports, with its signature. A component's props stand on its own page. Demo page: https://romanhaendler.github.io/umriss-ui/calculation/api/ #### Components ##### `Calculation` [Calculation](#/calculation) – Lets a reader follow and redo a figure (also called a KPI tree or calculation tree): each line a quantity with its operator, derived lines folded until opened beneath themselves. Reach for it where a number on the screen gets questioned: a cost, an availability, an invoice total. Used on [Scenarios](#/), [Installation](#/installation), [Calculation](#/calculation), [Tree](#/tree), [Chain](#/chain), [Rows from data](#/rows-from-data), [Given](#/given), [Metrics](#/metrics), [What can go wrong](#/what-can-go-wrong) and [Worked examples](#/worked-examples). ##### `Chain` [Chain](#/chain) – A sheet read top to bottom (a running calculation): each line worked into the value before it, and interims naming the value where they stand. Reach for it for invoices, budgets and cost sheets. Used on [Calculation](#/calculation), [Chain](#/chain), [Rows from data](#/rows-from-data), [Given](#/given), [Metrics](#/metrics), [What can go wrong](#/what-can-go-wrong) and [Worked examples](#/worked-examples). ##### `Difference` The first operand minus every other: a − b − c. ```ts const Difference: (props: OperatorProps) => null; ``` Used on [Scenarios](#/), [Installation](#/installation), [Calculation](#/calculation), [Tree](#/tree), [Rows from data](#/rows-from-data) and [Worked examples](#/worked-examples). ##### `DividedBy` Divides the named value before it by its operand. ```ts const DividedBy: (props: ChainOperandProps) => null; ``` Used on [Chain](#/chain), [Metrics](#/metrics) and [Worked examples](#/worked-examples). ##### `Given` [Given](#/given) – The numbers a calculation starts from (inputs, leaves): where they came from, when they were true, and whether they are still fresh. Used on [Scenarios](#/), [Installation](#/installation), [Calculation](#/calculation), [Tree](#/tree), [Chain](#/chain), [Rows from data](#/rows-from-data), [Given](#/given), [Metrics](#/metrics), [What can go wrong](#/what-can-go-wrong) and [Worked examples](#/worked-examples). ##### `Interim` Names the value of a chain where it stands. ```ts const Interim: (props: QuantityProps) => null; ``` Used on [Calculation](#/calculation), [Chain](#/chain), [Rows from data](#/rows-from-data), [Given](#/given), [Metrics](#/metrics), [What can go wrong](#/what-can-go-wrong) and [Worked examples](#/worked-examples). ##### `Minus` Takes its operand from the value before it. ```ts const Minus: (props: ChainOperandProps) => null; ``` Used on [Calculation](#/calculation), [Chain](#/chain), [Rows from data](#/rows-from-data), [Given](#/given) and [Metrics](#/metrics). ##### `Plus` Adds its operand to the value before it. ```ts const Plus: (props: ChainOperandProps) => null; ``` Used on [Calculation](#/calculation), [Chain](#/chain), [Rows from data](#/rows-from-data), [Metrics](#/metrics), [What can go wrong](#/what-can-go-wrong) and [Worked examples](#/worked-examples). ##### `Product` The product of two or more operands. ```ts const Product: (props: OperatorProps) => null; ``` Used on [Scenarios](#/), [Calculation](#/calculation), [Tree](#/tree), [Chain](#/chain), [Rows from data](#/rows-from-data), [Given](#/given), [Metrics](#/metrics), [What can go wrong](#/what-can-go-wrong) and [Worked examples](#/worked-examples). ##### `Quotient` Exactly two operands: the first divided by the second. ```ts const Quotient: (props: OperatorProps) => null; ``` Used on [Scenarios](#/), [Installation](#/installation), [Calculation](#/calculation), [Tree](#/tree), [Chain](#/chain), [Given](#/given), [Metrics](#/metrics), [What can go wrong](#/what-can-go-wrong) and [Worked examples](#/worked-examples). ##### `Ref` [Tree](#/tree) – A quantity defined elsewhere in the calculation, standing as an operand. Used on [Scenarios](#/), [Installation](#/installation), [Calculation](#/calculation), [Tree](#/tree), [Chain](#/chain), [Rows from data](#/rows-from-data), [Given](#/given) and [Worked examples](#/worked-examples). ##### `Sum` The sum of its operands, however many: rows from data, one or none included. The sum of none is zero, and its line says it has no entries. ```ts const Sum: (props: OperatorProps) => null; ``` Used on [Scenarios](#/), [Calculation](#/calculation), [Tree](#/tree), [Chain](#/chain), [Rows from data](#/rows-from-data), [Metrics](#/metrics), [What can go wrong](#/what-can-go-wrong) and [Worked examples](#/worked-examples). ##### `Times` Multiplies the named value before it by its operand. ```ts const Times: (props: ChainOperandProps) => null; ``` Used on [Calculation](#/calculation), [Chain](#/chain) and [Metrics](#/metrics). #### Types ##### `CalculationProps` Its table stands on [Calculation](#/calculation/type-CalculationProps). ##### `ChainOperandProps` Its table stands on [Chain](#/chain/type-ChainOperandProps). ##### `ChainProps` Its table stands on [Chain](#/chain/type-ChainProps). ##### `GivenProps` Its table stands on [Given](#/given/type-GivenProps). ##### `Metric` Its table stands on [Metrics](#/metrics/type-Metric). Used on [Rows from data](#/rows-from-data) and [Metrics](#/metrics). ##### `MetricValues` A given's numbers where the calculation has metrics: one per metric id, `null` where the number is absent. ```ts type MetricValues = Readonly>; ``` ##### `OperatorProps` Its table stands on [Tree](#/tree/type-OperatorProps). ##### `QuantityProps` Its table stands on [Chain](#/chain/type-QuantityProps). ##### `RefProps` Its table stands on [Tree](#/tree/type-RefProps).