/* ============================================================
   EMM — SVG chart primitives (React, no external libs)
   Colors come from CSS vars: --pos --neu --neg --accent
   --line (grid) --ink --muted --surface
   Exports to window at the end.
   ============================================================ */
(function () {
  "use strict";
  const { useState, useRef, useMemo, useEffect } = React;
  const C = window.EMM.fmt;

  const SENT = { pos: "var(--pos)", neu: "var(--neu)", neg: "var(--neg)" };

  // ============================================================
  //  ECharts engine — reusable React wrapper + theme from CSS vars
  // ============================================================
  function cssvar(name, fallback) {
    const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
    return v || fallback;
  }
  // ECharts' colour parser cannot read oklch(); resolve any CSS colour to rgba()
  // by painting it on a 1px canvas and reading the pixel back (the browser canvas
  // understands oklch). Cached. This is what keeps visualMap/gradients from going black.
  const _rgbCache = {};
  let _probeCtx = null;
  function toRgba(c, alpha) {
    if (c == null) c = "#000";
    const key = c + "|" + (alpha == null ? "" : alpha);
    if (_rgbCache[key]) return _rgbCache[key];
    if (!_probeCtx) { const cv = document.createElement("canvas"); cv.width = cv.height = 1; _probeCtx = cv.getContext("2d", { willReadFrequently: true }); }
    _probeCtx.clearRect(0, 0, 1, 1);
    _probeCtx.fillStyle = "#000";
    _probeCtx.fillStyle = c;            // invalid → stays #000
    _probeCtx.fillRect(0, 0, 1, 1);
    const d = _probeCtx.getImageData(0, 0, 1, 1).data;
    const out = `rgba(${d[0]},${d[1]},${d[2]},${alpha != null ? alpha : (d[3] / 255).toFixed(3)})`;
    _rgbCache[key] = out;
    return out;
  }
  // theme pulls live values from the CSS custom properties (resolved to rgb), so
  // light/dark + accent switching flow into the charts for free.
  function chartTheme() {
    const accentVar = cssvar("--accent", "#1b3a6b");
    return {
      accent: toRgba(accentVar),
      ramp: (a) => toRgba(accentVar, a),
      pos: toRgba(cssvar("--pos", "#2f8f5b")),
      neu: toRgba(cssvar("--neu", "#8a93a0")),
      neg: toRgba(cssvar("--neg", "#c0453a")),
      gold: toRgba(cssvar("--gold", "#b8893b")),
      ink: toRgba(cssvar("--ink", "#26303f")),
      ink2: toRgba(cssvar("--ink-2", "#3c4757")),
      muted: toRgba(cssvar("--muted", "#6b7686")),
      faint: toRgba(cssvar("--faint", "#9aa3b0")),
      line: toRgba(cssvar("--line", "#e4e8ee")),
      line2: toRgba(cssvar("--line-2", "#d3d9e2")),
      surface: toRgba(cssvar("--surface", "#ffffff")),
      surface2: toRgba(cssvar("--surface-2", "#f8fafc")),
      rgb: toRgba,
      font: '"IBM Plex Sans","IBM Plex Sans Devanagari",system-ui,sans-serif',
      mono: '"IBM Plex Mono",ui-monospace,monospace',
    };
  }
  // base ECharts skeleton shared by all charts (fonts, tooltip, grid colours)
  function baseOption(th) {
    return {
      textStyle: { fontFamily: th.font, color: th.ink2 },
      animationDuration: 320,
      tooltip: {
        backgroundColor: th.surface, borderColor: th.line2, borderWidth: 1,
        textStyle: { color: th.ink, fontFamily: th.font, fontSize: 12 },
        extraCssText: "box-shadow:0 6px 20px rgba(15,20,30,.12);border-radius:6px;",
      },
    };
  }

  function EChart({ option, height = 220, onEvents, className }) {
    const ref = useRef(null);
    const inst = useRef(null);
    useEffect(() => {
      const el = ref.current;
      const chart = echarts.init(el, null, { renderer: "canvas" });
      inst.current = chart;
      const ro = new ResizeObserver(() => chart.resize());
      ro.observe(el);
      return () => { ro.disconnect(); chart.dispose(); inst.current = null; };
    }, []);
    useEffect(() => {
      const chart = inst.current;
      if (!chart) return;
      chart.setOption(option, true);
      chart.off("click");
      if (onEvents) Object.keys(onEvents).forEach((ev) => chart.on(ev, onEvents[ev]));
    });
    return <div ref={ref} className={"emm-echart " + (className || "")} style={{ width: "100%", height }} />;
  }

  // ---------- Sparkline — ECharts mini line -------------------
  function Sparkline({ values, width = 96, height = 28, color = "var(--accent)", fill = true }) {
    if (!values || !values.length) return null;
    const th = chartTheme();
    const c = color && color.indexOf("var(") === 0 ? ({ "var(--accent)": th.accent, "var(--pos)": th.pos, "var(--neg)": th.neg, "var(--neu)": th.neu }[color] || th.accent) : (color || th.accent);
    const option = {
      animation: false,
      grid: { left: 1, right: 2, top: 2, bottom: 1 },
      xAxis: { type: "category", show: false, boundaryGap: false, data: values.map((_, i) => i) },
      yAxis: { type: "value", show: false, min: "dataMin", max: "dataMax" },
      series: [{ type: "line", data: values, showSymbol: false, lineStyle: { color: c, width: 1.5 }, itemStyle: { color: c },
        areaStyle: fill ? { color: c, opacity: 0.12 } : undefined,
        endLabel: { show: false }, markPoint: { symbol: "circle", symbolSize: 4, data: [{ coord: [values.length - 1, values[values.length - 1]] }], itemStyle: { color: c }, label: { show: false } } }],
    };
    return <div style={{ width, height, flex: "0 0 auto" }}><EChart option={option} height={height} /></div>;
  }

  // ---------- Sentiment meter — ECharts stacked bar ----------
  function SentimentMeter({ pos = 0, neu = 0, neg = 0, height = 10, showPct = false, gap = 1.5, radius = 0 }) {
    const th = chartTheme();
    const total = pos + neu + neg || 1;
    const option = {
      animation: false,
      grid: { left: 0, right: 0, top: 0, bottom: showPct ? 16 : 0 },
      xAxis: { type: "value", show: false, max: total },
      yAxis: { type: "category", show: false, data: ["s"] },
      series: [
        { name: "pos", type: "bar", stack: "s", data: [pos], itemStyle: { color: th.pos }, barWidth: height },
        { name: "neu", type: "bar", stack: "s", data: [neu], itemStyle: { color: th.neu }, barWidth: height },
        { name: "neg", type: "bar", stack: "s", data: [neg], itemStyle: { color: th.neg }, barWidth: height },
      ],
    };
    return (
      <div style={{ width: "100%" }}>
        <div style={{ height }}><EChart option={option} height={height} /></div>
        {showPct && (
          <div className="mono" style={{ display: "flex", justifyContent: "space-between", marginTop: 4, fontSize: 11 }}>
            <span style={{ color: th.pos }}>{Math.round((pos / total) * 100)}%</span>
            <span style={{ color: th.neu }}>{Math.round((neu / total) * 100)}%</span>
            <span style={{ color: th.neg }}>{Math.round((neg / total) * 100)}%</span>
          </div>
        )}
      </div>
    );
  }

  // ---------- Trend chart (area/line, optional sentiment split) — ECharts
  function TrendChart({ series, valueFn, height = 220, split = false, accent = "var(--accent)",
                        onPoint, scrubDay = null, labelEvery = 5 }) {
    const th = chartTheme();
    const { t } = window.useEmmT();
    const acc = (!accent || accent.indexOf("var(") === 0) ? th.accent : accent;
    const x = series.map((s) => "D" + (s.day + 1));
    const base = baseOption(th);
    const axis = {
      xAxis: { type: "category", data: x, boundaryGap: split, axisTick: { show: false },
        axisLine: { lineStyle: { color: th.line2 } },
        axisLabel: { color: th.muted, fontFamily: th.mono, fontSize: 10, interval: (i) => i % labelEvery === 0 || i === x.length - 1 } },
      yAxis: { type: "value", splitLine: { lineStyle: { color: th.line } },
        axisLabel: { color: th.muted, fontFamily: th.mono, fontSize: 10, formatter: (v) => C.compact(v) } },
    };
    let option;
    if (split) {
      const mk = (k, color, name) => ({ name, type: "line", stack: "s", areaStyle: { color, opacity: 0.85 },
        lineStyle: { width: 0 }, symbol: "none", emphasis: { focus: "series" }, data: series.map((s) => s[k]) });
      option = { ...base, tooltip: { ...base.tooltip, trigger: "axis" },
        grid: { left: 4, right: 10, top: 12, bottom: 22, containLabel: true }, ...axis,
        series: [mk("pos", th.pos, t("positive")), mk("neu", th.neu, t("neutral")), mk("neg", th.neg, t("negative"))] };
    } else {
      const vals = series.map((s) => (valueFn ? valueFn(s) : s.count));
      option = { ...base,
        tooltip: { ...base.tooltip, trigger: "axis", formatter: (ps) => `${ps[0].axisValue}<br/><b>${C.compact(ps[0].data)}</b>` },
        grid: { left: 4, right: 12, top: 12, bottom: 22, containLabel: true }, ...axis,
        series: [{ type: "line", data: vals, showSymbol: false, symbolSize: 6,
          lineStyle: { color: acc, width: 2 }, itemStyle: { color: acc }, areaStyle: { color: acc, opacity: 0.10 },
          markLine: scrubDay != null ? { silent: true, symbol: "none", lineStyle: { color: acc, type: "dashed", opacity: 0.5 }, data: [{ xAxis: "D" + (scrubDay + 1) }] } : undefined }] };
    }
    const onEvents = onPoint ? { click: (p) => { const d = series[p.dataIndex]; if (d) onPoint(d.day); } } : null;
    return <EChart option={option} height={height} onEvents={onEvents} />;
  }

  // ---------- Horizontal bars (leaderboard / breakdown) — ECharts
  function BarsH({ items, valueFn, max, labelFn, subFn, onClick, activeId, color = "var(--accent)",
                  sentiment = false, height = 30, showValue = true, rank = false }) {
    const th = chartTheme();
    const acc = color && color.indexOf("var(") === 0 ? th.accent : (color || th.accent);
    const valueOf = (it) => valueFn ? valueFn(it) : (it.count || 0);
    const labelOf = (it) => { const l = labelFn ? labelFn(it) : null; return (typeof l === "string") ? l : (it.name || it.id || ""); };
    const cats = items.map((it, i) => (rank ? String(i + 1).padStart(2, "0") + "  " : "") + labelOf(it));
    const base = baseOption(th);
    let series;
    if (sentiment) {
      const mk = (k, c) => ({ name: k, type: "bar", stack: "s", barMaxWidth: 16, itemStyle: { color: c }, data: items.map((it) => it[k] || 0) });
      series = [mk("pos", th.pos), mk("neu", th.neu), mk("neg", th.neg)];
    } else {
      series = [{ type: "bar", barMaxWidth: 16,
        data: items.map((it) => ({ value: valueOf(it), itemStyle: { color: acc, opacity: (activeId && it.id !== activeId) ? 0.5 : 1 } })),
        label: showValue ? { show: true, position: "right", formatter: (p) => C.compact(p.value), color: th.muted, fontFamily: th.mono, fontSize: 10 } : undefined }];
    }
    const option = {
      ...base,
      tooltip: { ...base.tooltip, trigger: "axis", axisPointer: { type: "shadow" },
        formatter: (ps) => { const it = items[ps[0].dataIndex];
          const sub = subFn ? `<br/><span style="color:${th.muted};font-size:11px">${subFn(it)}</span>` : "";
          return `${labelOf(it)}: <b>${C.compact(valueOf(it))}</b>${sub}`; } },
      grid: { left: 4, right: (showValue && !sentiment) ? 46 : 10, top: 4, bottom: 4, containLabel: true },
      xAxis: { type: "value", show: false },
      yAxis: { type: "category", inverse: true, data: cats, axisLine: { show: false }, axisTick: { show: false },
        axisLabel: { color: th.ink2, fontFamily: th.font, fontSize: 11.5, width: 168, overflow: "truncate" } },
      series,
    };
    const onEvents = onClick ? { click: (p) => onClick(items[p.dataIndex]) } : null;
    const per = height && height < 22 ? 26 : 33;
    return <EChart option={option} height={Math.max(70, items.length * per + 12)} onEvents={onEvents} />;
  }

  // ---------- Heatmap (topic × day) — ECharts -----------------
  function Heatmap({ rows, valueFn, onCell, labelFn, colLabelEvery = 5, activeTopic }) {
    const th = chartTheme();
    const yLabels = rows.map((r) => labelFn(r.topic));
    const xLabels = rows[0] ? rows[0].cells.map((c) => "D" + (c.day + 1)) : [];
    let maxV = 1;
    const data = [];
    rows.forEach((r, yi) => r.cells.forEach((c, xi) => {
      const v = valueFn(c); if (v > maxV) maxV = v;
      data.push([xi, yi, v]);
    }));
    const base = baseOption(th);
    const option = {
      ...base,
      tooltip: { ...base.tooltip, position: "top",
        formatter: (p) => `${yLabels[p.value[1]]} · ${xLabels[p.value[0]]}<br/><b>${C.compact(p.value[2])}</b>` },
      grid: { left: 2, right: 8, top: 8, bottom: 60, containLabel: true },
      xAxis: { type: "category", data: xLabels, splitArea: { show: false },
        axisLine: { lineStyle: { color: th.line2 } }, axisTick: { show: false },
        axisLabel: { color: th.muted, fontFamily: th.mono, fontSize: 10, interval: (i) => i % colLabelEvery === 0 || i === xLabels.length - 1 } },
      yAxis: { type: "category", data: yLabels, inverse: true, splitArea: { show: false },
        axisLine: { show: false }, axisTick: { show: false },
        axisLabel: { color: th.ink2, fontFamily: th.font, fontSize: 11.5 } },
      visualMap: { type: "continuous", min: 0, max: maxV, calculable: true, orient: "horizontal",
        left: "center", bottom: 8, itemWidth: 12, itemHeight: 140,
        text: ["high", "low"], textStyle: { color: th.muted, fontFamily: th.mono, fontSize: 10 },
        inRange: { color: [th.surface2, th.ramp(0.4), th.accent] } },
      series: [{ type: "heatmap", data,
        itemStyle: { borderColor: th.surface, borderWidth: 2 },
        emphasis: { itemStyle: { borderColor: th.ink, borderWidth: 1.5 } } }],
    };
    const onEvents = onCell ? { click: (p) => { const r = rows[p.value[1]]; onCell(r.topic, r.cells[p.value[0]]); } } : null;
    return <EChart option={option} height={Math.max(190, rows.length * 30 + 78)} onEvents={onEvents} />;
  }

  // ---------- Daypart grid (daypart × day-of-week) — ECharts --
  const DOW = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
  function DaypartGrid({ grid, onCell, labelKey = "daypart" }) {
    const th = chartTheme();
    const yLabels = grid.map((r) => r[labelKey]);
    let maxV = 1;
    const data = [];
    grid.forEach((r, yi) => r.cells.forEach((v, xi) => { if (v > maxV) maxV = v; data.push([xi, yi, v]); }));
    const base = baseOption(th);
    const option = {
      ...base,
      tooltip: { ...base.tooltip, position: "top", formatter: (p) => `${yLabels[p.value[1]]} · ${DOW[p.value[0]]}<br/><b>${p.value[2]}</b>` },
      grid: { left: 2, right: 8, top: 26, bottom: 52, containLabel: true },
      xAxis: { type: "category", data: DOW, position: "top", axisLine: { show: false }, axisTick: { show: false },
        axisLabel: { color: th.muted, fontFamily: th.mono, fontSize: 10 } },
      yAxis: { type: "category", data: yLabels, inverse: true, axisLine: { show: false }, axisTick: { show: false },
        axisLabel: { color: th.ink2, fontSize: 11 } },
      visualMap: { type: "continuous", min: 0, max: maxV, calculable: true, orient: "horizontal", left: "center", bottom: 6,
        itemHeight: 130, text: ["high", "low"], textStyle: { color: th.muted, fontFamily: th.mono, fontSize: 10 },
        inRange: { color: [th.surface2, th.ramp(0.45), th.accent] } },
      series: [{ type: "heatmap", data,
        label: { show: true, fontFamily: th.mono, fontSize: 10, color: (p) => p.value[2] > maxV * 0.55 ? "#fff" : th.ink2, formatter: (p) => p.value[2] || "" },
        itemStyle: { borderColor: th.surface, borderWidth: 2 } }],
    };
    const onEvents = onCell ? { click: (p) => onCell(grid[p.value[1]], p.value[0]) } : null;
    return <EChart option={option} height={grid.length * 40 + 84} onEvents={onEvents} />;
  }

  // ---------- Choropleth (representative CG district grid) ----
  // Stylised hex/block layout of Chhattisgarh divisions — labelled as representative.
  const CG_DISTRICTS = [
    { id: "surguja", n: "Surguja", x: 4, y: 0 }, { id: "korea", n: "Korea", x: 3, y: 0 },
    { id: "raigarh", n: "Raigarh", x: 5, y: 1 }, { id: "korba", n: "Korba", x: 4, y: 1 },
    { id: "bilaspur", n: "Bilaspur", x: 3, y: 2 }, { id: "mungeli", n: "Mungeli", x: 2, y: 2 },
    { id: "kabirdham", n: "Kabirdham", x: 1, y: 2 }, { id: "raipur", n: "Raipur", x: 3, y: 3 },
    { id: "durg", n: "Durg", x: 2, y: 3 }, { id: "rajnandgaon", n: "Rajnandgaon", x: 1, y: 3 },
    { id: "balod", n: "Balod", x: 2, y: 4 }, { id: "dhamtari", n: "Dhamtari", x: 3, y: 4 },
    { id: "gariaband", n: "Gariaband", x: 4, y: 4 }, { id: "kanker", n: "Kanker", x: 2, y: 5 },
    { id: "kondagaon", n: "Kondagaon", x: 3, y: 5 }, { id: "bastar", n: "Bastar", x: 3, y: 6 },
    { id: "narayanpur", n: "Narayanpur", x: 2, y: 6 }, { id: "dantewada", n: "Dantewada", x: 3, y: 7 },
    { id: "sukma", n: "Sukma", x: 3, y: 8 }, { id: "bijapur", n: "Bijapur", x: 2, y: 7 },
  ];
  // ECharts tile-map (district grid) — visualMap legend + tooltip + click.
  // (A real geo map needs CG district GeoJSON; this representative tile layout
  //  keeps the spatial arrangement while gaining a colour-scale legend.)
  function Choropleth({ valueById = {}, onCell, cell = 46 }) {
    const th = chartTheme();
    const maxV = Math.max(...Object.values(valueById), 1) || 1;
    const maxX = Math.max(...CG_DISTRICTS.map((d) => d.x));
    const maxY = Math.max(...CG_DISTRICTS.map((d) => d.y));
    const data = CG_DISTRICTS.map((d) => {
      const v = valueById[d.id] != null ? valueById[d.id] : (d.id.charCodeAt(0) % 9) / 9 * maxV;
      return { value: [d.x, -d.y, Math.round(v)], name: d.n, id: d.id };
    });
    const base = baseOption(th);
    const option = {
      ...base,
      tooltip: { ...base.tooltip, formatter: (p) => `${p.data.name}<br/><b>${C.compact(p.data.value[2])}</b>` },
      grid: { left: 6, right: 6, top: 8, bottom: 48, containLabel: true },
      xAxis: { type: "value", min: -0.6, max: maxX + 0.6, show: false },
      yAxis: { type: "value", min: -maxY - 0.6, max: 0.6, show: false },
      visualMap: { type: "continuous", min: 0, max: maxV, calculable: true, orient: "horizontal", left: "center", bottom: 6,
        dimension: 2, itemHeight: 130, text: ["high", "low"], textStyle: { color: th.muted, fontFamily: th.mono, fontSize: 10 },
        inRange: { color: [th.surface2, th.ramp(0.4), th.accent] } },
      series: [{ type: "scatter", data, symbol: "rect", symbolSize: cell - 8,
        label: { show: true, formatter: (p) => p.data.name.slice(0, 4), color: th.ink2, fontFamily: th.mono, fontSize: 8 },
        itemStyle: { borderColor: th.surface, borderWidth: 2 } }],
    };
    const onEvents = onCell ? { click: (p) => onCell(CG_DISTRICTS.find((d) => d.id === p.data.id)) } : null;
    return <EChart option={option} height={(maxY + 1) * 44 + 60} onEvents={onEvents} />;
  }

  // ---------- Matrix / pivot grid — ECharts heatmap -----------
  function MatrixGrid({ data, lang, colourBy = "intensity", onCell }) {
    const { t } = window.useEmmT();
    const L = window.emmL;
    const th = chartTheme();
    const { rows, cols, cells, colTot, grand, metric } = data;
    const fmtV = metric === "reach" ? C.compact : metric === "duration" ? C.dur : (v) => String(v);
    const xLabels = cols.map((c) => L(c.label, lang));
    const yLabels = rows.map((r) => L(r.label, lang));
    let maxV = 1;
    const meta = [];
    rows.forEach((r, yi) => cols.forEach((c, xi) => {
      const cell = cells[r.id] && cells[r.id][c.id];
      const v = cell ? cell.v : 0; if (v > maxV) maxV = v;
      let color = null;
      if (cell && v) {
        if (colourBy === "sentiment") { const tot = cell.pos + cell.neu + cell.neg || 1; color = cell.neg / tot > 0.4 ? th.neg : cell.pos / tot > 0.5 ? th.pos : th.neu; }
        else if (colourBy === "authenticity") { const fk = cell.fake / (cell.n || 1); color = fk > 0.22 ? th.neg : fk > 0.07 ? th.gold : th.pos; }
      }
      meta.push({ xi, yi, v, color });
    }));
    const useVisual = colourBy === "intensity";
    const seriesData = meta.map((m) => {
      if (useVisual) return [m.xi, m.yi, m.v];                 // visualMap colours (incl. 0 → surface)
      if (!m.v) return { value: [m.xi, m.yi, 0], itemStyle: { color: th.surface2 } };  // empty cell, not default blue
      return { value: [m.xi, m.yi, m.v], itemStyle: { color: m.color || th.accent, opacity: 0.22 + 0.7 * Math.pow(m.v / maxV, 0.7) } };
    });
    const base = baseOption(th);
    const option = {
      ...base,
      tooltip: { ...base.tooltip, position: "top", formatter: (p) => `${yLabels[p.value[1]]} × ${xLabels[p.value[0]]}<br/><b>${fmtV(p.value[2])}</b>` },
      grid: { left: 2, right: 8, top: 36, bottom: useVisual ? 52 : 12, containLabel: true },
      xAxis: { type: "category", data: xLabels, position: "top", axisLine: { show: false }, axisTick: { show: false },
        axisLabel: { color: th.ink2, fontSize: 10.5, interval: 0, rotate: xLabels.length > 6 ? 22 : 0 } },
      yAxis: { type: "category", data: yLabels, inverse: true, axisLine: { show: false }, axisTick: { show: false },
        axisLabel: { color: th.ink, fontWeight: 600, fontSize: 11.5 } },
      series: [{ type: "heatmap", data: seriesData,
        label: { show: true, fontFamily: th.mono, fontSize: 10.5, color: (p) => (useVisual && p.value[2] > maxV * 0.5) ? "#fff" : th.ink, formatter: (p) => p.value[2] ? fmtV(p.value[2]) : "" },
        itemStyle: { borderColor: th.surface, borderWidth: 3 } }],
    };
    if (useVisual) option.visualMap = { type: "continuous", min: 0, max: maxV, calculable: true, orient: "horizontal",
      left: "center", bottom: 8, itemHeight: 130, text: ["high", "low"], textStyle: { color: th.muted, fontFamily: th.mono, fontSize: 10 },
      inRange: { color: [th.surface2, th.ramp(0.35), th.accent] } };
    const onEvents = onCell ? { click: (p) => onCell(rows[p.value[1]], cols[p.value[0]]) } : null;
    return (
      <div>
        <EChart option={option} height={Math.max(240, rows.length * 34 + 96)} onEvents={onEvents} />
        <div className="emm-matrix-totstrip mono">
          <span className="emm-matrix-grand">{t("mxTotal")}: {fmtV(grand)}</span>
          {cols.map((c) => <span key={c.id} className="emm-matrix-totitem">{L(c.label, lang)} <b>{fmtV(colTot[c.id] || 0)}</b></span>)}
        </div>
      </div>
    );
  }

  // ---------- Propagation graph (node-link) — ECharts ---------
  function PropagationGraph({ data, height = 340, onNode }) {
    const th = chartTheme();
    const P = window.EMM.PLATFORMS || {};
    const { nodes, edges } = data;
    const platKeys = Array.from(new Set(nodes.map((n) => n.platform)));
    const categories = platKeys.map((k) => ({ name: (P[k] && P[k].label) || k }));
    const palette = platKeys.map((k) => th.rgb(`oklch(0.62 0.13 ${(P[k] && P[k].hue) || 260})`));
    const W = 720;
    const colX = [0.10 * W, 0.45 * W, 0.85 * W];
    const byTier = [0, 1, 2].map((tr) => nodes.filter((n) => n.tier === tr));
    const pos = {};
    byTier.forEach((arr, ti) => arr.forEach((n, i) => { pos[n.id] = { x: colX[ti], y: (height - 60) * ((i + 1) / (arr.length + 1)) + 24 }; }));
    const maxR = Math.max(...nodes.map((n) => n.reach), 1);
    const gnodes = nodes.map((n) => ({
      id: n.id, name: n.handle, x: pos[n.id].x, y: pos[n.id].y, fixed: true,
      symbolSize: 12 + Math.sqrt(n.reach / maxR) * 34,
      category: platKeys.indexOf(n.platform),
      itemStyle: n.tier === 0 ? { borderColor: th.neg, borderWidth: 2.5 } : { borderColor: th.surface, borderWidth: 1 },
      label: { show: n.tier < 2, position: "bottom", color: th.muted, fontFamily: th.mono, fontSize: 9,
        formatter: n.handle.length > 16 ? n.handle.slice(0, 15) + "…" : n.handle },
      value: n.reach, _join: n.joinMin, _platform: n.platform, _tier: n.tier, _handle: n.handle,
    }));
    const base = baseOption(th);
    const option = {
      ...base, color: palette,
      tooltip: { ...base.tooltip, formatter: (p) => p.dataType === "node" ? `<b>${p.data.name}</b><br/>${C.compact(p.data.value)} reach · +${p.data._join}m` : "" },
      legend: { data: categories.map((c) => c.name), bottom: 2, icon: "circle", itemWidth: 9, itemHeight: 9,
        textStyle: { color: th.muted, fontFamily: th.mono, fontSize: 10 } },
      series: [{
        type: "graph", layout: "none", categories, draggable: true, roam: false,
        edgeSymbol: ["none", "none"], lineStyle: { color: th.line2, width: 1, opacity: 0.7, curveness: 0.18 },
        emphasis: { focus: "adjacency", lineStyle: { width: 2 } },
        data: gnodes, links: edges.map((e) => ({ source: e.from, target: e.to })),
      }],
    };
    const onEvents = onNode ? { click: (p) => { if (p.dataType === "node") onNode({ handle: p.data._handle, reach: p.data.value, joinMin: p.data._join, platform: p.data._platform, tier: p.data._tier }); } } : undefined;
    return <EChart option={option} height={height} onEvents={onEvents} />;
  }

  // ---------- helpers -----------------------------------------
  function niceCeil(v) {
    if (v <= 0) return 1;
    const mag = Math.pow(10, Math.floor(Math.log10(v)));
    const f = v / mag;
    const nice = f <= 1 ? 1 : f <= 2 ? 2 : f <= 5 ? 5 : 10;
    return nice * mag;
  }
  function ticks(max, n) {
    const out = [];
    for (let i = 1; i <= n; i++) out.push(Math.round((max / n) * i));
    return out;
  }

  // ---------- CSV export (client-side, no libs) ---------------
  // header: ["Col A","Col B"], rows: [[v,v],[v,v]] → triggers a download.
  function downloadCsv(filename, header, rows) {
    const esc = (v) => {
      const s = v == null ? "" : String(v);
      return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
    };
    const lines = [];
    if (header && header.length) lines.push(header.map(esc).join(","));
    (rows || []).forEach((r) => lines.push(r.map(esc).join(",")));
    const blob = new Blob(["﻿" + lines.join("\r\n")], { type: "text/csv;charset=utf-8;" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url; a.download = filename;
    document.body.appendChild(a); a.click();
    document.body.removeChild(a);
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  }

  Object.assign(window, {
    EChart, chartTheme, baseOption, downloadCsv,
    SentimentMeter, Sparkline, TrendChart, BarsH, Heatmap, DaypartGrid, Choropleth,
    MatrixGrid, PropagationGraph,
  });
})();
