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") {