/* ═══════════════════════════════════════════════════════════════════ DASHBOARD HERO CHART · BTC/USDT premium candles + RSI Includes coin header strip on top + chart + RSI subpanel ═══════════════════════════════════════════════════════════════════ */ function HeroChart({ coin, live, candles, ema21, ema55, vwap, rsi, orders, supportZones, resistanceZones }) { /* The chart body is the shared pro engine (st-chart.jsx): live data per timeframe, wheel zoom, drag pan, real time axis. The premium header strip stays exactly as it was. */ return (
); } /* ═══════════════════════════════════════════════════════════════════ Coin header strip (within chart card top) ═══════════════════════════════════════════════════════════════════ */ function CoinHeaderStrip({ coin, live }) { const up = live.chg >= 0; return (
{coin.name} {coin.sym}/USDT
RANK #{coin.rank} · MCAP $2.07T
${ST_fmt.price(live.price)}
{ST_fmt.pct(live.chg, 2)}
{/* 24h range bar — fills the empty middle */}
); } function KvInline({ k, v, c }) { return (
{k} {v}
); } /* ── 24h range bar — where the live price sits in the day's low→high ──── */ function RangeBar24h({ low, high, price }) { const lo = parseFloat(low) || 0, hi = parseFloat(high) || 0, p = parseFloat(price) || 0; const range = hi - lo; const pct = range > 0 ? Math.max(0, Math.min(100, (p - lo) / range * 100)) : 50; const near = pct >= 66 ? ST.green : pct <= 33 ? ST.red : ST.amber; const label = pct >= 66 ? 'near high' : pct <= 33 ? 'near low' : 'mid-range'; return (
24H RANGE {pct.toFixed(0)}% · {label}
${ST_fmt.price(lo)} ${ST_fmt.price(hi)}
); } function ChartLegendItem({ color, dash, label, value }) { return (
{label} {value}
); } function ChartIconBtn({ icon, active }) { return ( ); } /* ─── The chart SVG ──────────────────────────────────────────────── */ function ChartSvg({ candles, orders, supportZones, resistanceZones, ema21, ema55, vwap, rsi, hover, setHover }) { const W = 900, H = 560; const PADL = 14, PADR = 78, PADT = 10; const PRICE_H = 400; const VOL_H = 38; const RSI_H = 90; const GAP = 10; const priceTop = PADT; const priceBot = priceTop + PRICE_H; const volTop = priceBot + 4; const volBot = volTop + VOL_H; const rsiTop = volBot + GAP; const rsiBot = rsiTop + RSI_H; // price scale — include candles, orders, and S/R zones so they're all visible const extraPrices = [ ...orders.map(o => o.price), ...supportZones.flatMap(z => [z.lo, z.hi]), ...resistanceZones.flatMap(z => [z.lo, z.hi]), ]; const hi = Math.max(...candles.map(c => c.h), ...extraPrices); const lo = Math.min(...candles.map(c => c.l), ...extraPrices); const pad = (hi - lo) * 0.08; const PMAX = hi + pad, PMIN = lo - pad; const Y = (p) => priceTop + (PRICE_H * (PMAX - p) / (PMAX - PMIN)); // x scale const innerW = W - PADL - PADR; const cw = innerW / candles.length; const X = (i) => PADL + i * cw + cw / 2; // gridlines const gridYs = []; const step = (PMAX - PMIN) / 5; for (let i = 1; i < 5; i++) gridYs.push(PMIN + step * i); // volume scale const vMax = Math.max(...candles.map(c => c.v)); const Yv = (v) => volBot - (VOL_H * v / vMax); // RSI scale const Yrsi = (r) => rsiTop + (RSI_H * (100 - r) / 100); const lastPrice = candles[candles.length - 1].c; const lastRsi = rsi[rsi.length - 1]; const fmtP = (p) => p.toLocaleString('en-US', { maximumFractionDigits: 0 }); return ( { const rect = e.currentTarget.getBoundingClientRect(); const sx = (e.clientX - rect.left) * (W / rect.width); const sy = (e.clientY - rect.top) * (H / rect.height); const idx = Math.max(0, Math.min(candles.length - 1, Math.floor((sx - PADL) / cw))); setHover({ x: sx, y: sy, idx }); }} onMouseLeave={() => setHover(null)} style={{ display: 'block' }} > {/* ── background gridlines ── */} {gridYs.map((p, i) => ( ))} {Array.from({ length: 7 }).map((_, i) => { const x = PADL + (i / 6) * innerW; return ; })} {/* ── support / resistance zones ── */} {resistanceZones.map((z, i) => ( RESISTANCE · ${fmtP(z.lo)}–${fmtP(z.hi)} ))} {supportZones.map((z, i) => ( SUPPORT · ${fmtP(z.lo)}–${fmtP(z.hi)} ))} {/* ── EMA 21 ── */} `${i === 0 ? 'M' : 'L'} ${X(i)} ${Y(ema21[i])}`).join(' ')} stroke={ST.amber} strokeWidth="1.5" fill="none" opacity="0.9" /> {/* ── EMA 55 ── */} `${i === 0 ? 'M' : 'L'} ${X(i)} ${Y(ema55[i])}`).join(' ')} stroke={ST.blue} strokeWidth="1.5" fill="none" opacity="0.9" /> {/* ── VWAP ── */} `${i === 0 ? 'M' : 'L'} ${X(i)} ${Y(vwap[i])}`).join(' ')} stroke={ST.ink2} strokeWidth="1.2" strokeDasharray="3 3" fill="none" opacity="0.7" /> {/* ── Candles ── */} {candles.map((c, i) => { const up = c.c >= c.o; const col = up ? ST.green : ST.red; const bodyTop = Y(Math.max(c.o, c.c)); const bodyBot = Y(Math.min(c.o, c.c)); const bodyH = Math.max(1, bodyBot - bodyTop); const bw = Math.max(1, cw * 0.7); return ( {!up && } ); })} {/* ── Volume bars ── */} {candles.map((c, i) => { const up = c.c >= c.o; const col = up ? ST.green : ST.red; const bw = Math.max(1, cw * 0.7); return ( ); })} {/* ── Order lines (amber dotted) ── */} {orders.map((o, i) => { const y = Y(o.price); const c = o.verdict === 'GOOD' ? ST.green : o.verdict === 'AVOID' ? ST.red : ST.amber; return ( ${fmtP(o.price)} BUY · {o.qty.toFixed(4)} BTC · {o.verdict} ); })} {/* ── Last price line + label ── */} ${fmtP(lastPrice)} {/* ── Price axis labels ── */} {gridYs.map((p, i) => ( {fmtP(p)} ))} {/* ── Volume label ── */} VOLUME {/* ── RSI subpanel ── */} 70 30 RSI · 14 current 70 ? ST.red : lastRsi < 30 ? ST.green : ST.ink} fontWeight="700">{lastRsi.toFixed(1)} {/* rsi line */} `${i === 0 ? 'M' : 'L'} ${X(i)} ${Yrsi(r)}`).join(' ')} stroke={ST.amber} strokeWidth="1.5" fill="none" /> {/* current rsi value */} {lastRsi.toFixed(1)} {/* ── Crosshair ── */} {hover && hover.x > PADL && hover.x < W - PADR && ( {/* tooltip */} {hover.y >= priceTop && hover.y <= priceBot && (() => { const c = candles[hover.idx]; const tooltipX = hover.x > W / 2 ? PADL + 8 : W - PADR - 162; return ( O {fmtP(c.o)} H {fmtP(c.h)} L {fmtP(c.l)} C = c.o ? ST.green : ST.red}>{fmtP(c.c)} V {c.v.toFixed(2)}B {candles.length - hover.idx}d ago ); })()} )} ); } /* ── Helpers: EMA + VWAP + RSI (same as Coin page) ─────────────────── */ window.ST_ema = (candles, period) => { const k = 2 / (period + 1); let e = candles[0].c; return candles.map((c, i) => { e = c.c * k + e * (1 - k); return e; }); }; window.ST_vwap = (candles) => { let cumPV = 0, cumV = 0; return candles.map(c => { const typ = (c.h + c.l + c.c) / 3; cumPV += typ * c.v; cumV += c.v; return cumPV / cumV; }); }; window.ST_rsi = (candles, period = 14) => { let gains = 0, losses = 0; const out = []; for (let i = 0; i < candles.length; i++) { if (i === 0) { out.push(50); continue; } const diff = candles[i].c - candles[i - 1].c; const g = Math.max(0, diff), l = Math.max(0, -diff); if (i <= period) { gains += g; losses += l; if (i === period) { const ag = gains / period, al = losses / period; out.push(100 - 100 / (1 + ag / (al || 0.0001))); } else { out.push(50); } } else { const ag = (gains * (period - 1) + g) / period; const al = (losses * (period - 1) + l) / period; gains = ag; losses = al; out.push(100 - 100 / (1 + ag / (al || 0.0001))); } } return out; }; /* Memoize chart — re-render only on coin switch / overlay change, not every live tick. The inner STProChart fetches its own candles. The header strip needs live, so allow re-render when price moves. */ window.HeroChart = React.memo(HeroChart, (prev, next) => { return prev.orders === next.orders && prev.supportZones === next.supportZones && prev.resistanceZones === next.resistanceZones && prev.coin?.sym === next.coin?.sym && prev.live?.price === next.live?.price; });