# GradeProfile (/docs/charts/grade-profile)

A stage climbs 67 m over 900, and the wall sits at 800. GradeProfile shows where. Each segment between two points is
filled from the baseline and colored by its grade, from gentle to brutal, with the elevation ridge riding on top, and
the steepest pitch is labeled so the hardest moment isn't buried.

**How to read it** — color is a _quantized_ grade bin, never a smooth ramp: flats and every descent take the faint band,
then a mid tone, the negative color, and the darkest fill for the brutal pitches. The default `bins` of `[3, 6, 10]`
percent match how climbs are described, so a route reads as gentle, rolling, steep, or wall. Height is the elevation
ridge for shape; the encoded channel is the color, and the interactive readout gives the exact grade. Below 72 pixels
wide the four bins collapse to a flat-versus-climb read, because the finer classes stop being separable there.

```tsx
<GradeProfile
  data={[
    { d: 0, elev: 800 },
    { d: 100, elev: 809 },
    { d: 250, elev: 812 },
    { d: 350, elev: 817 },
    { d: 500, elev: 835 },
    { d: 700, elev: 833 },
    { d: 900, elev: 865 },
  ]}
  format={{ style: "unit", unit: "meter", unitDisplay: "short" }}
  title="Queen stage"
  width={240}
  height={48}
/>
```

`d` and `elev` must share a unit (both metres, say) so that grade — rise ÷ run — is a true percent. Distance need only
be monotonic; the values themselves can be any scale.

## Install

```tsx
import { GradeProfile } from "@microcharts/react/grade-profile";

<GradeProfile data={trail} format={(n) => `${n} m`} title="Queen stage" />
```

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 { GradeProfile } from "@microcharts/react/grade-profile/interactive";

<GradeProfile
  data={trail}
/>
```

## When to use it

Use it for route and climb profiles in cycling, running, and hiking, and to show _where_ the hard pitches fall. For a
single elevation series use Sparkline; distance must be monotonic, so an unordered track won't work.

## Sizing

**stage cell**

```tsx
<GradeProfile data={stage.profile} label="none" width={80} height={24} />
```

**call out the wall**

```tsx
<GradeProfile data={trail} bins={[4, 8, 12]} />
```

## Variants

```tsx
const trail = [
  { d: 0, elev: 800 }, { d: 250, elev: 812 }, { d: 500, elev: 835 },
  { d: 700, elev: 833 }, { d: 900, elev: 865 },
];

<GradeProfile data={trail} label="none" />
```

```tsx
// classify the difficulty differently: brutal starts at 12%
const trail = [
  { d: 0, elev: 800 }, { d: 100, elev: 809 }, { d: 350, elev: 817 },
  { d: 500, elev: 835 }, { d: 900, elev: 865 },
];

<GradeProfile data={trail} bins={[5, 8, 12]} />
```

```tsx
const alps = [
  { d: 0, elev: 600 }, { d: 2000, elev: 780 }, { d: 5000, elev: 1150 },
  { d: 8000, elev: 1400 }, { d: 12000, elev: 1850 },
];

<GradeProfile data={alps} format={{ maximumFractionDigits: 0 }} locale="de-DE" />
```

`format` accepts `Intl.NumberFormatOptions` (with a `locale`) or a formatting function, and applies to **distance and
elevation** numbers in the summary and readout — not grade. Grades are always percent-encoded; the locale still decides
the decimal mark: the alpine profile above reads **"12.000, 1.250 gain; steepest 12,3% at 3.500."** in German.

## Edge cases

```tsx
const drop = [{ d: 0, elev: 900 }, { d: 300, elev: 840 }, { d: 600, elev: 780 }];

<GradeProfile data={drop} format={(n) => `${n} m\
```

```tsx
const gappy = [
  { d: 0, elev: 100 }, { d: 100, elev: 120 },
  { d: 200, elev: NaN }, { d: 300, elev: 160 },
];

<GradeProfile data={gappy} title="Partial track" />
```

A descent-only route has no climb to grade, so no pitch is emphasized and the summary reports **"600 m, no real
climb."** Descents always take the gentlest color rather than borrowing a climb's, because climbing difficulty is what
the chart grades. A non-finite elevation breaks the profile into a gap: the segments touching it drop out and the ridge
splits, so a missing reading never invents a grade. A flat route reports the "no real climb" sentence too, and a single
point has no segment to grade at all, so its accessible name falls back to **"No data."**

## Four homes

**In a sentence**

```tsx
<p>
  Queen stage elevation{" "}
  <span className="mc-inline">
    <GradeProfile data={trail} summary={false} />
  </span>{" "}
  — 865 m gain over 800 km.
</p>
```

**In a table cell**

```tsx
<td>
  <GradeProfile data={trail} />
</td>
```

**In a KPI card**

```tsx
<div className="kpi">
  <span className="figure">865 m</span>
  <span className="unit">total gain</span>
  <GradeProfile data={trail} />
</div>
```

**In a tab header**

```tsx
<button className="tab">
  Stage 12 <GradeProfile data={trail} />
</button>
```

## Accessibility

The accessible name gives the distance, the total climb, and where the hardest pitch falls: **"900, 67 gain; steepest
16% at 800."** None of it depends on color. The interactive entry announces each pitch as you rove it (`←`/`→`), with
the true grade and the cumulative climb.

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) | `{ d: number; elev: number }[]` | Distance + elevation, monotonic in d, same unit so grade is a true percent. |
| `bins` | `[number, number, number]` | Ascending grade-% thresholds (always percent) that quantize the four difficulty bins. |
| `format` | `Intl.NumberFormatOptions \| (n) => string` | Formats distance and elevation in the summary and readout; grades always render as percent. |
| `label` | `"max" \| "none"` | Mark the steepest pitch, or render the profile alone. |
| `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).
