// 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>`;
}