Source: js/chart-dom-utils.js

// chart-dom-utils.js
// DOM-Manipulation, Tooltips, Guides, Layout-Helpers

/**
 * Zeichnet Guide-Linie und Label (Einheit anpassbar)
 * Unterstützte Signaturen:
 *  - v1 (alt): makeGuide(y, cls, label, value, syFn, side, L, R, W) -> Standard-Einheit '%'
 *  - v2 (neu): makeGuide(y, cls, label, value, syFn, unit, side, L, R, W)
 */
export function makeGuide(y, cls, label, value, syFn, a, b, c, d, e) {
  if (!Number.isFinite(y)) return { line: '', label: '' };

  // Defaults (bewahren bisheriges Verhalten für Humidity: '%')
  let unit = '%';
  let side = 'R';
  let L = 48,
    R = 12,
    W = 360;

  // Parameter-Überladung erkennen
  if (a === 'L' || a === 'R') {
    // v1: (side, L, R, W)
    side = a;
    L = typeof b === 'number' ? b : 48;
    R = typeof c === 'number' ? c : 12;
    W = typeof d === 'number' ? d : 360;
  } else {
    // v2: (unit, side, L, R, W)
    unit = typeof a === 'string' ? a : '%';
    side = b === 'L' || b === 'R' ? b : 'R';
    L = typeof c === 'number' ? c : 48;
    R = typeof d === 'number' ? d : 12;
    W = typeof e === 'number' ? e : 360;
  }

  const ypx = syFn(y);
  const line = `<g class="g-${cls}"><line x1="${L}" y1="${ypx}" x2="${
    W - R
  }" y2="${ypx}" stroke-dasharray="4,4"></line></g>`;

  const isLeft = side === 'L';
  const xLabel = isLeft ? L + 4 : W - R - 4;
  const anchor = isLeft ? 'start' : 'end';
  const ttext = `<text class="guide-label g-${cls}" x="${xLabel}" y="${ypx - 4}"
    text-anchor="${anchor}">${label} ${
    Number.isFinite(value) ? value.toFixed(1) + unit : '–'
  }</text>`;
  return { line, label: ttext };
}

/**
 * Tooltip-Element sicherstellen (fallback, falls im Chart-Scaffold keins existiert)
 */
export function ensureTooltip(mount) {
  let tip = mount.querySelector('.chart-tooltip');
  if (!tip) {
    tip = document.createElement('div');
    tip.className = 'chart-tooltip';
    tip.style.position = 'fixed';
    tip.style.pointerEvents = 'none';
    tip.style.display = 'none';
    mount.appendChild(tip);
  }
  return tip;
}

/**
 * Positioniert das Tooltip neben dem Cursor.
 * Unterstützt BEIDE Signaturen:
 *  1) placeTooltipNearCursor(tooltip, evt, { container, offset, pad })
 *  2) placeTooltipNearCursor({ tooltip, mount, container, evt, offset, pad })
 */
export function placeTooltipNearCursor(a, b, c) {
  // --- Parameter-Normalisierung ---
  let tooltip,
    evt,
    container = null,
    offset = 12,
    pad = 6;

  if (a && typeof a === 'object' && a.tooltip && a.evt) {
    ({ tooltip, evt } = a);
    container = a.container || a.mount || null;
    if (Number.isFinite(a.offset)) offset = a.offset;
    if (Number.isFinite(a.pad)) pad = a.pad;
  } else {
    tooltip = a;
    evt = b;
    if (c) {
      container = c.container || null;
      if (Number.isFinite(c.offset)) offset = c.offset;
      if (Number.isFinite(c.pad)) pad = c.pad;
    }
  }
  if (!tooltip || !evt) return;

  // kurz sichtbar machen, damit wir Breite/Höhe kennen
  const prevDisplay = getComputedStyle(tooltip).display;
  if (prevDisplay === 'none') tooltip.style.display = 'block';

  const box = tooltip.getBoundingClientRect();

  // Viewport-Position (fixed) oder relativ zum Container (absolute)
  if (!container) {
    tooltip.style.position = 'fixed';
    let left = evt.clientX + offset;
    let top = evt.clientY + offset;
    left = Math.max(pad, Math.min(left, window.innerWidth - box.width - pad));
    top = Math.max(pad, Math.min(top, window.innerHeight - box.height - pad));
    tooltip.style.left = `${left}px`;
    tooltip.style.top = `${top}px`;
  } else {
    tooltip.style.position = 'absolute';
    const rect = container.getBoundingClientRect();
    let left = evt.clientX - rect.left + offset;
    let top = evt.clientY - rect.top + offset;
    left = Math.max(pad, Math.min(left, rect.width - box.width - pad));
    top = Math.max(pad, Math.min(top, rect.height - box.height - pad));
    tooltip.style.left = `${left}px`;
    tooltip.style.top = `${top}px`;
  }

  if (prevDisplay === 'none') tooltip.style.display = 'none';
}

/**
 * Entfernt Padding vom Container (optional, für Fullscreen)
 */
export function stripContainerPadding(mount) {
  const sec = mount.closest('section.container');
  if (sec && !sec.__paddingStripped) {
    sec.classList.add('px-0', 'py-0');
    sec.style.paddingLeft = '0';
    sec.style.paddingRight = '0';
    sec.__paddingStripped = true;
  }
}

/**
 * Berechnet Ticks innerhalb der Achsengrenzen
 */
export function clampTicksWithinAxis(axisMin, axisMax, syFn, { H, T, B }) {
  // Ganze °C-Grenzen innerhalb der Achse
  const start = Math.ceil(axisMin);
  const end = Math.floor(axisMax);
  if (end < start) return [];

  const spanI = end - start;
  // Schritt: 1°C bis ca. 12 Linien, sonst 2°C
  const step = spanI > 12 ? 2 : 1;

  const ticks = [];
  const bottomGuardPx = 16; // Mindestabstand zur X-Achse
  const topGuardPx = 6; // Mindestabstand zum oberen Rand

  for (let v = start; v <= end; v += step) {
    // nur innerhalb der Achse (numerisch robust)
    if (v < axisMin - 1e-9 || v > axisMax + 1e-9) continue;

    const y = syFn(v);
    // Schutzkanten gegen Kollision mit X-Achse / Rand
    if (y > H - B - bottomGuardPx) continue;
    if (y < T + topGuardPx) continue;

    ticks.push({ v, y });
  }
  return ticks;
}

/**
 * Hilfsfunktion: Metriken für Y-Achse(n)
 */
export function yAxisMetrics({
  T, // top padding (px)
  H, // Gesamthöhe (px)
  B, // bottom padding (px)
  innerH, // H - T - B
  twoAxes, // bool
  yMin,
  yMax, // gemeinsame Achse (wenn twoAxes == false)
  yMinIn,
  yMaxIn, // Innen (wenn twoAxes == true)
  yMinOut,
  yMaxOut, // Außen (wenn twoAxes == true)
}) {
  const yBottom = T + innerH;

  const make = (axisMin, axisMax, side) => {
    const span = axisMax - axisMin || 1;
    const degPerPx = span / innerH;
    const pxPerDeg = innerH / span;

    // Vor-/Rückwärtsmapping
    const valueAtY = (yPx) => {
      const ratio = 1 - (yPx - T) / innerH; // 1..0 von oben nach unten
      return axisMin + ratio * span;
    };
    const yAtValue = (v) => T + (1 - (v - axisMin) / span) * innerH;

    // Beispielrechnungen (nur Komfort)
    const deg16px = 16 * degPerPx;
    const deg64px = 64 * degPerPx;

    return {
      side, // 'L' oder 'R'
      axisMin,
      axisMax, // °C
      degPerPx,
      pxPerDeg, // Umrechnungen
      examples: { deg16px, deg64px },
      // praktische Helfer:
      valueAtY, // °C an gegebener y-Position
      yAtValue, // y-Position für gegebene °C
      topPx: T,
      bottomPx: yBottom,
      topDeg: valueAtY(T), // = axisMax
      bottomDeg: valueAtY(yBottom), // = axisMin
    };
  };

  if (twoAxes) {
    // Konvention: Außen (rot) links, Innen (blau) rechts
    const left = make(yMinOut, yMaxOut, 'L');
    const right = make(yMinIn, yMaxIn, 'R');
    return { mode: 'two', left, right };
  } else {
    // eine gemeinsame Achse → nur links
    const left = make(yMin, yMax, 'L');
    return { mode: 'one', left };
  }
}