# ComposedChart

Bars and lines over one axis — the volume, and the rate it moved at.

- Group: Charts
- Import: `import { ComposedChart } from '@misoto22/folio/charts'`
- Page: https://ui.misoto22.com/components/composed-chart/
- Related: bar-chart, line-chart

## When to reach for it

Two measures that share a scale. Two that do NOT share one belong in two charts or indexed to a common base: there is no second y-axis here, on purpose.

## Anatomy

- **Figure frame** (required) — ChartFigure’s <figure>, named by title, with the composed marks inside one measured ChartContainer.
- **The single value axis** (required) — <ComposedChart.YAxis>. One of them is the entire design: there is no dual-axis affordance and no second scale to configure, so the two measures are read against the same numbers.
- **Bars** — <ComposedChart.Bar>, the volume. Custom-shaped like BarChart’s, with the same transparent hit rectangle and the same grow-in anchored to the chart’s start rather than to each bar’s mount, so a hover cannot replay it.
- **Lines** — <ComposedChart.Line>, the rate. It inherits the chart’s curveType and reveal, so bars and line arrive as one figure rather than as two animations on two clocks.
- **Column highlight** — enableHoverHighlight dims every mark outside the hovered column, driven by the chart’s own onMouseMove index rather than by each mark’s hover — which is what makes a bar and a line in the same column light together.
- **Brush and toolbar** — <ComposedChart.Brush> in the container footer and <ComposedChart.Toolbar> above the plot. They drive one window, so a brushed range and a zoomed range cannot disagree about what is on screen.
- **Hidden data table** — The sr-only table of the FULL data rather than of the brushed window, so a reader on the table is never shown less than the CSV export holds. hideDataTable removes it, and zero rows render nothing.

## Best practices

### Do

- Index two measures to a common base — both as a percentage of January, say — when they do not share a scale. That is the substitute for the second axis this component deliberately does not have.
- Compose <ComposedChart.Legend> above two marks. A bar and a line at --series-1 and --series-2 differ by one step of grey and a shape, and only the shape is self-describing.
- Set enableHoverHighlight on every mark or on none. Set on one, the hovered column dims half of itself, which reads as a rendering fault rather than as emphasis.

### Don’t

- Do not smuggle a second y-axis in through chartProps or a yAxisId on <ComposedChart.YAxis>. Recharts allows it; the component’s claim is that it does not. Two scales chosen independently let the author decide where the lines cross, which is the single most misleading thing a chart can do — and it always works, on any two series.
- Do not assume the audio reading is here. AreaChart, BarChart and LineChart each carry a Sonify slot and this one does not, so a reader who has been listening across a dashboard falls back to the sr-only table — which is why hideDataTable is the prop not to set on this chart.

## Accessibility

- title is required; the rows are also rendered as a visually hidden table.
- One value axis only. A dual-axis chart lets its author choose where the lines cross, which is the single most misleading thing a chart can do.
- enableHoverHighlight dims every bar outside the hovered column, driven by the chart’s own tooltip index.

## ComposedChart

Bars and lines over one axis — the shape for "the volume, and the rate it moved at". One axis, always. Two measures at different scales belong in two charts or indexed to a common base; a second y-scale lets the author choose where the lines cross, which is the single most misleading thing a chart can do.

### Props

- `config` (required) — `TConfig & ValidateKeys<TData, TConfig>`.
- `data` (required) — `TData[]`. The rows the chart draws. One entry per point, bar or category.
- `title` (required) — `string`. What the chart shows, in a sentence a reader could act on. Required, and announced to a screen reader even when it is not printed.
- `showTitle` — `boolean`. Prints the title above the plot instead of hiding it from sight.
- `description` — `ReactNode`. A line under the title — the unit, the window, the caveat.
- `children` (required) — `ReactNode`. The composed parts — axes, grid, tooltip, legend, and the marks themselves.
- `className` — `string`. Merged onto the figure, last, so a call site can size or space it.
- `chartProps` — `ComponentProps<typeof RechartsComposedChart>`. Escape hatch onto the raw Recharts chart element.
- `curveType` — `ChartCurveType` default `'linear'`. How the line between two points is interpolated. Every mark inherits it unless it says otherwise.
- `animationType` — `ChartRevealType` default `'forward'`.
- `barGap` — `number`.
- `barCategoryGap` — `number`.
- `defaultSelectedDataKey` — `string | null` default `null`. The series lit on first render, when the chart keeps its own selection.
- `selectedDataKey` — `string | null`. The selected series, driven from outside. Give this and the chart follows it; leave it undefined and the chart keeps its own, starting from `defaultSelectedDataKey`.
- `onSelectionChange` — `(selectedDataKey: string | null) => void`. Fires when the selection changes, and with null when it is cleared.
- `isLoading` — `boolean` default `false`. Swaps the marks for an animated skeleton, keeping the measured height so the page does not jump when the data lands.
- `loadingBars` — `number`. How many bars the skeleton draws.
- `xDataKey` — `keyof TData & string`. The row field on the category axis. Needed by the brush and by the table view.
- `hideDataTable` — `boolean` default `false`. Drops the hidden table view. Only correct when the page prints the data itself.
- `empty` — `ChartEmptyProps | false`. What the chart shows when it has nothing to draw. Rendered instead of the plot whenever `data` is empty and the chart is not loading — the state a real dashboard reaches within a week, and the one an empty pair of axes is indistinguishable from a failed load. `false` keeps the axes, for a chart whose emptiness is itself the reading.

## Example — default

```tsx
import { ComposedChart, type ChartConfig } from '@misoto22/folio/charts'

<ComposedChart title="Revenue and profit" config={config} data={data} xDataKey="month">
  <ComposedChart.Grid />
  <ComposedChart.XAxis dataKey="month" />
  <ComposedChart.YAxis />
  <ComposedChart.Legend />
  <ComposedChart.Tooltip />
  <ComposedChart.Bar dataKey="revenue" />
  <ComposedChart.Line dataKey="profit" />
</ComposedChart>
```

## Example — variants

```tsx
import { ToggleGroup, ToggleGroupItem } from '@misoto22/folio'

<div className="flex w-full flex-col gap-4">
  <div className="flex flex-wrap gap-3">
    <ToggleGroup
      type="single"
      value={bar}
      onValueChange={(next) => next && setBar(next as BarVariant)}
      aria-label="Bar fill"
    >
      {BARS.map((option) => (
        <ToggleGroupItem key={option} value={option}>
          {option}
        </ToggleGroupItem>
      ))}
    </ToggleGroup>

    <ToggleGroup
      type="single"
      value={stroke}
      onValueChange={(next) => next && setStroke(next as AreaStrokeVariant)}
      aria-label="Line stroke"
    >
      {STROKES.map((option) => (
        <ToggleGroupItem key={option} value={option}>
          {option}
        </ToggleGroupItem>
      ))}
    </ToggleGroup>
  </div>

  <ComposedChart title="Revenue and profit" config={config} data={data}>
    <ComposedChart.Grid />
    <ComposedChart.XAxis dataKey="month" />
    <ComposedChart.Tooltip />
    <ComposedChart.Bar dataKey="revenue" variant={bar} />
    <ComposedChart.Line dataKey="profit" strokeVariant={stroke}>
      <ComposedChart.Dot variant="border" />
    </ComposedChart.Line>
  </ComposedChart>
</div>
```

## Example — hover highlight

```tsx
import { ComposedChart, type ChartConfig } from '@misoto22/folio/charts'

<ComposedChart title="Revenue and profit" config={config} data={data}>
  <ComposedChart.Background variant="diagonal-lines" />
  <ComposedChart.XAxis dataKey="month" />
  <ComposedChart.Legend isClickable />
  <ComposedChart.Tooltip />
  <ComposedChart.Bar dataKey="revenue" enableHoverHighlight isClickable />
  <ComposedChart.Line dataKey="profit" glowing isClickable />
</ComposedChart>
```

## Example — brush and loading

```tsx
import { ComposedChart, type ChartConfig } from '@misoto22/folio/charts'

<div className="grid w-full gap-8 lg:grid-cols-2">
  <ComposedChart title="Revenue and profit" showTitle config={config} data={data} xDataKey="day">
    <ComposedChart.Grid />
    <ComposedChart.XAxis dataKey="day" />
    <ComposedChart.Tooltip />
    <ComposedChart.Bar dataKey="revenue" variant="stripped" />
    <ComposedChart.Line dataKey="profit" />
    <ComposedChart.Brush height={48} />
  </ComposedChart>

  <ComposedChart title="Loading" showTitle config={config} data={[]} isLoading>
    <ComposedChart.Grid />
    <ComposedChart.Bar dataKey="revenue" />
  </ComposedChart>
</div>
```
