Zeitleiste: Fenster-Etikett nie vor dem Balken, Kettenlinie lotrecht

Passte ein Name hinter einem spaeten Fenster nicht mehr in die Spur, stand
das Etikett rechtsbuendig am Balkenende und hing weit vor dessen Beginn -
"Hitzeschutz West zurueck" (15:00-18:31) sah aus, als finge es um halb elf
an. Jetzt beginnt ein Fenster-Etikett immer am Balken; reicht der Platz
nicht, wird der Name gekuerzt, und nur wenn nicht einmal die Uhrzeit Platz
hat, rueckt es so wenig wie noetig nach links.

Die Kettenlinie verlaesst den Ausloeser lotrecht unter seiner Marke (bzw.
unter Laufmarke oder Balkenanfang), laeuft gerade hinunter und biegt mit
einer kleinen Rundung von links in den Nachfolger ein. Liegt der Nachfolger
links davon, faehrt sie ihn von oben an.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 13:33:30 +02:00
co-authored by Claude Opus 5
parent 48dc2b896a
commit 6aace0d9a2
2 changed files with 63 additions and 27 deletions
+5 -2
View File
@@ -2974,8 +2974,11 @@ input[type='range']::-webkit-slider-runnable-track {
background: var(--ton-flaeche, #23282c);
border-color: rgba(var(--zl-farbe-rgb), .3);
}
/* Am rechten Rand endet das Etikett am Balkenende statt dahinter. */
.zl-fenster.links { justify-content: flex-end; }
/* Reicht der Platz bis zum rechten Rand nicht, wird der Name gekuerzt. */
.zl-etikett.gekappt {
overflow: hidden;
text-overflow: ellipsis;
}
.zl-lauf {
margin-left: .25rem;
color: var(--ton-gut, #4caf7d);
+58 -25
View File
@@ -113,13 +113,26 @@ function zlStapeln(eintraege, breite) {
const lang = zlBreite(etikett) + 26;
let links = px(von) - 8;
let rechts = a.art === "fenster" ? Math.max(px(bis), px(von) + lang) : px(von) + lang;
// Am rechten Rand steht das Etikett links vom Punkt bzw. endet am
// Balkenende - sonst ragte es aus der Spur und der Rahmen bekaeme einen
// Scrollbalken fuer ein paar Buchstaben.
const nachLinks = px(von) + lang > breite - 4;
if (nachLinks && a.art !== "fenster") { links = px(von) - lang; rechts = px(von) + 8; }
if (nachLinks && a.art === "fenster") { links = Math.min(px(von), px(bis) - lang) - 8; rechts = px(bis); }
return { a, links, rechts, nachLinks, etikett };
const ueber = px(von) + lang > breite - 4;
// Ein Punkt am rechten Rand trägt sein Etikett links von sich - das
// liest sich natürlich, die Marke bleibt, wo die Zeit ist.
const nachLinks = ueber && a.art !== "fenster";
if (nachLinks) { links = px(von) - lang; rechts = px(von) + 8; }
/*
* Ein Fenster-Etikett beginnt dagegen nie vor seinem Balken - sonst sähe
* "15:0018:31" aus, als finge es um halb elf an. Reicht der Platz nicht,
* wird der Name gekürzt (voll steht er im Hinweis); nur wenn nicht einmal
* die Uhrzeit Platz hat, rückt es so wenig wie nötig nach links.
*/
let kappe = null, versatz = 0;
if (ueber && a.art === "fenster") {
const rest = breite - 4 - px(von);
kappe = Math.max(96, rest);
versatz = Math.min(0, rest - kappe);
links = px(von) + versatz - 8;
rechts = Math.max(px(bis), px(von) + versatz + kappe);
}
return { a, links, rechts, nachLinks, kappe, versatz, etikett };
}).sort((x, y) => x.links - y.links);
/*
@@ -322,7 +335,9 @@ function zlEintrag(e, d, prozent) {
const titel = zlText(zlTitel(a, zustand));
const lauf = zustand !== "gelaufen" ? ""
: ' <i class="bi bi-check2"></i>' + (a.art === "fenster" ? '<span class="zl-lauf">' + zlText(a.laeufe[a.laeufe.length - 1]) + "</span>" : "");
const etikett = '<span class="zl-etikett"><b>' + zlText(zlEtikettZeit(a)) + "</b>" + zlText(a.name) + lauf + "</span>";
const kappe = e.kappe ? ' style="max-width:' + Math.round(e.kappe) + "px" + (e.versatz ? ";margin-left:" + Math.round(e.versatz) + "px" : "") + '"' : "";
const etikett = '<span class="zl-etikett' + (e.kappe ? " gekappt" : "") + '"' + kappe + "><b>"
+ zlText(zlEtikettZeit(a)) + "</b>" + zlText(a.name) + lauf + "</span>";
if (a.art === "fenster") {
// Läufe und die früheste Marke als Punkte auf dem Balken.
@@ -334,7 +349,7 @@ 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 + '"'
return '<div class="zl-ding zl-fenster ' + zustand + '" 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>";
@@ -365,29 +380,47 @@ function zlKettenZeichnen(ziel) {
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.
* Die Linie verlässt den Auslöser lotrecht nach unten und biegt mit einer
* kleinen Rundung von links in den Nachfolger ein.
*
* Start: beim Punkt unter der Marke; beim Fenster unter der Laufmarke,
* sonst unter dem Balkenanfang. Ziel: beim Punkt links an der Marke, beim
* Fenster am Balkenanfang. Liegt der Nachfolger links vom Auslöser (etwa
* weil er früher beginnen darf), kommt die Linie von oben an.
*/
const anker = el => {
const rect = el => {
const r = el.getBoundingClientRect();
return { l: r.left - basis.left, r: r.right - basis.left, t: r.top - basis.top, b: r.bottom - basis.top };
};
const start = 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 };
if (marke) { const m = rect(marke); return { x: (m.l + m.r) / 2, y: m.b }; }
const b = rect(el.querySelector(".zl-balken") || el), ding = rect(el);
return { x: b.l + 6, y: ding.b - 2 };
};
const ankunft = el => {
const marke = el.querySelector(".zl-marke:not(.zl-marke-innen)");
const m = rect(marke || el.querySelector(".zl-balken") || el);
return { x: m.l, y: (m.t + m.b) / 2, oben: m.t, mitte: (m.l + m.r) / 2 };
};
let pfade = "";
raster.querySelectorAll(".zl-ding[data-kette]").forEach(kind => {
const vater = raster.querySelector('.zl-ding[data-id="' + kind.dataset.kette + '"]');
if (!vater) return;
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 s = start(vater), z = ankunft(kind);
const x1 = Math.round(s.x) + 0.5, y1 = Math.round(s.y);
let d, x2, y2;
if (z.x - x1 >= 10) {
// lotrecht hinunter, Rundung, waagerecht hinein
x2 = Math.round(z.x) - 1; y2 = Math.round(z.y);
const rund = Math.min(10, (y2 - y1) / 2, (x2 - x1) / 2);
d = "M" + x1 + " " + y1 + " V" + (y2 - rund) + " Q" + x1 + " " + y2 + " " + (x1 + rund) + " " + y2 + " H" + x2;
} else {
// Nachfolger liegt links oder genau darunter: von oben anfahren
x2 = Math.round(z.mitte) + 0.5; y2 = Math.round(z.oben) - 1;
const mitte = y1 + (y2 - y1) / 2;
d = "M" + x1 + " " + y1 + " V" + mitte + " H" + x2 + " V" + 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 + '"/>'