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