Skip to content

Candlestick ​

The createCandlestick helper turns OHLC (open/high/low/close) items into candles: a direction-colored body spanning open→close, drawn over a thin wick spanning low→high.

ts
// createCandlestick turns OHLC items into direction-colored open/close body
// bars drawn over thin low/high wick bars: four ordinary bar series.
import { createCandlestick } from '@mochart/core';
import type { MochartInputConfig } from '@mochart/core';

const candlestick = createCandlestick([
  { label: '2026-06-01', open: 96.5, high: 97.4, low: 96.1, close: 97.1 },
  { label: '2026-06-02', open: 97.2, high: 99.2, low: 97.2, close: 98.6 },
  { label: '2026-06-03', open: 98.1, high: 102.3, low: 98.0, close: 101.2 },
  { label: '2026-06-04', open: 101.4, high: 101.8, low: 98.4, close: 99.1 },
  { label: '2026-06-05', open: 99.0, high: 99.6, low: 96.8, close: 97.3 },
  { label: '2026-06-08', open: 97.5, high: 98.8, low: 96.9, close: 98.4 },
  { label: '2026-06-09', open: 98.3, high: 98.5, low: 95.7, close: 96.2 },
  { label: '2026-06-10', open: 96.2, high: 97.9, low: 95.9, close: 97.6 },
  { label: '2026-06-11', open: 97.7, high: 100.4, low: 97.5, close: 100.1 },
  { label: '2026-06-12', open: 100.0, high: 100.9, low: 98.6, close: 99.0 }
], { axisType: 'date' });

export const config: MochartInputConfig = {
  version: '1.0.0',
  title: { text: 'Daily Share Price (fictional, $)' },
  categoryAxis: {
    ...candlestick.categoryAxis,
    tickLabel: { format: '%b %d' },
    minorTickLabel: {
      format: '%a',
      font: { size: '0.85em' },
      textStyle: { normal: { fillOpacity: 0.6 }, focused: { fillOpacity: 0.6 }, defocused: { fillOpacity: 0.3 } }
    },
    valueFormat: '%a %b %d',
    tickStep: { interval: 'week' }
  },
  valueAxes: [{ title: { text: '$ per share' } }],
  series: candlestick.series
};

export const data = candlestick.data;

How it works ​

  • Each item is { label, open, high, low, close }. A candle is up when the close is at or above the open, down otherwise. Labels must be unique, every price must be a finite number, high may not be below low, and open and close must lie within them. The helper throws otherwise, so one bad tick can't blank the chart.
  • The helper returns { candles, data, categoryAxis, series }. The candles are four ordinary bar series: an up and a down body spanning from open via rangeProperty, and an up and a down wick spanning low→high, narrowed to a sliver of the slot with bar.widthFraction and listed first so the bodies draw over them. Every row carries values for exactly one direction, and missingValueMode: 'connect' with partialRangeIsMissing keeps the other direction's series from rendering, the same trick as the Waterfall.
  • A doji (open equal to close) would have a zero-height body, so filled bodies set bar.minExtent to 2px and stay visible as a line.
  • The category axis is ordinal, so non-trading days (weekends, holidays) simply don't exist on the axis instead of leaving gaps: above, the Friday before Jun 08 sits right next to it, with no weekend between. With axisType: 'date' the labels are ISO date strings, timestamps or Dates on an ordinal date axis, so the axis fragment can be spread with a d3 time tickLabel.format and tooltip valueFormat as above; without it every label is a string shown as given.
  • With months of daily candles the generated tick labels are truncated or skipped at arbitrary days; step the ticks by week with tickStep instead, as the example does, so the first trading day of each week gets the tick. The days between are minor ticks: minorTickLabel.format of %a names the weekday at each where the short labels fit, minorTickLabel.font sets them at 0.85em of the page font and minorTickLabel.textStyle fades them to 0.6 opacity (0.3 when defocused). Or list the dates to label in ticks. See labeling chosen dates.
  • The default direction colors are teal-green/red rather than a pure green/red: green↔red is the classic red-green-blindness collision, and shifting the green toward teal keeps the pair distinguishable on light and dark surfaces. Override per direction with colors, rename the legend entries with seriesTitles, and tune the widths with wickWidthFraction (default 0.15) / bodyWidthFraction (default 1).
  • The tooltip shows two rows per candle: the body's open – close span under its direction title, and the wick's low – high span under rangeTitle (default "Range"). The wicks stay out of the legend but follow their body's legend filtering and focus via followSeries, so toggling a direction removes whole candles and focusing a direction highlights whole candles.
  • Each row also carries the raw open/high/low/close plus change and direction, and the computed candles come back under candles, or call computeCandlesticks(items) alone for the math without the chart fragments.
  • For the tick-bar style of the same data (a thin low/high line with open and close ticks instead of a body), see OHLC bars.

Hollow candles ​

Pass hollow: true to draw up candles as outlines, the classic hollow-candle style where a filled body means down:

ts
// The hollow option outlines up candles instead of filling them, the classic
// hollow-candle style where a filled body means down. The wicks split into
// segments around the body so they don't show through the hollow interior.
import { createCandlestick } from '@mochart/core';
import type { MochartInputConfig } from '@mochart/core';

const candlestick = createCandlestick([
  { label: '2026-06-01', open: 96.5, high: 97.4, low: 96.1, close: 97.1 },
  { label: '2026-06-02', open: 97.2, high: 99.2, low: 97.2, close: 98.6 },
  { label: '2026-06-03', open: 98.1, high: 102.3, low: 98.0, close: 101.2 },
  { label: '2026-06-04', open: 101.4, high: 101.8, low: 98.4, close: 99.1 },
  { label: '2026-06-05', open: 99.0, high: 99.6, low: 96.8, close: 97.3 },
  { label: '2026-06-08', open: 97.5, high: 98.8, low: 96.9, close: 98.4 },
  { label: '2026-06-09', open: 98.3, high: 98.5, low: 95.7, close: 96.2 },
  { label: '2026-06-10', open: 96.2, high: 97.9, low: 95.9, close: 97.6 },
  { label: '2026-06-11', open: 97.7, high: 100.4, low: 97.5, close: 100.1 },
  { label: '2026-06-12', open: 100.0, high: 100.9, low: 98.6, close: 99.0 }
], { hollow: true, axisType: 'date' });

export const config: MochartInputConfig = {
  version: '1.0.0',
  title: { text: 'Daily Share Price (fictional, $)' },
  categoryAxis: {
    ...candlestick.categoryAxis,
    tickLabel: { format: '%b %d' },
    minorTickLabel: {
      format: '%a',
      font: { size: '0.85em' },
      textStyle: { normal: { fillOpacity: 0.6 }, focused: { fillOpacity: 0.6 }, defocused: { fillOpacity: 0.3 } }
    },
    valueFormat: '%a %b %d',
    tickStep: { interval: 'week' }
  },
  valueAxes: [{ title: { text: '$ per share' } }],
  series: candlestick.series
};

export const data = candlestick.data;

In hollow mode the low→high wick can't sit behind the body (it would show through the hollow interior), so the helper splits it into segments that stop at the body edges, and the original wick series turns shapeless: it keeps the tooltip's single low – high range row and its focus/filter wiring, but draws nothing. The up body outlines itself through shapeStyle (a stroke color and width against fillOpacity: 0, pinned to 0 in the focused and defocused states too so hovering thickens the outline rather than filling it), and its legend and tooltip icons pick up the stroke color automatically. colors, seriesTitles and the width options apply as in filled mode.

Volume pane ​

Give the items a volume and pass volume: true (works with hollow too, and with OHLC bars) to add the classic pane of direction-colored volume bars along the bottom of the plot:

ts
// The volume option adds a pane of direction-colored volume bars along the
// bottom of the plot: a hidden second axis confines the bars to the lower
// band via domain margins, and the returned valueAxes fragment
// carries both pane axes.
import { createCandlestick } from '@mochart/core';
import type { MochartInputConfig } from '@mochart/core';

const candlestick = createCandlestick([
  { label: '2026-06-01', open: 96.5, high: 97.4, low: 96.1, close: 97.1, volume: 310000 },
  { label: '2026-06-02', open: 97.2, high: 99.2, low: 97.2, close: 98.6, volume: 540000 },
  { label: '2026-06-03', open: 98.1, high: 102.3, low: 98.0, close: 101.2, volume: 890000 },
  { label: '2026-06-04', open: 101.4, high: 101.8, low: 98.4, close: 99.1, volume: 650000 },
  { label: '2026-06-05', open: 99.0, high: 99.6, low: 96.8, close: 97.3, volume: 480000 },
  { label: '2026-06-08', open: 97.5, high: 98.8, low: 96.9, close: 98.4, volume: 290000 },
  { label: '2026-06-09', open: 98.3, high: 98.5, low: 95.7, close: 96.2, volume: 610000 },
  { label: '2026-06-10', open: 96.2, high: 97.9, low: 95.9, close: 97.6, volume: 350000 },
  { label: '2026-06-11', open: 97.7, high: 100.4, low: 97.5, close: 100.1, volume: 720000 },
  { label: '2026-06-12', open: 100.0, high: 100.9, low: 98.6, close: 99.0, volume: 330000 }
], { volume: true, axisType: 'date' });

export const config: MochartInputConfig = {
  version: '1.0.0',
  title: { text: 'Daily Share Price (fictional, $)' },
  categoryAxis: { ...candlestick.categoryAxis, tickLabel: { format: '%b %d' }, valueFormat: '%a %b %d' },
  valueAxes: candlestick.valueAxes!.map((axisConfig) =>
    axisConfig.id === 'price' ? { ...axisConfig, title: { text: '$ per share' } } : axisConfig),
  series: candlestick.series
};

export const data = candlestick.data;

The pane is pure domain-margin geometry on a second value axis, so it adapts to every data update: the result gains a valueAxes fragment with a price axis whose enlarged minMarginFraction lifts the candles into the upper plot, and a hidden volume axis pinned at 0 whose maxMarginFraction confines the bars to the bottom band (margins above 1 are allowed for exactly this banding). Spread that fragment into valueAxes as the example does. Tune the split with volume: { heightFraction, gapFraction } (defaults 0.2 and 0.05; heightFraction must be above 0 and gapFraction at least 0, both below 1, and they must sum to less than 1, or the call throws), relabel the tooltip rows with valueLabel (default "Volume"), or set visible: true on the volume axis fragment to show its scale. The volume bars follow their direction series (toggling or focusing Up takes its volume bars along) and stay out of the legend, with one volume row per day in the tooltip.

Log price axis ​

A price that multiplies over the chart reads best on a log scale, where a candle's height is its percentage move rather than its dollar move, so the swings of a $10 stock are as visible as those of the same stock at $100:

ts
// A price that multiplies reads best on a log axis, where a candle's height is
// its percentage move. The helper's volume axis starts at 0, which a log axis
// has no place for, so scale: 'log' goes on the price axis alone, by its id.
import { createCandlestick } from '@mochart/core';
import type { MochartInputConfig } from '@mochart/core';

const candlestick = createCandlestick([
  { label: '2023-07-01', open: 9.91, high: 11.32, low: 9.74, close: 10.86, volume: 2502743 },
  { label: '2023-08-01', open: 10.97, high: 13.3, low: 10.76, close: 13.26, volume: 2173218 },
  { label: '2023-09-01', open: 13.04, high: 16.56, low: 12.71, close: 15.88, volume: 3014207 },
  { label: '2023-10-01', open: 15.77, high: 17.33, low: 15.66, close: 16.85, volume: 893563 },
  { label: '2023-11-01', open: 17.05, high: 17.41, low: 16.64, close: 16.79, volume: 488922 },
  { label: '2023-12-01', open: 16.67, high: 18.02, low: 16.3, close: 18.01, volume: 1293914 },
  { label: '2024-01-01', open: 18.15, high: 18.47, low: 17.37, close: 17.88, volume: 913006 },
  { label: '2024-02-01', open: 18.14, high: 21.41, low: 17.33, close: 20.83, volume: 2840181 },
  { label: '2024-03-01', open: 21.03, high: 21.78, low: 20.72, close: 21.28, volume: 457832 },
  { label: '2024-04-01', open: 21.02, high: 23.61, low: 20.3, close: 22.77, volume: 2436680 },
  { label: '2024-05-01', open: 22.37, high: 26.21, low: 21.89, close: 26.03, volume: 3898785 },
  { label: '2024-06-01', open: 26.3, high: 28.67, low: 25.75, close: 27.75, volume: 704007 },
  { label: '2024-07-01', open: 27.95, high: 35.03, low: 27.08, close: 33.63, volume: 2156540 },
  { label: '2024-08-01', open: 33.14, high: 38.89, low: 32.74, close: 38.35, volume: 1853128 },
  { label: '2024-09-01', open: 38.62, high: 43.94, low: 37.19, close: 43.61, volume: 1710602 },
  { label: '2024-10-01', open: 43.4, high: 47.92, low: 43.15, close: 46.94, volume: 1960551 },
  { label: '2024-11-01', open: 47.54, high: 48.94, low: 45.9, close: 48.27, volume: 1023672 },
  { label: '2024-12-01', open: 48.37, high: 50.37, low: 47.08, close: 48.39, volume: 612871 },
  { label: '2025-01-01', open: 47.64, high: 54.39, low: 45.96, close: 53.77, volume: 2363299 },
  { label: '2025-02-01', open: 53.73, high: 63.45, low: 52.24, close: 63.16, volume: 4180924 },
  { label: '2025-03-01', open: 62.2, high: 69.38, low: 60.92, close: 67.28, volume: 2070509 },
  { label: '2025-04-01', open: 66.07, high: 70.9, low: 65.81, close: 69.62, volume: 943656 },
  { label: '2025-05-01', open: 69.65, high: 87.59, low: 67.37, close: 84.88, volume: 2706720 },
  { label: '2025-06-01', open: 84.28, high: 103.83, low: 84.14, close: 101.07, volume: 2800259 }
], { volume: true, axisType: 'date' });

export const config: MochartInputConfig = {
  version: '1.0.0',
  title: { text: 'Monthly Share Price (fictional, $)' },
  categoryAxis: { ...candlestick.categoryAxis, tickLabel: { format: '%Y' }, valueFormat: '%b %Y', tickStep: { interval: 'year' } },
  valueAxes: candlestick.valueAxes!.map((axisConfig) =>
    axisConfig.id === 'price' ? { ...axisConfig, scale: 'log', title: { text: '$ per share' } } : axisConfig),
  series: candlestick.series
};

export const data = candlestick.data;

Set scale: 'log' on the price axis alone, by its id, as the example does. The volume axis starts at 0, which a log axis has no position for, so a log scale set for every value axis at once (in valueAxisDefaults, or on each entry of the fragment) fails validation on the volume axis. The volume pane still works on a log price axis: its minMarginFraction is taken from the logs of the price domain, so it reserves the same share of the plot either way.

Released under the MIT License.