Sextant
← Catalog
Block · sales

Deal Aging

Average days-in-stage against benchmark, flagging the stage where deals stall and drag the sales cycle.

State
Size
Mode

Sales cycle by stage

55 days

avg sales cycle · 5 stages

  • On track
  • Slow
  • Benchmark

Deals stall longest in Contract Review — 26d vs the 11d benchmark.

Sales cycle by stage — avg days in stage vs benchmark
StageAvg daysBenchmark
Prospect6d7d
Qualify8d8d
Proposal12d10d
Contract Review26d11d
Closing3d5d

Salesforce · updated hourly

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

How it computes

Average days-in-stage vs a per-stage benchmark, flagging the bottleneck above a slow-ratio threshold.

bottleneck = argmax(avgDays − benchmark);  red if avgDays > benchmark·slowRatio (default 1.2), amber if > benchmark

Assumptions

  • Each stage has a fixed scalar benchmark.
  • Slowness is multiplicative against the benchmark.
  • All open deals in a stage age alike.

Honest about

  • The 1.2× slow threshold is a hardcoded knob, not derived from the data.
  • The benchmark is a visual reference point — it doesn't enter any sum.
  • Empty input degrades safely; the worst stage defaults to the first when there's nothing to rank.

Use it with your data

Data contract

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

FieldTypeRequired
stagestringyes
avgDaysnumberyes
benchmarknumberyes
dealsnumberyes

Static props

import { SalesDealAging } from "@/components/blocks/sales-deal-aging/sales-deal-aging";

<SalesDealAging data={myData} />

Client fetch (SWR)

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

Server component (RSC)

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

<SalesDealAging data={data} />

Map arbitrary rows

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

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

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

Format numbers (currency / locale)

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

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

Replace or hide the narrative

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

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

Restyle any region

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

Drop the card chrome (headless plot)

import { SalesDealAgingPlot } from "@/components/blocks/sales-deal-aging/sales-deal-aging";

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