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

# Funnel

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 funnel shows how many of a starting count reach each stage of a process. Each stage is a bar centred on a common line, as long as its count, and a band joins each bar to the next so the narrowing reads as one shape.

<Warning>
  The funnel ships as its own package, `@graphysdk/geom-funnel`, 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-funnel
```

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

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

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

## One bar per stage

Pass one row per stage, in the order the funnel runs. Map the stage to `x` and its count to `y`:

<GraphEmbed story="chart-types-funnel--checkout" args={{ orientation: 'horizontal' }} />

```tsx theme={null}
const data = {
  columns: [{ key: 'stage' }, { key: 'visitors' }],
  rows: [
    { stage: 'Visited', visitors: 48200 },
    { stage: 'Viewed product', visitors: 31400 },
    { stage: 'Added to cart', visitors: 12900 },
    { stage: 'Started checkout', visitors: 7600 },
    { stage: 'Paid', visitors: 5100 },
  ],
};

const spec = kit.pipe(
  kit.createSpec({ x: 'stage', y: 'visitors' }),
  kit.geom.funnel({ dataLabels: { showDataLabels: true } }),
  kit.scale.x.discrete(),
  kit.scale.y.continuous(),
  kit.coord.flip(),
  kit.config({ axes: { y: { isVisible: false } } })
);

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

`coord.flip()` gives the classic funnel, the first stage at the top. Each bar runs from minus half its count to plus half, so the value axis is centred on zero and its labels measure from the centre line, not a figure a funnel is read for. Hide it with `config`, as above, and print the counts as data labels. The geom hides the grid lines itself.

A stage with no count, or a negative one, draws no bar, and the stages either side of it are joined across the gap.

## Upright

Leave out `kit.coord.flip()`. The stages run left to right and the bars stand across the centre line.

<GraphEmbed story="chart-types-funnel--checkout" args={{ orientation: 'vertical' }} />

## Conversion

The geom's default `funnel` stat writes each stage's shares, as fractions in a percentage format, to two variables you can name like any column:

| Variable | Value |
| - | - |
| `funnelPercentOfFirst` | The stage's count over the first stage's |
| `funnelPercentOfPrevious` | The stage's count over the stage before it; 100% for the first |

Map `label` to one to print it in the bars:

<GraphEmbed story="chart-types-funnel--conversion" args={{ orientation: 'horizontal', label: '% of first' }} />

```tsx theme={null}
kit.geom.funnel({
  aes: { label: 'funnelPercentOfFirst' },
  dataLabels: { showDataLabels: true },
});
```

A label sits in the middle of its bar, and moves past the bar's end when the bar is too short to hold it. An explicit `dataLabels.position` is honoured as written.

The shares run in data order. If your data already carries conversion rates, turn the stat off and map them:

<GraphEmbed story="chart-types-funnel--precomputed-shares" args={{ orientation: 'horizontal' }} />

```tsx theme={null}
kit.geom.funnel({
  stat: kit.stat.identity(),
  aes: { percentOfFirst: 'ofFirst', percentOfPrevious: 'ofPrevious' },
});
```

## Bar width

`params.width` is the fraction of its band a bar spans, from just above `0` to `1`, `0.7` by default. The rest of the band is the gap the connector crosses.

<GraphEmbed story="chart-types-funnel--bar-width" args={{ orientation: 'horizontal', width: 0.5 }} />

```tsx theme={null}
kit.geom.funnel({ params: { width: 0.5 } });
```

## Colour per stage

Map `color` to the stage for a colour each. Each band takes the colour of the stage it leaves:

<GraphEmbed story="chart-types-funnel--coloured-stages" args={{ orientation: 'horizontal' }} />

```tsx theme={null}
const spec = kit.pipe(
  kit.createSpec({ x: 'stage', y: 'deals', color: 'stage' }),
  kit.geom.funnel({ dataLabels: { showDataLabels: true } }),
  // …
  kit.config({ legend: { position: 'none' } })
);
```

## Styling

The bars are the observations, and the bands between them have a builder of their own:

| Builder | Properties | Default |
| - | - | - |
| `kit.style.geom.funnel()` | The shared geom properties, and `strokeWidth` | The geom colour, no border |
| `kit.style.geom.funnel.connector()` | `fill`, `fillAlpha`, `stroke`, `strokeWidth` | The geom colour at 25% |

A chart-wide `style.geom()` reaches the bars too.

<GraphEmbed story="chart-types-funnel--styled" args={{ orientation: 'horizontal' }} />

```tsx theme={null}
const spec = kit.pipe(
  // …
  styles({
    defaults: [
      kit.style.geom.funnel({ fill: '#3a3833' }),
      kit.style.geom.funnel.connector({
        fill: '#d8d3c4',
        fillAlpha: 1,
        stroke: '#898373',
        strokeWidth: 1,
      }),
    ],
  })
);
```

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

## Hover and highlight

Hovering anywhere in a stage's band emphasises its bar, and the tooltip lists its count and both shares.

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

<GraphEmbed story="chart-types-funnel--highlight" args={{ orientation: 'horizontal', target: 'steep drops' }} />

```tsx theme={null}
const spec = kit.pipe(
  // …
  highlight({ variable: 'funnelPercentOfPrevious', lt: 0.5 })
);
```

## Not yet supported

* **The editor's chart-type picker and the chart agent.** In editable mode the funnel renders, hovers, selects and takes annotations on the end of each bar, but it is authored in code.
* **Several funnels side by side.** Map `group` to split the data into funnels, each with its own shares, but they overlap on the same bands. A stage may appear once per funnel.
* **Hiding the value axis from the geom.** It takes the `config` line above.


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