Sextant
← Catalog
Block · finance

Scenario Sensitivity (Tornado)

Tornado ranking which driver moves an outcome most, with an auto-generated sensitivity narrative.

State
Size
Mode

Acme · FY26 sensitivity

$4.77M

base FY26 MRR · most sensitive to accountsActive

FY26 MRR is most sensitive to accountsActive — it swings $1.46M across the modeled range.

Acme · FY26 sensitivity — swing in FY26 MRR
DriverLowHighSwing
accountsActive$4.04M$5.5M$1.46M
arpu$4.34M$5.24M$904k
expansionRate$4.6M$5M$398k
churnRate$4.73M$4.81M$82k

Acme analytics · updated every 6 hours

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

How it computes

One-at-a-time sensitivity: flex each driver low↔high, measure the outcome swing, rank widest-first.

swingᵢ = |f(driverᵢ = high) − f(driverᵢ = low)|, all other drivers held at base

Assumptions

  • Drivers are varied independently — no joint moves.
  • The outcome function is deterministic and side-effect-free.
  • Each driver has just a low and a high, not a full range.

Honest about

  • It's strictly one-factor-at-a-time: it cannot capture a scenario where two drivers move together (e.g. volume and price both fall).
  • The hero number is the base-case outcome, not an expected value across scenarios.
  • Zero-swing drivers are still listed, with ties preserving 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
basenumberyes
barsarrayyes

Static props

import { FpaScenarioTornado } from "@/components/blocks/fpa-scenario-tornado/fpa-scenario-tornado";

<FpaScenarioTornado data={myData} />

Client fetch (SWR)

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

Server component (RSC)

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

<FpaScenarioTornado data={data} />

Map arbitrary rows

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

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

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

Format numbers (currency / locale)

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

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

Replace or hide the narrative

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

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

Restyle any region

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

Drop the card chrome (headless plot)

import { FpaScenarioTornadoPlot } from "@/components/blocks/fpa-scenario-tornado/fpa-scenario-tornado";

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