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 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 13:26:45 +02:00
co-authored by Claude Opus 5
parent 93c34bf069
commit 48dc2b896a
2 changed files with 85 additions and 20 deletions
+17 -4
View File
@@ -2999,12 +2999,25 @@ input[type='range']::-webkit-slider-runnable-track {
z-index: 0; z-index: 0;
overflow: visible; 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; fill: none;
stroke: rgba(154, 166, 173, .55); stroke: var(--ton-flaeche, #23282c);
stroke-width: 1.2; stroke-width: 4;
stroke-dasharray: 3 3; 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 { .zl-leer {
padding: 1.2rem 0 .6rem 132px; padding: 1.2rem 0 .6rem 132px;
+68 -16
View File
@@ -107,7 +107,7 @@ function zlZustand(a, d) {
*/ */
function zlStapeln(eintraege, breite) { function zlStapeln(eintraege, breite) {
const px = m => m / 1440 * breite; const px = m => m / 1440 * breite;
const liste = eintraege.map(a => { const alle = eintraege.map(a => {
const [von, bis] = zlLage(a); const [von, bis] = zlLage(a);
const etikett = zlEtikettText(a); const etikett = zlEtikettText(a);
const lang = zlBreite(etikett) + 26; const lang = zlBreite(etikett) + 26;
@@ -122,14 +122,35 @@ function zlStapeln(eintraege, breite) {
return { a, links, rechts, nachLinks, etikett }; return { a, links, rechts, nachLinks, etikett };
}).sort((x, y) => x.links - y.links); }).sort((x, y) => x.links - y.links);
const zeilen = []; /*
liste.forEach(e => { * Ketten bleiben beisammen: ein Nachfolger kommt in die erste freie Zeile
let z = zeilen.findIndex(ende => ende + 6 <= e.links); * direkt unter seinem Auslöser, nicht irgendwohin, wo zeitlich gerade
if (z < 0) { z = zeilen.length; zeilen.push(0); } * Platz ist. Deshalb wird familienweise eingesetzt - erst der Auslöser,
zeilen[z] = e.rechts; * gleich danach seine Nachfolger - und je Zeile eine Liste belegter
e.zeile = z; * 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); const m = zlMinuteAus(t);
marken += '<span class="zl-marke zl-marke-innen gelaufen" style="left:' + ((m - a.von) / Math.max(1, a.bis - a.von) * 100).toFixed(2) + '%"></span>'; marken += '<span class="zl-marke zl-marke-innen gelaufen" style="left:' + ((m - a.von) / Math.max(1, a.bis - a.von) * 100).toFixed(2) + '%"></span>';
}); });
return '<div class="zl-ding zl-fenster ' + zustand + (e.nachLinks ? " links" : "") + '" data-id="' + a.id + '" title="' + titel + '"' return '<div class="zl-ding zl-fenster ' + zustand + (e.nachLinks ? " links" : "") + '" data-id="' + a.id + '"'
+ (a.kette ? ' data-kette="' + a.kette + '"' : "") + ' title="' + titel + '"'
+ ' style="top:' + oben + "px;left:" + prozent(a.von) + ";width:" + prozent(Math.max(10, a.bis - a.von)) + '">' + ' style="top:' + oben + "px;left:" + prozent(a.von) + ";width:" + prozent(Math.max(10, a.bis - a.von)) + '">'
+ '<span class="zl-balken">' + marken + "</span>" + etikett + "</div>"; + '<span class="zl-balken">' + marken + "</span>" + etikett + "</div>";
} }
@@ -342,20 +364,44 @@ function zlKettenZeichnen(ziel) {
const basis = raster.getBoundingClientRect(); const basis = raster.getBoundingClientRect();
svg.setAttribute("width", basis.width); svg.setAttribute("width", basis.width);
svg.setAttribute("height", basis.height); 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 = ""; 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 + '"]'); const vater = raster.querySelector('.zl-ding[data-id="' + kind.dataset.kette + '"]');
if (!vater) return; if (!vater) return;
const mv = (vater.querySelector(".zl-marke") || vater).getBoundingClientRect(); const v = anker(vater), k = anker(kind);
const mk = kind.querySelector(".zl-marke").getBoundingClientRect(); const x1 = v.x + (v.r ? v.r * 0.7 : 0), y1 = v.y + (v.r ? v.r * 0.7 : 0);
const x1 = mv.left + mv.width / 2 - basis.left, y1 = mv.top + mv.height / 2 - basis.top; const x2 = k.x - (k.r ? k.r + 1 : 0), y2 = k.y;
const x2 = mk.left + mk.width / 2 - basis.left, y2 = mk.top + mk.height / 2 - basis.top; // Erst senkrecht nach unten, dann waagerecht hinein: so trifft der Bogen
const bogen = Math.max(18, Math.abs(y2 - y1) * 0.5); // den Nachfolger von links, auch wenn der fast senkrecht darunter liegt.
pfade += '<path d="M' + x1 + " " + y1 + " C" + (x1 + bogen) + " " + y1 + " " + (x2 - bogen) + " " + y2 + " " + x2 + " " + y2 + '"/>'; 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 += '<path class="zl-kette-grund" d="' + d + '"/>'
+ '<path class="zl-kette"' + paar + ' d="' + d + '"/>'
+ '<circle class="zl-kette-ende"' + paar + ' cx="' + x2 + '" cy="' + y2 + '" r="2.4"/>';
}); });
svg.innerHTML = 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 -------------------------------------------------- */ /* --- Laden und Bedienen -------------------------------------------------- */
async function zeitleisteLaden() { 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. // Neu zeichnen, wenn sich die Breite ändert - Stapeln und Ketten hängen daran.
let warte = null; let warte = null;
if (typeof ResizeObserver !== "undefined") { if (typeof ResizeObserver !== "undefined") {