Sextant
← Catalog
Block · finance

Variance Heatmap

Account × period grid of variance % vs plan with favorability-aware, magnitude-scaled coloring and a worst-cell hero. Pure CSS Grid.

State
Size
Mode

Acme · Expense variance by team

+21%

worst variance · Marketing in Apr 2026

  • Favorable
  • Unfavorable

Marketing is the largest unfavorable variance at +21% vs plan in Apr 2026.

Acme · Expense variance by team — variance % by account and period
AccountDec 2025Jan 2026Feb 2026Mar 2026Apr 2026May 2026
Sales+2%-1%+3%+8%+15%+9%
Marketing+6%+9%+12%+2%+21%+18%
R&D-5%-3%+1%-2%+4%-1%
G&A-1%+0%-1%+2%+6%+3%
Ops+4%+7%+5%+11%+8%+4%

Acme accounting · updated hourly

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

How it computes

Account × period variance-% grid on a diverging scale, with the worst unfavorable cell surfaced.

intensity = min(1, |variance%| / 0.2);  cell = mix(base, ≤50% alpha);  favorable = costAccounts ? v<0 : v>0

Assumptions

  • Variance % is precomputed and passed in, not derived from actual/budget here.
  • Cost accounts carry the opposite favorability polarity to revenue.
  • Color saturates at ±20% variance.

Honest about

  • Color stops deepening past ±20% — a ±20% and a ±60% miss look equally intense, so read the numbers for magnitude.
  • Alpha is capped at 50% to hold WCAG AA text contrast, not for any statistical reason.
  • The polarity toggle is global — every account is treated as cost or as revenue, with no per-row override.
  • When nothing is unfavorable the narrative says so explicitly rather than inventing a worst cell.

Use it with your data

Data contract

Passed via the data prop — an object: validated at runtime, so a bad shape degrades to the error state rather than crashing.

FieldTypeRequired
periodsarrayyes
rowsarrayyes
costAccountsbooleanno

Static props

import { FpaVarianceHeatmap } from "@/components/blocks/fpa-variance-heatmap/fpa-variance-heatmap";

<FpaVarianceHeatmap data={myData} />

Client fetch (SWR)

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

Server component (RSC)

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

<FpaVarianceHeatmap data={data} />

Map arbitrary rows

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

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

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

Format numbers (currency / locale)

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

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

Replace or hide the narrative

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

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

Restyle any region

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