Sextant
← Catalog
Block · finance

P&L Bridge

Waterfall walking a metric across periods (price, volume, mix, cost) with gain/loss coloring and a bridge narrative.

State
Size
Mode

Revenue bridge · Q1 2026

$4.75M

+$550k vs $4.2M prior

  • Increase
  • Decrease
  • Balance

Gross profit rose from $4.2M to $4.75M in Q1 2026, led by New customers (+$420k).

Revenue bridge — Q1 2026
StepChangeRunning
Start$4.2M
Higher ASP+$280k$4.48M
New customers+$420k$4.9M
Churn-$180k$4.72M
COGS increase-$120k$4.6M
Platform savings+$150k$4.75M
End+$550k$4.75M

Acme Finance · updated hourly

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

How it computes

Additive waterfall: a starting balance walked through sequential deltas to an ending balance.

each step: end = start + delta, running += delta;  net = to − from

Assumptions

  • Steps are additive and independent.
  • "Increase" / "Decrease" are display labels with no enforced sign.
  • The ending value is purely the cumulative result of the deltas.

Honest about

  • It's a descriptive decomposition — it shows where value moved, it does not infer cause.
  • Signs aren't validated: a negative delta can carry an "Increase" label, so the caller owns semantic correctness.
  • The top contributor is the largest absolute delta, ties resolved by input order.

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
fromnumberyes
stepsarrayyes

Static props

import { FpaPnlBridge } from "@/components/blocks/fpa-pnl-bridge/fpa-pnl-bridge";

<FpaPnlBridge data={myData} />

Client fetch (SWR)

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

Server component (RSC)

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

<FpaPnlBridge data={data} />

Map arbitrary rows

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

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

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

Format numbers (currency / locale)

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

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

Replace or hide the narrative

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

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

Restyle any region

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