ibcs-react

Data model

ScenarioKey, StatementLine and the category datum shapes - plus the statement adapters that project one model onto every view.

A handful of small types describe everything the components render. They all share one idea - a bag of values keyed by scenario - so the same figures flow into tables, charts and cards unchanged.

ScenarioKey and ScenarioDatum

The four IBCS scenarios. Every value in the library lives under one of these keys; a component reads whichever ones it needs and ignores the rest.

// AC = actual · PY = previous year · PL = plan/budget · FC = forecast
type ScenarioKey = "AC" | "PY" | "PL" | "FC";

// The standard category row: one label plus one optional value per scenario.
// Trend periods, line points, combo columns and variance columns are all this
// shape (or a narrow extension of it).
interface ScenarioDatum {
  category: string; // "EMEA", "Jan", "Q1", "2024"
  AC?: number;
  PY?: number;
  PL?: number;
  FC?: number;
}

A scenario is optional because real data is ragged: a forecast period has no AC, a new product has no PY. Missing means "not drawn" - never zero.

StatementLine

The core tree. A P&L or a balance sheet is an array of these. flow drives the waterfall, children make a line a collapsible group, and higherIsBetter flips variance favorability for cost-like lines.

interface StatementLine {
  id: string;
  label: string;
  // How the line moves the actual waterfall. Default "add".
  //  "add"      → moves the running total up   (revenue, other income)
  //  "subtract" → moves the running total down (cost, expense, tax)
  //  "result"   → a subtotal drawn to the running total; it does not move it
  flow?: "add" | "subtract" | "result";
  values: Partial<Record<ScenarioKey, number>>; // real currency units
  higherIsBetter?: boolean; // default true; set false on cost/expense/tax
  children?: StatementLine[]; // breakdown; collapsible. Must sum to the parent.
  defaultCollapsed?: boolean;
  emphasis?: boolean; // bold; auto-true for flow "result"
}

A flow statement vs a stock statement

The same shape renders a P&L (period flows, waterfall) and a balance sheet (point-in-time levels). Pass mode="stock" for the latter so each line draws an absolute bar instead of a step.

flow · profit and loss

 PYACΔPYΔPY%
Product revenue16.1M17.2M+1.1M+6.8
Service revenue9.5M12.9M+3.4M+35.8
=Revenue25.6M30.1M+4.5M+17.6
−Cost of goods sold8.4M9.7M+1.3M+15.5
=Gross margin17.2M20.4M+3.2M+18.6

stock · balance sheet

 PYACΔPYΔPY%
Current assets14.8M16.7M+1.9M+12.8
Cash and equivalents6.5M8.2M+1.7M+26.2
Accounts receivable4.9M5.4M+500K+10.2
Inventory3.4M3.1M-300K-8.8
Non-current assets20.1M21.4M+1.3M+6.5
=Total assets34.9M38.1M+3.2M+9.2

A group with an empty values reports the sum of its children, so a collapsed group still carries its full weight.

The category datum shapes

// VarianceColumnChart rows: a ScenarioDatum where AC is required.
type ColumnDatum = ScenarioDatum & { AC: number };

const quarterly: ColumnDatum[] = [
  { category: "Q1", AC: 6.8e6, PY: 6.1e6, PL: 6.5e6 },
  { category: "Q2", AC: 7.3e6, PY: 6.4e6, PL: 7.0e6 },
];

// TrendChart periods. Actual periods carry AC; the forecast tail carries FC
// (drawn hatched). PY / PL ride along as reference lines.
interface TrendDatum extends ScenarioDatum {
  // Set off as a total (e.g. a full-year column): divider, emphasis colour,
  // and its OWN scale treatment - a total that dwarfs the periods is drawn
  // capped with a marked scale break instead of crushing the months.
  summary?: boolean;
}

// StructureChart components - same `category` key as every other datum
// (`label` is accepted as a legacy alias from v1.0).
interface StructureDatum {
  category: string; // "North America", "Europe", …
  AC?: number;
  PY?: number;
  PL?: number;
  FC?: number;
  higherIsBetter?: boolean; // per-component override (e.g. a cost part)
}

// WaterfallChart contributions, in order.
interface WaterfallDatum {
  category: string;
  value: number;
  flow?: "add" | "subtract" | "result";
  higherIsBetter?: boolean;
}

One data model, many views: the statement adapters

Each chart view has its own flat input shape, because those views are also usable without a statement. The adapters are the bridge: pure projections from the StatementLine tree onto each view's shape, so you keep one authored data set and derive the rest instead of hand-reshaping the same lines on every screen.

import { statementToWaterfall, statementToStructure, statementToDataTableRows } from "ibcs-react";

// statementToWaterfall(lines, scenario = "AC", { expandGroups = false })
const acBridge = statementToWaterfall(lines); // top-level lines, AC
const pyBridge = statementToWaterfall(lines, "PY"); // the same bridge, PY
const detailed = statementToWaterfall(lines, "AC", { expandGroups: true });

// statementToStructure(lines, { skipResults = true })
const parts = statementToStructure(costLines); // subtotals dropped

// statementToDataTableRows(lines, { measure = "value" })
const rows = statementToDataTableRows(lines); // hierarchy preserved

statementToWaterfall keeps each line's label, flow (defaulting to "add") and higherIsBetter, so the bridge tells the same story as the statement's own waterfall lane. Group values resolve through the tree, so a collapsed group still carries its children's sum. With expandGroups: true, a group that has children, is not defaultCollapsed and is not a "result" hands the flow to its children - exactly what the table shows on first paint. An add or subtract line with no value for that scenario is skipped rather than drawn as a zero column; "result" lines are always emitted, because a result is drawn to the running total.

statementToStructure emits one datum per top-level line with every scenario it has data for. It drops flow: "result" lines by default - a composition shows the parts of a whole, and charting a subtotal next to the lines it already contains double-counts the total and shrinks every real component's share. Pass { skipResults: false } when the subtotals themselves are the composition you want.

statementToDataTableRows files each line's own scenario values under one measure (default "value") and recurses children in place, carrying flow, emphasis and defaultCollapsed across so the table draws the statement markers without extra wiring. Own values are used deliberately: the table already sums the children of a row that has no own value.

import { DataTable, statementToDataTableRows } from "ibcs-react";

const columns = [
  { key: "value", label: "AC" }, // measure defaults to the key
  { key: "value_py", label: "PY", measure: "value", scenario: "PY" },
  { key: "d_py", label: "ΔPY", kind: "variance", measure: "value", base: "PY" },
];

<DataTable columns={columns} rows={statementToDataTableRows(lines)} />;

Below, one StatementLine[] powers two views at once: the statement table reads the model directly, and the bridge is projected from the same array - twice, once per scenario, so the AC bridge can be compared against the PY one.

View 1 · StatementTable (the model, unchanged)
 PYACΔPYΔPY%
Product revenue16.1M17.2M+1.1M+6.8
Service revenue9.5M12.9M+3.4M+35.8
=Revenue25.6M30.1M+4.5M+17.6
−Cost of goods sold8.4M9.7M+1.3M+15.5
=Gross margin17.2M20.4M+3.2M+18.6
View 2 · WaterfallChart via statementToWaterfall
Revenue to gross margin - AC, Δ vs PY+€17.2M+€12.9M€30.1M-€9.7M€20.4MProduct revenueService revenueRevenueCost of goods so…Gross margin+€1.1M+€4.5M+€4.5M+€3.2M+€3.2M
Revenue to gross margin - AC, Δ vs PY - data table
ContributionRunning totalΔ vs comparison
Product revenue+€17.2M€17.2M+€1.1M
Service revenue+€12.9M€30.1M+€4.5M
Revenue€30.1M€30.1M+€4.5M
Cost of goods sold-€9.7M€20.4M+€3.2M
Gross margin€20.4M€20.4M+€3.2M
import { StatementTable, WaterfallChart, statementToWaterfall } from "ibcs-react";

const lines = fetchPnl(); // the ONE model

<StatementTable lines={lines} />
<WaterfallChart
  data={statementToWaterfall(lines)}
  comparisonData={statementToWaterfall(lines, "PY")}
/>

For export there are two more projections of the same model: statementToMatrix(lines, opts) returns a row/column matrix and statementToCSV(lines, opts) serializes it to CSV.

Consistency tip

Keep your model internally consistent: children should sum to their parent, and every flow: "result" line should equal the running total of the steps above it. The waterfall geometry and the share percentages depend on it.

ibcs-react is an independent open-source library and is not affiliated with, certified by, or endorsed by the IBCS Association or ISO. It follows the IBCS® notation rules (the basis of ISO 24896); IBCS® is a registered trademark of the IBCS Association.

On this page