/* ============================================================ CHAINUS — data-viz primitives (native SVG/CSS, no deps) Sparkline · CandleChart · Gauge · Ticker · LiquidityBars · RatesList · LiveNumber · Donut Exported to window for cross-file use. ============================================================ */ /* seeded pseudo-random so charts are stable across renders */ function seeded(seed) { let s = seed % 2147483647; if (s <= 0) s += 2147483646; return () => (s = (s * 16807) % 2147483647) / 2147483647; } /* ---- in-view hook: fires once when element enters viewport ---- */ function useInView(threshold = 0.4) { const ref = useRef(null); const [seen, setSeen] = useState(false); useEffect(() => { const el = ref.current; if (!el) return; if (reduceMotion()) { setSeen(true); return; } const io = new IntersectionObserver((es) => { es.forEach((e) => { if (e.isIntersecting) { setSeen(true); io.disconnect(); } }); }, { threshold }); io.observe(el); return () => io.disconnect(); }, [threshold]); return [ref, seen]; } /* ---- animated number that eases to a target & wiggles "live" ---- */ function LiveNumber({ value, decimals = 0, prefix = '', suffix = '', live = false, className = '' }) { const [ref, seen] = useInView(0.6); const [val, setVal] = useState(0); useEffect(() => { if (!seen) return; if (reduceMotion()) { setVal(value); return; } const t0 = performance.now(); const dur = 1200; const from = 0; let raf; const tick = (t) => { const p = Math.min(1, (t - t0) / dur); const e = 1 - Math.pow(1 - p, 3); setVal(from + (value - from) * e); if (p < 1) raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [seen, value]); useEffect(() => { if (!live || !seen || reduceMotion()) return; const id = setInterval(() => { setVal((v) => { const jitter = value * (0.0006 * (Math.random() - 0.5)); return value + jitter * 1; }); }, 1600); return () => clearInterval(id); }, [live, seen, value]); const text = prefix + val.toLocaleString('en-US', { minimumFractionDigits: decimals, maximumFractionDigits: decimals }) + suffix; return {text}; } /* ---- Sparkline: draws on with stroke-dashoffset ---- */ function Sparkline({ data, color = 'var(--up)', w = 100, h = 30, fill = true, strokeW = 2, id }) { const [ref, seen] = useInView(0.5); const max = Math.max(...data), min = Math.min(...data); const rng = max - min || 1; const pts = data.map((d, i) => [ (i / (data.length - 1)) * w, h - ((d - min) / rng) * (h - 4) - 2 ]); const line = pts.map((p, i) => `${i === 0 ? 'M' : 'L'}${p[0].toFixed(1)} ${p[1].toFixed(1)}`).join(' '); const area = `${line} L${w} ${h} L0 ${h} Z`; const gid = id || ('sg' + Math.round(min + max + data.length)); return ( {fill && } ); } /* ---- Candlestick chart with grid + axis ---- */ function CandleChart({ seed = 7, count = 34, height = 280, labels = true }) { const [ref, seen] = useInView(0.35); const data = React.useMemo(() => { const rnd = seeded(seed); let price = 100; const out = []; for (let i = 0; i < count; i++) { const drift = (rnd() - 0.46) * 6; const open = price; const close = Math.max(40, open + drift); const high = Math.max(open, close) + rnd() * 3.4; const low = Math.min(open, close) - rnd() * 3.4; out.push({ open, close, high, low, up: close >= open }); price = close; } return out; }, [seed, count]); const w = 560, h = height, padR = 46, padB = 22, padT = 12; const plotW = w - padR, plotH = h - padB - padT; const allHi = Math.max(...data.map((d) => d.high)); const allLo = Math.min(...data.map((d) => d.low)); const rng = allHi - allLo || 1; const y = (v) => padT + (1 - (v - allLo) / rng) * plotH; const slot = plotW / count; const bw = slot * 0.56; const grid = [0, 0.25, 0.5, 0.75, 1]; const lastClose = data[data.length - 1].close; const sparkData = data.map((d) => d.close); return ( {/* horizontal grid + price axis */} {grid.map((g, i) => { const yy = padT + g * plotH; const price = (allHi - g * rng); return ( {labels && {price.toFixed(0)}} ); })} {/* last price marker line */} {/* candles */} {data.map((d, i) => { const cx = i * slot + slot / 2; const col = d.up ? 'var(--up)' : 'var(--down)'; const yO = y(d.open), yC = y(d.close); const top = Math.min(yO, yC), bh = Math.max(2, Math.abs(yC - yO)); return ( ); })} ); } /* ---- Sentiment gauge (semicircle) ---- */ function Gauge({ value = 68, label = 'Optimistic' }) { const [ref, seen] = useInView(0.5); const r = 70, cx = 90, cy = 88, circ = Math.PI * r; const pct = Math.max(0, Math.min(100, value)) / 100; const dash = `${(circ * pct).toFixed(1)} ${circ.toFixed(1)}`; return (
{value} {label}
); } /* ---- Liquidity / volume bars ---- */ function LiquidityBars({ seed = 3, count = 16 }) { const [ref, seen] = useInView(0.4); const bars = React.useMemo(() => { const rnd = seeded(seed); return Array.from({ length: count }, (_, i) => ({ h: 24 + rnd() * 76, up: rnd() > 0.4 })); }, [seed, count]); return (
{bars.map((b, i) => ( ))}
); } /* ---- Donut (allocation) ---- */ function Donut({ segments }) { const [ref, seen] = useInView(0.5); const r = 34, c = 2 * Math.PI * r; let acc = 0; return ( {segments.map((s, i) => { const len = (s.v / 100) * c; const el = ; acc += len; return el; })} ); } /* ---- Ticker row data ---- */ const TICKER = [ { s: 'BTC', n: 'Bitcoin', p: 71204, c: 2.41, d: [60,62,61,64,63,66,68,67,70,71] }, { s: 'ETH', n: 'Ethereum', p: 3842, c: 1.12, d: [40,41,40,42,43,42,44,45,46,47] }, { s: 'SOL', n: 'Solana', p: 184.6, c: -0.84, d: [50,52,51,49,50,48,47,49,48,47] }, { s: 'RWA', n: 'Real-World Assets', p: 12.84, c: 4.27, d: [20,22,24,23,26,28,30,33,35,38] }, { s: 'LINK', n: 'Chainlink', p: 18.42, c: 0.96, d: [30,31,30,32,33,34,33,35,36,37] }, { s: 'ARB', n: 'Arbitrum', p: 1.04, c: -1.31, d: [44,43,42,43,41,40,41,39,40,38] }, { s: 'NFT', n: 'NFT Index', p: 842.3, c: 3.05, d: [55,57,56,59,61,60,63,65,67,69] }, ]; function TickerStrip() { const row = TICKER.concat(TICKER); return (
Live
{row.map((t, i) => ( {t.s} ${t.p.toLocaleString('en-US', { minimumFractionDigits: t.p < 100 ? 2 : 0 })} = 0 ? 'pos' : 'neg'}`}>{t.c >= 0 ? '+' : ''}{t.c.toFixed(2)}% ))}
); } Object.assign(window, { useInView, LiveNumber, Sparkline, CandleChart, Gauge, LiquidityBars, Donut, TickerStrip, TICKER, seeded, });