ShineBridge v1.9.3 — Finanzen-Chart: auf letzte 14 Perioden begrenzt, echt responsiv

- Zeigt nur noch die letzten 14 Balken (statt aller Perioden im Abrechnungsjahr)
  — genug für einen Trend, ohne dass Labels sich gegenseitig überlappen
- SVG jetzt echt responsiv (viewBox + width:100%, Mobile-first dimensioniert)
  statt fester Pixelbreite mit Scrollbalken — passt sich Handy wie Desktop an
- X-Achsen-Labels um -40° gedreht statt horizontal — brauchen dadurch deutlich
  weniger Platz pro Balken, alle 14 bleiben lesbar

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
retr0
2026-08-25 14:02:33 +02:00
co-authored by Claude Sonnet 5
parent 722301b1d6
commit 608f6bfd42
2 changed files with 25 additions and 24 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
name: ShineBridge name: ShineBridge
version: "1.9.2" version: "1.9.3"
slug: shinebridge slug: shinebridge
description: Energiemanagement für Growatt/Goodwe-Wechselrichter — PV-Überschussladen, Wallbox-EMS, Tarifvergleich, Zigbee2MQTT-Überschussgeräte description: Energiemanagement für Growatt/Goodwe-Wechselrichter — PV-Überschussladen, Wallbox-EMS, Tarifvergleich, Zigbee2MQTT-Überschussgeräte
url: https://gitea.bitfire.work/retr0/shinebridge url: https://gitea.bitfire.work/retr0/shinebridge
+22 -21
View File
@@ -798,11 +798,11 @@ async function loadFinance() {
// ── SVG-Balkendiagramm ──────────────────────────────────────── // ── SVG-Balkendiagramm ────────────────────────────────────────
// Bei langen Abrechnungsjahren nicht jeden einzelnen Tag zeigen — statt 112 // Bei langen Abrechnungsjahren nicht jeden einzelnen Tag zeigen — statt 112
// Tagesbalken lieber ~16 Wochenbalken, deutlich lesbarer und trotzdem // Tagesbalken lieber Wochenbalken, und davon nur die letzten 14 (reicht für
// aussagekräftig für "würde sich Spot lohnen". // einen Trend, bleibt aber lesbar statt endlos zu scrollen).
const rawN = days.length; const rawN = days.length;
const bucket = rawN > 180 ? 14 : rawN > 21 ? 7 : 1; const bucket = rawN > 180 ? 14 : rawN > 21 ? 7 : 1;
const periods = bucket === 1 ? days.map(d => ({ const allPeriods = bucket === 1 ? days.map(d => ({
fixed_eur: d.fixed_eur, spot_eur: d.spot_eur, label: d.date.slice(5), fixed_eur: d.fixed_eur, spot_eur: d.spot_eur, label: d.date.slice(5),
})) : (() => { })) : (() => {
const out = []; const out = [];
@@ -811,21 +811,24 @@ async function loadFinance() {
const fixed = chunk.reduce((s, d) => s + (d.fixed_eur || 0), 0); const fixed = chunk.reduce((s, d) => s + (d.fixed_eur || 0), 0);
const spotVals = chunk.map(d => d.spot_eur).filter(v => v != null); const spotVals = chunk.map(d => d.spot_eur).filter(v => v != null);
const spot = spotVals.length ? spotVals.reduce((a, b) => a + b, 0) : null; const spot = spotVals.length ? spotVals.reduce((a, b) => a + b, 0) : null;
const from = chunk[0].date.slice(5), to = chunk[chunk.length - 1].date.slice(5); out.push({ fixed_eur: fixed, spot_eur: spot, label: chunk[0].date.slice(5) });
out.push({ fixed_eur: fixed, spot_eur: spot, label: from === to ? from : `${from}–${to}` });
} }
return out; return out;
})(); })();
const MAX_PERIODS = 14;
const periods = allPeriods.slice(-MAX_PERIODS);
// Echtes responsives SVG: viewBox in einer "Mobile-first"-Breite, skaliert per
// width:100% frei auf den Container (Handy wie PC) — kein Scrollen mehr nötig,
// da wir uns auf max. 14 Balken begrenzen.
const n = periods.length; const n = periods.length;
const H = 190, PL = 44, PR = 12, PT = 14, PB = 30; const W = 380, H = 220, PL = 40, PR = 8, PT = 14, PB = 46;
const MIN_SLOT = 34; // px pro Balkenpaar — bewusst großzügig, jetzt wo wenige Balken übrig sind
const W = Math.max(600, PL + PR + n * MIN_SLOT);
const cW = W - PL - PR; const cW = W - PL - PR;
const cH = H - PT - PB; const cH = H - PT - PB;
const slot = cW / n; const slot = cW / n;
const barW = Math.min(22, Math.max(4, slot * 0.34)); const barW = Math.max(3, slot * 0.36);
const barGap = Math.max(2, slot * 0.06); const barGap = Math.max(1.5, slot * 0.08);
const rBar = Math.min(2.5, barW / 2);
const allVals = periods.flatMap(d => [d.fixed_eur, d.spot_eur].filter(v => v != null)); const allVals = periods.flatMap(d => [d.fixed_eur, d.spot_eur].filter(v => v != null));
const maxVal = Math.max(...allVals, 0.01); const maxVal = Math.max(...allVals, 0.01);
@@ -835,12 +838,12 @@ async function loadFinance() {
for (let i = 0; i <= yTicks; i++) { for (let i = 0; i <= yTicks; i++) {
const v = maxVal * i / yTicks; const v = maxVal * i / yTicks;
const y = PT + cH - (cH * i / yTicks); const y = PT + cH - (cH * i / yTicks);
gridLines += `<line x1="${PL}" y1="${y.toFixed(1)}" x2="${W-PR}" y2="${y.toFixed(1)}" stroke="${C.grid}" stroke-width="1" stroke-dasharray="2,4"/>`; gridLines += `<line x1="${PL}" y1="${y.toFixed(1)}" x2="${W-PR}" y2="${y.toFixed(1)}" stroke="${C.grid}" stroke-width="1" stroke-dasharray="2,3"/>`;
yLabels += `<text x="${PL-8}" y="${(y+3).toFixed(1)}" text-anchor="end" font-size="10" fill="#8b949e">${fEur(v)}</text>`; yLabels += `<text x="${PL-6}" y="${(y+3).toFixed(1)}" text-anchor="end" font-size="9" fill="#8b949e">${fEur(v)}</text>`;
} }
const labelStep = Math.max(1, Math.round(50 / slot)); // Labels schräg statt horizontal — braucht bei 14 Balken pro Label deutlich
const rBar = Math.min(3, barW / 2); // weniger Breite, dadurch bleiben trotz Platzmangel alle 14 lesbar.
let bars = '', xLabels = ''; let bars = '', xLabels = '';
periods.forEach((d, i) => { periods.forEach((d, i) => {
const cx = PL + i * slot + slot / 2; const cx = PL + i * slot + slot / 2;
@@ -850,9 +853,8 @@ async function loadFinance() {
const hSpot = Math.max(0, cH * d.spot_eur / maxVal); const hSpot = Math.max(0, cH * d.spot_eur / maxVal);
bars += `<rect x="${(cx + barGap/2).toFixed(1)}" y="${(PT + cH - hSpot).toFixed(1)}" width="${barW.toFixed(1)}" height="${Math.max(hSpot,1).toFixed(1)}" rx="${rBar}" fill="url(#gradSpot)"/>`; bars += `<rect x="${(cx + barGap/2).toFixed(1)}" y="${(PT + cH - hSpot).toFixed(1)}" width="${barW.toFixed(1)}" height="${Math.max(hSpot,1).toFixed(1)}" rx="${rBar}" fill="url(#gradSpot)"/>`;
} }
if (i % labelStep === 0) { const ly = H - PB + 12;
xLabels += `<text x="${cx.toFixed(1)}" y="${H - 6}" text-anchor="middle" font-size="9" fill="#8b949e">${d.label}</text>`; xLabels += `<text x="${cx.toFixed(1)}" y="${ly}" text-anchor="end" font-size="8.5" fill="#8b949e" transform="rotate(-40 ${cx.toFixed(1)} ${ly})">${d.label}</text>`;
}
}); });
const defs = `<defs> const defs = `<defs>
@@ -867,6 +869,7 @@ async function loadFinance() {
</defs>`; </defs>`;
const bucketLabel = bucket === 1 ? 'Tag' : bucket === 7 ? 'Woche' : '2 Wochen'; const bucketLabel = bucket === 1 ? 'Tag' : bucket === 7 ? 'Woche' : '2 Wochen';
const rangeNote = allPeriods.length > MAX_PERIODS ? ` · letzte ${n}` : '';
const legend = `<div style="display:flex;gap:18px;margin-top:10px;font-size:11px;color:var(--text-dim)"> const legend = `<div style="display:flex;gap:18px;margin-top:10px;font-size:11px;color:var(--text-dim)">
<span style="display:inline-flex;align-items:center;gap:6px"><span style="width:10px;height:10px;border-radius:3px;background:${C.fixed};display:inline-block"></span>Festpreis</span> <span style="display:inline-flex;align-items:center;gap:6px"><span style="width:10px;height:10px;border-radius:3px;background:${C.fixed};display:inline-block"></span>Festpreis</span>
<span style="display:inline-flex;align-items:center;gap:6px"><span style="width:10px;height:10px;border-radius:3px;background:${C.spot};display:inline-block"></span>Spot (hypothetisch)</span> <span style="display:inline-flex;align-items:center;gap:6px"><span style="width:10px;height:10px;border-radius:3px;background:${C.spot};display:inline-block"></span>Spot (hypothetisch)</span>
@@ -874,12 +877,10 @@ async function loadFinance() {
const chartSection = ` const chartSection = `
<div style="border-top:1px solid var(--border);padding-top:24px;margin-top:4px"> <div style="border-top:1px solid var(--border);padding-top:24px;margin-top:4px">
<div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:14px">Kosten je ${bucketLabel}</div> <div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:14px">Kosten je ${bucketLabel}${rangeNote}</div>
<div style="overflow-x:auto"> <svg viewBox="0 0 ${W} ${H}" style="width:100%;height:auto;display:block">
<svg viewBox="0 0 ${W} ${H}" width="${W}" height="${H}" style="width:${W}px;max-width:none;display:block">
${defs}${gridLines}${yLabels}${bars}${xLabels} ${defs}${gridLines}${yLabels}${bars}${xLabels}
</svg> </svg>
</div>
${legend} ${legend}
</div>`; </div>`;