Source: js/charts.js

// charts.js
/**
 * @deprecated Ungenutzt – Kandidat für Entfernung
 */
export function mmWithZero(arr) {
  const f = arr.filter(Number.isFinite);
  if (!f.length) return { min: 0, max: 1 };
  const min = Math.min(0, ...f),
    max = Math.max(0, ...f);
  return min === max ? { min: min - 0.5, max: max + 0.5 } : { min, max };
}

// Catmull-Rom → Bezier
export function smoothPath(points) {
  if (points.length < 2) return '';
  const d = [];
  for (let i = 0; i < points.length; i++) {
    const [x, y] = points[i];
    if (!i) {
      d.push(`M${x},${y}`);
      continue;
    }
    const [x0, y0] = points[i - 1];
    const [xm1, ym1] = i > 1 ? points[i - 2] : [x0, y0];
    const [xp1, yp1] = i < points.length - 1 ? points[i + 1] : [x, y];
    const c1x = x0 + (x - xm1) / 6,
      c1y = y0 + (y - ym1) / 6;
    const c2x = x - (xp1 - x0) / 6,
      c2y = y - (yp1 - y0) / 6;
    d.push(`C${c1x},${c1y},${c2x},${c2y},${x},${y}`);
  }
  return d.join('');
}

export function areaUnderLineSVG(
  values,
  color,
  idPrefix,
  { width = 1400, height = 260, padTop = 24, padBottom = 0, mode = 'split', labels = null } = {}
) {
  const n = values.length,
    w = width,
    h = height,
    top = padTop,
    bottomPad = padBottom;
  // echte min/max nur aus den Daten (ohne 0 dazwischen erzwingen)
  const finite = values.filter(Number.isFinite);
  const rawMin = finite.length ? Math.min(...finite) : 0;
  const rawMax = finite.length ? Math.max(...finite) : 0;

  // falls konstant: klein aufspannen, damit man was sieht
  const span0 = rawMax - rawMin;
  const span = span0 === 0 ? 1 : span0;

  // leicht asymmetrisches Padding: unten etwas mehr, oben etwas weniger
  const yMin = rawMin - span * 0.15; // 15 % des Spannens unten
  const yMax = rawMax;

  const sx = (i) => w * (i / (n - 1 || 1));
  const sy = (v) => top + (1 - (v - yMin) / (yMax - yMin || 1)) * (h - top - bottomPad);

  const pts = values.map((v, i) => [sx(i), sy(Number.isFinite(v) ? v : i ? values[i - 1] : 0)]);
  const line = smoothPath(pts);
  const bottomY = h - bottomPad;
  const curveOnly = line.includes('C') ? 'L' + line.split('C').slice(1).join('C') : '';
  const area = `M0,${bottomY} L${pts[0][0]},${bottomY} L${pts[0][0]},${pts[0][1]} ${curveOnly} L${w},${bottomY} Z`;
  const zeroY = sy(0),
    zeroPct = Math.max(0, Math.min(100, (zeroY / h) * 100)).toFixed(2);

  const idGrad = `${idPrefix}-bg`,
    idStroke = `${idPrefix}-stroke`;
  const idClipArea = `${idPrefix}-clip-area`;
  const bgStops =
    mode === 'mono'
      ? `<stop offset="0%" stop-color="${color}" stop-opacity=".18"/><stop offset="100%" stop-color="${color}" stop-opacity=".18"/>`
      : `<stop offset="0%" stop-color="#b2182b" stop-opacity=".18"/><stop offset="${zeroPct}%" stop-color="#b2182b" stop-opacity=".18"/><stop offset="${zeroPct}%" stop-color="#2166ac" stop-opacity=".18"/><stop offset="100%" stop-color="#2166ac" stop-opacity=".18"/>`;

  // Grid + Labels
  let gridLines = '',
    gridLabels = '';
  if (Array.isArray(labels) && labels.length === n) {
    for (let i = 0; i < n; i++) {
      const lab = labels[i];
      const d =
        lab instanceof Date
          ? lab
          : typeof lab === 'number'
          ? new Date(lab > 2_000_000_000 ? lab : lab * 1000)
          : new Date(String(lab).replace('Z', '').replace(' ', 'T'));
      if (!Number.isFinite(+d)) continue;
      let x = Math.round(Math.max(0, Math.min(w, sx(i)))) + 0.5;

      const isHour = d.getMinutes() === 0;
      const isMidnight = isHour && d.getHours() === 0;
      const isSix = isHour && d.getHours() % 6 === 0;

      if (isSix) {
        const dash = isMidnight ? '' : 'stroke-dasharray="6,4"';
        const width = isMidnight ? '2.0' : '1.5';
        gridLines += `<line x1="${x}" y1="${top}" x2="${x}" y2="${
          h - bottomPad
        }" stroke="#000" stroke-opacity=".15" stroke-width="${width}" ${dash} vector-effect="non-scaling-stroke" shape-rendering="crispEdges" pointer-events="none"></line>`;
        const hh = String(d.getHours()).padStart(2, '0');
        const mm = String(d.getMinutes()).padStart(2, '0');
        gridLabels += `<text x="${x + 20}" y="${
          h - bottomPad
        }" text-anchor="middle" font-size="12" dominant-baseline="ideographic" fill="#000" opacity=".80" pointer-events="none" style="font-family: inherit">${hh}:${mm}</text>`;
      } else {
        gridLines += `<line x1="${x}" y1="${top}" x2="${x}" y2="${
          h - bottomPad
        }" stroke="#000" stroke-opacity=".10" stroke-width="1" vector-effect="non-scaling-stroke" shape-rendering="crispEdges" pointer-events="none"></line>`;
      }
    }
  }

  return `
<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}" preserveAspectRatio="none">
  <defs>
    <!-- Clip an die tatsächliche Fläche, damit Grid-Linien unter der Kurve enden -->
    <clipPath id="${idClipArea}">
      <path d="${area}"/>
    </clipPath>
    <linearGradient id="${idGrad}" x1="0" y1="0" x2="0" y2="1">${bgStops}</linearGradient>
    <linearGradient id="${idStroke}" x1="0" y1="0" x2="0" y2="1">
      <stop offset="0%" stop-color="${color}" stop-opacity=".85"/>
      <stop offset="50%" stop-color="${color}" stop-opacity=".80"/>
      <stop offset="100%" stop-color="${color}" stop-opacity=".85"/>
    </linearGradient>
  </defs>
  <path d="${area}" fill="url(#${idGrad})"/>
  <!-- Linien an die Fläche geclippt, Labels NICHT clippen -->
  <g class="bg-grid-x-lines" clip-path="url(#${idClipArea})">${gridLines}</g>
  <g class="bg-grid-x-labels">${gridLabels}</g>
  <path d="${line}" fill="none" stroke="url(#${idStroke})" stroke-width="3" stroke-linecap="round"/>
</svg>`;
}