Attrition Trend
Annualized attrition over a trailing year with the regrettable split and the year-over-year move.
Team Attrition Trend
13.4%
trailing 12-mo · 59% regrettable
- Attrition
- Regrettable
Attrition grew +73.9% YoY to 13.4% — driven by regrettable exits.
| Month | Separations | Headcount | Regrettable |
|---|---|---|---|
| Apr 2025 | 2 | 285 | 1 |
| May 2025 | 2 | 295 | 1 |
| Jun 2025 | 2 | 308 | 1 |
| Jul 2025 | 3 | 322 | 1 |
| Aug 2025 | 3 | 337 | 1 |
| Sep 2025 | 3 | 352 | 1 |
| Oct 2025 | 3 | 368 | 1 |
| Nov 2025 | 2 | 385 | 1 |
| Dec 2025 | 2 | 402 | 1 |
| Jan 2026 | 7 | 410 | 5 |
| Feb 2026 | 8 | 415 | 6 |
| Mar 2026 | 7 | 418 | 5 |
| Apr 2026 | 6 | 421 | 4 |
| May 2026 | 5 | 424 | 3 |
People ops data · trailing 12-month annualized rate
Demo data is illustrative. Replace with your own typed data prop.
How it computes
Monthly annualized separation rate with a regrettable-attrition overlay and a trailing-twelve-month average.
annualized(m) = 12·separations / headcount; TTM = Σ₁₂ separations / mean₁₂ headcountAssumptions
- Headcount is a point-in-time snapshot, stable within the month.
- Regrettable separations are a subset of total separations.
- Twelve months is the trailing window.
Honest about
- Months with non-positive headcount annualize to 0 rather than blowing up.
- TTM divides by the mean headcount over the window, not a single month-end balance — so a hiring spike doesn't deflate the rate.
- Year-over-year delta needs both the latest and the 12-months-prior point; it is skipped rather than faked when history is short.
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 |
|---|---|---|
| month | string | yes |
| headcount | number | yes |
| separations | number | yes |
| regrettable | number | yes |
Static props
import { PeopleAttritionTrend } from "@/components/blocks/people-attrition-trend/people-attrition-trend";
<PeopleAttritionTrend data={myData} />Client fetch (SWR)
const { data, isLoading, error } = useSWR("/api/metric", fetcher);
// route isLoading/error through <ChartStates>, then:
<PeopleAttritionTrend data={data} />Server component (RSC)
const data = await fetch(url, {
next: { revalidate: 3600 },
}).then((r) => r.json());
<PeopleAttritionTrend data={data} />Map arbitrary rows
import { mapRows } from "@/lib/sextant/column-map";
const out = mapRows(rows, { /* contractKey: "yourColumn" */ }, dataSchema);
if (out.ok) <PeopleAttritionTrend 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 (attrition, regrettable)
<PeopleAttritionTrend
series={{ attrition: { color: "var(--chart-3)", label: "…" } }}
/>Format numbers (currency / locale)
const eur = new Intl.NumberFormat("de-DE", {
style: "currency",
currency: "EUR",
notation: "compact",
});
<PeopleAttritionTrend valueFormatter={(n) => eur.format(n)} />Replace or hide the narrative
<PeopleAttritionTrend narrative="Your own one-liner." />
// …or hide it entirely:
<PeopleAttritionTrend narrative={false} />Restyle any region
<PeopleAttritionTrend
className="max-w-xl"
classNames={{ body: "bg-muted/20" }}
/>Drop the card chrome (headless plot)
import { PeopleAttritionTrendPlot } from "@/components/blocks/people-attrition-trend/people-attrition-trend";
// Just the chart body — bring your own card/layout:
<PeopleAttritionTrendPlot data={data} config={config} />