Rolling Forecast vs Plan
Plan vs actual vs forecast by period with a Holt re-forecast of open periods and an attainment/bias narrative.
Acme MRR rolling forecast · 2026
99%
of plan attained · -0.1% forecast bias
- Plan
- Actual
- Re-forecast
MRR is tracking 99% of plan in 2026, on plan.
| Period | Plan | Forecast | Actual |
|---|---|---|---|
| Q1 2026 | $4.2M | $4.28M | $4.32M |
| Q2 2026 | $4.58M | $4.41M | $4.38M |
| Q3 2026 | $4.95M | $4.72M | |
| Q4 2026 | $5.38M | $5.31M |
Acme analytics · updated daily via FP&A model
Demo data is illustrative. Replace with your own typed data prop.
How it computes
Forecast attainment and bias, with a Holt(α=0.4, β=0.3) re-forecast of the open periods.
attainment = latest(actual||forecast)/plan; bias = mean((forecast − actual)/actual) over closed periods; reforecast = holtLinear(actuals).forecastAssumptions
- A period is "closed" only when both a forecast and a non-zero actual exist.
- Holt's additive trend fits the series (no seasonality or multiplicative effects).
- Only actuals feed the re-forecast; original forecasts are used only to measure bias.
Honest about
- The re-forecast cone widens by the ETS(A,A,N) SE σ·√(1 + Σ(α+jβ)²), faster than √h — honest about a trend model's compounding uncertainty.
- With a single closed period, bias is that one ratio with no smoothing.
- It's pure time-series extrapolation: no macro or product drivers enter the model.
Reference: Hyndman & Athanasopoulos, Forecasting: Principles and Practice
Use it with your data
Data contract
Passed via the data prop — an array of objects: validated at runtime, so a bad shape degrades to the error state rather than crashing.
| Field | Type | Required |
|---|---|---|
| period | string | yes |
| plan | number | yes |
| forecast | number | no |
| actual | number | no |
Static props
import { FpaRollingForecast } from "@/components/blocks/fpa-rolling-forecast/fpa-rolling-forecast";
<FpaRollingForecast data={myData} />Client fetch (SWR)
const { data, isLoading, error } = useSWR("/api/metric", fetcher);
// route isLoading/error through <ChartStates>, then:
<FpaRollingForecast data={data} />Server component (RSC)
const data = await fetch(url, {
next: { revalidate: 3600 },
}).then((r) => r.json());
<FpaRollingForecast data={data} />Map arbitrary rows
import { mapRows } from "@/lib/sextant/column-map";
const out = mapRows(rows, { /* contractKey: "yourColumn" */ }, dataSchema);
if (out.ok) <FpaRollingForecast data={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 (plan, actual, forecast, reforecast)
<FpaRollingForecast
series={{ plan: { color: "var(--chart-3)", label: "…" } }}
/>Format numbers (currency / locale)
const eur = new Intl.NumberFormat("de-DE", {
style: "currency",
currency: "EUR",
notation: "compact",
});
<FpaRollingForecast valueFormatter={(n) => eur.format(n)} />Replace or hide the narrative
<FpaRollingForecast narrative="Your own one-liner." />
// …or hide it entirely:
<FpaRollingForecast narrative={false} />Restyle any region
<FpaRollingForecast
className="max-w-xl"
classNames={{ body: "bg-muted/20" }}
/>Drop the card chrome (headless plot)
import { FpaRollingForecastPlot } from "@/components/blocks/fpa-rolling-forecast/fpa-rolling-forecast";
// Just the chart body — bring your own card/layout:
<FpaRollingForecastPlot data={data} config={config} />