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
opentoclose. Its height is|close - open|. - Wick (shadow) — the thin line extending from
lowtohigh, always centered on the body.
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:
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
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.