Candlestick Chart — Stock OHLC HTML CSS JS Snippet

Candlestick Chart · Charts · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

True OHLC candles
Each candle is a wick <line> (high→low) plus a body <rect> (open→close), coloured green/red by direction — the standard candlestick anatomy.
Auto-scaling axis
build maps prices to the data's min/max within padding, so any instrument or price range renders correctly with no config.
Doji-safe bodies
A minimum body height keeps open≈close candles visible as a thin line instead of disappearing.
Generous hit areas
A transparent full-height <rect> per candle makes hovering forgiving — you needn't land on the thin body.
Hover crosshair
A dashed vertical line snaps to the hovered candle, the crosshair UX traders expect from a terminal.
OHLC tooltip + readout
show fills a tooltip with formatted O/H/L/C and updates the header price and coloured percent change.
Latest-candle default
Leaving the chart restores the readout to the most recent candle, never leaving a stale hovered value.
Crisp scalable SVG
Candles are hoverable DOM elements that stay sharp at any size via preserveAspectRatio — no canvas hit-testing.

About this UI Snippet

Candlestick Chart — OHLC Bodies & Wicks, Hover Crosshair & Live Price Readout

Screenshot of the Candlestick Chart snippet rendered live

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

  1. 1
    Paste 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.
  2. 2
    Hover 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.
  3. 3
    Read 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.
  4. 4
    Move along the seriesSweep across the candles — the crosshair, price, and tooltip follow each one in turn.
  5. 5
    Leave the chartThe readout returns to the most recent candle and the crosshair hides.
  6. 6
    Plug in real OHLC dataReplace genData with your market data array of { o, h, l, c } and call build; the chart auto-scales.

Real-world uses

Common Use Cases

Trading and brokerage apps
The core price chart for stocks, forex, or crypto. Pair with a realtime line chart for the live tick line.
Crypto dashboards
Show OHLC candles per coin with the header price/percent readout; combine with a metric card grid for portfolio stats.
Market and watchlist widgets
Compact candlestick cards in a watchlist; a sparkline chart works for the tighter rows.
Backtesting and analytics tools
Visualise historical OHLC bars with hover inspection of each candle's values.
Fintech marketing and demos
An impressive, dependency-free chart for landing pages and product tours.
Education and trading tutorials
Teach candle anatomy — body, wick, colour — with an interactive, hoverable example.

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

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>
  <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>
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>
  <!-- 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>
React
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>
    </>
  );
}
React + Tailwind
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>
    </>
  );
}
Vue
<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>
Angular
// @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 });
  }
}