# SankeyChart

Where a quantity goes as it moves through stages.

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

## When to reach for it

A funnel, a budget, an energy or traffic breakdown. The only chart here whose data is a graph rather than a table.

## Anatomy

- **Figure frame** (required) — ChartFigure’s <figure>, named by title, with the diagram inside one ChartContainer.
- **Node rectangles** (required) — The root’s own node renderer. A node whose name is in config is painted from its gradient; one that is not falls back to currentColor, so it is drawn plainly rather than lost.
- **Node labels** — <SankeyChart.NodeLabel>, composed inside <SankeyChart.Node>, and entirely opt-in. Leave it out and NOTHING on the diagram is named — there is no legend here, so the names exist only in the tooltip and in the hidden table.
- **Flow bands** — <SankeyChart.Link>. gradient fades the source’s colour into the target’s and is the variant that actually reads as flow; solid gives up colour entirely and lets the node rectangles carry identity.
- **Hidden data table** (required) — The sr-only table lists the LINKS — from, to, value — rather than the nodes, because a table of node totals loses every from-and-to the diagram exists to state.

## Best practices

### Do

- Treat the nodes array as an addressing table. A link’s source and target are INDEXES into it, so inserting a node at the front silently re-points every link at a different pair — and the layout still renders, which is why this belongs in a test rather than in an eyeball.
- Compose <SankeyChart.Node> with a <SankeyChart.NodeLabel> inside it. Names are opt-in and there is no legend to fall back on, so a sankey without labels is a set of anonymous grey bands.
- Keep the flows conserved, or give the shortfall a node of its own with a name. Band width is the only arithmetic on screen, and a node that quietly loses eight percent simply reads as a smaller node.

### Don’t

- Do not reach for it when the quantity only narrows along one path. That is a FunnelChart; a sankey spends its whole layout budget on splits that are not there.
- Do not put twenty nodes in one column. nodePadding is 10px and the layout distributes what is left, so past a dozen a node rectangle is a few pixels tall and a label centred on it has nowhere to sit.

## Accessibility

- title is required. The hidden table lists the FLOWS rather than the nodes — a table of node totals would lose every “from → to” the diagram exists to show.
- Four link variants: gradient reads as flow, source and target attribute a band to one end, solid gives up colour and lets the nodes carry identity.

## SankeyChart

Where a quantity goes as it moves through stages — the shape for a funnel, a budget, an energy or traffic breakdown. The only chart here whose data is a GRAPH rather than a table, so it takes `{ nodes, links }` instead of rows, and the table view lists the flows rather than the nodes.

### Props

- `data` (required) — `SankeyData`. The nodes and the links between them, in Recharts' own shape.
- `config` (required) — `ChartConfig`. Node names → their label and paint.
- `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.
- `sankeyProps` — `Omit<SankeyProps, 'data'>`. Escape hatch onto the raw Recharts Sankey element.
- `nodeWidth` — `number` default `10`. How wide each node rectangle is, in pixels.
- `nodePadding` — `number` default `10`. Vertical gap between nodes in the same column, in pixels.
- `linkCurvature` — `number` default `0.5`. 0 draws straight links, 1 the fullest curve.
- `iterations` — `number` default `32`. Layout passes. More is tidier and slower.
- `sort` — `boolean` default `true`. Lets the layout reorder nodes for the fewest crossings.
- `align` — `'left' | 'justify'` default `'justify'`. How nodes are placed along the flow axis.
- `verticalAlign` — `'justify' | 'top'` default `'justify'`. How nodes are distributed within a column.
- `defaultSelectedNode` — `string | null` default `null`. The node lit on first render, when the chart keeps its own selection. Selecting one dims every flow it does not touch.
- `selectedNode` — `string | null`. The selected node, driven from outside. Give this and the chart follows it; leave it undefined and the chart keeps its own, starting from `defaultSelectedNode`.
- `onSelectionChange` — `(selection: { name: string; value: number } | 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.
- `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. `false` keeps the empty plot, for a chart whose emptiness is itself the reading.

## Example — default

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

<SankeyChart title="Visits by source and outcome" config={config} data={data} nodeWidth={12}>
  <SankeyChart.Node radius={3}>
    <SankeyChart.NodeLabel position="outside" showValues />
  </SankeyChart.Node>
  <SankeyChart.Link variant="gradient" />
  <SankeyChart.Tooltip />
</SankeyChart>
```

## Example — links and labels

```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={link}
      onValueChange={(next) => next && setLink(next as SankeyLinkVariant)}
      aria-label="Link"
    >
      {LINKS.map((option) => (
        <ToggleGroupItem key={option} value={option}>
          {option}
        </ToggleGroupItem>
      ))}
    </ToggleGroup>

    <ToggleGroup
      type="single"
      value={position}
      onValueChange={(next) => next && setPosition(next as SankeyLabelPosition)}
      aria-label="Labels"
    >
      {LABELS.map((option) => (
        <ToggleGroupItem key={option} value={option}>
          {option}
        </ToggleGroupItem>
      ))}
    </ToggleGroup>
  </div>

  <SankeyChart
    title="Visits by source and outcome"
    config={config}
    data={data}
    nodeWidth={position === 'inside' ? 76 : 12}
  >
    <SankeyChart.Node radius={3} isClickable>
      <SankeyChart.NodeLabel position={position} showValues />
    </SankeyChart.Node>
    <SankeyChart.Link variant={link} verticalPadding={2} />
    <SankeyChart.Tooltip />
  </SankeyChart>
</div>
```

## Example — loading

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

<SankeyChart title="Visits by source and outcome" config={config} data={data} isLoading>
  <SankeyChart.Node />
  <SankeyChart.Link />
</SankeyChart>
```
