CohortTriangle
React cohort triangle chart — retention by vintage × age as a shaded triangle heatmap. Tiny accessible SVG microchart; RSC-safe static or /interactive.
CohortTriangle stacks retention cohorts as rows and ages as columns, shading each cell by a discrete retention level. Because newer vintages have been observed for fewer ages, the block takes the classic triangle shape — and reading down a column compares every cohort at the same maturity, which is where "who retains worst" actually lives.
Install
import { CohortTriangle } from "@microcharts/react/cohort-triangle";// cohorts — real values under “Sample data” below<CohortTriangle data={cohorts} unit="month" title="Monthly retention" />Sample data
// Five monthly vintages, decaying then flattening — ragged because newer// cohorts have been observed for fewer months. March leaks worst by month 1.const cohorts = [ { label: "Jan", values: [1, 0.62, 0.48, 0.41, 0.38, 0.37] }, { label: "Feb", values: [1, 0.58, 0.44, 0.38, 0.35] }, { label: "Mar", values: [1, 0.47, 0.36, 0.31] }, { label: "Apr", values: [1, 0.55, 0.42] }, { label: "May", values: [1, 0.52] },];Try it
When to use it
- Good for — monthly or weekly cohorts side by side, spotting which vintage decays worst, reading retention at equal maturity in a KPI card.
- Avoid for — exact per-cell values (color intensity is deliberately approximate; pair it with a number when precision matters), or a single cohort's decay, where RetentionCurve draws the curve directly.
Sizing
CohortTriangle sizes from cell — the edge length of one square in viewBox units. Bump it to scale the whole grid; the
viewBox keeps the aspect ratio when CSS drives the width. Row labels seat automatically and drop out cleanly below
roughly an 8-unit cell.
Variants
Row labels are on by default. Turn them off for a dense, glanceable block, or highlight one vintage to anchor the
equal-maturity comparison.
The unit prop renames the age columns in the summary and announcements — "month", "week", "day", whatever the
cohort period is.
Edge cases
A single cohort skips the comparison and just states its first reading. A null (or any non-finite) value renders as an
outlined gap slot — a "measured nothing here" cell, never a shaded 0%. data accepts either a 0–1 fraction or a
0–100 percent series — whichever the max value implies — so a raw percent export renders identically to its fraction
form. With a locale, every announced number follows that locale's own formatting.
Four homes
Why this default
Rows are cohorts in input order and columns are age, because the decision — which vintage retains worst — is a vertical read at a fixed age, and the ragged right edge falls out naturally from newer cohorts simply having fewer observed ages. Intensity is quantized to five discrete levels (never a continuous ramp): a smooth gradient would imply a precision a handful of pixels cannot deliver. A worst-vintage flag is off by default — the marks stay honest and the summary names the laggard in words instead of painting a verdict onto the grid.
Accessibility
Intensity is a color channel, so CohortTriangle always pairs it with a numeric summary that compares at equal maturity — "5 cohorts; at month 1, Mar retains worst (47%); newest May starts at 100%.". Direction is stated in words, never by color alone, so it survives forced-colors and color-blind viewing. The interactive entry adds 2-D arrow-key navigation, announcing each cell as "Feb cohort, month 1: 58%" as you move.
The interactive entry follows the shared interaction contract:
arrow keys rove between units on both axes, Home and End jump to the ends, and a click, tap, Enter or
Space selects a unit — pinning its readout so it survives blur, until you select it again or press Escape.
On touch, a tap pins and a drag scrubs.
Props
| Prop | Type | Notes |
|---|---|---|
| data* | { label, values }[] | One row per cohort; values[i] = retention at age i (0–1 or 0–100, ragged). |
| labels | boolean | Cohort labels in a left gutter (default true; drops at tiny cell sizes). |
| highlight | string | Ring the cohort with this label — the comparison focus. |
| unit | string | Age-column noun for the summary (default "period"). |
| cell | number | Cell edge length in viewBox units. |
| title | string | Accessible name; joins the auto summary. |
| summary | string | false | Override or disable the auto summary. |
| animate | boolean | interactiveOpt-in entrance motion when the chart mounts client-side — add import "@microcharts/react/motion" once. Inert on the server, on hydrated server HTML, and under prefers-reduced-motion. |
Plus the shared grammar — data, domain, color, title, summary, format — and the layout props (width, height, className, style) that every chart accepts. Interactive entries also share animate and live, and — wherever a chart has more than one navigable unit — onActive, onSelect, selectedIndex and defaultSelectedIndex; and — wherever the chart shows a hover value — readout. See the shared grammar.
Related charts
EnsembleGhosts
React ensemble ghosts chart — faint ensemble paths with an emphasised median-like member. Tiny accessible SVG microchart; RSC-safe static or /interactive.
StreakSpark
React streak spark chart — consecutive outcomes as a compact streak spark. Tiny accessible SVG microchart; RSC-safe static or /interactive.