Cash Outcome Range
Projects where cash lands as a quantile dotplot with the downside highlighted, plus the probability of running out. Honest uncertainty — countable outcomes, not a band.
Acme · Cash runway
52%
chance of running out of cash by Nov 2026
- At risk
- Survives
Cash has a 52% chance of running out of cash by Nov 2026.
| Outcome | Cash |
|---|---|
| P10 (low) | -$513k |
| Median | -$20k |
| P90 (high) | $474k |
| P(running out of cash) | 52% |
10 of 20 modeled outcomes end below $0
Acme analytics · Holt forecast · updated hourly
Demo data is illustrative. Replace with your own typed data prop.
How it computes
Holt ETS(A,A,N) cash forecast rendered as 20 equally-likely quantile dots, with P(runway < threshold).
se = σ·√(1 + Σⱼ₌₁ʰ⁻¹(α+jβ)²), α=0.4 β=0.2; quantileᵢ = mean + σ·probit((i+0.5)/20); risk = Φ((threshold − mean)/σ)Assumptions
- Monthly, regular-interval history.
- Gaussian residuals around the Holt path.
- Fixed smoothing (α=0.4, β=0.2) and a linear trend over the 6/9/12-month horizon (default 9).
Honest about
- The dotplot shows uncertainty as 20 equally-likely outcomes (5% each) instead of one confident line — uncertainty you can count.
- The SE widens by the trend-model law, faster than a random walk's √h.
- Needs ≥2 points; risk collapses to a hard 0/1 if σ ≤ 0 (a point estimate).
- The threshold probability reads off the terminal forecast, not the whole trajectory.
Reference: Hyndman & Athanasopoulos, Forecasting: Principles and Practice
When to reach for it
A single forecast line implies a confidence it doesn't have. Use the Cash Outcome Range when the spread of outcomes is the point — runway, a covenant, a plan you might miss. Instead of a shaded band nobody can read, it shows where cash lands as countable dots and states the downside probability outright: "20% chance of running out by Mar 2027."
How it thinks
From your balance history it fits a Holt linear forecast (reusing the same holtLinear as the forecast widget) to get a terminal mean and a forecast standard error. That error uses the ETS(A,A,N) prediction-interval variance σ·√(1 + Σⱼ(α+jβ)²) (Hyndman & Athanasopoulos, FPP) — it widens with horizon faster than a naive σ·√h, so the spread doesn't claim a precision it lacks. Then:
forecastQuantiles()turns that Gaussian into N equally-likely outcome values — one dot each, so k of N dots below $0 reads directly as a probability.thresholdProbability()computes the exact tail,P(cash < threshold), via a normal CDF.<QuantileDotplot>bins and stacks the dots along the value axis with the at-risk side (below the threshold) highlighted; theriskinsight writes the headline.
This is "hypothetical outcome" thinking made legible — the research shows people reason far better about 3 of 20 runs miss payroll than about a fuzzy interval.
Good to know
- The threshold defaults to
$0(out of cash) but is configurable — set it to a covenant floor or a plan target. - The dotplot's entrance animation is the project's first consumer of
useReducedMotion(): it settles instantly for users who prefer reduced motion, and the static dotplot is the full accessible story. - The accessible table reports the distribution (P10 / median / P90 + the probability), never the raw dot cloud.
How honest is it?
The dots are only as trustworthy as the residual σ behind them, and σ is estimated in-sample — so the spread can read tighter than reality. rollingCoverage() keeps it honest: a one-step rolling-origin backtest that reports how often each band actually contained the next month's value. If the 80% interval only holds 65% over your history, the number says so — the sister Revenue Forecast widget surfaces exactly that line under its chart.
Use it with your data
Data contract
Passed via the history prop — an array of objects: validated at runtime, so a bad shape degrades to the error state rather than crashing.
| Field | Type | Required |
|---|---|---|
| date | string | yes |
| value | number | yes |
Static props
import { ChartUncertaintyRunway } from "@/components/blocks/chart-uncertainty-runway/chart-uncertainty-runway";
<ChartUncertaintyRunway history={myData} />Client fetch (SWR)
const { data, isLoading, error } = useSWR("/api/metric", fetcher);
// route isLoading/error through <ChartStates>, then:
<ChartUncertaintyRunway history={data} />Server component (RSC)
const history = await fetch(url, {
next: { revalidate: 3600 },
}).then((r) => r.json());
<ChartUncertaintyRunway history={history} />Map arbitrary rows
import { mapRows } from "@/lib/sextant/column-map";
const out = mapRows(rows, { /* contractKey: "yourColumn" */ }, dataSchema);
if (out.ok) <ChartUncertaintyRunway history={out.data} />;Make it yours
Every customization below is an optional prop — omit them all and you get the defaults shown above. Recolor, reformat, restyle, or drop the card chrome without forking the component.
Recolor or rename a series (atRisk, survives)
<ChartUncertaintyRunway
series={{ atRisk: { color: "var(--chart-3)", label: "…" } }}
/>Format numbers (currency / locale)
const eur = new Intl.NumberFormat("de-DE", {
style: "currency",
currency: "EUR",
notation: "compact",
});
<ChartUncertaintyRunway valueFormatter={(n) => eur.format(n)} />Replace or hide the narrative
<ChartUncertaintyRunway narrative="Your own one-liner." />
// …or hide it entirely:
<ChartUncertaintyRunway narrative={false} />Restyle any region
<ChartUncertaintyRunway
className="max-w-xl"
classNames={{ body: "bg-muted/20" }}
/>Drop the card chrome (headless plot)
import { ChartUncertaintyRunwayPlot } from "@/components/blocks/chart-uncertainty-runway/chart-uncertainty-runway";
// Just the chart body — bring your own card/layout:
<ChartUncertaintyRunwayPlot history={history} config={config} />