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.

import { Chain, Plus, Minus, Times, DividedBy, Interim } from "@umriss-ui/calculation";

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

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.

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

export default function NetWeight() {
  return (
    <Calculation aria-label="Net weight, shipment FP-1004223">
      <Chain>
        <Given label="Gross weight" value={1250} unit="kg" decimals={1} />
        <Minus label="Pallet" value={25} unit="kg" decimals={1} />
        <Minus label="Crates" value={144} unit="kg" decimals={1} />
        <Minus label="Packaging" value={11.5} unit="kg" decimals={1} />
        <Interim label="Net weight" unit="kg" decimals={1} />
      </Chain>
    </Calculation>
  );
}

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.

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

export default function TourCost() {
  return (
    <Calculation aria-label="Cost of tour T-01, Tuesday">
      <Chain>
        <Given label="Driver, 6.5 h" value={224.25} unit="€" decimals={2} />
        <Plus label="Diesel" value={13.89} unit="€" decimals={2} />
        <Plus label="Van, one day" value={58} unit="€" decimals={2} />
        <Plus label="Tolls" value={9.4} unit="€" decimals={2} />
        <Plus label="Parking" value={6} unit="€" decimals={2} />
        <Minus label="Fuel card rebate" value={0.7} unit="€" decimals={2} />
        <Interim label="Cost of the tour" unit="€" decimals={2} />
      </Chain>
    </Calculation>
  );
}

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.

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

export default function MarkUpAndVat() {
  return (
    <Calculation aria-label="Price of a workshop day">
      <Chain>
        <Given label="Two facilitators, one day" value={960} unit="€" decimals={2} />
        <Plus label="Room and materials" value={180} unit="€" decimals={2} />
        <Interim label="Cost of the day" unit="€" decimals={2} />
        <Times label="Mark-up" value={1.25} />
        <Interim label="Net price" unit="€" decimals={2} />
        <Times label="VAT factor" value={1.19} />
        <Interim label="Gross price" unit="€" decimals={2} />
      </Chain>
    </Calculation>
  );
}

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.

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 (
    <Calculation aria-label="Payslip, March">
      <Chain>
        <Given id="gross" label="Gross salary" value={4200} unit="€" decimals={2} />
        <Minus label="Income tax" value={612.5} unit="€" decimals={2} />
        <Minus label="Church tax" value={49} unit="€" decimals={2} />
        <Minus>
          <Sum label="Social security contributions" unit="€" decimals={2}>
            {CONTRIBUTIONS.map((contribution) => (
              <Product key={contribution.name} label={contribution.name} unit="€" decimals={2}>
                <Given label={`Rate, ${contribution.name.toLowerCase()}`} value={contribution.rate} format="percent" />
                <Ref to="gross" />
              </Product>
            ))}
          </Sum>
        </Minus>
        <Interim label="Net salary" unit="€" decimals={2} />
        <Minus label="Capital-forming benefits" value={40} unit="€" decimals={2} />
        <Plus label="Travel allowance" value={60} unit="€" decimals={2} />
        <Interim label="Amount paid out" unit="€" decimals={2} />
      </Chain>
    </Calculation>
  );
}

Build the lines from data

.map inside a chain makes each item a Plus, and all stand in view above their interim.

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 (
    <Calculation aria-label="Cloud bill, March">
      <Chain>
        <Given label={firstName} value={firstAmount} unit="€" decimals={2} />
        {rest.map(([name, amount]) => (
          <Plus key={name} label={name} value={amount} unit="€" decimals={2} />
        ))}
        <Interim label="Cloud bill, March" unit="€" decimals={2} />
      </Chain>
    </Calculation>
  );
}

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.

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

export default function ChainInATree() {
  return (
    <Calculation aria-label="Delivery rate, North depot, Tuesday">
      <Quotient label="Delivery rate" format="percent" target={0.97}>
        <Chain>
          <Given id="out" label="Parcels out" value={1000} unit="parcels" />
          <Minus label="Failed attempts" value={31} unit="parcels" />
          <Minus label="Refused at the door" value={6} unit="parcels" />
          <Plus label="Delivered on a second run" value={14} unit="parcels" />
          <Interim label="Parcels delivered" unit="parcels" />
        </Chain>
        <Ref to="out" />
      </Quotient>
    </Calculation>
  );
}

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.

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

export default function ErrorBudget() {
  return (
    <Calculation aria-label="Error budget of Checkout, March">
      <Chain>
        <Product label="Error budget, March" unit="min" decimals={1}>
          <Given label="Minutes in March" value={44640} unit="min" />
          <Given label="Allowed downtime" value={0.0005} format="percent" decimals={2} />
        </Product>
        <Minus label="INC-1039, payment provider timeouts" value={9} unit="min" decimals={1} />
        <Minus label="INC-1036, failed deploy" value={4} unit="min" decimals={1} />
        <Minus label="INC-1048, card payments time out" value={21} unit="min" decimals={1} />
        <Interim
          label="Budget left"
          unit="min"
          decimals={1}
          limits={[
            { value: 5, side: "lower", severity: "warning" },
            { value: 0, side: "lower", severity: "alarm" },
          ]}
        />
      </Chain>
    </Calculation>
  );
}

When to use something else

Keyboard

The keys of Calculation apply here.

API

ChainProps

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

Shown in: Take amounts away from a first quantity, Mix additions and deductions, Multiply between two interims and 22 more

ChainOperandProps

NameTypeDefaultDescription
labelstring—The operand's name, where the line is a given written in place. Left out where the line holds a quantity as its child.

Shown in: Take amounts away from a first quantity, Mix additions and deductions, Multiply between two interims and 17 more

value| number
| null
| MetricValues
—The operand's number, where the line is a given written in place. Absent as for a Given: the line and every interim after it are absent, with the reason. With metrics, an object by metric id, as on a Given.

Shown in: Take amounts away from a first quantity, Mix additions and deductions, Multiply between two interims and 17 more

childrenReactNode—Instead of label and value: exactly one quantity - a tree, a <Ref> or a <Chain>.

Shown in: Fold a group of lines into one, Write a chain (Calculation), Emphasis inside a group (Calculation) and 12 more

sourcestring—Where the number came from: a system, a report, a person. from GivenProps

Shown in: Say where a number came from (Given), State when a number was true (Given), Staff movement as a chain (Metrics) and 8 more

asOfDate | number—When the number was true - not when it was fetched. from GivenProps

Shown in: Say where a number came from (Given), State when a number was true (Given), The cost per stop of a day's tours (Worked examples)

agesFreshnessAges—The ages at which asOf turns stale and lost. With both, the given carries a freshness through core; with asOf alone, only the time. from GivenProps

Shown in: Say where a number came from (Given), The cost per stop of a day's tours (Worked examples)

idstring—Makes the quantity referable: <Ref to> stands for it elsewhere. Needed only for a quantity used more than once. from QuantityProps

Shown in: Fold a group of lines into one, Put a chain inside a tree, The availability of a service (Installation) and 10 more

unitstring—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, Mix additions and deductions, Multiply between two interims 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, Put a chain inside a tree, Hold an interim to its limits and 17 more

decimalsnumberone digit for a percentage, at most two for a derived number, a given as it was givenFixed number of fraction digits as shown. Rounding happens only in presentation; every operation uses the full value. from QuantityProps

Shown in: Take amounts away from a first quantity, Mix additions and deductions, Multiply between two interims and 29 more

targetnumber—The value this quantity is meant to reach - missed, never violated. from QuantityProps

Shown in: Put a chain inside a tree, The availability of a service (Installation), Write a tree (Calculation) and 6 more

limitsreadonly Limit[]—Bounds the quantity is assessed against, through core's assess(). from QuantityProps

Shown in: Hold an interim to its limits, Density (Calculation), Hold a figure to its limits (Tree) and 5 more

explanationstring—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), A number one metric does not have (Metrics), Staff movement as a chain (Metrics) and 2 more

asideReactNode—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)

emphasis"strong" | "muted"—How much the line stands out among the others (ADR-0049): 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), A quieter line (Calculation), Emphasis inside a group (Calculation)

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), Emphasis inside a group (Calculation)

QuantityProps

NameTypeDefaultDescription
labelrequiredstring—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, Mix additions and deductions, Multiply between two interims and 50 more

idstring—Makes the quantity referable: <Ref to> stands for it elsewhere. Needed only for a quantity used more than once.

Shown in: Fold a group of lines into one, Put a chain inside a tree, The availability of a service (Installation) and 10 more

unitstring—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, Mix additions and deductions, Multiply between two interims 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, Put a chain inside a tree, Hold an interim to its limits and 17 more

decimalsnumberone digit for a percentage, at most two for a derived number, a given as it was givenFixed number of fraction digits as shown. Rounding happens only in presentation; every operation uses the full value.

Shown in: Take amounts away from a first quantity, Mix additions and deductions, Multiply between two interims and 29 more

targetnumber—The value this quantity is meant to reach - missed, never violated.

Shown in: Put a chain inside a tree, The availability of a service (Installation), Write a tree (Calculation) and 6 more

limitsreadonly Limit[]—Bounds the quantity is assessed against, through core's assess().

Shown in: Hold an interim to its limits, Density (Calculation), Hold a figure to its limits (Tree) and 5 more

explanationstring—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), A number one metric does not have (Metrics), Staff movement as a chain (Metrics) and 2 more

asideReactNode—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)

emphasis"strong" | "muted"—How much the line stands out among the others (ADR-0049): 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), A quieter line (Calculation), Emphasis inside a group (Calculation)

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), Emphasis inside a group (Calculation)

Types on this page

MetricValues

A given's numbers where the calculation has metrics: one per metric id, null where the number is absent.

type MetricValues = Readonly<Record<string, number | null>>;

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.

NameTypeDefaultDescription
stalerequirednumber—From this age on the value counts as stale, but keeps its verdict.
lostrequirednumber—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.

Side

From @umriss-ui/core.

Whether a limit is an upper or a lower bound.

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.

type Severity = "warning" | "alarm";

Known limits

What umriss deliberately does not build, and why: ADR-0032.

Every page of @umriss-ui/calculation

Getting started

Writing a calculation

In practice

API index