// Chart primitives for Reports
// All pure SVG, no libs. Accent-aware, theme-aware.
// Exposes window.Charts

(function() {
  const { useMemo, useState } = React;

  // ---------- helpers ----------
  const fmt = {
    money: (n, c='USD') => {
      const abs = Math.abs(n);
      const sign = n < 0 ? '−' : '';
      if (c === 'BTC') return `${sign}₿${abs.toFixed(4)}`;
      if (c === 'ETH') return `${sign}Ξ${abs.toFixed(3)}`;
      if (abs >= 1e6) return `${sign}$${(abs/1e6).toFixed(2)}M`;
      if (abs >= 1e3) return `${sign}$${(abs/1e3).toFixed(1)}k`;
      return `${sign}$${abs.toFixed(0)}`;
    },
    pct: (n, d=1) => (n >= 0 ? '+' : '') + n.toFixed(d) + '%',
    k: (n) => {
      const abs = Math.abs(n);
      if (abs >= 1e6) return (n/1e6).toFixed(1) + 'M';
      if (abs >= 1e3) return (n/1e3).toFixed(0) + 'k';
      return n.toFixed(0);
    },
  };

  function niceTicks(min, max, count = 5) {
    const span = max - min || 1;
    const step0 = span / count;
    const mag = Math.pow(10, Math.floor(Math.log10(step0)));
    const norm = step0 / mag;
    const step = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag;
    const lo = Math.floor(min / step) * step;
    const hi = Math.ceil(max / step) * step;
    const ticks = [];
    for (let v = lo; v <= hi + step/2; v += step) ticks.push(+v.toFixed(10));
    return { ticks, min: lo, max: hi };
  }

  // ---------- Sparkline (inline) ----------
  function Spark({ data, w=120, h=28, color='var(--accent)', area=true, fill='var(--accent-bg)', thickness=1.2 }) {
    const min = Math.min(...data), max = Math.max(...data);
    const span = max - min || 1;
    const step = w / (data.length - 1);
    const pts = data.map((v, i) => [i * step, h - 2 - ((v - min)/span) * (h - 4)]);
    const d = pts.map(([x,y], i) => (i?'L':'M') + x + ' ' + y).join(' ');
    const fillD = d + ` L ${w} ${h} L 0 ${h} Z`;
    return (
      <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`} className="spark">
        {area && <path d={fillD} fill={fill} opacity="0.8" />}
        <path d={d} fill="none" stroke={color} strokeWidth={thickness} strokeLinejoin="round" strokeLinecap="round" />
      </svg>
    );
  }

  // ---------- Line / Area chart ----------
  function LineChart({ series, w=640, h=260, padL=56, padR=16, padT=18, padB=28, labels=null, area=false, yFmt=fmt.k, grid=true, yZero=false, legend=true }) {
    const allY = series.flatMap(s => s.data);
    const minY0 = Math.min(...allY);
    const maxY0 = Math.max(...allY);
    const { min: yMin, max: yMax, ticks } = niceTicks(yZero ? Math.min(0, minY0) : minY0, maxY0, 5);
    const span = yMax - yMin || 1;
    const n = series[0].data.length;
    const plotW = w - padL - padR;
    const plotH = h - padT - padB;
    const xOf = (i) => padL + (n <= 1 ? plotW/2 : (i/(n-1)) * plotW);
    const yOf = (v) => padT + plotH - ((v - yMin)/span) * plotH;

    return (
      <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" className="chart-fluid">
        {/* y-grid */}
        {grid && ticks.map((t, i) => (
          <g key={i}>
            <line x1={padL} x2={w-padR} y1={yOf(t)} y2={yOf(t)} stroke="var(--line)" strokeDasharray={t === 0 ? '0' : '1 3'} />
            <text x={padL-8} y={yOf(t)+3} textAnchor="end" fontSize="10" fill="var(--fg-2)" fontFamily="var(--font-mono)">{yFmt(t)}</text>
          </g>
        ))}
        {/* x labels */}
        {labels && labels.map((l, i) => (
          <text key={i} x={xOf(i)} y={h - padB + 14} textAnchor="middle" fontSize="10" fill="var(--fg-2)" fontFamily="var(--font-mono)">{l}</text>
        ))}
        {/* series */}
        {series.map((s, si) => {
          const d = s.data.map((v, i) => (i?'L':'M') + xOf(i) + ' ' + yOf(v)).join(' ');
          const fillD = d + ` L ${xOf(s.data.length-1)} ${yOf(yMin)} L ${xOf(0)} ${yOf(yMin)} Z`;
          return (
            <g key={si}>
              {area && <path d={fillD} fill={s.fill || s.color} opacity="0.15" />}
              <path d={d} fill="none" stroke={s.color} strokeWidth={s.width || 1.6} strokeLinejoin="round" strokeLinecap="round" strokeDasharray={s.dash || undefined} />
              {s.dots && s.data.map((v, i) => (
                <circle key={i} cx={xOf(i)} cy={yOf(v)} r="2.2" fill={s.color} />
              ))}
            </g>
          );
        })}
        {legend && series.length > 1 && (
          <g transform={`translate(${padL} ${padT - 10})`}>
            {series.map((s, i) => (
              <g key={i} transform={`translate(${i * 110} 0)`}>
                <rect width="10" height="2" y="-2" fill={s.color} />
                <text x="14" y="2" fontSize="10" fill="var(--fg-1)" fontFamily="var(--font-mono)" style={{textTransform:'uppercase',letterSpacing:'0.08em'}}>{s.name}</text>
              </g>
            ))}
          </g>
        )}
      </svg>
    );
  }

  // ---------- Bar chart (vertical) ----------
  function BarChart({ data, w=640, h=240, padL=48, padR=16, padT=14, padB=28, yFmt=fmt.k, color='var(--accent)', negColor='var(--neg)', labels=null, showValues=false }) {
    const yMax0 = Math.max(...data, 0);
    const yMin0 = Math.min(...data, 0);
    const { min: yMin, max: yMax, ticks } = niceTicks(yMin0, yMax0, 4);
    const span = yMax - yMin || 1;
    const plotW = w - padL - padR;
    const plotH = h - padT - padB;
    const bw = plotW / data.length * 0.68;
    const step = plotW / data.length;
    const yOf = (v) => padT + plotH - ((v - yMin)/span) * plotH;
    const zeroY = yOf(0);

    return (
      <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" className="chart-fluid">
        {ticks.map((t, i) => (
          <g key={i}>
            <line x1={padL} x2={w-padR} y1={yOf(t)} y2={yOf(t)} stroke="var(--line)" strokeDasharray={t === 0 ? '0' : '1 3'} />
            <text x={padL-8} y={yOf(t)+3} textAnchor="end" fontSize="10" fill="var(--fg-2)" fontFamily="var(--font-mono)">{yFmt(t)}</text>
          </g>
        ))}
        {data.map((v, i) => {
          const x = padL + i * step + (step - bw)/2;
          const top = Math.min(yOf(v), zeroY);
          const barH = Math.abs(yOf(v) - zeroY);
          return (
            <g key={i}>
              <rect x={x} y={top} width={bw} height={barH} fill={v < 0 ? negColor : color} opacity={0.9} />
              {labels && <text x={x + bw/2} y={h - padB + 14} textAnchor="middle" fontSize="10" fill="var(--fg-2)" fontFamily="var(--font-mono)">{labels[i]}</text>}
              {showValues && <text x={x + bw/2} y={top - 4} textAnchor="middle" fontSize="9" fill="var(--fg-1)" fontFamily="var(--font-mono)">{yFmt(v)}</text>}
            </g>
          );
        })}
      </svg>
    );
  }

  // ---------- Stacked bar chart ----------
  function StackedBars({ groups, keys, colors, w=640, h=260, padL=48, padR=16, padT=14, padB=28, yFmt=fmt.k, labels=null }) {
    const totals = groups.map(g => keys.reduce((s,k) => s + (g[k] || 0), 0));
    const yMax0 = Math.max(...totals, 0);
    const { min: yMin, max: yMax, ticks } = niceTicks(0, yMax0, 4);
    const span = yMax - yMin || 1;
    const plotW = w - padL - padR;
    const plotH = h - padT - padB;
    const bw = plotW / groups.length * 0.7;
    const step = plotW / groups.length;
    const yOf = (v) => padT + plotH - ((v - yMin)/span) * plotH;

    return (
      <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
        {ticks.map((t, i) => (
          <g key={i}>
            <line x1={padL} x2={w-padR} y1={yOf(t)} y2={yOf(t)} stroke="var(--line)" strokeDasharray="1 3" />
            <text x={padL-8} y={yOf(t)+3} textAnchor="end" fontSize="10" fill="var(--fg-2)" fontFamily="var(--font-mono)">{yFmt(t)}</text>
          </g>
        ))}
        {groups.map((g, i) => {
          const x = padL + i * step + (step - bw)/2;
          let acc = 0;
          return (
            <g key={i}>
              {keys.map((k, ki) => {
                const v = g[k] || 0;
                const y = yOf(acc + v);
                const h2 = yOf(acc) - y;
                acc += v;
                return <rect key={k} x={x} y={y} width={bw} height={h2} fill={colors[ki]} />;
              })}
              {labels && <text x={x + bw/2} y={h - padB + 14} textAnchor="middle" fontSize="10" fill="var(--fg-2)" fontFamily="var(--font-mono)">{labels[i]}</text>}
            </g>
          );
        })}
        <g transform={`translate(${padL} ${padT - 8})`}>
          {keys.map((k, i) => (
            <g key={k} transform={`translate(${i * 110} 0)`}>
              <rect width="10" height="10" y="-9" fill={colors[i]} />
              <text x="14" y="0" fontSize="10" fill="var(--fg-1)" fontFamily="var(--font-mono)" style={{textTransform:'uppercase',letterSpacing:'0.08em'}}>{k}</text>
            </g>
          ))}
        </g>
      </svg>
    );
  }

  // ---------- Waterfall (cash flow) ----------
  function Waterfall({ items, w=640, h=280, padL=64, padR=16, padT=20, padB=48, yFmt=fmt.k }) {
    // items: [{label, value, type}] type: 'start'|'end'|'pos'|'neg'
    let acc = 0;
    const bars = items.map(it => {
      if (it.type === 'start' || it.type === 'end') {
        const from = 0, to = it.value;
        acc = it.value;
        return { ...it, from, to, total: true };
      } else {
        const from = acc;
        acc += it.value;
        return { ...it, from, to: acc };
      }
    });
    const vals = bars.flatMap(b => [b.from, b.to, 0]);
    const yMax0 = Math.max(...vals);
    const yMin0 = Math.min(...vals);
    const { min: yMin, max: yMax, ticks } = niceTicks(yMin0, yMax0, 5);
    const span = yMax - yMin || 1;
    const plotW = w - padL - padR;
    const plotH = h - padT - padB;
    const step = plotW / bars.length;
    const bw = step * 0.65;
    const yOf = (v) => padT + plotH - ((v - yMin)/span) * plotH;

    return (
      <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
        {ticks.map((t, i) => (
          <g key={i}>
            <line x1={padL} x2={w-padR} y1={yOf(t)} y2={yOf(t)} stroke="var(--line)" strokeDasharray={t===0?'0':'1 3'} />
            <text x={padL-8} y={yOf(t)+3} textAnchor="end" fontSize="10" fill="var(--fg-2)" fontFamily="var(--font-mono)">{yFmt(t)}</text>
          </g>
        ))}
        {bars.map((b, i) => {
          const x = padL + i * step + (step - bw)/2;
          let top, h2, color;
          if (b.total) {
            top = yOf(Math.max(b.to, 0));
            h2 = Math.abs(yOf(b.to) - yOf(0));
            color = 'var(--fg-1)';
          } else {
            top = yOf(Math.max(b.from, b.to));
            h2 = Math.abs(yOf(b.from) - yOf(b.to));
            color = b.value >= 0 ? 'var(--pos)' : 'var(--neg)';
          }
          return (
            <g key={i}>
              <rect x={x} y={top} width={bw} height={h2} fill={color} opacity="0.9" />
              {/* connector */}
              {i < bars.length-1 && !bars[i+1].total && (
                <line x1={x+bw} x2={x+step} y1={yOf(b.to)} y2={yOf(b.to)} stroke="var(--line-2)" strokeDasharray="2 2" />
              )}
              <text x={x + bw/2} y={h - padB + 14} textAnchor="middle" fontSize="10" fill="var(--fg-1)" fontFamily="var(--font-mono)">{b.label}</text>
              <text x={x + bw/2} y={h - padB + 28} textAnchor="middle" fontSize="10" fill={b.total ? 'var(--fg-0)' : (b.value >= 0 ? 'var(--pos)' : 'var(--neg)')} fontFamily="var(--font-mono)" fontWeight="500">
                {b.total ? yFmt(b.to) : (b.value >= 0 ? '+' : '−') + yFmt(Math.abs(b.value))}
              </text>
            </g>
          );
        })}
      </svg>
    );
  }

  // ---------- Donut ----------
  function Donut({ slices, w=240, h=240, inner=0.58, center=null }) {
    const total = slices.reduce((a, s) => a + s.value, 0);
    const cx = w/2, cy = h/2, r = Math.min(w,h)/2 - 2;
    let a = -Math.PI/2;
    const parts = slices.map(s => {
      const frac = s.value/total;
      const a2 = a + frac * Math.PI * 2;
      const large = frac > 0.5 ? 1 : 0;
      const x1 = cx + r * Math.cos(a), y1 = cy + r * Math.sin(a);
      const x2 = cx + r * Math.cos(a2), y2 = cy + r * Math.sin(a2);
      const ri = r * inner;
      const xi1 = cx + ri * Math.cos(a2), yi1 = cy + ri * Math.sin(a2);
      const xi2 = cx + ri * Math.cos(a), yi2 = cy + ri * Math.sin(a);
      const d = `M ${x1} ${y1} A ${r} ${r} 0 ${large} 1 ${x2} ${y2} L ${xi1} ${yi1} A ${ri} ${ri} 0 ${large} 0 ${xi2} ${yi2} Z`;
      a = a2;
      return { ...s, d, frac };
    });
    return (
      <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
        {parts.map((p, i) => (
          <path key={i} d={p.d} fill={p.color} opacity="0.95" />
        ))}
        {center && (
          <g>
            <text x={cx} y={cy-6} textAnchor="middle" fontFamily="var(--font-mono)" fontSize="10" fill="var(--fg-2)" style={{textTransform:'uppercase',letterSpacing:'0.14em'}}>{center.label}</text>
            <text x={cx} y={cy+18} textAnchor="middle" fontFamily="var(--font-serif)" fontSize="22" fill="var(--fg-0)">{center.value}</text>
          </g>
        )}
      </svg>
    );
  }

  // ---------- Heatmap (cohort retention or day-of-week) ----------
  function Heatmap({ rows, cols, values, w=640, h=280, pad=40, colorMin='var(--bg-2)', colorMax='var(--accent)', fmt=v=>v, title }) {
    // values[rowIdx][colIdx]
    const flat = values.flat().filter(v => v != null);
    const mn = Math.min(...flat), mx = Math.max(...flat);
    const cellW = (w - pad*2) / cols.length;
    const cellH = (h - pad*2) / rows.length;
    const mix = (v) => {
      const t = mx === mn ? 0.5 : (v - mn)/(mx - mn);
      return `color-mix(in oklch, ${colorMax} ${Math.round(t*100)}%, ${colorMin})`;
    };
    return (
      <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
        {rows.map((r, ri) => (
          <g key={ri}>
            <text x={pad - 6} y={pad + ri*cellH + cellH/2 + 3} textAnchor="end" fontSize="10" fill="var(--fg-2)" fontFamily="var(--font-mono)">{r}</text>
            {cols.map((c, ci) => {
              const v = values[ri] && values[ri][ci];
              if (v == null) return null;
              return (
                <g key={ci}>
                  <rect x={pad + ci*cellW + 1} y={pad + ri*cellH + 1} width={cellW - 2} height={cellH - 2} fill={mix(v)} />
                  <text x={pad + ci*cellW + cellW/2} y={pad + ri*cellH + cellH/2 + 3} textAnchor="middle" fontSize="9.5" fill={((v - mn)/(mx-mn) > 0.55) ? 'var(--bg-0)' : 'var(--fg-1)'} fontFamily="var(--font-mono)">{fmt(v)}</text>
                </g>
              );
            })}
          </g>
        ))}
        {cols.map((c, ci) => (
          <text key={ci} x={pad + ci*cellW + cellW/2} y={pad - 8} textAnchor="middle" fontSize="10" fill="var(--fg-2)" fontFamily="var(--font-mono)">{c}</text>
        ))}
      </svg>
    );
  }

  // ---------- Treemap (simple binary splits) ----------
  function Treemap({ items, w=520, h=280 }) {
    // items: [{label, value, color}]
    const total = items.reduce((a,x) => a + x.value, 0);
    function layout(arr, x, y, w, h, horiz=true) {
      if (!arr.length) return [];
      if (arr.length === 1) return [{ ...arr[0], x, y, w, h }];
      const sum = arr.reduce((a,i) => a + i.value, 0);
      let acc = 0, results = [];
      // split into two halves by mass
      const half = sum/2;
      let splitIdx = 0;
      for (let i = 0; i < arr.length; i++) {
        acc += arr[i].value;
        if (acc >= half) { splitIdx = i + 1; break; }
      }
      if (splitIdx <= 0) splitIdx = 1;
      if (splitIdx >= arr.length) splitIdx = arr.length - 1;
      const left = arr.slice(0, splitIdx);
      const right = arr.slice(splitIdx);
      const leftSum = left.reduce((a,i) => a + i.value, 0);
      const frac = leftSum / sum;
      if (horiz) {
        const w1 = w * frac;
        return [
          ...layout(left, x, y, w1, h, !horiz),
          ...layout(right, x + w1, y, w - w1, h, !horiz),
        ];
      } else {
        const h1 = h * frac;
        return [
          ...layout(left, x, y, w, h1, !horiz),
          ...layout(right, x, y + h1, w, h - h1, !horiz),
        ];
      }
    }
    const sorted = [...items].sort((a,b) => b.value - a.value);
    const rects = layout(sorted, 0, 0, w, h, w > h);
    return (
      <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
        {rects.map((r, i) => (
          <g key={i}>
            <rect x={r.x+1} y={r.y+1} width={r.w-2} height={r.h-2} fill={r.color} opacity="0.9" />
            {r.w > 60 && r.h > 30 && (
              <g>
                <text x={r.x+8} y={r.y+16} fontSize="10.5" fill="var(--bg-0)" fontFamily="var(--font-mono)" style={{textTransform:'uppercase',letterSpacing:'0.08em'}}>{r.label}</text>
                <text x={r.x+8} y={r.y+34} fontSize="15" fill="var(--bg-0)" fontFamily="var(--font-serif)">{fmt.money(r.value)}</text>
                <text x={r.x+8} y={r.y+48} fontSize="9.5" fill="var(--bg-0)" opacity="0.7" fontFamily="var(--font-mono)">{((r.value/total)*100).toFixed(1)}%</text>
              </g>
            )}
            {r.w <= 60 && r.w > 25 && r.h > 20 && (
              <text x={r.x+4} y={r.y+14} fontSize="9" fill="var(--bg-0)" fontFamily="var(--font-mono)">{r.label}</text>
            )}
          </g>
        ))}
      </svg>
    );
  }

  // ---------- Candlestick ----------
  function Candles({ data, w=640, h=220, padL=56, padR=16, padT=10, padB=28, labels=null }) {
    // data: [{o,h,l,c}]
    const allY = data.flatMap(d => [d.h, d.l]);
    const { min: yMin, max: yMax, ticks } = niceTicks(Math.min(...allY), Math.max(...allY), 4);
    const span = yMax - yMin || 1;
    const plotW = w - padL - padR;
    const plotH = h - padT - padB;
    const step = plotW / data.length;
    const bw = step * 0.58;
    const yOf = v => padT + plotH - ((v - yMin)/span) * plotH;

    return (
      <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="xMidYMid meet" className="chart-fluid">
        {ticks.map((t, i) => (
          <g key={i}>
            <line x1={padL} x2={w-padR} y1={yOf(t)} y2={yOf(t)} stroke="var(--line)" strokeDasharray="1 3" />
            <text x={padL-8} y={yOf(t)+3} textAnchor="end" fontSize="10" fill="var(--fg-2)" fontFamily="var(--font-mono)">{fmt.k(t)}</text>
          </g>
        ))}
        {data.map((d, i) => {
          const x = padL + i * step + step/2;
          const up = d.c >= d.o;
          const color = up ? 'var(--pos)' : 'var(--neg)';
          const bodyTop = yOf(Math.max(d.o, d.c));
          const bodyH = Math.max(1, Math.abs(yOf(d.o) - yOf(d.c)));
          return (
            <g key={i}>
              <line x1={x} x2={x} y1={yOf(d.h)} y2={yOf(d.l)} stroke={color} />
              <rect x={x - bw/2} y={bodyTop} width={bw} height={bodyH} fill={color} opacity={up ? 0.85 : 1} />
              {labels && i % Math.ceil(data.length/8) === 0 && <text x={x} y={h - padB + 14} textAnchor="middle" fontSize="10" fill="var(--fg-2)" fontFamily="var(--font-mono)">{labels[i]}</text>}
            </g>
          );
        })}
      </svg>
    );
  }

  // ---------- Horizontal bars ----------
  function HBars({ items, w=420, h=null, rowH=22, xFmt=fmt.money, barColor='var(--accent)', max=null }) {
    const mx = max != null ? max : Math.max(...items.map(i => i.value));
    const H = h || items.length * rowH + 8;
    const labelW = 140;
    const valW = 64;
    const barArea = w - labelW - valW - 16;
    return (
      <svg width={w} height={H} viewBox={`0 0 ${w} ${H}`}>
        {items.map((it, i) => {
          const y = i * rowH + 4;
          const len = (it.value / mx) * barArea;
          return (
            <g key={i}>
              <text x={0} y={y + rowH/2 + 3} fontSize="11" fill="var(--fg-1)" fontFamily="var(--font-mono)" style={{textTransform:'uppercase',letterSpacing:'0.06em'}}>{it.label}</text>
              <rect x={labelW} y={y + 4} width={barArea} height={rowH - 10} fill="var(--bg-2)" />
              <rect x={labelW} y={y + 4} width={Math.max(1, len)} height={rowH - 10} fill={it.color || barColor} opacity="0.9" />
              <text x={w - 4} y={y + rowH/2 + 3} textAnchor="end" fontSize="11" fill="var(--fg-0)" fontFamily="var(--font-mono)">{xFmt(it.value, it.currency)}</text>
            </g>
          );
        })}
      </svg>
    );
  }

  // ---------- Progress ring ----------
  function Ring({ value, max=100, w=84, h=84, color='var(--accent)', track='var(--bg-3)', thickness=8, label, sublabel }) {
    const cx = w/2, cy = h/2;
    const r = Math.min(w,h)/2 - thickness/2 - 1;
    const circ = 2 * Math.PI * r;
    const pct = Math.min(1, value/max);
    return (
      <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
        <circle cx={cx} cy={cy} r={r} stroke={track} strokeWidth={thickness} fill="none" />
        <circle cx={cx} cy={cy} r={r} stroke={color} strokeWidth={thickness} fill="none" strokeDasharray={`${circ*pct} ${circ}`} strokeLinecap="butt" transform={`rotate(-90 ${cx} ${cy})`} />
        {label && <text x={cx} y={cy+2} textAnchor="middle" fontSize="14" fill="var(--fg-0)" fontFamily="var(--font-serif)">{label}</text>}
        {sublabel && <text x={cx} y={cy+16} textAnchor="middle" fontSize="9" fill="var(--fg-2)" fontFamily="var(--font-mono)" style={{textTransform:'uppercase',letterSpacing:'0.1em'}}>{sublabel}</text>}
      </svg>
    );
  }

  // ---------- Sankey (simplified 2-column) ----------
  function Sankey2({ left, right, flows, w=640, h=320 }) {
    // left, right: [{id,label,color}]
    // flows: [{from: leftId, to: rightId, value}]
    const lTot = left.map(l => flows.filter(f => f.from === l.id).reduce((a,f) => a + f.value, 0));
    const rTot = right.map(r => flows.filter(f => f.to === r.id).reduce((a,f) => a + f.value, 0));
    const totL = lTot.reduce((a,v) => a + v, 0);
    const totR = rTot.reduce((a,v) => a + v, 0);
    const pad = 10;
    const colW = 14;
    const gap = (h - pad*2) * 0.04;
    const lHeights = lTot.map(v => (v/totL) * (h - pad*2 - gap*(left.length-1)));
    const rHeights = rTot.map(v => (v/totR) * (h - pad*2 - gap*(right.length-1)));
    const lY = []; { let y = pad; lHeights.forEach(ht => { lY.push(y); y += ht + gap; }); }
    const rY = []; { let y = pad; rHeights.forEach(ht => { rY.push(y); y += ht + gap; }); }
    // cursors for where flows attach inside each node
    const lCur = lY.map(() => 0);
    const rCur = rY.map(() => 0);
    return (
      <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
        {/* flows */}
        {flows.map((f, i) => {
          const li = left.findIndex(l => l.id === f.from);
          const ri = right.findIndex(r => r.id === f.to);
          if (li < 0 || ri < 0) return null;
          const lh = (f.value / lTot[li]) * lHeights[li];
          const rh = (f.value / rTot[ri]) * rHeights[ri];
          const y0 = lY[li] + lCur[li]; lCur[li] += lh;
          const y1 = rY[ri] + rCur[ri]; rCur[ri] += rh;
          const x0 = colW + 4, x1 = w - colW - 4;
          const mx = (x0 + x1)/2;
          const d = `M ${x0} ${y0} C ${mx} ${y0}, ${mx} ${y1}, ${x1} ${y1} L ${x1} ${y1+rh} C ${mx} ${y1+rh}, ${mx} ${y0+lh}, ${x0} ${y0+lh} Z`;
          return <path key={i} d={d} fill={left[li].color} opacity="0.35" />;
        })}
        {/* nodes */}
        {left.map((l, i) => (
          <g key={'l'+i}>
            <rect x={4} y={lY[i]} width={colW} height={lHeights[i]} fill={l.color} />
            <text x={colW + 10} y={lY[i] + 12} fontSize="10" fill="var(--fg-0)" fontFamily="var(--font-mono)" style={{textTransform:'uppercase',letterSpacing:'0.08em'}}>{l.label}</text>
            <text x={colW + 10} y={lY[i] + 26} fontSize="10" fill="var(--fg-2)" fontFamily="var(--font-mono)">{fmt.money(lTot[i])}</text>
          </g>
        ))}
        {right.map((r, i) => (
          <g key={'r'+i}>
            <rect x={w - colW - 4} y={rY[i]} width={colW} height={rHeights[i]} fill={r.color} />
            <text x={w - colW - 10} y={rY[i] + 12} fontSize="10" textAnchor="end" fill="var(--fg-0)" fontFamily="var(--font-mono)" style={{textTransform:'uppercase',letterSpacing:'0.08em'}}>{r.label}</text>
            <text x={w - colW - 10} y={rY[i] + 26} fontSize="10" textAnchor="end" fill="var(--fg-2)" fontFamily="var(--font-mono)">{fmt.money(rTot[i])}</text>
          </g>
        ))}
      </svg>
    );
  }

  // ---------- Gauge ----------
  function Gauge({ value, min=0, max=100, w=200, h=120, color='var(--accent)', label, sublabel }) {
    const cx = w/2, cy = h - 12;
    const r = Math.min(w,h*2)/2 - 16;
    const t = Math.min(1, Math.max(0, (value - min)/(max - min)));
    const a1 = Math.PI;            // 180 left
    const a2 = a1 + t * Math.PI;   // sweep right
    const x1 = cx + r * Math.cos(a1), y1 = cy + r * Math.sin(a1);
    const x2 = cx + r * Math.cos(a2), y2 = cy + r * Math.sin(a2);
    const xe = cx + r * Math.cos(2*Math.PI), ye = cy + r * Math.sin(2*Math.PI);
    return (
      <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
        <path d={`M ${x1} ${y1} A ${r} ${r} 0 0 1 ${xe} ${ye}`} fill="none" stroke="var(--bg-3)" strokeWidth="10" />
        <path d={`M ${x1} ${y1} A ${r} ${r} 0 ${t>0.5?1:0} 1 ${x2} ${y2}`} fill="none" stroke={color} strokeWidth="10" strokeLinecap="butt" />
        <text x={cx} y={cy-10} textAnchor="middle" fontSize="24" fill="var(--fg-0)" fontFamily="var(--font-serif)">{label}</text>
        <text x={cx} y={cy+6} textAnchor="middle" fontSize="10" fill="var(--fg-2)" fontFamily="var(--font-mono)" style={{textTransform:'uppercase',letterSpacing:'0.14em'}}>{sublabel}</text>
      </svg>
    );
  }

  window.Charts = { Spark, LineChart, BarChart, StackedBars, Waterfall, Donut, Heatmap, Treemap, Candles, HBars, Ring, Sankey2, Gauge, fmt };
})();
