# Dashboard & charts

> We wrap a charting engine (Recharts, or visx where more control is needed). We do not rebuild one. What we add is everything a chart library doesn't know about money. Specs for 11 React components: DashboardShell, KpiRow, MoneyChart, MoneyBarChart and 7 more.

Source: https://lopifi.com/docs/catalog/07-dashboard-charts
Part of: Lopifi — US GAAP books on autopilot, reviewed by a CPA (https://lopifi.com)

---

# Dashboard & charts

We wrap a charting engine (Recharts, or visx where more control is needed). We do not rebuild one. What we add is everything a chart library doesn't know about money.

**The governing rules:** [Money Law 2](../motion/01-principles.md#law-2--data-does-not-re-animate) — a chart animates on first paint and never on data change. And every chart has an accessible table equivalent.

---

## 71. `DashboardShell`
12/8/4-column responsive grid with optional user rearrangement. Widget positions persisted per user. Drag uses FLIP + `spring.snap`, measured once at drag start.

Equal-height rows via `auto-rows-fr` so figure baselines align across a row — the most visible alignment failure on any dashboard, and it's a one-line fix.

## 72. `KpiRow`
2–4 `KpiCard`s with a shared figure baseline. **Forces Inter** on all figures regardless of the `family` prop, because these align across the row. Enforced by the parent, not left to the caller.

---

## 73. `MoneyChart`

Line and area, with everything a generic chart library gets wrong about money:

- **Money-formatted axis** — abbreviated (`$1.2M`) with the exact value in the tooltip. Abbreviation is licensed here; see [numerics](../foundations/06-numerics.md#abbreviation).
- **Zero baseline is emphasized** — a 1px `--border-strong` line, distinct from the `--border-subtle` gridlines. When data crosses zero, that line is the most important thing in the chart.
- **Negative regions are visually distinct** — a fill below zero at `--negative` with a 45° hatch, so it survives greyscale.
- **Y-axis starts at zero** for quantities. The only sanctioned exception is a rate chart (`CurrencyPairChart`), and there the axis is explicitly labeled.
- **Projections are dashed.** A forecast rendered identically to actuals is a lie. The boundary itself is drawn — a dashed vertical rule labelled `Forecast →` — because a dash pattern read across six months is not a date.
- **Gridlines: horizontal only**, `--border-subtle`. Vertical gridlines on a time series are noise.
- **Area fill** is the one sanctioned gradient — a single hue fading to transparent, because it keeps the line readable over gridlines. Legibility, not decoration.
- Multi-currency series is **rejected at the type level**. Convert to a stated presentation currency first, and label the axis with it.

### The FP&A layer

A plan chart has to say four things a line chart cannot, and each of them is a way of being wrong that a prettier chart hides.

- **The range, and what it is.** `band` takes a low and a high and a **required** `label` — `P10–P90`, `±1σ`, `Downside to upside`. A shaded cone with no stated interval is a precision claim nobody made, and a reader who cannot tell which interval they are looking at will assume whichever suits them. The band is bounded by a hairline as well as shaded, because a fill alone disappears on a projector, and the edges are the part people quote. It widens with distance or it is lying about what is knowable.
- **The alternatives, unranked.** `scenarios` draws base, upside and downside from the same anchor, told apart by **dash rhythm first and colour second** — a base/upside/downside triple in red-amber-green is three lines a third of readers cannot separate. No best-case styling and no ordering by outcome: a scenario is a set of assumptions, and presenting one as the good one is how a forecast review becomes a negotiation about which line to show the board.
- **The plan.** `plan` is a *reference*, not a series: dotted, in the neutral tertiary tone, taking no viz colour and absent from the legend's isolate control. A budget is the line the business agreed to, not another thing that happened, and giving it a series colour puts it in competition with the measure it exists to be read against.
- **The variance, in words that survive greyscale.** Stated as **F** and **U** rather than as a colour or a bare sign. Favourable and unfavourable are the terms of art, they are unambiguous under polarity — a cost under plan is F and revenue under plan is U, and *both are negative absolute variances* — and no amount of red and green carries that. `plan.polarity` is required for the same reason it is required everywhere else: a chart does not know its own polarity, and guessing one gets half of FP&A backwards.

The variance is always taken against the **first** series declared, and the readout disappears when that series is isolated away. A variance whose base silently changes as the reader filters is worse than no variance: the number moves and nothing on the chart says why.

The band, the scenarios and the plan are all inside the y-domain. A cone clipped by the axis is a chart that has quietly cropped the bad case out of the picture.

**The table equivalent carries all of it** — a `Basis` column marking each period actual or forecast, the band's range, each scenario, the plan and the variance. A table listing the base values alone is not the chart in another form; it is a different, more confident chart.

**Motion:** `stroke-dashoffset` draw, 400ms `--ease-out`, once on mount or scroll-into-view. Instant on data change.

**Touch:** 44px-tall scrub target across the plot, with a **persistent readout** in a fixed header — not a floating tooltip a finger covers.

## 74. `MoneyBarChart`
Grouped and stacked.

- **`transform-origin` at the zero baseline**, not the bottom. Otherwise negative bars grow upward from the floor and briefly display the wrong sign — a real correctness issue in an animation.
- Negative bars hatched.
- Stacked totals labeled at the bar end.
- Bar radius 4 on the outer corners only, and **square below 12px bar width** — a rounded 6px bar is a blob.
- Draw-in: `scaleY 0→1`, 400ms, stagger 30ms capped at 240ms. First paint only.

## 75. `WaterfallChart`

> The P&L bridge. Essential in finance, and almost nowhere in open-source charting.

Start → increases → decreases → end, with connector lines between bars. Increases `--positive`, decreases `--negative`, subtotals neutral — **and all three distinguished by position and connector, not color alone.**

The most useful chart in the family: it's how you answer "why did net income change?" in one image.

## 76. `CategoryBreakdown`
Ranked horizontal bars with values and percentages. **Not a pie chart.** Humans compare lengths accurately and angles poorly; a pie with more than three slices is decoration.

Top N + "Other" (max 6 series). Percentages use largest-remainder allocation so they sum to exactly 100%.

## 77. `CashCalendar`
Daily cash movement as a heatmap. Sequential OKLCH lightness ramp within one hue for magnitude, with the diverging in/out distinction carried by **two separate ramps that differ in lightness**, not by red/green.

## 78. `CandlestickChart`
For FX and crypto pairs. Up and down candles differ in **fill** (hollow vs. solid) as well as color — the traditional convention, and it happens to be the correct accessibility answer.

## 79. `ChartLegend`
Interactive: click to isolate a series, keyboard-navigable, `aria-pressed` state. Positioned below on phone as a wrapping row. Series colors are assigned by a **stable key hash**, never by array index, so a category is the same color in the dashboard and the drill-down.

## 80. `ChartTooltip`
Full values, never abbreviated. All series at the hovered point, in the same order as the legend. Touch-persistent variant. Follows the pointer with no animation — a tooltip that eases behind the cursor feels broken.

## 81. `DataTableToggle`

> Every chart has an accessible table equivalent, reachable by a visible toggle.

This is a WCAG requirement, and it's also the feature power users want most — the actual numbers. Serving both with one component is the reason it's P1 and not P3.
