` with a small caps
title. The library draws inside the panels; CSS grid does the rest.
```tsx
{/* three more cards */}
```
Charts take explicit `width` / `height` - they are printable artifacts, not
fluid boxes. For a panel that must fill a wide column on a laptop and still
show all twelve months on a phone, wrap the chart in
`ScrollChart` (or [ChartBox](/docs/interaction#sizing-and-fit) for full control
over fit and alignment): it renders at the measured width and scrolls sideways
below `minWidth` instead of squashing the categories.
```tsx
import { ScrollChart, TrendChart } from "ibcs-react";
{(w, h) => }
;
```
Because `ScrollChart` takes a render function as its child, the panel around it
has to be a client component - a function cannot be handed from a server
component to a client one.
## 1 · Executive overview [#1--executive-overview]
> The group closed FY26 ahead of prior year on every headline KPI - revenue
> **+15%** and a 1.6-point margin gain lift net income to **€8.9M (+10%)**.
**Contoso Group · Office of the CFO** - performance summary in € thousands, AC
versus PY (and Plan) · FY 2026, full year.
Four KPI cards with sparklines set the frame; the bridge explains how revenue
becomes operating income; the structure chart splits revenue by region with
ΔPY; the trend panel carries all twelve months, actuals to September and
forecast after.
Components: [KpiCard](/docs/components/kpi-card),
[WaterfallChart](/docs/components/waterfall-chart),
[StructureChart](/docs/components/structure-chart),
[TrendChart](/docs/components/trend-chart).
## 2 · Sales performance [#2--sales-performance]
> Bookings beat plan by **+€1.9M (+8%)** and ran **+14% on PY** - APAC and the
> new Cloud line carried the quarter while EMEA mid-market lagged.
**Northwind Trading · Commercial** - bookings and pipeline in € thousands, AC
versus Plan and PY · Q3 2026, quarter to date.
The regional columns show AC against a hollow Plan; the ranking chart sorts the
product lines by ΔPlan so the two losers sit together at the bottom; the rep
table repeats the same variance twice (absolute bar, percent pin) and adds a
run-rate sparkline. Click a column header to re-sort it.
Components: [VarianceColumnChart](/docs/components/variance-column),
[RankingVarianceChart](/docs/components/ranking-variance),
[DataTable](/docs/components/data-table).
## 3 · Budget versus actual [#3--budget-versus-actual]
> Revenue is tracking **+€0.42M (+3.3%) above plan** at month 9, but a
> procurement overrun keeps the EBIT beat to **+€0.18M** - watch raw-material
> spend.
**Helios Manufacturing · FP\&A** - budget versus actual in € thousands, AC
versus Plan · FY 2026, YTD through September.
The integrated variance chart puts the monthly ΔPlan directly above the columns
and closes with an FY landing bar split into actual and forecast. Below it, the
cost-centre table sorts on ΔBudget, and the ranking chart repeats the same
variances as a single ordered list. Every cost line is
`higherIsBetter: false`, so overspend is red even though the number is positive.
Components: [IntegratedVarianceChart](/docs/components/integrated-variance),
[DataTable](/docs/components/data-table),
[RankingVarianceChart](/docs/components/ranking-variance). For the full
drill-down version of this view see the [budget matrix](/docs/budget-matrix)
guide.
## 4 · P\&L deep-dive [#4--pl-deep-dive]
> Net income reached **€8.9M (+10% on PY, +19% versus plan)** - gross margin
> widened to 71.4% as service revenue grew faster than cost of sales.
**Software & Service Group · Group Finance** - profit and loss statement in €
thousands, AC with ΔPY and ΔPL · FY 2026, full year.
The statement carries its own integrated waterfall and two variance columns, so
the P\&L panel needs no companion chart to be readable. The right rail adds the
EBIT split by segment and a margin walk drawn as a bare sparkline.
Components: [StatementTable](/docs/components/statement-table),
[VarianceColumnChart](/docs/components/variance-column),
[Sparkline](/docs/components/sparkline).
## 5 · Regional breakdown [#5--regional-breakdown]
> Group net sales climbed from €25.6M to **€30.1M (+17%)** over four quarters -
> a fast-growing Asia Pacific (**+45%**) led, with every market ahead of PY.
**Global Retail Co. · Regional Operations** - net sales in € thousands by
market, AC versus PY · FY 2026, quarterly.
Three lenses on the same four markets: stacked columns for the quarterly build,
a structure chart for the share of the group with ΔPY, and a market-by-quarter
matrix whose variance column compares AC against PY (`varianceScenarios`)
rather than the default AC against PL.
Components: [StackedChart](/docs/components/stacked-chart),
[StructureChart](/docs/components/structure-chart),
[MatrixTable](/docs/components/matrix-table).
## 6 · Operations and cost [#6--operations-and-cost]
> Unit cost fell **−4.2% versus PY** on higher throughput, but an energy spike
> (**+18%**) offset half the saving - net conversion cost down €0.31M.
**Helios Plants · Operations** - conversion cost in € thousands, AC versus PY,
where lower is better · FY 2026, YTD.
A cost dashboard is where polarity matters most. The bridge walks PY cost to AC
cost through volume, labour efficiency, material yield and energy; the ranking
chart and the cost-centre table repeat the same story per category and per
centre. Every variance is flagged `higherIsBetter: false`, and the two KPIs that
should fall (cost per unit, scrap rate) carry it too.
Components: [WaterfallChart](/docs/components/waterfall-chart),
[RankingVarianceChart](/docs/components/ranking-variance),
[DataTable](/docs/components/data-table).
## 7 · Forecast and planning [#7--forecast-and-planning]
> The rolling forecast lands FY26 revenue at **€25.9M (+5% versus plan)** on a
> strong H2 backlog; EBIT closes **+€0.30M ahead of plan**.
**Atlas Industrial · Corporate Planning** - rolling forecast in € thousands, AC
and FC versus Plan · FY 2026, 9+3 forecast.
Nine months of actuals and a hatched forecast tail run against the plan line;
the EBIT panel marks its last two quarters `isForecast` and lands the year in a
stacked AC + FC total bar. The matrix does the same switch structurally - Q1
and Q2 print PL/AC, Q3 and Q4 print PL/FC, set per period.
Components: [TrendChart](/docs/components/trend-chart),
[IntegratedVarianceChart](/docs/components/integrated-variance),
[MatrixTable](/docs/components/matrix-table).
## Where to next [#where-to-next]
* [Report](/docs/components/report) - the same compositions declared as JSON
instead of JSX.
* [Gallery](/gallery) - one dataset through every chart, with a live theme
switch.
* [IBCS & ISO 24896](/docs/ibcs) - the notation these dashboards follow.
* [Theming](/docs/theming) - repaint every panel from one provider.
---
# Data model
URL: https://ibcs-react.com/docs/data-model
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 [#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.
```ts
// 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 [#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.
```ts
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
>; // 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 [#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.
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 [#the-category-datum-shapes]
```ts
// 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 [#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.
```ts
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.
```tsx
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" },
];
;
```
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.
```tsx
import { StatementTable, WaterfallChart, statementToWaterfall } from "ibcs-react";
const lines = fetchPnl(); // the ONE model
```
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 [#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 & ISO 24896](/docs/ibcs) - what `higherIsBetter` and the scenario keys
mean visually.
* [Components](/docs/components) - which component eats which shape.
---
# Example reports
URL: https://ibcs-react.com/docs/example-reports
Reports in the style of the IBCS® chart and table templates, built entirely
from `ibcs-react` components - no wrapper library, no post-processing, just the
props shown alongside each one.
Each report follows the same reporting pattern: the **message** first (the
takeaway is the headline, not a neutral label), then **who / what / when**, then
the exhibit. Data is realistic but fictitious, and company names are invented.
## Profit & loss as a dual bridge [#profit--loss-as-a-dual-bridge]
*Software and Service Group · Profit & loss statement in kEUR - AC, PY and ΔPY · 2025*
**Compared to 2024, higher operating expenses (+187 kEUR) were mainly offset by
higher licence sales (+183 kEUR), lifting the group result by +91 kEUR (+48%).**
Two row-aligned bridges (PY and AC) plus the absolute and relative deviation
tiers. Expense lines carry `higherIsBetter: false`, so a cost above last year
reads unfavorable even though its delta is positive.
[WaterfallStatementChart](/docs/components/waterfall-statement)
## Net sales by state, with a variance bridge [#net-sales-by-state-with-a-variance-bridge]
*Housing and Construction Inc. · Net sales in kUSD - by state, AC vs PY and PL · Q3 2025*
**Net sales fell 343 kUSD against previous year - Illinois (−288) drove almost
the entire decline.**
The bar chart carries the level, the attached waterfall carries the bridge from
the previous-year total to the actual one - one exhibit answering both "how big"
and "what moved".
[BarVarianceWaterfallChart](/docs/components/bar-variance-waterfall)
## Monthly contribution margin with an integrated variance [#monthly-contribution-margin-with-an-integrated-variance]
*Furniture Inc. · Contribution margin in kEUR - AC vs PY, monthly · 2025*
**Contribution margin runs ahead of prior year for most months; the forecast
closes the year at 2,036 kEUR (+5%).**
Forecast months are hatched (`isForecast: true`), and `fyTotal` sets the
full-year column apart on the right as an AC + FC stack.
[IntegratedVarianceChart](/docs/components/integrated-variance)
## Net sales build-up, columns plus a waterfall [#net-sales-build-up-columns-plus-a-waterfall]
*Furniture Inc. · Net sales in kEUR - AC vs PL, monthly build-up · 2025*
**We expect to close 24 kEUR (+15.6%) above plan on the back of a strong
forecast from September.**
Reference totals sit apart on the left in their own scenario notation; the
stacked AC + FC total closes the year on the right.
[ColumnVarianceWaterfallChart](/docs/components/column-variance-waterfall)
## Income statement as a calculation bridge [#income-statement-as-a-calculation-bridge]
*Enterprise Software Group · Income statement in bn EUR - AC vs PY · 2025*
**Operating profit grew +0.1 bn EUR: +1.6 bn of higher software revenue was
almost entirely consumed by +1.8 bn of higher operating expenses.**
The same component as the first report, on a longer scheme with two nested
subtotals - a statement stays legible as a bridge as long as the calculation
scheme is explicit.
## Profit after tax by region [#profit-after-tax-by-region]
*Electronic Inc. · Profit after tax in kEUR - by region, AC vs PY and PL · November 2025*
**Profit after tax beat plan by +234 kEUR (+5%) - the Americas (+292) carried
the month, partly offset by shortfalls in Europe.**
One measure, four deviation columns: absolute bars for size, relative pins for
rate, against two bases. The header sort is uncontrolled here - see
[Interaction & data](/docs/interaction#controlled-tables) to own it.
[DataTable](/docs/components/data-table)
## Net sales by product line, ranked by deviation [#net-sales-by-product-line-ranked-by-deviation]
*Furniture Inc. · Net sales in kEUR - by product line, ranked by ΔPL · 2025*
**Net sales missed plan by −1,420 kEUR: Seating and Tables drove the gap, while
Storage and Lighting beat plan.**
Sorting by deviation rather than by size puts the story - where the plan broke -
at the top of the page.
[RankingVarianceChart](/docs/components/ranking-variance)
## Order intake with a forecast tail [#order-intake-with-a-forecast-tail]
*Machinery Group · Order intake in kEUR - monthly, AC and FC vs PY · 2025*
**Order intake has tracked above prior year all year; the forecast holds the
lead into Q4, closing +8% vs PY.**
Switching a period from `AC` to `FC` is all it takes: the forecast tail picks up
the hatched forecast notation automatically.
[TrendChart](/docs/components/trend-chart)
## Net sales by region over time [#net-sales-by-region-over-time]
*Software and Service Group · Net sales in mEUR - by region, quarterly · 2025*
**Group net sales climbed from 27.0 to 33.9 mEUR over four quarters, led by EMEA
and a fast-growing APAC.**
A structure over time. Keep the series count low - a stack is readable for the
bottom segment and the total, and little else.
[StackedChart](/docs/components/stacked-chart)
## Quarterly EBIT against plan [#quarterly-ebit-against-plan]
*Industrial Components Ltd. · EBIT in kEUR - quarterly, AC vs PL · 2025*
**EBIT recovered through the year: after a soft Q2 (−40 vs plan) the second half
beat plan, closing +260 kEUR ahead.**
Grouped columns with the deviation tiers below - the plan bar is hollow-framed,
so scenario and variance never compete for the same visual channel.
[GroupedVarianceChart](/docs/components/grouped-variance)
## P\&L calculation scheme as a table [#pl-calculation-scheme-as-a-table]
*Software and Service Group · Profit & loss statement in mEUR - AC with ΔPY and ΔPL · 2025*
**Net income reached 8.9 mEUR (+10% vs PY, +19% vs plan) on strong service
revenue and disciplined operating expenses.**
The exact figures, with the waterfall lane drawn inside the table so the shape
of the calculation is visible without leaving the numbers.
[StatementTable](/docs/components/statement-table)
## Operating profit by segment [#operating-profit-by-segment]
*Enterprise Software Group · Operating profit by segment in kEUR - AC vs PY · 2025*
**All four segments grew operating profit against prior year; Cloud (+118 kEUR)
was the strongest absolute contributor.**
Segments on one shared scale, so a division is never flattered by its own axis.
[VarianceColumnChart](/docs/components/variance-column)
## Headline KPIs [#headline-kpis]
*Software and Service Group · Headline KPIs - AC vs PY · 2025*
**The group closed 2025 ahead of prior year on every headline KPI: revenue +12%,
margin up 1.4 points, and a lower cost ratio.**
The unit belongs to `format` - `currency` for a leading symbol, `suffix` for a
trailing one - and the cost ratio sets `higherIsBetter={false}`, so its fall
reads favorable.
[KpiCard](/docs/components/kpi-card)
## Where to next [#where-to-next]
* [Industry reports](/docs/industry-reports) - the same pattern applied to SaaS,
retail, manufacturing, banking, healthcare and a corporate P\&L.
* [Gallery](/gallery) - every component at a glance.
* [Report](/report) - a full report assembled from a JSON `ReportConfig`.
* [IBCS & ISO 24896](/docs/ibcs) - the notation rules these exhibits follow.
---
# Examples
URL: https://ibcs-react.com/docs/examples
Three complete dashboards, each assembled **entirely** from library components
and driven by the shared sample model (AC / PY / PL / FC). They show how the
building blocks - KPI cards, charts, statements and tables - compose into a
finished report in the IBCS notation. Each one is contained: it scrolls inside
its frame, and the grids stack to a single column on narrow screens.
## Executive overview [#executive-overview]
A one-glance summary built from a single `ReportConfig` handed to `Report`: a
four-card KPI strip, an operating-income **waterfall**, a revenue-by-region
**structure** chart and a 13-period **trend**, laid out on the responsive
12-column report grid.
```tsx
import { Report, type ReportConfig } from "ibcs-react";
import { sampleMonthlyTrend, sampleRevenueStructure } from "./sample-data";
// Sparkline series from the actual periods of the trend.
const acTrend = sampleMonthlyTrend.filter((d) => d.AC != null).map((d) => d.AC as number);
const executiveConfig: ReportConfig = {
title: {
who: "Contoso Group",
what: "Performance summary - € thousands",
when: "FY 2026 · Actual vs Previous year",
},
message: "Revenue +17.5% on PY; margin expansion lifts net income to €8.9M.",
columns: 12,
blocks: [
{
id: "k-rev",
type: "kpi",
span: 3,
config: {
label: "Revenue",
values: { AC: 30_100_000, PY: 25_600_000, PL: 28_500_000 },
comparisons: ["PY", "PL"],
format: { compact: true, decimals: 1 },
sparkline: acTrend,
},
},
// …three more KPI blocks: gross margin, operating income, net income
{
id: "ex-bridge",
type: "chart",
span: 7,
title: { who: "Contoso Group", what: "Operating income bridge", when: "FY 2026" },
config: {
type: "waterfall",
data: [
{ category: "Revenue", value: 30_100_000, flow: "add" },
{ category: "COGS", value: 9_700_000, flow: "subtract", higherIsBetter: false },
{ category: "Gross margin", value: 20_400_000, flow: "result" },
{ category: "Opex", value: 10_000_000, flow: "subtract", higherIsBetter: false },
{ category: "Op. income", value: 10_400_000, flow: "result" },
],
width: 460,
height: 280,
format: { compact: true, decimals: 1 },
},
},
{
id: "ex-structure",
type: "chart",
span: 5,
config: { type: "structure", data: sampleRevenueStructure, width: 360, height: 280 },
},
{
id: "ex-trend",
type: "chart",
span: 12,
config: { type: "trend", data: sampleMonthlyTrend, width: 820, height: 260 },
},
],
};
;
```
Components: [Report](/docs/components/report) ·
[KpiCard](/docs/components/kpi-card) ·
[WaterfallChart](/docs/components/waterfall-chart) ·
[StructureChart](/docs/components/structure-chart) ·
[TrendChart](/docs/components/trend-chart)
## Sales performance [#sales-performance]
A revenue team's regional review, hand-laid on a responsive grid: a ranked
structure chart, per-region quarterly small multiples, and a sortable table
with embedded variance bars, pins and a trend sparkline.
```tsx
import { StructureChart, MiniVarianceMultiples, DataTable } from "ibcs-react";
import { sampleRevenueStructure } from "./sample-data";
const regionMultiples = [
{
label: "North America",
data: [
{ category: "Q1", AC: 2.9e6, PY: 2.6e6 },
{ category: "Q2", AC: 3.1e6, PY: 2.7e6 },
{ category: "Q3", AC: 3.2e6, PY: 2.9e6 },
{ category: "Q4", AC: 3.2e6, PY: 2.9e6 },
],
},
// …Europe, Asia Pacific, Rest of world
];
const salesColumns = [
{ key: "rev", label: "Revenue AC", kind: "value" as const },
{
key: "rev_dpy",
label: "ΔPY",
kind: "variance" as const,
measure: "rev",
base: "PY" as const,
mode: "abs" as const,
mark: "bar" as const,
},
{
key: "rev_dpy_pct",
label: "ΔPY %",
kind: "variance" as const,
measure: "rev",
base: "PY" as const,
mode: "pct" as const,
mark: "pin" as const,
},
{
key: "trend",
label: "Trend",
kind: "sparkline" as const,
measure: "rev",
sparkType: "line" as const,
},
];
const salesRows = [
{
id: "na",
label: "North America",
values: { rev: { AC: 12_400_000, PY: 11_100_000 } },
spark: { rev: [2.6e6, 2.7e6, 2.9e6, 3.1e6, 3.2e6, 3.2e6] },
},
// …one row per region
];
// Panel chrome omitted for brevity.
;
```
Components: [StructureChart](/docs/components/structure-chart) ·
[MiniVarianceMultiples](/docs/components/small-multiples) ·
[DataTable](/docs/components/data-table)
## Financial statements [#financial-statements]
The finance pack: a waterfall **income statement** and a stock-mode **balance
sheet** side by side, each scrolling internally, plus a budget table of revenue
by quarter, Actual against Plan. All three are the same `StatementTable` and
`DataTable` primitives over the shared model.
```tsx
import { StatementTable, DataTable } from "ibcs-react";
import { sampleStatementFlat, sampleBalanceSheet, sampleQuarterlyStatement } from "./sample-data";
const budgetColumns = [
{ key: "rev", label: "Actual", kind: "value" as const },
{ key: "rev_pl", label: "Plan", kind: "value" as const, measure: "rev", scenario: "PL" as const },
{
key: "rev_dpl",
label: "ΔPL",
kind: "variance" as const,
measure: "rev",
base: "PL" as const,
mode: "abs" as const,
mark: "bar" as const,
},
{
key: "rev_dpl_pct",
label: "ΔPL %",
kind: "variance" as const,
measure: "rev",
base: "PL" as const,
mode: "pct" as const,
mark: "pin" as const,
},
];
const budgetRows = sampleQuarterlyStatement
.filter((l) => l.flow !== "result")
.map((l) => ({
id: l.id,
label: l.label,
values: { rev: { AC: l.values.AC, PL: l.values.PL } },
}));
// Panel chrome omitted for brevity.
;
```
Components: [StatementTable](/docs/components/statement-table) ·
[DataTable](/docs/components/data-table)
## One model, many reports [#one-model-many-reports]
Nothing here reshapes data per view - the same scenario-keyed figures feed
cards, charts, statements and tables alike. Swap the `tokens` prop on a
`Report` and every block re-themes at once.
## Where to next [#where-to-next]
* [Report](/docs/components/report) - the config-driven layout used above, and
the live [report demo](/report).
* [IBCS templates](/docs/templates) and
[Table templates](/docs/table-templates) - the individual layouts these
dashboards are built from.
* [Theming](/docs/theming) - one `tokens` prop re-themes a whole report.
---
# Export
URL: https://ibcs-react.com/docs/export
Reports get shared. `ExportMenu` wraps any chart and exposes a compact,
IBCS-clean toolbar for downloading or copying it; the underlying helpers are
exported too if you want your own UI.
## ExportMenu [#exportmenu]
```tsx
import { ExportMenu, TrendChart } from "ibcs-react";
notify(action)}
>
;
```
* **SVG / PNG** act on the first `` inside the wrapped children
(`pngScale` controls raster sharpness, default 2×).
* **CSV / JSON** appear only when you pass `csv` / `data`.
* **Copy SVG / Copy PNG / Print** use the clipboard and a print window; where
a browser blocks them, the action reports through `onError`.
* The popup is fully keyboard-operable: arrows rove, `Home`/`End` jump,
`Escape` closes and returns focus.
* Failures (denied clipboard, blocked downloads, rasterization errors) call
`onError(error, action)` - the `action` is an `ExportMenuAction`
(`"svg" | "png" | "csv" | "json" | "copy-svg" | "copy-png" | "print"`) so
you can tailor the message. Without a handler they log to the console;
they never escape as unhandled rejections.
## Props [#props]
## The helpers underneath [#the-helpers-underneath]
All exported for custom UIs:
```tsx
import {
serializeSvg, // svg element → standalone SVG markup string
downloadSVG, // svg element → .svg download
svgToPngBlob, // svg element → PNG Blob (scale-aware)
downloadPNG, // svg element → .png download
downloadCSV, // csv text → .csv download
downloadTextFile,
} from "ibcs-react";
import { statementToCSV, toCSV } from "ibcs-react/core";
```
Every chart forwards its `ref` to the ``, so no DOM queries are needed:
```tsx
const ref = useRef(null);
;
// later:
if (ref.current) downloadSVG(ref.current, "trend.svg");
```
---
# Getting started
URL: https://ibcs-react.com/docs/getting-started
Install the package, hand a component your numbers, and you get output drawn
in the IBCS notation - scenario fills, impact-coloured variances and
zero-baseline axes - with no chart wiring.
## Install [#install]
npm
pnpm
yarn
bun
```bash
npm install ibcs-react
```
```bash
pnpm add ibcs-react
```
```bash
yarn add ibcs-react
```
```bash
bun add ibcs-react
```
`ibcs-react` ships ESM + CJS with types, built one file per module so your
bundler drops what you don't import. The claim is enforced, not aspirational:
CI bundles a `KpiCard`-only fixture against the published output and fails if
it exceeds \~4 KB gzip or drags another component along.
## Peer dependencies [#peer-dependencies]
React and React DOM are **peer** dependencies - the library uses your app's
copy rather than bundling its own. Any React **18 or newer** works (18 and 19
are both fine).
npm
pnpm
yarn
bun
```bash
npm install react react-dom
```
```bash
pnpm add react react-dom
```
```bash
yarn add react react-dom
```
```bash
bun add react react-dom
```
There are no other runtime dependencies: charts are hand-rolled inline SVG, so
there is no D3, no charting engine and nothing to configure.
## The one-data-model idea [#the-one-data-model-idea]
The whole library is built on a single model: **values keyed by scenario** -
`AC` (actual), `PY` (previous year), `PL` (plan) and `FC` (forecast). A
`StatementTable`, a `VarianceColumnChart` and a `KpiCard` are just different
*views* over that same shape. Compute your figures once and reuse them
everywhere - you never reshape data per component.
Components never fetch. You bring the numbers (from your ERP, an API, static
JSON); the library handles the notation.
## Your first dashboard [#your-first-dashboard]
A KPI card and a statement table, both driven by the same scenario-keyed
values:
```tsx
import { KpiCard, StatementTable } from "ibcs-react";
// ONE data model - values keyed by scenario (AC/PY/PL/FC) - feeds every
// component. Nothing here fetches; you bring the data.
const statement = [
{ id: "rev-product", label: "Product revenue", flow: "add", values: { AC: 17.2e6, PY: 16.1e6 } },
{ id: "rev-service", label: "Service revenue", flow: "add", values: { AC: 12.9e6, PY: 9.5e6 } },
{ id: "revenue", label: "Revenue", flow: "result", values: { AC: 30.1e6, PY: 25.6e6 } },
{
id: "cogs",
label: "Cost of goods sold",
flow: "subtract",
higherIsBetter: false,
values: { AC: 9.7e6, PY: 8.4e6 },
},
{ id: "gross-margin", label: "Gross margin", flow: "result", values: { AC: 20.4e6, PY: 17.2e6 } },
];
export function Dashboard() {
return (
<>
>
);
}
```
## Sizing [#sizing]
Charts draw an SVG at an explicit pixel size: pass `width` and `height` (every
chart has a sensible default - `VarianceColumnChart` is 560 × 320,
`WaterfallChart` 640 × 360, `TrendChart` 720 × 360). Labels, ticks and bar
widths are laid out for exactly that box, which is what keeps a chart readable
in print.
```tsx
```
For a chart that follows its container, wrap it in `ChartBox` - one sizing
primitive with `fit` modes (`"scale"`, `"fixed"`, `"contain"`, `"fill"`),
alignment, padding and scroll behaviour. It re-renders the chart at the
resolved pixel size instead of scaling a bitmap, so text and strokes stay
crisp:
```tsx
import { ChartBox, TrendChart } from "ibcs-react";
// Scale with the space, but hold a readable 680px and scroll below that.
{(w, h) => }
;
```
See [Interaction & data](/docs/interaction) for the full fit/align/scroll
options. (`ChartFrame` still exists but is deprecated - `ChartBox` covers it.)
## "use client" and server components [#use-client-and-server-components]
The package entry ships its own `"use client"` directive, so a React Server
Component can import and render a chart directly - you do not need to add a
`"use client"` file of your own just to place a `VarianceColumnChart` on a
server-rendered page:
```tsx
// app/page.tsx - a server component
import { VarianceColumnChart } from "ibcs-react";
export default function Page() {
return ;
}
```
You do need your own `"use client"` component when *your* code holds state or
handlers around the chart - a scenario toggle, an `onSelect` drill-down, a
theme switcher. Rendering itself is SSR-safe: the markup is plain SVG, and the
optional tooltip only appears on the client.
## Where to next [#where-to-next]
* [Data model](/docs/data-model) - the exact shape of `StatementLine`,
`TrendDatum` and friends, plus the statement adapters.
* [IBCS & ISO 24896](/docs/ibcs) - what the notation means and why a variance
is red when it grows.
* [Theming](/docs/theming) - token presets, `IbcsThemeProvider` and per-component
overrides.
* [Conformance](/docs/conformance) - lint a config against the IBCS notation
rules the library implements.
* [Components](/docs/components) - a page each, with live examples, code and a
props table generated from the source types.
* [Playground](/playground) - configure charts live in the browser.
## Project status [#project-status]
ibcs-react is in active development. It is an independent implementation of
the IBCS notation described by ISO 24896 - not certified by, affiliated with or
endorsed by IBCS or ISO. Open gaps and feedback are tracked on
[GitHub](https://github.com/NibelungAI/ibcs-react).
---
# Hooks
URL: https://ibcs-react.com/docs/hooks
The library exports the same hooks its own components are built on:
zero-dependency, SSR-safe and tree-shakeable. Import any of them from
`ibcs-react`. They fall into four families - **animation** primitives,
**data** helpers, **interaction** models and one **layout** measurement hook.
Nothing here is required to render a chart. Each hook exists so you can build a
custom view - your own statement, your own mark, your own toolbar - and have it
behave exactly like the built-in components.
## Animation [#animation]
The library does not sell animation as a feature. It ships small
`requestAnimationFrame` + easing primitives so *you* can add motion - an
entrance growth, a value tween - without a motion library. Every one of them
collapses to its final value when the user prefers reduced motion, and every
one renders the *finished* value on the server, so SSR markup carries real
geometry instead of a collapsed first frame.
The easing curves are exported too: `easeOutCubic` (the default),
`easeOutQuart` and `easeInOutCubic`, plus the `Easing` type
(`(t: number) => number`) if you want to pass your own.
### usePrefersReducedMotion [#useprefersreducedmotion]
```ts
usePrefersReducedMotion(): boolean
```
True when the OS asks for reduced motion, kept live through `matchMedia` and
read via `useSyncExternalStore` - so the very first client render already
reflects the real preference instead of flashing one frame of animation at
exactly the users who asked not to see one. On the server it returns `false`
(the other hooks render their finished value there anyway).
```tsx
import { usePrefersReducedMotion } from "ibcs-react";
function Pulse() {
const reduced = usePrefersReducedMotion();
return
;
}
```
**When to use:** gate any custom animation you add outside the library so it
respects the same accessibility preference as the built-ins.
### useMountGrow [#usemountgrow]
```ts
useMountGrow(duration = 600, delay = 0, key?: unknown): number
```
Eased progress from `0` to `1`, played once on mount and replayed when `key`
changes **shape**. Multiply a bar height or a path length by it to drive an
entrance. The hook starts *finished* (`1`) and rewinds in a layout effect, so
the server, the first paint and any client whose effects never run all show the
final frame. Reduced motion, `duration <= 0` or a missing
`requestAnimationFrame` keep it at `1` with no frame loop at all.
`key` is reduced to a cheap structural signature rather than compared by
identity, and numbers are erased from that signature. An inline `data={[…]}`
literal therefore does not restart the entrance on every parent render, and a
live feed re-emitting the same rows with new values does not re-grow the chart
from its baseline - value updates glide via [`useDataTween`](#usedatatween).
What replays the entrance is a genuinely new dataset: rows added or removed,
categories renamed.
```tsx
import { useMountGrow } from "ibcs-react";
function GrowingBar({ data, fullHeight, base }) {
// Replays when the data changes shape - not on value ticks or re-renders.
const p = useMountGrow(600, 0, data);
return ;
}
```
**When to use:** a one-shot entrance for a chart you draw yourself, replayed
only when a genuinely different dataset arrives.
### useAnimatedValue [#useanimatedvalue]
```ts
useAnimatedValue(target: number, opts?: AnimateOptions): number
interface AnimateOptions {
duration?: number; // ms, default 600; <= 0 renders the target instantly
easing?: Easing; // default easeOutCubic
delay?: number; // ms, default 0 (staggering)
from?: number; // start value for the FIRST run only (e.g. 0)
}
```
Tweens a number toward `target` whenever it changes, gliding from the value
currently on screen - retargeting mid-flight is supported. The first render
(server included) shows `target`, so static output is always correct; pass
`from` to opt into a real mount animation. Reduced motion jumps straight to the
target.
```tsx
import { useAnimatedValue, easeOutQuart } from "ibcs-react";
function Gauge({ value }) {
const v = useAnimatedValue(value, { duration: 800, easing: easeOutQuart });
return ;
}
```
**When to use:** a value that changes over time and should glide rather than
snap - a live gauge, an axis maximum that retargets.
### useCountUp [#usecountup]
```ts
useCountUp(target: number, opts?: AnimateOptions): number
```
A thin alias of `useAnimatedValue` with a clearer name at the call site: an
animated number you render through your own formatter. This is what powers
`KpiCard`'s headline, which passes `{ duration: 700, from: 0 }`. Without
`from`, the first render shows the target and only later changes tween - pass
`from: 0` when you want the figure to count up on mount.
```tsx
import { useCountUp, formatValue } from "ibcs-react";
function Figure({ amount }) {
const n = useCountUp(amount, { duration: 700, from: 0 });
return {formatValue(n, { compact: true })} ;
}
```
**When to use:** a headline KPI or metric that should count up to its value.
### useDataTween [#usedatatween]
```ts
useDataTween(target: T, opts?: Omit): T
```
`useAnimatedValue` for a whole data structure: every numeric leaf glides from
where it currently sits to its new value, everything else switches instantly.
This is the hook behind live charts - every chart runs its `data` through it
and recomputes layout from the interpolated rows each frame, so a feed's tick
moves bars from their previous heights, stretches scales smoothly and slides
variance pins, instead of replaying the entrance from zero.
A **shape** change does not morph: rows added or removed, categories renamed,
or a value appearing where none was is a new dataset - it shows immediately,
and the entrance replays for it. Retargeting mid-flight continues from the
frame currently on screen. Reduced motion (or `duration <= 0`) jumps straight
to the target, and the first render (server included) is always the target
itself.
```tsx
import { useDataTween } from "ibcs-react";
function CustomChart({ data }) {
const live = useDataTween(data); // glides between live ticks
const layout = useMemo(() => myLayout(live), [live]);
// …
}
```
**When to use:** a custom chart or figure on a live source that should glide
between updates the way the built-in charts do.
## Data [#data]
These own a slice of report state or derive IBCS layout and variance from your
model. They are pure logic over the same scenario-keyed data the components
render, so you can compose custom views without re-implementing the notation.
### useStatement [#usestatement]
```ts
useStatement(lines: StatementLine[], opts?: UseStatementOptions): UseStatementResult
interface UseStatementOptions {
mode?: "flow" | "stock"; // default "flow"
scenario?: ScenarioKey; // default "AC"
defaultCollapsed?: readonly string[]; // uncontrolled seed
collapsed?: ReadonlySet | readonly string[]; // controlled value
onCollapsedChange?: (collapsedIds: string[]) => void;
}
interface UseStatementResult {
rows: WaterfallRow[];
collapsed: Set;
toggle: (id: string) => void;
isCollapsed: (id: string) => boolean;
expandAll: () => void;
collapseAll: () => void;
groupIds: string[]; // every collapsible group, in document order
allCollapsed: boolean;
allExpanded: boolean;
domainMin: number; // most negative point on the value axis (<= 0)
domainMax: number; // most positive point on the value axis (>= 0)
}
```
Owns a statement's collapse/expand state and derives its waterfall (or, in
`"stock"` mode, its levels) layout - literally the engine
[StatementTable](/docs/components/statement-table) runs on, so a custom view
behaves exactly like the built-in one. Flatten plus layout are memoized against
the model, the collapsed set, the scenario and the mode.
Uncontrolled by default (seed with `defaultCollapsed`, or with each line's own
`defaultCollapsed` flag); pass `collapsed` + `onCollapsedChange` to own the
state yourself, exactly like the tables. `groupIds` is empty for a flat
statement - the cue to hide an expand/collapse toolbar entirely.
```tsx
import { useStatement } from "ibcs-react";
function MyStatement({ lines }) {
const { rows, toggle, isCollapsed, groupIds, domainMax } = useStatement(lines, {
mode: "flow",
scenario: "AC",
});
return rows.map((r) => (
toggle(r.id)} />
));
}
```
**When to use:** a custom statement or waterfall view that needs IBCS layout
(running totals, a shared zero baseline, collapsible groups) without the stock
table chrome.
### useStatementBridge [#usestatementbridge]
```ts
useStatementBridge(
lines: StatementLine[],
comparison?: ScenarioKey,
options?: { scenario?: ScenarioKey; expandGroups?: boolean },
): { data: WaterfallDatum[]; comparisonData?: WaterfallDatum[] }
```
Derives a `WaterfallChart`'s `data` **and** `comparisonData` from one
statement, so the bridge takes part in the same `comparison` toggle as every
other chart. The siblings accept a scenario *key* (`comparison="PY"`) while a
bridge needs the other scenario's contributions spelled out as a dataset -
this hook absorbs that asymmetry:
```tsx
import { useStatementBridge, WaterfallChart, VarianceColumnChart } from "ibcs-react";
function Dashboard({ pnl, regions, comparison }) {
return (
<>
{/* every other chart */}
{/* the bridge - same toggle, no special-case branch */}
>
);
}
```
Both datasets come from `statementToWaterfall` with the same options, so they
stay structurally parallel. Pass no comparison for a bare bridge; the result
is memoized on the inputs.
**When to use:** a dashboard-wide "vs PY / vs PL" switch that includes a
bridge chart.
### useFilters [#usefilters]
```ts
useFilters(initial: F): UseFiltersResult
interface UseFiltersResult {
filters: F;
setFilter: (key: K, value: F[K]) => void;
patch: (partial: Partial) => void;
reset: () => void;
}
```
Generic, typed report-filter state - scenario, comparison, period or dimension
selections - in one object with ergonomic setters. There is no schema: `F` is
whatever you pass as `initial`, and `reset()` returns to the value captured on
the first render.
```tsx
import { useFilters } from "ibcs-react";
const { filters, setFilter, patch, reset } = useFilters({
comparison: "PY",
period: "FY",
mode: "flow",
});
setFilter("comparison", "PL"); // type-checked against F
patch({ period: "Q4" }); // merge a partial
reset(); // back to the initial object
```
**When to use:** a dashboard or report toolbar with a handful of controls - one
typed state object instead of several `useState` calls, with a free `reset`.
### useLiveData [#uselivedata]
```ts
useLiveData(producer: () => T, opts?: UseLiveDataOptions): UseLiveDataResult
interface UseLiveDataOptions {
intervalMs?: number; // default 2000
enabled?: boolean; // live switch, default true
immediate?: boolean; // produce a value as soon as the feed starts, default false
}
interface UseLiveDataResult {
data: T;
refresh: () => void; // produce one value now
running: boolean;
start: () => void;
stop: () => void; // data keeps its last value
}
```
Emits a fresh value on an interval - a zero-dependency live feed. SSR-safe: the
producer seeds the first value synchronously, and the interval only ever runs
inside an effect. `enabled` is a live switch rather than a seed, but between
its transitions your own `start()` / `stop()` wins, so a manual pause is never
undone by an unrelated re-render.
```tsx
import { useLiveData, useCountUp, VarianceColumnChart } from "ibcs-react";
const feed = useLiveData(() => jitter(baseRevenue), { intervalMs: 2500, enabled: false });
const shown = useCountUp(feed.data.reduce((s, d) => s + d.AC, 0), { duration: 600 });
(feed.running ? feed.stop() : feed.start())}>
{feed.running ? "Streaming" : "Go live"}
;
```
**When to use:** demos, monitoring walls or anything that refreshes on a timer.
Charts glide between ticks on their own (every chart tweens its rows through
[`useDataTween`](#usedatatween)); pair your own figures with `useCountUp` so
they tween too.
### useVariance and useVariances [#usevariance-and-usevariances]
```ts
useVariance(
current: number | undefined,
base: number | undefined,
higherIsBetter = true,
): Variance | null
useVariances(
data: ReadonlyArray,
comparison: ReadonlyArray,
higherIsBetter = true,
): Array
```
Memoized wrappers over the core `computeVariance`. `useVariance` gives the delta
for one pair - `{ abs, pct, favorable }`, or `null` when either side is missing.
`useVariances` applies the same comparison element-wise across two parallel
arrays; the result has the length of `data`, with `null` wherever a pair is
incomplete.
```tsx
import { useVariance, useVariances } from "ibcs-react";
// One pair on a cost line - favorable colouring flips:
const v = useVariance(actual, plan, /* higherIsBetter */ false);
// v?.abs, v?.pct, v?.favorable
// Element-wise across two series:
const deltas = useVariances(acValues, pyValues);
```
**When to use:** IBCS-correct variances for your own cells or labels - coloured
by `favorable` (impact), not by sign. See
[IBCS & ISO 24896](/docs/ibcs) for why that distinction matters.
### useAsyncData [#useasyncdata]
```ts
useAsyncData(
fetcher: (signal?: AbortSignal) => Promise,
opts?: UseAsyncDataOptions,
): UseAsyncDataResult
interface UseAsyncDataOptions {
deps?: unknown[]; // extra reactive inputs; CONSTANT length
enabled?: boolean; // default true, reactive
initialData?: T; // seed / SSR value
refreshMs?: number; // poll interval; omit for one fetch per deps change
keepPreviousData?: boolean; // default true
}
interface UseAsyncDataResult {
data: T | undefined;
error: Error | null;
loading: boolean; // first load, nothing to show yet
refreshing: boolean; // re-fetch with the previous data still on screen
refetch: () => void;
lastUpdated: number | null; // epoch ms of the last success
}
```
Drives a component off an API call, a DB query or anything returning a Promise.
The fetcher runs on mount and whenever `deps` change - never during render, so
the hook is SSR-safe. In-flight requests are aborted on unmount, on a manual
`refetch`, when `enabled` flips to `false` and before each new run; aborts are
ignored rather than surfaced as errors. Because `deps` is spliced into a real
dependency array, its length must stay constant across renders (use `null` for
"not applicable" slots).
```tsx
import { useAsyncData, ChartState, StatementTable } from "ibcs-react";
const { data, loading, refreshing, error, refetch } = useAsyncData(
(signal) => fetch("/api/pnl", { signal }).then((r) => r.json()),
{ refreshMs: 30_000 },
);
;
```
**When to use:** feeding any component from a remote source with first-class
loading and refresh state instead of hand-wiring `useEffect` + `useState` +
abort handling. There is a live demo on
[Interaction & data](/docs/interaction).
## Interaction [#interaction]
### useChartSelection [#usechartselection]
```ts
useChartSelection(initial?: Iterable): UseChartSelectionResult
interface UseChartSelectionResult {
selected: ReadonlySet;
isSelected: (key: K) => boolean;
toggle: (key: K) => void;
clear: () => void;
set: (keys: Iterable) => void; // replace the selection wholesale
}
```
A tiny selection model for click-to-filter: pair it with a chart's `onSelect`,
which fires `{ category, scenario?, value, datum }`. Generic over the key type
(usually the category string) and SSR-safe - pure `useState`, no DOM access.
```tsx
import { VarianceColumnChart, useChartSelection } from "ibcs-react";
const sel = useChartSelection(["Q1"]); // optional initial selection
sel.toggle(info.category)} />;
// elsewhere: rows.filter((r) => sel.isSelected(r.category))
```
**When to use:** cross-filtering dashboards, drill-downs, or letting a user pick
the categories a detail table should show.
### useChartHover [#usecharthover]
```ts
useChartHover(): UseChartHoverResult
interface UseChartHoverResult {
hovered: ChartHover | null; // { category, scenario?, value, datum, x, y }
tooltipRef: RefObject; // pass to
onMove: (info: ChartHoverInfo, event: PointerLike) => void; // PointerLike = { clientX, clientY }
onLeave: () => void;
clear: () => void; // alias of onLeave
}
```
The hover model behind every built-in tooltip: it holds the hovered datum plus
the pointer's viewport coordinates, and it wires Escape / outside-tap dismissal
for you. Give `tooltipRef` to [ChartTooltip](/docs/interaction#custom-tooltips)
and pointer moves are applied imperatively on an animation frame, so following
the cursor costs no React re-render.
It pairs directly with a chart's `onHover` (the mirror of `onSelect`, plus
`x`/`y`) once you have switched the built-in panel off with `tooltip={false}` -
this is the canonical wiring:
```tsx
import { VarianceColumnChart, ChartTooltip, useChartHover, type ColumnDatum } from "ibcs-react";
const hover = useChartHover();
(h ? hover.onMove(h, { clientX: h.x, clientY: h.y }) : hover.onLeave())}
/>;
{
hover.hovered && (
);
}
```
For a mark you draw yourself there is no `onHover` to borrow, so call `onMove`
from the element's own `onPointerMove` and `onLeave` from `onPointerLeave` -
any event with `clientX` / `clientY` satisfies `PointerLike`.
**When to use:** a fully custom tooltip panel, or the library's panel on a chart
you drew yourself. Live demo on [Interaction & data](/docs/interaction).
## Layout [#layout]
### useElementSize [#useelementsize]
```ts
useElementSize(): [RefObject, ElementSize]
interface ElementSize {
width: number;
height: number;
}
```
Observes an element's content-box size with a `ResizeObserver` and returns a
`[ref, size]` tuple. The first real measurement lands in a layout effect, before
the browser paints, so a consumer never flashes an empty box; later resizes are
applied on the next animation frame, which keeps a component that reacts to its
own measurement from tripping the browser's "ResizeObserver loop" warning. This
is what `ChartBox` and `ResponsiveChart` are built on.
```tsx
import { useElementSize, TrendChart } from "ibcs-react";
function Panel({ data }) {
const [ref, { width }] = useElementSize();
return {width > 0 && }
;
}
```
SSR-safe: nothing touches `window` or `ResizeObserver` at module load or during
render. Without a `ResizeObserver` the element is measured once and then left
static.
**When to use:** measuring your own container. For sizing a chart, reach for
[ChartBox](/docs/interaction#sizing-and-fit) first - it does this and the fit
maths for you.
## Theming [#theming]
`useIbcsTokens(override?)` resolves a `tokens` prop against the nearest
`IbcsThemeProvider` and the defaults - the hook every component calls
internally, so a custom chart participates in the same theme. It is documented
with the rest of the token system in [Theming](/docs/theming).
## Where to next [#where-to-next]
* [Interaction & data](/docs/interaction) - live demos of `useChartSelection`,
`useChartHover` and `useAsyncData`, plus the sizing wrappers.
* [Data model](/docs/data-model) - the `StatementLine` and scenario datum shapes
these hooks operate on.
* [Components](/docs/components) - the built-ins, each assembled from exactly
these hooks.
---
# IBCS & ISO 24896
URL: https://ibcs-react.com/docs/ibcs
This library implements the IBCS® notation - the notation described by
**ISO 24896:2026, "Notation for business reporting"**. The rules below are
baked into every component, so reports come out consistent without a
house-style debate on every chart.
## Scenario fills [#scenario-fills]
Scenarios are distinguished by *fill*, not colour - so a report stays legible
in greyscale and never confuses "what happened" with "what we planned". The
four scenario keys render as:
Actuals are **solid**, previous year is **solid grey**, plan is an **outline**
(it hasn't happened yet) and forecast is **hatched** (expected). You see the
same grammar in `TrendChart`, where actual columns are solid and the forecast
tail is hatched.
Plan / budget (PL) is the easy one to miss - a deliberately hollow outline.
Below, solid actuals are compared against plan: the thin rectangular frames
behind each quarter are the PL columns, and the variance panel reads AC vs PL.
Switch the toggle to compare against previous year instead - same data, a
different yardstick.
## Variance is coloured by impact, not sign [#variance-is-coloured-by-impact-not-sign]
A deviation is green when it is **good for the business** and red when it is
**bad** - regardless of whether the number went up or down. Revenue up is
green; cost up is red. Lines and data carry `higherIsBetter` (set it to `false`
on cost, expense and tax measures) so the colour follows favorability, not the
arithmetic sign.
Up +4.5M - favorable, so green.
Also up - but cost up is unfavorable, so red.
```tsx
// Same arithmetic, opposite meaning:
```
## Signed values [#signed-values]
Variances always show their sign - `+4.5M`, `−1.3M`, `+17.6%`. The sign tells
the direction of change; the colour tells whether that change is good. The two
are independent on purpose, which is why a `+1.3M` cost overrun reads red.
## Absolute deviations are bars, relative ones are pins [#absolute-deviations-are-bars-relative-ones-are-pins]
Absolute deviations (currency units) are drawn as **bars** - they share the
value axis, so magnitudes are comparable. Relative deviations (percent) are
drawn as **pins** (a lollipop: line plus dot) on their own percent scale, so a
small absolute swing on a small base doesn't masquerade as a big bar.
```tsx
```
`StatementTable` follows the same convention out of the box: its default
variance panels are an absolute bar column and a percent pin column against
PY.
## Zero-baseline axes [#zero-baseline-axes]
Bar and column charts always start at zero - never a truncated axis - so the
length of a bar is proportional to its value and visual comparisons aren't
exaggerated. The waterfall lane in `StatementTable` shares one zero baseline
across all rows, and steps that cross zero render to the left of it.
## Who / What / When titles, message kept separate [#who--what--when-titles-message-kept-separate]
A report title answers **Who** (entity or unit), **What** (measure and unit,
e.g. "Revenue - € thousands") and **When** (period and comparison). The
*interpretive* key message ("Up 17.5% on prior year") is kept separate from the
descriptive title - `Report` takes a structured `title` and a `message` field
for exactly this reason.
Acme Corporation
Revenue - € thousands
FY 2026 vs PY
Key message · Up 17.5% on prior year, led by Service revenue.
## In short [#in-short]
Fills carry the scenario, colour carries favorability, length carries magnitude
from zero, and the title states facts while the message states the point.
* [Conformance](/docs/conformance) - lint a config against the IBCS notation
rules the library implements.
* [Theming](/docs/theming) - change the palette without breaking the grammar.
* [Data model](/docs/data-model) - the shapes that carry `higherIsBetter` and
the scenario values.
---
# Overview
URL: https://ibcs-react.com/docs
**ibcs-react** is a zero-dependency React charting library for business
communication in the IBCS® notation - the basis of ISO 24896. Variance
columns, waterfalls, statement tables, small multiples, dashboards and whole
reports: printable, self-explanatory, and consistent by construction.
npm
pnpm
yarn
bun
```bash
npm install ibcs-react
```
```bash
pnpm add ibcs-react
```
```bash
yarn add ibcs-react
```
```bash
bun add ibcs-react
```
```tsx
import { VarianceColumnChart } from "ibcs-react";
;
```
## Where to go next [#where-to-go-next]
* [Getting started](/docs/getting-started) - install, first chart, sizing.
* [Components](/docs/components) - every chart and table with live examples
and prop tables generated from the source types.
* [Playground](/playground) - configure charts live in the browser.
---
# Industry reports
URL: https://ibcs-react.com/docs/industry-reports
The management report a controller in each of six industries would recognise -
SaaS, retail, manufacturing, banking, healthcare and a classic corporate profit
& loss - each built entirely from `ibcs-react` components. Pick the one closest
to your world and copy the composition.
Every report follows the same pattern: the **message** first, then
**who / what / when**, then a KPI strip, one or two charts and a statement or
table. Data is realistic but fictitious, and company names are invented.
Two notation habits hold throughout, and they are worth copying. There are no
gauges or pies for KPIs - a `KpiCard` with an impact-coloured delta says more in
less space. And every cost, churn, scrap or ratio measure carries
`higherIsBetter={false}`, so an increase reads unfavorable even though its sign
is positive.
## SaaS [#saas]
*Helix Cloud Inc. (B2B SaaS) · ARR, retention and unit economics - AC vs PY, PL and FC · 2025*
**ARR grew to 48.6 mEUR (+22% vs PY) on 118% net revenue retention: expansion
more than covered churn, and the forecast holds the pace into Q4.**
**ARR bridge - opening to closing (mEUR)**
**MRR - monthly, AC and FC vs PY (kEUR)**
**ARR by plan tier - AC vs PY and PL (kEUR)**
The ARR bridge is the report's backbone: opening and closing are `flow: "result"`
levels, and the four movements between them are signed steps. Contraction and
churn carry `higherIsBetter={false}`, so growth in either reads red.
## Retail [#retail]
*Northgate Retail Group · Sales, like-for-like and margin by category - AC vs PY · FY 2025*
**Net sales reached 1,284 mEUR (+6.1% vs PY) on +3.2% like-for-like; Food and
Home led, while margin pressure in Apparel trimmed the gross rate by 0.4 points.**
**Like-for-like sales by category - ranked, AC vs PY (%)**
**Sales and gross margin by category - AC vs PY and PL (mEUR)**
Ranking by deviation, not by size, is what makes the category story readable:
Apparel is a mid-sized category but the only negative one, so it sorts to the
bottom of the ranking and into the message.
## Manufacturing [#manufacturing]
*Rhine Manufacturing AG, Plant Düsseldorf · Output, OEE, scrap and unit-cost variance - AC vs PL and standard · 2025*
**Unit cost rose +110 €/unit against standard - adverse material prices (+180)
outweighed favourable usage and overhead; OEE recovered to 78% after a weak Q2.**
**Unit-cost variance - standard to actual (€/unit)**
One modelling detail worth stealing: the standard cost is the *opening level*,
so it is `flow: "add"` (a full bar from zero), not `flow: "result"`. A result at
the top of a bridge would be drawn at the running total - which is zero before
anything has accrued - and render as an empty bar.
**OEE by quarter - AC vs plan (%)**
**Cost of production - AC vs PY and PL (kEUR)**
A cost statement builds *up*, so every line is `flow: "add"` - and every line is
`higherIsBetter: false`, which is what makes the deviation bars read correctly
on a report where more is worse.
## Banking [#banking]
*Meridian Bank · Net interest income, efficiency and loan book - AC vs PY · 2025*
**Pre-tax profit rose to 312 mEUR (+9% vs PY) as net interest income climbed
+94 mEUR; the cost-income ratio improved 2.1 points and loan losses stayed
contained.**
**Income statement - AC vs PY, calculation bridge (mEUR)**
**Loan book by segment - AC vs PY and PL (mEUR)**
Loan loss provisions and the two ratio KPIs are the only lines where more is
worse, and each says so once through `higherIsBetter` - the colours then follow
without any per-cell styling.
## Healthcare [#healthcare]
*St. Mary's Health System · Volumes, cost per case and quality - AC vs PY and PL · 2025*
**More patients at a lower cost per case: admissions +4% vs PY and cost per case
2% under plan, with a shorter stay freeing capacity - readmissions (+0.3 points)
the one watch item.**
**Patient volume by service line - AC vs PY (cases)**
**Cost per case by quarter - AC vs plan (€)**
The cost chart is the clearest illustration of impact colouring: `Q1` is above
plan and reads red, `Q2` to `Q4` are below plan and read green - the opposite of
what the raw sign would suggest, because the chart is told
`higherIsBetter={false}` once at the top.
## Corporate P\&L [#corporate-pl]
*Atlas Corporation · Group profit & loss - AC vs PY and PL · 2025*
**Net income reached 96.4 mEUR (+14% vs PY, +8% vs plan) as revenue grew 9% and
disciplined operating expenses lifted the EBIT margin 1.3 points to 18.7%.**
**Income statement - AC vs PY, calculation bridge (mEUR)**
**Profit & loss statement - AC with ΔPY and ΔPL (mEUR)**
The bridge and the table are two views of one model: the chart carries the shape
of the calculation, the table carries the exact figures and both deviations. In
a real report you would author the statement once and project it into both - see
[Data model](/docs/data-model) for the adapters that do it.
## Making these fit your layout [#making-these-fit-your-layout]
The charts above are drawn at fixed pixel widths, which is what keeps IBCS
geometry honest. To drop one into a fluid dashboard, wrap it in `ChartBox` (or
its `ScrollChart` preset) and pick a fit - see
[Sizing and fit](/docs/interaction#sizing-and-fit).
## Where to next [#where-to-next]
* [Example reports](/docs/example-reports) - the flagship chart and table
templates, one exhibit at a time.
* [Theming](/docs/theming) - put your palette on any of these without touching
the notation.
* [Report](/report) - the same composition driven by a JSON `ReportConfig`.
* [Conformance](/docs/conformance) - the library's own check against IBCS
notation rules, run over a report config.
---
# Interaction & data
URL: https://ibcs-react.com/docs/interaction
Everything that turns a static report into a live one: the tooltip and the
`onHover` / `onSelect` events charts emit, tables whose sort and collapse state
you can own, an async data hook with first-class loading and refresh state, and
the wrappers that fit a chart to the space it is given. All of it is SSR-safe
and adds no dependencies.
## Hover tooltips [#hover-tooltips]
Every chart that draws interactive marks - and `StatementTable`'s rows - shows a
floating tooltip. It is **on by default**: `tooltip` defaults to `true`, so
there is nothing to wire.
What it does today, exactly:
* **Trigger** - a pointer within a few pixels of the mark itself (the generous
hit band stays for clicks, but the tooltip no longer fires over blank plot
space), keyboard focus on a selectable mark, or a tap on touch, where the
panel anchors to the mark rather than the finger.
* **Placement** - on the client it renders into a `document.body` portal, so
an ancestor with `transform`, `filter` or `overflow` (a dashboard shell, a
`ChartBox`) can neither clip it nor re-anchor it. It sits 16 px from the
pointer and flips to the other side at the right or bottom viewport edge, and
it never captures the pointer.
* **Content** - the category as the title, the value, the comparison value and
the impact-coloured delta, printed at **full precision** even when the chart's
own labels are compact: the tooltip is where you go for the exact figure.
* **Dismissal** - `Escape` hides it without moving the pointer or focus
(WCAG 1.4.13), a tap elsewhere dismisses a tap-anchored one, and leaving or
blurring the mark clears it.
* **At rest** - nothing is rendered. The server output is just the ``, so
there is no stray panel in static HTML or in a print.
```tsx
import { VarianceColumnChart } from "ibcs-react";
// On by default - hover, focus or tap a column.
// Opt out, or listen yourself with the mirror of onSelect:
setHovered(h)} />
```
`onHover` fires with `{ category, scenario?, value, datum, x, y }` as the
pointer moves over a mark and with `null` when it leaves - the same payload as
`onSelect` plus the pointer's viewport coordinates.
## Custom tooltips [#custom-tooltips]
To design the panel yourself, switch the built-in one off and pair
[useChartHover](/docs/hooks#usecharthover) with `ChartTooltip`. The hook holds
the hovered datum and the pointer position and brings the Escape / outside-tap
dismissal with it; passing its `tooltipRef` to the panel lets pointer moves be
applied on an animation frame, so following the cursor costs no re-render.
```tsx
import { VarianceColumnChart, ChartTooltip, useChartHover, type ColumnDatum } from "ibcs-react";
function CustomTooltipChart({ data }) {
const hover = useChartHover();
return (
<>
(h ? hover.onMove(h, { clientX: h.x, clientY: h.y }) : hover.onLeave())}
/>
{hover.hovered && (
)}
>
);
}
```
For a mark you draw yourself there is no `onHover` to borrow: call
`hover.onMove(info, event)` from the element's own `onPointerMove` and
`hover.onLeave` from `onPointerLeave`. Any event carrying `clientX` / `clientY`
works, so the same panel can label a custom SVG shape.
## Click to filter [#click-to-filter]
`VarianceColumnChart`, `StructureChart`, `TrendChart`, `StackedChart` and
`PieChart` take an optional `onSelect`. Click any mark and it fires
`{ category, scenario?, value, datum }` - the whole element is a click target,
the cursor turns to a pointer, and the mark is reachable and activatable by
keyboard. Omit the prop and nothing changes: no visual, no behaviour.
Pair it with [useChartSelection](/docs/hooks#usechartselection), a small `Set`
model, to drive a second view:
```tsx
import { VarianceColumnChart, useChartSelection } from "ibcs-react";
function QuarterFilter({ data }) {
const sel = useChartSelection();
const chosen = data.filter((d) => sel.isSelected(d.category));
return (
<>
sel.toggle(info.category)}
/>
{chosen.map((d) => (
{d.category}: {d.AC}
))}
>
);
}
```
**When to use:** cross-filtering dashboards, drill-downs, or letting a user pick
which categories a detail table or a second chart should show.
## Controlled tables [#controlled-tables]
Every stateful table follows the React convention: a `default*` prop seeds the
uncontrolled behaviour, and a value + `on*Change` pair hands the state to you.
Provide the controlled value and the table never mutates it - it only reports
what the next state would be, which is what makes URL sync, persistence and two
views kept in step possible.
| Component | Uncontrolled seed | Controlled pair |
| ---------------- | ------------------------------------------------------------ | -------------------------------------------------------------------------------- |
| `DataTable` | `defaultSort`, `defaultCollapsed` | `sort` + `onSortChange`, `collapsed` + `onCollapsedChange` |
| `StatementTable` | `defaultCollapsed` (or a line's own `defaultCollapsed` flag) | `collapsed` + `onCollapsedChange` |
| `MatrixTable` | `defaultExpandedRows`, `defaultExpandedCols` | `expandedRows` + `onExpandedRowsChange`, `expandedCols` + `onExpandedColsChange` |
Note the matrix's polarity: it names what is **open**, because its period
columns deliberately start collapsed - a matrix opens year by year.
The sort below is owned by the page. Header clicks and the buttons write to the
same state, so they can never disagree:
```tsx
import { useState } from "react";
import { DataTable, type DataTableSort } from "ibcs-react";
const [sort, setSort] = useState({ key: "rev", dir: "desc" });
;
```
Collapse state works the same way. `onCollapsedChange` reports the **next**
sorted id list on every toggle, so persisting it is a one-liner:
```tsx
const [collapsed, setCollapsed] = useState(["opex"]);
;
```
Leave the controlled props off and the table owns its state as before -
`onCollapsedChange` then acts as a plain observer, useful for analytics.
The same engine is available on its own as
[useStatement](/docs/hooks#usestatement) when you render your own statement
chrome.
## Async and API data [#async-and-api-data]
`useAsyncData` runs a fetcher on mount and whenever its `deps` change, exposing
`loading` (the first load), `refreshing` (a re-fetch with the previous data
still on screen), `error`, `lastUpdated` and a manual `refetch()`. It hands the
fetcher an `AbortSignal`, cancels in-flight requests on unmount, refetch or an
`enabled` flip, ignores aborts, and can poll through `refreshMs`. Nothing
fetches during render, so it is SSR-safe.
`ChartState` is its rendering counterpart: a skeleton while loading, an error
message with an optional retry, an empty state, or your content - each slot
overridable through `renderLoading` / `renderError` / `renderEmpty`. The demo
below uses a mock fetcher that resolves after about 0.9 s.
```tsx
import { useAsyncData, ChartState, StatementTable } from "ibcs-react";
function LivePnl() {
const { data, loading, refreshing, error, refetch } = useAsyncData(
// The fetcher gets an AbortSignal - forward it to fetch().
(signal) => fetch("/api/pnl", { signal }).then((r) => r.json()),
{ refreshMs: 30_000, keepPreviousData: true },
);
return (
);
}
```
For a timer-driven feed with no network behind it - a demo, a monitoring wall -
reach for [useLiveData](/docs/hooks#uselivedata) instead.
## Sizing and fit [#sizing-and-fit]
Charts take explicit `width` and `height` in pixels, because IBCS geometry is
pixel work: a shared scale across small multiples, hairline gridlines, labels
that must not collide. To fit one into a fluid layout, wrap it the way you would
fit a picture into a frame.
`ChartBox` is the one sizing primitive. Pick a `fit` and it re-renders the chart
at the resolved integer size - unlike scaling a bitmap, text and strokes stay
crisp:
* `"scale"` (default) - fill the available width keeping the aspect ratio, but
never below `minWidth`; past that it scrolls. The everyday responsive choice.
* `"contain"` - scale to fit both dimensions, then letterbox and align the spare
space. Needs a bounded height (`maxHeight`).
* `"fixed"` - always the intrinsic size; the container scrolls around it.
* `"fill"` - stretch to the box: the width fills, the height is `maxHeight` or
the intrinsic height.
Switch the mode and resize the window to feel each:
```tsx
import { ChartBox, TrendChart } from "ibcs-react";
// fit works like an image's object-fit: "scale" | "contain" | "fixed" | "fill"
// A single chart child gets the resolved width/height cloned onto it:
;
// The render-prop form remains for when you need the numbers yourself:
{(w, h) => }
;
```
`align`, `verticalAlign`, `padding`, `background` and `scroll` place the chart
inside the box; the child receives whole pixels, never `0` or `NaN`. The same
two child forms work in `ScrollChart`, `ResponsiveChart` and `ChartFrame`.
### ScrollChart, ResponsiveChart and ChartFrame [#scrollchart-responsivechart-and-chartframe]
* **`ScrollChart`** is a thin preset of `ChartBox` for the common
"fill one dimension, scroll the other" case. Give it a `height` and it renders
`fit="scale"` with your `minWidth`; give it a `width` and it renders
`fit="fixed"` inside a `maxHeight` viewport. Reach for `ChartBox` directly
when you want control over fit, alignment or padding.
* **`ResponsiveChart`** is an independent aspect-ratio wrapper, not a `ChartBox`
preset. It measures its parent and hands the child integer dimensions; with
`aspect` set, the height is derived from the measured width
(`height = width / aspect`) and the container's own height is ignored, clamped
by `minWidth` / `minHeight` / `maxHeight`. Nothing is drawn before the first
measurement, so there is no layout jump. Use it when the ratio is the
requirement.
* **`ChartFrame`** is **deprecated**. It still exports and still works, but the
`fit` union on `ChartBox` covers both of its modes and adds `"scale"` and
`"fixed"` with the same `align` / `verticalAlign` / `padding` / `background`
controls. Port it by renaming the element.
```tsx
// A wide trend that stays readable on a phone by scrolling.
{(w, h) => }
// A 16:9 panel that follows its parent's width.
{(w, h) => }
```
All three measure with [useElementSize](/docs/hooks#useelementsize), which you
can use directly for your own containers.
## Where to next [#where-to-next]
* [Hooks](/docs/hooks) - the full signatures of `useChartSelection`,
`useChartHover`, `useAsyncData` and the rest.
* [Accessibility](/docs/accessibility) - keyboard reachability of selectable
marks, and how the tooltip satisfies WCAG 1.4.13.
* [Playground](/playground) - the same interaction wiring on live data.
---
# Table templates
URL: https://ibcs-react.com/docs/table-templates
The **ibcs.com table templates** - `T01`…`T04`, part of the catalogue behind
ISO 24896:2026, "Notation for business reporting". Each one is built live below
from library components on one palette: scenario-keyed values (AC / PY / PL /
FC), impact-coloured variances, grouped headers, build-up subtotals and
right-aligned numerics, with whitespace rather than vertical rules setting the
column groups apart.
`T01` and `T02` share one region dataset in the centre-label **flanking**
layout ([ComparisonTable](/docs/components/comparison-table)) and differ only in
how the variance is shown: plain numbers against embedded bars and pins. `T03`
is a multi-year profit and loss statement
([DataTable](/docs/components/data-table) with flow markers); `T04` is the
integrated [StatementTable](/docs/components/statement-table).
For the chart codes `C01`-`C13`, see [IBCS templates](/docs/templates).
## T01 · Table with hierarchical rows and variance columns [#t01--table-with-hierarchical-rows-and-variance-columns]
Row labels sit in the **centre**, flanked by two column groups - November (the
current month) on the left, January-November (year to date) on the right. Each
group lists PY · PL · AC, then **numeric** AC-PY and AC-PL variances, absolute
and percentage, impact-coloured. Countries build up to bold Europe / Americas /
Rest of world subtotals and a World grand total under a double rule.
Component: [ComparisonTable](/docs/components/comparison-table)
## T02 · Table with hierarchical rows and integrated bar charts [#t02--table-with-hierarchical-rows-and-integrated-bar-charts]
The same flanking hierarchy, but the variance is **embedded**: PY and AC stay
as figures, then ΔPY becomes a signed green/red magnitude **bar** and ΔPY% a
**pin** with off-scale arrows. Deviation is read from the marks at a glance
rather than from the digits.
Component: [ComparisonTable](/docs/components/comparison-table)
## T03 · Table with measure rows (a multi-year P\&L) [#t03--table-with-measure-rows-a-multi-year-pl]
A P\&L build-up: every line carries a `+` / `−` / `=` flow marker. The `=` lines
* Revenue, Gross profit and the other results - are bold with a top rule, and
Net income closes with a double rule. Columns span four years (2012-2014 PL and
AC, then the current 2015 PL and FC).
Component: [DataTable](/docs/components/data-table)
## T04 · Table with measure rows and integrated variances [#t04--table-with-measure-rows-and-integrated-variances]
The integrated statement: PY · AC figures, then ΔPY as a green/red bar and
ΔPY% as a pin. This is the dedicated `StatementTable` with `showWaterfall`
set to `false`, so the waterfall lane is dropped for a purely numeric
statement. Result lines are bold with rules, and flow markers run down the
labels.
Component: [StatementTable](/docs/components/statement-table)
```tsx
import { StatementTable } from "ibcs-react";
;
```
## Where to next [#where-to-next]
* [IBCS templates](/docs/templates) - the chart codes `C01`-`C13`.
* [Examples](/docs/examples) - the tables inside complete dashboards.
* [The data model](/docs/data-model) - the row, column and line shapes these
tables consume.
---
# IBCS templates
URL: https://ibcs-react.com/docs/templates
The components on this page reproduce the **ibcs.com template set** - the
catalogue of standard chart and table layouts described by the IBCS notation,
the basis of ISO 24896:2026, "Notation for business reporting". Each template
carries a code (`C01`…`C13` for charts, `T01`…`T04` for tables); below, every
chart code maps to the `ibcs-react` component that draws it, with a small live
preview.
Every preview is a real component rendered with sample data at roughly
300×180px. Follow the component link under a card for the full page: more
examples, the code and the prop table.
## Chart templates [#chart-templates]
### C01 · Stacked column charts [#c01--stacked-column-charts]
Vertical columns split into their parts - the composition of a total per
period.
Component: [StackedChart](/docs/components/stacked-chart)
### C02 · Stacked bar charts [#c02--stacked-bar-charts]
The same composition rotated: horizontal bars, so long category labels stay
readable.
Component: [StackedChart](/docs/components/stacked-chart)
### C03 · Multi-tier column chart [#c03--multi-tier-column-chart]
Actual next to its comparison, with the absolute and relative variance stacked
above as separate tiers.
Component: [GroupedVarianceChart](/docs/components/grouped-variance)
### C04 · Multi-tier bar charts [#c04--multi-tier-bar-charts]
The horizontal reading of the same chart: grouped bars with the variance tiers
as side-by-side panels.
Component: [GroupedVarianceChart](/docs/components/grouped-variance)
### C05 · Horizontal waterfall chart [#c05--horizontal-waterfall-chart]
Monthly actual against plan, with the period variances carried into a
horizontal waterfall that bridges the gap for the year.
Component: [ColumnVarianceWaterfallChart](/docs/components/column-variance-waterfall)
### C06 · Vertical waterfall chart [#c06--vertical-waterfall-chart]
The bar counterpart: grouped rows on the left, a vertical waterfall on the
right accumulating each row's contribution to the total variance.
Component: [BarVarianceWaterfallChart](/docs/components/bar-variance-waterfall)
### C07 · Line charts [#c07--line-charts]
A time series with its comparison scenario as a reference line - the standard
trend reading.
Component: [LineChart](/docs/components/line-chart)
### C08 · Area charts [#c08--area-charts]
The same series with the area between actual and baseline filled, so the
cumulative gap is the visible quantity.
Component: [AreaChart](/docs/components/area-chart)
### C09 · Scattergrams [#c09--scattergrams]
Two measures against each other, one point per object, with iso-lines marking
constant products or ratios.
Component: [ScatterChart](/docs/components/scatter-chart)
### C10 · Bubble charts [#c10--bubble-charts]
A scattergram with a third measure carried by the area of each mark.
Component: [BubbleChart](/docs/components/bubble-chart)
### C11 · Tree charts (calculation and ratio trees) [#c11--tree-charts-calculation-and-ratio-trees]
A ratio decomposed into its drivers, each node carrying its own small trend.
Components: [RatioTreeChart](/docs/components/ratio-tree) ·
[TreeChart](/docs/components/tree-chart)
### C12 · Vertical waterfalls [#c12--vertical-waterfalls]
A result built from its add and subtract steps - the bridge from one figure to
another.
Components: [WaterfallChart](/docs/components/waterfall-chart) ·
[WaterfallStatementChart](/docs/components/waterfall-statement)
### C13 · Small multiples [#c13--small-multiples]
One small chart per group, all on a shared scale, so the panels are directly
comparable.
Component: [MiniVarianceMultiples](/docs/components/small-multiples)
## Table templates [#table-templates]
### T01 · Table with hierarchical rows and variance columns [#t01--table-with-hierarchical-rows-and-variance-columns]
Rows of a hierarchy with their figures, then the variance against a base as an
embedded bar plus a percentage pin.
Component: [DataTable](/docs/components/data-table)
The table codes have a page of their own: [Table templates](/docs/table-templates)
builds `T01`-`T04` at full size, with the flanking layout, the multi-year
profit and loss statement and the integrated statement.
## C12 as an integrated statement [#c12-as-an-integrated-statement]
The vertical-waterfall idea also appears embedded in a financial statement:
each line is a step in the running total, with its variance bars alongside.
Component: [StatementTable](/docs/components/statement-table)
## One model, every template [#one-model-every-template]
The same scenario-keyed values (AC / PY / PL / FC) feed all of these. Switching
template is mostly a matter of choosing the component and its orientation, not
reshaping the data - see [The data model](/docs/data-model) for the shape every
component expects.
## Where to next [#where-to-next]
* [Table templates](/docs/table-templates) - `T01`-`T04` at full size.
* [Examples](/docs/examples) - the templates composed into complete dashboards.
* [IBCS and ISO 24896](/docs/ibcs) - the notation the templates encode.
* [Components](/docs/components) - the full reference.
---
# Theming
URL: https://ibcs-react.com/docs/theming
Every colour, font stack and scenario fill is a token. Pass a ready-made
preset, set one for a whole subtree with a provider, or override just the few
values you care about - overrides are deep-merged onto the theme underneath.
## How a component resolves its tokens [#how-a-component-resolves-its-tokens]
Nearest wins, and each layer is deep-merged onto the one below:
1. the component's own `tokens` prop (a partial override is fine), merged onto
2. the nearest `IbcsThemeProvider` theme, merged onto
3. `defaultTokens`.
Providers nest: an inner provider's override composes onto the outer theme
rather than replacing it. That means "brand palette at the app root, dark
surface for one panel, one red tweaked on one card" is three small overrides,
not three full themes.
## IbcsThemeProvider [#ibcsthemeprovider]
Set the theme once instead of threading a `tokens` prop through every
component:
```tsx
import { IbcsThemeProvider, KpiCard, StatementTable, tokenPresets } from "ibcs-react";
;
```
`tokens` accepts a full theme (a `tokenPresets` entry) or a partial
`IbcsTokensOverride` - either way it is deep-merged onto the parent theme.
Below, one provider (the Ocean preset) themes all three components; the middle
card additionally passes its own `tokens` prop, which wins over the provider
for that one leaf.
```tsx
{/* Provider theme + this card's own override - the prop wins. */}
```
## The tokens prop is deep-merged [#the-tokens-prop-is-deep-merged]
Every component takes `tokens?: IbcsTokensOverride`. You supply only the leaves
you want to change; everything else falls back to the provider theme and then
to `defaultTokens`. Tweaking the favorable colour does not force you to
re-specify all four scenario fills:
```tsx
import { StatementTable } from "ibcs-react";
;
```
Prefer the strict black/red "good = neutral" convention? Override `color.good`
to your neutral ink and keep `color.bad` red - the deep merge makes that a
one-line change.
## useIbcsTokens [#useibcstokens]
`useIbcsTokens(override?)` is what every component calls internally to resolve
its `tokens` prop against the active theme. It is public, so a custom chart you
build on the library's core helpers participates in the same theme:
```tsx
import { useIbcsTokens, type IbcsTokensOverride } from "ibcs-react";
function MyMiniBar({ tokens }: { tokens?: IbcsTokensOverride }) {
const t = useIbcsTokens(tokens); // provider theme + this override + defaults
return ;
}
```
Outside any provider it returns `defaultTokens` with your override applied, so
a component works standalone as well as inside a theme.
## The eight presets [#the-eight-presets]
The library ships eight full token sets plus a named registry:
* `defaultTokens` - neutral greys for actuals, muted green/red variance. Tuned
for a light surface.
* `oceanTokens` - a cool blue-grey set: dark navy actuals, light blue-grey
previous year, IBCS semantic green/red.
* `azureTokens` - a monochromatic bright-blue alternative: navy actuals, azure
previous year.
* `greenRedTokens` - the strict "good stands out, bad stands out" business
look: darker actuals, vivid green/red.
* `vividTokens` - teal/blue actual bars with vivid variance, for a more
colourful deck.
* `cvdTokens` - colour-vision-deficiency safe: favorable teal, unfavorable
orange. Only the impact colours change; scenario fills stay greyscale,
because IBCS distinguishes scenarios by fill, not hue.
* `monoTokens` - greyscale for black-and-white printing: favorable reads as a
darker grey, unfavorable as a lighter one, with the signed labels and the
hatched/framed fills carrying the rest.
* `darkTokens` - a dark-surface ink set (including `color.surface` and
`color.onFill`, so cards, tooltips and in-bar labels come along).
* `tokenPresets` - all eight under stable, autocompleting ids (`default`,
`ocean`, `azure`, `greenRed`, `vivid`, `cvd`, `mono`, `dark` - the
`TokenPresetId` type), with matching display names in `tokenPresetLabels` -
ready to drive a theme switcher:
```tsx
{
(Object.keys(tokenPresets) as TokenPresetId[]).map((id) => (
{tokenPresetLabels[id]}
));
}
```
Same data, same props - only `tokens` changes. Each card is painted with its
preset's own `color.surface`:
```tsx
import { VarianceColumnChart, greenRedTokens, tokenPresets } from "ibcs-react";
// Pass a whole preset…
// …or pick one by id from the registry (handy for a theme switcher):
```
## Dark surfaces [#dark-surfaces]
The surface itself is a token, so dark mode is a preset swap rather than a
fork: `darkTokens` carries `color.surface` (cards, menus, tooltips, the fill of
hollow plan shapes), `color.surfaceMuted` and `color.onFill` (ink drawn on a
solid bar) alongside the scenario fills.
```tsx
import { IbcsThemeProvider, KpiCard, StatementTable, darkTokens } from "ibcs-react";
;
```
## Cards and blocks [#cards-and-blocks]
How a KPI card or a `Report` block is framed is a theme decision too. The
`card` group is the default behind every `appearance` prop: `framedCard` (what
every preset ships with - a hairline border, gently rounded, not lifted) or
`flatCard` - no border, no rounding, no shadow, no padding; blocks separated
by whitespace alone. That is the IBCS SIMPLIFY ideal, and what a printed page
wants. It composes with any palette, so paper is one override:
```tsx
import { Report, flatCard, tokenPresets } from "ibcs-react";
;
// flat blocks in the Ocean palette
;
```
A component's own `appearance` prop still wins per instance:
` ` lifts one card in an otherwise
flat theme.
## What each token controls [#what-each-token-controls]
| Token | Type | What it drives |
| ------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `color.neutral` | `string` | Actual step bars (add / subtract waterfall steps). |
| `color.total` | `string` | Emphasised actual bars - subtotals and results. |
| `color.good` | `string` | Favorable variance (impact, not sign). |
| `color.bad` | `string` | Unfavorable variance. |
| `color.zero` | `string` | Zero / no-change marks. |
| `color.axis`, `color.gridline` | `string` | Axis line and gridlines. |
| `color.text`, `color.textMuted` | `string` | Labels and secondary text. |
| `color.rowBorder` | `string` | Table row rules and hairlines. |
| `color.surface`, `color.surfaceMuted` | `string` | Opaque component background (cards, tooltips, sticky cells, hollow plan fills) and its subtle tint. |
| `color.onFill` | `string` | Ink drawn on a solid scenario bar - in-bar value labels. |
| `scenario.AC` / `PY` / `PL` / `FC` | `IbcsScenarioStyle` | Per-scenario `fill`, `stroke` and `variant` (`"solid"`, `"frame"`, `"hatch"`). |
| `font.family` | `string` | Font stack for component chrome and SVG text. |
| `card.radius`, `card.border`, `card.borderWidth`, `card.shadow`, `card.padding` | `number`, `boolean \| string`, `number`, `boolean`, `number \| string` | How cards and report blocks are framed - the defaults behind every `appearance` prop. `framedCard` / `flatCard` are ready-made sets. |
## Where to next [#where-to-next]
* [IBCS & ISO 24896](/docs/ibcs) - why the fills and colours mean what they
mean before you change them.
* [Components](/docs/components) - every component takes the same `tokens`
prop.
---
# AreaChart
URL: https://ibcs-react.com/docs/components/area-chart
A single scenario filled from zero, with a second scenario drawn over it as a
reference line. Because the fill starts at zero, the area is only honest for
measures where zero is meaningful.
## When to use [#when-to-use]
A continuous magnitude over time - cash position, headcount, backlog - where
the filled area communicates level.
## Example [#example]
```tsx
import { AreaChart } from "ibcs-react";
// area only
```
To show the running over/under-performance against a benchmark rather than the
level, use [VarianceAreaChart](/docs/components/variance-area).
## Props [#props]
---
# BarVarianceWaterfallChart
URL: https://ibcs-react.com/docs/components/bar-variance-waterfall
Entities ranked by impact: their AC bars on one side, the bridge of their
absolute deviations on the other, framed by the previous-year and plan totals,
with a relative deviation tier.
## When to use [#when-to-use]
Explain a total movement by entity: who drove the gap from previous year to
actual, sorted by impact, as a bridge - with the plan and previous-year totals
framing it.
## Example [#example]
```tsx
import { BarVarianceWaterfallChart } from "ibcs-react";
;
```
`base` is the plan value and `py` last year's; passing `pyTotal` switches the
chart into the PY → AC bridge mode where the deviations must add up to the gap
between the two totals. `pctBase` picks which of the two the relative tier
measures against.
Sorting by variance is the point - it puts the entities that moved the total at
the top. Collect the long tail into an "Others" row so the bridge stays
readable.
## Props [#props]
---
# BubbleChart
URL: https://ibcs-react.com/docs/components/bubble-chart
A scattergram where the marker area encodes a third measure. Area, not radius,
is proportional to `size`, so the visual weight matches the number.
## When to use [#when-to-use]
Portfolio / positioning - market attractiveness vs share with revenue as bubble
size. Up to \~100 bubbles.
## Example [#example]
```tsx
import { BubbleChart } from "ibcs-react";
;
```
Name the units in the axis and size labels - a bubble chart without them is
unreadable. Beyond roughly a hundred bubbles the overlap wins; drop the size
dimension and use [ScatterChart](/docs/components/scatter-chart) instead.
## Props [#props]
---
# ColumnVarianceWaterfallChart
URL: https://ibcs-react.com/docs/components/column-variance-waterfall
Each period's actual and plan columns sit under a bridge of the period
deviations, which walks from the plan total on the left to the actual plus
forecast total on the right, with a relative deviation tier on top.
## When to use [#when-to-use]
A monthly plan-vs-actual build-up: see each period's columns, how the variances
bridge from the plan total to the actual+forecast total, and the relative
deviations - the flagship integrated period chart.
## Example [#example]
```tsx
import { ColumnVarianceWaterfallChart } from "ibcs-react";
;
```
Periods marked `isForecast` draw hatched, and the matching `forecast: true`
segment in `endTotal` keeps the year total honest about how much of it is still
forecast. The total columns are set apart from the period axis, as IBCS
prescribes for figures that are not part of the series.
## Props [#props]
---
# ComboChart
URL: https://ibcs-react.com/docs/components/combo-chart
Scenario columns on the left axis with a second, differently-scaled measure
drawn against a right-hand axis - and the column variance underneath.
## When to use [#when-to-use]
Two related measures with different units on one chart - e.g. revenue (€,
columns) and margin (%, line).
## Example [#example]
```tsx
import { ComboChart } from "ibcs-react";
;
```
Two axes only earn their keep when the two measures are genuinely related and
carry different units - always label both, and format the secondary series in
its own unit via `secondaryFormat`. If the units match, one axis is the honest
choice.
## Props [#props]
---
# ComparisonTable
URL: https://ibcs-react.com/docs/components/comparison-table
The flanking layout of the IBCS table templates **T01** and **T02**: the row
labels sit in the centre of the table, with one column group to their left
(here the current month) and a second, structurally identical group to their
right (year to date). Each group carries its own scenario columns - PY, PL,
AC - followed by the variances against a chosen base, either as numeric
figures (T01) or as embedded bars and pins (T02).
Rows are hierarchical: a top-level row with `children` renders as a bold
subtotal summed from its detail rows, and `showTotals` caps the table with a
grand total under a double rule.
## When to use [#when-to-use]
A period-over-period comparison where the same measures are read twice - a
month against the year to date, a quarter against the full year - and the
reader should scan both without the labels drifting to one edge. If you only
need one column group, use [DataTable](/docs/components/data-table); if the
rows build up to a result through add/subtract steps, use
[StatementTable](/docs/components/statement-table).
This component is a **static layout**: it holds no interactive state. There is
no sorting, and no collapse or expand - what you pass is what is rendered.
Hierarchy comes from the row tree, and everything else is computed from the
columns. Only a horizontal scroll wrapper is added when the two groups do not
fit their container.
## Example [#example]
Profit after tax by region, in kEUR. The left group prints numeric AC-PY
variances (`mark: "none"`, the T01 notation, with the pair centred under a
shared `subgroup` header); the right group embeds the same variance as a bar
plus a percentage pin (T02). `gapBefore` opens the IBCS group gap - whitespace,
never a rule.
```tsx
import { ComparisonTable } from "ibcs-react";
// One measure per side: `m` = the current month, `y` = year to date.
const left = [
{ key: "m_py", label: "PY", kind: "value", measure: "m", scenario: "PY" },
{ key: "m_pl", label: "PL", kind: "value", measure: "m", scenario: "PL" },
{ key: "m_ac", label: "AC", kind: "value", measure: "m", scenario: "AC" },
{
key: "m_dpy",
label: "ΔPY",
kind: "variance",
measure: "m",
base: "PY",
mode: "abs",
mark: "none",
subgroup: "AC-PY",
gapBefore: true,
},
{
key: "m_dpy_pct",
label: "%",
kind: "variance",
measure: "m",
base: "PY",
mode: "pct",
mark: "none",
subgroup: "AC-PY",
},
];
const right = [
{ key: "y_py", label: "PY", kind: "value", measure: "y", scenario: "PY" },
{ key: "y_pl", label: "PL", kind: "value", measure: "y", scenario: "PL" },
{ key: "y_ac", label: "AC", kind: "value", measure: "y", scenario: "AC" },
{
key: "y_dpy",
label: "ΔPY",
kind: "variance",
measure: "y",
base: "PY",
mode: "abs",
mark: "bar",
gapBefore: true,
},
{
key: "y_dpy_pct",
label: "ΔPY%",
kind: "variance",
measure: "y",
base: "PY",
mode: "pct",
mark: "pin",
gapBefore: true,
},
];
;
```
Both sides take the same `DataTableColumn` shape as
[DataTable](/docs/components/data-table), so a column is a value, a numeric
variance (`mark: "none"`, signed and impact-coloured, percentages in italics),
or an embedded mark (`"bar"` for absolute magnitude, `"pin"` for relative,
with off-scale arrows). Column widths are derived from the widest formatted
figure per side, so each group stays internally aligned while the two sides may
differ in width. Sparkline columns are not drawn in this layout.
See [table templates](/docs/table-templates) for the full T01-T04 set.
## Props [#props]
---
# DataTable
URL: https://ibcs-react.com/docs/components/data-table
The general comparison table: rows are entities (regions, teams, products),
columns are measures. A column is a plain value, a variance drawn as an
embedded bar or pin, or a sparkline - so magnitude, deviation and trend sit
side by side in one grid.
## When to use [#when-to-use]
A cross-entity comparison: revenue by region, headcount by team, cost by
category. Use [StatementTable](/docs/components/statement-table) instead when
rows build up to subtotals (a P\&L or a balance sheet), and
[ComparisonTable](/docs/components/comparison-table) when the row labels should
sit in the centre, flanked by two period groups.
## Example [#example]
Six regions and two measures (revenue and operating income), each with an
absolute ΔPY bar; revenue also gets a relative-variance pin and a sparkline.
Header clicks re-sort, and `showTotals` appends a summed row.
```tsx
import { DataTable } from "ibcs-react";
;
```
Rows may nest through `children`: a parent renders as a bold group that sums
its children and collapses on click.
## Sorting: controlled or uncontrolled [#sorting-controlled-or-uncontrolled]
Clicking a header cycles the sort: descending, then ascending, then off.
* **Uncontrolled** - seed the initial order with `defaultSort` (`{ key, dir }`,
or `null` for unsorted) and let the table own it. `onSortChange` still fires
as an observer.
* **Controlled** - pass `sort` (or `null`) plus `onSortChange`. The table
renders exactly that sort and never mutates it; each header activation
reports the next sort - `null` when the cycle clears it - for you to apply.
The collapsed groups follow the same convention: `defaultCollapsed` is the
uncontrolled seed, `collapsed` (a `ReadonlySet` or string array) plus
`onCollapsedChange` is the controlled pair. Once `collapsed` is provided, the
matching `default*` prop is ignored.
```tsx
const [sort, setSort] = useState({ key: "rev", dir: "desc" });
const [collapsed, setCollapsed] = useState([]);
;
```
## Props [#props]
---
# GroupedVarianceChart
URL: https://ibcs-react.com/docs/components/grouped-variance
AC and its comparison stand side by side rather than overlaid, with an absolute
and a relative deviation tier. The comparison keeps its own notation: PY solid
grey, PL a hollow frame, FC hatched.
## When to use [#when-to-use]
Compare two scenarios side by side (not overlaid) with both Δ and Δ% tiers -
the canonical IBCS multi-tier column chart (vertical) or multi-tier bar chart
(horizontal) layout. Use
[IntegratedVarianceChart](/docs/components/integrated-variance) instead when you
want the comparison overlaid behind AC.
## Example [#example]
```tsx
import { GroupedVarianceChart } from "ibcs-react";
// C03 - grouped columns (vertical), tiers stack: Δ% / Δabs / columns
;
```
In column orientation the tiers stack vertically (Δ% on top, then Δ, then the
columns); in bar orientation they sit side by side as panels. Set
`higherIsBetter={false}` for cost measures, and use `clampPct` so one extreme
percentage draws an off-scale arrow instead of compressing the tier.
## Props [#props]
---
# HorizontalWaterfallChart
URL: https://ibcs-react.com/docs/components/horizontal-waterfall
The same bridge as [WaterfallChart](/docs/components/waterfall-chart), turned on
its side: each step gets a full-width label, and a parallel comparison bridge
adds a row-aligned variance panel on the right.
## When to use [#when-to-use]
A P\&L / driver bridge where the line labels are long - laying the waterfall on
its side gives each step a full-width label, unlike the vertical
`WaterfallChart`. Add `comparisonData` for a row-aligned variance panel.
## Example [#example]
```tsx
import { HorizontalWaterfallChart } from "ibcs-react";
variance panel
/>;
```
`comparisonData` must carry the same categories in the same order - the
variance panel aligns row by row. `mark="pin"` draws those deviations as pins
instead of bars, which keeps small lines readable next to large ones.
## Props [#props]
---
# IntegratedVarianceChart
URL: https://ibcs-react.com/docs/components/integrated-variance
The signature integrated variance column chart: three tiers over one time axis
* relative deviation as pins on top, absolute deviation as bars in the middle,
and the AC columns with the comparison overlaid at the bottom. Forecast periods
are hatched, and an optional full-year total column is set apart on the right.
## When to use [#when-to-use]
A monthly actual-vs-plan (or vs previous year) story where you want the values,
the absolute deviation and the relative deviation read together - with a
hatched forecast tail and an optional full-year total.
## Example [#example]
```tsx
import { IntegratedVarianceChart } from "ibcs-react";
hatched
{ category: "Dec", AC: 211, PY: 183, isForecast: true },
]}
fyTotal={{
label: "2,036",
segments: [
{ label: "AC", value: 1458 },
{ label: "FC", value: 578 },
],
}}
/>;
```
Either deviation tier can be dropped (`showPctPanel`, `showAbsPanel`) when only
one of them carries the message. For a cost or expense measure set
`higherIsBetter={false}` so a rise reads as unfavorable.
Use [GroupedVarianceChart](/docs/components/grouped-variance) instead when the
comparison should stand beside AC rather than behind it.
## Props [#props]
---
# KpiCard
URL: https://ibcs-react.com/docs/components/kpi-card
A single headline number with one or more IBCS impact-coloured deltas - the
colour follows favorability, not the sign - and an optional sparkline under the
figure. Built to drop into a report grid as a `kpi` block.
## When to use [#when-to-use]
A single KPI in a dashboard strip - revenue, margin, headcount - with its
variance and trend at a glance.
## Example [#example]
```tsx
import { KpiCard } from "ibcs-react";
;
```
## More examples [#more-examples]
### A cost KPI (higherIsBetter = false) [#a-cost-kpi-higherisbetter--false]
For costs, an increase is unfavorable. Setting `higherIsBetter={false}` flips
the impact colouring, so the +1.3M rise reads red even though the number went
up.
```tsx
// Revenue up -> green (favorable)
// Cost up -> red, because higher is worse
```
### Units live in `format` [#units-live-in-format]
There is no `unit` prop. The unit is part of `format`: `currency` for a leading
symbol (`{ currency: "€" }` renders €30.1M), `suffix` for a trailing one
(`{ suffix: "%" }` renders 18.4%). The card states the unit once, muted, beside
the headline instead of repeating it on every delta.
```tsx
```
Set `animate={false}` to render the headline outright - no frame loop, no
re-renders - which is what you want in tests, print and SSR-heavy pages.
Users with `prefers-reduced-motion` set never see the count-up either way:
the default consults the OS preference and renders the final value
immediately, and SSR always emits the finished figure.
## Ratio measures: percentage-point deltas [#ratio-measures-percentage-point-deltas]
A percentage MEASURE - a margin, a rate, a share - moves in percentage
points. Declare it with `unit="ratio"` and the delta renders as `+0.6pp`,
while the relative delta is dropped: "+0.9%" beside "18.4%" invites reading
a relative change as points, the exact confusion ISO 24896 separates the two
notations to prevent.
```tsx
```
The linter pairs with it: `checkIbcs` emits a `ratio-units` info for a KPI
formatted with `suffix: "%"` that has not been declared a ratio.
## Props [#props]
---
# LineChart
URL: https://ibcs-react.com/docs/components/line-chart
Scenario lines in IBCS notation with point markers, plus an optional variance
panel against a chosen base.
## When to use [#when-to-use]
Trend over many periods (10+, up to hundreds of points). Lines carry markers so
they read as connectors, not continuous values. Use columns instead for few
periods.
## Example [#example]
```tsx
import { LineChart } from "ibcs-react";
; // markers auto-hide above ~40 points for performance
```
By default every scenario present in the data is drawn; narrow that with
`series={["AC", "PY"]}`. `showMarkers` forces the markers back on (or off)
when the automatic density rule picks the wrong side.
## Props [#props]
---
# MatrixTable
URL: https://ibcs-react.com/docs/components/matrix-table
Two hierarchies at right angles: a statement row tree (with `+` / `−` / `=`
flow markers) down the side, and a period tree - year, quarter, month - across
the top. Every leaf period prints its scenario sub-columns (PL and AC by
default) and, with `showVariance`, the ΔBudget between them. Both axes expand
and collapse in place.
## When to use [#when-to-use]
Budget-versus-actual reporting and any other planning grid where the reader
starts at the year and drills into the quarter or month that moved. For a
single-period statement use [StatementTable](/docs/components/statement-table);
for entities rather than periods use [DataTable](/docs/components/data-table).
## Example [#example]
Click a period header to drill into (or fold up) its children, and a row label
to break the line down. The label column stays frozen while the period bands
scroll.
```tsx
import { MatrixTable } from "ibcs-react";
quarter > month
values={values} // values[rowId][periodId][scenario]
scenarios={["PL", "AC"]}
showVariance
defaultExpandedCols={["2024"]}
/>;
```
A row with `children` and no values of its own aggregates them, so only the
leaf lines have to be supplied.
## Expansion: controlled or uncontrolled [#expansion-controlled-or-uncontrolled]
Note the polarity. Unlike [StatementTable](/docs/components/statement-table)
and [DataTable](/docs/components/data-table), which track what is *collapsed*,
the matrix names what is **expanded** - its periods deliberately start closed,
because a matrix is opened year by year.
* **Uncontrolled** - `defaultExpandedRows` and `defaultExpandedCols` seed the
two open sets on mount (rows default to everything except lines flagged
`defaultCollapsed`; periods default to those flagged `defaultExpanded`).
* **Controlled** - `expandedRows` / `expandedCols` (a `ReadonlySet` or
a string array) with `onExpandedRowsChange` / `onExpandedColsChange`. The
matrix then renders exactly those sets and never mutates them; every toggle,
and the Expand all / Collapse all buttons, report the next sorted ids for you
to apply. The change callbacks also fire uncontrolled, as observers.
```tsx
const [cols, setCols] = useState(["2024"]);
;
```
Each value sub-cell is an addressable target: `onCellClick` reports the row and
period ids and labels, the scenario (`"DELTA"` for the variance cell) and the
value, while `cellDecorations` draws a corner ribbon - enough to build a
comment layer on top of the grid. See the
[budget matrix](/docs/budget-matrix) guide for that walkthrough.
## Props [#props]
---
# PieChart
URL: https://ibcs-react.com/docs/components/pie-chart
A pie or donut with muted greys and at most one emphasised slice, plus a
single-share mode with an impact-coloured growth sliver for "pie multiples"
layouts. Included for the occasional valid share and to reproduce IBCS pie
multiple exhibits.
## When to use [#when-to-use]
Reach for a column or bar chart first - angles and areas are hard to compare.
Use a pie only for a single, simple part-to-whole share (or to reproduce an
IBCS "pie multiples" layout), never for trends or precise comparison.
`checkIbcs` is the library's own check against IBCS notation rules - pass it a chart or report
config and it returns rule violations. Its `linear-chart-type` rule permits only linear charts
(column, bar, line, area, scatter, bubble, waterfall), so a pie trips the linter by design. Prefer
a bar or column unless the pie is truly the point.
## Example [#example]
{[
{ label: "EMEA", value: 48, total: 100, delta: 3 },
{ label: "Americas", value: 41, total: 100, delta: -2 },
{ label: "APAC", value: 33, total: 100, delta: 6 },
{ label: "LATAM", value: 22, total: 100, delta: 1 },
].map((r) => (
))}
```tsx
import { PieChart } from "ibcs-react";
// Multi-slice (one emphasised slice, muted greys - not a rainbow)
// "Pie multiples" share with a green/red growth sliver
```
The second example above is the pie-multiples pattern: one donut per entity,
same size and same total, so the comparison happens across panels rather than
between slices. Donuts stay legible down to about 120px.
## Props [#props]
---
# RankingVarianceChart
URL: https://ibcs-react.com/docs/components/ranking-variance
Rows sorted by deviation: AC bars with the comparison overlaid, then the
absolute variance as bars, then the relative variance as pins - with a bold
total row underneath.
## When to use [#when-to-use]
Rank entities (regions, states, products) by their variance vs plan - see who
beats and who misses, with absolute and relative deviation side by side and a
bold total.
## Example [#example]
```tsx
import { RankingVarianceChart } from "ibcs-react";
;
```
The relative panel is the reason small bases stay honest: a tiny entity can
double while contributing almost nothing in absolute terms. Percentages beyond
`clampPct` draw an off-scale arrow instead of stretching the whole scale for
one outlier.
`baseLabel` is what the headers say - set it to `"PY"` when the base values are
last year's.
## Props [#props]
---
# RatioTreeChart
URL: https://ibcs-react.com/docs/components/ratio-tree
Every node in the calculation tree carries a small time series instead of a
single figure, with the operators (÷ − + ×) sitting on the branches - so both
the arithmetic and the movement of each driver are on the page.
## When to use [#when-to-use]
Decompose a ratio/KPI into its drivers and show each as a small trend, with the
operators on the branches - how a headline metric is built and how each driver
moves.
## Example [#example]
```tsx
import { RatioTreeChart } from "ibcs-react";
;
```
Put the unit in each node's label - a tree mixing percent and currency nodes is
otherwise ambiguous. `miniChart="line"` draws the node series as lines instead
of columns, which reads better for long series.
For the same decomposition with single values rather than series, use
[TreeChart](/docs/components/tree-chart).
## Props [#props]
---
# Report
URL: https://ibcs-react.com/docs/components/report
One component renders an entire page from a serializable `ReportConfig`: a
structured Who / What / When title, a key message, and a responsive grid of
blocks. Each block names its type, its span on the grid, and a config object -
so a report is data you can store, version and generate, not hand-written JSX.
## When to use [#when-to-use]
Assembling a dynamic report from cards and blocks that are authored, stored or
produced elsewhere - a report builder, a saved layout, a server-generated
dashboard. When the layout is fixed and hand-written, compose the individual
components directly instead.
## Example [#example]
```tsx
import { Report } from "ibcs-react";
const config = {
title: { who: "Acme", what: "Revenue - EUR thousands", when: "FY 2026 vs PY" },
message: "Up 17.5% on prior year.",
columns: 12,
blocks: [
{
id: "k1",
type: "kpi",
span: 4,
config: { label: "Revenue", values: { AC: 30.1e6, PY: 25.6e6 } },
},
{ id: "c1", type: "chart", span: 8, config: { type: "trend", data: months } },
{ id: "s1", type: "statement", span: 12, config: { lines: statement } },
],
};
;
```
## Blocks [#blocks]
Every block carries an `id`, an optional `span` (columns of the grid, which
defaults per type), an optional structured `title` and an optional `message` -
the interpretive one-liner, kept separate from the neutral title. The `type`
then selects the renderer and the shape of `config`:
| `type` | Renders | `config` |
| ----------- | -------------------------------------------------- | ----------------------------------------------------------------------------- |
| `kpi` | [KpiCard](/docs/components/kpi-card) | `KpiConfig` - label, scenario values, comparisons, format, optional sparkline |
| `chart` | `ConfiguredChart` | `ChartConfig` - a `type` plus that chart's data and options |
| `statement` | [StatementTable](/docs/components/statement-table) | `StatementBlockConfig` - lines, mode, variance columns, format |
| `table` | [DataTable](/docs/components/data-table) | `TableBlockConfig` - columns, rows, totals, initial sort, format |
| `text` | Prose | the block's own `title`, `message` and `body` (blank lines split paragraphs) |
On screens narrower than 760px the grid collapses to a single column, so every
block spans the full width; `collapseBelow` moves that breakpoint or, with
`false`, removes it. It is a screen rule only - when printed (or rendered to
PDF) the authored spans hold whatever the paper width, so an A4 page does not
turn a 12-column report into a stack.
How blocks are framed is the theme's `card` tokens: the default is a hairline
card, `flatCard` is whitespace alone - the IBCS SIMPLIFY look and what paper
wants. See [Theming](/docs/theming#cards-and-blocks). For the CSS a theme
cannot express, the parts have stable selectors: `.ibcs-report` (root, before
your `className`), `.ibcs-report-grid`, and one `.ibcs-report-block` per block
carrying `data-block-type` and `data-block-id`.
```tsx
import { Report, flatCard } from "ibcs-react";
// print / PDF: no frames, spans kept
;
```
Chart blocks go through `ConfiguredChart`, which validates the config and
renders a readable message instead of throwing when it is invalid - 11 of the
23 chart components are reachable this way. Use `validateReportConfig` to check
an untrusted (JSON-authored) report before rendering it.
```tsx
import { ConfiguredChart, validateReportConfig } from "ibcs-react";
;
const result = validateReportConfig(JSON.parse(saved));
if (!result.ok) console.warn(result.error);
```
Blocks that measure the same thing can be tagged with the same
`sharedScaleGroup` and resolved with `sharedScales` (or the standalone
`resolveSharedScales`). Today this is advisory: the resolved domain is
published on each chart block's wrapper as `data-shared-scale-group` and
`data-shared-scale-domain`, rather than forced onto the rendered axis.
## Props [#props]
### ConfiguredChart [#configuredchart]
---
# ScatterChart
URL: https://ibcs-react.com/docs/components/scatter-chart
Two measures on two value axes, points optionally grouped, with hyperbolas of
constant x·y drawn behind them - the iso-lines that turn a cloud into a
statement about a meaningful product (e.g. equal gross profit).
## When to use [#when-to-use]
Correlation between two measures - margin vs revenue, price vs volume.
Iso-lines reveal a meaningful product (x·y).
## Example [#example]
```tsx
import { ScatterChart } from "ibcs-react";
;
```
The chart handles anything from a handful of points to thousands - dense data
is down-sampled automatically. Label only the points that carry the message;
unlabelled points still read as the cloud they belong to.
## Props [#props]
---
# SmallMultiples
URL: https://ibcs-react.com/docs/components/small-multiples
Two components: `MiniVarianceMultiples`, a ready-made grid of mini variance
panels (one per group), and the generic `SmallMultiples`, which renders any
chart per item and hands each panel the shared scale.
## When to use [#when-to-use]
Structure / comparison across many groups - regions, products, business units -
where one big chart would be cluttered. Shared scaling keeps panels honestly
comparable.
## Example [#example]
```tsx
import { MiniVarianceMultiples, SmallMultiples } from "ibcs-react";
// Built-in: one mini ΔPY panel per group, shared scale
// Generic: render any chart per item with a shared scale
r.points.flatMap((p) => [p.AC, p.PY])}
renderItem={(r, scale) => axis */ />}
/>
```
`valuesOf` is what makes the grid honest: every number a panel contributes goes
into one shared domain, so a bar in one panel means the same as a bar in the
next. `sharedScale` on `MiniVarianceMultiples` additionally rounds that domain
to a nice symmetric bound (and, via `showScaleHint`, states it once as a caption
above the grid); `clampPercentile` stops one extreme group flattening the
others.
## Props [#props]
### MiniVarianceMultiples [#minivariancemultiples]
### SmallMultiples [#smallmultiples]
---
# Sparkline
URL: https://ibcs-react.com/docs/components/sparkline
A micro-chart with no axes and no labels: the shape carries the trend, the
number next to it carries the value. The last point is the current value.
## When to use [#when-to-use]
Inline trend context next to a number - inside a KPI card or a table cell.
## Example [#example]
```tsx
import { Sparkline } from "ibcs-react";
```
`KpiCard` draws its own sparkline (`sparkline` + `sparklineType`), so reach for
this component directly when you need the micro-chart somewhere else - a table
cell, a list row, next to a headline in prose.
## Props [#props]
---
# StackedChart
URL: https://ibcs-react.com/docs/components/stacked-chart
Series stack in the order you declare them, each category printing its total at
the end of the stack. `orientation="column"` is the C01 time layout;
`orientation="bar"` is the C02 structure layout.
## When to use [#when-to-use]
Part-to-whole over time or across a structure - when one series dominates and
the total matters. IBCS prefers small multiples when many series compete.
## Example [#example]
```tsx
import { StackedChart } from "ibcs-react";
;
```
Only one series can carry the story: `highlight="product"` emphasises that
segment and greys the rest. If several series compete for attention, split the
chart into [small multiples](/docs/components/small-multiples) instead.
## Props [#props]
---
# StatementTable
URL: https://ibcs-react.com/docs/components/statement-table
A financial statement rendered as one integrated view: the label column, an
embedded waterfall lane for the actual scenario, and right-hand variance panels
against each comparison base. Deviation and structure are read in a single
pass, without leaving the table.
## When to use [#when-to-use]
An income statement, a balance sheet, or any build-up of figures to subtotals -
lines that add, subtract and resolve into results. For a cross-entity
comparison (revenue by region, headcount by team) where rows do not build up to
a total, use [DataTable](/docs/components/data-table) instead.
## Example [#example]
```tsx
import { StatementTable } from "ibcs-react";
;
```
## Pin mark and relative variance [#pin-mark-and-relative-variance]
Draw the waterfall lane as pins and add a percent (ΔPY%) panel - pins keep
small bases honest against the absolute bars.
```tsx
```
## Stock mode - a balance sheet [#stock-mode---a-balance-sheet]
The same component renders point-in-time levels instead of a flow waterfall:
each line is an absolute bar, with no running total. Liability lines carry
`higherIsBetter: false`, so a rise in debt reads as unfavorable. Groups with
`children` collapse and expand.
```tsx
```
## Collapsed groups: controlled or uncontrolled [#collapsed-groups-controlled-or-uncontrolled]
Grouped statements track a **collapsed** set of group ids. Both modes are
supported, and mixing them is the usual mistake:
* **Uncontrolled** - pass `defaultCollapsed` (a seed applied on mount, which
overrides each line's own `defaultCollapsed` flag) and let the table own the
state. `onCollapsedChange` still fires, as a plain observer.
* **Controlled** - pass `collapsed` (a `ReadonlySet` or a string array).
The table then renders exactly that set and never mutates it; every toggle,
and the Expand all / Collapse all buttons, report the next ids through
`onCollapsedChange` for you to apply - useful for URL sync, persistence, or
keeping two tables in step.
```tsx
const [collapsed, setCollapsed] = useState(["noncurrent-assets"]);
;
```
Set `maxHeight` for long consolidations: the body scrolls under a sticky
header and row virtualization turns on by default (`virtualize`).
## Props [#props]
---
# StructureChart
URL: https://ibcs-react.com/docs/components/structure-chart
Components ranked largest to smallest as horizontal bars, each overlapped with
its comparison scenario, with the share of the total and a variance beside it.
## When to use [#when-to-use]
Structure / composition - revenue by region, cost by category. Ranked, with a
total row.
## Example [#example]
```tsx
import { StructureChart } from "ibcs-react";
;
```
The name key is `category` - the same key every other chart's data uses - so
one array can feed a `VarianceColumnChart` and a `StructureChart` unchanged.
(`label`, the only key before v1.1, still works as an alias.)
## More examples [#more-examples]
### Composition vs plan [#composition-vs-plan]
Rank the same regions but measure each against plan - Middle East & Africa,
ahead of a small plan, stands out.
```tsx
```
Cost components take `higherIsBetter: false` on the row so an increase reads as
unfavorable.
## Props [#props]
---
# TreeChart
URL: https://ibcs-react.com/docs/components/tree-chart
Nodes carry a value, an optional prior-year value and the operator that binds
their children (`+`, `-`, `*`, `/`), so the arithmetic of a derived KPI is on
the page next to its result.
## When to use [#when-to-use]
Show how a derived KPI decomposes into its drivers, each with its value and Δ
vs prior year.
## Example [#example]
```tsx
import { TreeChart } from "ibcs-react";
;
```
Each node can override `format`, which matters when a ratio and its
currency-valued drivers share one tree. Supply `py` per node and the impact
colouring shows which driver moved the headline; `showVariance={false}` turns
the deltas off.
For the same decomposition with a mini time series per node, use
[RatioTreeChart](/docs/components/ratio-tree).
## Props [#props]
---
# TrendChart
URL: https://ibcs-react.com/docs/components/trend-chart
Actuals as solid columns, forecast periods hatched, prior year and plan drawn
as reference lines over them, and a variance panel underneath. A `summary` row
is set off as a total - on its own terms: summaries stay out of the period
scale, so a full-year total is drawn capped with a marked scale break and its
value label instead of crushing the months into slivers.
## When to use [#when-to-use]
Trend over time - 12-13 periods with actuals then forecast, against prior year
and plan.
## Example [#example]
```tsx
import { TrendChart } from "ibcs-react";
;
```
## More examples [#more-examples]
### Relative variance vs plan [#relative-variance-vs-plan]
Compare the forecast tail against plan as a percent - the hatched FC periods
carry through to the percent variance panel.
```tsx
```
Reference lines follow IBCS notation: PY solid with dots, PL dashed. Drop one
by narrowing `referenceLines`, e.g. `referenceLines={["PY"]}`.
### A year plus its total [#a-year-plus-its-total]
Mark the total with `summary: true`. It is set off by a divider, drawn in the
emphasis colour - and kept OFF the period scale: the total is \~12× any month,
so on a shared axis the months would collapse to slivers. Instead the summary
column (and its variance bar) is capped with a marked scale break, its real
value printed above; the PY line stops at December, because a total is not a
point in the time series. A summary in the same range as the periods (a
monthly average, say) shares the scale and gets no break.
```tsx
```
## Props [#props]
---
# VarianceAreaChart
URL: https://ibcs-react.com/docs/components/variance-area
The actual line runs over a grey reference; the band between them is filled
with the impact colour, so favorable and unfavorable stretches read as shapes.
The tail can be hatched as forecast.
## When to use [#when-to-use]
Show a trend against a benchmark (average Ø, previous year, plan) where the
running over/under-performance matters - favorable and unfavorable stretches
read at a glance. Tiles well as small multiples.
## Example [#example]
```tsx
import { VarianceAreaChart } from "ibcs-react";
;
```
The reference can be any benchmark - a location average, previous year, plan -
as long as `referenceLabel` names it. For a cost measure set
`higherIsBetter={false}` so the fill above the reference turns red.
Because a panel stays legible when small, this chart tiles well through
[SmallMultiples](/docs/components/small-multiples).
## Props [#props]
---
# VarianceColumnChart
URL: https://ibcs-react.com/docs/components/variance-column
The workhorse period comparison: solid AC columns over the comparison
scenario's own notation (PY solid grey, PL hollow frame, FC hatched), with an
absolute or percent variance panel underneath.
## When to use [#when-to-use]
Comparison / deviation across a few categories or periods - e.g. revenue by
quarter, AC vs PY.
## Example [#example]
```tsx
import { VarianceColumnChart } from "ibcs-react";
;
```
## More examples [#more-examples]
### Relative variance as pins [#relative-variance-as-pins]
Switch the lower panel to percent and the marks to pins - Q4 is well ahead of
last year, which the percent variance makes explicit.
```tsx
```
### Compare against plan (PL) [#compare-against-plan-pl]
Point the variance at a different base - here actuals vs plan instead of prior
year.
```tsx
```
## Props [#props]
---
# WaterfallChart
URL: https://ibcs-react.com/docs/components/waterfall-chart
Contributions float above a running total; `result` rows drop to a full
zero-based bar. Subtractions carry `higherIsBetter: false`, so a cost increase
reads as unfavorable in the variance panel.
## When to use [#when-to-use]
Variance contribution / bridge - explain how a total got from A to B (e.g.
revenue → costs → operating income), as a chart rather than a table.
## Example [#example]
```tsx
import { WaterfallChart } from "ibcs-react";
;
```
A `result` row needs no value - the component computes the running total. Pass
`comparisonData` (the same categories for another scenario) to get a variance
panel; `mark` decides whether those deviations draw as bars or pins.
For long line labels, lay the bridge on its side with
[HorizontalWaterfallChart](/docs/components/horizontal-waterfall); for a
statement with embedded variance columns use
[StatementTable](/docs/components/statement-table).
## Props [#props]
---
# WaterfallStatementChart
URL: https://ibcs-react.com/docs/components/waterfall-statement
Two row-aligned bridges - the comparison scenario and AC - followed by the
absolute and relative deviation tiers. The richest of the statement-as-chart
templates.
## When to use [#when-to-use]
Tell a full P\&L story comparing two scenarios as bridges: the previous-year and
actual waterfalls sit side by side, row-aligned, with ΔPY and ΔPY% tiers.
## Example [#example]
```tsx
import { WaterfallStatementChart } from "ibcs-react";
;
```
Expense lines carry `higherIsBetter: false`, so a cost above last year reads as
unfavorable even though the absolute deviation is positive. `showPctPanel={false}`
drops the relative tier when the absolute story is enough.
Prefer [StatementTable](/docs/components/statement-table) when the exact figures
matter more than the shape of the bridge.
## Props [#props]