<!-- Protocore Design System — ChartContainer -->
# ChartContainer

- **Category:** Charts (`charts`)
- **Slug:** `charts/chart-container`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { ChartContainer } from "@protocore/pds/charts";`
- **Docs:** https://pds.protocore.io/components/charts/chart-container

> The bordered, theme-aware frame that hosts a chart and exposes the --pds-chart-1..5 ramp.

## The chart color doctrine — monochrome + signal

The categorical ramp is **five ranked slots, not five equal colors**:

- **`--pds-chart-1`** — ink. The near-fg tone (`#fafafa` dark / `#0a0a0a` light). The lead series.
- **`--pds-chart-2`, `--pds-chart-3`** — progressively muted greys. Secondary and structural series.
- **`--pds-chart-4`** — signal green (`#3fcf8e` / `#0e9e6e`).
- **`--pds-chart-5`** — signal amber (`#d29922` / `#9a6c00`).

The rule: **rank your series and let the ink slot lead; keep supporting series greyscale; spend the two signal hues only where color carries meaning** — the live metric, the threshold breach, the healthy-vs-degraded split. A chart where every series is a different bright color says nothing about which one matters. This is the charts-family expression of the system-wide differentiation contract, where `--pds-accent` and the status hues are reserved signals, never decoration.

The ramp flips per theme so contrast holds on either background, and the values are hardcoded identically in `tokens/index.ts` (for recharts, via JS) and `ChartContainer.css` (for CSS) so the two never drift. `ChartContainer` is what exposes them: the recharts wrappers resolve each series to `var(--pds-chart-N)` by position, so they inherit whatever the nearest container pins.

## When to use it

Use **ChartContainer** as the shell for every [LineChart](/charts/line-chart), [AreaChart](/charts/area-chart), and [BarChart](/charts/bar-chart) on a dashboard — it standardizes the header, legend placement, and (critically) the ramp so charts read as one family. It is server-safe and ships in the main `@protocore/pds` entry.

You don't need it for a bare [Sparkline](/charts/sparkline) (which is frameless by design), and you can render a recharts wrapper without it — but then you must supply the `--pds-chart-*` ramp yourself. Prefer the container.

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `actions` | `ReactNode` | no | — | Actions slot pinned to the right of the header row (e.g. a menu button). |
| `children` | `ReactNode` | no | — | Chart body — typically one of the recharts wrappers. |
| `className` | `string` | no | — | — |
| `height` | `number` | no | `240` | Fixed body height in px. Defaults to `240`. |
| `label` | `ReactNode` | no | — | Mono UPPERCASE label rendered at the left of the header row. |
| `legend` | `ReactNode` | no | — | Legend slot rendered below the label row (e.g. a `ChartLegend`). |
| `style` | `CSSProperties` | no | — | — |
| `theme` | `enum` | no | `auto` | Pin the `--pds-chart-*` ramp to a theme, or defer to CSS defaults (`auto`). |

## Examples

### Basics

Wrap any chart in a `ChartContainer` to get the family frame: a bordered surface, a mono-UPPERCASE `label` header, and a fixed-height body. Match `height` on the container and the chart so the body doesn't clip or leave a gap.

```tsx
import { ChartContainer } from "@protocore/pds";
import { LineChart } from "@protocore/pds/charts";

const data = [
  { t: "00:00", rps: 412 },
  { t: "04:00", rps: 466 },
  { t: "08:00", rps: 540 },
  { t: "12:00", rps: 598 },
  { t: "16:00", rps: 631 },
  { t: "20:00", rps: 690 },
];

export default function Demo() {
  return (
    <ChartContainer label="RPC throughput / req·s⁻¹" height={220}>
      <LineChart data={data} xKey="t" series={[{ key: "rps", label: "req/s" }]} height={220} />
    </ChartContainer>
  );
}
```

### Legend and actions

The `actions` slot pins controls (a menu, a range toggle) to the right of the header; the `legend` slot sits below it — typically a [ChartLegend](/charts/chart-legend). Both are plain `ReactNode`s, so compose them from any pds control.

```tsx
import { ChartContainer, IconButton } from "@protocore/pds";
import { AreaChart, ChartLegend } from "@protocore/pds/charts";
import { MoreHorizontal } from "lucide-react";

const data = [
  { day: "Mon", inbound: 1240, settled: 1180 },
  { day: "Tue", inbound: 1360, settled: 1290 },
  { day: "Wed", inbound: 1180, settled: 1150 },
  { day: "Thu", inbound: 1520, settled: 1440 },
  { day: "Fri", inbound: 1710, settled: 1660 },
];

const series = [
  { key: "inbound", label: "Inbound" },
  { key: "settled", label: "Settled" },
];

export default function Demo() {
  return (
    <ChartContainer
      label="Ledger settlement / txns"
      actions={
        <IconButton aria-label="Chart options" variant="ghost" size="sm">
          <MoreHorizontal />
        </IconButton>
      }
      legend={<ChartLegend items={series.map((s) => ({ label: s.label }))} />}
      height={200}
    >
      <AreaChart data={data} xKey="day" series={series} height={200} />
    </ChartContainer>
  );
}
```

## Do & don't

**Do**

- Keep `height` on the container and the chart in sync.
- Write `label` as a short mono-UPPERCASE metric with its unit (e.g. "RPC throughput / req·s⁻¹").
- Rank series and let --pds-chart-1 (ink) lead; keep supporting series greyscale.
- Pin `theme` only when a chart lives on a fixed-theme surface; otherwise leave it auto.

**Don't**

- Don't give five equally-important series five bright colors — the ramp is a rank, not a palette.
- Don't spend the signal hues (chart-4/5) on decorative series; reserve them for meaning.
- Don't override `--pds-chart-*` with raw hex — pin a `theme` or adjust the tokens.
- Don't wrap a Sparkline in it; sparklines are intentionally frameless.

## Accessibility

**Notes**

- ChartContainer is a presentational frame; the accessible meaning comes from the chart inside it and its `label`.
- The ramp keeps a monochrome luminance spread, so series stay distinguishable in grayscale and for color-vision deficiencies — pair with a legend rather than relying on hue.
- The `actions` slot must contain real controls (e.g. an IconButton with an `aria-label`); it is not announced on its own.

## Related

`line-chart`, `area-chart`, `bar-chart`, `chart-legend`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
