// Modern chart primitives — Apple/shadcn flavor // Smooth area charts, gradient fills, hairline axes, dotted grid const CH = window.TLB_TOKENS.color; // Catmull-Rom → cubic Bezier path function smoothPath(points) { if (points.length < 2) return ''; const d = [`M ${points[0][0]} ${points[0][1]}`]; for (let i = 0; i < points.length - 1; i++) { const p0 = points[i - 1] || points[i]; const p1 = points[i]; const p2 = points[i + 1]; const p3 = points[i + 2] || p2; const t = 0.18; const c1x = p1[0] + (p2[0] - p0[0]) * t; const c1y = p1[1] + (p2[1] - p0[1]) * t; const c2x = p2[0] - (p3[0] - p1[0]) * t; const c2y = p2[1] - (p3[1] - p1[1]) * t; d.push(`C ${c1x} ${c1y} ${c2x} ${c2y} ${p2[0]} ${p2[1]}`); } return d.join(' '); } // AreaChart — single or stacked series, gradient fill, dotted grid function AreaChart({ data, // [{label, ...keys}] keys = ['value'], height = 220, yTicks = 4, showAxis = true, formatY = (v) => v, formatX = (l) => l, highlightIdx = null, uniqueId, }) { const id = React.useMemo(() => uniqueId || `ch${Math.random().toString(36).slice(2, 8)}`, [uniqueId]); const w = 800; const h = height; const padL = showAxis ? 44 : 8; const padR = 12; const padT = 16; const padB = showAxis ? 28 : 8; const innerW = w - padL - padR; const innerH = h - padT - padB; const allVals = data.flatMap(d => keys.map(k => d[k] || 0)); const maxV = Math.max(...allVals) * 1.08; const minV = 0; const xStep = innerW / Math.max(1, data.length - 1); const xy = (i, v) => [padL + i * xStep, padT + innerH - ((v - minV) / (maxV - minV || 1)) * innerH]; const ticks = Array.from({ length: yTicks + 1 }, (_, i) => minV + (maxV - minV) * (i / yTicks)); return (
{keys.map((k, ki) => ( ))} {/* horizontal grid — dotted */} {ticks.map((t, i) => { const y = padT + innerH - ((t - minV) / (maxV - minV || 1)) * innerH; return ( {showAxis && {formatY(Math.round(t))}} ); })} {/* area + line per key */} {keys.map((k, ki) => { const pts = data.map((d, i) => xy(i, d[k] || 0)); const linePath = smoothPath(pts); const baseline = padT + innerH; const areaPath = `${linePath} L ${pts[pts.length-1][0]} ${baseline} L ${pts[0][0]} ${baseline} Z`; return ( ); })} {/* highlight dot */} {highlightIdx !== null && highlightIdx >= 0 && keys.map((k, ki) => { const [hx, hy] = xy(highlightIdx, data[highlightIdx][k] || 0); return ( ); })} {/* x-axis labels */} {showAxis && data.map((d, i) => { if (data.length > 12 && i % Math.ceil(data.length / 8) !== 0 && i !== data.length - 1) return null; const [x] = xy(i, 0); return {formatX(d.label)}; })}
); } // SparkArea — tiny inline area chart function SparkArea({ values, height = 40, width = 120 }) { const id = React.useMemo(() => `sp${Math.random().toString(36).slice(2, 8)}`, []); const max = Math.max(...values) * 1.05; const min = 0; const step = width / Math.max(1, values.length - 1); const pts = values.map((v, i) => [i * step, height - ((v - min) / (max - min || 1)) * (height - 4) - 2]); const linePath = smoothPath(pts); const areaPath = `${linePath} L ${pts[pts.length-1][0]} ${height} L 0 ${height} Z`; return ( ); } // Donut — minimal, single value function Donut({ value, size = 140, label, sub }) { const r = size / 2 - 8; const c = size / 2; const circ = 2 * Math.PI * r; return (
{label || `${value}%`}
{sub &&
{sub}
}
); } // Stacked horizontal bar — channel mix etc. function StackBar({ segments, height = 28 }) { const total = segments.reduce((a, s) => a + s.value, 0); return (
{segments.map((s, i) => { const tones = [CH.ink, CH.deep, CH.mid, CH.muted]; return (
{s.value/total > 0.12 ? `${s.label} · ${Math.round(s.value/total*100)}%` : ''}
); })}
); } // Money — modern figure rendering // - tabular nums // - smaller currency symbol, lifted, with tighter letter-spacing function Money({ value, currency = 'USD', size = 22, weight = 600, color, italic = false }) { const symbol = currency === 'USD' ? '$' : currency === 'IDR' ? 'Rp' : currency; const v = typeof value === 'number' ? value.toLocaleString(undefined, { maximumFractionDigits: 0 }) : value; return ( {symbol} {v} ); } Object.assign(window, { AreaChart, SparkArea, Donut, StackBar, Money });