# Dumbbell (/docs/charts/dumbbell)

Dumbbell draws each row as a hollow dot at `from`, a filled dot at `to`, and a connector between them: a band that moved
62,000 → 84,000 reads without a legend. Direction is shape-coded, so it never relies on color alone, and the pair
renders the same whichever way the values run. With `positive` the connector takes the valence token by direction. Drop
`positive` for ranges such as min→max or a confidence span, which have no valence to color.

```tsx
<Dumbbell data={[{ from: 62000, to: 84000 }]} title="Band move" style={{ width: 200, height: 28 }} />
```

## Install

```tsx
import { Dumbbell } from "@microcharts/react/dumbbell";

<Dumbbell data={[{ from: 62000, to: 84000 }]} title="Band move" />
```

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 { Dumbbell } from "@microcharts/react/dumbbell/interactive";

<Dumbbell
  data={bands}
/>
```

## When to use it

Use it for salary bands, before/after per table row, and ranges. When you have many categories and the crossings matter,
use Slope; when the path between the two ends matters, use Sparkline.

## Sizing

**table cell**

```tsx
<Dumbbell data={[row.band]} width={60} height={12} />
```

**a range, not a change**

```tsx
// no positive prop — a min→max range has no valence to color
<Dumbbell data={[{ from: p5, to: p95 }]} />
```

## Variants

```tsx
<Dumbbell
  data={[
    { label: "Paris", from: 52, to: 61 },
    { label: "Oslo", from: 66, to: 60 },
  ]}
  positive="up"
/>
```

```tsx
<Dumbbell data={[{ from: 40, to: 60 }]} label="value" domain={[0, 100]} />
```

## Edge cases

```tsx
// from === to → a single dot, no connector
<Dumbbell data={[{ from: 55, to: 55 }]} />
```

When a row's `from` equals its `to`, the connector is dropped and one filled dot is drawn, rather than a hollow and a
filled dot stacked on the same coordinate. The summary says **"No change at 55."** for a single row.

## Four homes

**In a sentence**

```tsx
<p>
  Berlin's band moved{" "}
  <span className="mc-inline">
    <Dumbbell data={[{ from: 48, to: 68 }]} width={70} height={14} summary={false} />
  </span>{" "}
  from €48k to €68k — up 42%.
</p>
```

**In a table cell**

```tsx
<td>
  <Dumbbell data={[{ label: "Berlin", from: 48, to: 68 }]} />
</td>
```

**In a KPI card**

```tsx
<div className="kpi">
  <span className="figure">€48k → €68k</span>
  <span className="unit">Berlin, up 42%</span>
  <Dumbbell data={bands} positive="up" width={130} height={52} />
</div>
```

**In a tab header**

```tsx
<button className="tab">
  Offices <Dumbbell data={[{ from: 48, to: 68 }]} width={60} height={12} summary={false} />
</button>
```

## Accessibility

A single row reads **"From 40 to 60, up 50%."**, a degenerate pair reads **"No change at 55."**, and a multi-row chart
leads with the largest change (**"2 rows. Largest change Paris, up 17%."**). The interactive entry roves rows with ↑/↓
(←/→ do the same) and announces each row's own pair: **"From 52 to 61, up 17%."**

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?; from; to }[]` | Start/end pairs. |
| `positive` | `"up" \| "down"` | Direction valence for CHANGES; drop it for ranges (no valence). |
| `label` | `"value" \| "none"` | From/to values outside the dots (drop when the span is tight). |
| `highlight` | `number \| string` | Accent one row. |
| `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).
