Skip to content
microcharts
ReferenceDecisionCohortTriangle

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.

CohortTriangle
interactive · 5.17 kB · static · 2.62 kB

Install

Import & usereact/cohort-triangle
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] },];
Needs package + stylesheetSet up with AI

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.

highlight a vintage vs dense, no labels

The unit prop renames the age columns in the summary and announcements — "month", "week", "day", whatever the cohort period is.

Edge cases

single cohort
missing reading (gap, not zero)
0–100 input
locale

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

PropTypeNotes
data*{ label, values }[]One row per cohort; values[i] = retention at age i (0–1 or 0–100, ragged).
labelsbooleanCohort labels in a left gutter (default true; drops at tiny cell sizes).
highlightstringRing the cohort with this label — the comparison focus.
unitstringAge-column noun for the summary (default "period").
cellnumberCell edge length in viewBox units.
titlestringAccessible name; joins the auto summary.
summarystring | falseOverride or disable the auto summary.
animatebooleaninteractiveOpt-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