/* global React */
// Chart components — Looker-style with multi-color categorical palette

const { useMemo, useState, useRef, useEffect } = React;

// Multi-color categorical palette (matches reference dashboards)
const PALETTE = ["#2eb5e6", "#1f3a68", "#d44b6c", "#6dc065", "#d97a26", "#5d4ea3", "#2e8b8e", "#e0b020", "#a0a8b3"];

// ===== LineChart =====
function LineChart({ series, labels, height = 240, color = "#2eb5e6", areaFill = true, format = (v) => v.toFixed(2), yMin: yMinProp, yMax: yMaxProp, secondary, points: showPoints = true, refLine }) {
  const ref = useRef();
  const [w, setW] = useState(600);
  const [hover, setHover] = useState(null);
  useEffect(() => {
    if (!ref.current) return;
    const ro = new ResizeObserver(() => setW(ref.current.clientWidth));
    ro.observe(ref.current);
    setW(ref.current.clientWidth);
    return () => ro.disconnect();
  }, []);
  const padL = 40,padR = 12,padT = 8,padB = 24;
  const innerW = Math.max(50, w - padL - padR);
  const innerH = height - padT - padB;
  const isNum = (v) => v !== null && v !== undefined && !isNaN(v);
  const allValues = [...series, ...(secondary ? secondary.data : [])].filter(isNum);
  const dataMin = Math.min(...allValues),dataMax = Math.max(...allValues);
  const span = dataMax - dataMin || 1;
  const yMin = yMinProp !== undefined ? yMinProp : dataMin - span * 0.08;
  const yMax = yMaxProp !== undefined ? yMaxProp : dataMax + span * 0.08;
  const yScale = (v) => padT + innerH - (v - yMin) / (yMax - yMin) * innerH;
  const xScale = (i) => padL + innerW * i / Math.max(1, series.length - 1);
  // null-safe path — breaks the line across missing/future months
  const buildPath = (arr) => {
    let d = "",pen = false;
    arr.forEach((v, i) => {
      if (!isNum(v)) {pen = false;return;}
      d += `${pen ? "L" : "M"}${xScale(i).toFixed(1)},${yScale(v).toFixed(1)}`;
      pen = true;
    });
    return d;
  };
  let lastIdx = series.length - 1;
  for (let i = series.length - 1; i >= 0; i--) {if (isNum(series[i])) {lastIdx = i;break;}}
  const path = buildPath(series);
  const areaPath = `${path} L${xScale(lastIdx).toFixed(1)},${padT + innerH} L${padL},${padT + innerH} Z`;
  const secPath = secondary ? buildPath(secondary.data) : null;
  const ticks = Array.from({ length: 5 }, (_, i) => yMin + (yMax - yMin) * i / 4);
  const xStep = series.length > 10 ? Math.ceil(series.length / 8) : 1;

  const onMove = (e) => {
    const rect = ref.current.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const rel = (x - padL) / innerW;
    const idx = Math.max(0, Math.min(series.length - 1, Math.round(rel * (series.length - 1))));
    if (!isNum(series[idx])) {setHover(null);return;}
    setHover({ idx, x: xScale(idx), y: yScale(series[idx]) });
  };
  const gid = `lc-fill-${color.replace(/[^a-z0-9]/gi, "")}`;
  return (
    <div className="chart-wrap" ref={ref} onMouseMove={onMove} onMouseLeave={() => setHover(null)} style={{ width: "100%" }}>
      <svg width="100%" height={height} viewBox={`0 0 ${w} ${height}`}>
        <defs>
          <linearGradient id={gid} x1="0" x2="0" y1="0" y2="1">
            <stop offset="0%" stopColor={color} stopOpacity="0.18" />
            <stop offset="100%" stopColor={color} stopOpacity="0" />
          </linearGradient>
        </defs>
        {ticks.map((t, i) =>
        <g key={i}>
            <line x1={padL} x2={w - padR} y1={yScale(t)} y2={yScale(t)} className="chart-grid-line" />
            <text x={padL - 4} y={yScale(t)} dy="3" textAnchor="end" className="chart-axis-y">{format(t)}</text>
          </g>
        )}
        <line x1={padL} x2={w - padR} y1={padT + innerH} y2={padT + innerH} className="chart-baseline" />
        {refLine && refLine.value >= yMin && refLine.value <= yMax &&
        <g>
            <line x1={padL} x2={w - padR} y1={yScale(refLine.value)} y2={yScale(refLine.value)}
          stroke={refLine.color || "#8a93a0"} strokeWidth="1.3" strokeDasharray="5 4" />
            {refLine.label &&
          <text x={w - padR} y={yScale(refLine.value) - 5} textAnchor="end" fontSize="10" fontWeight="600"
          fill={refLine.color || "#6b7480"}>{refLine.label}</text>}
          </g>
        }
        {areaFill && <path d={areaPath} fill={`url(#${gid})`} />}
        <path d={path} fill="none" stroke={color} strokeWidth="2" strokeLinejoin="round" strokeLinecap="round" />
        {secPath && <path d={secPath} fill="none" stroke={secondary.color || "#d97a26"} strokeWidth="2" strokeLinejoin="round" strokeLinecap="round" />}
        {showPoints && series.map((v, i) => isNum(v) ? <circle key={i} cx={xScale(i)} cy={yScale(v)} r="2.5" fill={color} stroke="#fff" strokeWidth="1.2" /> : null)}
        {showPoints && secondary && secondary.data.map((v, i) => isNum(v) ? <circle key={`s${i}`} cx={xScale(i)} cy={yScale(v)} r="2.5" fill={secondary.color || "#d97a26"} stroke="#fff" strokeWidth="1.2" /> : null)}
        {labels.map((l, i) => {
          if (i % xStep !== 0 && i !== labels.length - 1) return null;
          return <text key={i} x={xScale(i)} y={height - 6} textAnchor="middle" className="chart-axis-x">{l}</text>;
        })}
        {hover &&
        <g>
            <line x1={hover.x} x2={hover.x} y1={padT} y2={padT + innerH} stroke="#b8bdc4" strokeDasharray="3 3" />
            <circle cx={hover.x} cy={hover.y} r="4" fill={color} stroke="#fff" strokeWidth="2" />
          </g>
        }
      </svg>
      {hover &&
      <div className="chart-tooltip" style={{ left: Math.min(w - 150, Math.max(8, hover.x - 70)), top: 4 }}>
          <div className="ttt">{labels[hover.idx]}</div>
          <div className="ttv"><span className="l">Value</span><span className="v">{format(series[hover.idx])}</span></div>
          {secondary && <div className="ttv"><span className="l">{secondary.label}</span><span className="v">{format(secondary.data[hover.idx])}</span></div>}
        </div>
      }
    </div>);

}

// ===== BarChart (vertical with optional secondary line) =====
function BarChart({ series, labels, height = 200, color = "#2eb5e6", format = (v) => v.toFixed(1), negativeColor = "#c5363c", baseline = 0, secondary, showLabels = false }) {
  const ref = useRef();
  const [w, setW] = useState(600);
  const [hover, setHover] = useState(null);
  useEffect(() => {
    if (!ref.current) return;
    const ro = new ResizeObserver(() => setW(ref.current.clientWidth));
    ro.observe(ref.current);
    setW(ref.current.clientWidth);
    return () => ro.disconnect();
  }, []);
  const dualAxis = !!(secondary && secondary.axis);
  const padL = 40,padR = dualAxis ? 46 : 12,padT = showLabels ? 18 : 8,padB = 24;
  const innerW = Math.max(50, w - padL - padR);
  const innerH = height - padT - padB;
  const mainVals = dualAxis ? [baseline, ...series] : [baseline, ...series, ...(secondary?.data || [])];
  const dataMin = Math.min(...mainVals),dataMax = Math.max(...mainVals);
  const span = dataMax - dataMin || 1;
  const yMin = dataMin - span * 0.05;
  const yMax = dataMax + span * 0.05;
  const yScale = (v) => padT + innerH - (v - yMin) / (yMax - yMin) * innerH;
  // Secondary (right) axis — own scale so a % line isn't squashed onto the bar axis
  let sMin = yMin,sMax = yMax;
  const isNumB = (v) => v !== null && v !== undefined && !isNaN(v);
  if (dualAxis) {
    const sv = secondary.data.filter(isNumB);
    const smin = Math.min(0, ...sv),smax = Math.max(...sv);
    const sspan = smax - smin || 1;
    sMin = smin - sspan * 0.12;sMax = smax + sspan * 0.12;
  }
  const sScale = (v) => padT + innerH - (v - sMin) / (sMax - sMin) * innerH;
  const secY = (v) => dualAxis ? sScale(v) : yScale(v);
  const secFmt = secondary && secondary.format || format;
  const barWidth = innerW / series.length * 0.62;
  const barGap = innerW / series.length * 0.38;
  const xPos = (i) => padL + innerW / series.length * i + barGap / 2;
  const ticks = Array.from({ length: 5 }, (_, i) => yMin + (yMax - yMin) * i / 4);
  const xStep = series.length > 12 || w < 460 && series.length > 7 ? Math.ceil(series.length / (w < 460 ? 6 : 8)) : 1;
  const baselineY = yScale(baseline);
  let secStarted = false;
  const secPath = secondary ? secondary.data.map((v, i) => {
    if (!isNumB(v)) return "";
    const cmd = secStarted ? "L" : "M";secStarted = true;
    return `${cmd}${(xPos(i) + barWidth / 2).toFixed(1)},${secY(v).toFixed(1)}`;
  }).filter(Boolean).join(" ") : null;

  return (
    <div className="chart-wrap" ref={ref} style={{ width: "100%" }} onMouseLeave={() => setHover(null)}>
      <svg width="100%" height={height} viewBox={`0 0 ${w} ${height}`}>
        {ticks.map((t, i) =>
        <g key={i}>
            <line x1={padL} x2={w - padR} y1={yScale(t)} y2={yScale(t)} className="chart-grid-line" />
            <text x={padL - 4} y={yScale(t)} dy="3" textAnchor="end" className="chart-axis-y">{format(t)}</text>
          </g>
        )}
        {dualAxis && ticks.map((t, i) =>
        <text key={`ry${i}`} x={w - padR + 5} y={yScale(t)} dy="3" textAnchor="start" className="chart-axis-y"
        style={{ fill: secondary.color || "#d97a26" }}>{secFmt(sMin + (sMax - sMin) * i / 4)}</text>
        )}
        <line x1={padL} x2={w - padR} y1={baselineY} y2={baselineY} className="chart-baseline" />
        {series.map((v, i) => {
          const y = v >= baseline ? yScale(v) : baselineY;
          const h = Math.abs(yScale(v) - baselineY);
          const c = v >= baseline ? color : negativeColor;
          return (
            <g key={i}>
              <rect x={xPos(i)} y={y} width={barWidth} height={Math.max(1, h)} fill={c} rx="1"
              onMouseEnter={() => setHover({ i, x: xPos(i) + barWidth / 2, y })} />
              {showLabels &&
              <text x={xPos(i) + barWidth / 2} y={y - 4} textAnchor="middle" fontSize="9.5" fill={c} fontWeight="600">
                  {format(v)}
                </text>
              }
            </g>);

        })}
        {secPath &&
        <>
            <path d={secPath} fill="none" stroke={secondary.color || "#d97a26"} strokeWidth="1.8" />
            {secondary.data.map((v, i) => isNumB(v) ? <circle key={i} cx={xPos(i) + barWidth / 2} cy={secY(v)} r="2.5" fill={secondary.color || "#d97a26"} /> : null)}
          </>
        }
        {labels.map((l, i) => {
          if (i % xStep !== 0 && i !== labels.length - 1) return null;
          return <text key={i} x={xPos(i) + barWidth / 2} y={height - 6} textAnchor="middle" className="chart-axis-x">{l}</text>;
        })}
      </svg>
      {hover !== null &&
      <div className="chart-tooltip" style={{ left: Math.min(w - 150, Math.max(8, hover.x - 70)), top: 4 }}>
          <div className="ttt">{labels[hover.i]}</div>
          <div className="ttv"><span className="l">Value</span><span className="v">{format(series[hover.i])}</span></div>
          {secondary && isNumB(secondary.data[hover.i]) && <div className="ttv"><span className="l">{secondary.label}</span><span className="v">{secFmt(secondary.data[hover.i])}</span></div>}
        </div>
      }
    </div>);

}

// ===== Horizontal Bar Chart with data labels (matches references) =====
function HorizontalBarChart({ data, height, color = "#2eb5e6", format = (v) => v.toLocaleString("id-ID"), showLabels = true, colorByCategory = false, labelsOnTop = false, labelWidth }) {
  // data: [{ name, value }]
  const ref = useRef();
  const [w, setW] = useState(600);
  useEffect(() => {
    if (!ref.current) return;
    const ro = new ResizeObserver(() => setW(ref.current.clientWidth));
    ro.observe(ref.current);
    setW(ref.current.clientWidth);
    return () => ro.disconnect();
  }, []);

  const maxVal = Math.max(...data.map((d) => d.value), 0);

  // ----- labelsOnTop: nama sektor di atas tiap bar (tak terpotong) -----
  if (labelsOnTop) {
    const rowH = 36; // ruang untuk label + bar
    const padR = 46;
    const padX = 2;
    const padTB = 6;
    const H = height || data.length * rowH + padTB * 2;
    const innerW = Math.max(40, w - padX - padR);
    return (
      <div className="chart-wrap" ref={ref} style={{ width: "100%" }}>
        <svg width="100%" height={H} viewBox={`0 0 ${w} ${H}`}>
          {data.map((d, i) => {
            const y = padTB + i * rowH;
            const bw = d.value / maxVal * innerW;
            const c = colorByCategory ? PALETTE[i % PALETTE.length] : color;
            return (
              <g key={i}>
                <text x={padX} y={y + 11} fontSize="10.5" fill="#3c4146">{d.name}</text>
                <rect x={padX} y={y + 17} width={Math.max(2, bw)} height={11} fill={c} rx="1.5" />
                {showLabels &&
                <text x={padX + Math.max(2, bw) + 4} y={y + 26} fontSize="10.5" fill="#1f2329" fontWeight="600">
                    {format(d.value)}
                  </text>
                }
              </g>);

          })}
        </svg>
      </div>);

  }

  const padR = 50;
  const padTB = 8;
  // Rows expand to fill the given tile height so there's no empty space below.
  const rowH = height ? (height - padTB * 2) / data.length : 22;
  const barThick = Math.min(26, Math.max(10, rowH * 0.62));
  // Label gutter sizes to the longest region name so names aren't clipped
  // (cap keeps very long labels from eating the plot area; labelWidth overrides).
  const maxNameLen = Math.max(...data.map((d) => d.name.length), 0);
  const padL = labelWidth || Math.round(Math.min(168, Math.max(80, maxNameLen * 6.1 + 14)));
  const maxChars = Math.max(14, Math.floor((padL - 14) / 6.1));
  const H = height || data.length * rowH + padTB * 2;
  const innerW = Math.max(40, w - padL - padR);

  return (
    <div className="chart-wrap" ref={ref} style={{ width: "100%" }}>
      <svg width="100%" height={H} viewBox={`0 0 ${w} ${H}`}>
        {data.map((d, i) => {
          const y = padTB + i * rowH;
          const bw = d.value / maxVal * innerW;
          const c = colorByCategory ? PALETTE[i % PALETTE.length] : color;
          return (
            <g key={i}>
              <text x={padL - 6} y={y + rowH / 2 + 4} textAnchor="end" fontSize="10.5" fill="#3c4146">
                {d.name.length > maxChars ? d.name.slice(0, maxChars - 1) + "…" : d.name}
              </text>
              <rect x={padL} y={y + (rowH - barThick) / 2} width={Math.max(2, bw)} height={barThick} fill={c} rx="1.5" />
              {showLabels &&
              <text x={padL + Math.max(2, bw) + 4} y={y + rowH / 2 + 4} fontSize="10.5" fill="#1f2329" fontWeight="600">
                  {format(d.value)}
                </text>
              }
            </g>);

        })}
      </svg>
    </div>);

}

// ===== Stacked Horizontal Bar (100% normalized) =====
function StackedBar100({ rows, segments, format = (v) => v.toFixed(0) + "%", height }) {
  // rows: [{ name, values: [s1, s2, s3] }]
  // segments: [{ name, color }]
  const ref = useRef();
  const [w, setW] = useState(600);
  useEffect(() => {
    if (!ref.current) return;
    const ro = new ResizeObserver(() => setW(ref.current.clientWidth));
    ro.observe(ref.current);
    setW(ref.current.clientWidth);
    return () => ro.disconnect();
  }, []);
  const rowH = 22;
  const padL = 100;
  const padR = 12;
  const padTB = 6;
  const H = height || rows.length * rowH + padTB * 2;
  const innerW = Math.max(50, w - padL - padR);

  return (
    <div className="chart-wrap" ref={ref} style={{ width: "100%" }}>
      <svg width="100%" height={H} viewBox={`0 0 ${w} ${H}`}>
        {rows.map((r, i) => {
          const y = padTB + i * rowH;
          const total = r.values.reduce((s, v) => s + v, 0) || 1;
          let xCursor = padL;
          return (
            <g key={i}>
              <text x={padL - 6} y={y + rowH / 2 + 4} textAnchor="end" fontSize="10.5" fill="#3c4146">
                {r.name.length > 14 ? r.name.slice(0, 14) + "…" : r.name}
              </text>
              {r.values.map((v, k) => {
                const segW = v / total * innerW;
                const x = xCursor;
                xCursor += segW;
                const pct = v / total * 100;
                return (
                  <g key={k}>
                    <rect x={x} y={y + 4} width={Math.max(0, segW)} height={rowH - 8} fill={segments[k].color} />
                    {segW > 30 &&
                    <text x={x + segW / 2} y={y + rowH / 2 + 4} textAnchor="middle" fontSize="9.5" fill="#fff" fontWeight="600">
                        {format(pct)}
                      </text>
                    }
                  </g>);

              })}
            </g>);

        })}
      </svg>
    </div>);

}

// ===== Donut Chart =====
function Donut({ data, height = 180, innerRatio = 0.6, centerLabel, centerValue, showLegend = true }) {
  // data: [{ name, value, color? }]
  const total = data.reduce((s, d) => s + d.value, 0) || 1;
  const size = height - 24;
  const cx = size / 2;
  const cy = size / 2;
  const r = size / 2 - 4;
  const ir = r * innerRatio;
  let cumulative = 0;
  const arcs = data.map((d, i) => {
    const start = cumulative / total * Math.PI * 2;
    const end = (cumulative + d.value) / total * Math.PI * 2;
    cumulative += d.value;
    const x1 = cx + Math.cos(start - Math.PI / 2) * r;
    const y1 = cy + Math.sin(start - Math.PI / 2) * r;
    const x2 = cx + Math.cos(end - Math.PI / 2) * r;
    const y2 = cy + Math.sin(end - Math.PI / 2) * r;
    const ix1 = cx + Math.cos(end - Math.PI / 2) * ir;
    const iy1 = cy + Math.sin(end - Math.PI / 2) * ir;
    const ix2 = cx + Math.cos(start - Math.PI / 2) * ir;
    const iy2 = cy + Math.sin(start - Math.PI / 2) * ir;
    const large = end - start > Math.PI ? 1 : 0;
    const path = `M ${x1} ${y1} A ${r} ${r} 0 ${large} 1 ${x2} ${y2} L ${ix1} ${iy1} A ${ir} ${ir} 0 ${large} 0 ${ix2} ${iy2} Z`;
    return { ...d, path, color: d.color || PALETTE[i % PALETTE.length], pct: d.value / total * 100 };
  });

  const [hover, setHover] = useState(null);

  return (
    <div className="donut-wrap">
      <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} style={{ overflow: "visible" }}>
        {arcs.map((a, i) =>
        <path key={i} d={a.path} fill={a.color} stroke="#fff" strokeWidth="1.5"
        opacity={hover !== null && hover !== i ? 0.5 : 1}
        onMouseEnter={() => setHover(i)} onMouseLeave={() => setHover(null)}
        style={{ cursor: "pointer", transition: "opacity .15s" }} />
        )}
        {(centerLabel || centerValue) &&
        <g>
            {centerValue && <text x={cx} y={cy - 4} textAnchor="middle" fontSize="18" fontWeight="600" fill="#1f2329" style={{ opacity: "1" }}>{centerValue}</text>}
            {centerLabel && <text x={cx} y={cy + 12} textAnchor="middle" fontSize="9.5" fill="#6b7280">{centerLabel}</text>}
          </g>
        }
      </svg>
      {showLegend &&
      <div className="legend">
          {arcs.map((a, i) =>
        <span key={i} className="item">
              <span className="sw" style={{ background: a.color }} />
              {a.name} <span className="muted">· {a.pct.toFixed(1)}%</span>
            </span>
        )}
        </div>
      }
    </div>);

}

// ===== Gauge =====
function Gauge({ value, min = 0, max = 100, label, format = (v) => v.toFixed(1), color = "#2eb5e6", height = 120 }) {
  const pct = Math.max(0, Math.min(1, (value - min) / (max - min)));
  const w = 200;
  const pad = 8;
  const outerR = w / 2 - pad;
  const band = Math.round(outerR * 0.44); // thick donut band like native Looker gauge
  const r = outerR - band / 2; // centerline radius for stroked arcs
  const cx = w / 2;
  const cy = outerR + 6; // flat bottom of the semicircle
  const vbH = cy + 18;
  const start = Math.PI,end = 0;
  const angle = start - pct * Math.PI;
  const polar = (a, rad) => [cx + Math.cos(a) * rad, cy + Math.sin(-a) * rad];
  const [sx, sy] = polar(start, r);
  const [ex, ey] = polar(end, r);
  const [hx, hy] = polar(angle, r);
  const arcTrack = `M ${sx} ${sy} A ${r} ${r} 0 0 1 ${ex} ${ey}`;
  const arcFill = `M ${sx} ${sy} A ${r} ${r} 0 0 1 ${hx} ${hy}`;
  // tapered needle: narrow tip at the outer edge, slim base across the hub
  const tipR = outerR + 2;
  const baseW = 5;
  const perp = angle - Math.PI / 2;
  const [ntx, nty] = polar(angle, tipR);
  const b1 = [cx + Math.cos(perp) * baseW, cy + Math.sin(-perp) * baseW];
  const b2 = [cx - Math.cos(perp) * baseW, cy - Math.sin(-perp) * baseW];
  const needle = `${ntx},${nty} ${b1[0]},${b1[1]} ${b2[0]},${b2[1]}`;
  const fmtEnd = (v) => v.toLocaleString("id-ID");
  return (
    <div className="gauge-wrap">
      <svg width="100%" height="auto" viewBox={`0 0 ${w} ${vbH}`} style={{ maxWidth: 232, overflow: "visible" }}>
        <path d={arcTrack} fill="none" stroke="#d3d6db" strokeWidth={band} strokeLinecap="butt" />
        <path d={arcFill} fill="none" stroke={color} strokeWidth={band} strokeLinecap="butt" />
        <polygon points={needle} fill="#1f2329" />
        <circle cx={cx} cy={cy} r={baseW + 2} fill="#1f2329" />
        <text x={cx - outerR} y={cy + 14} fontSize="11" fill="#6b7280">{fmtEnd(min)}</text>
        <text x={cx + outerR} y={cy + 14} textAnchor="end" fontSize="11" fill="#6b7280">{fmtEnd(max)}</text>
      </svg>
      <div className="gauge-value" style={{ lineHeight: "1.5", padding: "1px" }}>{format(value)}</div>
      {label && <div className="gauge-label">{label}</div>}
    </div>);

}

// ===== Indonesia Map (real GADM 4.1 province boundaries via window.ME30_GEO) =====
// Two data modes:
//  • regions  -> island-level objects keyed by r.id (island id). Each province polygon
//                inherits its parent-island color + tooltip (value / table). Backward-compatible.
//  • provinceData -> [{name, value, valueFmt}] true per-province choropleth. Data province
//                names are resolved via geo.aliases (incl. Papua 6→2 merge, averaged).
function IndonesiaMap({ regions, provinceData, height = 300, hueBase = 250 }) {
  const geo = typeof window !== "undefined" && window.ME30_GEO || null;
  const [hover, setHover] = useState(null);
  const [pos, setPos] = useState({ x: 0, y: 0, w: 0, h: 0 });
  const wrapRef = React.useRef(null);

  const trackMove = (e) => {
    const el = wrapRef.current;
    if (!el) return;
    const r = el.getBoundingClientRect();
    setPos({ x: e.clientX - r.left, y: e.clientY - r.top, w: r.width, h: r.height });
  };

  if (!geo) {
    return <div className="chart-wrap" style={{ padding: 24, color: "#9aa0a6", fontSize: 13 }}>Peta belum dimuat (geo-indonesia.js).</div>;
  }

  const [,, W, H] = geo.viewBox;
  const provinceMode = Array.isArray(provinceData) && provinceData.length > 0;

  // Build value lookup keyed by geo province NAME_1
  const provVal = {};
  if (provinceMode) {
    const agg = {}; // geoName -> {sum,count, sample}
    provinceData.forEach((p) => {
      const geoName = geo.aliases[p.name] || p.name;
      const a = agg[geoName] || (agg[geoName] = { sum: 0, count: 0, sample: p });
      a.sum += p.value;a.count += 1;
    });
    Object.keys(agg).forEach((g) => {
      const a = agg[g];
      provVal[g] = { value: a.sum / a.count, merged: a.count > 1, sample: a.sample };
    });
  }

  // Island-level lookup keyed by region.id
  const islandVal = {};
  (regions || []).forEach((r) => {islandVal[r.id] = r;});

  // color scale across active values
  const activeVals = provinceMode ?
  Object.values(provVal).map((d) => d.value) :
  (regions || []).map((r) => r.value);
  const vMax = activeVals.length ? Math.max(...activeVals) : 1;
  const vMin = activeVals.length ? Math.min(...activeVals) : 0;
  const colorFor = (v) => {
    if (v === undefined || v === null || isNaN(v)) return "#eceef0";
    const t = (v - vMin) / (vMax - vMin || 1);
    return `oklch(${0.95 - t * 0.45} ${0.03 + t * 0.07} ${hueBase})`;
  };

  const valueForProvince = (prov) => {
    if (provinceMode) {
      const d = provVal[prov.name];
      return d ? d.value : undefined;
    }
    const r = islandVal[prov.island];
    return r ? r.value : undefined;
  };

  const onEnter = (prov) => {
    if (provinceMode) {
      const d = provVal[prov.name];
      setHover({ mode: "prov", name: prov.name, value: d ? d.value : undefined, valueFmt: d && d.sample.valueFmt, merged: d && d.merged });
    } else {
      const r = islandVal[prov.island];
      setHover({ mode: "island", name: prov.name, island: prov.island, r });
    }
  };

  const islandName = { sumatera: "Sumatera", java: "Jawa", bali_nt: "Bali & Nusa Tenggara", kalimantan: "Kalimantan", sulawesi: "Sulawesi", maluku: "Maluku", papua: "Papua" };

  // Tooltip follows the cursor but is offset ~14px and flips its anchor corner
  // near the right/bottom edge — so it never sits on top of the hovered region
  // and never spills outside the tile (no width clamp → table shows in full).
  const wrapW = pos.w || 600,wrapH = pos.h || height;
  const flipX = pos.x > wrapW * 0.5;
  const flipY = pos.y > wrapH * 0.62;
  const tipTransform = `translate(${flipX ? "calc(-100% - 14px)" : "14px"}, ${flipY ? "calc(-100% - 14px)" : "14px"})`;

  return (
    <div className="chart-wrap" style={{ position: "relative" }} ref={wrapRef}>
      <svg width="100%" height={height} viewBox={`0 0 ${W} ${H}`} onMouseMove={trackMove} style={{ display: "block", background: "#f3f7fb" }}>
        {geo.provinces.map((prov) => {
          if (!prov.d) return null;
          const isHot = hover && (provinceMode ? hover.name === prov.name : hover.island === prov.island);
          return (
            <path key={prov.name} d={prov.d}
            fill={colorFor(valueForProvince(prov))}
            stroke={isHot ? "#1f3a68" : "#ffffff"}
            strokeWidth={isHot ? 1.1 : 0.5}
            className="map-prov"
            onMouseEnter={() => onEnter(prov)}
            onMouseLeave={() => setHover(null)} />);

        })}
        {geo.islands.map((isl) =>
        <text key={isl.id} x={isl.cx} y={isl.cy} fontSize={isl.id === "bali_nt" ? 8.5 : 11}
        fill="#3c4146" fontWeight="700" textAnchor="middle"
        style={{ pointerEvents: "none", letterSpacing: 0.3, textShadow: "0 1px 2px rgba(255,255,255,0.9)" }}>
            {isl.label}
          </text>
        )}
      </svg>
      {hover &&
      <div className="chart-tooltip" style={{ left: pos.x, top: pos.y, transform: tipTransform, pointerEvents: "none" }}>
          {hover.mode === "prov" ?
        <>
              <div className="ttt">{hover.name}{hover.merged ? " *" : ""}</div>
              <div className="ttv"><span className="l">Nilai</span><span className="v">{hover.valueFmt !== undefined && hover.valueFmt !== null ? hover.valueFmt : hover.value !== undefined ? hover.value.toFixed(2) : "—"}</span></div>
              {hover.merged && <div className="ttv"><span className="l muted" style={{ fontSize: 10 }}>* rata-rata wil. pemekaran</span></div>}
            </> :
        <>
              <div className="ttt">{hover.name}{hover.island && islandName[hover.island] && hover.island !== "java" ? "" : ""}{hover.r && hover.r.share !== undefined ? ` · ${hover.r.share.toFixed(1)}% nasional` : ""}</div>
              <div className="ttv"><span className="l muted" style={{ fontSize: 10 }}>Wilayah: {islandName[hover.island] || "—"}</span></div>
              {hover.r && hover.r.table ?
          <table className="map-tip-table">
                <tbody>
                  {hover.r.table.map((row, i) =>
              <tr key={i} className={row.total ? "tot" : ""}>
                      <td>{row.k}</td>
                      <td className="num">{row.v.toLocaleString("id-ID")}</td>
                      {row.c !== undefined && row.c !== null && <td className="num cagr">{row.c >= 0 ? "+" : ""}{row.c.toFixed(1)}%</td>}
                    </tr>
              )}
                </tbody>
              </table> :
          hover.r ?
          <>
                <div className="ttv"><span className="l">Nilai</span><span className="v">{hover.r.valueFmt || hover.r.value.toLocaleString("id-ID")}</span></div>
                {hover.r.share !== undefined && <div className="ttv"><span className="l">Share</span><span className="v">{hover.r.share.toFixed(1)}%</span></div>}
                {hover.r.growth !== undefined && <div className="ttv"><span className="l">Growth</span><span className="v">{hover.r.growth.toFixed(2)}%</span></div>}
              </> :
          <div className="ttv"><span className="l muted">Tidak ada data</span></div>}
            </>}
        </div>
      }
      <div className="map-legend">
        <span>Lower</span>
        <span className="grad" style={{ background: `linear-gradient(90deg, oklch(0.95 0.03 ${hueBase}), oklch(0.72 0.065 ${hueBase}), oklch(0.50 0.10 ${hueBase}))` }} />
        <span>Higher</span>
      </div>
    </div>);

}

Object.assign(window, { LineChart, BarChart, HorizontalBarChart, StackedBar100, Donut, Gauge, IndonesiaMap, PALETTE });