# Accessibility URL: https://ibcs-react.com/docs/accessibility ibcs-react treats accessibility as part of the component contract, not an add-on. This page describes what every chart and table ships by default and what you should still do on your side. ## Screen readers: a real table behind every chart [#screen-readers-a-real-table-behind-every-chart] Every chart renders a visually-hidden `` next to its `` with the same numbers the chart draws - row and column headers, a `caption`, and values formatted exactly as the chart labels them. Screen-reader users read actual values instead of a picture description. (The hiding style sits on a wrapper `
`, not on the table - CSS table boxes treat the 1×1 clamp as a minimum, so styling the table directly would leave a full-height invisible box that inflates ancestor scrollbars.) The `` itself carries an accessible name (`role="img"` plus a descriptive `aria-label`) when the chart is non-interactive. It is never `aria-hidden` - the label and the data table complement each other. ```tsx ``` Tables (`StatementTable`, `DataTable`, `MatrixTable`, `ComparisonTable`) are semantic HTML tables with `scope` attributes and an optional visually-hidden `caption` prop. ## Keyboard [#keyboard] * **Selectable marks are buttons.** When a chart gets an `onSelect` handler, its marks become focusable with `role="button"` - `Tab` reaches them, `Enter`/`Space` activates, and focusing a mark shows the same tooltip hover shows, anchored to the mark (WCAG 1.4.13 hover-content-on-focus). * **Tables** - sorting headers, expand/collapse chevrons and matrix cells are keyboard-operable; internal buttons are `type="button"` so they never submit an enclosing form. * **ExportMenu** is a real menu: focus moves into it on open, arrow keys rove (wrapping), `Home`/`End` jump, `Escape` closes and restores focus. ## Tooltips (WCAG 1.4.13) [#tooltips-wcag-1413] Built-in tooltips appear on hover **near the mark**, on keyboard focus of a selectable mark, and on tap (touch). They are dismissible with `Escape` without moving the pointer or focus, and they never trap the pointer (the panel is `pointer-events: none`). For charts without `onSelect`, the hidden data table carries the same values the tooltip shows - keyboard users are not locked out of information. ## Color and motion [#color-and-motion] * The [CVD-safe preset](/docs/theming) swaps the red/green variance pair for a colour-vision-safe teal/orange; the Mono preset removes hue entirely. Variance is additionally always encoded by signed labels and IBCS fill/frame/hatch shapes, never by color alone. * Entrance animations respect `prefers-reduced-motion` and render the final state immediately, with no flash. Live data updates tween between values; under reduced motion they jump straight to the new state instead. ## What remains your job [#what-remains-your-job] * Give charts meaningful `title`s - they become the svg label and the data table caption. * Keep text contrast in your surrounding page; the default themes are tuned for their own surfaces. * If you replace the built-in tooltip via `onHover`, provide your own focus/dismiss behavior. --- # Budget matrix URL: https://ibcs-react.com/docs/budget-matrix A budget / control statement crossed two ways: a P\&L **row hierarchy** down the side and an **expanding period column tree** across the top. Click a year header to drill Year to Quarter to Month in place, expand the row groups, and read full-year Plan versus Actual with a signed ΔBudget - all on a sticky first column with horizontal scroll. This guide is the deep-dive; the component reference lives at [MatrixTable](/docs/components/matrix-table). ## What it does [#what-it-does] * **Row hierarchy** - a statement tree with `+` / `−` / `=` markers, bold subtotals with rules, a double rule on the final result, and expand/collapse on any group. * **Column tree** - periods nest (Year to Quarter to Month). Clicking a period header expands its children *in place*, with centered super-headers over the scenario sub-columns. * **Scenario sub-columns** - each leaf period fans out into Plan / Actual / Forecast (FC is italic and hatched, per IBCS notation), configurable per period. * **ΔBudget variance** - an optional AC − PL column per period, signed and coloured by business impact. * **Sticky scroll** - the label column is frozen and the header sticks, so many period columns scroll horizontally (and vertically once `maxHeight` caps the body). ## 1 · The budget and control statement [#1--the-budget-and-control-statement] The full picture: a P\&L for **three years** with Plan, Actual and a signed **ΔBudget**, opened on the live **Year to Quarter to Month** drill-down. 2024 is expanded to quarters and Q1 to its months - click any period header to collapse it or drill further, expand **Revenue** or **Operating expenses** to break the rows down, and scroll horizontally while the P\&L labels and the period bands stay aligned. ## Per-cell interaction - commentable cells [#per-cell-interaction---commentable-cells] Every value sub-cell (AC, the comparison, Δ) is an addressable, clickable target. `onCellClick` fires with the row and period ids and labels, the scenario (`"DELTA"` for the Δ cell) and the value; `cellDecorations` draws a corner ribbon; and each cell carries a `data-cell-ref` attribute (the value of `cellRefOf(rowId, periodId, scenario)`) so an outside panel can scroll to it and flash it. That is enough to build a full comment layer on top of the grid without forking the table. **Click any number below to flag it.** ```tsx import { MatrixTable, cellRefOf } from "ibcs-react"; const [comments, setComments] = useState(new Set()); // refs with a thread { const ref = cellRefOf(c.rowId, c.periodId, c.scenario); // "net::2024-01::DELTA" openCommentThread(ref, c); // your sidebar }} cellDecorations={(ref) => (comments.has(ref) ? { ribbon: true } : undefined)} getCellClassName={(ref) => (ref === selectedRef ? "is-flashing" : undefined)} />; // scroll to + flash a cell from the sidebar: document.querySelector(`[data-cell-ref="${ref}"]`)?.scrollIntoView({ block: "center" }); ``` ## 2 · IBCS scenario notation (Plan / Actual / Forecast) [#2--ibcs-scenario-notation-plan--actual--forecast] The classic statement template: historic years compare **PL** against **AC**, while the current year shows **PL** against **FC** - the forecast header italic and hatched. The scenario mix is set per period via `period.scenarios`; no variance column here. ```tsx const columns = [ { id: "2022", label: "2022", scenarios: ["PL", "AC"] }, { id: "2023", label: "2023", scenarios: ["PL", "AC"] }, { id: "2024", label: "2024", scenarios: ["PL", "FC"] }, // current year: forecast ]; ``` ## 3 · Quarters only, capped and scrolling [#3--quarters-only-capped-and-scrolling] The minimal layout when you do not need the multi-year drill-down: a single year as four quarters with a ΔBudget column. It scrolls horizontally under a sticky header, while `maxHeight` caps the body for vertical scroll. The Revenue and Operating-expenses groups start collapsed - `defaultExpandedRows={[]}` seeds an empty open set. ## 4 · Months only (one year) [#4--months-only-one-year] The simplest period layout: a single year shown straight as its **12 months**, with no year or quarter tier - pass the months as the top-level `columns`. It scrolls horizontally under the sticky label column. ## 5 · Years plus months (drill a year straight to months) [#5--years-plus-months-drill-a-year-straight-to-months] Two years, each expanding **directly into its 12 months**, skipping the quarter tier. Click a year header to open or close it, or use the **Expand all periods** toolbar (`columnExpandControls`) to open every year at once rather than one at a time. ## 6 · Monthly distribution (AC vs PL / PY) [#6--monthly-distribution-ac-vs-pl--py] A table gives the numbers; a chart gives the *shape* of the year. This companion view spreads full-year revenue across all **12 months** as solid **Actual** columns with a comparison column behind - toggle it between **Plan (PL)**, a hollow outline, and **Previous year (PY)**, solid grey. The variance panel below re-reads against whichever base you pick, so the same data answers both "are we on budget?" and "are we growing?". It is a plain [VarianceColumnChart](/docs/components/variance-column); clicking a month fires `onSelect`, which you would wire to filter the matrix above. ## Building the model [#building-the-model] Three serializable inputs: a `rows` tree, a `columns` period tree and a `values` lookup. Parent rows aggregate from their children automatically, so only the leaf lines have to be supplied - and derived subtotals should be stored at every granularity you let the reader drill to, so a quarter still adds up once it is opened. ```tsx import { MatrixTable, oceanTokens } from "ibcs-react"; // ROWS: a P&L tree - flow markers (+ / − / =), subtotals, drill-down. const rows = [ { id: "revenue", label: "Revenue", flow: "result", children: [ { id: "software", label: "Software revenue", flow: "add" }, { id: "support", label: "Support revenue", flow: "add" }, ], }, { id: "cogs", label: "Cost of sales", flow: "subtract", higherIsBetter: false }, { id: "gross", label: "Gross profit", flow: "result" }, ]; // COLUMNS: a PERIOD tree - a year expands into quarters, into months, in place. const columns = [ { id: "2023", label: "2023", children: [ { id: "2023-Q1", label: "Q1", children: [ { id: "2023-01", label: "Jan" }, { id: "2023-02", label: "Feb" }, { id: "2023-03", label: "Mar" }, ], }, ], }, { id: "2024", label: "2024" }, // PL/FC handled via period.scenarios ]; // VALUES: a serializable lookup, values[rowId][periodId][scenario]. const values = { software: { "2023": { PL: 543, AC: 565 }, "2023-Q1": { PL: 130, AC: 138 } }, // … parent rows (Revenue) auto-aggregate from their children. }; ; ``` `higherIsBetter: false` on the cost lines is what makes an overspend read red in the ΔBudget column: the variance is coloured by business impact, not by sign. Change which scenarios the Δ column compares with `varianceScenarios={{ actual: "AC", base: "PL" }}`. ## Props [#props] Every prop - including the controlled `expandedRows` / `expandedCols` pairs and the per-cell hooks used above - is documented on the component page: [MatrixTable](/docs/components/matrix-table). ## Where to next [#where-to-next] * [MatrixTable](/docs/components/matrix-table) - the full prop reference and the controlled/uncontrolled expansion contract. * [StatementTable](/docs/components/statement-table) - the same statement for a single period, with an integrated waterfall. * [DataTable](/docs/components/data-table) - entities rather than periods across the top. * [Data model](/docs/data-model) - scenarios, flows and `higherIsBetter`. * [IBCS & ISO 24896](/docs/ibcs) - why PL is hollow and FC is hatched. --- # Conformance URL: https://ibcs-react.com/docs/conformance Most chart libraries only let you draw. This one can also **check** a config against the IBCS notation rules it implements and tell you where the config departs from them - before it ships. ## A notation linter [#a-notation-linter] The same JSON config that *renders* a chart, KPI or report can also be *linted*. Call `checkIbcs(config)` and you get back an array of `IbcsFinding` objects: non-linear chart types, unstructured titles, missing data, wrong favorability for cost measures, and so on. An empty array means the config passes every rule the linter implements. It is pure logic - zero React, zero dependencies, JSON in and JSON out - so it runs in a unit test, a CI gate or an authoring tool. ```tsx import { checkIbcs, ConformanceReport } from "ibcs-react"; // A config that breaks the rules: pie isn't a linear chart type, the title is // a bare string (not Who/What/When), and there's no data to plot. const config = { type: "pie", title: "Revenue 2026", data: [] }; const findings = checkIbcs(config); // → [ // { rule: "linear-chart-type", severity: "error", path: "type", message: … }, // { rule: "data-present", severity: "error", path: "data", message: … }, // { rule: "structured-title", severity: "warning", path: "title", message: … }, // ] // An empty array means no rule was violated. // Or render the findings - pass a target to lint on the fly… // …or hand it findings you computed yourself: ``` The linter runs at build time, not as a house-style debate. A config either passes the IBCS notation rules the linter implements, or it lists exactly what to change - by rule id and JSON path. ## Lint the JSX path [#lint-the-jsx-path] Most apps author charts as JSX, not JSON configs - the props are the same shapes minus the `type` discriminator, which the component name carries. `checkIbcsProps` maps the name back and runs the same rules, so a dashboard written the way [getting started](/docs/getting-started) teaches is lintable in a unit test without restructuring: ```ts import { checkIbcsProps } from "ibcs-react"; // test("the revenue chart follows IBCS notation", () => { expect( checkIbcsProps("VarianceColumnChart", { data: productLines, comparison: "PY", variance: "abs", title: { who: "ACME", what: "Revenue (€k)", when: "2026" }, }), ).toEqual([]); }); ``` Render-only props (`width`, `tokens`, `onSelect`, …) carry no rules and are ignored; lint-only declarations (`measureKind`) can ride along. `KpiCard` props already are a `KpiConfig` and lint directly. The specialised variance charts lint as the linear family they render - and `checkIbcsProps("PieChart", …)` flags the pie, exactly as it should. ## Live playground [#live-playground] The config below deliberately breaks three rules: a `pie` (a non-linear chart type), a bare string `title` instead of a structured Who/What/When, and an empty `data` array. Edit the JSON and watch the findings update live - the same `ConformanceReport` component renders the result. Out of the box this example produces **two errors** (non-linear chart type, no data) and **one warning** (bare-string title). Switch `"pie"` to a linear type such as `"varianceColumn"`, give it a data row and replace the title with a structured `{ who, what, when }` object - the list empties and the report says so. Deleting the title is not a way out: a chart or report with **no** title is flagged at the same severity (ISO 24896 SAY requires one), and an unknown `type` string gets a did-you-mean plus the list of valid values. ## The rule catalog [#the-rule-catalog] Every finding references a rule id from `IBCS_RULES`, a serializable catalog you can surface in your own UI legends or docs. The full set the linter encodes: ```tsx import { IBCS_RULES } from "ibcs-react"; IBCS_RULES.map((rule) => `${rule.id} (${rule.severity}) - ${rule.title}`); ``` ## Declare what the measure is [#declare-what-the-measure-is] The `cost-favorability` rule normally detects cost measures from the title or KPI label ("Operating expenses", "Tax", …) - structured titles included. When the wording doesn't give it away, declare it: `measureKind: "cost"` on a chart or KPI config makes the linter insist on `higherIsBetter: false` no matter what the title says, and `measureKind: "revenue"` silences the heuristic for measures that merely sound like costs ("Cost recovery revenue"). The declaration is linter-only - rendering still follows `higherIsBetter`. ```ts checkIbcs({ type: "varianceColumn", measureKind: "cost", data, title }); // → cost-favorability warning until the config sets higherIsBetter: false ``` ## What a finding looks like [#what-a-finding-looks-like] Each `IbcsFinding` carries a `rule` id, a `severity` (`"error"`, `"warning"` or `"info"`), a human-readable `message` and an optional `path` locating the offending value (for example `blocks[2].config.type`). | Field | Type | Meaning | | ---------- | -------------------------------- | ------------------------------------------------------------- | | `rule` | `string` | Rule id - matches an entry in `IBCS_RULES`. | | `severity` | `"error" \| "warning" \| "info"` | Errors mark a broken rule; warnings and info are advisory. | | `message` | `string` | Human-readable description of the departure. | | `path` | `string?` | JSON-ish path to the offending value, e.g. `blocks[2].title`. | ## Scope [#scope] `checkIbcs` is the library's own check against the IBCS notation rules listed above, implemented from the publicly described notation. It is not a certification, an audit or an assessment by IBCS or ISO, and a clean run says nothing beyond "none of these rules were violated". Rules the linter does not implement - and anything about the correctness of your numbers - remain your call. ## Pair it with rendering [#pair-it-with-rendering] The linter works on the very same configs the components render, so you can lint in CI and render in the app from one source of truth. ```ts import { checkIbcs } from "ibcs-react"; test("dashboard configs break no notation rule", () => { for (const config of dashboardConfigs) { const errors = checkIbcs(config).filter((f) => f.severity === "error"); expect(errors).toEqual([]); } }); ``` * [IBCS & ISO 24896](/docs/ibcs) - what the rules mean. * [Components](/docs/components) - the config shapes the linter understands. --- # Report cookbook URL: https://ibcs-react.com/docs/cookbook 97 ready-made business reports and dashboards across seven domains, each a live mini-report built from one to four `ibcs-react` components. Figures are fictitious and deliberately varied (long and short labels; big, small and negative numbers; sparse and dense series), so the collection doubles as a visual stress test. Every recipe carries a *who / what / when* line, the live report and its source. One scenario-keyed model (AC / PY / PL / FC) feeds tables, charts and KPI cards alike - switching report type is mostly choosing the component, not reshaping the numbers. For the notation rules behind the marks, see [IBCS in practice](/docs/ibcs); for the data shapes, see the [data model](/docs/data-model). ## Shared shorthands [#shared-shorthands] To keep each snippet to a line or two, the samples below reuse a handful of local shorthands - datum builders, format presets and a width budget - defined once here: ```tsx import { KpiCard, Sparkline } from "ibcs-react"; const CARD_W = 560; // width budget every chart is sized to // format presets const fM = { compact: true, decimals: 1 } as const; // currency in millions const fK = { compact: true } as const; // compact, integer-ish const fN = { compact: false } as const; // plain counts / scores const fN1 = { compact: false, decimals: 1 } as const; const fPct1 = { compact: false, decimals: 1 } as const; // datum builders - a `category` plus optional AC / PY / PL / FC const C = (category, AC, PY, PL) => ({ category, AC, ...(PY !== undefined ? { PY } : {}), ...(PL !== undefined ? { PL } : {}), }); const L = (category, o) => ({ category, ...o }); const S = (category, AC, PY, PL, higherIsBetter) => ({ category, AC, ...(PY !== undefined ? { PY } : {}), ...(PL !== undefined ? { PL } : {}), ...(higherIsBetter === false ? { higherIsBetter: false } : {}), }); const W = (category, value, flow, higherIsBetter) => ({ category, value, ...(flow ? { flow } : {}), ...(higherIsBetter === false ? { higherIsBetter: false } : {}), }); // a DataTable value + ΔPY(bar) + ΔPY%(pin) column trio for one measure const varCols = (measure, label, higherIsBetter) => [ { key: measure, label, kind: "value", scenario: "AC" }, { key: measure + "_d", label: "ΔPY", kind: "variance", measure, base: "PY", mode: "abs", mark: "bar", ...(higherIsBetter === false ? { higherIsBetter: false } : {}), }, { key: measure + "_p", label: "ΔPY%", kind: "variance", measure, base: "PY", mode: "pct", mark: "pin", ...(higherIsBetter === false ? { higherIsBetter: false } : {}), }, ]; // KpiStrip / SparkTile are thin local layout wrappers around KpiCard / Sparkline. ``` Nothing in the library requires them: every builder just returns a plain object, so you can inline your own data or map it straight out of an API response. ## Finance & accounting [#finance--accounting] Statements, bridges, variance and working-capital views - the backbone of the monthly close. 18 recipes. Components used here: [AreaChart](/docs/components/area-chart), [ComboChart](/docs/components/combo-chart), [DataTable](/docs/components/data-table), [KpiCard](/docs/components/kpi-card), [LineChart](/docs/components/line-chart), [StackedChart](/docs/components/stacked-chart), [StatementTable](/docs/components/statement-table), [StructureChart](/docs/components/structure-chart), [TreeChart](/docs/components/tree-chart), [TrendChart](/docs/components/trend-chart), [VarianceColumnChart](/docs/components/variance-column), [WaterfallChart](/docs/components/waterfall-chart). ### Income statement (waterfall) [#income-statement-waterfall] Northwind Materials · € m · FY26 vs PY/PL ```tsx import { StatementTable } from "ibcs-react"; ; ``` ### Balance sheet [#balance-sheet] Northwind Materials · € m · point-in-time ```tsx import { StatementTable } from "ibcs-react"; ; ``` ### Cash flow bridge [#cash-flow-bridge] Helios Foods · € m · opening → closing cash ```tsx import { WaterfallChart } from "ibcs-react"; ; ``` ### P\&L bridge - PY → AC operating income [#pl-bridge---py--ac-operating-income] Helios Foods · € m · effect decomposition ```tsx import { WaterfallChart } from "ibcs-react"; ; ``` ### Budget vs actual - quarterly revenue [#budget-vs-actual---quarterly-revenue] Aurora Retail · € m · AC vs PL ```tsx import { VarianceColumnChart } from "ibcs-react"; ; ``` ### Revenue variance analysis [#revenue-variance-analysis] Aurora Retail · € m · 13 periods, AC vs PY ```tsx import { TrendChart } from "ibcs-react"; ; ``` ### Gross-margin walk [#gross-margin-walk] Cobalt Devices · € m · PY → AC ```tsx import { WaterfallChart } from "ibcs-react"; ; ``` ### Operating-expense breakdown [#operating-expense-breakdown] Cobalt Devices · € m · AC vs PY share ```tsx import { StructureChart } from "ibcs-react"; ; ``` ### Working-capital metrics [#working-capital-metrics] Northwind Materials · days · AC vs PY ```tsx import { KpiCard } from "ibcs-react"; // KpiStrip is a small local wrapper around KpiCard ; ``` ### AR aging by segment [#ar-aging-by-segment] Aurora Retail · € k · open receivables ```tsx import { StackedChart } from "ibcs-react"; ; ``` ### Revenue by region [#revenue-by-region] Northwind Materials · € m · AC vs PY ```tsx import { StructureChart } from "ibcs-react"; ; ``` ### EBITDA trend [#ebitda-trend] Helios Foods · € m · monthly, AC vs PL line ```tsx import { LineChart } from "ibcs-react"; ; ``` ### Multi-year P\&L statement [#multi-year-pl-statement] Vector Software · € m · 2012-2015 (wide → scroll) ```tsx import { DataTable } from "ibcs-react";
; ``` ### Cost-centre variance [#cost-centre-variance] Cobalt Devices · € k · AC vs PY ```tsx import { DataTable } from "ibcs-react"; ; ``` ### Capex vs depreciation [#capex-vs-depreciation] Northwind Materials · € m · invest vs D\&A % ```tsx import { ComboChart } from "ibcs-react"; ; ``` ### Free cash flow [#free-cash-flow] Helios Foods · € m · monthly with PY baseline ```tsx import { AreaChart } from "ibcs-react"; ; ``` ### Return-on-assets driver tree [#return-on-assets-driver-tree] Northwind Materials · ratio decomposition ```tsx import { TreeChart } from "ibcs-react"; ; ``` ### Interest-coverage & leverage [#interest-coverage--leverage] Northwind Materials · ratios · AC vs PY ```tsx import { KpiCard } from "ibcs-react"; // KpiStrip is a small local wrapper around KpiCard ; ``` ## Sales [#sales] Pipeline, attainment, channel and account performance, all on a zero baseline with impact colour. 14 recipes. Components used here: [DataTable](/docs/components/data-table), [LineChart](/docs/components/line-chart), [MiniVarianceMultiples](/docs/components/small-multiples), [ScatterChart](/docs/components/scatter-chart), [StackedChart](/docs/components/stacked-chart), [StructureChart](/docs/components/structure-chart), [TrendChart](/docs/components/trend-chart), [VarianceColumnChart](/docs/components/variance-column). ### Revenue by product line [#revenue-by-product-line] Aurora Retail · € m · AC vs PY ```tsx import { StructureChart } from "ibcs-react"; ; ``` ### Revenue by channel [#revenue-by-channel] Aurora Retail · € m · channel mix over quarters ```tsx import { StackedChart } from "ibcs-react"; ; ``` ### Sales-rep leaderboard [#sales-rep-leaderboard] Vector Software · € k · bookings AC vs PY ```tsx import { DataTable } from "ibcs-react"; ; ``` ### Sales pipeline funnel [#sales-pipeline-funnel] Vector Software · count · stage drop-off ```tsx import { StructureChart } from "ibcs-react"; ; ``` ### Win rate by region [#win-rate-by-region] Vector Software · % · AC vs PY ```tsx import { DataTable } from "ibcs-react"; ; ``` ### Quota attainment by team [#quota-attainment-by-team] Vector Software · % · AC vs target (PL=100) ```tsx import { VarianceColumnChart } from "ibcs-react"; ; ``` ### Bookings vs target [#bookings-vs-target] Vector Software · € m · monthly AC vs PL ```tsx import { TrendChart } from "ibcs-react"; ; ``` ### Discount vs deal size [#discount-vs-deal-size] Vector Software · won deals this quarter ```tsx import { ScatterChart } from "ibcs-react"; ; ``` ### New vs existing business [#new-vs-existing-business] Aurora Retail · € m · quarterly split ```tsx import { StackedChart } from "ibcs-react"; ; ``` ### Average deal size trend [#average-deal-size-trend] Vector Software · € k · 12 months (dense) ```tsx import { LineChart } from "ibcs-react"; L(m, { AC: 18 + Math.round(6 * Math.sin(i / 1.8)) + i, PY: 16 + i }), )} comparison="PY" width={CARD_W} height={210} format={fN} />; ``` ### Top accounts by revenue [#top-accounts-by-revenue] Aurora Retail · € k · AC vs PY (sortable) ```tsx import { DataTable } from "ibcs-react"; ; ``` ### Regional bookings - small multiples [#regional-bookings---small-multiples] Vector Software · € m · AC vs PY ```tsx import { MiniVarianceMultiples } from "ibcs-react";
; ``` ### Discount analysis by tier [#discount-analysis-by-tier] Aurora Retail · % · AC vs PY (cost-like) ```tsx import { VarianceColumnChart } from "ibcs-react"; ; ``` ### Lost-deal reasons [#lost-deal-reasons] Vector Software · count · this quarter ```tsx import { StructureChart } from "ibcs-react"; ; ``` ## Marketing [#marketing] Funnels, CAC/LTV economics, channel ROI and campaign performance. 13 recipes. Components used here: [AreaChart](/docs/components/area-chart), [ComboChart](/docs/components/combo-chart), [DataTable](/docs/components/data-table), [KpiCard](/docs/components/kpi-card), [LineChart](/docs/components/line-chart), [StackedChart](/docs/components/stacked-chart), [StructureChart](/docs/components/structure-chart), [VarianceColumnChart](/docs/components/variance-column). ### Marketing funnel [#marketing-funnel] Lumen Media · count · impressions → won ```tsx import { StructureChart } from "ibcs-react"; ; ``` ### MQL → SQL conversion [#mql--sql-conversion] Lumen Media · % · monthly AC vs PY ```tsx import { VarianceColumnChart } from "ibcs-react"; ; ``` ### CAC by channel [#cac-by-channel] Lumen Media · € · cost per acquisition (lower better) ```tsx import { StructureChart } from "ibcs-react"; ; ``` ### LTV : CAC & payback [#ltv--cac--payback] Lumen Media · ratio / months · AC vs PY ```tsx import { KpiCard } from "ibcs-react"; // KpiStrip is a small local wrapper around KpiCard ; ``` ### Channel ROI [#channel-roi] Lumen Media · € k · spend vs return (sortable) ```tsx import { DataTable } from "ibcs-react"; ; ``` ### Web traffic trend [#web-traffic-trend] Lumen Media · k sessions · AC vs PY ```tsx import { AreaChart } from "ibcs-react"; L(w, { AC: 120 + i * 9 + (i % 2 ? 8 : 0), PY: 110 + i * 6 }), )} scenario="AC" baseline="PY" width={CARD_W} height={210} format={fN} />; ``` ### Campaign performance [#campaign-performance] Lumen Media · multi-metric · current month ```tsx import { DataTable } from "ibcs-react"; ; ``` ### Email engagement [#email-engagement] Lumen Media · % · open vs click, 8 weeks ```tsx import { LineChart } from "ibcs-react"; L(w, { AC: 24 + (i % 3) * 2, PY: 21 + i * 0.4 }), )} comparison="PY" width={CARD_W} height={210} format={fN} />; ``` ### Spend & cost-per-lead [#spend--cost-per-lead] Lumen Media · € k / € · monthly combo ```tsx import { ComboChart } from "ibcs-react"; ; ``` ### Channel mix of MQLs [#channel-mix-of-mqls] Lumen Media · count · quarter-over-quarter ```tsx import { StackedChart } from "ibcs-react"; ; ``` ### Brand vs performance spend [#brand-vs-performance-spend] Lumen Media · € k · split by half ```tsx import { StackedChart } from "ibcs-react"; ; ``` ### SEO vs paid sessions [#seo-vs-paid-sessions] Lumen Media · k · two-series line ```tsx import { LineChart } from "ibcs-react"; L(m, { AC: 60 + i * 8, PY: 50 + i * 4 }), )} series={["AC", "PY"]} width={CARD_W} height={200} format={fN} />; ``` ### Landing-page conversion [#landing-page-conversion] Lumen Media · % · AC vs PL target ```tsx import { VarianceColumnChart } from "ibcs-react"; ; ``` ## Product & SaaS [#product--saas] Recurring-revenue, retention, engagement and adoption metrics for a subscription business. 14 recipes. Components used here: [BubbleChart](/docs/components/bubble-chart), [ComboChart](/docs/components/combo-chart), [KpiCard](/docs/components/kpi-card), [LineChart](/docs/components/line-chart), [MatrixTable](/docs/components/matrix-table), [StackedChart](/docs/components/stacked-chart), [StructureChart](/docs/components/structure-chart), [TrendChart](/docs/components/trend-chart), [VarianceColumnChart](/docs/components/variance-column), [WaterfallChart](/docs/components/waterfall-chart). ### MRR trend [#mrr-trend] Vector Software · € k · 13 periods, AC + forecast ```tsx import { TrendChart } from "ibcs-react"; ({ category: `M${i + 1}`, ...(d.AC ? { AC: d.AC / 1000 } : {}), ...(d.FC ? { FC: d.FC / 1000 } : {}), PL: (d.PL ?? 0) / 1000, }))} comparison="PL" width={CARD_W} height={236} format={fK} />; ``` ### ARR bridge [#arr-bridge] Vector Software · € k · beginning → ending ARR ```tsx import { WaterfallChart } from "ibcs-react"; ; ``` ### Net revenue retention [#net-revenue-retention] Vector Software · % · AC vs PY ```tsx import { KpiCard } from "ibcs-react"; // KpiStrip is a small local wrapper around KpiCard ; ``` ### Monthly churn rate [#monthly-churn-rate] Vector Software · % · lower is better ```tsx import { LineChart } from "ibcs-react"; L(m, { AC: 3.4 - i * 0.1 + (i % 2 ? 0.3 : 0), PY: 3.8 - i * 0.05 }), )} comparison="PY" higherIsBetter={false} variance="abs" width={CARD_W} height={236} format={fN1} />; ``` ### Cohort retention [#cohort-retention] Vector Software · % retained by month (matrix) ```tsx import { MatrixTable } from "ibcs-react"; ; ``` ### DAU & stickiness [#dau--stickiness] Vector Software · k users / DAU-MAU % ```tsx import { ComboChart } from "ibcs-react"; ; ``` ### NPS trend [#nps-trend] Vector Software · score · quarterly (with PY) ```tsx import { VarianceColumnChart } from "ibcs-react"; ; ``` ### Feature adoption [#feature-adoption] Vector Software · % of accounts using feature ```tsx import { StructureChart } from "ibcs-react"; ; ``` ### Active users by plan [#active-users-by-plan] Vector Software · k · plan mix over time ```tsx import { StackedChart } from "ibcs-react"; ; ``` ### Expansion vs contraction [#expansion-vs-contraction] Vector Software · € k · net by month ```tsx import { VarianceColumnChart } from "ibcs-react"; ; ``` ### Trial conversion funnel [#trial-conversion-funnel] Vector Software · count · signup → paid ```tsx import { StructureChart } from "ibcs-react"; ; ``` ### Support volume & CSAT [#support-volume--csat] Vector Software · tickets / CSAT % ```tsx import { ComboChart } from "ibcs-react"; ; ``` ### Time-to-value vs account size [#time-to-value-vs-account-size] Vector Software · onboarded accounts ```tsx import { BubbleChart } from "ibcs-react"; ; ``` ### ARPU by segment [#arpu-by-segment] Vector Software · € / month · AC vs PY ```tsx import { StructureChart } from "ibcs-react"; ; ``` ## Operations & supply chain [#operations--supply-chain] Inventory, service levels, capacity, supplier risk and quality. 13 recipes. Components used here: [AreaChart](/docs/components/area-chart), [BubbleChart](/docs/components/bubble-chart), [DataTable](/docs/components/data-table), [KpiCard](/docs/components/kpi-card), [LineChart](/docs/components/line-chart), [MiniVarianceMultiples](/docs/components/small-multiples), [StructureChart](/docs/components/structure-chart), [TrendChart](/docs/components/trend-chart), [VarianceColumnChart](/docs/components/variance-column), [WaterfallChart](/docs/components/waterfall-chart). ### Inventory by warehouse [#inventory-by-warehouse] Northwind Materials · € m · AC vs PY ```tsx import { StructureChart } from "ibcs-react"; ; ``` ### OTIF performance [#otif-performance] Northwind Materials · % · AC vs target (PL=95) ```tsx import { VarianceColumnChart } from "ibcs-react"; ; ``` ### Capacity utilization [#capacity-utilization] Cobalt Devices · % · 13 periods, AC vs PL ```tsx import { TrendChart } from "ibcs-react"; L(p, { AC: 74 + (i % 3) * 4 + i, PL: 80 }), )} comparison="PL" width={CARD_W} height={236} format={fN} />; ``` ### Demand vs production [#demand-vs-production] Cobalt Devices · k units · plan vs actual ```tsx import { LineChart } from "ibcs-react"; L(m, { AC: 40 + i * 3 + (i % 2 ? 4 : -2), PL: 42 + i * 3 }), )} comparison="PL" higherIsBetter variance="abs" width={CARD_W} height={236} format={fN} />; ``` ### Supplier risk map [#supplier-risk-map] Northwind Materials · spend vs risk score ```tsx import { BubbleChart } from "ibcs-react"; ; ``` ### Inventory & service KPIs [#inventory--service-kpis] Northwind Materials · AC vs PY ```tsx import { KpiCard } from "ibcs-react"; // KpiStrip is a small local wrapper around KpiCard ; ``` ### On-time delivery trend [#on-time-delivery-trend] Northwind Materials · % · weekly, AC vs PY ```tsx import { AreaChart } from "ibcs-react"; L(w, { AC: 88 + (i % 3) * 2, PY: 85 + i * 0.5 }), )} scenario="AC" baseline="PY" width={CARD_W} height={210} format={fN} />; ``` ### Defect rate (PPM) [#defect-rate-ppm] Cobalt Devices · ppm · lower is better ```tsx import { LineChart } from "ibcs-react"; L(m, { AC: 820 - i * 40 + (i % 2 ? 60 : 0), PY: 900 - i * 20 }), )} comparison="PY" higherIsBetter={false} width={CARD_W} height={210} format={fN} />; ``` ### Order backlog [#order-backlog] Cobalt Devices · € m · monthly with PY ```tsx import { AreaChart } from "ibcs-react"; L(m, { AC: 12 + i * 1.4 - (i > 3 ? 2 : 0), PY: 11 + i }), )} scenario="AC" baseline="PY" width={CARD_W} height={210} format={fK} />; ``` ### Lead time by supplier [#lead-time-by-supplier] Northwind Materials · days · AC vs PY ```tsx import { DataTable } from "ibcs-react"; ; ``` ### Production yield - small multiples [#production-yield---small-multiples] Cobalt Devices · % · AC vs PY by line ```tsx import { MiniVarianceMultiples } from "ibcs-react";
; ``` ### Freight cost bridge [#freight-cost-bridge] Northwind Materials · € k · PY → AC ```tsx import { WaterfallChart } from "ibcs-react"; ; ``` ### Scrap & rework cost [#scrap--rework-cost] Cobalt Devices · € k · PY → AC ```tsx import { WaterfallChart } from "ibcs-react"; ; ``` ## People & HR [#people--hr] Headcount, attrition, hiring, compensation and diversity. 13 recipes. Components used here: [BubbleChart](/docs/components/bubble-chart), [ComboChart](/docs/components/combo-chart), [DataTable](/docs/components/data-table), [KpiCard](/docs/components/kpi-card), [LineChart](/docs/components/line-chart), [StackedChart](/docs/components/stacked-chart), [StructureChart](/docs/components/structure-chart), [VarianceColumnChart](/docs/components/variance-column). ### Headcount by department [#headcount-by-department] Aurora Retail · FTE · AC vs PY ```tsx import { StructureChart } from "ibcs-react"; ; ``` ### Attrition trend [#attrition-trend] Aurora Retail · % annualized · lower better ```tsx import { LineChart } from "ibcs-react"; L(m, { AC: 14 - i * 0.3 + (i % 2 ? 1 : 0), PY: 16 - i * 0.2 }), )} comparison="PY" higherIsBetter={false} variance="abs" width={CARD_W} height={236} format={fN1} />; ``` ### Hiring funnel [#hiring-funnel] Aurora Retail · count · application → hire ```tsx import { StructureChart } from "ibcs-react"; ; ``` ### Compensation by level [#compensation-by-level] Aurora Retail · € k · base salary spread ```tsx import { BubbleChart } from "ibcs-react"; ; ``` ### Gender diversity by org [#gender-diversity-by-org] Aurora Retail · % · current ```tsx import { StackedChart } from "ibcs-react"; ; ``` ### Headcount plan vs actual [#headcount-plan-vs-actual] Aurora Retail · FTE · AC vs PL by quarter ```tsx import { VarianceColumnChart } from "ibcs-react"; ; ``` ### Tenure distribution [#tenure-distribution] Aurora Retail · FTE · by band ```tsx import { StackedChart } from "ibcs-react"; ; ``` ### Talent KPIs [#talent-kpis] Aurora Retail · AC vs PY ```tsx import { KpiCard } from "ibcs-react"; // KpiStrip is a small local wrapper around KpiCard ; ``` ### Span of control [#span-of-control] Aurora Retail · reports per manager · by org ```tsx import { DataTable } from "ibcs-react"; ; ``` ### Offer acceptance rate [#offer-acceptance-rate] Aurora Retail · % · monthly AC vs PY ```tsx import { LineChart } from "ibcs-react"; L(m, { AC: 78 + i + (i % 2 ? 3 : 0), PY: 75 + i }), )} comparison="PY" width={CARD_W} height={200} format={fN} />; ``` ### Training hours per FTE [#training-hours-per-fte] Aurora Retail · hours · AC vs PL target ```tsx import { VarianceColumnChart } from "ibcs-react"; ; ``` ### Workforce cost & FTE [#workforce-cost--fte] Aurora Retail · € m / FTE · combo ```tsx import { ComboChart } from "ibcs-react"; ; ``` ### Absenteeism rate [#absenteeism-rate] Aurora Retail · % · monthly, lower better ```tsx import { LineChart } from "ibcs-react"; L(m, { AC: 3.1 + (i % 3 === 0 ? 0.8 : -0.2), PY: 3.4 }), )} comparison="PY" higherIsBetter={false} width={CARD_W} height={200} format={fN1} />; ``` ## Executive & KPI scorecards [#executive--kpi-scorecards] Roll-ups for the board pack: scorecards, profit walks, regional multiples and a balanced scorecard. 12 recipes. Components used here: [AreaChart](/docs/components/area-chart), [ComparisonTable](/docs/components/comparison-table), [DataTable](/docs/components/data-table), [KpiCard](/docs/components/kpi-card), [MatrixTable](/docs/components/matrix-table), [MiniVarianceMultiples](/docs/components/small-multiples), [Sparkline](/docs/components/sparkline), [StructureChart](/docs/components/structure-chart), [TrendChart](/docs/components/trend-chart), [WaterfallChart](/docs/components/waterfall-chart). ### Company scorecard [#company-scorecard] Northwind Materials · group KPIs · AC vs PY ```tsx import { KpiCard } from "ibcs-react"; // KpiStrip is a small local wrapper around KpiCard ; ``` ### Revenue vs plan [#revenue-vs-plan] Group · € m · 13 periods, AC + FC vs PL ```tsx import { TrendChart } from "ibcs-react"; ; ``` ### Profit waterfall [#profit-waterfall] Group · € m · revenue → net income ```tsx import { WaterfallChart } from "ibcs-react"; ; ``` ### Regional performance [#regional-performance] Group · € m · AC vs PY (small multiples) ```tsx import { MiniVarianceMultiples } from "ibcs-react";
; ``` ### Strategic initiatives [#strategic-initiatives] Group · status & impact · current quarter ```tsx import { DataTable } from "ibcs-react"; ; ``` ### Market share [#market-share] Group · % · AC vs PY by category ```tsx import { StructureChart } from "ibcs-react"; ; ``` ### Customer satisfaction [#customer-satisfaction] Group · CSAT · sparkline tiles ```tsx import { Sparkline } from "ibcs-react"; // SparkTile is a small local wrapper around Sparkline
; ``` ### Cash position [#cash-position] Group · € m · 12-month area, AC vs PY ```tsx import { AreaChart } from "ibcs-react"; L(m, { AC: 4 + Math.round(Math.sin(i / 2) * 2) + i * 0.3, PY: 3.5 + i * 0.2 }), )} scenario="AC" baseline="PY" width={CARD_W} height={210} format={fK} />; ``` ### Top & bottom movers [#top--bottom-movers] Group · € k · biggest ΔPY swings ```tsx import { DataTable } from "ibcs-react"; ; ``` ### Balanced scorecard [#balanced-scorecard] Group · perspectives × quarters (AC vs PL) ```tsx import { MatrixTable } from "ibcs-react"; ; ``` ### ESG metrics [#esg-metrics] Group · sustainability KPIs · AC vs PY ```tsx import { KpiCard } from "ibcs-react"; // KpiStrip is a small local wrapper around KpiCard ; ``` ### Flanking comparison table [#flanking-comparison-table] Electronic Inc. · kEUR · month vs YTD ```tsx import { ComparisonTable } from "ibcs-react";
; ``` ## About these examples [#about-these-examples] All companies - Northwind Materials, Aurora Retail, Vector Software, Lumen Media, Cobalt Devices, Helios Foods and the like - and every number in them are invented for illustration. Charts are sized to a single `CARD_W` budget so the recipes stay comparable; wide tables scroll inside their frame rather than bleeding across the page. Want to try variations on your own numbers? The [playground](/playground) edits a live statement, and the [report](/report) route shows a full page assembled from these building blocks. --- # Dashboards URL: https://ibcs-react.com/docs/dashboards Seven realistic **management dashboards** - the things people most commonly build with BI and reporting tools - each assembled entirely from `ibcs-react` components. Every one is a multi-panel composition (a KPI strip, two or more charts and a table), leads with its **message** and names its **Who / What / When**. The figures are fictitious but plausible. Notation is consistent throughout: AC solid, PY grey, PL hollow frame, FC hatched; variance coloured by impact; cost lines carry `higherIsBetter: false` so a rise reads red. ## How a dashboard is put together [#how-a-dashboard-is-put-together] Three things do the work, and none of them is a chart: 1. **The headline is the conclusion.** Not "Revenue by region" but "APAC and the new Cloud line carried the quarter". The panels below it are the evidence. 2. **Who / What / When** under the headline - the audience, the measure and its unit, and the period. Without them a number is unreadable. 3. **One layout grid.** A four-up KPI strip, then panels in a two-column grid (sometimes with a wider left column), each panel a `
` 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.
flow · profit and loss
stock · balance sheet
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 }); ; ``` **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) => ( )); } ``` 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) => (
{r.label}
))}
```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]