/* Candlestick chart with MA + KC overlay + indicator subpanels (MACD/RSI/KD)
   + drawing tools (hline / tline / ruler) + crosshair + price axis drag. */
const { Icon } = window.UI;

/* Generate realistic-ish OHLC mock series */
function genCandles(n = 120, seed = 42, start = 920) {
  let s = seed;
  const rng = () => { s = (s * 9301 + 49297) % 233280; return s / 233280; };
  const out = [];
  let close = start;
  const baseDate = new Date(2026, 0, 5);
  for (let i = 0; i < n; i++) {
    const drift = (rng() - 0.45) * close * 0.018;
    const open = close;
    const c = open + drift;
    const wick = Math.abs(drift) * (0.7 + rng() * 1.4) + close * 0.004;
    const high = Math.max(open, c) + rng() * wick;
    const low = Math.min(open, c) - rng() * wick;
    const vol = (1.2 + rng() * 1.6) * 1e7 * (1 + Math.abs(drift)/close * 8);
    const d = new Date(baseDate);
    d.setDate(baseDate.getDate() + i);
    if (d.getDay() === 0) d.setDate(d.getDate() + 1);
    if (d.getDay() === 6) d.setDate(d.getDate() + 2);
    out.push({ date: d, o: +open.toFixed(2), h: +high.toFixed(2), l: +low.toFixed(2), c: +c.toFixed(2), v: Math.round(vol) });
    close = c;
  }
  return out;
}

function ma(arr, key, period) {
  const out = new Array(arr.length).fill(null);
  for (let i = period - 1; i < arr.length; i++) {
    let sum = 0;
    for (let j = i - period + 1; j <= i; j++) sum += arr[j][key];
    out[i] = sum / period;
  }
  return out;
}

const MA_CONFIG = [
  { period: 5,   color: "#F6C85F", on: true },
  { period: 10,  color: "#F87171", on: true },
  { period: 20,  color: "#4DA3FF", on: true },
  { period: 60,  color: "#A78BFA", on: true },
  { period: 120, color: "#22C55E", on: false },
  { period: 240, color: "#94A3B8", on: false },
];

const KC_MARKERS = [
  { idx: 12,  type: "low",         label: "底"},
  { idx: 18,  type: "long-exit",   label: "多單出場", reason: "跌破 5MA" },
  { idx: 22,  type: "high",        label: "頭"},
  { idx: 28,  type: "long-entry",  label: "多單進場", reason: "盤整突破" },
  { idx: 32,  type: "low",         label: "底"},
  { idx: 36,  type: "long-entry",  label: "多單進場", reason: "回後買上漲" },
  { idx: 42,  type: "high",        label: "頭"},
  { idx: 46,  type: "long-exit",   label: "多單出場", reason: "跌破 5MA" },
  { idx: 52,  type: "low",         label: "底"},
  { idx: 56,  type: "long-entry",  label: "多單進場", reason: "盤整突破" },
  { idx: 62,  type: "high",        label: "頭"},
  { idx: 66,  type: "long-exit",   label: "多單出場", reason: "頭頭低" },
  { idx: 70,  type: "long-entry",  label: "多單進場", reason: "回後買上漲" },
  { idx: 74,  type: "high",        label: "頭"},
  { idx: 78,  type: "long-exit",   label: "多單出場", reason: "跌破 5MA" },
  { idx: 84,  type: "low",         label: "底"},
  { idx: 92,  type: "long-entry",  label: "多單進場", reason: "回後買上漲" },
  { idx: 98,  type: "high",        label: "頭"},
  { idx: 104, type: "long-exit",   label: "多單出場", reason: "跌破 5MA" },
  { idx: 110, type: "low",         label: "底"},
];

const SUB_HEIGHT = { MACD: 92, RSI: 80, KD: 80 };
const SUB_GAP = 8;

/* shape 工具規格 — 每個 toolMode 對應的繪圖樣式
 * pts:  正數 = 需要 N 次點擊；-1 = 多點到 dblclick 結束；"drag" = mouse drag path
 * prompt: 觸發 window.prompt 取得文字
 */
const SHAPE_SPECS = {
  /* 所有 2 點工具一律 drag2：按住滑鼠拖出範圍 */
  ray:           { pts: "drag2" },
  extend:        { pts: "drag2" },
  vline:         { pts: 1 },
  parallel:      { pts: 3 },
  "fib-retrace": { pts: "drag2" },
  "fib-extend":  { pts: 3 },
  "fib-channel": { pts: 3 },
  "fib-timezone":{ pts: "drag2" },
  "fib-fan":     { pts: "drag2" },
  "fib-wedge":   { pts: "drag2" },
  "gann-box":    { pts: "drag2" },
  "gann-square": { pts: "drag2" },
  "gann-fan":    { pts: "drag2" },
  rect:          { pts: "drag2" },
  circle:        { pts: "drag2" },
  ellipse:       { pts: "drag2" },
  triangle:      { pts: 3 },
  arc:           { pts: 3 },
  curve:         { pts: 3 },
  polyline:      { pts: -1 },
  brush:         { pts: "drag" },
  highlight:     { pts: "drag" },
  "arrow-mark":  { pts: "drag2" },
  "arrow-up":    { pts: 1 },
  "arrow-down":  { pts: 1 },
  emoji:         { pts: 1, prompt: "輸入 emoji（單字符）" },
  flag:          { pts: 1 },
  text:          { pts: 1, prompt: "輸入文字" },
  label:         { pts: 1, prompt: "輸入標籤" },
  note:          { pts: 1, prompt: "輸入註解" },
  abcd:          { pts: 4 },
  elliott:       { pts: 5 },
  "head-shoulders": { pts: 5 },
  "triangle-pattern": { pts: 3 },
  "long-pos":    { pts: "drag2" },
  "short-pos":   { pts: "drag2" },
  "price-range": { pts: "drag2" },
  "date-range":  { pts: "drag2" },
};

const SHAPE_COLORS = {
  ray: "#4DA3FF", extend: "#4DA3FF", vline: "#4DA3FF", parallel: "#4DA3FF",
  "fib-retrace": "#F6C85F", "fib-extend": "#F6C85F", "fib-channel": "#F6C85F",
  "fib-timezone": "#F6C85F", "fib-fan": "#F6C85F", "fib-wedge": "#F6C85F",
  "gann-box": "#A78BFA", "gann-square": "#A78BFA", "gann-fan": "#A78BFA",
  rect: "#4ADE80", circle: "#4ADE80", ellipse: "#4ADE80", triangle: "#4ADE80",
  arc: "#4ADE80", curve: "#4ADE80", polyline: "#4ADE80",
  brush: "#F87171", highlight: "rgba(246,200,95,0.35)",
  "arrow-mark": "#4DA3FF", "arrow-up": "var(--c-up)", "arrow-down": "var(--c-down)",
  emoji: "#F6C85F", flag: "#F87171",
  text: "#E6EDF3", label: "#4DA3FF", note: "#F6C85F",
  abcd: "#A78BFA", elliott: "#A78BFA", "head-shoulders": "#A78BFA", "triangle-pattern": "#A78BFA",
  "long-pos": "var(--c-up)", "short-pos": "var(--c-down)",
  "price-range": "#4DA3FF", "date-range": "#4DA3FF",
};

const FIB_RATIOS_RETRACE = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1];
const FIB_RATIOS_EXTEND  = [0, 0.382, 0.618, 1, 1.272, 1.618, 2, 2.618];
const FIB_TIMEZONE_SEQ   = [1, 2, 3, 5, 8, 13, 21, 34, 55];
const GANN_RATIOS        = [1/8, 1/4, 1/3, 1/2, 1/1, 2/1, 3/1, 4/1, 8/1];

const CandlestickChart = ({
  data,
  height = 520,
  showMA = { 5: true, 10: true, 20: true, 60: true, 120: false, 240: false },
  showKC = true,
  kcOptions = {
    swing: true, dashed: true, connect: true, neckline: true, trend: true,
    waitSignal: true, longEntry: true, longExit: true, shortEntry: true, shortExit: true, maLabel: true,
  },
  kcEvents = null,
  market = "TW",
  formingIdx = null,
  showIndicators = { MA: true, EMA: false, MACD: false, RSI: false, KD: false, BB: false, VolMA: true },
  toolMode = "crosshair",
  drawings = { hlines: [], tlines: [], shapes: [] },
  onDrawingsChange = () => {},
  magnet = false,
  lockDrawings = false,
  hideDrawings = false,
  showDataWindow = false,
  showFxWatch = false,
  actionRef = null,
}) => {
  const effectiveEvents = (Array.isArray(kcEvents) ? kcEvents : KC_MARKERS)
    .filter(m => m.idx >= 0 && m.idx < data.length);
  const wrapRef = React.useRef(null);
  const [hover, setHover] = React.useState(null);
  const [w, setW] = React.useState(900);

  const initialView = (n) => ({ start: 0, end: Math.max(n, 1) });
  const [view, setView] = React.useState(initialView(data.length));
  const [drag, setDrag] = React.useState(null);
  const [priceScale, setPriceScale] = React.useState(1);
  const [priceDrag, setPriceDrag] = React.useState(null);

  /* tool drafts — 還沒 commit 的繪圖 */
  const [tlineDraft, setTlineDraft] = React.useState(null);   // { idx1, price1 }
  const [rulerDraft, setRulerDraft] = React.useState(null);   // { idx1, price1, idx2, price2 }
  const [shapeDraft, setShapeDraft] = React.useState(null);   // { type, pts: [], dragging?: bool }

  React.useEffect(() => {
    setView(initialView(data.length));
    setPriceScale(1);
    setTlineDraft(null);
    setRulerDraft(null);
    setShapeDraft(null);
  }, [data.length]);

  React.useEffect(() => {
    setTlineDraft(null);
    setRulerDraft(null);
    setShapeDraft(null);
  }, [toolMode]);

  React.useEffect(() => {
    const el = wrapRef.current;
    if (!el) return;
    const ro = new ResizeObserver(() => setW(el.clientWidth));
    ro.observe(el);
    setW(el.clientWidth);
    return () => ro.disconnect();
  }, []);

  const padL = 8, padR = 64, padT = 12, padB = 24;
  const volH = 88;
  /* 主圖 priceH = 由 height 扣掉 vol/padding 而得 */
  const priceH = height - volH - padT - padB - 6;
  const chartW = Math.max(400, w - padL - padR);

  /* Visible slice */
  const vStart = Math.max(0, Math.floor(view.start));
  const vEnd = Math.min(data.length, Math.ceil(view.end));
  const visible = data.slice(vStart, vEnd);
  const vCount = Math.max(2, visible.length);

  const min = Math.min(...visible.map(d => d.l));
  const max = Math.max(...visible.map(d => d.h));
  const baseRange = (max - min) || 1;
  const scaledHalf = baseRange / 2 / priceScale;
  const mid = (max + min) / 2;
  const yPad = scaledHalf * 2 * 0.04;
  const yMin = mid - scaledHalf - yPad;
  const yMax = mid + scaledHalf + yPad;
  const yScale = (v) => padT + (1 - (v - yMin) / (yMax - yMin)) * priceH;
  const yInv = (px) => yMin + (1 - (px - padT) / priceH) * (yMax - yMin);

  const maxVol = Math.max(...visible.map(d => d.v)) || 1;
  const volTop = padT + priceH + 6;
  const vScale = (v) => volTop + (1 - v / maxVol) * volH;

  const cw = chartW / vCount;
  const bodyW = Math.max(1.5, cw * 0.5);
  const xAt = (i) => padL + (i - vStart) * cw + cw / 2;
  const xToIdx = (x) => Math.max(vStart, Math.min(vEnd - 1, Math.floor(vStart + (x - padL) / cw)));

  /* Sub panels — 依 indicators 啟用順序 */
  const subKeys = [];
  if (showIndicators.MACD) subKeys.push("MACD");
  if (showIndicators.RSI)  subKeys.push("RSI");
  if (showIndicators.KD)   subKeys.push("KD");

  const panels = [];
  let cur = volTop + volH;
  for (const key of subKeys) {
    cur += SUB_GAP;
    const h = SUB_HEIGHT[key];
    panels.push({ key, top: cur, bottom: cur + h, h });
    cur += h;
  }
  const totalH = cur + padB;
  const lastPanelBottom = panels.length ? panels[panels.length - 1].bottom : volTop + volH;

  /* Indicator 計算（cache via React.useMemo） */
  const ind = React.useMemo(() => {
    if (!window.Indicators) return {};
    const closes = data.map((c) => c.c);
    const out = {};
    if (showIndicators.EMA)  out.ema20 = window.Indicators.ema(closes, 20);
    if (showIndicators.BB)   out.bb = window.Indicators.bbands(data, 20, 2);
    if (showIndicators.MACD) out.macd = window.Indicators.macd(data, 12, 26, 9);
    if (showIndicators.RSI)  out.rsi = window.Indicators.rsi(data, 14);
    if (showIndicators.KD)   out.kd = window.Indicators.kd(data, 9, 3, 3);
    return out;
  }, [data, showIndicators.EMA, showIndicators.BB, showIndicators.MACD, showIndicators.RSI, showIndicators.KD]);

  const zoomBy = (factor, focusIdx) => {
    setView(v => {
      const len = v.end - v.start;
      const newLen = Math.max(15, Math.min(data.length, len * factor));
      const f = focusIdx == null ? (v.start + len / 2) : focusIdx;
      const ratio = (f - v.start) / len;
      let start = f - newLen * ratio;
      let end = start + newLen;
      if (start < 0) { end -= start; start = 0; }
      if (end > data.length) { start -= (end - data.length); end = data.length; }
      start = Math.max(0, start);
      return { start, end };
    });
  };

  const resetView = () => {
    setView(initialView(data.length));
    setPriceScale(1);
    setTlineDraft(null);
    setRulerDraft(null);
    setShapeDraft(null);
  };

  /* magnet — 螢幕距離 ≤ 12px 才吸附到附近 K 棒 H/L/C，否則維持原位
   * 這樣使用者拖到 empty area（沒有 K 棒的高點）不會被「縮回」最近 K 棒 */
  const magnetSnap = (idx, price) => {
    if (!magnet) return { idx, price };
    const i0 = Math.max(0, Math.round(idx) - 2);
    const i1 = Math.min(data.length - 1, Math.round(idx) + 2);
    const px = xAt(idx);
    const py = yScale(price);
    let best = null;
    let bestDist = 12;
    for (let i = i0; i <= i1; i++) {
      const c = data[i];
      [c.h, c.l, c.c].forEach((p) => {
        const dx = xAt(i) - px;
        const dy = yScale(p) - py;
        const d = Math.hypot(dx, dy);
        if (d < bestDist) { best = { idx: i, price: p }; bestDist = d; }
      });
    }
    return best || { idx, price };
  };

  /* commit shape */
  const commitShape = (type, pts, opts = {}) => {
    const snapped = pts.map((p) => magnetSnap(p.idx, p.price));
    const id = "s" + Date.now() + "_" + Math.random().toString(36).slice(2, 6);
    const shape = { id, type, pts: snapped, opts };
    const next = { ...drawings, shapes: [...(drawings.shapes || []), shape] };
    onDrawingsChange(next);
    setShapeDraft(null);
  };

  /* actionRef expose — Toolbar 與 keyboard 用 */
  React.useEffect(() => {
    if (!actionRef) return;
    actionRef.current = {
      reset: resetView,
      zoomIn: () => zoomBy(0.77),
      zoomOut: () => zoomBy(1.3),
      screenshot: () => {
        const svg = wrapRef.current && wrapRef.current.querySelector("svg");
        if (!svg) return;
        const xml = new XMLSerializer().serializeToString(svg);
        const blob = new Blob([xml], { type: "image/svg+xml" });
        const url = URL.createObjectURL(blob);
        const a = document.createElement("a");
        a.href = url;
        a.download = `chart-${Date.now()}.svg`;
        a.click();
        setTimeout(() => URL.revokeObjectURL(url), 500);
      },
      /* 刪最後一個繪圖（順序：shapes → tlines → hlines） */
      deleteLast: () => {
        if (lockDrawings) return false;
        const shapes = drawings.shapes || [];
        const tlines = drawings.tlines || [];
        const hlines = drawings.hlines || [];
        if (shapes.length > 0) {
          onDrawingsChange({ ...drawings, shapes: shapes.slice(0, -1) });
          return true;
        }
        if (tlines.length > 0) {
          onDrawingsChange({ ...drawings, tlines: tlines.slice(0, -1) });
          return true;
        }
        if (hlines.length > 0) {
          onDrawingsChange({ ...drawings, hlines: hlines.slice(0, -1) });
          return true;
        }
        return false;
      },
    };
  });

  /* native wheel — React 的 onWheel 是 passive，e.preventDefault() 在那邊無效，
   * 會讓瀏覽器同步捲動頁面，造成 chart 位移後拖移座標錯亂 */
  const wheelHandlerRef = React.useRef();
  wheelHandlerRef.current = (e) => {
    e.preventDefault();
    const r = wrapRef.current.getBoundingClientRect();
    const x = e.clientX - r.left;
    const focusIdx = vStart + (x - padL) / cw;
    if (e.ctrlKey || e.metaKey || Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
      const factor = e.deltaY > 0 ? 1.12 : 0.89;
      zoomBy(factor, focusIdx);
    } else {
      const dx = e.deltaX;
      const candlesPerPx = (view.end - view.start) / chartW;
      setView(v => {
        const len = v.end - v.start;
        let start = v.start + dx * candlesPerPx;
        if (start < 0) start = 0;
        if (start + len > data.length) start = data.length - len;
        return { start, end: start + len };
      });
    }
  };

  React.useEffect(() => {
    const el = wrapRef.current;
    if (!el) return;
    const handler = (e) => wheelHandlerRef.current && wheelHandlerRef.current(e);
    el.addEventListener("wheel", handler, { passive: false });
    return () => el.removeEventListener("wheel", handler);
  }, []);

  const inPriceArea = (x, y) =>
    x >= padL && x < padL + chartW && y >= padT && y <= padT + priceH;

  const onMouseDown = (e) => {
    if (e.button !== 0) return;
    const r = wrapRef.current.getBoundingClientRect();
    const x = e.clientX - r.left;
    const y = e.clientY - r.top;

    /* tool 模式：在主圖內處理；不在主圖則正常 pan */
    if (toolMode !== "crosshair" && inPriceArea(x, y)) {
      const idx = xToIdx(x);
      const price = yInv(y);

      if (toolMode === "hline") {
        const snap = magnetSnap(idx, price);
        const id = "h" + Date.now();
        const next = { ...drawings, hlines: [...(drawings.hlines || []), { id, price: snap.price }] };
        onDrawingsChange(next);
        return;
      }
      if (toolMode === "tline") {
        /* drag：按住起點拖到終點放開 */
        setTlineDraft({ idx1: idx, price1: price, idx2: idx, price2: price, dragging: true });
        return;
      }
      if (toolMode === "ruler") {
        setRulerDraft({ idx1: idx, price1: price, idx2: idx, price2: price, dragging: true });
        return;
      }

      /* 通用 shape 工具 */
      const spec = SHAPE_SPECS[toolMode];
      if (spec) {
        /* drag-path 工具：brush / highlight */
        if (spec.pts === "drag") {
          setShapeDraft({ type: toolMode, pts: [{ idx, price }], dragging: true });
          return;
        }
        /* drag2：起點 → 拖到終點放開（如 fib-retrace） */
        if (spec.pts === "drag2") {
          setShapeDraft({ type: toolMode, pts: [{ idx, price }, { idx, price }], dragging: true, mode: "endpoint" });
          return;
        }
        /* 多點 polyline：dblclick 結束 */
        if (spec.pts === -1) {
          if (e.detail === 2) {
            if (shapeDraft && shapeDraft.pts.length >= 2) {
              commitShape(shapeDraft.type, shapeDraft.pts);
            }
            return;
          }
          if (!shapeDraft || shapeDraft.type !== toolMode) {
            setShapeDraft({ type: toolMode, pts: [{ idx, price }] });
          } else {
            setShapeDraft({ type: toolMode, pts: [...shapeDraft.pts, { idx, price }] });
          }
          return;
        }
        /* 1 點工具 — 直接 commit（含 prompt） */
        if (spec.pts === 1) {
          let opts = {};
          if (spec.prompt) {
            const txt = window.prompt(spec.prompt, "");
            if (txt === null) return;
            opts.text = txt;
          }
          commitShape(toolMode, [{ idx, price }], opts);
          return;
        }
        /* N 點工具（2-5） */
        const cur = (shapeDraft && shapeDraft.type === toolMode) ? shapeDraft.pts : [];
        const next = [...cur, { idx, price }];
        if (next.length >= spec.pts) {
          commitShape(toolMode, next);
        } else {
          setShapeDraft({ type: toolMode, pts: next });
        }
        return;
      }
    }
    setDrag({ x: e.clientX, start: view.start, end: view.end });
  };
  const onMouseUp = () => {
    setDrag(null);
    setPriceDrag(null);
    if (rulerDraft && rulerDraft.dragging) {
      setRulerDraft({ ...rulerDraft, dragging: false });
    }
    if (tlineDraft && tlineDraft.dragging) {
      const { idx1, price1, idx2, price2 } = tlineDraft;
      if (idx1 !== idx2 || Math.abs(price1 - price2) > 1e-6) {
        const s1 = magnetSnap(idx1, price1);
        const s2 = magnetSnap(idx2, price2);
        const id = "t" + Date.now();
        onDrawingsChange({
          ...drawings,
          tlines: [
            ...(drawings.tlines || []),
            { id, idx1: s1.idx, price1: s1.price, idx2: s2.idx, price2: s2.price },
          ],
        });
      }
      setTlineDraft(null);
    }
    if (shapeDraft && shapeDraft.dragging) {
      const pts = shapeDraft.pts;
      const degenerate = shapeDraft.mode === "endpoint" && pts.length >= 2 &&
        pts[0].idx === pts[1].idx && Math.abs(pts[0].price - pts[1].price) < 1e-6;
      if (pts.length >= 2 && !degenerate) {
        commitShape(shapeDraft.type, pts);
      } else {
        setShapeDraft(null);
      }
    }
  };

  const onPriceAxisDown = (e) => {
    if (e.button !== 0) return;
    e.stopPropagation();
    setPriceDrag({ y: e.clientY, scale: priceScale });
  };
  const onPriceAxisDoubleClick = () => setPriceScale(1);

  const mas = MA_CONFIG.map(m => ({ ...m, values: ma(data, "c", m.period) }));

  const last = data[data.length - 1];
  const prev = data[data.length - 2] || last;
  const lastIsUp = last.c >= prev.c;
  const upColor = "var(--c-up)";
  const dnColor = "var(--c-down)";

  const yTicks = 5;
  const ticks = [];
  for (let i = 0; i <= yTicks; i++) {
    const v = yMin + ((yMax - yMin) / yTicks) * i;
    ticks.push({ v, y: yScale(v) });
  }

  const onMove = (e) => {
    const r = wrapRef.current.getBoundingClientRect();
    const x = e.clientX - r.left;
    const y = e.clientY - r.top;
    const i = xToIdx(x);
    setHover({ i, x: xAt(i), y, mx: x });

    if (priceDrag) {
      const dy = e.clientY - priceDrag.y;
      const factor = Math.exp(-dy / 150);
      const next = Math.max(0.3, Math.min(5, priceDrag.scale * factor));
      setPriceScale(next);
      return;
    }
    if (rulerDraft && rulerDraft.dragging) {
      const idx = xToIdx(x);
      const price = yInv(y);
      setRulerDraft({ ...rulerDraft, idx2: idx, price2: price });
      return;
    }
    if (tlineDraft && tlineDraft.dragging) {
      const idx = xToIdx(x);
      const price = yInv(y);
      setTlineDraft({ ...tlineDraft, idx2: idx, price2: price });
      return;
    }
    if (shapeDraft && shapeDraft.dragging) {
      const idx = xToIdx(x);
      const price = yInv(y);
      if (shapeDraft.mode === "endpoint") {
        setShapeDraft({ ...shapeDraft, pts: [shapeDraft.pts[0], { idx, price }] });
      } else {
        const last = shapeDraft.pts[shapeDraft.pts.length - 1];
        if (Math.hypot((idx - last.idx) * cw, yScale(price) - yScale(last.price)) > 2) {
          setShapeDraft({ ...shapeDraft, pts: [...shapeDraft.pts, { idx, price }] });
        }
      }
      return;
    }
    if (drag) {
      const dx = e.clientX - drag.x;
      const candlesPerPx = (drag.end - drag.start) / chartW;
      const len = drag.end - drag.start;
      let start = drag.start - dx * candlesPerPx;
      if (start < 0) start = 0;
      if (start + len > data.length) start = data.length - len;
      setView({ start, end: start + len });
    }
  };
  const onLeave = () => {
    setHover(null);
    setDrag(null);
    setPriceDrag(null);
    if (rulerDraft && rulerDraft.dragging) setRulerDraft({ ...rulerDraft, dragging: false });
    if (tlineDraft && tlineDraft.dragging) setTlineDraft(null);
    if (shapeDraft && shapeDraft.dragging) {
      const pts = shapeDraft.pts;
      const degenerate = shapeDraft.mode === "endpoint" && pts.length >= 2 &&
        pts[0].idx === pts[1].idx && Math.abs(pts[0].price - pts[1].price) < 1e-6;
      if (pts.length >= 2 && !degenerate) commitShape(shapeDraft.type, pts);
      else setShapeDraft(null);
    }
  };

  const maLine = (vals, yfn = yScale) => {
    const pts = [];
    for (let i = vStart; i < vEnd; i++) {
      const v = vals[i];
      if (v == null) continue;
      pts.push(`${xAt(i).toFixed(1)},${yfn(v).toFixed(1)}`);
    }
    return pts.join(" ");
  };

  /* KC marker rendering */
  const kcMarker = (m) => {
    if (!showKC) return null;
    const d = data[m.idx];
    if (!d) return null;
    const x = xAt(m.idx);
    if (m.type === "wait-long" && !kcOptions.waitSignal) return null;
    if ((m.type === "long-entry") && !kcOptions.longEntry) return null;
    if ((m.type === "long-exit") && !kcOptions.longExit) return null;
    if ((m.type === "short-entry") && !kcOptions.shortEntry) return null;
    if ((m.type === "short-exit") && !kcOptions.shortExit) return null;
    if ((m.type === "high" || m.type === "low") && !kcOptions.swing) return null;
    if ((m.type === "neckline-up" || m.type === "neckline-dn") && !kcOptions.neckline) return null;
    if (m.type === "trend" && !kcOptions.trend) return null;

    if (m.type === "high") {
      const y = yScale(d.h) - 8;
      return (
        <g key={m.idx + m.type}>
          <rect x={x - 11} y={y - 9} width={22} height={14} rx="3" fill="#E8546B" opacity="0.92"/>
          <text x={x} y={y + 1} textAnchor="middle" fontSize="10" fill="#fff" fontWeight="700">{m.label}</text>
        </g>
      );
    }
    if (m.type === "low") {
      const y = yScale(d.l) + 14;
      return (
        <g key={m.idx + m.type}>
          <rect x={x - 11} y={y - 9} width={22} height={14} rx="3" fill="#3B7B8A" opacity="0.92"/>
          <text x={x} y={y + 1} textAnchor="middle" fontSize="10" fill="#fff" fontWeight="700">{m.label}</text>
        </g>
      );
    }

    const isEntry = m.type === "long-entry" || m.type === "short-exit";
    const isExit  = m.type === "long-exit" || m.type === "short-entry";
    const isWait  = m.type === "wait-long" || m.type === "wait-short";
    const isPreview = m.type === "preview";

    if (isEntry || isExit || isWait || isPreview) {
      const above = isExit;
      const tipY = above ? yScale(d.h) - 6 : yScale(d.l) + 6;
      const bubbleH = 30;
      const tailH = 5;
      const bubbleY = above ? tipY - tailH - bubbleH : tipY + tailH;
      const lineLen = Math.max(m.label.length, (m.reason || "").length);
      const bubbleW = Math.max(56, lineLen * 11 + 12);
      const bx = x - bubbleW / 2;

      let bg = "#2E2A36"; let txt = "#fff";
      if (m.type === "long-entry") bg = "#E8546B";
      else if (m.type === "long-exit" || m.type === "short-entry") bg = "#2E2A36";
      else if (m.type === "wait-long" || m.type === "wait-short") { bg = "#F6C85F"; txt = "#1a1410"; }
      else if (m.type === "preview") { bg = "transparent"; txt = "#4DA3FF"; }

      const tailPath = above
        ? `M${x-5},${bubbleY+bubbleH} L${x+5},${bubbleY+bubbleH} L${x},${bubbleY+bubbleH+tailH} Z`
        : `M${x-5},${bubbleY} L${x+5},${bubbleY} L${x},${bubbleY-tailH} Z`;

      return (
        <g key={m.idx + m.type}>
          <rect x={bx} y={bubbleY} width={bubbleW} height={bubbleH} rx="4"
            fill={bg}
            stroke={isPreview ? "#4DA3FF" : "none"}
            strokeDasharray={isPreview ? "3 2" : ""}
            opacity={isPreview ? 1 : 0.95}/>
          {!isPreview && <path d={tailPath} fill={bg} opacity="0.95"/>}
          <text x={x} y={bubbleY + 12} textAnchor="middle" fontSize="10" fill={txt} fontWeight="700"
            style={{fontFamily:"var(--font-sans)"}}>{m.label}</text>
          {m.reason && (
            <text x={x} y={bubbleY + 24} textAnchor="middle" fontSize="9.5" fill={txt} fontWeight="500" opacity="0.92"
              style={{fontFamily:"var(--font-sans)"}}>{m.reason}</text>
          )}
        </g>
      );
    }

    if (m.type === "trend") {
      const y = yScale(d.h) - 24;
      let bg = "#F6C85F", txt = "#1a1410";
      if (m.label === "多頭") { bg = "#E8546B"; txt = "#fff"; }
      else if (m.label === "空頭") { bg = "#22C55E"; txt = "#0B0F14"; }
      return (
        <g key={m.idx + m.type}>
          <rect x={x - 16} y={y - 8} width={32} height={14} rx="3" fill={bg}/>
          <text x={x} y={y + 2} textAnchor="middle" fontSize="10" fill={txt} fontWeight="700">{m.label}</text>
        </g>
      );
    }
    return null;
  };

  /* Necklines */
  const necklines = [];
  if (kcOptions.neckline && showKC) {
    const visi = (i) => i >= vStart && i < vEnd;
    effectiveEvents
      .filter((m) => m.type === "neckline-up" || m.type === "neckline-dn")
      .forEach((m) => {
        const startI = m.idx;
        const endI = m.endIdx != null ? m.endIdx : m.idx;
        if (!visi(startI) && !visi(endI)) return;
        const a = data[startI], b = data[endI];
        if (!a || !b) return;
        const y = m.priceLevel != null ? yScale(m.priceLevel)
                : m.type === "neckline-up" ? yScale(a.h) : yScale(a.l);
        necklines.push({ x1: xAt(startI), x2: xAt(endI), y1: y, y2: y, color: "#A78BFA" });
      });
  }

  /* Swing zig-zag */
  const swings = [];
  if (kcOptions.connect && kcOptions.swing && showKC) {
    const swingIdxs = effectiveEvents.filter(m => m.type === "high" || m.type === "low");
    for (let i = 0; i < swingIdxs.length - 1; i++) {
      const a = swingIdxs[i], b = swingIdxs[i+1];
      const inView = (a.idx >= vStart && a.idx < vEnd) || (b.idx >= vStart && b.idx < vEnd);
      if (!inView) continue;
      const ay = a.type === "high" ? yScale(data[a.idx].h) : yScale(data[a.idx].l);
      const by = b.type === "high" ? yScale(data[b.idx].h) : yScale(data[b.idx].l);
      swings.push({ x1: xAt(a.idx), y1: ay, x2: xAt(b.idx), y2: by });
    }
  }

  /* Sub panel renderers */
  const renderMACDPanel = (panel) => {
    if (!ind.macd) return null;
    const m = ind.macd;
    const visMacd = m.macd.slice(vStart, vEnd).filter((v) => v != null);
    const visSig = m.signal.slice(vStart, vEnd).filter((v) => v != null);
    const visHist = m.hist.slice(vStart, vEnd).filter((v) => v != null);
    const all = [...visMacd, ...visSig, ...visHist];
    if (all.length === 0) return null;
    const lo = Math.min(...all);
    const hi = Math.max(...all);
    const range = (hi - lo) || 1;
    const pad = range * 0.1;
    const yMinP = lo - pad;
    const yMaxP = hi + pad;
    const ys = (v) => panel.top + (1 - (v - yMinP) / (yMaxP - yMinP)) * panel.h;
    const zeroY = ys(0);
    const macdPts = [];
    const sigPts = [];
    for (let i = vStart; i < vEnd; i++) {
      if (m.macd[i] != null) macdPts.push(`${xAt(i).toFixed(1)},${ys(m.macd[i]).toFixed(1)}`);
      if (m.signal[i] != null) sigPts.push(`${xAt(i).toFixed(1)},${ys(m.signal[i]).toFixed(1)}`);
    }
    return (
      <g key="macd-panel">
        <line x1={padL} x2={padL + chartW} y1={panel.top} y2={panel.top} stroke="var(--border)" opacity="0.5"/>
        <text x={padL + 4} y={panel.top + 11} fontSize="9.5" fill="var(--text-3)" fontFamily="var(--font-mono)">MACD(12,26,9)</text>
        {/* zero line */}
        {zeroY > panel.top && zeroY < panel.bottom && (
          <line x1={padL} x2={padL + chartW} y1={zeroY} y2={zeroY} stroke="var(--text-3)" strokeDasharray="2 4" opacity="0.4"/>
        )}
        {/* histogram */}
        {visible.map((d, vi) => {
          const i = vStart + vi;
          const h = m.hist[i];
          if (h == null) return null;
          const isUp = h >= 0;
          const yTop = isUp ? ys(h) : zeroY;
          const yBot = isUp ? zeroY : ys(h);
          return (
            <rect key={"mh"+i}
              x={xAt(i) - bodyW/2}
              y={yTop}
              width={bodyW}
              height={Math.max(0.5, yBot - yTop)}
              fill={isUp ? "var(--c-up)" : "var(--c-down)"}
              opacity="0.55"/>
          );
        })}
        <polyline points={macdPts.join(" ")} fill="none" stroke="#4DA3FF" strokeWidth="1.2"/>
        <polyline points={sigPts.join(" ")} fill="none" stroke="#F6C85F" strokeWidth="1.2"/>
      </g>
    );
  };

  const renderRSIPanel = (panel) => {
    if (!ind.rsi) return null;
    const yMinP = 0, yMaxP = 100;
    const ys = (v) => panel.top + (1 - (v - yMinP) / (yMaxP - yMinP)) * panel.h;
    const pts = [];
    for (let i = vStart; i < vEnd; i++) {
      if (ind.rsi[i] != null) pts.push(`${xAt(i).toFixed(1)},${ys(ind.rsi[i]).toFixed(1)}`);
    }
    return (
      <g key="rsi-panel">
        <line x1={padL} x2={padL + chartW} y1={panel.top} y2={panel.top} stroke="var(--border)" opacity="0.5"/>
        <text x={padL + 4} y={panel.top + 11} fontSize="9.5" fill="var(--text-3)" fontFamily="var(--font-mono)">RSI(14)</text>
        {/* 70 / 30 reference */}
        <line x1={padL} x2={padL + chartW} y1={ys(70)} y2={ys(70)} stroke="var(--c-down)" strokeDasharray="2 4" opacity="0.5"/>
        <line x1={padL} x2={padL + chartW} y1={ys(30)} y2={ys(30)} stroke="var(--c-up)" strokeDasharray="2 4" opacity="0.5"/>
        <line x1={padL} x2={padL + chartW} y1={ys(50)} y2={ys(50)} stroke="var(--text-3)" strokeDasharray="1 3" opacity="0.3"/>
        <polyline points={pts.join(" ")} fill="none" stroke="#A78BFA" strokeWidth="1.4"/>
        <text x={padL + chartW + 4} y={ys(70) + 3} fontSize="9" fill="var(--c-down)" fontFamily="var(--font-mono)">70</text>
        <text x={padL + chartW + 4} y={ys(30) + 3} fontSize="9" fill="var(--c-up)" fontFamily="var(--font-mono)">30</text>
      </g>
    );
  };

  const renderKDPanel = (panel) => {
    if (!ind.kd) return null;
    const yMinP = 0, yMaxP = 100;
    const ys = (v) => panel.top + (1 - (v - yMinP) / (yMaxP - yMinP)) * panel.h;
    const kPts = [], dPts = [];
    for (let i = vStart; i < vEnd; i++) {
      if (ind.kd.k[i] != null) kPts.push(`${xAt(i).toFixed(1)},${ys(ind.kd.k[i]).toFixed(1)}`);
      if (ind.kd.d[i] != null) dPts.push(`${xAt(i).toFixed(1)},${ys(ind.kd.d[i]).toFixed(1)}`);
    }
    return (
      <g key="kd-panel">
        <line x1={padL} x2={padL + chartW} y1={panel.top} y2={panel.top} stroke="var(--border)" opacity="0.5"/>
        <text x={padL + 4} y={panel.top + 11} fontSize="9.5" fill="var(--text-3)" fontFamily="var(--font-mono)">KD(9,3,3)</text>
        <line x1={padL} x2={padL + chartW} y1={ys(80)} y2={ys(80)} stroke="var(--c-down)" strokeDasharray="2 4" opacity="0.5"/>
        <line x1={padL} x2={padL + chartW} y1={ys(20)} y2={ys(20)} stroke="var(--c-up)" strokeDasharray="2 4" opacity="0.5"/>
        <polyline points={kPts.join(" ")} fill="none" stroke="#F87171" strokeWidth="1.4"/>
        <polyline points={dPts.join(" ")} fill="none" stroke="#4DA3FF" strokeWidth="1.4"/>
        <text x={padL + chartW + 4} y={ys(80) + 3} fontSize="9" fill="var(--c-down)" fontFamily="var(--font-mono)">80</text>
        <text x={padL + chartW + 4} y={ys(20) + 3} fontSize="9" fill="var(--c-up)" fontFamily="var(--font-mono)">20</text>
      </g>
    );
  };

  const renderPanel = (panel) => {
    if (panel.key === "MACD") return renderMACDPanel(panel);
    if (panel.key === "RSI") return renderRSIPanel(panel);
    if (panel.key === "KD") return renderKDPanel(panel);
    return null;
  };

  const hoverData = hover ? data[hover.i] : null;

  /* ---------- 通用 shape 渲染 ---------- */
  const renderShape = (s, isPreview = false) => {
    const onDel = (e) => {
      e.stopPropagation();
      if (lockDrawings) return;
      onDrawingsChange({ ...drawings, shapes: drawings.shapes.filter(x => x.id !== s.id) });
    };
    const color = SHAPE_COLORS[s.type] || "#4DA3FF";
    const P = s.pts.map(p => ({ x: xAt(p.idx), y: yScale(p.price), idx: p.idx, price: p.price }));
    const cx = P.reduce((a, p) => a + p.x, 0) / P.length;
    const cy = P.reduce((a, p) => a + p.y, 0) / P.length;
    const Delete = (!lockDrawings && !isPreview) ? (
      <g style={{cursor: "pointer"}}
         onMouseDown={(e) => e.stopPropagation()}
         onClick={onDel}>
        <circle cx={cx} cy={cy} r="7" fill="rgba(11,15,20,0.85)" stroke={color} strokeWidth="0.8"/>
        <text x={cx} y={cy + 3} textAnchor="middle" fontSize="10" fill={color} fontWeight="700">×</text>
      </g>
    ) : null;

    let body = null;
    const t = s.type;
    const [a, b, c, d, e0] = P;

    if (t === "ray") {
      const dx = b.x - a.x, dy = b.y - a.y;
      const m = Math.max(0, (padL + chartW - a.x) / (dx || 0.001));
      body = <line x1={a.x} y1={a.y} x2={a.x + dx * Math.max(1, m)} y2={a.y + dy * Math.max(1, m)} stroke={color} strokeWidth="1.4"/>;
    } else if (t === "extend") {
      const dx = b.x - a.x, dy = b.y - a.y;
      const t1 = (padL - a.x) / (dx || 0.001);
      const t2 = (padL + chartW - a.x) / (dx || 0.001);
      body = <line x1={a.x + dx * t1} y1={a.y + dy * t1} x2={a.x + dx * t2} y2={a.y + dy * t2} stroke={color} strokeWidth="1.4"/>;
    } else if (t === "vline") {
      body = <line x1={a.x} x2={a.x} y1={padT} y2={padT + priceH} stroke={color} strokeWidth="1.4" strokeDasharray="3 3"/>;
    } else if (t === "parallel") {
      const ox = c.x - a.x, oy = c.y - a.y;
      body = (
        <g>
          <line x1={a.x} y1={a.y} x2={b.x} y2={b.y} stroke={color} strokeWidth="1.4"/>
          <line x1={a.x + ox} y1={a.y + oy} x2={b.x + ox} y2={b.y + oy} stroke={color} strokeWidth="1.4"/>
          <line x1={a.x + ox/2} y1={a.y + oy/2} x2={b.x + ox/2} y2={b.y + oy/2} stroke={color} strokeWidth="0.8" strokeDasharray="3 3" opacity="0.6"/>
        </g>
      );
    } else if (t === "fib-retrace") {
      /* drag 從低拖到高（或反向）— 自動以 hi/lo 計算 ratios */
      const FIB = [
        { r: 0,     color: "#9CA3AF" },
        { r: 0.236, color: "#EF4444" },
        { r: 0.382, color: "#F97316" },
        { r: 0.5,   color: "#84CC16" },
        { r: 0.618, color: "#22C55E" },
        { r: 0.786, color: "#06B6D4" },
        { r: 1,     color: "#9CA3AF" },
        { r: 1.618, color: "#3B82F6" },
      ];
      const BANDS = [
        "rgba(239,68,68,0.10)",   // 0 - 0.236
        "rgba(249,115,22,0.10)",  // 0.236 - 0.382
        "rgba(132,204,22,0.10)",  // 0.382 - 0.5
        "rgba(34,197,94,0.10)",   // 0.5 - 0.618
        "rgba(6,182,212,0.10)",   // 0.618 - 0.786
        "rgba(156,163,175,0.08)", // 0.786 - 1
        "rgba(99,102,241,0.10)",  // 1 - 1.618
      ];
      const hi = Math.max(a.price, b.price);
      const lo = Math.min(a.price, b.price);
      const fibPrice = (r) => hi - (hi - lo) * r;
      const fibY = (r) => yScale(fibPrice(r));
      const xL = Math.min(a.x, b.x);
      const xR = padL + chartW;
      const aIsHi = a.price >= b.price;
      const hiP = aIsHi ? a : b;
      const loP = aIsHi ? b : a;
      body = (
        <g>
          {BANDS.map((bandColor, i) => {
            const y1 = fibY(FIB[i].r);
            const y2 = fibY(FIB[i + 1].r);
            const top = Math.min(y1, y2);
            const h = Math.abs(y2 - y1);
            if (h < 0.1) return null;
            return <rect key={"b" + i} x={xL} y={top} width={xR - xL} height={h} fill={bandColor}/>;
          })}
          {FIB.map((lv, i) => {
            const y = fibY(lv.r);
            const price = fibPrice(lv.r);
            return (
              <g key={"l" + i}>
                <line x1={xL} x2={xR} y1={y} y2={y} stroke={lv.color} strokeWidth="1.2"/>
                <text x={xL - 6} y={y - 3} fontSize="10" fill={lv.color} fontWeight="700" textAnchor="end">
                  {lv.r} ({price.toLocaleString(undefined, {maximumFractionDigits: 2})})
                </text>
              </g>
            );
          })}
          <line x1={hiP.x} y1={fibY(0)} x2={loP.x} y2={fibY(1)}
                stroke="#6B7280" strokeWidth="1" strokeDasharray="6 4" opacity="0.85"/>
          <circle cx={hiP.x} cy={fibY(0)} r="5"
                  fill="rgba(11,15,20,0.9)" stroke="#3B82F6" strokeWidth="2"/>
          <circle cx={loP.x} cy={fibY(1)} r="5"
                  fill="rgba(11,15,20,0.9)" stroke="#3B82F6" strokeWidth="2"/>
        </g>
      );
    } else if (t === "fib-extend") {
      const left = Math.min(a.x, b.x, c.x), right = Math.max(a.x, b.x, c.x);
      body = <g>{FIB_RATIOS_EXTEND.map((r, i) => {
        const y = c.y - (b.y - a.y) * r;
        return <g key={i}>
          <line x1={left} x2={right} y1={y} y2={y} stroke={color} strokeWidth="1" opacity="0.7"/>
          <text x={right + 4} y={y + 3} fontSize="10" fill={color}>{r.toFixed(3)}</text>
        </g>;
      })}</g>;
    } else if (t === "fib-channel") {
      const ox = c.x - a.x, oy = c.y - a.y;
      body = <g>{FIB_RATIOS_RETRACE.map((r, i) => (
        <line key={i} x1={a.x + ox * r} y1={a.y + oy * r} x2={b.x + ox * r} y2={b.y + oy * r} stroke={color} strokeWidth="1" opacity="0.7"/>
      ))}</g>;
    } else if (t === "fib-timezone") {
      const base = b.x - a.x;
      body = <g>{FIB_TIMEZONE_SEQ.map((n, i) => {
        const x = a.x + base * n;
        if (x > padL + chartW) return null;
        return <g key={i}>
          <line x1={x} x2={x} y1={padT} y2={padT + priceH} stroke={color} strokeWidth="0.8" strokeDasharray="2 3" opacity="0.7"/>
          <text x={x + 2} y={padT + 10} fontSize="9" fill={color}>{n}</text>
        </g>;
      })}</g>;
    } else if (t === "fib-fan") {
      const dy = b.y - a.y;
      body = <g>{[0.382, 0.5, 0.618].map((r, i) => {
        const targY = a.y + dy * r;
        const dx2 = b.x - a.x, dy2 = targY - a.y;
        const m = Math.max(1, (padL + chartW - a.x) / (dx2 || 0.001));
        return <line key={i} x1={a.x} y1={a.y} x2={a.x + dx2 * m} y2={a.y + dy2 * m} stroke={color} strokeWidth="1" opacity="0.7"/>;
      })}</g>;
    } else if (t === "fib-wedge") {
      const dx = b.x - a.x, dy = b.y - a.y;
      body = <g>{[0.382, 0.5, 0.618, 1].map((r, i) => (
        <line key={i} x1={a.x} y1={a.y} x2={a.x + dx * (1 + r)} y2={a.y + dy} stroke={color} strokeWidth="1" opacity="0.7"/>
      ))}</g>;
    } else if (t === "gann-box") {
      const left = Math.min(a.x, b.x), right = Math.max(a.x, b.x);
      const top = Math.min(a.y, b.y), bot = Math.max(a.y, b.y);
      body = (
        <g>
          <rect x={left} y={top} width={right - left} height={bot - top} fill="none" stroke={color} strokeWidth="1.2"/>
          {[0.25, 0.5, 0.75].map((r, i) => (
            <g key={i}>
              <line x1={left + (right - left) * r} x2={left + (right - left) * r} y1={top} y2={bot} stroke={color} strokeWidth="0.7" opacity="0.5"/>
              <line x1={left} x2={right} y1={top + (bot - top) * r} y2={top + (bot - top) * r} stroke={color} strokeWidth="0.7" opacity="0.5"/>
            </g>
          ))}
          <line x1={left} y1={top} x2={right} y2={bot} stroke={color} strokeWidth="0.8" opacity="0.6"/>
          <line x1={left} y1={bot} x2={right} y2={top} stroke={color} strokeWidth="0.8" opacity="0.6"/>
        </g>
      );
    } else if (t === "gann-square") {
      const side = Math.min(Math.abs(b.x - a.x), Math.abs(b.y - a.y));
      const sx = a.x, sy = a.y;
      const ex = sx + side * Math.sign(b.x - a.x || 1);
      const ey = sy + side * Math.sign(b.y - a.y || 1);
      const left = Math.min(sx, ex), right = Math.max(sx, ex);
      const top = Math.min(sy, ey), bot = Math.max(sy, ey);
      body = (
        <g>
          <rect x={left} y={top} width={right - left} height={bot - top} fill="none" stroke={color} strokeWidth="1.2"/>
          <line x1={left} y1={top} x2={right} y2={bot} stroke={color} strokeWidth="0.8" opacity="0.6"/>
          <line x1={left} y1={bot} x2={right} y2={top} stroke={color} strokeWidth="0.8" opacity="0.6"/>
        </g>
      );
    } else if (t === "gann-fan") {
      const dx = b.x - a.x;
      const dy = b.y - a.y;
      const m = Math.max(1, (padL + chartW - a.x) / (dx || 0.001));
      body = <g>{GANN_RATIOS.map((r, i) => (
        <line key={i} x1={a.x} y1={a.y} x2={a.x + dx * m} y2={a.y + dy * r * m} stroke={color} strokeWidth="0.9" opacity="0.7"/>
      ))}</g>;
    } else if (t === "rect") {
      const left = Math.min(a.x, b.x), right = Math.max(a.x, b.x);
      const top = Math.min(a.y, b.y), bot = Math.max(a.y, b.y);
      body = <rect x={left} y={top} width={right - left} height={bot - top}
                   fill="rgba(239,68,68,0.15)"
                   stroke="rgba(239,68,68,0.6)" strokeWidth="1"/>;
    } else if (t === "circle") {
      const rr = Math.hypot(b.x - a.x, b.y - a.y);
      body = <circle cx={a.x} cy={a.y} r={rr} fill="none" stroke={color} strokeWidth="1.4"/>;
    } else if (t === "ellipse") {
      const ecx = (a.x + b.x) / 2, ecy = (a.y + b.y) / 2;
      const rx = Math.abs(b.x - a.x) / 2, ry = Math.abs(b.y - a.y) / 2;
      body = <ellipse cx={ecx} cy={ecy} rx={rx} ry={ry} fill="none" stroke={color} strokeWidth="1.4"/>;
    } else if (t === "triangle" || t === "triangle-pattern") {
      body = <polygon points={`${a.x},${a.y} ${b.x},${b.y} ${c.x},${c.y}`} fill="none" stroke={color} strokeWidth="1.4"/>;
    } else if (t === "arc") {
      body = <path d={`M ${a.x} ${a.y} Q ${b.x} ${b.y} ${c.x} ${c.y}`} fill="none" stroke={color} strokeWidth="1.4"/>;
    } else if (t === "curve") {
      body = <path d={`M ${a.x} ${a.y} C ${b.x} ${b.y}, ${b.x} ${b.y}, ${c.x} ${c.y}`} fill="none" stroke={color} strokeWidth="1.4"/>;
    } else if (t === "polyline") {
      body = <polyline points={P.map(p => `${p.x},${p.y}`).join(" ")} fill="none" stroke={color} strokeWidth="1.4"/>;
    } else if (t === "brush") {
      const dpath = "M " + P.map(p => `${p.x} ${p.y}`).join(" L ");
      body = <path d={dpath} fill="none" stroke={color} strokeWidth="1.6" strokeLinejoin="round" strokeLinecap="round"/>;
    } else if (t === "highlight") {
      const dpath = "M " + P.map(p => `${p.x} ${p.y}`).join(" L ");
      body = <path d={dpath} fill="none" stroke="#F6C85F" strokeWidth="12" strokeLinejoin="round" strokeLinecap="round" opacity="0.35"/>;
    } else if (t === "arrow-mark") {
      const ang = Math.atan2(b.y - a.y, b.x - a.x);
      const h = 9;
      const hx1 = b.x - h * Math.cos(ang - Math.PI / 7);
      const hy1 = b.y - h * Math.sin(ang - Math.PI / 7);
      const hx2 = b.x - h * Math.cos(ang + Math.PI / 7);
      const hy2 = b.y - h * Math.sin(ang + Math.PI / 7);
      body = (
        <g>
          <line x1={a.x} y1={a.y} x2={b.x} y2={b.y} stroke={color} strokeWidth="1.4"/>
          <polygon points={`${b.x},${b.y} ${hx1},${hy1} ${hx2},${hy2}`} fill={color}/>
        </g>
      );
    } else if (t === "arrow-up") {
      const px = a.x, py = a.y;
      body = <polygon points={`${px},${py-12} ${px-7},${py} ${px-3},${py} ${px-3},${py+10} ${px+3},${py+10} ${px+3},${py} ${px+7},${py}`} fill={color}/>;
    } else if (t === "arrow-down") {
      const px = a.x, py = a.y;
      body = <polygon points={`${px},${py+12} ${px-7},${py} ${px-3},${py} ${px-3},${py-10} ${px+3},${py-10} ${px+3},${py} ${px+7},${py}`} fill={color}/>;
    } else if (t === "emoji") {
      body = <text x={a.x} y={a.y + 6} fontSize="22" textAnchor="middle">{s.opts.text || "⭐"}</text>;
    } else if (t === "flag") {
      body = (
        <g>
          <line x1={a.x} x2={a.x} y1={a.y} y2={a.y + 18} stroke={color} strokeWidth="1.4"/>
          <polygon points={`${a.x},${a.y} ${a.x+14},${a.y+4} ${a.x},${a.y+8}`} fill={color}/>
        </g>
      );
    } else if (t === "text" || t === "label" || t === "note") {
      const txt = s.opts.text || "";
      const fontSize = t === "note" ? 11 : 12;
      const approxW = Math.max(8, txt.length * fontSize * 0.75);
      const isLabel = t === "label", isNote = t === "note";
      body = (
        <g>
          {(isLabel || isNote) && (
            <rect x={a.x - 6} y={a.y - fontSize - 4}
                  width={approxW + 12} height={fontSize + 8}
                  rx="3"
                  fill={isLabel ? color : "rgba(246,200,95,0.9)"}/>
          )}
          <text x={a.x} y={a.y - 4} fontSize={fontSize}
                fill={isLabel || isNote ? "#0B0F14" : color}
                fontWeight={isLabel || isNote ? 600 : 400}>{txt}</text>
        </g>
      );
    } else if (t === "abcd") {
      body = (
        <g>
          <polyline points={P.map(p => `${p.x},${p.y}`).join(" ")} fill="none" stroke={color} strokeWidth="1.4"/>
          {["A","B","C","D"].map((lbl, i) => P[i] && (
            <text key={i} x={P[i].x + 4} y={P[i].y - 4} fontSize="11" fontWeight="700" fill={color}>{lbl}</text>
          ))}
        </g>
      );
    } else if (t === "elliott") {
      body = (
        <g>
          <polyline points={P.map(p => `${p.x},${p.y}`).join(" ")} fill="none" stroke={color} strokeWidth="1.4"/>
          {[1,2,3,4,5].map((n, i) => P[i] && (
            <text key={i} x={P[i].x + 4} y={P[i].y - 4} fontSize="11" fontWeight="700" fill={color}>({n})</text>
          ))}
        </g>
      );
    } else if (t === "head-shoulders") {
      const labels = ["LS","頭","RS","頸L","頸R"];
      body = (
        <g>
          <polyline points={P.slice(0,3).map(p => `${p.x},${p.y}`).join(" ")} fill="none" stroke={color} strokeWidth="1.4"/>
          {labels.map((lbl, i) => P[i] && (
            <text key={i} x={P[i].x + 4} y={P[i].y - 4} fontSize="10" fill={color}>{lbl}</text>
          ))}
          {d && e0 && (
            <line x1={d.x} y1={d.y} x2={e0.x} y2={e0.y} stroke={color} strokeWidth="0.8" strokeDasharray="3 3"/>
          )}
        </g>
      );
    } else if (t === "long-pos" || t === "short-pos") {
      const isLong = t === "long-pos";
      const left = Math.min(a.x, b.x), right = Math.max(a.x, b.x);
      const top = Math.min(a.y, b.y), bot = Math.max(a.y, b.y);
      const entryY = (top + bot) / 2;
      body = (
        <g>
          <rect x={left} y={isLong ? entryY : top} width={right - left} height={isLong ? (bot - entryY) : (entryY - top)} fill="rgba(34,197,94,0.10)"/>
          <rect x={left} y={isLong ? top : entryY} width={right - left} height={isLong ? (entryY - top) : (bot - entryY)} fill="rgba(239,68,68,0.10)"/>
          <rect x={left} y={top} width={right - left} height={bot - top} fill="none" stroke={color} strokeWidth="1"/>
          <line x1={left} x2={right} y1={entryY} y2={entryY} stroke={color} strokeWidth="1.4"/>
          <text x={left + 4} y={entryY - 3} fontSize="10" fill={color}>{isLong ? "做多 進場" : "做空 進場"}</text>
        </g>
      );
    } else if (t === "price-range") {
      const top = Math.min(a.y, b.y), bot = Math.max(a.y, b.y);
      body = (
        <g>
          <rect x={padL} y={top} width={chartW} height={bot - top} fill="rgba(77,163,255,0.10)" stroke={color} strokeWidth="1" strokeDasharray="3 3"/>
          <text x={padL + chartW - 4} y={top + 11} fontSize="10" textAnchor="end" fill={color}>{(yInv(top) || 0).toFixed(2)}</text>
          <text x={padL + chartW - 4} y={bot - 3} fontSize="10" textAnchor="end" fill={color}>{(yInv(bot) || 0).toFixed(2)}</text>
        </g>
      );
    } else if (t === "date-range") {
      const left = Math.min(a.x, b.x), right = Math.max(a.x, b.x);
      const days = Math.abs(b.idx - a.idx);
      body = (
        <g>
          <rect x={left} y={padT} width={right - left} height={priceH} fill="rgba(77,163,255,0.08)" stroke={color} strokeWidth="1" strokeDasharray="3 3"/>
          <text x={(left + right) / 2} y={padT + 14} fontSize="10" textAnchor="middle" fill={color}>{days} 根</text>
        </g>
      );
    }

    if (!body) return null;
    return (
      <g key={s.id} pointerEvents={lockDrawings ? "none" : undefined}>
        {body}
        {Delete}
      </g>
    );
  };

  const renderShapes = () => {
    if (hideDrawings) return null;
    return (drawings.shapes || []).map(renderShape);
  };

  /* shape 預覽：尚未 commit 的點顯示 */
  const renderShapeDraft = () => {
    if (!shapeDraft || hideDrawings) return null;
    const spec = SHAPE_SPECS[shapeDraft.type];
    if (!spec) return null;
    const color = SHAPE_COLORS[shapeDraft.type] || "#4DA3FF";
    /* drag / drag2：reuse renderShape 完整預覽（不畫刪除按鈕） */
    if (spec.pts === "drag" || spec.pts === "drag2") {
      if (shapeDraft.pts.length < 2) return null;
      const fake = { id: "draft", type: shapeDraft.type, pts: shapeDraft.pts, opts: {} };
      return <g pointerEvents="none" opacity="0.85">{renderShape(fake, true)}</g>;
    }
    /* 多點工具：圓點 + 連線預覽 */
    const P = shapeDraft.pts.map(p => ({ x: xAt(p.idx), y: yScale(p.price) }));
    const dots = P.map((p, i) => <circle key={i} cx={p.x} cy={p.y} r="3" fill={color}/>);
    const preview = P.length >= 2 ? (
      <polyline points={P.map(p => `${p.x},${p.y}`).join(" ")} fill="none"
                stroke={color} strokeWidth="1.2" strokeDasharray="3 3" opacity="0.7"/>
    ) : null;
    return <g pointerEvents="none">{preview}{dots}</g>;
  };

  /* Drawings — hlines / tlines / ruler */
  const renderHLines = () =>
    (drawings.hlines || []).map((h) => {
      const y = yScale(h.price);
      if (y < padT || y > padT + priceH) return null;
      return (
        <g key={h.id}>
          <line x1={padL} x2={padL + chartW} y1={y} y2={y}
            stroke="#F6C85F" strokeWidth="1.2" strokeDasharray="6 4" opacity="0.85"/>
          <rect x={padL + chartW + 4} y={y - 7} width={42} height={14} rx="2" fill="#F6C85F"/>
          <text x={padL + chartW + 25} y={y + 3} fontSize="10" textAnchor="middle"
            fill="#1a1410" fontFamily="var(--font-mono)" fontWeight="700">{h.price.toFixed(2)}</text>
          {/* delete button */}
          <g style={{cursor: "pointer"}}
             onMouseDown={(e) => e.stopPropagation()}
             onClick={(e) => {
               e.stopPropagation();
               onDrawingsChange({ ...drawings, hlines: drawings.hlines.filter((x) => x.id !== h.id) });
             }}>
            <circle cx={padL + 4} cy={y} r="6" fill="rgba(11,15,20,0.8)" stroke="#F6C85F" strokeWidth="0.8"/>
            <text x={padL + 4} y={y + 3} textAnchor="middle" fontSize="9" fill="#F6C85F" fontWeight="700">×</text>
          </g>
        </g>
      );
    });

  const renderTLines = () => {
    const elems = (drawings.tlines || []).map((t) => {
      const x1 = xAt(t.idx1), y1 = yScale(t.price1);
      const x2 = xAt(t.idx2), y2 = yScale(t.price2);
      return (
        <g key={t.id}>
          <line x1={x1} x2={x2} y1={y1} y2={y2}
            stroke="#4DA3FF" strokeWidth="1.4" opacity="0.9"/>
          <circle cx={x1} cy={y1} r="3" fill="#4DA3FF"/>
          <circle cx={x2} cy={y2} r="3" fill="#4DA3FF"/>
          <g style={{cursor: "pointer"}}
             onMouseDown={(e) => e.stopPropagation()}
             onClick={(e) => {
               e.stopPropagation();
               onDrawingsChange({ ...drawings, tlines: drawings.tlines.filter((x) => x.id !== t.id) });
             }}>
            <circle cx={(x1 + x2) / 2} cy={(y1 + y2) / 2} r="6" fill="rgba(11,15,20,0.8)" stroke="#4DA3FF" strokeWidth="0.8"/>
            <text x={(x1 + x2) / 2} y={(y1 + y2) / 2 + 3} textAnchor="middle" fontSize="9" fill="#4DA3FF" fontWeight="700">×</text>
          </g>
        </g>
      );
    });
    if (tlineDraft) {
      const x1 = xAt(tlineDraft.idx1), y1 = yScale(tlineDraft.price1);
      const i2 = tlineDraft.idx2 != null ? tlineDraft.idx2 : (hover ? hover.i : tlineDraft.idx1);
      const p2 = tlineDraft.price2 != null ? tlineDraft.price2 : (hover ? yInv(hover.y) : tlineDraft.price1);
      const x2 = xAt(i2), y2 = yScale(p2);
      elems.push(
        <g key="tline-draft" pointerEvents="none">
          <line x1={x1} x2={x2} y1={y1} y2={y2} stroke="#4DA3FF" strokeWidth="1.4" strokeDasharray="4 3" opacity="0.85"/>
          <circle cx={x1} cy={y1} r="3" fill="#4DA3FF"/>
          <circle cx={x2} cy={y2} r="3" fill="#4DA3FF"/>
        </g>
      );
    }
    return elems;
  };

  const renderRuler = () => {
    if (!rulerDraft) return null;
    const r = rulerDraft;
    const x1 = xAt(r.idx1), x2 = xAt(r.idx2);
    const y1 = yScale(r.price1), y2 = yScale(r.price2);
    const left = Math.min(x1, x2), right = Math.max(x1, x2);
    const top = Math.min(y1, y2), bot = Math.max(y1, y2);
    const days = Math.abs(r.idx2 - r.idx1);
    const priceDiff = r.price2 - r.price1;
    const pct = r.price1 > 0 ? (priceDiff / r.price1) * 100 : 0;
    const color = pct >= 0 ? "var(--c-up)" : "var(--c-down)";
    const colorRgba = pct >= 0 ? "rgba(239,68,68,0.12)" : "rgba(34,197,94,0.12)";
    const label = `${pct >= 0 ? "+" : ""}${pct.toFixed(2)}% · ${days} 根 · ${priceDiff >= 0 ? "+" : ""}${priceDiff.toFixed(2)}`;
    const cnCount = (label.match(/[一-龥]/g) || []).length;
    const otherCount = label.length - cnCount;
    const labelW = Math.max(120, Math.ceil(cnCount * 12 + otherCount * 6.8 + 24));
    const labelH = 20;
    const cx = (left + right) / 2;
    const labelX = cx - labelW / 2;
    const labelY = top - labelH - 4;
    return (
      <g pointerEvents={r.dragging ? "none" : undefined}>
        <rect x={left} y={top} width={right - left} height={bot - top}
          fill={colorRgba} stroke={color} strokeWidth="1" strokeDasharray="3 3"/>
        <line x1={x1} x2={x2} y1={y1} y2={y2} stroke={color} strokeWidth="1.4"/>
        <rect x={labelX} y={labelY} width={labelW} height={labelH} rx="3" fill="rgba(11,15,20,0.92)" stroke={color}/>
        <text x={cx} y={labelY + labelH / 2 + 3.5} textAnchor="middle" fontSize="10" fill={color} fontWeight="700"
          fontFamily="var(--font-mono)">
          {label}
        </text>
        {!r.dragging && (
          <g style={{cursor: "pointer"}}
             onMouseDown={(e) => e.stopPropagation()}
             onClick={(e) => { e.stopPropagation(); setRulerDraft(null); }}>
            <circle cx={labelX + labelW + 4} cy={labelY + labelH / 2} r="7" fill="rgba(11,15,20,0.9)" stroke={color} strokeWidth="0.8"/>
            <text x={labelX + labelW + 4} y={labelY + labelH / 2 + 3} textAnchor="middle" fontSize="10" fill={color} fontWeight="700">×</text>
          </g>
        )}
      </g>
    );
  };

  const cursorStyle =
    priceDrag ? "ns-resize" :
    drag ? "grabbing" :
    toolMode === "hline" ? "crosshair" :
    toolMode === "tline" ? "crosshair" :
    toolMode === "ruler" ? "crosshair" : "crosshair";

  return (
    <div className="chart-wrap" ref={wrapRef} style={{ position: "relative", width: "100%" }}>
      <div className="chart-zoom-ctrls">
        <button className="btn ghost icon-btn" title="縮小" onClick={() => zoomBy(1.3)}><Icon name="minus" size={13}/></button>
        <button className="btn ghost icon-btn" title="放大" onClick={() => zoomBy(0.77)}><Icon name="plus" size={13}/></button>
        <button className="btn ghost" style={{padding:"2px 8px", fontSize:"var(--fs-xs)"}} onClick={resetView} title="重設">重設</button>
        <span className="dim mono" style={{fontSize:"var(--fs-xs)", marginLeft:6}}>{vCount} 根</span>
      </div>

      {(showDataWindow || showFxWatch) && (() => {
        const idx = hover ? hover.i : data.length - 1;
        const k = data[idx];
        if (!k) return null;
        const indVals = {};
        if (showIndicators.RSI && ind.rsi) indVals.RSI = ind.rsi[idx];
        if (showIndicators.MACD && ind.macd) {
          indVals.MACD = ind.macd.macd && ind.macd.macd[idx];
          indVals["Signal"] = ind.macd.signal && ind.macd.signal[idx];
        }
        if (showIndicators.KD && ind.kd) {
          indVals.K = ind.kd.k && ind.kd.k[idx];
          indVals.D = ind.kd.d && ind.kd.d[idx];
        }
        if (showIndicators.EMA && ind.ema20) indVals.EMA20 = ind.ema20[idx];
        const maRows = [];
        if (showIndicators.MA) {
          mas.forEach((m) => {
            if (showMA[m.period]) {
              const v = m.values[idx];
              if (v != null) maRows.push({ label: `MA${m.period}`, value: v, color: m.color });
            }
          });
        }
        return (
          <div className="chart-data-panel">
            <div className="cdp-head">
              <span className="cdp-title">資料視窗</span>
              <span className="cdp-date mono">{k.date}</span>
            </div>
            {showDataWindow && (
              <div className="cdp-section">
                <div className="cdp-row"><span className="dim">開</span><span className="mono">{k.o.toFixed(2)}</span></div>
                <div className="cdp-row"><span className="dim">高</span><span className="mono up">{k.h.toFixed(2)}</span></div>
                <div className="cdp-row"><span className="dim">低</span><span className="mono down">{k.l.toFixed(2)}</span></div>
                <div className="cdp-row"><span className="dim">收</span><span className="mono">{k.c.toFixed(2)}</span></div>
                <div className="cdp-row"><span className="dim">量</span><span className="mono">{Math.round(k.v).toLocaleString()}</span></div>
              </div>
            )}
            {showFxWatch && (maRows.length > 0 || Object.keys(indVals).length > 0) && (
              <div className="cdp-section cdp-fx">
                {maRows.map((r) => (
                  <div key={r.label} className="cdp-row">
                    <span style={{color: r.color}}>{r.label}</span>
                    <span className="mono">{r.value.toFixed(2)}</span>
                  </div>
                ))}
                {Object.entries(indVals).map(([k2, v]) => v != null && (
                  <div key={k2} className="cdp-row">
                    <span className="dim">{k2}</span>
                    <span className="mono">{Number(v).toFixed(2)}</span>
                  </div>
                ))}
              </div>
            )}
          </div>
        );
      })()}
      <svg width={w} height={totalH}
           onMouseDown={onMouseDown}
           onMouseUp={onMouseUp}
           onMouseMove={onMove}
           onMouseLeave={onLeave}
           style={{ display: "block", cursor: cursorStyle, userSelect: "none" }}>
        <defs>
          <clipPath id="chart-clip">
            <rect x={padL} y={padT} width={chartW} height={priceH + volH + 6}/>
          </clipPath>
        </defs>
        {/* Y grid */}
        {ticks.map((t, i) => (
          <g key={i}>
            <line x1={padL} x2={padL + chartW} y1={t.y} y2={t.y} stroke="var(--border)" strokeDasharray="2 4" opacity="0.5"/>
            <text x={padL + chartW + 6} y={t.y + 3} fontSize="10" fill="var(--text-3)" fontFamily="var(--font-mono)">
              {t.v.toFixed(2)}
            </text>
          </g>
        ))}

        <line x1={padL} x2={padL + chartW} y1={volTop - 2} y2={volTop - 2} stroke="var(--border)"/>

        {/* Volume bars */}
        {visible.map((d, vi) => {
          const i = vStart + vi;
          const isUp = d.c >= d.o;
          const barH = volTop + volH - vScale(d.v);
          const forming = i === formingIdx;
          return (
            <rect key={"v"+i}
              x={xAt(i) - bodyW/2}
              y={vScale(d.v)}
              width={bodyW}
              height={Math.max(1, barH)}
              fill={isUp ? upColor : dnColor}
              opacity={forming ? 0.5 : 0.36}
            />
          );
        })}

        {/* Volume MA20 */}
        {showIndicators.VolMA && (() => {
          const v20 = ma(data, "v", 20);
          const pts = [];
          for (let i = vStart; i < vEnd; i++) {
            if (v20[i] == null) continue;
            pts.push(`${xAt(i).toFixed(1)},${vScale(v20[i]).toFixed(1)}`);
          }
          return <polyline points={pts.join(" ")} fill="none" stroke="var(--text-3)" strokeWidth="1" opacity="0.7"/>;
        })()}

        {/* MA lines */}
        {showIndicators.MA && mas.map(m => showMA[m.period] && (
          <polyline key={m.period} points={maLine(m.values)} fill="none" stroke={m.color} strokeWidth="1.2" opacity="0.95"/>
        ))}

        {/* EMA20 overlay */}
        {showIndicators.EMA && ind.ema20 && (
          <polyline points={maLine(ind.ema20)} fill="none" stroke="#22D3EE" strokeWidth="1.2" strokeDasharray="3 2" opacity="0.95"/>
        )}

        {/* Bollinger Bands */}
        {showIndicators.BB && ind.bb && (
          <g>
            <polyline points={maLine(ind.bb.upper)} fill="none" stroke="#A78BFA" strokeWidth="1" opacity="0.7"/>
            <polyline points={maLine(ind.bb.middle)} fill="none" stroke="#A78BFA" strokeWidth="0.8" strokeDasharray="2 3" opacity="0.5"/>
            <polyline points={maLine(ind.bb.lower)} fill="none" stroke="#A78BFA" strokeWidth="1" opacity="0.7"/>
          </g>
        )}

        {/* Necklines */}
        {necklines.map((n, i) => (
          <line key={"n"+i} x1={n.x1} x2={n.x2} y1={n.y1} y2={n.y2} stroke={n.color} strokeWidth="1" strokeDasharray="4 3" opacity="0.85"/>
        ))}

        {/* Swing zig-zag — 用 text-2 跟著主題切換顏色 */}
        {swings.map((s, i) => (
          <line key={"s"+i} x1={s.x1} y1={s.y1} x2={s.x2} y2={s.y2}
            stroke="var(--text-2)" strokeWidth="1.4" opacity="0.85"/>
        ))}

        {/* Candles */}
        {visible.map((d, vi) => {
          const i = vStart + vi;
          const isUp = d.c >= d.o;
          const color = isUp ? upColor : dnColor;
          const yOpen = yScale(d.o), yClose = yScale(d.c);
          const yHigh = yScale(d.h), yLow = yScale(d.l);
          const top = Math.min(yOpen, yClose);
          const bH = Math.max(1, Math.abs(yClose - yOpen));
          const forming = i === formingIdx;
          return (
            <g key={i} opacity={forming ? 0.85 : 1}>
              <line x1={xAt(i)} x2={xAt(i)} y1={yHigh} y2={yLow} stroke={color} strokeWidth="1"
                strokeDasharray={forming ? "2 1.5" : ""}/>
              <rect x={xAt(i) - bodyW/2} y={top} width={bodyW} height={bH}
                fill={color}
                stroke={color} strokeWidth="0.8"
                strokeDasharray={forming ? "2 1.5" : ""}
                opacity={isUp ? 0.95 : 1}/>
            </g>
          );
        })}

        {/* MA labels */}
        {showIndicators.MA && kcOptions.maLabel && mas.filter(m => showMA[m.period]).map((m, i) => {
          const lastV = m.values[m.values.length - 1];
          if (lastV == null) return null;
          return (
            <g key={m.period}>
              <rect x={padL + chartW + 4} y={yScale(lastV) - 7} width={42} height={14} rx="2" fill={m.color} opacity="0.92"/>
              <text x={padL + chartW + 25} y={yScale(lastV) + 3} fontSize="9" textAnchor="middle" fill="#0B0F14"
                fontFamily="var(--font-mono)" fontWeight="700">
                MA{m.period}
              </text>
            </g>
          );
        })}

        {/* Current price line */}
        <g>
          <line x1={padL} x2={padL + chartW} y1={yScale(last.c)} y2={yScale(last.c)}
            stroke={lastIsUp ? upColor : dnColor} strokeWidth="0.8" strokeDasharray="3 3" opacity="0.7"/>
          <rect x={padL + chartW + 4} y={yScale(last.c) - 9} width={56} height={18} rx="2"
            fill={lastIsUp ? upColor : dnColor}/>
          <text x={padL + chartW + 32} y={yScale(last.c) + 4} fontSize="11" textAnchor="middle" fill="#0B0F14"
            fontFamily="var(--font-mono)" fontWeight="700">
            {last.c.toFixed(2)}
          </text>
        </g>

        {/* KC markers */}
        {showKC && effectiveEvents.filter(m => m.idx >= vStart && m.idx < vEnd).map(kcMarker)}

        {/* Drawings (主圖內) — hide 時整層不渲染 */}
        {!hideDrawings && renderHLines()}
        {!hideDrawings && renderTLines()}
        {!hideDrawings && renderRuler()}
        {renderShapes()}
        {renderShapeDraft()}

        {/* Sub panels */}
        {panels.map(renderPanel)}

        {/* 右側價格軸拖曳熱區 */}
        <rect
          x={padL + chartW}
          y={padT}
          width={padR}
          height={priceH}
          fill="transparent"
          style={{ cursor: "ns-resize" }}
          onMouseDown={onPriceAxisDown}
          onDoubleClick={onPriceAxisDoubleClick}
        />

        {/* Crosshair — 縱線貫穿 price/vol/sub panels */}
        {hover && (
          <g pointerEvents="none">
            <line x1={hover.x} x2={hover.x} y1={padT} y2={lastPanelBottom}
              stroke="var(--text-2)" strokeDasharray="2 3" opacity="0.6"/>
            {hover.y >= padT && hover.y <= padT + priceH && (
              <line x1={padL} x2={padL + chartW} y1={hover.y} y2={hover.y}
                stroke="var(--text-2)" strokeDasharray="2 3" opacity="0.6"/>
            )}
          </g>
        )}
      </svg>

      {/* Tooltip */}
      {hover && hoverData && (() => {
        const i = hover.i;
        const rows = [];
        rows.push(["O 開盤", hoverData.o.toFixed(2)]);
        rows.push(["H 最高", hoverData.h.toFixed(2), "var(--c-up)"]);
        rows.push(["L 最低", hoverData.l.toFixed(2), "var(--c-down)"]);
        rows.push(["C 收盤", hoverData.c.toFixed(2)]);
        const prevC = i > 0 ? data[i - 1].c : null;
        if (prevC != null && prevC) {
          const pct = ((hoverData.c - prevC) / prevC) * 100;
          const chg = hoverData.c - prevC;
          rows.push(["Chg% 漲跌", `${chg >= 0 ? "+" : ""}${chg.toFixed(2)} (${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%)`, pct >= 0 ? "var(--c-up)" : "var(--c-down)"]);
        }
        rows.push(["Vol 成交量", `${(hoverData.v/1e6).toFixed(2)}M`]);
        if (showIndicators.EMA && ind.ema20 && ind.ema20[i] != null)
          rows.push(["EMA20 指數均線", ind.ema20[i].toFixed(2)]);
        if (showIndicators.BB && ind.bb && ind.bb.middle[i] != null)
          rows.push(["BB 布林通道", `${ind.bb.upper[i].toFixed(1)} / ${ind.bb.middle[i].toFixed(1)} / ${ind.bb.lower[i].toFixed(1)}`]);
        if (showIndicators.MACD && ind.macd && ind.macd.macd[i] != null)
          rows.push(["MACD 動能", `${ind.macd.macd[i].toFixed(2)} / sig ${ind.macd.signal[i].toFixed(2)} / hist ${ind.macd.hist[i].toFixed(2)}`]);
        if (showIndicators.RSI && ind.rsi && ind.rsi[i] != null)
          rows.push(["RSI 強弱", ind.rsi[i].toFixed(1)]);
        if (showIndicators.KD && ind.kd && ind.kd.k[i] != null)
          rows.push(["KD 隨機", `K ${ind.kd.k[i].toFixed(1)} / D ${ind.kd.d[i].toFixed(1)}`]);

        const tipW = 240, tipH = 28 + rows.length * 16;
        const margin = 12;
        let left = hover.x + 16;
        let top = hover.y + 16;
        if (left + tipW > w - padR - margin) left = hover.x - tipW - 16;
        if (top < 44) top = 44;
        if (top + tipH > totalH - margin) top = hover.y - tipH - 8;
        if (left < margin) left = margin;
        return (
          <div style={{
            position: "absolute", top, left,
            background: "rgba(11,15,20,0.92)",
            border: "1px solid var(--border)",
            borderRadius: "var(--radius)",
            padding: "8px 10px",
            fontSize: "11px",
            fontFamily: "var(--font-mono)",
            color: "var(--text-1)",
            minWidth: tipW,
            pointerEvents: "none",
            backdropFilter: "blur(8px)",
            zIndex: 3,
          }}>
            <div style={{display:"flex", justifyContent:"space-between", marginBottom: 4, color: "var(--text-2)"}}>
              <span>{hoverData.date.toISOString().slice(0,10)}</span>
            </div>
            <div style={{display:"grid", gridTemplateColumns:"auto 1fr", gap:"2px 14px"}}>
              {rows.map(([k, v, color], idx) => (
                <React.Fragment key={idx}>
                  <span style={{color:"var(--text-3)"}}>{k}</span>
                  <span style={{color: color || undefined, textAlign: "right"}}>{v}</span>
                </React.Fragment>
              ))}
            </div>
          </div>
        );
      })()}
    </div>
  );
};

window.CandlestickChart = CandlestickChart;
window.genCandles = genCandles;
window.MA_CONFIG = MA_CONFIG;
window.KC_MARKERS = KC_MARKERS;
