More Charts Snippets
Candlestick Chart — Stock OHLC HTML CSS JS Snippet
Candlestick Chart · Charts · Plain HTML, CSS & JS · Live preview
What's included
Features
<line> (high→low) plus a body <rect> (open→close), coloured green/red by direction — the standard candlestick anatomy.build maps prices to the data's min/max within padding, so any instrument or price range renders correctly with no config.<rect> per candle makes hovering forgiving — you needn't land on the thin body.show fills a tooltip with formatted O/H/L/C and updates the header price and coloured percent change.preserveAspectRatio — no canvas hit-testing.About this UI Snippet
Candlestick Chart — OHLC Bodies & Wicks, Hover Crosshair & Live Price Readout

The candlestick chart is the language of financial markets — every trading app, crypto dashboard, and stock tracker uses it because a single candle encodes four values (open, high, low, close) and its colour shows direction at a glance. This snippet draws a real OHLC candlestick chart with an SVG in plain HTML, CSS, and vanilla JavaScript: green/red bodies, high-low wicks, a hover crosshair, and a tooltip with the candle's OHLC plus a live price and percent-change readout.
Anatomy of a candle
Each candle is an SVG <g> containing a thin <line> wick from the high to the low and a <rect> body spanning the open-to-close range. The body is green (.ck-up) when close ≥ open and red (.ck-down) otherwise — the universal convention. build computes the data's overall high and low, derives a y mapping that flips the axis (higher price = higher on screen) and fits everything within padding, then positions each candle at an evenly spaced cx. A minimum body height (Math.max(1.5, …)) keeps doji candles (open ≈ close) visible as a thin line rather than vanishing.
Hover crosshair and OHLC tooltip
Each candle's <g> includes a full-height transparent <rect> as a generous hit area, so you do not have to land precisely on the thin body. On mouseenter, show moves a dashed vertical crosshair to that candle, updates the header price and percent change (coloured green/red), and fills a tooltip with the formatted O, H, L, C values positioned over the candle. Leaving the plot snaps the readout back to the latest candle and hides the crosshair — exactly how trading terminals behave.
Auto-scaling and data generation
The chart auto-scales to the min/max of the dataset, so it works for any price range without configuration. A genData function produces a realistic random walk (each candle opens at the previous close, with randomised high/low spreads) so the demo looks like a real instrument; replace it with your OHLC array from a market API and call build.
Why SVG over Canvas
SVG candles are individually hoverable DOM elements — no hit-testing math, no manual redraws on hover — which makes the crosshair and tooltip trivial, and it stays crisp at any size via preserveAspectRatio. For thousands of candles you would switch to Canvas, but for the typical 20–60 visible candles a dashboard shows, SVG is simpler and exports cleanly to every framework.
Pair this with a realtime line chart for live ticks, a sparkline chart for compact trends, or a metric card grid for portfolio stats.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA dark "AURX · 1D" card appears with 24 green/red candlesticks and the latest candle's price and % change in the header.
- 2Hover a candleA dashed crosshair snaps to it, the header price and percent change update, and a tooltip shows that candle's O/H/L/C.
- 3Read direction at a glanceGreen candles closed up, red closed down; the wick shows the full high-to-low range, the body the open-to-close range.
- 4Move along the seriesSweep across the candles — the crosshair, price, and tooltip follow each one in turn.
- 5Leave the chartThe readout returns to the most recent candle and the crosshair hides.
- 6Plug in real OHLC dataReplace
genDatawith your market data array of{ o, h, l, c }and callbuild; the chart auto-scales.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Replace genData with your data: fetch an array of { o, h, l, c } (open/high/low/close) from a market API and pass it to build (assign it to DATA and skip the generator). The chart auto-scales to the data's min/max, so no axis configuration is needed. For live updates, append the newest candle and re-run build, or update only the last candle's rect/line for efficiency.
For volume, reserve the bottom ~20% of the viewBox and draw a thin <rect> per candle scaled to its volume. For a moving average, compute the rolling mean and draw a smooth <path> (a polyline or Bézier) over the candles. Both overlay cleanly because everything shares the same x step and y scale used by the candles.
SVG is ideal up to a few hundred candles: each is a real DOM element, so hover, crosshair, and tooltips need no hit-testing and it stays crisp at any size. For thousands of candles or high-frequency live updates, Canvas (or WebGL) is faster because it avoids per-element DOM overhead. This snippet uses SVG for the typical 20–60 visible candles a dashboard shows.
An SVG price chart is hard for screen readers, so expose the data textually: the header already shows the current price and change as real text, and you can add an aria-label to the SVG summarising the trend plus an off-screen data table of OHLC values. Ensure up/down is conveyed by more than colour — the percent arrow (▲/▼) and the tooltip labels do this.
In React, compute the candle geometry with useMemo from your OHLC array and render <g>/<line>/<rect> elements in JSX, tracking the hovered index in useState for the crosshair and tooltip. In Vue, use a computed for the candles and v-for. In Angular, compute in the component and *ngFor. The y-scale math and colour logic port unchanged.
Candlestick Chart — Export as HTML, React, Vue, Angular & Tailwind
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Candlestick Chart</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0f172a;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.ck-card{background:#111827;border:1px solid #1f2937;border-radius:16px;padding:18px;width:100%;max-width:420px;box-shadow:0 18px 44px rgba(0,0,0,.45)}
.ck-head{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:12px}
.ck-sym{font-size:15px;font-weight:800;color:#f9fafb}
.ck-tf{color:#6b7280;font-weight:600;font-size:12px}
.ck-price{display:flex;align-items:baseline;gap:8px;margin-top:4px}
.ck-price #ckPrice{font-size:22px;font-weight:800;color:#f9fafb;font-variant-numeric:tabular-nums}
.ck-chg{font-size:12px;font-weight:700}
.ck-chg.up{color:#34d399}.ck-chg.down{color:#f87171}
.ck-hl{display:flex;flex-direction:column;gap:2px;text-align:right;font-size:11px;font-weight:600}
.ck-hl .up{color:#34d399}.ck-hl .down{color:#f87171}
.ck-hl b{font-variant-numeric:tabular-nums}
.ck-plot{position:relative}
.ck-svg{width:100%;height:180px;display:block}
.ck-wick{stroke-width:1.4}
.ck-body{transition:opacity .12s}
.ck-up{fill:#10b981;stroke:#10b981}
.ck-down{fill:#ef4444;stroke:#ef4444}
.ck-col:hover .ck-body{opacity:.7}
.ck-cross{stroke:#475569;stroke-width:1;stroke-dasharray:3 3;opacity:0;transition:opacity .12s}
.ck-tip{position:absolute;top:6px;transform:translateX(-50%);background:#1f2937;border:1px solid #374151;color:#e5e7eb;border-radius:8px;padding:7px 10px;font-size:11px;line-height:1.5;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .12s;box-shadow:0 6px 18px rgba(0,0,0,.4);z-index:2}
.ck-tip b{font-variant-numeric:tabular-nums}
.ck-tip .g{color:#9ca3af}
</style>
</head>
<body>
<div class="ck-card">
<div class="ck-head">
<div>
<div class="ck-sym">AURX <span class="ck-tf">· 1D</span></div>
<div class="ck-price"><span id="ckPrice">—</span> <span class="ck-chg" id="ckChg">hover a candle</span></div>
</div>
<div class="ck-hl"><span class="up">H <b id="ckHi">—</b></span><span class="down">L <b id="ckLo">—</b></span></div>
</div>
<div class="ck-plot" id="ckPlot">
<svg class="ck-svg" id="ckSvg" viewBox="0 0 320 180" preserveAspectRatio="none"></svg>
<div class="ck-tip" id="ckTip"></div>
</div>
</div>
<script>
var svg = document.getElementById('ckSvg');
var plot = document.getElementById('ckPlot');
var tip = document.getElementById('ckTip');
var NS = 'http://www.w3.org/2000/svg';
var W = 320, H = 180, PAD = 10;
var DATA = [];
function genData() {
var price = 120, out = [];
for (var i = 0; i < 24; i++) {
var open = price;
var close = open + (Math.random() - 0.48) * 9;
var high = Math.max(open, close) + Math.random() * 4;
var low = Math.min(open, close) - Math.random() * 4;
out.push({ o: open, c: close, h: high, l: low });
price = close;
}
return out;
}
function build() {
DATA = genData();
var max = Math.max.apply(null, DATA.map(function (d) { return d.h; }));
var min = Math.min.apply(null, DATA.map(function (d) { return d.l; }));
var range = max - min || 1;
var n = DATA.length;
var step = (W - PAD * 2) / n;
var bodyW = step * 0.6;
function y(v) { return PAD + (1 - (v - min) / range) * (H - PAD * 2); }
var frag = '';
DATA.forEach(function (d, i) {
var cx = PAD + step * i + step / 2;
var up = d.c >= d.o;
var cls = up ? 'ck-up' : 'ck-down';
var bodyTop = y(Math.max(d.o, d.c));
var bodyH = Math.max(1.5, Math.abs(y(d.o) - y(d.c)));
frag += '<g class="ck-col" data-i="' + i + '">' +
'<rect x="' + (cx - step / 2) + '" y="0" width="' + step + '" height="' + H + '" fill="transparent"/>' +
'<line class="ck-wick ' + cls + '" x1="' + cx + '" y1="' + y(d.h) + '" x2="' + cx + '" y2="' + y(d.l) + '"/>' +
'<rect class="ck-body ' + cls + '" x="' + (cx - bodyW / 2) + '" y="' + bodyTop + '" width="' + bodyW + '" height="' + bodyH + '" rx="1"/>' +
'</g>';
});
frag += '<line class="ck-cross" id="ckCross" x1="0" y1="' + PAD + '" x2="0" y2="' + (H - PAD) + '"/>';
svg.innerHTML = frag;
var last = DATA[n - 1];
document.getElementById('ckHi').textContent = max.toFixed(2);
document.getElementById('ckLo').textContent = min.toFixed(2);
show(n - 1);
svg.querySelectorAll('.ck-col').forEach(function (g) {
g.addEventListener('mouseenter', function () { show(+g.dataset.i); });
});
plot.addEventListener('mouseleave', function () { show(DATA.length - 1); hideCross(); });
}
function show(i) {
var d = DATA[i];
var price = document.getElementById('ckPrice');
price.textContent = d.c.toFixed(2);
var chg = ((d.c - d.o) / d.o * 100);
var chgEl = document.getElementById('ckChg');
chgEl.textContent = (chg >= 0 ? '▲ ' : '▼ ') + Math.abs(chg).toFixed(2) + '%';
chgEl.className = 'ck-chg ' + (chg >= 0 ? 'up' : 'down');
var step = (W - PAD * 2) / DATA.length;
var cx = PAD + step * i + step / 2;
var cross = document.getElementById('ckCross');
if (cross) { cross.setAttribute('x1', cx); cross.setAttribute('x2', cx); cross.style.opacity = '1'; }
tip.innerHTML = '<span class="g">O</span> <b>' + d.o.toFixed(1) + '</b> <span class="g">C</span> <b>' + d.c.toFixed(1) + '</b><br>' +
'<span class="g">H</span> <b>' + d.h.toFixed(1) + '</b> <span class="g">L</span> <b>' + d.l.toFixed(1) + '</b>';
tip.style.left = (cx / W * 100) + '%';
tip.style.opacity = '1';
}
function hideCross() {
var cross = document.getElementById('ckCross');
if (cross) cross.style.opacity = '0';
tip.style.opacity = '0';
}
build();
</script>
</body>
</html><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Candlestick Chart</title>
<!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
* {
box-sizing:border-box;margin:0;padding:0
}
body {
font-family:system-ui,-apple-system,sans-serif;background:#0f172a;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px
}
.ck-price #ckPrice {
font-size:22px;font-weight:800;color:#f9fafb;font-variant-numeric:tabular-nums
}
.ck-chg {
font-size:12px;font-weight:700
}
.ck-chg.up {
color:#34d399
}
.ck-chg.down {
color:#f87171
}
.ck-hl .up {
color:#34d399
}
.ck-hl .down {
color:#f87171
}
.ck-hl b {
font-variant-numeric:tabular-nums
}
.ck-wick {
stroke-width:1.4
}
.ck-body {
transition:opacity .12s
}
.ck-up {
fill:#10b981;stroke:#10b981
}
.ck-down {
fill:#ef4444;stroke:#ef4444
}
.ck-col:hover .ck-body {
opacity:.7
}
.ck-cross {
stroke:#475569;stroke-width:1;stroke-dasharray:3 3;opacity:0;transition:opacity .12s
}
.ck-tip b {
font-variant-numeric:tabular-nums
}
.ck-tip .g {
color:#9ca3af
}
</style>
</head>
<body>
<div class="bg-[#111827] border border-[#1f2937] rounded-2xl p-[18px] w-full max-w-[420px] shadow-[0_18px_44px_rgba(0,0,0,.45)]">
<div class="flex items-start justify-between mb-3">
<div>
<div class="text-[15px] font-extrabold text-[#f9fafb]">AURX <span class="text-[#6b7280] font-semibold text-xs">· 1D</span></div>
<div class="ck-price flex items-baseline gap-2 mt-1"><span id="ckPrice">—</span> <span class="ck-chg" id="ckChg">hover a candle</span></div>
</div>
<div class="ck-hl flex flex-col gap-0.5 text-right text-[11px] font-semibold"><span class="up">H <b id="ckHi">—</b></span><span class="down">L <b id="ckLo">—</b></span></div>
</div>
<div class="relative" id="ckPlot">
<svg class="w-full h-[180px] block" id="ckSvg" viewBox="0 0 320 180" preserveAspectRatio="none"></svg>
<div class="ck-tip absolute top-1.5 [transform:translateX(-50%)] bg-[#1f2937] border border-[#374151] text-[#e5e7eb] rounded-lg py-[7px] px-2.5 text-[11px] leading-normal whitespace-nowrap opacity-0 pointer-events-none [transition:opacity_.12s] shadow-[0_6px_18px_rgba(0,0,0,.4)] z-[2]" id="ckTip"></div>
</div>
</div>
<script>
var svg = document.getElementById('ckSvg');
var plot = document.getElementById('ckPlot');
var tip = document.getElementById('ckTip');
var NS = 'http://www.w3.org/2000/svg';
var W = 320, H = 180, PAD = 10;
var DATA = [];
function genData() {
var price = 120, out = [];
for (var i = 0; i < 24; i++) {
var open = price;
var close = open + (Math.random() - 0.48) * 9;
var high = Math.max(open, close) + Math.random() * 4;
var low = Math.min(open, close) - Math.random() * 4;
out.push({ o: open, c: close, h: high, l: low });
price = close;
}
return out;
}
function build() {
DATA = genData();
var max = Math.max.apply(null, DATA.map(function (d) { return d.h; }));
var min = Math.min.apply(null, DATA.map(function (d) { return d.l; }));
var range = max - min || 1;
var n = DATA.length;
var step = (W - PAD * 2) / n;
var bodyW = step * 0.6;
function y(v) { return PAD + (1 - (v - min) / range) * (H - PAD * 2); }
var frag = '';
DATA.forEach(function (d, i) {
var cx = PAD + step * i + step / 2;
var up = d.c >= d.o;
var cls = up ? 'ck-up' : 'ck-down';
var bodyTop = y(Math.max(d.o, d.c));
var bodyH = Math.max(1.5, Math.abs(y(d.o) - y(d.c)));
frag += '<g class="ck-col" data-i="' + i + '">' +
'<rect x="' + (cx - step / 2) + '" y="0" width="' + step + '" height="' + H + '" fill="transparent"/>' +
'<line class="ck-wick ' + cls + '" x1="' + cx + '" y1="' + y(d.h) + '" x2="' + cx + '" y2="' + y(d.l) + '"/>' +
'<rect class="ck-body ' + cls + '" x="' + (cx - bodyW / 2) + '" y="' + bodyTop + '" width="' + bodyW + '" height="' + bodyH + '" rx="1"/>' +
'</g>';
});
frag += '<line class="ck-cross" id="ckCross" x1="0" y1="' + PAD + '" x2="0" y2="' + (H - PAD) + '"/>';
svg.innerHTML = frag;
var last = DATA[n - 1];
document.getElementById('ckHi').textContent = max.toFixed(2);
document.getElementById('ckLo').textContent = min.toFixed(2);
show(n - 1);
svg.querySelectorAll('.ck-col').forEach(function (g) {
g.addEventListener('mouseenter', function () { show(+g.dataset.i); });
});
plot.addEventListener('mouseleave', function () { show(DATA.length - 1); hideCross(); });
}
function show(i) {
var d = DATA[i];
var price = document.getElementById('ckPrice');
price.textContent = d.c.toFixed(2);
var chg = ((d.c - d.o) / d.o * 100);
var chgEl = document.getElementById('ckChg');
chgEl.textContent = (chg >= 0 ? '▲ ' : '▼ ') + Math.abs(chg).toFixed(2) + '%';
chgEl.className = 'ck-chg ' + (chg >= 0 ? 'up' : 'down');
var step = (W - PAD * 2) / DATA.length;
var cx = PAD + step * i + step / 2;
var cross = document.getElementById('ckCross');
if (cross) { cross.setAttribute('x1', cx); cross.setAttribute('x2', cx); cross.style.opacity = '1'; }
tip.innerHTML = '<span class="g">O</span> <b>' + d.o.toFixed(1) + '</b> <span class="g">C</span> <b>' + d.c.toFixed(1) + '</b><br>' +
'<span class="g">H</span> <b>' + d.h.toFixed(1) + '</b> <span class="g">L</span> <b>' + d.l.toFixed(1) + '</b>';
tip.style.left = (cx / W * 100) + '%';
tip.style.opacity = '1';
}
function hideCross() {
var cross = document.getElementById('ckCross');
if (cross) cross.style.opacity = '0';
tip.style.opacity = '0';
}
build();
</script>
</body>
</html>import React, { useEffect } from 'react';
// CSS — optionally move to CandlestickChart.module.css
const css = `
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0f172a;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.ck-card{background:#111827;border:1px solid #1f2937;border-radius:16px;padding:18px;width:100%;max-width:420px;box-shadow:0 18px 44px rgba(0,0,0,.45)}
.ck-head{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:12px}
.ck-sym{font-size:15px;font-weight:800;color:#f9fafb}
.ck-tf{color:#6b7280;font-weight:600;font-size:12px}
.ck-price{display:flex;align-items:baseline;gap:8px;margin-top:4px}
.ck-price #ckPrice{font-size:22px;font-weight:800;color:#f9fafb;font-variant-numeric:tabular-nums}
.ck-chg{font-size:12px;font-weight:700}
.ck-chg.up{color:#34d399}.ck-chg.down{color:#f87171}
.ck-hl{display:flex;flex-direction:column;gap:2px;text-align:right;font-size:11px;font-weight:600}
.ck-hl .up{color:#34d399}.ck-hl .down{color:#f87171}
.ck-hl b{font-variant-numeric:tabular-nums}
.ck-plot{position:relative}
.ck-svg{width:100%;height:180px;display:block}
.ck-wick{stroke-width:1.4}
.ck-body{transition:opacity .12s}
.ck-up{fill:#10b981;stroke:#10b981}
.ck-down{fill:#ef4444;stroke:#ef4444}
.ck-col:hover .ck-body{opacity:.7}
.ck-cross{stroke:#475569;stroke-width:1;stroke-dasharray:3 3;opacity:0;transition:opacity .12s}
.ck-tip{position:absolute;top:6px;transform:translateX(-50%);background:#1f2937;border:1px solid #374151;color:#e5e7eb;border-radius:8px;padding:7px 10px;font-size:11px;line-height:1.5;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .12s;box-shadow:0 6px 18px rgba(0,0,0,.4);z-index:2}
.ck-tip b{font-variant-numeric:tabular-nums}
.ck-tip .g{color:#9ca3af}
`;
export default function CandlestickChart() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
var svg = document.getElementById('ckSvg');
var plot = document.getElementById('ckPlot');
var tip = document.getElementById('ckTip');
var NS = 'http://www.w3.org/2000/svg';
var W = 320, H = 180, PAD = 10;
var DATA = [];
function genData() {
var price = 120, out = [];
for (var i = 0; i < 24; i++) {
var open = price;
var close = open + (Math.random() - 0.48) * 9;
var high = Math.max(open, close) + Math.random() * 4;
var low = Math.min(open, close) - Math.random() * 4;
out.push({ o: open, c: close, h: high, l: low });
price = close;
}
return out;
}
function build() {
DATA = genData();
var max = Math.max.apply(null, DATA.map(function (d) { return d.h; }));
var min = Math.min.apply(null, DATA.map(function (d) { return d.l; }));
var range = max - min || 1;
var n = DATA.length;
var step = (W - PAD * 2) / n;
var bodyW = step * 0.6;
function y(v) { return PAD + (1 - (v - min) / range) * (H - PAD * 2); }
var frag = '';
DATA.forEach(function (d, i) {
var cx = PAD + step * i + step / 2;
var up = d.c >= d.o;
var cls = up ? 'ck-up' : 'ck-down';
var bodyTop = y(Math.max(d.o, d.c));
var bodyH = Math.max(1.5, Math.abs(y(d.o) - y(d.c)));
frag += '<g class="ck-col" data-i="' + i + '">' +
'<rect x="' + (cx - step / 2) + '" y="0" width="' + step + '" height="' + H + '" fill="transparent"/>' +
'<line class="ck-wick ' + cls + '" x1="' + cx + '" y1="' + y(d.h) + '" x2="' + cx + '" y2="' + y(d.l) + '"/>' +
'<rect class="ck-body ' + cls + '" x="' + (cx - bodyW / 2) + '" y="' + bodyTop + '" width="' + bodyW + '" height="' + bodyH + '" rx="1"/>' +
'</g>';
});
frag += '<line class="ck-cross" id="ckCross" x1="0" y1="' + PAD + '" x2="0" y2="' + (H - PAD) + '"/>';
svg.innerHTML = frag;
var last = DATA[n - 1];
document.getElementById('ckHi').textContent = max.toFixed(2);
document.getElementById('ckLo').textContent = min.toFixed(2);
show(n - 1);
svg.querySelectorAll('.ck-col').forEach(function (g) {
g.addEventListener('mouseenter', function () { show(+g.dataset.i); });
});
plot.addEventListener('mouseleave', function () { show(DATA.length - 1); hideCross(); });
}
function show(i) {
var d = DATA[i];
var price = document.getElementById('ckPrice');
price.textContent = d.c.toFixed(2);
var chg = ((d.c - d.o) / d.o * 100);
var chgEl = document.getElementById('ckChg');
chgEl.textContent = (chg >= 0 ? '▲ ' : '▼ ') + Math.abs(chg).toFixed(2) + '%';
chgEl.className = 'ck-chg ' + (chg >= 0 ? 'up' : 'down');
var step = (W - PAD * 2) / DATA.length;
var cx = PAD + step * i + step / 2;
var cross = document.getElementById('ckCross');
if (cross) { cross.setAttribute('x1', cx); cross.setAttribute('x2', cx); cross.style.opacity = '1'; }
tip.innerHTML = '<span class="g">O</span> <b>' + d.o.toFixed(1) + '</b> <span class="g">C</span> <b>' + d.c.toFixed(1) + '</b><br>' +
'<span class="g">H</span> <b>' + d.h.toFixed(1) + '</b> <span class="g">L</span> <b>' + d.l.toFixed(1) + '</b>';
tip.style.left = (cx / W * 100) + '%';
tip.style.opacity = '1';
}
function hideCross() {
var cross = document.getElementById('ckCross');
if (cross) cross.style.opacity = '0';
tip.style.opacity = '0';
}
build();
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
}, []);
return (
<>
<style>{css}</style>
<div className="ck-card">
<div className="ck-head">
<div>
<div className="ck-sym">AURX <span className="ck-tf">· 1D</span></div>
<div className="ck-price"><span id="ckPrice">—</span> <span className="ck-chg" id="ckChg">hover a candle</span></div>
</div>
<div className="ck-hl"><span className="up">H <b id="ckHi">—</b></span><span className="down">L <b id="ckLo">—</b></span></div>
</div>
<div className="ck-plot" id="ckPlot">
<svg className="ck-svg" id="ckSvg" viewBox="0 0 320 180" preserveAspectRatio="none"></svg>
<div className="ck-tip" id="ckTip"></div>
</div>
</div>
</>
);
}import React, { useEffect } from 'react';
// Requires Tailwind CSS v3+ — https://tailwindcss.com/docs/installation
// Arbitrary value classes (e.g. bg-[#0f172a]) are valid Tailwind v3+
export default function CandlestickChart() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
var svg = document.getElementById('ckSvg');
var plot = document.getElementById('ckPlot');
var tip = document.getElementById('ckTip');
var NS = 'http://www.w3.org/2000/svg';
var W = 320, H = 180, PAD = 10;
var DATA = [];
function genData() {
var price = 120, out = [];
for (var i = 0; i < 24; i++) {
var open = price;
var close = open + (Math.random() - 0.48) * 9;
var high = Math.max(open, close) + Math.random() * 4;
var low = Math.min(open, close) - Math.random() * 4;
out.push({ o: open, c: close, h: high, l: low });
price = close;
}
return out;
}
function build() {
DATA = genData();
var max = Math.max.apply(null, DATA.map(function (d) { return d.h; }));
var min = Math.min.apply(null, DATA.map(function (d) { return d.l; }));
var range = max - min || 1;
var n = DATA.length;
var step = (W - PAD * 2) / n;
var bodyW = step * 0.6;
function y(v) { return PAD + (1 - (v - min) / range) * (H - PAD * 2); }
var frag = '';
DATA.forEach(function (d, i) {
var cx = PAD + step * i + step / 2;
var up = d.c >= d.o;
var cls = up ? 'ck-up' : 'ck-down';
var bodyTop = y(Math.max(d.o, d.c));
var bodyH = Math.max(1.5, Math.abs(y(d.o) - y(d.c)));
frag += '<g class="ck-col" data-i="' + i + '">' +
'<rect x="' + (cx - step / 2) + '" y="0" width="' + step + '" height="' + H + '" fill="transparent"/>' +
'<line class="ck-wick ' + cls + '" x1="' + cx + '" y1="' + y(d.h) + '" x2="' + cx + '" y2="' + y(d.l) + '"/>' +
'<rect class="ck-body ' + cls + '" x="' + (cx - bodyW / 2) + '" y="' + bodyTop + '" width="' + bodyW + '" height="' + bodyH + '" rx="1"/>' +
'</g>';
});
frag += '<line class="ck-cross" id="ckCross" x1="0" y1="' + PAD + '" x2="0" y2="' + (H - PAD) + '"/>';
svg.innerHTML = frag;
var last = DATA[n - 1];
document.getElementById('ckHi').textContent = max.toFixed(2);
document.getElementById('ckLo').textContent = min.toFixed(2);
show(n - 1);
svg.querySelectorAll('.ck-col').forEach(function (g) {
g.addEventListener('mouseenter', function () { show(+g.dataset.i); });
});
plot.addEventListener('mouseleave', function () { show(DATA.length - 1); hideCross(); });
}
function show(i) {
var d = DATA[i];
var price = document.getElementById('ckPrice');
price.textContent = d.c.toFixed(2);
var chg = ((d.c - d.o) / d.o * 100);
var chgEl = document.getElementById('ckChg');
chgEl.textContent = (chg >= 0 ? '▲ ' : '▼ ') + Math.abs(chg).toFixed(2) + '%';
chgEl.className = 'ck-chg ' + (chg >= 0 ? 'up' : 'down');
var step = (W - PAD * 2) / DATA.length;
var cx = PAD + step * i + step / 2;
var cross = document.getElementById('ckCross');
if (cross) { cross.setAttribute('x1', cx); cross.setAttribute('x2', cx); cross.style.opacity = '1'; }
tip.innerHTML = '<span class="g">O</span> <b>' + d.o.toFixed(1) + '</b> <span class="g">C</span> <b>' + d.c.toFixed(1) + '</b><br>' +
'<span class="g">H</span> <b>' + d.h.toFixed(1) + '</b> <span class="g">L</span> <b>' + d.l.toFixed(1) + '</b>';
tip.style.left = (cx / W * 100) + '%';
tip.style.opacity = '1';
}
function hideCross() {
var cross = document.getElementById('ckCross');
if (cross) cross.style.opacity = '0';
tip.style.opacity = '0';
}
build();
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
}, []);
return (
<>
<style>{`
* {
box-sizing:border-box;margin:0;padding:0
}
body {
font-family:system-ui,-apple-system,sans-serif;background:#0f172a;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px
}
.ck-price #ckPrice {
font-size:22px;font-weight:800;color:#f9fafb;font-variant-numeric:tabular-nums
}
.ck-chg {
font-size:12px;font-weight:700
}
.ck-chg.up {
color:#34d399
}
.ck-chg.down {
color:#f87171
}
.ck-hl .up {
color:#34d399
}
.ck-hl .down {
color:#f87171
}
.ck-hl b {
font-variant-numeric:tabular-nums
}
.ck-wick {
stroke-width:1.4
}
.ck-body {
transition:opacity .12s
}
.ck-up {
fill:#10b981;stroke:#10b981
}
.ck-down {
fill:#ef4444;stroke:#ef4444
}
.ck-col:hover .ck-body {
opacity:.7
}
.ck-cross {
stroke:#475569;stroke-width:1;stroke-dasharray:3 3;opacity:0;transition:opacity .12s
}
.ck-tip b {
font-variant-numeric:tabular-nums
}
.ck-tip .g {
color:#9ca3af
}
`}</style>
<div className="bg-[#111827] border border-[#1f2937] rounded-2xl p-[18px] w-full max-w-[420px] shadow-[0_18px_44px_rgba(0,0,0,.45)]">
<div className="flex items-start justify-between mb-3">
<div>
<div className="text-[15px] font-extrabold text-[#f9fafb]">AURX <span className="text-[#6b7280] font-semibold text-xs">· 1D</span></div>
<div className="ck-price flex items-baseline gap-2 mt-1"><span id="ckPrice">—</span> <span className="ck-chg" id="ckChg">hover a candle</span></div>
</div>
<div className="ck-hl flex flex-col gap-0.5 text-right text-[11px] font-semibold"><span className="up">H <b id="ckHi">—</b></span><span className="down">L <b id="ckLo">—</b></span></div>
</div>
<div className="relative" id="ckPlot">
<svg className="w-full h-[180px] block" id="ckSvg" viewBox="0 0 320 180" preserveAspectRatio="none"></svg>
<div className="ck-tip absolute top-1.5 [transform:translateX(-50%)] bg-[#1f2937] border border-[#374151] text-[#e5e7eb] rounded-lg py-[7px] px-2.5 text-[11px] leading-normal whitespace-nowrap opacity-0 pointer-events-none [transition:opacity_.12s] shadow-[0_6px_18px_rgba(0,0,0,.4)] z-[2]" id="ckTip"></div>
</div>
</div>
</>
);
}<template>
<div class="ck-card">
<div class="ck-head">
<div>
<div class="ck-sym">AURX <span class="ck-tf">· 1D</span></div>
<div class="ck-price"><span id="ckPrice">—</span> <span class="ck-chg" id="ckChg">hover a candle</span></div>
</div>
<div class="ck-hl"><span class="up">H <b id="ckHi">—</b></span><span class="down">L <b id="ckLo">—</b></span></div>
</div>
<div class="ck-plot" id="ckPlot">
<svg class="ck-svg" id="ckSvg" viewBox="0 0 320 180" preserveAspectRatio="none"></svg>
<div class="ck-tip" id="ckTip"></div>
</div>
</div>
</template>
<script setup>
import { onMounted } from 'vue';
let svg;
let plot;
let tip;
let NS;
var W = 320, H = 180, PAD = 10;
var DATA = [];
function genData() {
var price = 120, out = [];
for (var i = 0; i < 24; i++) {
var open = price;
var close = open + (Math.random() - 0.48) * 9;
var high = Math.max(open, close) + Math.random() * 4;
var low = Math.min(open, close) - Math.random() * 4;
out.push({ o: open, c: close, h: high, l: low });
price = close;
}
return out;
}
function build() {
DATA = genData();
var max = Math.max.apply(null, DATA.map(function (d) { return d.h; }));
var min = Math.min.apply(null, DATA.map(function (d) { return d.l; }));
var range = max - min || 1;
var n = DATA.length;
var step = (W - PAD * 2) / n;
var bodyW = step * 0.6;
function y(v) { return PAD + (1 - (v - min) / range) * (H - PAD * 2); }
var frag = '';
DATA.forEach(function (d, i) {
var cx = PAD + step * i + step / 2;
var up = d.c >= d.o;
var cls = up ? 'ck-up' : 'ck-down';
var bodyTop = y(Math.max(d.o, d.c));
var bodyH = Math.max(1.5, Math.abs(y(d.o) - y(d.c)));
frag += '<g class="ck-col" data-i="' + i + '">' +
'<rect x="' + (cx - step / 2) + '" y="0" width="' + step + '" height="' + H + '" fill="transparent"/>' +
'<line class="ck-wick ' + cls + '" x1="' + cx + '" y1="' + y(d.h) + '" x2="' + cx + '" y2="' + y(d.l) + '"/>' +
'<rect class="ck-body ' + cls + '" x="' + (cx - bodyW / 2) + '" y="' + bodyTop + '" width="' + bodyW + '" height="' + bodyH + '" rx="1"/>' +
'</g>';
});
frag += '<line class="ck-cross" id="ckCross" x1="0" y1="' + PAD + '" x2="0" y2="' + (H - PAD) + '"/>';
svg.innerHTML = frag;
var last = DATA[n - 1];
document.getElementById('ckHi').textContent = max.toFixed(2);
document.getElementById('ckLo').textContent = min.toFixed(2);
show(n - 1);
svg.querySelectorAll('.ck-col').forEach(function (g) {
g.addEventListener('mouseenter', function () { show(+g.dataset.i); });
});
plot.addEventListener('mouseleave', function () { show(DATA.length - 1); hideCross(); });
}
function show(i) {
var d = DATA[i];
var price = document.getElementById('ckPrice');
price.textContent = d.c.toFixed(2);
var chg = ((d.c - d.o) / d.o * 100);
var chgEl = document.getElementById('ckChg');
chgEl.textContent = (chg >= 0 ? '▲ ' : '▼ ') + Math.abs(chg).toFixed(2) + '%';
chgEl.className = 'ck-chg ' + (chg >= 0 ? 'up' : 'down');
var step = (W - PAD * 2) / DATA.length;
var cx = PAD + step * i + step / 2;
var cross = document.getElementById('ckCross');
if (cross) { cross.setAttribute('x1', cx); cross.setAttribute('x2', cx); cross.style.opacity = '1'; }
tip.innerHTML = '<span class="g">O</span> <b>' + d.o.toFixed(1) + '</b> <span class="g">C</span> <b>' + d.c.toFixed(1) + '</b><br>' +
'<span class="g">H</span> <b>' + d.h.toFixed(1) + '</b> <span class="g">L</span> <b>' + d.l.toFixed(1) + '</b>';
tip.style.left = (cx / W * 100) + '%';
tip.style.opacity = '1';
}
function hideCross() {
var cross = document.getElementById('ckCross');
if (cross) cross.style.opacity = '0';
tip.style.opacity = '0';
}
onMounted(() => {
svg = document.getElementById('ckSvg');
plot = document.getElementById('ckPlot');
tip = document.getElementById('ckTip');
NS = 'http://www.w3.org/2000/svg';
build();
});
</script>
<style scoped>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0f172a;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.ck-card{background:#111827;border:1px solid #1f2937;border-radius:16px;padding:18px;width:100%;max-width:420px;box-shadow:0 18px 44px rgba(0,0,0,.45)}
.ck-head{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:12px}
.ck-sym{font-size:15px;font-weight:800;color:#f9fafb}
.ck-tf{color:#6b7280;font-weight:600;font-size:12px}
.ck-price{display:flex;align-items:baseline;gap:8px;margin-top:4px}
.ck-price #ckPrice{font-size:22px;font-weight:800;color:#f9fafb;font-variant-numeric:tabular-nums}
.ck-chg{font-size:12px;font-weight:700}
.ck-chg.up{color:#34d399}.ck-chg.down{color:#f87171}
.ck-hl{display:flex;flex-direction:column;gap:2px;text-align:right;font-size:11px;font-weight:600}
.ck-hl .up{color:#34d399}.ck-hl .down{color:#f87171}
.ck-hl b{font-variant-numeric:tabular-nums}
.ck-plot{position:relative}
.ck-svg{width:100%;height:180px;display:block}
.ck-wick{stroke-width:1.4}
.ck-body{transition:opacity .12s}
.ck-up{fill:#10b981;stroke:#10b981}
.ck-down{fill:#ef4444;stroke:#ef4444}
.ck-col:hover .ck-body{opacity:.7}
.ck-cross{stroke:#475569;stroke-width:1;stroke-dasharray:3 3;opacity:0;transition:opacity .12s}
.ck-tip{position:absolute;top:6px;transform:translateX(-50%);background:#1f2937;border:1px solid #374151;color:#e5e7eb;border-radius:8px;padding:7px 10px;font-size:11px;line-height:1.5;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .12s;box-shadow:0 6px 18px rgba(0,0,0,.4);z-index:2}
.ck-tip b{font-variant-numeric:tabular-nums}
.ck-tip .g{color:#9ca3af}
</style>// @ts-nocheck
// Note: vanilla JS DOM manipulation is preserved as-is inside ngAfterViewInit().
// For idiomatic Angular, replace document.getElementById() with @ViewChild() refs
// and move state into component properties with two-way binding.
import { Component, AfterViewInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-candlestick-chart',
standalone: true,
imports: [CommonModule],
encapsulation: ViewEncapsulation.None,
template: `
<div class="ck-card">
<div class="ck-head">
<div>
<div class="ck-sym">AURX <span class="ck-tf">· 1D</span></div>
<div class="ck-price"><span id="ckPrice">—</span> <span class="ck-chg" id="ckChg">hover a candle</span></div>
</div>
<div class="ck-hl"><span class="up">H <b id="ckHi">—</b></span><span class="down">L <b id="ckLo">—</b></span></div>
</div>
<div class="ck-plot" id="ckPlot">
<svg class="ck-svg" id="ckSvg" viewBox="0 0 320 180" preserveAspectRatio="none"></svg>
<div class="ck-tip" id="ckTip"></div>
</div>
</div>
`,
styles: [`
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0f172a;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.ck-card{background:#111827;border:1px solid #1f2937;border-radius:16px;padding:18px;width:100%;max-width:420px;box-shadow:0 18px 44px rgba(0,0,0,.45)}
.ck-head{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:12px}
.ck-sym{font-size:15px;font-weight:800;color:#f9fafb}
.ck-tf{color:#6b7280;font-weight:600;font-size:12px}
.ck-price{display:flex;align-items:baseline;gap:8px;margin-top:4px}
.ck-price #ckPrice{font-size:22px;font-weight:800;color:#f9fafb;font-variant-numeric:tabular-nums}
.ck-chg{font-size:12px;font-weight:700}
.ck-chg.up{color:#34d399}.ck-chg.down{color:#f87171}
.ck-hl{display:flex;flex-direction:column;gap:2px;text-align:right;font-size:11px;font-weight:600}
.ck-hl .up{color:#34d399}.ck-hl .down{color:#f87171}
.ck-hl b{font-variant-numeric:tabular-nums}
.ck-plot{position:relative}
.ck-svg{width:100%;height:180px;display:block}
.ck-wick{stroke-width:1.4}
.ck-body{transition:opacity .12s}
.ck-up{fill:#10b981;stroke:#10b981}
.ck-down{fill:#ef4444;stroke:#ef4444}
.ck-col:hover .ck-body{opacity:.7}
.ck-cross{stroke:#475569;stroke-width:1;stroke-dasharray:3 3;opacity:0;transition:opacity .12s}
.ck-tip{position:absolute;top:6px;transform:translateX(-50%);background:#1f2937;border:1px solid #374151;color:#e5e7eb;border-radius:8px;padding:7px 10px;font-size:11px;line-height:1.5;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .12s;box-shadow:0 6px 18px rgba(0,0,0,.4);z-index:2}
.ck-tip b{font-variant-numeric:tabular-nums}
.ck-tip .g{color:#9ca3af}
`]
})
export class CandlestickChartComponent implements AfterViewInit {
ngAfterViewInit(): void {
var svg = document.getElementById('ckSvg');
var plot = document.getElementById('ckPlot');
var tip = document.getElementById('ckTip');
var NS = 'http://www.w3.org/2000/svg';
var W = 320, H = 180, PAD = 10;
var DATA = [];
function genData() {
var price = 120, out = [];
for (var i = 0; i < 24; i++) {
var open = price;
var close = open + (Math.random() - 0.48) * 9;
var high = Math.max(open, close) + Math.random() * 4;
var low = Math.min(open, close) - Math.random() * 4;
out.push({ o: open, c: close, h: high, l: low });
price = close;
}
return out;
}
function build() {
DATA = genData();
var max = Math.max.apply(null, DATA.map(function (d) { return d.h; }));
var min = Math.min.apply(null, DATA.map(function (d) { return d.l; }));
var range = max - min || 1;
var n = DATA.length;
var step = (W - PAD * 2) / n;
var bodyW = step * 0.6;
function y(v) { return PAD + (1 - (v - min) / range) * (H - PAD * 2); }
var frag = '';
DATA.forEach(function (d, i) {
var cx = PAD + step * i + step / 2;
var up = d.c >= d.o;
var cls = up ? 'ck-up' : 'ck-down';
var bodyTop = y(Math.max(d.o, d.c));
var bodyH = Math.max(1.5, Math.abs(y(d.o) - y(d.c)));
frag += '<g class="ck-col" data-i="' + i + '">' +
'<rect x="' + (cx - step / 2) + '" y="0" width="' + step + '" height="' + H + '" fill="transparent"/>' +
'<line class="ck-wick ' + cls + '" x1="' + cx + '" y1="' + y(d.h) + '" x2="' + cx + '" y2="' + y(d.l) + '"/>' +
'<rect class="ck-body ' + cls + '" x="' + (cx - bodyW / 2) + '" y="' + bodyTop + '" width="' + bodyW + '" height="' + bodyH + '" rx="1"/>' +
'</g>';
});
frag += '<line class="ck-cross" id="ckCross" x1="0" y1="' + PAD + '" x2="0" y2="' + (H - PAD) + '"/>';
svg.innerHTML = frag;
var last = DATA[n - 1];
document.getElementById('ckHi').textContent = max.toFixed(2);
document.getElementById('ckLo').textContent = min.toFixed(2);
show(n - 1);
svg.querySelectorAll('.ck-col').forEach(function (g) {
g.addEventListener('mouseenter', function () { show(+g.dataset.i); });
});
plot.addEventListener('mouseleave', function () { show(DATA.length - 1); hideCross(); });
}
function show(i) {
var d = DATA[i];
var price = document.getElementById('ckPrice');
price.textContent = d.c.toFixed(2);
var chg = ((d.c - d.o) / d.o * 100);
var chgEl = document.getElementById('ckChg');
chgEl.textContent = (chg >= 0 ? '▲ ' : '▼ ') + Math.abs(chg).toFixed(2) + '%';
chgEl.className = 'ck-chg ' + (chg >= 0 ? 'up' : 'down');
var step = (W - PAD * 2) / DATA.length;
var cx = PAD + step * i + step / 2;
var cross = document.getElementById('ckCross');
if (cross) { cross.setAttribute('x1', cx); cross.setAttribute('x2', cx); cross.style.opacity = '1'; }
tip.innerHTML = '<span class="g">O</span> <b>' + d.o.toFixed(1) + '</b> <span class="g">C</span> <b>' + d.c.toFixed(1) + '</b><br>' +
'<span class="g">H</span> <b>' + d.h.toFixed(1) + '</b> <span class="g">L</span> <b>' + d.l.toFixed(1) + '</b>';
tip.style.left = (cx / W * 100) + '%';
tip.style.opacity = '1';
}
function hideCross() {
var cross = document.getElementById('ckCross');
if (cross) cross.style.opacity = '0';
tip.style.opacity = '0';
}
build();
// Bind inline-handler functions to the component so the template can call them
Object.assign(this, { genData, build, show, hideCross });
}
}