Deal Aging
Average days-in-stage against benchmark, flagging the stage where deals stall and drag the sales cycle.
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.
| Stage | Avg days | Benchmark |
|---|---|---|
| Prospect | 6d | 7d |
| Qualify | 8d | 8d |
| Proposal | 12d | 10d |
| Contract Review | 26d | 11d |
| Closing | 3d | 5d |
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 > benchmarkAssumptions
- 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.
| Field | Type | Required |
|---|---|---|
| stage | string | yes |
| avgDays | number | yes |
| benchmark | number | yes |
| deals | number | yes |
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} />