Sextant
← Catalog
Block · intelligent

Forecast Projection

Holt linear forecast with confidence cone fading into uncertainty. Auto-generated projection narrative.

State
Size
Mode

Acme · MRR forecast

$4,754,655

projected by Dec 2026

  • Actual
  • Forecast

Revenue projected to reach $4.75M by Dec 2026 (80% confidence).

Acme · MRR forecast — actual and forecast by month
MonthActualForecast
Jul 20254,280,000
Aug 20254,405,135
Sep 20254,459,343
Oct 20254,559,836
Nov 20254,644,729
Dec 20254,572,250
Jan 20264,388,731
Feb 20264,306,970
Mar 20264,409,854
Apr 20264,551,300
May 20264,607,613
Jun 20264,662,8174,662,817
Jul 20264,599,242
Aug 20264,630,325
Sep 20264,661,407
Oct 20264,692,490
Nov 20264,723,573
Dec 20264,754,655

Acme analytics · updated hourly · Holt linear model

80% band — backtested 38% coverage (1-mo ahead, n=8)

Demo data is illustrative. Replace with your own typed data prop.

How it computes

Holt's linear-trend exponential smoothing — ETS(A,A,N) — with model-derived confidence cones.

se = σ·√(1 + Σⱼ₌₁ʰ⁻¹(α + jβ)²);  band half-width = z(confidence) · se

Assumptions

  • Residuals are Gaussian and symmetric — fine for most financial series, an approximation for strongly skewed or bounded metrics.
  • The fitted level + trend continue forward over the horizon.
  • Residual σ is estimated from in-sample one-step errors.
  • Prior-calibrated mode (optional): the installed Benchmark Pack prior describes a cohort whose relative error scale transfers to this series — built-in packs ship illustrative defaults, not measured cohorts.

Honest about

  • The cone widens with the trend-model SE √(1 + Σ(α+jβ)²), not a naive σ·√h — so long horizons are honestly wide, not falsely tight.
  • z(confidence) is the real two-sided normal quantile (Acklam probit): 95% → 1.96, no stretched bands.
  • A one-step rolling-origin backtest (rollingCoverage) reports how often actuals landed inside each band — surfaced in the footnote, e.g. "80% band — backtested 78% coverage".
  • Below two data points it renders an explicit empty state, never a flat-zero forecast.
  • Optional prior-calibration: install a Benchmark Pack and the band half-width becomes a precision-weighted blend of the model's own empirical error and a cohort prior (weight priorPrecision/(priorPrecision + n), so the prior fades ~1/n as your history grows). It moves only the band, never the point forecast — and rollingCoveragePrior reports coverage against the prior too, flagging when the cohort doesn't fit your series (cohort divergence).

Reference: Hyndman & Athanasopoulos, Forecasting: Principles and Practice

When to reach for it

Use the Revenue Forecast when someone needs a number and its uncertainty — a board target, a hiring plan, a runway check. It draws the Holt-linear projection as a dashed median inside 50% / 80% confidence cones, so the range is visible instead of implied by a single confident-looking line.

How it thinks

From dated history it fits Holt's linear-trend exponential smoothing (holtLinear, level α + trend β) and extends the final level + trend forward. Around that median it draws a cone whose half-width is z(confidence) × se, where:

  • se is the ETS(A,A,N) forecast standard error σ·√(1 + Σⱼ₌₁ʰ⁻¹(α+jβ)²) (Hyndman & Athanasopoulos, Forecasting: Principles and Practice). The residual σ comes from the in-sample one-step errors; the 1 + Σ(α+jβ)² term is how a trend model's uncertainty actually compounds — it widens faster than a naive σ·√h, so a 12-month cone is honestly wide rather than falsely tight.
  • z(confidence) is the real two-sided normal quantile (zForConfidence, an Acklam probit), so 95% → 1.96. No fudge factors, no stretched bands.

The bands are Gaussian and symmetric by default — fine for most financial series, but a strongly skewed or bounded metric is only being approximated. For those, flip intervalMethod to "conformal": instead of z × σ, the cone is sized from the empirical quantile of the model's own out-of-sample one-step errors — split / inductive conformal prediction (Vovk, Gammerman & Shafer, Algorithmic Learning in a Random World, 2005; Angelopoulos & Bates, A Gentle Introduction to Conformal Prediction, 2021). That drops the normality assumption entirely — the width comes from how wrong the model has actually been on held-out data, not a bell curve — and the "signed" mode yields an asymmetric band that captures skew the Gaussian one can't represent. The horizon-widening still follows the ETS variance law, so conformal changes the cone's level, not its shape; and when there's too little history to certify the level (95% needs ≥19 held-out errors) it falls back to the Gaussian band rather than printing an over-confident one. Its own honest caveat: the conformal guarantee assumes residual exchangeability, which a trending series only approximately meets — the rolling-origin design keeps the scores out-of-sample to soften that. The accessible table mirrors the median and the band edges.

Prior-calibrated — borrow strength from your cohort

When the history is short, the model's own error estimate is noisy — exactly when a confident-looking band is most dangerous. Pass a prior (a SegmentPrior from an installable Benchmark Pack, sextant-insights/benchmark-packs.ts) and the cone is prior-calibrated: the band half-width becomes a precision-weighted blend of the model's own empirical error and the cohort's typical error, q = w·qₚ + (1 − w)·qₑ with w = priorPrecision / (priorPrecision + n). So a brand-new series leans on its segment's prior, and the prior fades like ~1/n as your own history accrues (priorShrinkConformal). It moves only the band — the point forecast stays your own Holt path — and the horizon-widening still follows the ETS law, so the prior resizes the cone's level, never its shape. The honest check is built in: rollingCoveragePrior() reports coverage against the prior band too, flagging cohort divergence when the borrowed prior doesn't actually fit your series. (The built-in packs ship illustrative defaults; install your own measured pack via loadBenchmarkPack.)

How honest is the band?

A band built from in-sample σ can read tighter than reality. rollingCoverage() is the check: a one-step rolling-origin backtest that re-fits at each historical origin and reports how often the next actual landed inside each band. The footnote surfaces it — "80% band — backtested 78% coverage" — so an evaluator can see whether the interval is calibrated instead of taking it on faith. Persistent under-coverage there is the signal to widen σ, revisit the model, or switch to intervalMethod: "conformal" — which sizes the band from those same held-out errors directly, so it can't be over-confident in the way an in-sample σ can.

Good to know

  • alpha / beta, the horizon, the confidence level, and the interval method (intervalMethod: "gaussian" | "conformal") are all live knobs; the headless <ChartForecastProjectionPlot> takes the same props and derives the same series via buildForecastSeries, so you can drop the plot into your own layout.
  • Below two data points the widget renders an explicit empty state — never a flat-zero forecast or a silently-missing narrative.

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 { ChartForecastProjection } from "@/components/blocks/chart-forecast-projection/chart-forecast-projection";

<ChartForecastProjection history={myData} />

Client fetch (SWR)

const { data, isLoading, error } = useSWR("/api/metric", fetcher);
// route isLoading/error through <ChartStates>, then:
<ChartForecastProjection history={data} />

Server component (RSC)

const history = await fetch(url, {
  next: { revalidate: 3600 },
}).then((r) => r.json());

<ChartForecastProjection history={history} />

Map arbitrary rows

import { mapRows } from "@/lib/sextant/column-map";

const out = mapRows(rows, { /* contractKey: "yourColumn" */ }, dataSchema);
if (out.ok) <ChartForecastProjection 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 (actual, forecast, band)

<ChartForecastProjection
  series={{ actual: { color: "var(--chart-3)", label: "…" } }}
/>

Format numbers (currency / locale)

const eur = new Intl.NumberFormat("de-DE", {
  style: "currency",
  currency: "EUR",
  notation: "compact",
});

<ChartForecastProjection valueFormatter={(n) => eur.format(n)} />

Replace or hide the narrative

<ChartForecastProjection narrative="Your own one-liner." />

// …or hide it entirely:
<ChartForecastProjection narrative={false} />

Restyle any region

<ChartForecastProjection
  className="max-w-xl"
  classNames={{ body: "bg-muted/20" }}
/>

Drop the card chrome (headless plot)

import { ChartForecastProjectionPlot } from "@/components/blocks/chart-forecast-projection/chart-forecast-projection";

// Just the chart body — bring your own card/layout:
<ChartForecastProjectionPlot history={history} config={config} />