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);
},
powerUnit() {
const watt = Math.max(this.gridImport, this.selfPv, this.selfBattery, this.pvExport);
if (watt >= 1_000_000) {
return POWER_UNIT.MW;
} else if (watt >= 1000) {
return POWER_UNIT.KW;
} else {
return POWER_UNIT.W;
}
return this.getPowerUnit(
Math.max(this.gridImport, this.selfPv, this.selfBattery, this.pvExport)
);
},
inPower() {
return this.gridImport + this.pvProduction + this.batteryDischarge;

View file

@ -120,6 +120,7 @@ export function tooltipStyle(
export interface TooltipRow {
name?: string;
values: string[];
total?: boolean;
}
// 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 valueCols = Math.max(1, ...rows.map((r) => r.values.length));
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 => {
if (!hasName && valueCols > 1 && i === 0) return "fw-normal text-start";
if (hasName || valueCols > 1) return "fw-normal text-end ps-3";
return "fw-normal text-center";
// first of two unnamed value cols
if (!hasName && valueCols > 1 && i === 0) return "text-start";
// named or multiple cols
if (hasName || valueCols > 1) return "text-end ps-3";
// lone value
return "text-center";
};
const headerRow = headers?.length
? `<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>`
: "";
const body = rows
.map((r) => {
const rowHtml = (r: TooltipRow) => {
const cls = r.total ? " pt-1" : "";
const nameTd = hasName
? `<td class="fw-normal text-start">${escapeHtml(r.name ?? "")}</td>`
? `<td class="fw-normal text-start${cls}">${escapeHtml(r.name ?? "")}</td>`
: "";
const valTds = r.values.map((v, i) => `<td class="${valClsFn(i)}">${v}</td>`).join("");
return `<tr>${nameTd}${valTds}</tr>`;
})
const valTds = r.values
.map((v, i) => `<td class="fw-normal tabular ${valClsFn(i)}${cls}">${v}</td>`)
.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>`;
return `<tr>${nameTd}${valTds}</tr>`;
};
const body = rows
.filter((r) => !r.total)
.map(rowHtml)
.join("");
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() {

View file

@ -14,6 +14,7 @@ import {
tooltipStyle,
tooltipTable,
xAxisLabelStyle,
type TooltipRow,
} from "../Forecast/echarts";
import colors, { resolveColors, deviceColorMap, darken, batteryColor, setAlpha } from "@/colors";
import store from "@/store";
@ -528,12 +529,6 @@ export default defineComponent({
if (!first) return "";
const ts = cats[first.dataIndex];
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.
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 rows = indices.map((i) => {
const t = totals.get(i) ?? { energy: 0, returnEnergy: 0 };
// one unit for all rows, based on the largest individual value (not the total)
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
? [formatValue(t.energy), formatValue(t.returnEnergy)]
: [formatValue(t.energy + t.returnEnergy)];
@ -569,6 +585,17 @@ export default defineComponent({
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);
},
},

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", () => {
test("should format with units", () => {
expect(fmt.fmtWh(0, POWER_UNIT.AUTO)).eq("0,0 kWh");

View file

@ -73,17 +73,14 @@ export default defineComponent({
const base = 10 ** 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) {
let unit = format;
let d = digits;
if (POWER_UNIT.AUTO === unit) {
if (watt >= 10_000_000) {
unit = POWER_UNIT.MW;
} else if (watt >= 1000 || 0 === watt) {
unit = POWER_UNIT.KW;
} else {
unit = POWER_UNIT.W;
}
unit = watt === 0 ? POWER_UNIT.KW : this.getPowerUnit(watt);
}
let value = watt;
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");
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(
["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("")
);
});