From 65dd010227a9be05e855ee07d47360f9d1af47a3 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Sat, 16 May 2026 22:15:04 +0200 Subject: [PATCH] History UI: optimize production chart (#29946) --- assets/js/components/History/GroupChart.vue | 18 ++-- assets/js/views/History.vue | 91 ++++++++++++--------- 2 files changed, 63 insertions(+), 46 deletions(-) diff --git a/assets/js/components/History/GroupChart.vue b/assets/js/components/History/GroupChart.vue index e6b878406..5c56293d9 100644 --- a/assets/js/components/History/GroupChart.vue +++ b/assets/js/components/History/GroupChart.vue @@ -97,10 +97,8 @@ export default defineComponent({ valueFactor(): number { return this.period === PERIODS.DAY ? 4 : 1; }, - // Peak absolute net power per slot in kW (day view only). Absolute so - // export-dominant slots (battery discharge, grid export) count too. - maxVisibleKw(): number { - if (this.period !== PERIODS.DAY) return Infinity; + // Peak per-slot stacked value in the display unit (kW for day, kWh otherwise). + axisPeak(): number { const factor = this.valueFactor; const slotSums = new Map(); for (const s of this.visibleSeries) { @@ -113,10 +111,18 @@ export default defineComponent({ for (const v of slotSums.values()) if (v > max) max = v; return max; }, + maxVisibleKw(): number { + return this.period === PERIODS.DAY ? this.axisPeak : Infinity; + }, // Switch to watts when the peak is below 1 kW so small values stay readable. useWatts(): boolean { return this.period === PERIODS.DAY && this.maxVisibleKw < 1; }, + // 1 decimal when echarts may pick 0.5-step splits, else 0 — avoids "1, 1, 2, 2". + axisDigits(): number { + if (this.isBidirectional || this.useWatts) return 0; + return this.axisPeak < 3 ? 1 : 0; + }, unit(): "W" | "kW" | "kWh" { if (this.period !== PERIODS.DAY) return "kWh"; return this.useWatts ? "W" : "kW"; @@ -607,9 +613,9 @@ export default defineComponent({ v * 1000, this.useWatts ? POWER_UNIT.W : POWER_UNIT.KW, false, - 0 + this.axisDigits ) - : this.fmtWh(v * 1000, POWER_UNIT.KW, false, 0), + : this.fmtWh(v * 1000, POWER_UNIT.KW, false, this.axisDigits), }, }), series: this.echartsSeries, diff --git a/assets/js/views/History.vue b/assets/js/views/History.vue index 29bdef336..c9b72a825 100644 --- a/assets/js/views/History.vue +++ b/assets/js/views/History.vue @@ -53,37 +53,16 @@ class="history-tile mb-4" :data-testid="`history-section-${group}`" > -
-

- - {{ $t(`main.history.group.${group}`) }} - - - {{ groupTotalLabel(group) }} - -

-
- - -
-
+

+ + {{ $t(`main.history.group.${group}`) }} + + + {{ groupTotalLabel(group) }} + +

  • {{ legend.label }} {{ legend.value }}
  • +
  • + + + {{ $t("main.history.group.forecast") }} + + + {{ forecastTotalLabel }} + +

@@ -193,7 +195,6 @@ export default defineComponent({ loading: true, interval: null as ReturnType | null, startDate: new Date(2020, 0, 1), - showForecast: true, focusedEntity: {} as Record, }; }, @@ -358,6 +359,14 @@ export default defineComponent({ s.data.some((slot) => slot.energy !== 0 || slot.returnEnergy !== 0) ); }, + forecastTotalLabel(): string { + const list = this.seriesByGroup["forecast"] || []; + let sum = 0; + for (const s of list) { + for (const slot of s.data) sum += slot.energy - slot.returnEnergy; + } + return this.fmtWh(Math.abs(sum) * 1000, POWER_UNIT.AUTO); + }, csvLink(): string { const params = new URLSearchParams({ format: "csv", @@ -406,6 +415,9 @@ export default defineComponent({ if (group === "pv" || group === "battery") return list.length > 1; return false; }, + hasForecastLegend(group: string): boolean { + return group === "pv" && this.hasForecast && this.effectivePeriod === PERIODS.DAY; + }, entityLegends(group: string): (Legend & { entityIndex: number })[] { const list = this.displaySeries(group); const baseColor = groupColor(group); @@ -594,13 +606,12 @@ export default defineComponent({ color: var(--evcc-default-text); text-decoration: underline; } -.forecast-toggle .form-check-input:checked { - background-color: var(--forecast-color); - border-color: var(--forecast-color); +.entity-legend-dot--line { + height: 2px; + border-radius: 1px; } -.forecast-toggle .form-check-input:focus { - border-color: var(--forecast-color); - box-shadow: 0 0 0 0.25rem color-mix(in srgb, var(--forecast-color) 25%, transparent); +.entity-legend-item--static { + cursor: default; } .history-tile-title { margin: 0 0 0.5rem;