---
title: "Metrics & analysis"
description: "Metrics & analysis — specs for 13 React components: MetricDefinition, NonGaapReconciliation, MrrBridge, BudgetVsActual and 9 more."
canonical: "https://lopifi.com/docs/catalog/metrics-and-analysis"
---

# Metrics & analysis

> Metrics & analysis — specs for 13 React components: MetricDefinition, NonGaapReconciliation, MrrBridge, BudgetVsActual and 9 more.

Source: https://lopifi.com/docs/catalog/metrics-and-analysis
Part of: Lopifi — Planned: US GAAP books on autopilot, checked by a CPA (https://lopifi.com)

---

# Metrics & analysis

Definitions in [`domain/metrics.md`](https://lopifi.com/md/docs/domain/metrics).

---

## 162. `MetricDefinition`

> **Every metric card requires one.** There is no variant without a definition.

Renders the formula with the actual input values substituted, plus cohort, period, source, and caveat:

```
  Net Revenue Retention                                    104.6%
  ────────────────────────────────────────────────────────────────
  (Starting MRR + Expansion − Contraction − Churn) ÷ Starting MRR
  (  412,000    +  61,400   −    18,200    − 24,100 ) ÷  412,000

  Cohort:  customers active on 1 Sep 2026
  Period:  1–30 Sep 2026
  Source:  subscription ledger · excludes one-time fees
  Note:    new customers excluded by definition
```

"ARR" means four different things at four companies. This component makes sure the number here means one.

**Definitions are versioned.** When one changes, historical values computed under the old definition are marked. An unannounced definition change looks like a business change.

## 163. `NonGaapReconciliation`
EBITDA, adjusted EBITDA, free cash flow, adjusted net income. Appears **below** net income in a separated block, never inside the P&L, with a line-by-line reconciliation and **every adjustment named**. "Adjusted EBITDA" with unnamed adjustments is not a number.

## 164. `MrrBridge`
Opening MRR → new → expansion → contraction → churn → closing, as a waterfall. **A single MRR number with a percentage change explains nothing**; the bridge is the primary view.

Renders GRR and NRR together or neither, because NRR alone hides churn behind expansion. States that MRR is an operating metric that will not tie to the P&L.

## 165. `BudgetVsActual`
Actual, budget, variance (absolute and relative) and forecast, by period and by dimension. Respects `polarity`: a favorable expense variance is *under* budget. Materiality thresholds are applied and displayed. Reforecast versions are tracked so you can see what was believed when.

The variance is a **view, not a column**. Actual, Δ and Δ% are the same figures asked three questions, and showing all of them at once turns a budget review into a wall of numbers nobody finishes. The comparison is declared, not positional: the actual column names budget as its base, so hiding the budget column to fit the screen doesn't silently re-point the variance at whatever column ends up to its left.

Two more things keep a budget review from going wrong.

**The cut.** A favorable total is often two departments canceling out. Engineering 1.6M over and sales 1.7M under reads at the entity line as 0.1M under, favorable, and says nothing about either. A report readable only at the entity level gets believed a quarter longer than it should. Members don't have to sum to the entity, since unallocated corporate spend sits outside them. A vintage that restates an entity budget says nothing about one department's share, so under a cut the member's own budget stands. The component doesn't invent an allocation nobody approved.

**The vintage.** The original plan and each reforecast are versions of one object, not "budget" plus "reforecasts." Model a reforecast as a different kind of thing and "what did we believe in February" stops having one answer. A vintage states only what it changed. The comparison base is chosen out loud in the toolbar, and the walk between versions sits under the grid, because a reforecast that quietly replaces the plan makes every past month look better than it was called at the time. September can beat the version in force and miss the one the board saw in November. Both readings are true, and a component that picked one silently would pick the flattering one.

## 166. `ForecastTable`
Driver-based projections with scenarios. **Projections render dashed** on every chart and carry a "forecast" label on every figure. Actuals to date and forecast remaining look different within the same period.

The line between what happened and what's believed is drawn in three places.

**Between periods.** Everything after the last closed period is shaded, ruled with a dashed edge, set in the secondary tone, and headed by a column that says `forecast` under its label. Every projected figure sits beneath that word, not just near it.

**Within a period.** A month that has started but not finished is three columns, not one: what has posted, what's expected for the rest, and the landing they sum to. A part-elapsed month shown as one figure next to eight closed ones borrows their authority. That's the difference between "we're at 41.2 with nine days to go" and "we did 58.2," which nobody said and everybody quotes. The landing is **computed from the other two**, never supplied. A caller who provides all three will eventually provide three figures that don't add up, and the grid would render the contradiction without noticing.

**Between scenarios.** The scenario in force is named in the caption and the toolbar. The component won't rank scenarios or style one as the good case. An unlabeled forecast is indistinguishable from a claim, and a ranked one is a recommendation dressed as a forecast.

**API change:** `ForecastRow.polarity` is now required, no default (it previously fell back to `standard`).

## 167. `CovenantMonitor`
Per-covenant: the **contractual** definition (lenders define EBITDA with specific add-backs), the computed value, the threshold, the headroom, and the next test date.

The number here may legitimately differ from the dashboard's EBITDA because the credit agreement defines it differently. That's not a bug, and the component says so on the card.

Headroom under 20% is `--warning`; a breach is `--negative`. The numeric headroom is always shown, never color alone.

## 168. `KpiHealthStrip`
The dashboard's top row: liquidity, profitability, and cash metrics with polarity, comparison basis, and definitions. **Metrics derived from unreconciled data are marked provisional.** A KPI computed mid-close, before the bank is reconciled, is an estimate, and the label is what stops people quoting it externally.

---

## 174. `RevenueTracker`
Revenue by stream, period by period, answering the four questions an operator asks: how much this
period, versus what, from where, and how concentrated.

The summary tiles are computed from the same array as the grid cells. There's no second data
path, because a header KPI that disagrees with the table under it loses the reader fast. For the
same reason, total revenue is summed here, not accepted as a prop.

Month-on-month **and** year-on-year, together. Month-on-month on a seasonal business is noise;
year-on-year on a fast-growing one is stale.

The run-rate tile is labeled as a projection from one period. A month times twelve isn't ARR: it
ignores churn, seasonality and contract length. The tile says so before the figure travels into
a deck.

Any stream over a third of revenue is flagged in words. Concentration is a fact about the
business's risk, not about the month.

## 175. `ExpenseTracker`
Spend by category on three bases (actual, budget, variance), because they're the same categories
asked three questions, and nobody reads a nine-column grid past the third column.

**Every row carries a polarity, and the component derives it.** More spend isn't good news: under
budget is favorable, over budget isn't. A grid that paints a rising cost green tells the reader
the opposite of the truth.

Polarity describes the figure *as stored*, not the concept behind it, and that's the easiest thing
here to get backwards. This system holds costs as negatives, so a heavier month moves **down** and
the row wants `standard`. A model that holds the same costs as positive magnitudes wants
`inverted`. `ExpenseTracker` takes a `sign` and works it out, so the caller doesn't have to reason
about a storage convention to get the colors right.

Committed-but-unbilled sits beside the total, never inside it. A purchase order isn't an expense.
Accruing it silently is a cutoff error, and hiding it is how a category with six figures of open
POs looks under budget until the invoices land.

The movers list ranks by amount, not percentage. A line that doubled from 400 to 800 isn't the
story of a month where another went from 180,000 to 240,000.

## 176. `DriverModel`
A forecast's inputs and the outputs they produce, in one grid with the boundary between them
visible. It keeps three things apart: actual from projection, input from output, and assumption
from arithmetic.

Inputs are typed; outputs carry the ƒ mark and their formula. If you can't tell which is which,
nobody can find the assumption that moved the answer.

Only **projected inputs** are editable. An actual is what the ledger posted, and a model that lets
you type over it will eventually explain a month that never happened.

Named assumptions sit under the grid in words ("new logos hold at 14 a month"), because that's the
sentence someone can disagree with, and disagreement is the point of a forecast review. A growth
rate buried three rows into a projected column isn't.

**API change:** `ModelDriver.polarity` is now required, no default — computed outputs included (it previously fell back to `standard`).

## 177. `ScenarioCompare`
Scenarios in columns, measures down, every column measured against the base. The comparison is
declared, not positional, so it survives the reader reordering or hiding columns.

**It won't rank the scenarios.** No best-case styling, no sorting by outcome. A scenario is a set
of assumptions, and calling the biggest number the good one turns a forecast review into a
negotiation over which column the board sees.

**It won't hide a breach.** A measure with a limit (a covenant floor, a minimum cash balance)
names, in words under the grid, the scenarios that cross it. A downside that breaches a covenant
is the only thing on the page that matters, so it isn't left as a cell someone has to notice.

**API change:** `ScenarioMeasure.polarity` is now required, no default (it previously fell back to `standard`).

## 178. `PlByDimension`
The P&L cut by department, entity, product or region instead of by period. Same lines, different
question, and it's the question the department head came to ask.

Total is a column computed from the members beside it plus anything unallocated, never supplied
separately. A total that doesn't equal its parts loses the room.

Unallocated is its own column, not a rounding and not an allocation. Some corporate costs belong to
no department, and spreading them on a rule nobody agreed to gives the department head a P&L they
can't argue with.

**API change:** `PlDimensionLine.polarity` is now required, no default — read it from the account type with `polarityForAccountType` (it previously fell back to `standard`).

## 187. `RatioAnalysis`
Liquidity, leverage, coverage, efficiency and profitability ratios in one table, grouped in that
order. Each row carries the ratio's name, its formula in words, and **the two figures that produced
it**, so the value can be checked without opening the ledger.

The value is **computed from its inputs**, never passed in beside them. A ratio whose own formula
line contradicts it is worse than no ratio.

- **Kind is explicit:** `times` (1.84×), `percent` (34.2%) or `days` (47 days). A `days` ratio
  takes the length of the period its denominator covers — DSO on a quarter's revenue is × 91, not
  × 365 — so the component never guesses a year.
- **Polarity is required**, no default. A current ratio rising is good; DSO or debt to equity
  rising is not.
- **Change is in the ratio's own unit.** A percent ratio moves in points, not percent of itself.
  The change is computed on the displayed precision, so it never shows a move where both readings
  print the same.
- **Three gaps, three sentences, none of them zero:** an input missing ("Input not available"), a
  zero denominator ("Not meaningful" — never Infinity), and inputs in two currencies ("Inputs in
  two currencies" — a ratio across currencies needs a rate nobody passed).
- **Thresholds name their source** — the credit agreement section, the board policy. A breach is
  stated in words with an icon; the `--negative` color repeats it, never replaces it.
- **The basis is a required prop:** period, entity, currency, accounting basis. Ratios computed
  mid-close inherit the provisional state of their inputs; pass it in the basis line.

Not a covenant monitor: `CovenantMonitor` carries the contractual definition and the test date.
Use this for analysis and the covenant card for compliance.
