/* ═══════════════════════════════════════════════════════════════════
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 (
);
}
/* ── 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;
});