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
maxHeightcaps 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 | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Jan | Feb | Mar | ||||||||||||||||||||||
| PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | |
| = | 711 | 661 | -50 | 906 | 895 | -11 | 67 | 66 | -1 | 67 | 66 | -1 | 87 | 86 | -1 | 241 | 237 | -4 | 241 | 237 | -4 | 260 | 255 | -5 |
| + | 467 | 453 | -14 | 543 | 565 | +22 | 43 | 42 | -1 | 43 | 42 | -1 | 56 | 54 | -2 | 155 | 150 | -5 | 155 | 150 | -5 | 167 | 161 | -6 |
| + | 99 | 87 | -12 | 132 | 121 | -11 | 10 | 9 | -1 | 10 | 9 | -1 | 13 | 12 | -1 | 35 | 32 | -3 | 35 | 32 | -3 | 38 | 35 | -3 |
| + | 145 | 121 | -24 | 231 | 209 | -22 | 14 | 15 | +1 | 14 | 15 | +1 | 18 | 20 | +2 | 51 | 55 | +4 | 51 | 55 | +4 | 55 | 59 | +4 |
| − | 282 | 231 | -51 | 199 | 222 | +23 | 16 | 17 | +1 | 16 | 17 | +1 | 21 | 22 | +1 | 58 | 61 | +3 | 58 | 61 | +3 | 62 | 66 | +4 |
| = | 429 | 430 | +1 | 707 | 673 | -34 | 51 | 49 | -2 | 51 | 49 | -2 | 66 | 63 | -3 | 184 | 175 | -9 | 184 | 175 | -9 | 198 | 189 | -9 |
| − | 113 | 119 | +6 | 188 | 206 | +18 | 15 | 15 | 0 | 15 | 15 | 0 | 19 | 20 | +1 | 53 | 54 | +1 | 53 | 54 | +1 | 57 | 58 | +1 |
| − | 79 | 78 | -1 | 91 | 104 | +13 | 8 | 8 | 0 | 8 | 8 | 0 | 10 | 11 | +1 | 28 | 30 | +2 | 28 | 30 | +2 | 30 | 32 | +2 |
| − | 34 | 41 | +7 | 97 | 102 | +5 | 7 | 7 | 0 | 7 | 7 | 0 | 9 | 9 | 0 | 25 | 24 | -1 | 25 | 24 | -1 | 27 | 26 | -1 |
| = | 316 | 311 | -5 | 519 | 467 | -52 | 37 | 34 | -3 | 37 | 34 | -3 | 47 | 44 | -3 | 131 | 122 | -9 | 131 | 122 | -9 | 142 | 131 | -11 |
| + | 8 | 12 | +4 | 6 | 3 | -3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | +1 | 1 | 2 | +1 | 1 | 2 | +1 | 1 | 2 | +1 |
| = | 324 | 323 | -1 | 525 | 470 | -55 | 37 | 34 | -3 | 37 | 34 | -3 | 48 | 44 | -4 | 133 | 123 | -10 | 133 | 123 | -10 | 143 | 133 | -10 |
| − | 59 | 54 | -5 | 31 | 25 | -6 | 4 | 3 | -1 | 4 | 3 | -1 | 5 | 4 | -1 | 15 | 12 | -3 | 15 | 12 | -3 | 16 | 13 | -3 |
| = | 265 | 269 | +4 | 494 | 445 | -49 | 33 | 31 | -2 | 33 | 31 | -2 | 42 | 40 | -2 | 118 | 111 | -7 | 118 | 111 | -7 | 127 | 120 | -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 | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Jan | Feb | Mar | ||||||||||||||||||||||
| PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | |
| = | 711 | 661 | -50 | 906 | 895 | -11 | 67 | 66 | -1 | 67 | 66 | -1 | 87 | 86 | -1 | 241 | 237 | -4 | 241 | 237 | -4 | 260 | 255 | -5 |
| + | 467 | 453 | -14 | 543 | 565 | +22 | 43 | 42 | -1 | 43 | 42 | -1 | 56 | 54 | -2 | 155 | 150 | -5 | 155 | 150 | -5 | 167 | 161 | -6 |
| + | 99 | 87 | -12 | 132 | 121 | -11 | 10 | 9 | -1 | 10 | 9 | -1 | 13 | 12 | -1 | 35 | 32 | -3 | 35 | 32 | -3 | 38 | 35 | -3 |
| + | 145 | 121 | -24 | 231 | 209 | -22 | 14 | 15 | +1 | 14 | 15 | +1 | 18 | 20 | +2 | 51 | 55 | +4 | 51 | 55 | +4 | 55 | 59 | +4 |
| − | 282 | 231 | -51 | 199 | 222 | +23 | 16 | 17 | +1 | 16 | 17 | +1 | 21 | 22 | +1 | 58 | 61 | +3 | 58 | 61 | +3 | 62 | 66 | +4 |
| = | 429 | 430 | +1 | 707 | 673 | -34 | 51 | 49 | -2 | 51 | 49 | -2 | 66 | 63 | -3 | 184 | 175 | -9 | 184 | 175 | -9 | 198 | 189 | -9 |
| − | 113 | 119 | +6 | 188 | 206 | +18 | 15 | 15 | 0 | 15 | 15 | 0 | 19 | 20 | +1 | 53 | 54 | +1 | 53 | 54 | +1 | 57 | 58 | +1 |
| − | 79 | 78 | -1 | 91 | 104 | +13 | 8 | 8 | 0 | 8 | 8 | 0 | 10 | 11 | +1 | 28 | 30 | +2 | 28 | 30 | +2 | 30 | 32 | +2 |
| − | 34 | 41 | +7 | 97 | 102 | +5 | 7 | 7 | 0 | 7 | 7 | 0 | 9 | 9 | 0 | 25 | 24 | -1 | 25 | 24 | -1 | 27 | 26 | -1 |
| = | 316 | 311 | -5 | 519 | 467 | -52 | 37 | 34 | -3 | 37 | 34 | -3 | 47 | 44 | -3 | 131 | 122 | -9 | 131 | 122 | -9 | 142 | 131 | -11 |
| + | 8 | 12 | +4 | 6 | 3 | -3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | +1 | 1 | 2 | +1 | 1 | 2 | +1 | 1 | 2 | +1 |
| = | 324 | 323 | -1 | 525 | 470 | -55 | 37 | 34 | -3 | 37 | 34 | -3 | 48 | 44 | -4 | 133 | 123 | -10 | 133 | 123 | -10 | 143 | 133 | -10 |
| − | 59 | 54 | -5 | 31 | 25 | -6 | 4 | 3 | -1 | 4 | 3 | -1 | 5 | 4 | -1 | 15 | 12 | -3 | 15 | 12 | -3 | 16 | 13 | -3 |
| = | 265 | 269 | +4 | 494 | 445 | -49 | 33 | 31 | -2 | 33 | 31 | -2 | 42 | 40 | -2 | 118 | 111 | -7 | 118 | 111 | -7 | 127 | 120 | -7 |
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.
| 2022 | 2023 | 2024 | ||||
|---|---|---|---|---|---|---|
| PL | AC | PL | AC | PL | FC | |
| = | 711 | 661 | 906 | 895 | 965 | 954 |
| + | 467 | 453 | 543 | 565 | 620 | 610 |
| + | 99 | 87 | 132 | 121 | 140 | 131 |
| + | 145 | 121 | 231 | 209 | 205 | 213 |
| − | 282 | 231 | 199 | 222 | 230 | 240 |
| = | 429 | 430 | 707 | 673 | 735 | 714 |
| − | 113 | 119 | 188 | 206 | 210 | 223 |
| − | 79 | 78 | 91 | 104 | 110 | 124 |
| − | 34 | 41 | 97 | 102 | 100 | 99 |
| = | 316 | 311 | 519 | 467 | 525 | 491 |
| + | 8 | 12 | 6 | 3 | 5 | 4 |
| = | 324 | 323 | 525 | 470 | 530 | 495 |
| − | 59 | 54 | 31 | 25 | 60 | 31 |
| = | 265 | 269 | 494 | 445 | 470 | 464 |
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.
| PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| = | 222 | 217 | -5 | 241 | 237 | -4 | 241 | 237 | -4 | 260 | 255 | -5 |
| − | 53 | 56 | +3 | 58 | 61 | +3 | 58 | 61 | +3 | 62 | 66 | +4 |
| = | 169 | 161 | -8 | 184 | 175 | -9 | 184 | 175 | -9 | 198 | 189 | -9 |
| − | 48 | 49 | +1 | 53 | 54 | +1 | 53 | 54 | +1 | 57 | 58 | +1 |
| = | 121 | 112 | -9 | 131 | 122 | -9 | 131 | 122 | -9 | 142 | 131 | -11 |
| + | 1 | 1 | 0 | 1 | 2 | +1 | 1 | 2 | +1 | 1 | 2 | +1 |
| = | 122 | 113 | -9 | 133 | 123 | -10 | 133 | 123 | -10 | 143 | 133 | -10 |
| − | 14 | 11 | -3 | 15 | 12 | -3 | 15 | 12 | -3 | 16 | 13 | -3 |
| = | 108 | 102 | -6 | 118 | 111 | -7 | 118 | 111 | -7 | 127 | 120 | -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.
| Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec | |||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | |
| = | 67 | 66 | -1 | 67 | 66 | -1 | 87 | 86 | -1 | 77 | 75 | -2 | 77 | 75 | -2 | 87 | 86 | -1 | 77 | 75 | -2 | 77 | 75 | -2 | 87 | 86 | -1 | 77 | 75 | -2 | 87 | 86 | -1 | 97 | 95 | -2 |
| + | 43 | 42 | -1 | 43 | 42 | -1 | 56 | 54 | -2 | 50 | 48 | -2 | 50 | 48 | -2 | 56 | 54 | -2 | 50 | 48 | -2 | 50 | 48 | -2 | 56 | 54 | -2 | 50 | 48 | -2 | 56 | 54 | -2 | 62 | 60 | -2 |
| + | 10 | 9 | -1 | 10 | 9 | -1 | 13 | 12 | -1 | 11 | 10 | -1 | 11 | 10 | -1 | 13 | 12 | -1 | 11 | 10 | -1 | 11 | 10 | -1 | 13 | 12 | -1 | 11 | 10 | -1 | 13 | 12 | -1 | 14 | 13 | -1 |
| + | 14 | 15 | +1 | 14 | 15 | +1 | 18 | 20 | +2 | 16 | 17 | +1 | 16 | 17 | +1 | 18 | 20 | +2 | 16 | 17 | +1 | 16 | 17 | +1 | 18 | 20 | +2 | 16 | 17 | +1 | 18 | 20 | +2 | 21 | 22 | +1 |
| − | 16 | 17 | +1 | 16 | 17 | +1 | 21 | 22 | +1 | 18 | 20 | +2 | 18 | 20 | +2 | 21 | 22 | +1 | 18 | 20 | +2 | 18 | 20 | +2 | 21 | 22 | +1 | 18 | 20 | +2 | 21 | 22 | +1 | 23 | 24 | +1 |
| = | 51 | 49 | -2 | 51 | 49 | -2 | 66 | 63 | -3 | 59 | 56 | -3 | 59 | 56 | -3 | 66 | 63 | -3 | 59 | 56 | -3 | 59 | 56 | -3 | 66 | 63 | -3 | 59 | 56 | -3 | 66 | 63 | -3 | 74 | 70 | -4 |
| − | 15 | 15 | 0 | 15 | 15 | 0 | 19 | 20 | +1 | 17 | 17 | 0 | 17 | 17 | 0 | 19 | 20 | +1 | 17 | 17 | 0 | 17 | 17 | 0 | 19 | 20 | +1 | 17 | 17 | 0 | 19 | 20 | +1 | 21 | 22 | +1 |
| − | 8 | 8 | 0 | 8 | 8 | 0 | 10 | 11 | +1 | 9 | 9 | 0 | 9 | 9 | 0 | 10 | 11 | +1 | 9 | 9 | 0 | 9 | 9 | 0 | 10 | 11 | +1 | 9 | 9 | 0 | 10 | 11 | +1 | 11 | 12 | +1 |
| − | 7 | 7 | 0 | 7 | 7 | 0 | 9 | 9 | 0 | 8 | 8 | 0 | 8 | 8 | 0 | 9 | 9 | 0 | 8 | 8 | 0 | 8 | 8 | 0 | 9 | 9 | 0 | 8 | 8 | 0 | 9 | 9 | 0 | 10 | 10 | 0 |
| = | 37 | 34 | -3 | 37 | 34 | -3 | 47 | 44 | -3 | 42 | 39 | -3 | 42 | 39 | -3 | 47 | 44 | -3 | 42 | 39 | -3 | 42 | 39 | -3 | 47 | 44 | -3 | 42 | 39 | -3 | 47 | 44 | -3 | 53 | 49 | -4 |
| + | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | +1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | +1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | +1 | 0 | 0 | 0 | 0 | 1 | +1 | 1 | 1 | 0 |
| = | 37 | 34 | -3 | 37 | 34 | -3 | 48 | 44 | -4 | 42 | 39 | -3 | 42 | 39 | -3 | 48 | 44 | -4 | 42 | 39 | -3 | 42 | 39 | -3 | 48 | 44 | -4 | 42 | 39 | -3 | 48 | 44 | -4 | 53 | 49 | -4 |
| − | 4 | 3 | -1 | 4 | 3 | -1 | 5 | 4 | -1 | 5 | 4 | -1 | 5 | 4 | -1 | 5 | 4 | -1 | 5 | 4 | -1 | 5 | 4 | -1 | 5 | 4 | -1 | 5 | 4 | -1 | 5 | 4 | -1 | 6 | 5 | -1 |
| = | 33 | 31 | -2 | 33 | 31 | -2 | 42 | 40 | -2 | 38 | 36 | -2 | 38 | 36 | -2 | 42 | 40 | -2 | 38 | 36 | -2 | 38 | 36 | -2 | 42 | 40 | -2 | 38 | 36 | -2 | 42 | 40 | -2 | 47 | 44 | -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.
| Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | PL | AC | ΔBdg | |
| = | 906 | 895 | -11 | 67 | 66 | -1 | 67 | 66 | -1 | 87 | 86 | -1 | 77 | 75 | -2 | 77 | 75 | -2 | 87 | 86 | -1 | 77 | 75 | -2 | 77 | 75 | -2 | 87 | 86 | -1 | 77 | 75 | -2 | 87 | 86 | -1 | 97 | 95 | -2 |
| + | 543 | 565 | +22 | 43 | 42 | -1 | 43 | 42 | -1 | 56 | 54 | -2 | 50 | 48 | -2 | 50 | 48 | -2 | 56 | 54 | -2 | 50 | 48 | -2 | 50 | 48 | -2 | 56 | 54 | -2 | 50 | 48 | -2 | 56 | 54 | -2 | 62 | 60 | -2 |
| + | 132 | 121 | -11 | 10 | 9 | -1 | 10 | 9 | -1 | 13 | 12 | -1 | 11 | 10 | -1 | 11 | 10 | -1 | 13 | 12 | -1 | 11 | 10 | -1 | 11 | 10 | -1 | 13 | 12 | -1 | 11 | 10 | -1 | 13 | 12 | -1 | 14 | 13 | -1 |
| + | 231 | 209 | -22 | 14 | 15 | +1 | 14 | 15 | +1 | 18 | 20 | +2 | 16 | 17 | +1 | 16 | 17 | +1 | 18 | 20 | +2 | 16 | 17 | +1 | 16 | 17 | +1 | 18 | 20 | +2 | 16 | 17 | +1 | 18 | 20 | +2 | 21 | 22 | +1 |
| − | 199 | 222 | +23 | 16 | 17 | +1 | 16 | 17 | +1 | 21 | 22 | +1 | 18 | 20 | +2 | 18 | 20 | +2 | 21 | 22 | +1 | 18 | 20 | +2 | 18 | 20 | +2 | 21 | 22 | +1 | 18 | 20 | +2 | 21 | 22 | +1 | 23 | 24 | +1 |
| = | 707 | 673 | -34 | 51 | 49 | -2 | 51 | 49 | -2 | 66 | 63 | -3 | 59 | 56 | -3 | 59 | 56 | -3 | 66 | 63 | -3 | 59 | 56 | -3 | 59 | 56 | -3 | 66 | 63 | -3 | 59 | 56 | -3 | 66 | 63 | -3 | 74 | 70 | -4 |
| − | 188 | 206 | +18 | 15 | 15 | 0 | 15 | 15 | 0 | 19 | 20 | +1 | 17 | 17 | 0 | 17 | 17 | 0 | 19 | 20 | +1 | 17 | 17 | 0 | 17 | 17 | 0 | 19 | 20 | +1 | 17 | 17 | 0 | 19 | 20 | +1 | 21 | 22 | +1 |
| − | 91 | 104 | +13 | 8 | 8 | 0 | 8 | 8 | 0 | 10 | 11 | +1 | 9 | 9 | 0 | 9 | 9 | 0 | 10 | 11 | +1 | 9 | 9 | 0 | 9 | 9 | 0 | 10 | 11 | +1 | 9 | 9 | 0 | 10 | 11 | +1 | 11 | 12 | +1 |
| − | 97 | 102 | +5 | 7 | 7 | 0 | 7 | 7 | 0 | 9 | 9 | 0 | 8 | 8 | 0 | 8 | 8 | 0 | 9 | 9 | 0 | 8 | 8 | 0 | 8 | 8 | 0 | 9 | 9 | 0 | 8 | 8 | 0 | 9 | 9 | 0 | 10 | 10 | 0 |
| = | 519 | 467 | -52 | 37 | 34 | -3 | 37 | 34 | -3 | 47 | 44 | -3 | 42 | 39 | -3 | 42 | 39 | -3 | 47 | 44 | -3 | 42 | 39 | -3 | 42 | 39 | -3 | 47 | 44 | -3 | 42 | 39 | -3 | 47 | 44 | -3 | 53 | 49 | -4 |
| + | 6 | 3 | -3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | +1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | +1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | +1 | 0 | 0 | 0 | 0 | 1 | +1 | 1 | 1 | 0 |
| = | 525 | 470 | -55 | 37 | 34 | -3 | 37 | 34 | -3 | 48 | 44 | -4 | 42 | 39 | -3 | 42 | 39 | -3 | 48 | 44 | -4 | 42 | 39 | -3 | 42 | 39 | -3 | 48 | 44 | -4 | 42 | 39 | -3 | 48 | 44 | -4 | 53 | 49 | -4 |
| − | 31 | 25 | -6 | 4 | 3 | -1 | 4 | 3 | -1 | 5 | 4 | -1 | 5 | 4 | -1 | 5 | 4 | -1 | 5 | 4 | -1 | 5 | 4 | -1 | 5 | 4 | -1 | 5 | 4 | -1 | 5 | 4 | -1 | 5 | 4 | -1 | 6 | 5 | -1 |
| = | 494 | 445 | -49 | 33 | 31 | -2 | 33 | 31 | -2 | 42 | 40 | -2 | 38 | 36 | -2 | 38 | 36 | -2 | 42 | 40 | -2 | 38 | 36 | -2 | 38 | 36 | -2 | 42 | 40 | -2 | 38 | 36 | -2 | 42 | 40 | -2 | 47 | 44 | -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.
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.
Examples
Three complete dashboards - an executive overview, a sales review and a finance pack - assembled entirely from ibcs-react components.
Report cookbook
97 ready-made business reports and dashboards across seven domains - each a live mini-report built from one to four ibcs-react components, with copy-pasteable source.