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
+68 -16
View File
@@ -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 += '<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)) + '">'
+ '<span class="zl-balken">' + marken + "</span>" + etikett + "</div>";
}
@@ -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 += '<path d="M' + x1 + " " + y1 + " C" + (x1 + bogen) + " " + y1 + " " + (x2 - bogen) + " " + y2 + " " + x2 + " " + y2 + '"/>';
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 += '<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;
}
/** 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") {