/* global React */ const { useState, useEffect, useMemo, useRef } = window.ReactHooks; // Deterministic pseudo-random (so charts are stable across renders) function seeded(seed) { let s = seed >>> 0; return () => { s = (s * 1664525 + 1013904223) >>> 0; return s / 0xffffffff; }; } // --- Animated line+area chart (hero) --- function LineChart({ width = 720, height = 380, seed = 17, live = true }) { const [t, setT] = useState(0); useEffect(() => { if (!live) return; let raf, start; const loop = (ts) => { if (!start) start = ts; setT((ts - start) / 1000); raf = requestAnimationFrame(loop); }; raf = requestAnimationFrame(loop); return () => cancelAnimationFrame(raf); }, [live]); const pad = { l: 44, r: 24, t: 16, b: 32 }; const w = width, h = height; const series = useMemo(() => { const rnd = seeded(seed); const N = 48; const arr = []; let v = 120 + rnd() * 40; for (let i = 0; i < N; i++) { v += (rnd() - 0.4) * 14; arr.push(Math.max(40, v)); } return arr; }, [seed]); const targets = useMemo(() => { return series.map((v, i) => v + 8 + Math.sin(i / 6) * 10); }, [series]); const maxV = Math.max(...series, ...targets) * 1.08; const minV = Math.min(...series, ...targets) * 0.9; // Moving reveal index const reveal = Math.min(series.length, Math.floor((t % 8) * 8)); const x = (i) => pad.l + (i / (series.length - 1)) * (w - pad.l - pad.r); const y = (v) => pad.t + (1 - (v - minV) / (maxV - minV)) * (h - pad.t - pad.b); const pathFor = (data, upTo) => { let d = ""; for (let i = 0; i <= upTo && i < data.length; i++) { d += (i === 0 ? "M" : "L") + x(i).toFixed(1) + " " + y(data[i]).toFixed(1) + " "; } return d; }; const areaFor = (data, upTo) => { if (upTo < 1) return ""; let d = "M" + x(0).toFixed(1) + " " + y(data[0]).toFixed(1) + " "; for (let i = 1; i <= upTo; i++) d += "L" + x(i).toFixed(1) + " " + y(data[i]).toFixed(1) + " "; d += "L" + x(upTo).toFixed(1) + " " + (h - pad.b) + " "; d += "L" + x(0).toFixed(1) + " " + (h - pad.b) + " Z"; return d; }; const gridY = 4; const xTicks = 6; return ( {/* Gridlines */} {Array.from({ length: gridY + 1 }).map((_, i) => { const yy = pad.t + (i / gridY) * (h - pad.t - pad.b); return ( {Math.round(maxV - (i / gridY) * (maxV - minV))} ); })} {/* X ticks */} {Array.from({ length: xTicks }).map((_, i) => { const idx = Math.round((i / (xTicks - 1)) * (series.length - 1)); const xx = x(idx); return ( {"W" + String(idx + 1).padStart(2, "0")} ); })} {/* Target (dashed) */} {/* Actual area */} {/* Last point marker */} {reveal > 0 && ( )} {/* Axis */} ); } // --- Bars chart (small) --- function BarChart({ width = 400, height = 200, seed = 3, accent = false }) { const rnd = useMemo(() => seeded(seed), [seed]); const N = 14; const data = useMemo(() => Array.from({ length: N }, () => 20 + Math.floor(rnd() * 90)), []); const max = Math.max(...data); const pad = { l: 8, r: 8, t: 10, b: 16 }; const bw = (width - pad.l - pad.r) / N - 3; return ( {data.map((v, i) => { const bh = ((v / max) * (height - pad.t - pad.b)); const xx = pad.l + i * ((width - pad.l - pad.r) / N); return ( ); })} ); } // --- Sparkline --- function Sparkline({ seed = 7, accent = true, width = 120, height = 36 }) { const rnd = useMemo(() => seeded(seed), [seed]); const N = 24; const d = useMemo(() => { const arr = []; let v = 50 + rnd() * 20; for (let i = 0; i < N; i++) { v += (rnd() - 0.5) * 16; arr.push(Math.max(10, Math.min(90, v))); } return arr; }, []); const max = Math.max(...d), min = Math.min(...d); const x = (i) => (i / (N - 1)) * (width - 2) + 1; const y = (v) => height - 2 - ((v - min) / (max - min + 0.001)) * (height - 4); let p = ""; d.forEach((v, i) => (p += (i === 0 ? "M" : "L") + x(i) + " " + y(v) + " ")); return ( ); } // --- Waterfall --- function Waterfall({ width = 520, height = 260 }) { const data = [ { l: "Q1 Rev", v: 100, type: "base" }, { l: "+ New biz", v: 28, type: "inc" }, { l: "+ Expansion", v: 14, type: "inc" }, { l: "− Churn", v: -9, type: "dec" }, { l: "− FX", v: -5, type: "dec" }, { l: "Q2 Rev", v: 128, type: "base" } ]; const pad = { l: 32, r: 16, t: 16, b: 40 }; const max = 150; const bw = (width - pad.l - pad.r) / data.length - 10; let run = 0; return ( {data.map((d, i) => { const xx = pad.l + i * ((width - pad.l - pad.r) / data.length); let y1, y2, fill; if (d.type === "base") { y1 = pad.t + (1 - d.v / max) * (height - pad.t - pad.b); y2 = height - pad.b; fill = "var(--fg)"; run = d.v; } else { const from = run; const to = run + d.v; const top = Math.max(from, to); const bot = Math.min(from, to); y1 = pad.t + (1 - top / max) * (height - pad.t - pad.b); y2 = pad.t + (1 - bot / max) * (height - pad.t - pad.b); fill = d.type === "inc" ? "var(--accent)" : "var(--fg)"; run = to; } return ( {d.l} {d.v > 0 && d.type !== "base" ? "+" : ""}{d.v} ); })} ); } // --- Heatmap --- function Heatmap({ cols = 20, rows = 7, width = 400, height = 180, seed = 11 }) { const rnd = useMemo(() => seeded(seed), [seed]); const grid = useMemo(() => { const g = []; for (let r = 0; r < rows; r++) { const row = []; for (let c = 0; c < cols; c++) { row.push(rnd()); } g.push(row); } return g; }, [seed, cols, rows]); const cw = width / cols; const ch = height / rows; return ( {grid.map((row, r) => row.map((v, c) => ( )) )} ); } // --- Small multiples --- function SmallMultiples({ width = 520, height = 260 }) { const panels = 8; const cols = 4, rows = 2; const pw = width / cols, ph = height / rows; return ( {Array.from({ length: panels }).map((_, i) => { const r = Math.floor(i / cols), c = i % cols; const rnd = seeded(i * 7 + 3); const N = 14; const data = Array.from({ length: N }, () => 20 + rnd() * 80); const max = Math.max(...data), min = Math.min(...data); const x = (j) => c * pw + 6 + (j / (N - 1)) * (pw - 12); const y = (v) => r * ph + 12 + (1 - (v - min) / (max - min)) * (ph - 24); let p = ""; data.forEach((v, j) => (p += (j === 0 ? "M" : "L") + x(j).toFixed(1) + " " + y(v).toFixed(1) + " ")); return ( {"Seg " + String(i + 1).padStart(2, "0")} ); })} ); } // --- Funnel --- function Funnel({ width = 400, height = 240 }) { const stages = [ { l: "Visitors", v: 100 }, { l: "Sign-ups", v: 62 }, { l: "Activated", v: 41 }, { l: "Paid", v: 17 } ]; const pad = 16; const rowH = (height - pad * 2) / stages.length; return ( {stages.map((s, i) => { const w = (s.v / 100) * (width - pad * 2); const xx = (width - w) / 2; const yy = pad + i * rowH + 3; return ( {s.l.toUpperCase() + " " + s.v + "%"} ); })} ); } // --- Choropleth placeholder (stylised grid map) --- function GridMap({ width = 520, height = 260, seed = 5 }) { const rnd = useMemo(() => seeded(seed), [seed]); const cols = 22, rows = 11; const cw = width / cols, ch = height / rows; return ( {Array.from({ length: rows * cols }).map((_, i) => { const r = Math.floor(i / cols), c = i % cols; const cx = c * cw + cw / 2, cy = r * ch + ch / 2; // rough continent-shaped mask const nx = (c - cols / 2) / (cols / 2); const ny = (r - rows / 2) / (rows / 2); const inside = (nx * nx * 0.7 + ny * ny * 1.4) < (0.55 + rnd() * 0.25); if (!inside) return null; const v = rnd(); return ( ); })} ); } // --- Decomposition tree --- function DecompTree({ width = 520, height = 260 }) { const root = { l: "Revenue", v: 128, x: 40, y: 130 }; const mids = [ { l: "NA", v: 72, y: 70 }, { l: "EU", v: 38, y: 130 }, { l: "APAC", v: 18, y: 200 } ]; const leafs = [ ["Enterprise", 46, 40], ["Mid-market", 26, 90], ["Enterprise", 22, 140], ["SMB", 16, 180], ["APAC total", 18, 220] ]; return ( ROOT Revenue · 128 {mids.map((m, i) => ( {m.l} {m.v} ))} {leafs.map((l, i) => ( {l[0]} · {l[1]} ))} ); } // --- Gauge --- function Gauge({ width = 220, height = 160, value = 72 }) { const cx = width / 2, cy = height - 18, r = 80; const arc = (from, to) => { const a1 = Math.PI + from * Math.PI; const a2 = Math.PI + to * Math.PI; const x1 = cx + Math.cos(a1) * r, y1 = cy + Math.sin(a1) * r; const x2 = cx + Math.cos(a2) * r, y2 = cy + Math.sin(a2) * r; return `M${x1},${y1} A${r},${r} 0 0 1 ${x2},${y2}`; }; return ( {value}% OF TARGET ); } // --- Network --- function Network({ width = 520, height = 260, seed = 4 }) { const rnd = useMemo(() => seeded(seed), [seed]); const N = 28; const nodes = useMemo(() => { return Array.from({ length: N }, (_, i) => ({ x: 40 + rnd() * (width - 80), y: 30 + rnd() * (height - 60), g: Math.floor(rnd() * 3), r: 3 + rnd() * 4 })); }, [seed]); const edges = useMemo(() => { const e = []; for (let i = 0; i < N; i++) { for (let j = i + 1; j < N; j++) { const d = Math.hypot(nodes[i].x - nodes[j].x, nodes[i].y - nodes[j].y); if (d < 90 && rnd() < 0.6) e.push([i, j]); } } return e; }, [nodes]); const colors = ["var(--fg)", "var(--accent)", "var(--muted)"]; return ( {edges.map(([a, b], i) => ( ))} {nodes.map((n, i) => ( ))} ); } // --- Table visual (ledger) --- function Ledger() { const rows = [ ["Americas", 46200, 0.12, 7], [" United States", 38800, 0.14, 8], [" Canada", 4900, 0.07, 4], [" Latam", 2500, -0.03, 3], ["EMEA", 31400, 0.05, 5], [" UK", 12100, 0.09, 6], [" DACH", 10200, 0.04, 5], [" Iberia", 4800, 0.18, 9], ["APAC", 16800, 0.22, 7] ]; return ( {rows.map((r, i) => { const top = !r[0].startsWith(" "); return ( ); })}
Segment Revenue YoY Trend
{r[0]} {r[1].toLocaleString()} {(r[2] * 100).toFixed(1)}%
); } // Route product id to a representative chart function ProductVisual({ id, big = false }) { const common = big ? { width: 720, height: 360 } : { width: 320, height: 140 }; switch (id) { case "horizon-kpi": return ; case "strata-waterfall": return ; case "atlas-small-multiples": return ; case "meridian-map": return ; case "cascade-funnel": return ; case "lattice-heatmap": return ; case "beacon-gauge": return ; case "prism-decomp": return ; case "orbit-network": return ; case "ledger-table": return big ? : ; default: return ; } } Object.assign(window, { LineChart, BarChart, Sparkline, Waterfall, Heatmap, SmallMultiples, Funnel, GridMap, DecompTree, Gauge, Network, Ledger, ProductVisual });