Source: https://monarchui.dev/components/chart-frame/

Components · Charts

# ChartFrame

The container every chart sits in: title, controls, legend, loading / empty / error states and an accessible data table.

@rookcom/monarch-react/charts

Component

## Header controls, legend and table toggle

## Usage examples

### Loading, empty and error

## Purpose

Gives every chart the same frame and the same accessibility: a labelled figure, a header with period or metric controls, a legend, consistent loading, empty and error states, and the data as a real table, visible on demand and always present for screen readers.

### Use when

- Wrapping a custom visualisation that Monarch does not ship, so it still gets the header, states and table fallback.
- Building a new chart component: LineChart and BarChart render ChartFrame for you, and the other ./charts components build on it.

### Avoid when

- A single headline number with no plot: use StatCard.
- A plain list of labelled values: use DescriptionList or BarList.

## Design principles

- The data is never only a picture: `table` is always rendered for assistive technology and one click away for everyone else.
- States keep the reserved `height`, so loading, empty, error and ready swap without the page jumping.
- Controls belong in the header (`actions`), above the plot, in one row that wraps on narrow screens.
- A legend only for two or more series; a single series is named by the title.
- Series colours are palette slots (`chartColor(1…5)` → `--mn-color-data-*`), never status colours and never raw values.

## API reference

```tsx
import { ChartFrame, chartColor } from '@rookcom/monarch-react/charts';
```

| Export | Kind | Rendering | Root class |
| --- | --- | --- | --- |
| `ChartFrame` | component | client | `.mn-chart-frame` |
| `chartColor` | utility | server-safe | – |

`ChartFrame` renders a `<figure>` (ref to it) labelled by `title` and described by `description`; other HTML attributes pass through.

`chartColor(slot)` returns the CSS colour for a palette slot (`1`–`5` → `var(--mn-color-data-N)`, `neutral` → `var(--mn-color-text-subtle)`), for custom marks drawn inside a frame.

Types shared by every chart are exported from here: `ChartSeries`, `ChartDatum`, `ChartColor`, `ChartLegendItem`, `ChartTableData`, `ChartTableColumn`.

Generated from the TypeScript declarations of `packages/react/src/components/charts/chart-frame`.

## Properties

### `ChartFrame`

The container every Monarch chart sits in: a `<figure>` with a title, description, controls slot, legend, loading / empty / error states and an accessible data table ("View as table", always present for screen readers).

| Prop | Type | Description |
| --- | --- | --- |
| `title` required | `ReactNode` | The chart's name. Labels the figure. Required. |
| `actions` | `ReactNode` | Period or metric controls in the header, such as a pill TabsFilter or a Select. |
| `children` | `ReactNode` | The chart. |
| `className` | `string` |  |
| `defaultShowTable` | `boolean` | Uncontrolled initial table view. Default `false`. |
| `description` | `ReactNode` | One line under the title: the period, unit or what the chart answers. Describes the figure. |
| `emptyActions` | `ReactNode` | Actions in the empty state, such as a link to set up tracking. |
| `emptyDescription` | `ReactNode` | Empty-state description, such as what will appear and when. |
| `footer` | `ReactNode` | Notes under the chart: source, last updated, a footnote. |
| `headingLevel` | `ChartFrameHeadingLevel` | Render the title as a heading (`h2`–`h6`) to match the page outline. Default: not a heading. |
| `height` | `number` | Height reserved for the chart body (loading, empty and error keep it, so nothing jumps). Default 240. |
| `id` | `string` |  |
| `labels` | `ChartFrameLabels` |  |
| `legend` | `readonly ChartLegendItem[]` | Legend entries, shown under the header while the chart is visible. |
| `legendMarker` | `ChartLegendMarker` | Legend marker shape. Default `line`. |
| `onLegendToggle` | `((key: string) => void)` | Makes legend entries toggle buttons; called with the series key. |
| `onRetry` | `(() => unknown)` | Error-state retry. Return the refetch promise to keep the button busy until it settles. |
| `onShowTableChange` | `((showTable: boolean) => void)` |  |
| `showTable` | `boolean` | Controlled table view. |
| `status` | `ChartFrameStatus` | `ready` (default) shows the chart; `loading` a skeleton with `aria-busy`; `empty` an empty state; `error` an error with Try again when `onRetry` is set. Exposed as `data-status`. |
| `table` | `ChartTableData` | The accessible data table: every value the chart shows, as text. It is always in the page for screen readers, and visible through the "View as table" toggle when `tableToggle` is on. |
| `tableToggle` | `boolean` | Show the "View as table" toggle. Default `true` when `table` is set. |

## Variants

| Prop | Values | Description |
| --- | --- | --- |
| `status` | `'ready'``'loading'``'empty'``'error'` | Which body shows: the chart, a skeleton, an EmptyState or an error (InlineErrorRetry with `onRetry`, otherwise an alert). Exposed as `data-status`. |
| `headingLevel` | `'2'``'3'``'4'``'5'``'6'` | Renders the title as that heading; without it the title is a plain block. |
| `legendMarker` | `'line'``'square'` | Short line (line charts) or square (bars) beside each legend label. |
| `tableToggle` | `'true'``'false'` | Shows the "View as table" button (default when `table` is set). Off, the table is still present for screen readers. |

## States

| State | Exposed as | Behaviour |
| --- | --- | --- |
| Ready | `data-status="ready", data-view="chart"` | The chart, plus the visually hidden table. |
| Table view | `data-view="table"` | The table replaces the chart in a focusable, scrollable region; the legend hides. |
| Loading | `data-status="loading", aria-busy="true"` | A skeleton of the reserved height and a polite "Loading chart" status. |
| Empty | `data-status="empty"` | A compact EmptyState with `emptyDescription` and `emptyActions`. |
| Error | `data-status="error"` | InlineErrorRetry with Try again when `onRetry` is set; otherwise a message with `role="alert"`. |
| Hidden series | `aria-pressed="false", data-hidden` | A toggled-off legend entry: struck-through label and faded swatch. |

## Interaction behaviour

- "View as table" swaps the chart for the table and changes to "View as chart"; controlled with `showTable` + `onShowTableChange`, or uncontrolled with `defaultShowTable`.
- With `onLegendToggle`, each legend entry is a toggle button; the chart decides what hiding means.
- Try again calls `onRetry`; return the refetch promise and the button stays busy until it settles.

## Keyboard support

| Keys | Action |
| --- | --- |
| Tab | Moves through the header controls, the table toggle, legend toggles and the chart. |
| Enter / Space | Activates the table toggle or a legend toggle. |
| Arrow keys (table view) | Scroll the focused table region when it overflows. |

## Accessibility

Role

figure

Accessible name

title

(aria-labelledby);

description

is the accessible description.

- The data table has a caption (the title unless `table.caption` is set), column headers with `scope="col"` and row headers with `scope="row"`.
- While the chart shows, the table sits in a visually hidden container, so screen readers always reach the values.
- The visible table is a focusable region (`tabindex="0"`), so keyboard users can scroll it.
- Legend toggles are buttons with `aria-pressed`; the legend list is named "Legend" (`labels.legend`).
- Loading sets `aria-busy` and announces "Loading chart"; a retry-less error is `role="alert"`.
- Identity is never colour alone: every legend entry has its label, dashed series show a dashed marker.
- All text is translatable through `labels`.

## Responsive behaviour

- The header wraps: actions move under the title when there is no room.
- Padding tightens below 30rem; the frame works at 320px.
- Legend toggles grow to a 44px touch target on coarse pointers.

## Theme support

- Surface, border and text use Monarch tokens and follow light, dark and high-contrast themes.
- Series colours are `--mn-color-data-1` … `-5`, chosen and contrast-checked per theme in @rookcom/monarch-tokens (3:1 on every chart background, 4.5:1 in high contrast).
- Focus rings use `--mn-color-focus-ring`.

## Do and don't

### Do

- Pass `table` with every value the chart shows, formatted as the chart formats it.
- Reserve a `height` close to the real chart so states do not shift the layout.
- Put period and metric switches in `actions`.

### Don't

- Hide the only text alternative: never set `tableToggle={false}` and leave out `table`.
- Colour series with status tokens or hex values; use `chartColor` slots.
- Put a legend on a single-series chart.

## Related components

- [LineChart](https://monarchui.dev/components/line-chart/): Trends over time; renders ChartFrame for you.
- [BarChart](https://monarchui.dev/components/bar-chart/): Comparisons across categories; renders ChartFrame for you.
- [Sparkline](https://monarchui.dev/components/sparkline/): A word-sized trend with no frame, for tables and cards.
- [StatCard](https://monarchui.dev/components/stat-card/): One headline number, optionally with a sparkline.
- [Card](https://monarchui.dev/components/card/): A general container when there is no chart.

## Dependencies

| Kind | Depends on |
| --- | --- |
| npm packages | `react` |
| Monarch components | [Button](https://monarchui.dev/components/button/), [EmptyState](https://monarchui.dev/components/empty-state/), [InlineErrorRetry](https://monarchui.dev/components/inline-error-retry/), [Skeleton](https://monarchui.dev/components/skeleton/) |

Shared chart internals (legend, table, palette) come from `src/internal/charts`; their styles are `styles/chart.css` (`.mn-chart__*`).

## Testing coverage

- Figure named by the title and described by the description; no axe violations.
- The data table is always present (visually hidden) with row and column headers.
- The toggle swaps chart and table, uncontrolled and controlled; the visible table is a focusable region.
- `tableToggle={false}` removes the button but keeps the hidden table.
- Heading level and actions slot render.
- Loading (aria-busy, status text), empty (description, actions) and error (Try again calls onRetry; alert without it; translated labels).
- Legend toggles expose aria-pressed and report the key; a static legend has no buttons.
- `chartColor` maps slots to data tokens only.
- Renders on the server.

| Check | Covered |
| --- | --- |
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | Visual baselines: /__gallery/chart-frame; Cross-browser axe: /components/chart-frame |

Tests: `packages/react/src/components/charts/chart-frame`

## All example code

### Header controls, legend and table toggle

```tsx
import { useState } from 'react';
import { TabsFilter } from '@rookcom/monarch-react';
import { ChartFrame, Sparkline, type ChartTableData } from '@rookcom/monarch-react/charts';

const WEEK = [
  { day: 'Mon', requests: 48 },
  { day: 'Tue', requests: 61 },
  { day: 'Wed', requests: 55 },
  { day: 'Thu', requests: 72 },
  { day: 'Fri', requests: 66 },
];

const TABLE: ChartTableData = {
  columns: [
    { key: 'day', label: 'Day' },
    { key: 'requests', label: 'Requests' },
  ],
  rows: WEEK.map((d) => ({ day: d.day, requests: d.requests })),
};

const RANGES = [
  { value: '7d', label: '7 days' },
  { value: '30d', label: '30 days' },
];

function FrameDemo() {
  const [range, setRange] = useState('7d');
  return (
    <ChartFrame
      title="Requests"
      description="New requests per day"
      headingLevel={3}
      height={96}
      actions={
        <TabsFilter
          aria-label="Range"
          variant="pill"
          options={RANGES}
          value={range}
          onValueChange={setRange}
        />
      }
      legend={[{ key: 'requests', label: 'Requests', color: 1 }]}
      table={TABLE}
      footer="Counts update every hour."
    >
      <Sparkline
        values={WEEK.map((d) => d.requests)}
        variant="area"
        color={1}
        height={96}
        responsive
      />
    </ChartFrame>
  );
}

<FrameDemo />
```

### Loading, empty and error

```tsx
import { ChartFrame } from '@rookcom/monarch-react/charts';

<ChartFrame title="Loading" status="loading" height={80} />
<ChartFrame
  title="Empty"
  status="empty"
  height={80}
  emptyDescription="Requests appear here after the first one arrives."
/>
<ChartFrame title="Error" status="error" height={80} onRetry={() => undefined} />
```
