# CohortTriangle (/docs/charts/cohort-triangle)

At month 1, the March vintage retains worst: 47%. Reading down a column compares every cohort at the **same maturity**,
which is the question this chart is for. CohortTriangle stacks cohorts as rows and ages as columns, shading each cell by
a discrete retention level. Newer vintages have been observed for fewer ages, which gives the block its **triangle**
shape.

```tsx
<CohortTriangle
  data={[
    { 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] },
  ]}
  cell={16}
  unit="month"
  title="Monthly retention"
/>
```

## Install

```tsx
import { CohortTriangle } from "@microcharts/react/cohort-triangle";

<CohortTriangle data={cohorts} unit="month" title="Monthly retention" />
```

Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.

## Try it

```tsx
import { CohortTriangle } from "@microcharts/react/cohort-triangle/interactive";

<CohortTriangle
  data={cohorts}
  cell={12}
  unit="month"
/>
```

## When to use it

Use it for monthly or weekly cohorts side by side, spotting which vintage decays worst, or reading retention at equal
maturity in a KPI card. Color intensity is deliberately approximate, so pair it with a number when you need exact
per-cell values. For a single cohort, [RetentionCurve](/docs/charts/retention-curve) draws the decay 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.

**labelled grid**

```tsx
<CohortTriangle data={cohorts} unit="month" />
```

**highlight a vintage**

```tsx
// ring one cohort to anchor the equal-maturity comparison
<CohortTriangle data={cohorts} highlight="Mar" />
```

**dense (no labels)**

```tsx
<CohortTriangle data={cohorts} labels={false} cell={8} />
```

## Variants

Rows are cohorts in input order and columns are age. Row labels are on by default; turn them off for a denser block, or
`highlight` one vintage to anchor the equal-maturity comparison. No worst-vintage flag is painted onto the grid — the
summary names the laggard in words instead.

```tsx
<CohortTriangle data={cohorts} highlight="Mar" />
<CohortTriangle data={cohorts} labels={false} cell={9} />
```

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

## Edge cases

```tsx
<CohortTriangle data={[{ label: "Jan", values: [1, 0.6, 0.45, 0.4] }]} unit="month" />
```

```tsx
// a null / non-finite value is a measured-nothing slot: outlined, never shaded as 0%
<CohortTriangle data={[{ label: "Jan", values: [1, 0.6, null, 0.4] }]} />
```

```tsx
// values over 1.001 are auto-detected as percent, not fraction — same shading
<CohortTriangle data={[{ label: "Jan", values: [100, 62, 48] }, { label: "Feb", values: [100, 47] }]} />
```

```tsx
<CohortTriangle
  data={[{ label: "Jan", values: [1, 0.62, 0.48] }, { label: "Feb", values: [1, 0.47] }]}
  unit="month"
  locale="de-DE"
/>
```

A single cohort skips the comparison and 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

**In a sentence**

```tsx
<p>
  January retention{" "}
  <span className="mc-inline">
    <CohortTriangle data={jan} labels={false} cell={10} unit="month" summary={false} />
  </span>{" "}
  — the January vintage retains 37% at month 5.
</p>
```

**In a table cell**

```tsx
<td>
  <CohortTriangle data={jan} labels={false} cell={12} unit="month" summary={false} />
</td>
```

**In a KPI card**

```tsx
<div className="kpi">
  <span className="figure">37%</span>
  <span className="unit">M5 retention</span>
  <CohortTriangle data={jan} labels={false} cell={12} unit="month" summary={false} />
</div>
```

**In a tab header**

```tsx
<button className="tab">
  Jan <CohortTriangle data={jan} labels={false} cell={10} unit="month" summary={false} />
</button>
```

Best at KPI/card scale — cohort cells need room to resolve.

## Accessibility

Intensity is quantized to five discrete levels rather than a continuous ramp, since a smooth gradient would imply a
precision a handful of pixels cannot deliver. It is still 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](/docs/accessibility#one-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, press `Escape`, or
press outside the chart. On touch, a tap pins and a drag scrubs.

## Props

| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `{ 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. |
| `gap` | `number` | Gap between cells in viewBox units (default 2). |
| `animate` | `boolean` | (interactive) Opt-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](/docs/quickstart#the-shared-grammar).
