From 6aace0d9a2a654bfd9ac88834ec8141c5989351c Mon Sep 17 00:00:00 2001 From: "m0@nas" Date: Tue, 15 Sep 2026 13:33:30 +0200 Subject: [PATCH] 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 --- css/solar.css | 7 +++- js/solar/zeitleiste.js | 83 +++++++++++++++++++++++++++++------------- 2 files changed, 63 insertions(+), 27 deletions(-) diff --git a/css/solar.css b/css/solar.css index b0fde4c..bd45463 100644 --- a/css/solar.css +++ b/css/solar.css @@ -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); diff --git a/js/solar/zeitleiste.js b/js/solar/zeitleiste.js index 71505a2..9fbdf5f 100644 --- a/js/solar/zeitleiste.js +++ b/js/solar/zeitleiste.js @@ -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:00–18: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" ? "" : ' ' + (a.art === "fenster" ? '' + zlText(a.laeufe[a.laeufe.length - 1]) + "" : ""); - const etikett = '' + zlText(zlEtikettZeit(a)) + "" + zlText(a.name) + lauf + ""; + const kappe = e.kappe ? ' style="max-width:' + Math.round(e.kappe) + "px" + (e.versatz ? ";margin-left:" + Math.round(e.versatz) + "px" : "") + '"' : ""; + const etikett = '" + + zlText(zlEtikettZeit(a)) + "" + zlText(a.name) + lauf + ""; 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 += ''; }); - return '"; @@ -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 += '' + ''