From 48dc2b896aa94406e00e4095123a5e88a24c31a0 Mon Sep 17 00:00:00 2001 From: "m0@nas" Date: Tue, 15 Sep 2026 13:26:45 +0200 Subject: [PATCH] Zeitleiste: Ketten stehen unter ihrem Ausloeser, Linien deutlicher Gestapelt wurde nur nach Uhrzeit in die erste freie Zeile - ein Nachfolger wie "Hitzeschutz Ost zurueck" landete irgendwo, wo gerade Platz war. Jetzt wird familienweise eingesetzt: der Nachfolger kommt in die erste freie Zeile direkt unter seinem Ausloeser. Je Zeile werden dafuer die belegten Strecken gefuehrt statt nur ihr Ende. Die Kettenlinie fehlte bei Nachfolgern mit Zeitfenster ganz und ging sonst im Stundenraster unter. Sie hat jetzt einen dunklen Grund, eine hellere Farbe, einen Endpunkt, laeuft erst nach unten und trifft den Nachfolger von links. Beim Ueberfahren eines Eintrags leuchtet seine Kette auf. Co-Authored-By: Claude Opus 5 --- css/solar.css | 21 +++++++++-- js/solar/zeitleiste.js | 84 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 85 insertions(+), 20 deletions(-) diff --git a/css/solar.css b/css/solar.css index a6ac725..b0fde4c 100644 --- a/css/solar.css +++ b/css/solar.css @@ -2999,12 +2999,25 @@ input[type='range']::-webkit-slider-runnable-track { z-index: 0; overflow: visible; } -.zl-ketten path { +/* Die Linie liegt ueber dem Raster, aber unter den Etiketten. Ein dunkler + Grund darunter trennt sie von den Stundenlinien, sonst verschwindet sie + dort, wo sie eine kreuzt. */ +.zl-ketten .zl-kette-grund { fill: none; - stroke: rgba(154, 166, 173, .55); - stroke-width: 1.2; - stroke-dasharray: 3 3; + stroke: var(--ton-flaeche, #23282c); + stroke-width: 4; + opacity: .7; } +.zl-ketten .zl-kette { + fill: none; + stroke: rgba(214, 222, 227, .72); + stroke-width: 1.6; + stroke-dasharray: 4 3; + transition: stroke .15s; +} +.zl-ketten .zl-kette-ende { fill: rgba(214, 222, 227, .85); transition: fill .15s; } +.zl-ketten .zl-kette.hell { stroke: var(--ton-sonne, #f0b429); stroke-dasharray: none; } +.zl-ketten .zl-kette-ende.hell { fill: var(--ton-sonne, #f0b429); } .zl-leer { padding: 1.2rem 0 .6rem 132px; diff --git a/js/solar/zeitleiste.js b/js/solar/zeitleiste.js index 54df163..71505a2 100644 --- a/js/solar/zeitleiste.js +++ b/js/solar/zeitleiste.js @@ -107,7 +107,7 @@ function zlZustand(a, d) { */ function zlStapeln(eintraege, breite) { const px = m => m / 1440 * breite; - const liste = eintraege.map(a => { + const alle = eintraege.map(a => { const [von, bis] = zlLage(a); const etikett = zlEtikettText(a); const lang = zlBreite(etikett) + 26; @@ -122,14 +122,35 @@ function zlStapeln(eintraege, breite) { return { a, links, rechts, nachLinks, etikett }; }).sort((x, y) => x.links - y.links); - const zeilen = []; - liste.forEach(e => { - let z = zeilen.findIndex(ende => ende + 6 <= e.links); - if (z < 0) { z = zeilen.length; zeilen.push(0); } - zeilen[z] = e.rechts; - e.zeile = z; + /* + * Ketten bleiben beisammen: ein Nachfolger kommt in die erste freie Zeile + * direkt unter seinem Auslöser, nicht irgendwohin, wo zeitlich gerade + * Platz ist. Deshalb wird familienweise eingesetzt - erst der Auslöser, + * gleich danach seine Nachfolger - und je Zeile eine Liste belegter + * Strecken geführt statt nur ihres Endes, weil ein Nachfolger links von + * etwas liegen kann, das schon eingesetzt ist. + */ + const inBahn = new Map(alle.map(e => [e.a.id, e])); + const kinder = new Map(); + alle.forEach(e => { + const v = e.a.kette; + if (v && inBahn.has(v)) { + if (!kinder.has(v)) kinder.set(v, []); + kinder.get(v).push(e); + } }); - return { liste, zeilen: Math.max(1, zeilen.length) }; + const zeilen = []; // je Zeile: [[links, rechts], ...] + const frei = (z, e) => !(zeilen[z] || []).some(([l, r]) => l < e.rechts + 6 && e.links < r + 6); + const setzen = (e, ab) => { + let z = ab; + while (!frei(z, e)) z++; + (zeilen[z] = zeilen[z] || []).push([e.links, e.rechts]); + e.zeile = z; + (kinder.get(e.a.id) || []).forEach(k => { if (k.zeile === undefined) setzen(k, z + 1); }); + }; + alle.filter(e => !(e.a.kette && inBahn.has(e.a.kette))).forEach(e => setzen(e, 0)); + alle.forEach(e => { if (e.zeile === undefined) setzen(e, 0); }); // Kreise, sicher ist sicher + return { liste: alle, zeilen: Math.max(1, zeilen.length) }; } /** @@ -313,7 +334,8 @@ function zlEintrag(e, d, prozent) { const m = zlMinuteAus(t); marken += ''; }); - return '"; } @@ -342,20 +364,44 @@ function zlKettenZeichnen(ziel) { const basis = raster.getBoundingClientRect(); svg.setAttribute("width", basis.width); svg.setAttribute("height", basis.height); + /* + * Ankerpunkt eines Eintrags: beim Punkt die Marke, beim Fenster die + * Laufmarke, sonst der Balkenanfang. Die Linie beginnt am Auslöser rechts + * vom Anker und endet am Nachfolger links davon - so kommt sie an und + * läuft nicht quer durch den Kreis. + */ + const anker = el => { + const marke = el.querySelector(".zl-marke-innen.gelaufen") || el.querySelector(".zl-marke:not(.zl-marke-innen)"); + const r = (marke || el.querySelector(".zl-balken") || el).getBoundingClientRect(); + return marke + ? { x: r.left + r.width / 2 - basis.left, y: r.top + r.height / 2 - basis.top, r: r.width / 2 } + : { x: r.left - basis.left, y: r.top + r.height / 2 - basis.top, r: 0 }; + }; let pfade = ""; - raster.querySelectorAll(".zl-punkt[data-kette]").forEach(kind => { + raster.querySelectorAll(".zl-ding[data-kette]").forEach(kind => { const vater = raster.querySelector('.zl-ding[data-id="' + kind.dataset.kette + '"]'); if (!vater) return; - const mv = (vater.querySelector(".zl-marke") || vater).getBoundingClientRect(); - const mk = kind.querySelector(".zl-marke").getBoundingClientRect(); - const x1 = mv.left + mv.width / 2 - basis.left, y1 = mv.top + mv.height / 2 - basis.top; - const x2 = mk.left + mk.width / 2 - basis.left, y2 = mk.top + mk.height / 2 - basis.top; - const bogen = Math.max(18, Math.abs(y2 - y1) * 0.5); - pfade += ''; + const v = anker(vater), k = anker(kind); + const x1 = v.x + (v.r ? v.r * 0.7 : 0), y1 = v.y + (v.r ? v.r * 0.7 : 0); + const x2 = k.x - (k.r ? k.r + 1 : 0), y2 = k.y; + // Erst senkrecht nach unten, dann waagerecht hinein: so trifft der Bogen + // den Nachfolger von links, auch wenn der fast senkrecht darunter liegt. + const d = "M" + x1 + " " + y1 + " C" + x1 + " " + (y1 + (y2 - y1) * 0.8) + " " + + (x2 - Math.max(14, (x2 - x1) * 0.5)) + " " + y2 + " " + x2 + " " + y2; + const paar = ' data-von="' + kind.dataset.kette + '" data-nach="' + kind.dataset.id + '"'; + pfade += '' + + '' + + ''; }); svg.innerHTML = pfade; } +/** Beim Überfahren die Kette des Eintrags hervorheben - in beide Richtungen. */ +function zlKetteHervorheben(id, an) { + document.querySelectorAll('#zeitleiste .zl-ketten [data-von="' + id + '"], #zeitleiste .zl-ketten [data-nach="' + id + '"]') + .forEach(el => el.classList.toggle("hell", an)); +} + /* --- Laden und Bedienen -------------------------------------------------- */ async function zeitleisteLaden() { @@ -465,6 +511,12 @@ document.addEventListener("DOMContentLoaded", function () { } }); + ["mouseover", "mouseout"].forEach(typ => ziel.addEventListener(typ, function (ereignis) { + const ding = ereignis.target.closest(".zl-ding"); + if (!ding || (ereignis.relatedTarget && ding.contains(ereignis.relatedTarget))) return; + zlKetteHervorheben(ding.dataset.id, typ === "mouseover"); + })); + // Neu zeichnen, wenn sich die Breite ändert - Stapeln und Ketten hängen daran. let warte = null; if (typeof ResizeObserver !== "undefined") {