Sextant
← Catalog
Block · intelligent

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.

State
Size
Mode

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.

Acme · Cash runway — projected Cash distribution in 9 months
OutcomeCash
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; the risk insight 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.

FieldTypeRequired
datestringyes
valuenumberyes

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} />