> ## 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.

# Waterfall

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 waterfall shows how a running total gets from where it starts to where it ends. Each change is a bar floating from the total before it to the total after, coloured by whether it adds or takes away, totals stand on zero, and a connector carries the total across from each bar to the next.

<Warning>
  The waterfall ships as its own package, `@graphysdk/geom-waterfall`, on a
  `0.x` version line. While it is on `0.x`, a minor release may change its
  aesthetics, style parts 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-waterfall
```

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

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

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

## One bar per change

Pass one row per bar, in the order the total runs. Map the category to `x` and the change to `y`, and map `measure` to a column saying which rows are totals:

<GraphEmbed story="chart-types-waterfall--profit-bridge" args={{ orientation: 'vertical' }} />

```tsx theme={null}
const data = {
  columns: [{ key: 'item' }, { key: 'amount' }, { key: 'measure' }],
  rows: [
    { item: 'Revenue', amount: 1250, measure: 'absolute' },
    { item: 'Cost of sales', amount: -480 },
    { item: 'Gross profit', measure: 'total' },
    { item: 'Sales & marketing', amount: -210 },
    // …
    { item: 'Net income', measure: 'total' },
  ],
};

const spec = kit.pipe(
  kit.createSpec({ x: 'item', y: 'amount' }),
  kit.geom.waterfall({ aes: { measure: 'measure' } }),
  kit.scale.x.discrete(),
  kit.scale.y.continuous()
);

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

The value axis fits the running totals rather than the changes, and holds zero, since a bar is a length. The category axis is always discrete.

The total runs in data order, and each connector joins a bar to the next row's bar. Sort the rows, not the axis: a discrete `domain` that reorders the categories moves the bars but not the total, so the connectors then skip across bars.

## Totals

`measure` reads each row as one of three kinds, the names Plotly uses:

| Measure | The bar |
| - | - |
| `'relative'` | Moves the running total by `y`. An empty cell reads so. |
| `'total'` | Draws the running total so far, from zero. Its own `y` may be empty. |
| `'absolute'` | Draws `y` from zero, and sets the running total to it. |

Without `measure`, every row is a step. Any other value is an error. Use `'absolute'` for an opening balance, and `'total'` for each subtotal and the closing figure:

<GraphEmbed story="chart-types-waterfall--headcount" />

## Running totals already in the data

When the data already holds each bar's running total before and after, as a finance tool exports a bridge, set the layer's stat to `identity` and map `start` and `end` instead of `y`. `measure` still marks the totals, which colour apart from the steps:

<GraphEmbed story="chart-types-waterfall--precomputed-totals" />

```tsx theme={null}
kit.geom.waterfall({
  stat: kit.stat.identity(),
  aes: { start: 'opening', end: 'closing', measure: 'measure' },
});
```

Under `identity`, a missing `start` or `end` is an error. Under the default stat, mapping either is an error too, since the stat computes them from `y`.

## Horizontal

Add `kit.coord.flip()`. The categories move to the vertical axis and the bars run across.

<GraphEmbed story="chart-types-waterfall--profit-bridge" args={{ orientation: 'horizontal' }} />

## Bar width and connectors

`width` sets how much of its band each bar spans, as a fraction from just above `0` up to `1`. It defaults to `0.8`, which leaves a gap for the connector; at `1` neighbouring bars touch. A width above `1` is clamped to the full band, and one at or below `0` falls back to the default; the compiler warns in both cases.

`connector` sets how the running total is carried from each bar to the next: `'spanning'` (the default) runs the line across both bars, along their edges, and `'between'` runs it across the gap only. To hide connectors, style them: `kit.style.geom.waterfall.connector({ alpha: 0 })` hides every one, and a `where` hides only those leaving the bars it picks. An unknown value falls back to `'spanning'` with a warning.

```tsx theme={null}
kit.geom.waterfall({
  aes: { measure: 'measure' },
  params: { width: 0.6, connector: 'between' },
});
```

<GraphEmbed story="chart-types-waterfall--bars-and-connectors" args={{ width: 0.6, connector: 'between' }} />

## Styling

The bars are the observations, and each kind and the connectors have a builder of their own. A bar's colour says what the step did, so a chart-wide `style.geom()` does not recolour it:

| Builder | Properties | Default |
| - | - | - |
| `kit.style.geom.waterfall()` | `alpha`, `fillAlpha`, `strokeWidth` | Opaque, no border |
| `kit.style.geom.waterfall.increase()` | `fill`, `stroke` | The positive trend colour |
| `kit.style.geom.waterfall.decrease()` | `fill`, `stroke` | The negative trend colour |
| `kit.style.geom.waterfall.total()` | `fill`, `stroke` | The neutral trend colour |
| `kit.style.geom.waterfall.connector()` | `stroke`, `alpha`, `strokeWidth`, `dashArray` | A 1px line |

<GraphEmbed story="chart-types-waterfall--styled" />

```tsx theme={null}
const spec = kit.pipe(
  // …
  styles({
    defaults: [
      kit.style.geom.waterfall({ fillAlpha: 0.85 }),
      kit.style.geom.waterfall.increase({ fill: '#7fb685' }),
      kit.style.geom.waterfall.decrease({ fill: '#d98a7e' }),
      kit.style.geom.waterfall.total({ fill: '#3a3833' }),
      kit.style.geom.waterfall.connector({
        stroke: '#898373',
        dashArray: [3, 3],
      }),
    ],
  })
);
```

A connector is styled against the bar it leaves, so an entry's `where` can pick connectors out by that bar's values.

### Colour through a scale

To colour the kinds through the colour scale, with a legend, map `color` to `waterfallKind`. A mapped colour beats the kind builders' `defaults` entries; an `overrides` entry still beats it. The connectors keep their own colour.

<GraphEmbed story="chart-types-waterfall--colour-by-kind" />

```tsx theme={null}
const spec = kit.pipe(
  kit.createSpec({ x: 'item', y: 'amount' }),
  kit.geom.waterfall({ aes: { measure: 'measure', color: 'waterfallKind' } }),
  kit.scale.x.discrete(),
  kit.scale.y.continuous(),
  kit.scale.color.discrete({
    domain: ['increase', 'decrease', 'total'],
    range: ['#2a9d8f', '#e76f51', '#264653'],
  })
);
```

## The running-total variables

The geom writes four variables you can name in a `highlight` or an entry's `where`:

| Variable | Value |
| - | - |
| `waterfallStart` | The running total the bar starts from, in `y`'s units |
| `waterfallEnd` | The running total the bar ends on, in `y`'s units |
| `waterfallKind` | `'increase'`, `'decrease'` or `'total'` |
| `waterfallValue` | The figure the bar shows: its change, or the total it draws |

Under `stat: 'identity'` your own `start` and `end` columns hold the running totals, so the geom writes only `waterfallKind` and `waterfallValue`. `y` keeps the values you passed. A pinned number or a difference arrow reads `waterfallValue`. An absolute bar is a `'total'` kind; highlight on your `measure` column to pick it out.

## Hover and highlight

Hovering anywhere in a category's band emphasises its bar, and the tooltip lists its value and the running total it lands on.

A highlight raises each matched bar. The connectors stay with the dimmed layer:

<GraphEmbed story="chart-types-waterfall--highlight" args={{ target: 'subtotals' }} />

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

## Not yet supported

* **The editor's chart-type picker and the chart agent.** In editable mode the waterfall renders, hovers, selects and takes annotations on each bar's running total, but it is authored in code.
* **Dodged groups.** Each group runs its own total, but groups on the same category overlap rather than sitting side by side.
* **Data labels.** `dataLabels` prints nothing on a bar yet.


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