---
id: "packages/active-charts/interaction-and-states"
title: "Interaction and states"
description: "Tooltips, legend, selection under your control, the context menu and export - and what a chart shows when it cannot draw the data."
url: "/packages/active-charts/interaction-and-states"
locale: "en"
revision: "b3468e2f727534eb"
---

# Interaction and states

**Example: Selection, tooltip, legend and export**

Client: Runs entirely in your browser. Source: `examples/charts/interaction.tsx`.

```tsx
// apps/docs/examples/charts/interaction.tsx
'use client';

import {useRef, useState} from 'react';
import {MONTHLY_SALES, SALES_SERIES} from '@/lib/examples/chart-data';

import {
  BarChart,
  englishChartTranslator,
  type ChartHandle,
  type ChartItemRef,
} from '@workspace/active-charts';
import {Button} from '@workspace/active-ui/components/primitives/button';

/** A readable name for a selected bar: its series and its month. */
function describe(ref: ChartItemRef): string {
  if ('seriesId' in ref) return `${ref.seriesId} @ ${String(ref.key)}`;
  return JSON.stringify(ref);
}

/**
 * Selection under the host's control, the shared tooltip and legend, and
 * the export of the imperative handle. Click a bar, or move to it with the
 * arrow keys and press Space, to select it; Shift+F10 opens the chart's menu
 * with copy, the data table and export.
 */
export default function ChartInteraction() {
  const chart = useRef<ChartHandle>(null);
  const [selected, setSelected] = useState<ChartItemRef[]>([]);
  const [exported, setExported] = useState<string | null>(null);
  return (
    <div className="flex w-full flex-col gap-4">
      <BarChart
        ref={chart}
        data={MONTHLY_SALES}
        xKey="month"
        xAxis="category"
        series={SALES_SERIES}
        legend
        tooltip={{enabled: true, mode: 'shared'}}
        selection
        selectedItems={selected}
        onSelectionChange={(next) => setSelected(next)}
        height={300}
        locale="en-GB"
        t={englishChartTranslator}
      />
      <div className="flex flex-wrap items-center gap-2">
        <Button
          variant="outline"
          size="sm"
          onClick={async () => {
            const result = await chart.current?.exportAsCsv('sales.csv');
            setExported(
              result ? `${result.filename}: ${result.blob.size} bytes` : null,
            );
          }}
        >
          Export CSV
        </Button>
        <Button
          variant="outline"
          size="sm"
          onClick={async () => {
            const result = await chart.current?.exportAsPng('sales.png', 2);
            setExported(
              result ? `${result.filename}: ${result.blob.size} bytes` : null,
            );
          }}
        >
          Export PNG
        </Button>
        <Button
          variant="ghost"
          size="sm"
          disabled={selected.length === 0}
          onClick={() => setSelected([])}
        >
          Clear selection
        </Button>
      </div>
      <p className="text-sm" aria-live="polite" data-chart-selection>
        Selected:{' '}
        {selected.length > 0 ? selected.map(describe).join(', ') : 'nothing'}
      </p>
      {exported ? (
        <p className="text-muted-foreground text-sm">{exported}</p>
      ) : null}
    </div>
  );
}
```

## Selection

Turn selection on with `selection`. Controlled, you own `selectedItems` and update it from
`onSelectionChange`; uncontrolled, seed it with `defaultSelectedItems`. An item is a logical
identity (`{seriesId, key}` for an item of a series, a cell, a node or a link), never a position, so
one selection can be given to several charts. `onItemActivate` fires on a click or on Enter.

## Tooltip, legend and menu

- `tooltip={{mode: 'shared'}}` shows every series at the hovered x; `'single'` one item; `'split'`
  one box per series.
- `legend` toggles series; a hidden series leaves the scales.
- The context menu (right click, long press, Shift+F10) carries copy, the data table and export; add
  your own items with `contextMenu={{items}}`.

## Export

The imperative `ChartHandle` (`ref`) offers `exportAsPng`, `exportAsSvg`, `exportAsCsv` and
`exportAsPdf`. Each waits until the chart is still and resolves with the file (`blob`, `filename`)
when it was handed to the browser. See [Export](/packages/active-charts/export).

## States

**Example: Data a chart cannot draw**

Client: Runs entirely in your browser. Source: `examples/charts/states.tsx`.

```tsx
// apps/docs/examples/charts/states.tsx
'use client';

import {
  BarChart,
  ChartDefaults,
  englishChartTranslator,
  GaugeChart,
  SankeyChart,
} from '@workspace/active-charts';

/**
 * What a chart shows when it cannot draw the data truthfully: no rows, a
 * gauge whose range is empty, a flow that loops. Each is a translated
 * unavailable state, not an empty frame or a broken drawing.
 */
export default function ChartStates() {
  return (
    <ChartDefaults locale="en-GB" t={englishChartTranslator}>
      <div className="grid w-full gap-6 sm:grid-cols-3">
        <section aria-label="No rows" data-chart-state="empty">
          <p className="mb-2 text-sm font-medium">No rows</p>
          <BarChart
            data={[]}
            xKey="month"
            xAxis="category"
            yKey="sales"
            name="Sales"
            height={180}
          />
        </section>
        <section aria-label="Empty range" data-chart-state="range">
          <p className="mb-2 text-sm font-medium">Empty gauge range</p>
          <GaugeChart value={5} min={10} max={10} label="Score" height={180} />
        </section>
        <section aria-label="A loop" data-chart-state="cycle">
          <p className="mb-2 text-sm font-medium">A flow with a loop</p>
          <SankeyChart
            nodes={[
              {id: 'a', label: 'A'},
              {id: 'b', label: 'B'},
            ]}
            links={[
              {source: 'a', target: 'b', value: 3},
              {source: 'b', target: 'a', value: 1},
            ]}
            height={180}
          />
        </section>
      </div>
    </ChartDefaults>
  );
}
```

Input a chart cannot draw truthfully (no rows, an empty gauge range, a sankey cycle, an inverted
axis domain) shows a translated unavailable state, with a development diagnostic to your sink. For
unexpected errors, wrap charts in your own `<ChartErrorBoundary>`.
