/*
* 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 =
'
'
+ (opt.werkzeuge ? '
'
+ ''
+ ''
+ ''
+ ''
+ ''
+ '–'
+ ''
+ '
" : "")
+ '
'
+ "
"
+ '
Statistik wird geladen …
'
+ "
"
+ (opt.fussnoten ? '
'
+ "
Davon teilt den Ertrag auf: Eigenverbrauch, Einspeisung und Heizung "
+ "ergeben ihn zusammen. Darin 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.
"
+ "
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.
" : "");
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 = '";
}
/* --- Zeichnen ---------------------------------------------------------- */
function zeichneKopf() {
const gleich = zeitraum === "gleich";
const laufend = new Date().getFullYear();
const paar = vergleichsPaar();
let h = '
";
}
kopf.innerHTML = h;
const text = teil('[data-rolle="zeitraumtext"]');
if (text) {
text.innerHTML = gleich
? "Gleicher Zeitraum: jedes Jahr vom 1. Januar bis zum " + stichtagText()
+ " – so vergleicht sich das laufende Jahr mit dem, was zum selben Stichtag im "
+ "Vorjahr zusammengekommen war."
: "Ganze Jahre: 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 += '
' + kz.gruppe + "
";
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 + '' + kz.einheit + "";
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 = '";
} 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 = ''
+ (traeger ? "im " + traeger.name : "darin") + "" + text + "";
} else if (kz.teil) {
name = '' + text + "";
} else {
name = text;
}
h += '
' + name + "
";
jahre.forEach(function (jahr, i) {
const w = werteVon(jahr);
h += '
'
+ statZahl(w ? w[kz.id] : null, kz.dez) + "
";
});
const d = paar ? statDelta((werteVon(paar[0]) || {})[kz.id],
(werteVon(paar[1]) || {})[kz.id], kz) : null;
h += "