ibcs-react

Budget matrix

A budget and control statement crossed two ways - a P&L row hierarchy against an expanding period column tree, with Plan / Actual / Forecast sub-columns and a signed ΔBudget.

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.

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

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.

 2022
JanFebMar
PLACΔBdgPLACΔBdgPLACΔBdgPLACΔBdgPLACΔBdgPLACΔBdgPLACΔBdgPLACΔBdg
=Revenue711661-50906895-116766-16766-18786-1241237-4241237-4260255-5
+Software revenue467453-14543565+224342-14342-15654-2155150-5155150-5167161-6
+Support revenue9987-12132121-11109-1109-11312-13532-33532-33835-3
+Consulting revenue145121-24231209-221415+11415+11820+25155+45155+45559+4
−Cost of sales282231-51199222+231617+11617+12122+15861+35861+36266+4
=Gross profit429430+1707673-345149-25149-26663-3184175-9184175-9198189-9
−Operating expenses113119+6188206+1815150151501920+15354+15354+15758+1
−Research & development7978-191104+138808801011+12830+22830+23032+2
−Selling & admin3441+797102+57707709902524-12524-12726-1
=Operating income (EBIT)316311-5519467-523734-33734-34744-3131122-9131122-9142131-11
+Other financial income, net812+463-300000001+112+112+112+1
=Income before tax324323-1525470-553734-33734-34844-4133123-10133123-10143133-10
−Income tax5954-53125-643-143-154-11512-31512-31613-3
=Net income265269+4494445-493331-23331-24240-2118111-7118111-7127120-7

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.

 2022
JanFebMar
PLACΔBdgPLACΔBdgPLACΔBdgPLACΔBdgPLACΔBdgPLACΔBdgPLACΔBdgPLACΔBdg
=Revenue711661-50906895-116766-16766-18786-1241237-4241237-4260255-5
+Software revenue467453-14543565+224342-14342-15654-2155150-5155150-5167161-6
+Support revenue9987-12132121-11109-1109-11312-13532-33532-33835-3
+Consulting revenue145121-24231209-221415+11415+11820+25155+45155+45559+4
−Cost of sales282231-51199222+231617+11617+12122+15861+35861+36266+4
=Gross profit429430+1707673-345149-25149-26663-3184175-9184175-9198189-9
−Operating expenses113119+6188206+1815150151501920+15354+15354+15758+1
−Research & development7978-191104+138808801011+12830+22830+23032+2
−Selling & admin3441+797102+57707709902524-12524-12726-1
=Operating income (EBIT)316311-5519467-523734-33734-34744-3131122-9131122-9142131-11
+Other financial income, net812+463-300000001+112+112+112+1
=Income before tax324323-1525470-553734-33734-34844-4133123-10133123-10143133-10
−Income tax5954-53125-643-143-154-11512-31512-31613-3
=Net income265269+4494445-493331-23331-24240-2118111-7118111-7127120-7
0 flagged · last click: nothing yet
import { MatrixTable, cellRefOf } from "ibcs-react";

const [comments, setComments] = useState(new Set<string>()); // refs with a thread

<MatrixTable
  rows={rows}
  columns={periods}
  values={values}
  scenarios={["PL", "AC"]}
  showVariance
  onCellClick={(c) => {
    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)

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.

 202220232024
PLACPLACPLFC
=Revenue711661906895965954
+Software revenue467453543565620610
+Support revenue9987132121140131
+Consulting revenue145121231209205213
−Cost of sales282231199222230240
=Gross profit429430707673735714
−Operating expenses113119188206210223
−Research & development797891104110124
−Selling & admin34419710210099
=Operating income (EBIT)316311519467525491
+Other financial income, net8126354
=Income before tax324323525470530495
−Income tax595431256031
=Net income265269494445470464
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

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.

 
PLACΔBdgPLACΔBdgPLACΔBdgPLACΔBdg
=Revenue222217-5241237-4241237-4260255-5
−Cost of sales5356+35861+35861+36266+4
=Gross profit169161-8184175-9184175-9198189-9
−Operating expenses4849+15354+15354+15758+1
=Operating income (EBIT)121112-9131122-9131122-9142131-11
+Other financial income, net11012+112+112+1
=Income before tax122113-9133123-10133123-10143133-10
−Income tax1411-31512-31512-31613-3
=Net income108102-6118111-7118111-7127120-7

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.

 JanFebMarAprMayJunJulAugSepOctNovDec
PLACΔBdgPLACΔBdgPLACΔBdgPLACΔBdgPLACΔBdgPLACΔBdgPLACΔBdgPLACΔBdgPLACΔBdgPLACΔBdgPLACΔBdgPLACΔBdg
=Revenue6766-16766-18786-17775-27775-28786-17775-27775-28786-17775-28786-19795-2
+Software revenue4342-14342-15654-25048-25048-25654-25048-25048-25654-25048-25654-26260-2
+Support revenue109-1109-11312-11110-11110-11312-11110-11110-11312-11110-11312-11413-1
+Consulting revenue1415+11415+11820+21617+11617+11820+21617+11617+11820+21617+11820+22122+1
−Cost of sales1617+11617+12122+11820+21820+22122+11820+21820+22122+11820+22122+12324+1
=Gross profit5149-25149-26663-35956-35956-36663-35956-35956-36663-35956-36663-37470-4
−Operating expenses15150151501920+117170171701920+117170171701920+1171701920+12122+1
−Research & development8808801011+19909901011+19909901011+19901011+11112+1
−Selling & admin77077099088088099088088099088099010100
=Operating income (EBIT)3734-33734-34744-34239-34239-34744-34239-34239-34744-34239-34744-35349-4
+Other financial income, net00000001+100000001+100000001+100001+1110
=Income before tax3734-33734-34844-44239-34239-34844-44239-34239-34844-44239-34844-45349-4
−Income tax43-143-154-154-154-154-154-154-154-154-154-165-1
=Net income3331-23331-24240-23836-23836-24240-23836-23836-24240-23836-24240-24744-3

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.

 
JanFebMarAprMayJunJulAugSepOctNovDec
PLACΔBdgPLACΔBdgPLACΔBdgPLACΔBdgPLACΔBdgPLACΔBdgPLACΔBdgPLACΔBdgPLACΔBdgPLACΔBdgPLACΔBdgPLACΔBdgPLACΔBdg
=Revenue906895-116766-16766-18786-17775-27775-28786-17775-27775-28786-17775-28786-19795-2
+Software revenue543565+224342-14342-15654-25048-25048-25654-25048-25048-25654-25048-25654-26260-2
+Support revenue132121-11109-1109-11312-11110-11110-11312-11110-11110-11312-11110-11312-11413-1
+Consulting revenue231209-221415+11415+11820+21617+11617+11820+21617+11617+11820+21617+11820+22122+1
−Cost of sales199222+231617+11617+12122+11820+21820+22122+11820+21820+22122+11820+22122+12324+1
=Gross profit707673-345149-25149-26663-35956-35956-36663-35956-35956-36663-35956-36663-37470-4
−Operating expenses188206+1815150151501920+117170171701920+117170171701920+1171701920+12122+1
−Research & development91104+138808801011+19909901011+19909901011+19901011+11112+1
−Selling & admin97102+577077099088088099088088099088099010100
=Operating income (EBIT)519467-523734-33734-34744-34239-34239-34744-34239-34239-34744-34239-34744-35349-4
+Other financial income, net63-300000001+100000001+100000001+100001+1110
=Income before tax525470-553734-33734-34844-44239-34239-34844-44239-34239-34844-44239-34844-45349-4
−Income tax3125-643-143-154-154-154-154-154-154-154-154-154-165-1
=Net income494445-493331-23331-24240-23836-23836-24240-23836-23836-24240-23836-24240-24744-3

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; clicking a month fires onSelect, which you would wire to filter the matrix above.

Compare each month's actual to: ACPL
Revenue by month - Actual vs Plan66Jan66Feb85Mar76Apr76May85Jun76Jul76Aug85Sep76Oct85Nov94Dec-2-2-2-1-1-2-1-1-2-1-2-3
Revenue by month - Actual vs Plan - data table
ACPLΔPLΔPL%
Jan6668-2-2.9%
Feb6668-2-2.9%
Mar8587-2-2.3%
Apr7677-1-1.3%
May7677-1-1.3%
Jun8587-2-2.3%
Jul7677-1-1.3%
Aug7677-1-1.3%
Sep8587-2-2.3%
Oct7677-1-1.3%
Nov8587-2-2.3%
Dec9497-3-3.1%
Selected month: none - wire `onSelect` to filter the matrix above.

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.

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.
};

<MatrixTable
  rows={rows}
  columns={columns}
  values={values}
  scenarios={["PL", "AC"]} // sub-column order per leaf period
  showVariance // adds a ΔBudget (AC − PL) column
  tokens={oceanTokens} // theme via tokens, never hardcoded colours
  format={{ compact: false, decimals: 0 }}
  stickyFirstColumn // freeze labels during horizontal scroll
/>;

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

Every prop - including the controlled expandedRows / expandedCols pairs and the per-cell hooks used above - is documented on the component page: MatrixTable.

Where to next

  • MatrixTable - the full prop reference and the controlled/uncontrolled expansion contract.
  • StatementTable - the same statement for a single period, with an integrated waterfall.
  • DataTable - entities rather than periods across the top.
  • Data model - scenarios, flows and higherIsBetter.
  • IBCS & ISO 24896 - why PL is hollow and FC is hatched.
ibcs-react is an independent open-source library and is not affiliated with, certified by, or endorsed by the IBCS Association or ISO. It follows the IBCS® notation rules (the basis of ISO 24896); IBCS® is a registered trademark of the IBCS Association.

On this page