Sextant
← Catalog
Block · intelligent

ARR Goal-Seek (Explorable)

Drag the projection's endpoint to a goal and growth back-solves; drag the driver handles and a live tornado ranks what moves the outcome. The chart as an instrument.

State
Size
Mode

12-month revenue projection

$7.72M

5.2%/mo · 12 months

Ending ARR is most sensitive to growth — it swings $33.2M across the modeled range.

12-month revenue projection — projected ARR over 12 months
FieldValue
Start ARR$4.2M
Growth / mo5.2%
Ending ARR$7.72M
Most sensitiveGrowth

drag the ● (or arrow-key it) to a goal — growth back-solves

Drag the endpoint to explore required growth rates

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

How it computes

Compound ARR projection arr(t) = start·(1+g)ᵗ; drag the endpoint to back-solve the required growth by bisection.

ending = start·(1+g)^months;  back-solve g via bisection to tol = $1;  tornado swing = |ending(high) − ending(low)|

Assumptions

  • Growth is a constant monthly rate — no acceleration or decay.
  • Start ARR is held fixed while solving for growth.
  • Sensitivity ranges (g ∈ [−5%, +20%], start ∈ [50k, 2M]) are hardcoded.

Honest about

  • The back-solve clamps to its bracket rather than diverging outside it.
  • Pixel-to-value mapping for the drag handle is exact only on linear axes.
  • The tornado assumes a monotonic outcome (true for compound growth) — it would mislead on a non-monotonic model.

When to reach for it

Most charts are outputs. This one is an instrument. Use the ARR Goal-Seek when the question is "what would it take?" — drag the projection's endpoint to a number you want to hit, and it back-solves the growth rate required to get there. It turns a planning conversation ("can we reach $10M by December?") into a direct manipulation.

How it thinks

  • Drag-to-target — the endpoint handle reads the live axis scale (useYAxisInverseScale) to turn your drag into a target value, then backSolve() (a deterministic monotone bisection) finds the monthly growth that lands the projection exactly there.
  • Driver handles — the growth and starting-ARR sliders reshape the projection directly; everything recomputes live.
  • Live tornadotornado() flexes each driver across its range and ranks them, so a sensitivity insight names what moves the outcome most: "Ending ARR is most sensitive to growth."

Good to know

  • It's fully interactive standalone — the model lives in local state, so dragging works in production with no provider or wiring.
  • Keyboard-accessible: the handle is a role="slider" you can focus and nudge with the arrow keys; the Y-axis domain is fixed so the mapping stays stable through a drag.
  • Deterministic throughout — the solver and tornado are pure functions, no LLM, so the same drag always yields the same answer.

Use it with your data

Data contract

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

FieldTypeRequired
startArrnumberyes
horizonintegeryes
initialGrowthnumberyes

Static props

import { ChartExplorableTarget } from "@/components/blocks/chart-explorable-target/chart-explorable-target";

<ChartExplorableTarget config={myData} />

Client fetch (SWR)

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

Server component (RSC)

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

<ChartExplorableTarget config={config} />

Map arbitrary rows

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

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

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

Format numbers (currency / locale)

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

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

Replace or hide the narrative

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

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

Restyle any region

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