Forecast Projection
Holt linear forecast with confidence cone fading into uncertainty. Auto-generated projection narrative.
Acme · MRR forecast
$4,754,655
projected by Dec 2026
- Actual
- Forecast
Revenue projected to reach $4.75M by Dec 2026 (80% confidence).
| Month | Actual | Forecast |
|---|---|---|
| Jul 2025 | 4,280,000 | |
| Aug 2025 | 4,405,135 | |
| Sep 2025 | 4,459,343 | |
| Oct 2025 | 4,559,836 | |
| Nov 2025 | 4,644,729 | |
| Dec 2025 | 4,572,250 | |
| Jan 2026 | 4,388,731 | |
| Feb 2026 | 4,306,970 | |
| Mar 2026 | 4,409,854 | |
| Apr 2026 | 4,551,300 | |
| May 2026 | 4,607,613 | |
| Jun 2026 | 4,662,817 | 4,662,817 |
| Jul 2026 | 4,599,242 | |
| Aug 2026 | 4,630,325 | |
| Sep 2026 | 4,661,407 | |
| Oct 2026 | 4,692,490 | |
| Nov 2026 | 4,723,573 | |
| Dec 2026 | 4,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) · seAssumptions
- 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:
seis 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; the1 + Σ(α+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 Acklamprobit), 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 viabuildForecastSeries, 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.
| Field | Type | Required |
|---|---|---|
| date | string | yes |
| value | number | yes |
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} />