History: normalize tooltip units, add total row (#32262)

This commit is contained in:
Michael Geers 2026-07-29 14:39:31 +02:00 • committed by GitHub
parent 03a8c744d0
commit ce75e49192
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 81 additions and 39 deletions

View file

@ -466,14 +466,9 @@ export default defineComponent({
return Math.max(0, this.gridPower * -1); return Math.max(0, this.gridPower * -1);
}, },
powerUnit() { powerUnit() {
const watt = Math.max(this.gridImport, this.selfPv, this.selfBattery, this.pvExport); return this.getPowerUnit(
if (watt >= 1_000_000) { Math.max(this.gridImport, this.selfPv, this.selfBattery, this.pvExport)
return POWER_UNIT.MW; );
} else if (watt >= 1000) {
return POWER_UNIT.KW;
} else {
return POWER_UNIT.W;
}
}, },
inPower() { inPower() {
return this.gridImport + this.pvProduction + this.batteryDischarge; return this.gridImport + this.pvProduction + this.batteryDischarge;

View file

@ -120,6 +120,7 @@ export function tooltipStyle(
export interface TooltipRow { export interface TooltipRow {
name?: string; name?: string;
values: string[]; values: string[];
total?: boolean;
} }
// Shared tooltip: bold date headline, non-bold rows, optional name + value columns. // Shared tooltip: bold date headline, non-bold rows, optional name + value columns.
@ -127,28 +128,40 @@ export function tooltipTable(head: string, rows: TooltipRow[], headers?: string[
const hasName = rows.some((r) => r.name != null); const hasName = rows.some((r) => r.name != null);
const valueCols = Math.max(1, ...rows.map((r) => r.values.length)); const valueCols = Math.max(1, ...rows.map((r) => r.values.length));
const colCount = (hasName ? 1 : 0) + valueCols; const colCount = (hasName ? 1 : 0) + valueCols;
// No name col + two value cols: first col left-aligned, second right-aligned.
// Otherwise: lone value centers, multiple/named columns right-align.
const valClsFn = (i: number): string => { const valClsFn = (i: number): string => {
if (!hasName && valueCols > 1 && i === 0) return "fw-normal text-start"; // first of two unnamed value cols
if (hasName || valueCols > 1) return "fw-normal text-end ps-3"; if (!hasName && valueCols > 1 && i === 0) return "text-start";
return "fw-normal text-center"; // named or multiple cols
if (hasName || valueCols > 1) return "text-end ps-3";
// lone value
return "text-center";
}; };
const headerRow = headers?.length const headerRow = headers?.length
? `<tr>${hasName ? "<td></td>" : ""}${headers ? `<tr>${hasName ? "<td></td>" : ""}${headers
.map((h, i) => `<td class="${valClsFn(i)}">${h}</td>`) .map((h, i) => `<td class="fw-normal tabular ${valClsFn(i)}">${h}</td>`)
.join("")}</tr>` .join("")}</tr>`
: ""; : "";
const rowHtml = (r: TooltipRow) => {
const cls = r.total ? " pt-1" : "";
const nameTd = hasName
? `<td class="fw-normal text-start${cls}">${escapeHtml(r.name ?? "")}</td>`
: "";
const valTds = r.values
.map((v, i) => `<td class="fw-normal tabular ${valClsFn(i)}${cls}">${v}</td>`)
.join("");
return `<tr>${nameTd}${valTds}</tr>`;
};
const body = rows const body = rows
.map((r) => { .filter((r) => !r.total)
const nameTd = hasName .map(rowHtml)
? `<td class="fw-normal text-start">${escapeHtml(r.name ?? "")}</td>`
: "";
const valTds = r.values.map((v, i) => `<td class="${valClsFn(i)}">${v}</td>`).join("");
return `<tr>${nameTd}${valTds}</tr>`;
})
.join(""); .join("");
return `<table class="lh-sm"><thead><tr><th colspan="${colCount}" class="fw-bold text-center">${head}</th></tr></thead><tbody>${headerRow}${body}</tbody></table>`; const footRows = rows.filter((r) => r.total);
const foot = footRows.length
? `<tfoot><tr><td colspan="${colCount}" class="pt-1 border-bottom"></td></tr>${footRows
.map(rowHtml)
.join("")}</tfoot>`
: "";
return `<table class="lh-sm"><thead><tr><th colspan="${colCount}" class="fw-bold text-center pb-1">${head}</th></tr></thead><tbody>${headerRow}${body}</tbody>${foot}</table>`;
} }
export function forecastGrid() { export function forecastGrid() {

View file

@ -14,6 +14,7 @@ import {
tooltipStyle, tooltipStyle,
tooltipTable, tooltipTable,
xAxisLabelStyle, xAxisLabelStyle,
type TooltipRow,
} from "../Forecast/echarts"; } from "../Forecast/echarts";
import colors, { resolveColors, deviceColorMap, darken, batteryColor, setAlpha } from "@/colors"; import colors, { resolveColors, deviceColorMap, darken, batteryColor, setAlpha } from "@/colors";
import store from "@/store"; import store from "@/store";
@ -528,12 +529,6 @@ export default defineComponent({
if (!first) return ""; if (!first) return "";
const ts = cats[first.dataIndex]; const ts = cats[first.dataIndex];
const head = ts != null ? tooltipDate(ts) : ""; const head = ts != null ? tooltipDate(ts) : "";
const formatValue = (v: number) => {
const watts = Math.abs(v) * 1000;
return this.period === PERIODS.DAY
? this.fmtW(watts, POWER_UNIT.AUTO)
: this.fmtWh(watts, POWER_UNIT.AUTO);
};
// Collect energy/returnEnergy values per entity from this slot's params. // Collect energy/returnEnergy values per entity from this slot's params.
const totals = new Map<number, { energy: number; returnEnergy: number }>(); const totals = new Map<number, { energy: number; returnEnergy: number }>();
@ -559,8 +554,29 @@ export default defineComponent({
); );
const showName = this.series.length > 1 && this.focusedEntity === null; const showName = this.series.length > 1 && this.focusedEntity === null;
const rows = indices.map((i) => { // one unit for all rows, based on the largest individual value (not the total)
const t = totals.get(i) ?? { energy: 0, returnEnergy: 0 }; const rowValues = indices.map(
(i) => totals.get(i) ?? { energy: 0, returnEnergy: 0 }
);
const unit = this.getPowerUnit(
Math.max(
0,
...rowValues.flatMap((t) =>
this.isBidirectional
? [t.energy, t.returnEnergy]
: [t.energy + t.returnEnergy]
)
) * 1000
);
const formatValue = (v: number) => {
const watts = Math.abs(v) * 1000;
return this.period === PERIODS.DAY
? this.fmtW(watts, unit)
: this.fmtWh(watts, unit);
};
const rows: TooltipRow[] = indices.map((i, idx) => {
const t = rowValues[idx] ?? { energy: 0, returnEnergy: 0 };
const values = this.isBidirectional const values = this.isBidirectional
? [formatValue(t.energy), formatValue(t.returnEnergy)] ? [formatValue(t.energy), formatValue(t.returnEnergy)]
: [formatValue(t.energy + t.returnEnergy)]; : [formatValue(t.energy + t.returnEnergy)];
@ -569,6 +585,17 @@ export default defineComponent({
values, values,
}; };
}); });
if (showName) {
const sum = (key: "energy" | "returnEnergy") =>
rowValues.reduce((acc, t) => acc + t[key], 0);
rows.push({
name: this.$t("sessions.total"),
values: this.isBidirectional
? [formatValue(sum("energy")), formatValue(sum("returnEnergy"))]
: [formatValue(sum("energy") + sum("returnEnergy"))],
total: true,
});
}
return tooltipTable(head, rows, this.directionHeaders ?? undefined); return tooltipTable(head, rows, this.directionHeaders ?? undefined);
}, },
}, },

View file

@ -56,6 +56,16 @@ describe("fmtW", () => {
}); });
}); });
describe("getPowerUnit", () => {
test("should pick unit based on largest value", () => {
expect(fmt.getPowerUnit(0)).eq(POWER_UNIT.W);
expect(fmt.getPowerUnit(999)).eq(POWER_UNIT.W);
expect(fmt.getPowerUnit(1000)).eq(POWER_UNIT.KW);
expect(fmt.getPowerUnit(9_999_999)).eq(POWER_UNIT.KW);
expect(fmt.getPowerUnit(10_000_000)).eq(POWER_UNIT.MW);
});
});
describe("fmtWh", () => { describe("fmtWh", () => {
test("should format with units", () => { test("should format with units", () => {
expect(fmt.fmtWh(0, POWER_UNIT.AUTO)).eq("0,0 kWh"); expect(fmt.fmtWh(0, POWER_UNIT.AUTO)).eq("0,0 kWh");

View file

@ -73,17 +73,14 @@ export default defineComponent({
const base = 10 ** precision; const base = 10 ** precision;
return (Math.round(num * base) / base).toFixed(precision); return (Math.round(num * base) / base).toFixed(precision);
}, },
getPowerUnit(watt: number): POWER_UNIT {
return watt >= 10_000_000 ? POWER_UNIT.MW : watt >= 1000 ? POWER_UNIT.KW : POWER_UNIT.W;
},
fmtW(watt = 0, format = POWER_UNIT.KW, withUnit = true, digits?: number) { fmtW(watt = 0, format = POWER_UNIT.KW, withUnit = true, digits?: number) {
let unit = format; let unit = format;
let d = digits; let d = digits;
if (POWER_UNIT.AUTO === unit) { if (POWER_UNIT.AUTO === unit) {
if (watt >= 10_000_000) { unit = watt === 0 ? POWER_UNIT.KW : this.getPowerUnit(watt);
unit = POWER_UNIT.MW;
} else if (watt >= 1000 || 0 === watt) {
unit = POWER_UNIT.KW;
} else {
unit = POWER_UNIT.W;
}
} }
let value = watt; let value = watt;
if (POWER_UNIT.KW === unit) { if (POWER_UNIT.KW === unit) {

View file

@ -26,8 +26,8 @@ test("tooltip shows one merged grid entity", async ({ page }) => {
const tooltip = chart.locator("table"); const tooltip = chart.locator("table");
await expect(tooltip).toBeVisible(); await expect(tooltip).toBeVisible();
// single unnamed row, no per-entity rows // single unnamed row, no per-entity rows; one unit across both columns
await expect(tooltip).toHaveText( await expect(tooltip).toHaveText(
["12:00 – 12:15", "imported", "exported", "2.0 kW", "400 W"].join("") ["12:00 – 12:15", "imported", "exported", "2.0 kW", "0.4 kW"].join("")
); );
}); });