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

# Candlestick

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 candlestick chart shows how a price moved in each trading session. Each candle has a body from the open to the close and a wick from the low to the high, coloured by whether the session rose or fell.

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

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

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

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

## One candle per session

Map the session to `x`, and each price to its aesthetic:

<GraphEmbed story="chart-types-candlestick--equity-uptrend" args={{ orientation: 'vertical' }} />

```tsx theme={null}
const data = {
  columns: [
    { key: 'date', label: 'Date' },
    { key: 'open', label: 'Open' },
    { key: 'high', label: 'High' },
    { key: 'low', label: 'Low' },
    { key: 'close', label: 'Close' },
  ],
  rows: [
    { date: 'Apr 01', open: 100, high: 103, low: 99, close: 102 },
    { date: 'Apr 02', open: 102, high: 104, low: 101, close: 101 },
    // …
  ],
};

const spec = kit.pipe(
  kit.createSpec({ x: 'date' }),
  kit.geom.candlestick({
    aes: { open: 'open', high: 'high', low: 'low', close: 'close' },
  }),
  kit.scale.x.discrete(),
  kit.scale.y.continuous()
);

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

All four prices are required. The sessions sit one per band at equal spacing, so a weekend or holiday takes no room. The session axis is always discrete, even for dates. The price axis fits the prices rather than holding zero:

<GraphEmbed story="chart-types-candlestick--volatile-selloff" />

A session rises when its close is at or above its open, so a doji (open equal to close) counts as rising and draws as a flat body.

The close is the candle's `y`: a highlight or a headline that reads `y` reads the close. A `y` in the root mapping does not reach the candlestick layer, and the compiler warns that it is ignored.

## Horizontal

Add `kit.coord.flip()`. The sessions move to the vertical axis and the wicks lie across.

<GraphEmbed story="chart-types-candlestick--equity-uptrend" args={{ orientation: 'horizontal' }} />

## Body width

`width` sets how much of its band each body spans, as a fraction from just above `0` up to `1`, like a bar's. It defaults to `0.6`; at `1` neighbouring bodies touch. The wick stays on the band centre.

```tsx theme={null}
kit.geom.candlestick({
  aes: { open: 'open', high: 'high', low: 'low', close: 'close' },
  params: { width: 0.8 },
});
```

<GraphEmbed story="chart-types-candlestick--body-width" args={{ width: 0.8 }} />

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.

## Styling

A candle's colour says which way its session went, so it comes from a part per direction. The bodies' other properties and the wicks have builders of their own:

| Builder | Properties | Default |
| - | - | - |
| `kit.style.geom.candlestick.rising()` | `fill`, `stroke` | The positive trend colour |
| `kit.style.geom.candlestick.falling()` | `fill`, `stroke` | The negative trend colour |
| `kit.style.geom.candlestick()` | `alpha`, `fillAlpha`, `strokeWidth` | 1px border |
| `kit.style.geom.candlestick.wick()` | `stroke`, `alpha`, `strokeWidth`, `dashArray` | 1px, in its direction's `stroke` |

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

```tsx theme={null}
const spec = kit.pipe(
  // …
  styles({
    defaults: [
      kit.style.geom.candlestick.rising({
        fill: 'transparent',
        stroke: '#1f7a6d',
      }),
      kit.style.geom.candlestick.falling({
        fill: '#3a3833',
        stroke: '#3a3833',
      }),
      kit.style.geom.candlestick.wick({ stroke: '#898373' }),
    ],
  })
);
```

A wick takes its direction's `stroke` unless a wick entry names one. A chart-wide `style.geom({ fill })` does not reach the candles, so it cannot flatten them to one colour. To colour the candles from the data instead, see [Mapped colour](#mapped-colour).

Each session carries a `candlestickDirection` variable, `'rising'` or `'falling'`, which replaces a data column of the same name. Name it in an entry's `where` to style one direction's bodies or wicks:

```tsx theme={null}
kit.style.geom.candlestick(
  { strokeWidth: 2 },
  { where: { variable: 'candlestickDirection', eq: 'falling' } }
);
```

## Mapped colour

`color` is opt-in. Map it to `candlestickDirection` to colour the directions through a colour scale, with a legend:

<GraphEmbed story="chart-types-candlestick--mapped-color" args={{ recipe: 'direction legend' }} />

```tsx theme={null}
const spec = kit.pipe(
  kit.createSpec({ x: 'date' }),
  kit.geom.candlestick({
    aes: {
      open: 'open',
      high: 'high',
      low: 'low',
      close: 'close',
      color: 'candlestickDirection',
    },
  }),
  kit.scale.x.discrete(),
  kit.scale.y.continuous(),
  kit.scale.color.discrete({
    domain: ['rising', 'falling'],
    range: ['#1f8a70', '#c8453b'],
  })
);
```

Map it to a column you compute to colour by your own rule. A third value paints a doji (open equal to close) apart from both directions:

<GraphEmbed story="chart-types-candlestick--mapped-color" args={{ recipe: 'neutral doji' }} />

```tsx theme={null}
const rows = sessions.map((row) => ({
  ...row,
  move: row.close > row.open ? 'up' : row.close < row.open ? 'down' : 'flat',
}));

kit.scale.color.discrete({
  domain: ['up', 'down', 'flat'],
  range: ['#1f8a70', '#c8453b', '#8a8a8a'],
});
```

Hollow candles colour each session by its close against the previous close, and hollow out the bodies that closed above their open:

<GraphEmbed story="chart-types-candlestick--mapped-color" args={{ recipe: 'hollow by previous close' }} />

```tsx theme={null}
const rows = sessions.map((row, index) => ({
  ...row,
  versusPrevious:
    row.close >= (sessions[index - 1]?.close ?? row.open) ? 'higher' : 'lower',
}));

// aes: { …, color: 'versusPrevious' }
styles({
  overrides: [kit.style.geom.candlestick.rising({ fill: 'transparent' })],
});
```

Or map it to a data column to colour the candles by its groups. Colour then no longer shows the direction, so give it a second cue, such as hollow rising bodies:

<GraphEmbed story="chart-types-candlestick--mapped-color" args={{ recipe: 'by week' }} />

```tsx theme={null}
styles({
  overrides: [kit.style.geom.candlestick.rising({ fill: 'transparent' })],
});
```

A mapped colour replaces the direction parts' built-in colours and beats their `defaults` entries. An `overrides` entry beats the mapped colour, which is why the hollow bodies above are an override. A wick follows its body's colour unless a wick entry names one.

## Hover and highlight

Hovering a session emphasises its candle, and the tooltip lists its open, high, low and close, each labelled by its column's `label` (or its key when it has none). A press anywhere along the wick takes its session.

A highlight raises each matched candle whole, wick and body together. Match on a price, or on the direction:

<GraphEmbed story="chart-types-candlestick--highlight" args={{ target: 'falling sessions' }} />

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

## Not yet supported

* **The editor's chart-type picker and the chart agent.** In editable mode the candlestick renders, hovers, selects and takes annotations on each session's close, but it is authored in code.
* **Data labels.** `dataLabels` prints nothing on a candle yet.
* **Volume and OHLC bars.** Draw volume as a separate bar chart; OHLC ticks in place of a body are not drawn yet.
* **Aggregation.** Duplicate rows for one session all draw; aggregate them first with `transform.aggregate()`.
