CandlestickChart

Open, high, low and close for a period, drawn as one mark.

Open, high, low and close for a period, drawn as one mark. The body spans open to close and is filled by direction — green when the period closed at or above where it opened, red when it closed below — and the wick behind it spans the low to the high.

Composed the way every chart here is: the grid, the candles, the axes and the readout are separate children, so a chart that wants no grid simply does not have one.

Installation

CandlestickChart ships with the library — no separate install.

import { CandlestickChart, Frame, Text } from 'panelui-native';

Or copy the source into your project, to own and edit it:

npx panelui-cli@latest add candlestick-chart

Usage

<CandlestickChart data={sessions} xDataKey="day">
  <CandlestickChart.Grid />
  <CandlestickChart.Candles />
  <CandlestickChart.YAxis />
  <CandlestickChart.XAxis />
  <CandlestickChart.Tooltip />
</CandlestickChart>

Composition

<CandlestickChart>
  <CandlestickChart.Header />       {/* title, readout and key, above the plot */}
  <CandlestickChart.Grid />         {/* lines across the price axis */}
  <CandlestickChart.Candles />      {/* the marks */}
  <CandlestickChart.XAxis />        {/* period labels, under the candles */}
  <CandlestickChart.YAxis />        {/* price labels, down the side */}
  <CandlestickChart.Legend />
  <CandlestickChart.Tooltip />      {/* the drag, and the readout */}
</CandlestickChart>
  • CandlestickChart.Header — The strip above the plot: what the chart is of, what it currently reads, and what the two colours mean. The value is passed in rather than derived, so one header can show the last close when nothing is pressed and a session's close when something is.
  • CandlestickChart.Grid — Lines across the price axis, so a candle can be read against a number and not only against the candle beside it.
  • CandlestickChart.Candles — The marks. Every rising body is a subpath of one path and every falling body of another, so the chart is four animated props a frame whether it holds twenty periods or two hundred.
  • CandlestickChart.XAxis — Period labels under the candles. Real text, so they follow the theme's font and the platform's text scaling.
  • CandlestickChart.YAxis — Price labels down the side. The chart reserves a gutter for them, rather than drawing them over the plot.
  • CandlestickChart.Tooltip — The drag that selects a period, and the card that reports its four prices and its change.
  • CandlestickChart.Legend — The two colours and what they mean, floated over the plot. Prefer Header's legend on a chart that has a header.

Examples

A run of sessions

The whole component. Each row carries open, high, low and close; onActiveIndexChange reports the period under the finger, which is how a readout above the plot gets its value.

const sessions = [
  { day: '1 Sep', open: 182.4, high: 185.1, low: 181.2, close: 184.6 },
  { day: '2 Sep', open: 184.6, high: 186.0, low: 182.9, close: 183.1 },
  // …
];

<CandlestickChart
  data={sessions}
  xDataKey="day"
  aspectRatio={1.5}
  onActiveIndexChange={(_index, datum) => setActive(datum)}
>
  <CandlestickChart.Header
    title="NWM · Daily"
    value={price(active?.close ?? last.close)}
    legend
  />
  <CandlestickChart.Grid />
  <CandlestickChart.Candles />
  <CandlestickChart.YAxis />
  <CandlestickChart.XAxis />
  <CandlestickChart.Tooltip formatValue={price} />
</CandlestickChart>

Naming the four columns

The four prices are read from open, high, low and close by default. Point them at whatever the data already calls them rather than reshaping it on the way in.

<CandlestickChart
  data={bars}
  xDataKey="t"
  openDataKey="o"
  highDataKey="h"
  lowDataKey="l"
  closeDataKey="c"
>
  <CandlestickChart.Candles />
</CandlestickChart>

Colours of your own

Green and red are the defaults because up and down are not two series — they are two states of one, and the convention for them is older than any palette. Both are overridable for a chart that has to match something else.

<CandlestickChart.Candles
  risingColor="#2dd4bf"
  fallingColor="#fb7185"
  cornerRadius={2}
/>

Sized for a card

No axes and a wider gap between the candles, for a chart that is a glance rather than a screen. candleGap is a fraction of each period's slice, so the proportions hold at any width.

<CandlestickChart data={recent} xDataKey="day" aspectRatio={1.9} candleGap={0.42}>
  <CandlestickChart.Header value={price(last.close)} caption={change} />
  <CandlestickChart.Candles cornerRadius={2} />
  <CandlestickChart.XAxis />
</CandlestickChart>

Waiting for the series

status="loading" draws flat placeholder candles and grows them into the real ones when it turns ready. One component throughout, rather than a spinner swapped for a chart — swapping loses the transition.

<CandlestickChart data={sessions} xDataKey="day" status={status}>
  <CandlestickChart.Grid />
  <CandlestickChart.Candles />
  <CandlestickChart.XAxis />
</CandlestickChart>

Versions

Sessions

Thirty sessions, with a readout that follows the finger.

<CandlestickChart data={sessions} xDataKey="day" aspectRatio={1.5}>
  <CandlestickChart.Header title="NWM · Daily" value={price(shown.close)} legend />
  <CandlestickChart.Grid />
  <CandlestickChart.Candles />
  <CandlestickChart.YAxis />
  <CandlestickChart.XAxis />
  <CandlestickChart.Tooltip formatValue={price} />
</CandlestickChart>

No axes

The last ten, sized for a card rather than for a screen.

<CandlestickChart data={recent} xDataKey="day" aspectRatio={1.9} candleGap={0.42}>
  <CandlestickChart.Header value={price(last.close)} caption={change} />
  <CandlestickChart.Candles cornerRadius={2} />
  <CandlestickChart.XAxis />
</CandlestickChart>

Loading

Placeholder candles growing into the real ones.

<CandlestickChart data={recent} xDataKey="day" status={status}>
  <CandlestickChart.Header title="NWM · Daily" value={status === 'loading' ? '—' : price(last.close)} />
  <CandlestickChart.Grid />
  <CandlestickChart.Candles />
  <CandlestickChart.XAxis />
</CandlestickChart>

API Reference

CandlestickChart

PropTypeDefaultDescription
classNamestring
dataCandlestickChartDatum[]The rows. Each one is a period along the x-axis.
xDataKeystring'date'Key holding the period label. Used by the axis and the readout.
openDataKeystring'open'Key holding the opening price.
highDataKeystring'high'Key holding the period's high.
lowDataKeystring'low'Key holding the period's low.
closeDataKeystring'close'Key holding the closing price.
statusCandlestickChartStatus'ready'loading draws a row of flat placeholder candles and grows them into the real ones when it turns ready. One component throughout, rather than a spinner swapped for a chart — swapping loses the transition.
aspectRationumber1.6Width ÷ height. 1.6 suits a chart this dense better than 2.
animationDurationnumber1100Milliseconds for the candles to grow in on mount.
domainDurationnumber500Milliseconds for the price axis to settle after the data changes.
yDomain[number, number]Fix the price axis instead of deriving it from the lows and highs. Note that the derived domain deliberately does not include zero — see the notes on why a candle's axis is not a bar's.
candleGapnumber0.3Fraction of each period's slice left empty, 0 to 1. A fraction rather than a pixel gap so the proportions hold at any width.
candleWidthnumberFixed body width in points. Derived from the slice when omitted.
fadedOpacitynumber0.3Opacity of the candles that are not under the finger.
onActiveIndexChange(index: number, datum: CandlestickChartDatum | null) => voidThe candle under the finger as it moves, and -1/null when it lifts. This is how a readout in the card's header gets its value — that header is outside the chart, so it cannot use useCandlestickChart. Fires when the index changes, not per frame.
compactbooleanfalseDrop the axis padding, for a dense strip with no axis or readout.

CandlestickChart.Grid

PropTypeDefaultDescription
rowsnumber4How many lines to draw across the price axis.
colorstring
dashArraystring
opacitynumber1

CandlestickChart.Candles

PropTypeDefaultDescription
risingColorstringColour of a period that closed at or above its open. Green by default.
fallingColorstringColour of a period that closed below its open. Red by default.
cornerRadiusnumber1.5Corner radius on a body.

CandlestickChart.XAxis

PropTypeDefaultDescription
ticksnumberHow many labels to show. Derived from the room available when omitted.
format(datum: CandlestickChartDatum, index: number) => stringFormat a row's label. Defaults to the value at xDataKey.
classNamestring

CandlestickChart.YAxis

PropTypeDefaultDescription
ticksnumber4How many labels to show along the price axis.
format(value: number) => stringFormat a price for its label. Defaults to a compact number.
classNamestring

CandlestickChart.Tooltip

PropTypeDefaultDescription
formatValue(value: number, field: 'open' | 'high' | 'low' | 'close') => stringFormat one of the four prices. Defaults to a compact number.
formatX(datum: CandlestickChartDatum) => stringFormat the readout's heading from the row. Defaults to the value at xDataKey.
showChangebooleantrueShow the period's change from open to close under the four prices.
classNamestring

CandlestickChart.Header

PropTypeDefaultDescription
classNamestring
titlestringSmall line above the value — what the chart is of.
valuestringThe readout. The largest thing on the card, and the first thing read.
captionstringOne muted line under the value — a period, a comparison, a change.
legendbooleanfalseName the two colours, for a reader who has not met the convention.
risingLabelstring'Up'What the rising colour is called.
fallingLabelstring'Down'What the falling colour is called.

CandlestickChart.Legend

PropTypeDefaultDescription
classNamestring
risingLabelstring'Up'What the rising colour is called.
fallingLabelstring'Down'What the falling colour is called.

Every part also accepts the underlying React Native props (ViewProps or TextProps) and a className for Tailwind utilities.

Notes

Why the axis does not reach zero

A bar chart's axis has to, because a bar compares lengths and a bar cropped at the bottom is a length that lies. A candle compares nothing to zero. What is being read is the distance between four numbers that sit close together and usually far from the origin — a share at 180 that moved between 178 and 183 is a chart of that five-point span, and forcing zero onto the axis compresses the whole thing into a band at the top and every candle in it into a dash.

So the domain runs from the lowest low to the highest high, with a tenth of the span as margin at each end so the extremes are not drawn on the frame. yDomain fixes it outright.

Colour is direction, not identity

There is one thing plotted, so there is no series list and no colour per series. The two colours are the two states of that one thing, and they come from the theme's success and destructive tokens rather than from the chart palette — that palette is picked so several series can be told apart, which is not what green and red are doing here.

What it costs to draw

Every rising body is a subpath of one path and every falling body of another, and the wicks likewise. Four animated props a frame whether the chart holds twenty periods or two hundred. The candle under the finger is drawn once more over the top rather than splitting all four paths in two, and only the index crosses to JavaScript while a finger is moving.

Periods, not points

A candle owns a slice of the width rather than sitting on a point, so the finger is inside whichever slice it lands on. candleGap is the fraction of that slice left empty — a fraction rather than a pixel gap, so a chart of thirty periods and a chart of six look like the same chart.

Labels thin themselves

A chart of thirty sessions gives each about eleven points, which is not a date. The axis asks the plot how much room there is and keeps every nth label, spaced far enough apart that no two touch. ticks overrides the count where a particular one is wanted.

Reduced motion

The grow-in and the domain tween are both skipped, and the chart draws straight to its final shape.

aspectRatio measures the plot, not the whole chart. The header sits outside the measured box, so a chart with a readout above it keeps the shape it asked for instead of losing as much height as the readout took.

On this page