> ## Documentation Index
> Fetch the complete documentation index at: https://docs.graphy.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Sankey

export const GraphEmbed = ({src, story, aspectRatio, borderRadius = '14px', args}) => {
  const STORYBOOK_BASE_URL = typeof window !== 'undefined' && window.location.hostname === 'localhost' ? 'http://localhost:6006' : 'https://storybook-sdk.vercel.app';
  function buildArgsString(args) {
    if (!args) return '';
    const encodeValue = value => {
      if (value === null) return '!null';
      if (value === undefined) return '!undefined';
      if (typeof value === 'boolean') return `!${value}`;
      if (typeof value === 'number') return String(value);
      return encodeURIComponent(String(value));
    };
    const properties = Object.entries(args).map(([key, value]) => `${key}:${encodeValue(value)}`).join(';');
    return `&args=${properties}`;
  }
  const resolvedSrc = story ? `${STORYBOOK_BASE_URL}/iframe.html?id=${story}&viewMode=story&embed=1&globals=mode:readonly${buildArgsString(args)}` : src;
  const resolvedAspectRatio = aspectRatio ?? (story ? '16 / 9' : '10 / 6');
  return <iframe src={resolvedSrc} loading="lazy" allowfullscreen="true" style={{
    width: '100%',
    aspectRatio: resolvedAspectRatio,
    border: 'none',
    borderRadius,
    colorScheme: 'light'
  }} />;
};

A sankey diagram draws flows between nodes, each flow as wide as its amount. What runs through a system, and where it ends up, reads at a glance: energy from its sources to its uses, a budget from income to spending, visitors from a landing page to a sale.

<Warning>
  The sankey ships as its own package, `@graphysdk/geom-sankey`, on a `0.x`
  version line. While it is on `0.x`, a minor release may change its aesthetics,
  params, style or look; pin a caret range (`^0.1.0`) to take only patches. Tell
  us what works and what doesn't on [Discord](https://discord.gg/yGmYCjkSr).
</Warning>

## Install

```bash theme={null}
npm install @graphysdk/geom-sankey
```

It peer-depends on `@graphysdk/react` and nothing else. Register the plugin once with `createGraphyKit`, which adds `kit.geom.sankey()` and binds `kit.GraphProvider` to the same plugin:

```tsx theme={null}
import {
  createGraphyKit,
  GraphRenderer,
  highlight,
  styles,
} from '@graphysdk/react';
import { sankey } from '@graphysdk/geom-sankey';

const kit = createGraphyKit({ plugins: [sankey] });
```

## A sankey diagram

Pass one row per flow. Map the node it leaves to `source`, the node it reaches to `target` and its amount to `value`:

<GraphEmbed story="chart-types-sankey--energy" args={{ align: 'justify' }} />

```tsx theme={null}
const data = {
  columns: [
    { key: 'from', label: 'From' },
    { key: 'to', label: 'To' },
    { key: 'twh', label: 'Energy (TWh)' },
  ],
  rows: [
    { from: 'Coal', to: 'Electricity', twh: 110 },
    { from: 'Gas', to: 'Electricity', twh: 90 },
    { from: 'Gas', to: 'Heat', twh: 120 },
    { from: 'Electricity', to: 'Homes', twh: 120 },
    // …
  ],
};

const spec = kit.pipe(
  kit.createSpec({
    source: 'from',
    target: 'to',
    value: 'twh',
  }),
  kit.geom.sankey()
);

export function Energy() {
  return (
    <kit.GraphProvider spec={spec} data={data}>
      <GraphRenderer />
    </kit.GraphProvider>
  );
}
```

The nodes are the names the flows share, each as tall as the larger of what flows into it and what flows out. The flows run from left to right, a node in the column after every node that flows into it, and each flow is as wide at both ends as its amount. The geom lays itself out across the panel, so it takes no scales and draws no axes.

A missing, zero or negative amount draws no flow and adds nothing to either node. A flow from a node to itself, or one that would close a loop with the rows before it, is left out: a sankey's flows run one way.

## Color

Each node takes its own color from the palette, and each flow the color of the node it leaves, as above. Each node already carries its name, so the chart draws no legend for it unless you set `config.legend.position`.

Color always groups by node. Map it to your source or target column to choose which node a flow takes its color from:

| `color` | A flow is colored as… |
| - | - |
| unmapped, or the source | The node it leaves |
| the target (`color: 'to'`) | The node it reaches |

A node keeps its own color either way. Any other column is refused with `UNSUPPORTED_MAPPING`: a node has no value in it, so every node would fall into one group. Set `color: { value: '#…' }` to paint every node and flow one color.

## Alignment

`params.align` chooses the column a node sits in:

| `align` | A node sits… |
| - | - |
| `'justify'` (default) | As far left as its inflows allow, except a node no flow leaves: the last column |
| `'left'` | As far left as its inflows allow |
| `'right'` | As far right as its outflows allow |
| `'center'` | As far left as its inflows allow, except a node no flow reaches: as far right as its outflows allow |

```tsx theme={null}
kit.geom.sankey({ params: { align: 'left' } });
```

## Styling

A sankey paints three things, each with its own builder:

| Builder | Paints | Properties | Default |
| - | - | - | - |
| `kit.style.geom.sankey()` | The flows | `fill`, `fillAlpha`, `alpha`, `stroke`, `strokeAlpha`, and the rest of the shared geom vocabulary | The `color` scale's color at `fillAlpha` 0.4, and 0.65 while hovered |
| `kit.style.geom.sankey.node()` | The nodes | `fill`, `fillAlpha`, `alpha`, `stroke`, `strokeWidth` | The `color` scale's color, with no border |
| `kit.style.geom.sankey.label()` | The node names | `fontFamily`, `fontSize`, `fontWeight`, `textColor`, `textOutlineColor`, `textOutlineWidth` | 12px, weight 500, in the primary text color, outlined in the background's color |

The outline keeps a name legible where it crosses a flow. The flows and the nodes read the `color` scale, and the scale beats an entry in `defaults`, so an entry that replaces their color is an override. Every entry takes a `where`, which tests the observation it paints:

<GraphEmbed story="chart-types-sankey--styled" args={{ align: 'justify', flowAlpha: 0.5 }} />

```tsx theme={null}
const spec = kit.pipe(
  // …
  kit.scale.color.discrete({
    range: ['#0b3c49', '#115e67', '#16808a', '#2aa1a6', '#5cbfbf', '#8fd6d2'],
  }),
  styles({
    defaults: [
      kit.style.geom.sankey({ fillAlpha: 0.5 }),
      kit.style.geom.sankey.node({ stroke: '#ffffff', strokeWidth: 1 }),
      kit.style.geom.sankey.label({ fontWeight: 700, fontSize: 13 }),
    ],
    overrides: [kit.style.geom.sankey.node({ fill: '#1f2933' })],
  })
);
```

A flow's `fill` takes a pattern (`diagonal`, `crosshatch` or `dots`) or a gradient as well as a color, and so does a node's.

## Hover and highlight

Hovering a node raises it with every flow into and out of it; hovering a flow raises it with the two nodes it joins. Everything else dims. A node's tooltip is headed by its name and gives its total; a flow's is headed by the node it leaves, and names where it goes:

```
Gas                        Electricity
To      Electricity        Energy (TWh)   360
Energy (TWh)   90
```

A highlight raises each matched node and flow. A node holds its own name in your source column, so matching a source raises the node with the flows leaving it:

<GraphEmbed story="chart-types-sankey--highlight" args={{ align: 'justify', target: 'gas' }} />

```tsx theme={null}
const spec = kit.pipe(
  // …
  highlight({ variable: 'from', eq: 'Gas' })
);
```

A node's total is written into your amount column, so `highlight({ variable: 'twh', gt: 100 })` matches the nodes and the flows over 100.

## Not yet supported

* **The editor's chart-type picker and the chart agent.** In editable mode the sankey renders and takes annotations on a node's center or a flow's middle, but it is authored in code.
* **Color by a column other than the source or target.** Color groups by node.
* **A flow colored from its source to its target.** A flow takes one color; a gradient between its two nodes' colors is not drawn.
* **Node width and spacing.** The nodes and the gaps between them are sized to the panel, and are not yet options.
* **Loops.** A flow that would close a loop is left out rather than drawn back around.
* **Names that would overlap.** A name is drawn however short its node is, so the names of two short nodes in one column can meet.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.