Drei Teile, die zusammengehoeren: die Fahrzeugkachel zeigt den Ladestand jetzt dort, wo er im Auto auch sitzt, die Jahresstatistik wird zur Tabelle, und beide holen ihr Aussehen aus einer gemeinsamen Farbwelt. Fahrzeugkachel. Die Kachel "Batterie und Ladung" ist aufgeloest; ihr Inhalt steckt in der Draufsicht. Die Akkuplatte im Fahrzeugboden fuellt sich von hinten nach vorn, traegt die Zielmarke und nennt Prozent und Reichweite in ihrer Mitte - auf dem Goldenen Schnitt der Plattenhoehe, damit sie nicht am Klimaknopf klebt. Beim Laden wandern Streifen im gefuellten Teil nach vorn, und vom Anschluss fliesst eine Linie in die Platte. Was sich nicht zeichnen laesst - Ladeleistung, Restzeit, Kilometerstand, Klima, Herkunft des Ladestroms - steht als Kennzahl daneben, dazu die gefahrenen Kilometer aus Woche, Monat und Jahr (?was=strecke, gerechnet aus dem Zaehlerstand statt aus erkannten Fahrten - faellt eine Zeile aus, fehlt die Fahrt in der Liste, im Zaehlerstand aber nicht). Am Fahrzeug selbst: kein Panoramadach, das hat unseres nicht. Reling und Spoiler neu gezeichnet, weil sie mit der Dachplatte bisher ein geschlossenes Rechteck bildeten und das wie ein Fensterrahmen aussah. Die Blende am Bug ist schmaler und heller - von oben sieht man von einer senkrechten Flaeche nur einen Streifen, und fast schwarz las sie sich wie ein Loch. Die Ladeklappe ist gebaut wie der Klimaknopf; den Ladezustand traegt jetzt die Farbe statt einer zweiten Form. Jahresvergleich. getStats.php liefert kein fertiges HTML mehr, sondern JSON, das sich selbst beschreibt: Einheit, Nachkommastellen, Richtung und Zugehoerigkeit stehen als Metadaten je Kennzahl, nicht mehr als zwei Arrays, die ueber den Spaltenindex mit der Abfrage synchron gehalten werden mussten. Zwei Abfragen holen alles - Monatswerte, daraus die ganzen Jahre und die Summe seit Aufzeichnungsbeginn, dazu der gleiche Zeitraum jedes Jahres bis zum heutigen Kalendertag. Verhaeltniszahlen sind auf jeder Ebene neu aus den Summen gerechnet; ein Jahresmittel ist nicht das Mittel der Monatsmittel. Gezeichnet wird das von einem Baustein, den sich Historien- und Solarseite teilen: dort drei Jahresspalten mit verschiebbarem Fenster bis zurueck zum ersten Jahr, hier eine Spalte ohne Werkzeuge. Zeilen, die sich aufteilen, stehen als "davon" darunter; was bereits in einer anderen Zeile steckt, als "darin" samt Namen der Zeile - der Beitrag der Batterie und die Solarladung des Autos duerfen nicht zum Ertrag addiert werden. Zusammengefasste Zeilen sind zunaechst zu, damit die Tabelle mit zehn Zeilen aufmacht statt mit siebzehn. Farbwelt. Statt Regel fuer Regel zu ueberschreiben, ist die Palette ausgetauscht: ein paar Variablen am Wurzelelement, an die Bootstrap und AdminLTE ohnehin gehen. Anthrazit als Grund, eine Spur hellere Flaeche fuer Karten, Haarlinien statt Rahmen und Schatten, Bernstein als einzige Akzentfarbe - auf dieser Seite die Farbe der eigenen Anlage. Gruen, Rot und Gelb bleiben den Zustaenden vorbehalten. Erklaerungen, die bisher nur im title standen, sind antippbar: am Finger gibt es kein Ueberfahren, und ein title erscheint dort nie. Die beiden neuen Abfragen sind gegen solarLog gelaufen, die Monatswerte decken sich mit einer unabhaengigen Zerlegung. grundpreisImJahr() ruft jetzt grundpreisImZeitraum() auf - dieselbe Rechnung, nur auch fuer einen Teil des Jahres. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
466 lines
20 KiB
JavaScript
466 lines
20 KiB
JavaScript
/*
|
||
* Jahresvergleich der Anlage - ein Baustein, zwei Seiten.
|
||
*
|
||
* Auf der Historienseite steht er mit allem: drei Jahresspalten, umschaltbarem
|
||
* Zeitraum, verschiebbarem Jahrfenster. Auf der Solarseite genügt das laufende
|
||
* Jahr. Beide bauen dieselbe Tabelle aus derselben Antwort - was sich
|
||
* unterscheidet, sind Einstellungen, nicht zwei Fassungen desselben Codes.
|
||
*
|
||
* baueJahresstatistik(wurzel, { spalten: 3, werkzeuge: true, fussnoten: true })
|
||
*
|
||
* Alles Inhaltliche kommt von ajax/getStats.php, und zwar auch die Beschreibung
|
||
* der Zeilen: welche Kennzahlen es gibt, wie sie heißen, welche Einheit sie
|
||
* tragen, ob weniger besser ist und wie sie zusammenhängen. Hier steht nur, wie
|
||
* das aussieht. Eine neue Kennzahl braucht deshalb keine Zeile in dieser Datei.
|
||
*
|
||
* Der Abruf liefert alle Jahre auf einmal. Das Verschieben des Fensters und das
|
||
* Umschalten des Zeitraums brauchen darum keine weitere Anfrage - und die
|
||
* Antwort wird geteilt, falls eine Seite den Baustein zweimal zeigt.
|
||
*/
|
||
|
||
const MONATSNAMEN = ["Jan", "Feb", "Mär", "Apr", "Mai", "Jun",
|
||
"Jul", "Aug", "Sep", "Okt", "Nov", "Dez"];
|
||
|
||
/* Ein Abruf für alle Bausteine der Seite. */
|
||
let statistikAbruf = null;
|
||
function holeJahresstatistik() {
|
||
if (!statistikAbruf) {
|
||
statistikAbruf = fetch("ajax/getStats.php", { cache: "no-store" })
|
||
.then(function (antwort) {
|
||
if (!antwort.ok) throw new Error("HTTP " + antwort.status);
|
||
return antwort.json();
|
||
})
|
||
.then(function (daten) {
|
||
if (daten.error) throw new Error(daten.error);
|
||
return daten;
|
||
});
|
||
}
|
||
return statistikAbruf;
|
||
}
|
||
|
||
/* ------------------------------------------------------------------ Format */
|
||
|
||
const statZahl = (wert, dez) => (wert === null || wert === undefined || !isFinite(wert))
|
||
? "–" : Number(wert).toLocaleString("de-DE",
|
||
{ minimumFractionDigits: dez, maximumFractionDigits: dez });
|
||
|
||
/**
|
||
* Veränderung zweier Werte.
|
||
*
|
||
* Prozentwerte vergleichen sich in Prozentpunkten: "die Autarkie stieg um 5 %"
|
||
* ließe offen, ob 70 auf 75 oder auf 73,5 gemeint ist. Bei allem anderen ist
|
||
* die relative Veränderung die Aussage. Was unter einem Prozent bzw. drei
|
||
* Zehntel Prozentpunkten liegt, gilt als unverändert - sonst zeigt die Tabelle
|
||
* Bewegung, wo keine ist.
|
||
*/
|
||
function statDelta(jetzt, vorher, kz) {
|
||
if (jetzt === null || jetzt === undefined || !vorher) return null;
|
||
const roh = kz.punkte ? jetzt - vorher : (jetzt - vorher) / Math.abs(vorher) * 100;
|
||
const klein = Math.abs(roh) < (kz.punkte ? 0.3 : 1);
|
||
const besser = kz.invers ? roh < 0 : roh > 0;
|
||
return {
|
||
text: (roh >= 0 ? "+" : "−") + statZahl(Math.abs(roh), 1) + (kz.punkte ? " Pp" : " %"),
|
||
art: klein ? "gleich" : (besser ? "gut" : "schlecht"),
|
||
zeichen: klein ? "→" : (roh > 0 ? "↑" : "↓")
|
||
};
|
||
}
|
||
|
||
/* --------------------------------------------------------------- Baustein */
|
||
|
||
function baueJahresstatistik(wurzel, einstellungen) {
|
||
if (!wurzel) return null;
|
||
|
||
const opt = Object.assign({
|
||
spalten: 3, // Jahresspalten nebeneinander
|
||
werkzeuge: true, // Zeitraum, Jahrfenster, Verlauf
|
||
fussnoten: true,
|
||
zeitraum: "gleich", // "gleich" (bis zum Stichtag) oder "voll"
|
||
verlauf: true
|
||
}, einstellungen || {});
|
||
|
||
let daten = null;
|
||
let zeitraum = opt.zeitraum;
|
||
let verlauf = opt.verlauf;
|
||
let fenster = 0; // 0 = die jüngsten Jahre
|
||
const offen = {}; // aufgeklappte Summenzeilen
|
||
|
||
/* --- Gerüst ------------------------------------------------------------ */
|
||
wurzel.classList.add("jahresstatistik");
|
||
wurzel.innerHTML =
|
||
'<div class="leitzahlen"></div>'
|
||
+ (opt.werkzeuge ? '<div class="werkzeuge">'
|
||
+ '<span class="umschalter" data-rolle="zeitraum" role="group" aria-label="Zeitraum des Vergleichs">'
|
||
+ '<button type="button" data-zeitraum="gleich">Gleicher Zeitraum</button>'
|
||
+ '<button type="button" data-zeitraum="voll">Ganze Jahre</button></span>'
|
||
+ '<span class="jahrwahl">'
|
||
+ '<button type="button" data-schritt="-1" aria-label="Frühere Jahre zeigen">'
|
||
+ '<i class="bi bi-chevron-left"></i></button>'
|
||
+ '<span class="fenster">–</span>'
|
||
+ '<button type="button" data-schritt="1" aria-label="Spätere Jahre zeigen">'
|
||
+ '<i class="bi bi-chevron-right"></i></button></span>'
|
||
+ '<label class="kasten"><input type="checkbox" data-rolle="verlauf" checked>'
|
||
+ "Monatsverlauf zeigen</label></div>" : "")
|
||
+ '<div class="tafel-scroll"><table class="tafel">'
|
||
+ "<thead><tr></tr></thead>"
|
||
+ '<tbody><tr><td class="leer">Statistik wird geladen …</td></tr></tbody>'
|
||
+ "</table></div>"
|
||
+ (opt.fussnoten ? '<div class="fussnoten"><p data-rolle="zeitraumtext"></p>'
|
||
+ "<p><b>Davon</b> teilt den Ertrag auf: Eigenverbrauch, Einspeisung und Heizung "
|
||
+ "ergeben ihn zusammen. <b>Darin</b> greift aus einer dieser Zeilen etwas heraus – "
|
||
+ "der Beitrag der Batterie und die Solarladung des Autos stecken beide im "
|
||
+ "Eigenverbrauch, denn Strom aus der Batterie und Strom ins Auto ist Strom, der im "
|
||
+ "Haus geblieben ist. Sie dürfen deshalb nicht dazugezählt werden. Der Beitrag der "
|
||
+ "Batterie ist dabei netto gerechnet: abzüglich der Vergütung, die für den zum "
|
||
+ "Laden verwendeten Strom entgangen ist.</p>"
|
||
+ "<p>Verhältniszahlen – Autarkie, Quoten, effektiver Strompreis – sind für jeden "
|
||
+ "Zeitraum neu aus seinen Summen gerechnet und nicht über Jahre gemittelt. Der "
|
||
+ "effektive Strompreis ist Netzkosten samt Grundpreis geteilt durch den "
|
||
+ "Netzbezug.</p></div>" : "");
|
||
|
||
const teil = (auswahl) => wurzel.querySelector(auswahl);
|
||
const kopf = teil("thead tr");
|
||
const koerper = teil("tbody");
|
||
|
||
/* --- Auswahl ----------------------------------------------------------- */
|
||
|
||
/** Die Jahre ab dem Fensteranfang, jüngstes zuerst. */
|
||
function jahreAbFenster() {
|
||
const alle = daten.jahre.slice().sort((a, b) => b - a);
|
||
const start = Math.max(0, Math.min(fenster, Math.max(0, alle.length - opt.spalten)));
|
||
return alle.slice(start);
|
||
}
|
||
function spaltenJahre() { return jahreAbFenster().slice(0, opt.spalten); }
|
||
|
||
function werteVon(jahr) {
|
||
const satz = daten[zeitraum] || {};
|
||
return satz[jahr] || null;
|
||
}
|
||
|
||
/**
|
||
* Welche beiden Jahre die Δ-Spalte vergleicht. Bei ganzen Jahren bleibt das
|
||
* laufende außen vor: es ist noch nicht fertig, ein Vergleich mit einem
|
||
* vollen Jahr wäre keiner. Das Paar hängt am Fenster, nicht an den
|
||
* sichtbaren Spalten - so vergleicht auch eine einspaltige Ansicht.
|
||
*/
|
||
function vergleichsPaar() {
|
||
const laufend = new Date().getFullYear();
|
||
const jahre = jahreAbFenster();
|
||
const brauchbar = zeitraum === "gleich" ? jahre : jahre.filter(j => j !== laufend);
|
||
return brauchbar.length >= 2 ? [brauchbar[0], brauchbar[1]] : null;
|
||
}
|
||
|
||
/** Das Jahr, dessen Monatsverlauf die letzte Spalte zeigt. */
|
||
function verlaufsJahr() {
|
||
const paar = vergleichsPaar();
|
||
return paar ? paar[0] : (spaltenJahre()[0] || null);
|
||
}
|
||
|
||
/** Der Stichtag als "7. September" - so, wie man ihn ausspricht. */
|
||
function stichtagText() {
|
||
const d = new Date(daten.stichtag + "T00:00:00");
|
||
return d.toLocaleDateString("de-DE", { day: "numeric", month: "long" });
|
||
}
|
||
|
||
/* --- Monatsbalken ------------------------------------------------------ */
|
||
|
||
/*
|
||
* Zwölf Balken, eine Grundlinie, sonst nichts: der Verlauf sagt "wann", die
|
||
* Zahlen daneben sagen "wieviel". Der letzte Monat mit Wert ist hervorgehoben,
|
||
* damit erkennbar bleibt, wo das Jahr steht.
|
||
*/
|
||
function sparkline(kz, jahr) {
|
||
const reihe = (daten.monate[jahr] || {})[kz.id];
|
||
if (!reihe) return "";
|
||
const zahlen = reihe.map(w => (w === null || !isFinite(w)) ? null : Number(w));
|
||
const gueltig = zahlen.filter(w => w !== null);
|
||
if (!gueltig.length) return "";
|
||
|
||
const hoch = Math.max.apply(null, gueltig.map(Math.abs)) || 1;
|
||
const letzter = zahlen.reduce((idx, w, i) => (w === null ? idx : i), -1);
|
||
const b = 7, luecke = 2, H = 24;
|
||
const breite = zahlen.length * b + (zahlen.length - 1) * luecke;
|
||
|
||
let s = '<svg class="spark" width="' + breite + '" height="' + H + '" viewBox="0 0 '
|
||
+ breite + " " + H + '" role="img" aria-label="Monatsverlauf ' + jahr + '">'
|
||
+ '<line class="basis" x1="0" y1="' + (H - .5) + '" x2="' + breite
|
||
+ '" y2="' + (H - .5) + '"/>';
|
||
zahlen.forEach(function (w, i) {
|
||
if (w === null) return;
|
||
const x = i * (b + luecke);
|
||
const h = Math.max(1.5, Math.abs(w) / hoch * (H - 4));
|
||
s += '<rect class="balken' + (i === letzter ? " jetzt" : "") + '" x="' + x + '" y="'
|
||
+ (H - h - 1) + '" width="' + b + '" height="' + h + '" rx="1.5"'
|
||
+ ' data-platz="' + i + '"'
|
||
+ ' data-monat="' + MONATSNAMEN[i] + " " + jahr + '"'
|
||
+ ' data-wert="' + statZahl(w, kz.dez) + " " + kz.einheit + '"></rect>';
|
||
});
|
||
return s + "</svg>";
|
||
}
|
||
|
||
/* --- Zeichnen ---------------------------------------------------------- */
|
||
|
||
function zeichneKopf() {
|
||
const gleich = zeitraum === "gleich";
|
||
const laufend = new Date().getFullYear();
|
||
const paar = vergleichsPaar();
|
||
|
||
let h = '<th scope="col">Kennzahl</th>';
|
||
spaltenJahre().forEach(function (jahr, i) {
|
||
const unten = gleich ? "bis " + stichtagText()
|
||
: (jahr === laufend ? "läuft noch" : "ganzes Jahr");
|
||
h += '<th scope="col"' + (i === 0 ? ' class="jetzt"' : "") + ">" + jahr
|
||
+ '<span class="zeitraum">' + unten + "</span></th>";
|
||
});
|
||
h += '<th scope="col">Δ<span class="zeitraum">'
|
||
+ (paar ? paar[0] + " zu " + paar[1] : "–") + "</span></th>"
|
||
+ '<th scope="col" class="gesamt">Gesamt<span class="zeitraum">seit '
|
||
+ daten.jahre[0] + "</span></th>";
|
||
if (verlauf) {
|
||
h += '<th scope="col" class="verlauf">Monatsverlauf<span class="zeitraum">'
|
||
+ verlaufsJahr() + "</span></th>";
|
||
}
|
||
kopf.innerHTML = h;
|
||
|
||
const text = teil('[data-rolle="zeitraumtext"]');
|
||
if (text) {
|
||
text.innerHTML = gleich
|
||
? "<b>Gleicher Zeitraum:</b> jedes Jahr vom 1. Januar bis zum " + stichtagText()
|
||
+ " – so vergleicht sich das laufende Jahr mit dem, was zum selben Stichtag im "
|
||
+ "Vorjahr zusammengekommen war."
|
||
: "<b>Ganze Jahre:</b> abgeschlossene Jahre vollständig; das laufende Jahr steht "
|
||
+ "so weit, wie es ist, und trägt deshalb keine Tendenz.";
|
||
}
|
||
}
|
||
|
||
function zeichneZeilen() {
|
||
const jahre = spaltenJahre();
|
||
const paar = vergleichsPaar();
|
||
const jahrDesVerlaufs = verlaufsJahr();
|
||
const spalten = jahre.length + 3 + (verlauf ? 1 : 0);
|
||
let h = "";
|
||
|
||
daten.kennzahlen.forEach(function (kz) {
|
||
if (kz.gruppe) {
|
||
h += '<tr class="gruppe"><td colspan="' + spalten + '">' + kz.gruppe + "</td></tr>";
|
||
return;
|
||
}
|
||
|
||
const versteckt = kz.unter && !offen[kz.unter];
|
||
const klassen = [kz.summe ? "summe" : "", kz.teilmenge ? "teilmenge" : "",
|
||
versteckt ? "versteckt" : ""].filter(Boolean).join(" ");
|
||
|
||
/*
|
||
* Beschriftung. Die Einheit steht einmal an der Zeile statt in jeder
|
||
* Spalte - sie ändert sich ja nicht von Jahr zu Jahr. Sie sitzt INNERHALB
|
||
* der Einrückung, damit sie beim Umbruch dem Namen folgt und nicht
|
||
* allein am linken Zellenrand landet.
|
||
*/
|
||
const text = kz.name + '<span class="einheit-klein">' + kz.einheit + "</span>";
|
||
let name;
|
||
if (kz.klapp) {
|
||
/*
|
||
* Zusammengefasste Zeilen stehen zunaechst zu - die Tabelle soll mit
|
||
* zehn Zeilen aufmachen und nicht mit siebzehn. Damit das Zugeklappte
|
||
* nicht verschwindet, sagt die Zeile, wieviel darin steckt.
|
||
*/
|
||
const posten = daten.kennzahlen.filter(k => k.unter === kz.id).length;
|
||
name = '<button type="button" class="klapp" data-klapp="' + kz.id + '" aria-expanded="'
|
||
+ (offen[kz.id] ? "true" : "false") + '"><span class="pfeil">›</span>'
|
||
// Der Name als eigener Baustein: bei knapper Breite rutscht dann
|
||
// die Anzahl in die naechste Zeile, statt den Namen zu zerreissen.
|
||
+ "<span>" + text + "</span>"
|
||
+ (offen[kz.id] || !posten ? ""
|
||
: '<span class="posten">' + posten + " Posten</span>") + "</button>";
|
||
} else if (kz.teilmenge) {
|
||
// "darin" allein liesse offen, worin - deshalb steht der Name der
|
||
// uebergeordneten Zeile daneben. Wer die Tabelle ueberfliegt, soll
|
||
// nicht raten muessen, ob die Zeile direkt darueber gemeint ist.
|
||
const traeger = daten.kennzahlen.find(k => k.id === kz.teilmenge);
|
||
name = '<span class="stufe-2"><span class="darin">'
|
||
+ (traeger ? "im " + traeger.name : "darin") + "</span>" + text + "</span>";
|
||
} else if (kz.teil) {
|
||
name = '<span class="stufe-1">' + text + "</span>";
|
||
} else {
|
||
name = text;
|
||
}
|
||
|
||
h += '<tr class="' + klassen + '"><td class="name">' + name + "</td>";
|
||
|
||
jahre.forEach(function (jahr, i) {
|
||
const w = werteVon(jahr);
|
||
h += '<td class="' + (i === 0 ? "wert-jetzt" : "wert-alt") + '">'
|
||
+ statZahl(w ? w[kz.id] : null, kz.dez) + "</td>";
|
||
});
|
||
|
||
const d = paar ? statDelta((werteVon(paar[0]) || {})[kz.id],
|
||
(werteVon(paar[1]) || {})[kz.id], kz) : null;
|
||
h += "<td>" + (d ? '<span class="delta ' + d.art + '">' + d.zeichen + " " + d.text
|
||
+ "</span>" : '<span class="leer">–</span>') + "</td>";
|
||
|
||
// Die Summe über alles. Verhältniszahlen sind darin über den ganzen
|
||
// Zeitraum gerechnet, nicht über die Jahre gemittelt.
|
||
h += '<td class="gesamt">'
|
||
+ statZahl(daten.gesamt ? daten.gesamt[kz.id] : null, kz.dez) + "</td>";
|
||
|
||
if (verlauf) h += '<td class="verlauf">' + sparkline(kz, jahrDesVerlaufs) + "</td>";
|
||
h += "</tr>";
|
||
});
|
||
|
||
koerper.innerHTML = h;
|
||
}
|
||
|
||
function zeichneLeitzahlen() {
|
||
const paar = vergleichsPaar();
|
||
const jahr = paar ? paar[0] : spaltenJahre()[0];
|
||
const gegen = paar ? paar[1] : null;
|
||
const werte = werteVon(jahr) || {};
|
||
const vorher = gegen ? (werteVon(gegen) || {}) : {};
|
||
|
||
teil(".leitzahlen").innerHTML = daten.leitzahlen.map(function (id, i) {
|
||
const kz = daten.kennzahlen.find(k => k.id === id);
|
||
if (!kz) return "";
|
||
const d = statDelta(werte[id], vorher[id], kz);
|
||
return '<div class="leitzahl' + (i === 0 ? " betont" : "") + '">'
|
||
+ '<div class="name">' + kz.name + "</div>"
|
||
+ '<div class="zahl">' + statZahl(werte[id], kz.dez)
|
||
+ '<span class="einheit">' + kz.einheit + "</span></div>"
|
||
+ '<div class="fuss">'
|
||
+ (d ? '<span class="delta ' + d.art + '">' + d.zeichen + " " + d.text + "</span>" : "")
|
||
+ "<span>" + jahr + (zeitraum === "gleich" ? " bis " + stichtagText() : "")
|
||
+ (gegen ? " gegen " + gegen : "") + "</span></div></div>";
|
||
}).join("");
|
||
}
|
||
|
||
function zeichneJahrwahl() {
|
||
const wahl = teil(".jahrwahl");
|
||
if (!wahl) return;
|
||
const jahre = spaltenJahre();
|
||
wahl.querySelector(".fenster").textContent =
|
||
jahre.length > 1 ? jahre[jahre.length - 1] + "–" + jahre[0] : String(jahre[0] || "–");
|
||
// Links liegt die Vergangenheit: dort ist Schluss beim ersten
|
||
// aufgezeichneten Jahr, rechts beim laufenden.
|
||
wahl.querySelector('[data-schritt="-1"]').disabled =
|
||
fenster >= daten.jahre.length - opt.spalten;
|
||
wahl.querySelector('[data-schritt="1"]').disabled = fenster <= 0;
|
||
}
|
||
|
||
function zeichne() {
|
||
if (!daten || !daten.jahre.length) return;
|
||
zeichneLeitzahlen();
|
||
zeichneKopf();
|
||
zeichneZeilen();
|
||
zeichneJahrwahl();
|
||
}
|
||
|
||
/* --- Bedienung --------------------------------------------------------- */
|
||
|
||
wurzel.querySelectorAll("[data-zeitraum]").forEach(function (b) {
|
||
b.setAttribute("aria-pressed", String(b.dataset.zeitraum === zeitraum));
|
||
b.addEventListener("click", function () {
|
||
zeitraum = b.dataset.zeitraum;
|
||
wurzel.querySelectorAll("[data-zeitraum]").forEach(function (x) {
|
||
x.setAttribute("aria-pressed", String(x === b));
|
||
});
|
||
zeichne();
|
||
});
|
||
});
|
||
|
||
wurzel.querySelectorAll("[data-schritt]").forEach(function (b) {
|
||
b.addEventListener("click", function () {
|
||
const grenze = Math.max(0, daten.jahre.length - opt.spalten);
|
||
fenster = Math.max(0, Math.min(grenze, fenster - Number(b.dataset.schritt)));
|
||
zeichne();
|
||
});
|
||
});
|
||
|
||
const schalter = teil('[data-rolle="verlauf"]');
|
||
if (schalter) {
|
||
schalter.checked = verlauf;
|
||
schalter.addEventListener("change", function (e) {
|
||
verlauf = e.target.checked;
|
||
zeichneKopf();
|
||
zeichneZeilen();
|
||
});
|
||
}
|
||
|
||
koerper.addEventListener("click", function (e) {
|
||
const knopf = e.target.closest("[data-klapp]");
|
||
if (!knopf) return;
|
||
offen[knopf.dataset.klapp] = !offen[knopf.dataset.klapp];
|
||
zeichneZeilen();
|
||
});
|
||
|
||
/*
|
||
* Sprechblase am Monatsverlauf.
|
||
*
|
||
* Nicht je Balken, sondern über die ganze Reihe: ein Balken ist sieben Pixel
|
||
* breit, eine Fingerkuppe deckt rund vierzig - einzeln zu treffen wären sie
|
||
* nicht, und ein hoher Balken deckte seine eigene Trefferfläche zu. Deshalb
|
||
* entscheidet die Stelle, an der Finger oder Zeiger die Reihe berühren,
|
||
* welcher Monat gemeint ist; Wischen fährt sie ab. Damit ist jeder Monat
|
||
* erreichbar, unabhängig von seiner Höhe.
|
||
*/
|
||
const tipp = document.createElement("div");
|
||
tipp.className = "spark-tipp";
|
||
tipp.setAttribute("role", "status");
|
||
document.body.appendChild(tipp);
|
||
|
||
/** Der Balken, über dem die Stelle x liegt - auch wenn sie daneben trifft. */
|
||
function balkenBei(svg, x) {
|
||
const r = svg.getBoundingClientRect();
|
||
if (!r.width) return null;
|
||
const platz = Math.max(0, Math.min(MONATSNAMEN.length - 1,
|
||
Math.floor((x - r.left) / r.width * MONATSNAMEN.length)));
|
||
// Monate ohne Wert haben keinen Balken - dort gibt es nichts zu zeigen.
|
||
return svg.querySelector('rect.balken[data-platz="' + platz + '"]');
|
||
}
|
||
|
||
function zeigeTipp(balken) {
|
||
koerper.querySelectorAll("rect.balken.aktiv").forEach(b => b.classList.remove("aktiv"));
|
||
balken.classList.add("aktiv");
|
||
tipp.textContent = balken.dataset.monat + " · " + balken.dataset.wert;
|
||
const r = balken.getBoundingClientRect();
|
||
tipp.style.left = Math.max(8, Math.min(window.innerWidth - 180, r.left - 60)) + "px";
|
||
tipp.style.top = Math.max(8, r.top - 34) + "px";
|
||
tipp.classList.add("an");
|
||
}
|
||
|
||
function verbergeTipp() {
|
||
koerper.querySelectorAll("rect.balken.aktiv").forEach(b => b.classList.remove("aktiv"));
|
||
tipp.classList.remove("an");
|
||
}
|
||
|
||
/*
|
||
* Gemeint ist die ganze Zelle, nicht die Zeichnung darin: die Balkenreihe
|
||
* ist 24 Pixel hoch, die Zelle gut dreissig - wer knapp darueber oder
|
||
* darunter tippt, meint trotzdem diesen Monat.
|
||
*/
|
||
function zeigeAnStelle(e) {
|
||
const zelle = e.target.closest("td.verlauf");
|
||
const svg = zelle && zelle.querySelector("svg.spark");
|
||
if (!svg) { verbergeTipp(); return; }
|
||
const balken = balkenBei(svg, e.clientX);
|
||
if (balken) zeigeTipp(balken); else verbergeTipp();
|
||
}
|
||
|
||
koerper.addEventListener("pointerdown", zeigeAnStelle);
|
||
koerper.addEventListener("pointermove", zeigeAnStelle);
|
||
koerper.addEventListener("pointerleave", verbergeTipp);
|
||
|
||
/* --- Laden ------------------------------------------------------------- */
|
||
|
||
holeJahresstatistik().then(function (antwort) {
|
||
daten = antwort;
|
||
if (!daten.jahre.length) {
|
||
koerper.innerHTML = '<tr><td class="leer">Noch keine ausgewerteten Tage.</td></tr>';
|
||
return;
|
||
}
|
||
zeichne();
|
||
}).catch(function (e) {
|
||
koerper.innerHTML = '<tr><td class="leer">Statistik nicht abrufbar.</td></tr>';
|
||
console.log("Jahresstatistik: " + e.message);
|
||
});
|
||
|
||
return { zeichne: zeichne };
|
||
}
|