ShineBridge v1.9.2 — Finanzen-Chart modernisiert: Wochen-/2-Wochen-Aggregation, abgerundete Balken, Verlauf
Statt bei langen Abrechnungsjahren jeden einzelnen Tag zu zeigen (bei 112 Tagen unlesbar), aggregiert der Chart jetzt automatisch: - ≤21 Tage: täglich (wie bisher) - 22–180 Tage: wöchentlich (~16 Balken bei einem Jahr) - >180 Tage: 2-wöchentlich Zusätzlich: abgerundete Balken mit Farbverlauf statt flacher Füllung, echter Abstand zwischen Festpreis-/Spot-Balkenpaar statt direkt aneinandergrenzender Hälften, gestrichelte Gridlines, HTML-Legende mit runden Farbchips statt SVG-Text-Legende. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
name: ShineBridge
|
||||
version: "1.9.1"
|
||||
version: "1.9.2"
|
||||
slug: shinebridge
|
||||
description: Energiemanagement für Growatt/Goodwe-Wechselrichter — PV-Überschussladen, Wallbox-EMS, Tarifvergleich, Zigbee2MQTT-Überschussgeräte
|
||||
url: https://gitea.bitfire.work/retr0/shinebridge
|
||||
|
||||
@@ -797,19 +797,37 @@ async function loadFinance() {
|
||||
</div>`;
|
||||
|
||||
// ── SVG-Balkendiagramm ────────────────────────────────────────
|
||||
// Breite skaliert mit der Anzahl Tage (Mindestplatz pro Tag), statt bei vielen
|
||||
// Tagen alles in eine feste Breite zu quetschen — sonst werden Balken bei
|
||||
// langen Abrechnungsjahren (>60 Tage) zu Haarlinien und unlesbar.
|
||||
const H = 180, PL = 44, PR = 12, PT = 10, PB = 28;
|
||||
const n = days.length;
|
||||
const MIN_SLOT = 10; // px pro Tag (Balkenpaar + Abstand), unterhalb dessen nichts mehr lesbar ist
|
||||
// Bei langen Abrechnungsjahren nicht jeden einzelnen Tag zeigen — statt 112
|
||||
// Tagesbalken lieber ~16 Wochenbalken, deutlich lesbarer und trotzdem
|
||||
// aussagekräftig für "würde sich Spot lohnen".
|
||||
const rawN = days.length;
|
||||
const bucket = rawN > 180 ? 14 : rawN > 21 ? 7 : 1;
|
||||
const periods = bucket === 1 ? days.map(d => ({
|
||||
fixed_eur: d.fixed_eur, spot_eur: d.spot_eur, label: d.date.slice(5),
|
||||
})) : (() => {
|
||||
const out = [];
|
||||
for (let i = 0; i < days.length; i += bucket) {
|
||||
const chunk = days.slice(i, i + bucket);
|
||||
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 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: from === to ? from : `${from}–${to}` });
|
||||
}
|
||||
return out;
|
||||
})();
|
||||
|
||||
const n = periods.length;
|
||||
const H = 190, PL = 44, PR = 12, PT = 14, PB = 30;
|
||||
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 cH = H - PT - PB;
|
||||
const barW = Math.max(3, Math.floor(cW / n) - 2);
|
||||
const gap = Math.max(1, Math.floor(cW / n) - barW);
|
||||
const slot = cW / n;
|
||||
const barW = Math.min(22, Math.max(4, slot * 0.34));
|
||||
const barGap = Math.max(2, slot * 0.06);
|
||||
|
||||
const allVals = days.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 yTicks = 4;
|
||||
@@ -817,40 +835,52 @@ async function loadFinance() {
|
||||
for (let i = 0; i <= yTicks; i++) {
|
||||
const v = maxVal * 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="0.5"/>`;
|
||||
yLabels += `<text x="${PL-4}" y="${(y+4).toFixed(1)}" text-anchor="end" font-size="9" fill="#8b949e">${fEur(v)}</text>`;
|
||||
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"/>`;
|
||||
yLabels += `<text x="${PL-8}" y="${(y+3).toFixed(1)}" text-anchor="end" font-size="10" fill="#8b949e">${fEur(v)}</text>`;
|
||||
}
|
||||
|
||||
// Label-Abstand an der tatsächlichen Balkenbreite ausrichten (~60px pro Label),
|
||||
// statt pauschal 8 Labels über die ganze — jetzt variable — Breite zu verteilen.
|
||||
const labelStep = Math.max(1, Math.round(60 / (barW + gap)));
|
||||
const labelStep = Math.max(1, Math.round(50 / slot));
|
||||
const rBar = Math.min(3, barW / 2);
|
||||
let bars = '', xLabels = '';
|
||||
days.forEach((d, i) => {
|
||||
const x0 = PL + i * (barW + gap);
|
||||
const hFixed = cH * d.fixed_eur / maxVal;
|
||||
bars += `<rect x="${x0}" y="${(PT + cH - hFixed).toFixed(1)}" width="${barW/2}" height="${hFixed.toFixed(1)}" fill="${C.fixed}" opacity="0.85"/>`;
|
||||
periods.forEach((d, i) => {
|
||||
const cx = PL + i * slot + slot / 2;
|
||||
const hFixed = Math.max(0, cH * d.fixed_eur / maxVal);
|
||||
bars += `<rect x="${(cx - barGap/2 - barW).toFixed(1)}" y="${(PT + cH - hFixed).toFixed(1)}" width="${barW.toFixed(1)}" height="${Math.max(hFixed,1).toFixed(1)}" rx="${rBar}" fill="url(#gradFixed)"/>`;
|
||||
if (d.spot_eur != null) {
|
||||
const hSpot = cH * d.spot_eur / maxVal;
|
||||
bars += `<rect x="${x0 + barW/2}" y="${(PT + cH - hSpot).toFixed(1)}" width="${barW/2}" height="${hSpot.toFixed(1)}" fill="${C.spot}" opacity="0.85"/>`;
|
||||
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)"/>`;
|
||||
}
|
||||
if (i % labelStep === 0) {
|
||||
const label = d.date.slice(5);
|
||||
xLabels += `<text x="${(x0 + barW/2).toFixed(1)}" y="${H - 4}" text-anchor="middle" font-size="9" fill="#8b949e">${label}</text>`;
|
||||
xLabels += `<text x="${cx.toFixed(1)}" y="${H - 6}" text-anchor="middle" font-size="9" fill="#8b949e">${d.label}</text>`;
|
||||
}
|
||||
});
|
||||
|
||||
const legend = `<text x="${PL}" y="${H+18}" font-size="10" fill="${C.fixed}">■ Festpreis</text>
|
||||
<text x="${PL+70}" y="${H+18}" font-size="10" fill="${C.spot}">■ Spot (hypothetisch)</text>`;
|
||||
const defs = `<defs>
|
||||
<linearGradient id="gradFixed" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="${C.fixed}" stop-opacity="0.95"/>
|
||||
<stop offset="100%" stop-color="${C.fixed}" stop-opacity="0.55"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="gradSpot" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="${C.spot}" stop-opacity="0.95"/>
|
||||
<stop offset="100%" stop-color="${C.spot}" stop-opacity="0.55"/>
|
||||
</linearGradient>
|
||||
</defs>`;
|
||||
|
||||
const bucketLabel = bucket === 1 ? 'Tag' : bucket === 7 ? 'Woche' : '2 Wochen';
|
||||
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.spot};display:inline-block"></span>Spot (hypothetisch)</span>
|
||||
</div>`;
|
||||
|
||||
const chartSection = `
|
||||
<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 Tag</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}</div>
|
||||
<div style="overflow-x:auto">
|
||||
<svg viewBox="0 0 ${W} ${H+24}" width="${W}" height="${H+24}" style="width:${W}px;max-width:none;display:block">
|
||||
${gridLines}${yLabels}${bars}${xLabels}${legend}
|
||||
<svg viewBox="0 0 ${W} ${H}" width="${W}" height="${H}" style="width:${W}px;max-width:none;display:block">
|
||||
${defs}${gridLines}${yLabels}${bars}${xLabels}
|
||||
</svg>
|
||||
</div>
|
||||
${n > 60 ? `<div style="font-size:11px;color:var(--text-dim);margin-top:6px">← Horizontal scrollbar, ${n} Tage</div>` : ''}
|
||||
${legend}
|
||||
</div>`;
|
||||
|
||||
el.innerHTML = `<div style="padding:4px 0">${empfehlung}${cards}${chartSection}</div>`;
|
||||
|
||||
Reference in New Issue
Block a user