JSON to Candlestick Chart — Educational Guide

Learn how to visualize OHLC (Open, High, Low, Close) JSON data as an interactive SVG candlestick chart using pure JavaScript. Candlestick charts are the standard in financial visualization — each candle encodes four values in one symbol, showing direction, magnitude, and price range at a glance. No external libraries needed.

Objective

  • Understand the anatomy of a candlestick: body, wick, and color semantics
  • Build a categorical X axis with evenly spaced slots (vs. a numeric scale)
  • Reuse the niceAxis() algorithm on the Y axis for clean price ticks
  • Implement Simple Moving Average (SMA) as a line overlay
  • Parse multiple OHLC formats including compact {t,o,h,l,c} keys
  • Export the finished chart as SVG, PNG, or CSV

1. Anatomy of a Candlestick

Each candle encodes four values: Open, High, Low, and Close. Color encodes direction — green for bullish (price rose), red for bearish (price fell).

The two visual components:

  • Body — the wide rectangle from open to close. Its height is |close - open|.
  • Wick (shadow) — the thin line extending from low to high, always centered on the body.
Bullish — close ≥ open
Bearish — close < open

A special case is the Doji: when open equals close, the body height is zero. We render it as a filled empty rectangle (no fill) so only the wick is visible — a valid and meaningful signal in technical analysis.

// Inside candles.forEach(function(c, i) { ... })
const x      = cx(i);
const isBull = c.close >= c.open;
const color  = isBull ? BULL : BEAR;

const bodyTop = scaleY(Math.max(c.open, c.close));
const bodyBot = scaleY(Math.min(c.open, c.close));
const bodyH   = Math.max(1, bodyBot - bodyTop);  // min 1px for doji

// Wick — thin line from low to high
svgEl("line", { x1: x, y1: scaleY(c.high), x2: x, y2: scaleY(c.low),
                stroke: color, "stroke-width": "1.5" }, gCandles);

// Body — rect from open to close
svgEl("rect", { x: x - bodyW / 2, y: bodyTop, width: bodyW, height: bodyH,
                fill: c.open === c.close ? "none" : color, stroke: color, "stroke-width": "1" }, gCandles);

2. X Axis — Evenly Spaced Categories

Unlike scatter plots (numeric X), candlestick X positions are categorical slots. The n candles are distributed evenly across the plot width regardless of time gaps between them.

With layout constants:

PAD_L = 64, PAD_R = 20, PAD_T = 24, PAD_B = 52 W_TOTAL = 560, H_TOTAL = 400 PLOT_W = 476, PLOT_H = 324

Slot and body sizing:

const n     = candles.length;
const slotW = PLOT_W / n;             // width of each time slot
const bodyW = Math.max(2, slotW * 0.6); // body = 60% of slot, min 2px

// Center x-coordinate of candle i
function cx(i) {
  return PAD_L + (i + 0.5) * slotW;
}

X axis labels are suppressed when candles are dense to prevent overlap. The label step adapts automatically:

// Show every 1st, 2nd, 4th, or 8th label depending on count
const labelStep = n <= 12 ? 1 : n <= 24 ? 2 : n <= 48 ? 4 : 8;

candles.forEach(function(c, i) {
  if (i % labelStep !== 0) return;
  svgTxt(cx(i), PAD_T + PLOT_H + 18, c.label, Object.assign({}, tAttr, { "text-anchor": "middle" }), gAxes);
});

3. Y Axis — niceAxis() for Price Ranges

The Y axis spans from Math.min(...lows) to Math.max(...highs). We apply niceAxis() — the same algorithm from the scatter plot — to get clean tick intervals at any price scale.

const allHighs = candles.map(function(c) { return c.high; });
const allLows  = candles.map(function(c) { return c.low; });
const yAxis    = niceAxis(Math.min.apply(null, allLows), Math.max.apply(null, allHighs), 6);

function scaleY(v) {
  return PAD_T + PLOT_H - ((v - yAxis.min) / (yAxis.max - yAxis.min)) * PLOT_H;
}

The niceAxis() function (same as used in the scatter plot):

function niceNumber(v, round) {
  if (v === 0) return 0;
  const exp = Math.floor(Math.log10(Math.abs(v)));
  const frac = Math.abs(v) / Math.pow(10, exp);
  const nice = round
    ? (frac < 1.5 ? 1 : frac < 3 ? 2 : frac < 7 ? 5 : 10)
    : (frac <= 1  ? 1 : frac <= 2 ? 2 : frac <= 5 ? 5 : 10);
  return nice * Math.pow(10, exp);
}

function niceAxis(dMin, dMax, n) {
  n = n || 6;
  if (dMin === dMax) { const p = Math.abs(dMin) * 0.1 || 1; dMin -= p; dMax += p; }
  const step = niceNumber(niceNumber(dMax - dMin, false) / (n - 1), true);
  const mn = Math.floor(dMin / step) * step;
  const mx = Math.ceil(dMax  / step) * step;
  const count = Math.round((mx - mn) / step);
  const ticks = Array.from({ length: count + 1 }, function(_, i) {
    return parseFloat((mn + i * step).toPrecision(12));
  });
  return { min: ticks[0], max: ticks[ticks.length - 1], ticks: ticks, step: step };
}

4. Simple Moving Average (SMA)

SMA is a classic technical indicator. For period p, the SMA at position i is the mean of the last p closing prices. The first p−1 values are null (insufficient history).

function computeSMA(candles, period) {
  return candles.map(function(_, i) {
    if (i < period - 1) return null;  // not enough data yet
    const slice = candles.slice(i - period + 1, i + 1);
    const sum   = slice.reduce(function(s, c) { return s + c.close; }, 0);
    return sum / period;
  });
}

To draw it as a smooth line, collect (x, y) pairs where the value is not null and build an SVG path:

const smaVals = computeSMA(candles, smaPeriod);
let pathD = "";

smaVals.forEach(function(v, i) {
  if (v === null) return;
  pathD += pathD === "" ? "M" + cx(i) + "," + scaleY(v) : " L" + cx(i) + "," + scaleY(v);
});

if (pathD) {
  svgEl("path", { d: pathD, fill: "none", stroke: SMA_COLOR, "stroke-width": "1.5", "clip-path": "url(#clip)" }, svg);
}

A period of 5 reacts quickly to price changes; 20 is smoother and slower. The SMA line starts only after period candles have been seen.

5. Live Demo

Edit the JSON below and click Render to update the chart. Try changing values, adding candles, or switching to compact key format.

OHLC JSON Input

Ready.

Chart Output

6. Data Formats

Three formats are supported and auto-detected:

Format A — Full keys (recommended)

[
  { "date": "Jan", "open": 148, "high": 158, "low": 142, "close": 155 },
  { "date": "Feb", "open": 155, "high": 162, "low": 149, "close": 151 }
]

Format B — Compact keys {t, o, h, l, c}

[
  { "t": "W01", "o": 42000, "h": 44200, "l": 41100, "c": 43500 },
  { "t": "W02", "o": 43500, "h": 45800, "l": 42800, "c": 44900 }
]

Format C — With volume

[
  { "date": "Q1", "open": 310, "high": 345, "low": 298, "close": 338, "volume": 18200 },
  { "date": "Q2", "open": 338, "high": 352, "low": 285, "close": 292, "volume": 22500 }
]
Field Full key Compact key Required
Period label date / label / time t Yes
Opening price open o Yes
Period high high h Yes
Period low low l Yes
Closing price close c Yes
Volume volume v Optional

Full Code (Copy)

A complete standalone HTML page — copy and paste into any .html file and open it in a browser. No build tools, no dependencies.

Showcase

JSON Candlestick Chart Showcase

Full-featured multi-panel viewer with monthly, weekly, and quarterly OHLC data, SMA overlays, tooltips, statistics, and SVG/PNG/CSV export.

→