Sextant
← Catalog
Block · intelligent

Linked KPI Board

KPI sparklines that cross-filter on a shared selection — hover one, all highlight — under a meta-narrative reading across them. The reasoning dashboard.

State
Size
Mode

Acme · FP&A scorecard

3 of 3 KPIs improved MoM — MRR led (+8.0%).

Acme · FP&A scorecard — KPIs by month
MonthMRRGross marginHeadcount
Jul 2025$3.2M58.0%142
Aug 2025$3.42M59.0%148
Sep 2025$3.68M60.0%156
Oct 2025$4.05M62.0%171
Nov 2025$4.38M64.0%185
Dec 2025$4.62M65.0%198
Jan 2026$4.85M66.0%210
Feb 2026$5.12M67.0%224
Mar 2026$5.34M68.0%235
Apr 2026$5.58M69.0%248
May 2026$5.48M70.0%251
Jun 2026$5.92M71.0%267

hover any panel — the guide and values move across all of them

Acme analytics · updated hourly

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

When to reach for it

A board of KPIs read in isolation hides the relationships between them. Use the Linked KPI Board when the cross-reading is the insight — revenue is up but cash shortened the same month. Hover any panel and a guide plus the shown value move across all of them, and a single meta-narrative reads the whole board at once.

How it thinks

  • Shared selection — every panel reads one useSelection() context. Hovering a month sets activeX (via Recharts' activeLabel), and all panels mark it and switch their headline to that month's value. That's linked brushing, with no per-panel wiring.
  • Meta-narrativesummarizeBoard() reads the per-KPI delta insights and synthesizes one sentence: "2 of 3 KPIs improved MoM — Revenue led (+8.1%)." It reasons across widgets, not within one.

The selection-kit provider follows the same no-op-safe discipline as Studio: outside a provider the hook returns inert defaults, so a panel renders standalone with zero overhead.

Good to know

  • Pass any number of aligned monthly KPIs (format: "currency" | "percent" | "number"); they just need to share the months axis.
  • The cross-filter is a hover enhancement; the accessible representation is the data table (months × KPIs), and the meta-narrative is the figure's label.
  • selection-kit is reusable on its own — wrap any group of time-aligned widgets to make them cross-filter.

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
monthsarrayyes
kpisarrayyes

Static props

import { ChartLinkedBoard } from "@/components/blocks/chart-linked-board/chart-linked-board";

<ChartLinkedBoard data={myData} />

Client fetch (SWR)

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

Server component (RSC)

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

<ChartLinkedBoard data={data} />

Map arbitrary rows

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

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

Format numbers (currency / locale)

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

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

Replace or hide the narrative

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

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

Restyle any region

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

Drop the card chrome (headless plot)

import { ChartLinkedBoardPlot } from "@/components/blocks/chart-linked-board/chart-linked-board";

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