// 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 (
);
}
// ---------- 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 (
{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 (
{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 (
{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 });
})();