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