// SVG charts — sparkline, area, donut, bars. No external libs. (function () { const { useMemo, useState, useRef, useEffect } = React; // ---------- helpers ---------- function makePath(points, w, h, padding = 4) { if (!points.length) return ''; const max = Math.max(...points, 1); const min = Math.min(...points, 0); const range = Math.max(max - min, 1); const stepX = (w - padding * 2) / Math.max(points.length - 1, 1); return points.map((p, i) => { const x = padding + i * stepX; const y = padding + (h - padding * 2) * (1 - (p - min) / range); return (i === 0 ? 'M' : 'L') + x.toFixed(2) + ',' + y.toFixed(2); }).join(' '); } function makeAreaPath(points, w, h, padding = 4) { if (!points.length) return ''; const max = Math.max(...points, 1); const min = Math.min(...points, 0); const range = Math.max(max - min, 1); const stepX = (w - padding * 2) / Math.max(points.length - 1, 1); let d = ''; points.forEach((p, i) => { const x = padding + i * stepX; const y = padding + (h - padding * 2) * (1 - (p - min) / range); d += (i === 0 ? 'M' : 'L') + x.toFixed(2) + ',' + y.toFixed(2); }); d += ` L ${padding + (points.length - 1) * stepX},${h - padding} L ${padding},${h - padding} Z`; return d; } // ---------- Sparkline ---------- function Sparkline({ points, color = '#ffb547', height = 36, width = 120, fill = true }) { const id = useMemo(() => 'spk-' + Math.random().toString(36).slice(2, 9), []); return ( {fill && } ); } // ---------- Big area chart ---------- function AreaChart({ series, height = 280, labels, yFormat = v => v }) { // series: [{ name, color, points: [v0..vN] }] const wrapRef = useRef(null); const [w, setW] = useState(800); useEffect(() => { if (!wrapRef.current) return; const ro = new ResizeObserver(entries => { for (const e of entries) setW(Math.max(320, e.contentRect.width)); }); ro.observe(wrapRef.current); return () => ro.disconnect(); }, []); const [hover, setHover] = useState(null); // {i, x} const pad = { l: 56, r: 16, t: 16, b: 28 }; const innerW = w - pad.l - pad.r; const innerH = height - pad.t - pad.b; const n = series[0]?.points.length || 0; const maxAll = Math.max(1, ...series.flatMap(s => s.points)); // Round up to nice number const niceMax = niceCeil(maxAll); const stepX = innerW / Math.max(n - 1, 1); const ticks = 4; const yTicks = Array.from({ length: ticks + 1 }, (_, i) => (niceMax * i) / ticks); function xy(s, i) { const x = pad.l + i * stepX; const y = pad.t + innerH * (1 - s.points[i] / niceMax); return [x, y]; } function handleMove(e) { const rect = e.currentTarget.getBoundingClientRect(); const mx = e.clientX - rect.left - pad.l; const i = Math.round(mx / stepX); if (i >= 0 && i < n) setHover({ i, x: pad.l + i * stepX }); } return (
setHover(null)} style={{ cursor: 'crosshair' }}> {/* Y grid */} {yTicks.map((t, i) => { const y = pad.t + innerH * (1 - t / niceMax); return ( {yFormat(t)} ); })} {/* Defs */} {series.map((s, si) => ( ))} {/* Area fills */} {series.map((s, si) => { const pts = s.points.map((_, i) => xy(s, i)); const d = pts.map((p, i) => (i === 0 ? 'M' : 'L') + p[0] + ',' + p[1]).join(' ') + ` L ${pad.l + (n - 1) * stepX},${pad.t + innerH} L ${pad.l},${pad.t + innerH} Z`; return ; })} {/* Stroke lines */} {series.map((s, si) => { const pts = s.points.map((_, i) => xy(s, i)); const d = pts.map((p, i) => (i === 0 ? 'M' : 'L') + p[0] + ',' + p[1]).join(' '); return ; })} {/* X axis labels */} {labels && labels.map((lab, i) => { const x = pad.l + i * stepX; return {lab}; })} {/* Hover crosshair */} {hover && ( {series.map((s, si) => { const [x, y] = xy(s, hover.i); return ( ); })} )} {hover && (
{labels && labels[hover.i]}
{series.map((s, si) => (
{s.name} {yFormat(s.points[hover.i])}
))}
)}
); } function niceCeil(v) { if (v <= 0) return 1; const exp = Math.floor(Math.log10(v)); const base = Math.pow(10, exp); const n = v / base; let nice; if (n <= 1) nice = 1; else if (n <= 2) nice = 2; else if (n <= 5) nice = 5; else nice = 10; return nice * base; } // ---------- Bar chart (vertical bars for stock movement) ---------- function BarChart({ data, height = 220, palette }) { // data: [{ label, valueIn, valueOut }] const wrapRef = useRef(null); const [w, setW] = useState(800); useEffect(() => { if (!wrapRef.current) return; const ro = new ResizeObserver(entries => { for (const e of entries) setW(Math.max(320, e.contentRect.width)); }); ro.observe(wrapRef.current); return () => ro.disconnect(); }, []); const pad = { l: 32, r: 12, t: 12, b: 28 }; const innerW = w - pad.l - pad.r; const innerH = height - pad.t - pad.b; const max = Math.max(1, ...data.flatMap(d => [d.valueIn, d.valueOut])); const niceMax = niceCeil(max); const groupW = innerW / data.length; const barW = Math.max(2, Math.min(14, groupW * 0.32)); const [hover, setHover] = useState(null); return (
{[0, 0.5, 1].map((t, i) => { const y = pad.t + innerH * (1 - t); return ; })} {data.map((d, i) => { const cx = pad.l + groupW * (i + 0.5); const hIn = (d.valueIn / niceMax) * innerH; const hOut = (d.valueOut / niceMax) * innerH; return ( setHover(i)} onMouseLeave={() => setHover(null)} style={{ cursor: 'pointer' }}> ); })} {data.filter((_, i) => i % Math.max(1, Math.floor(data.length / 6)) === 0).map((d, idx) => { const i = data.indexOf(d); const cx = pad.l + groupW * (i + 0.5); return {d.label}; })} {hover !== null && (
{data[hover].label}
รับเข้า: {data[hover].valueIn}
จ่ายออก: {data[hover].valueOut}
)}
); } // ---------- Donut ---------- function Donut({ segments, size = 160, thickness = 18, centerLabel, centerValue }) { const r = size / 2 - thickness / 2 - 2; const c = 2 * Math.PI * r; const total = segments.reduce((s, v) => s + v.value, 0) || 1; let acc = 0; return (
{segments.map((seg, i) => { const len = (seg.value / total) * c; const dash = `${len} ${c - len}`; const offset = -acc; acc += len; return ( ); })}
{centerLabel}
{centerValue}
); } // ---------- Animated counter ---------- function Counter({ value, format = v => v, duration = 900 }) { const [v, setV] = useState(0); const startRef = useRef(null); const fromRef = useRef(0); useEffect(() => { fromRef.current = v; startRef.current = null; let raf; function tick(t) { if (!startRef.current) startRef.current = t; const p = Math.min(1, (t - startRef.current) / duration); const eased = 1 - Math.pow(1 - p, 3); setV(fromRef.current + (value - fromRef.current) * eased); if (p < 1) raf = requestAnimationFrame(tick); } raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); // eslint-disable-next-line }, [value]); return {format(v)}; } Object.assign(window, { Sparkline, AreaChart, BarChart, Donut, Counter }); })();