Fix two timeline bugs

1. /api/timeline silently defaulted missing export_rate to None → 0,
   which broke the LP for dad's terugleveringskosten scenario. With
   export=0, charging during surplus has no benefit, so the battery
   stayed half-empty while solar exported. Fix: when the param is
   absent, fall back to DEFAULTS['export_rate'] (matches the form's
   initial value). Explicit blank still means 'use saldering toggle'.

2. uPlot charts had legend disabled. Re-enabled with per-series value
   formatters so hovering shows '14:00 NL · −1.70 kWh/h' style values.
This commit is contained in:
Michiel Berger 2026-05-01 09:54:08 +02:00
parent af55fb33dd
commit e39f78fcc5
4 changed files with 39 additions and 17 deletions

View file

@ -376,32 +376,46 @@ function paintTimeline(d) {
const grid = batteryOn ? d.hours.map(h => h.grid_with_bat)
: d.hours.map(h => h.grid_no_bat);
drawChart("irr", ["Time", "W/m²"], xs, irr, "tl-chart-irradiance",
{ stroke: "#fbbf24", fill: "rgba(251, 191, 36, 0.2)" });
drawChart("soc", ["Time", "kWh"], xs, soc, "tl-chart-soc",
{ stroke: "#4ade80", fill: "rgba(74, 222, 128, 0.2)" });
drawChart("grid", ["Time", "kWh/h"], xs, grid, "tl-chart-grid",
{ stroke: batteryOn ? "#93c5fd" : "#fb7185",
fill: batteryOn ? "rgba(147, 197, 253, 0.18)" : "rgba(251, 113, 133, 0.18)",
zeroline: true });
drawChart("irr", ["Time", "W/m²"], xs, irr, "tl-chart-irradiance", {
stroke: "#fbbf24", fill: "rgba(251, 191, 36, 0.2)",
fmtY: (v) => v == null ? "" : `${v.toFixed(0)} W/m²`,
});
drawChart("soc", ["Time", "kWh"], xs, soc, "tl-chart-soc", {
stroke: "#4ade80", fill: "rgba(74, 222, 128, 0.2)",
fmtY: (v) => v == null ? "" : `${v.toFixed(2)} kWh`,
});
drawChart("grid", ["Time", "kWh/h"], xs, grid, "tl-chart-grid", {
stroke: batteryOn ? "#93c5fd" : "#fb7185",
fill: batteryOn ? "rgba(147, 197, 253, 0.18)" : "rgba(251, 113, 133, 0.18)",
zeroline: true,
fmtY: (v) => v == null ? "" : `${v >= 0 ? "+" : ""}${v.toFixed(2)} kWh/h`,
});
}
function drawChart(key, axes, xs, ys, containerId, opts = {}) {
const el = document.getElementById(containerId);
if (charts[key]) charts[key].destroy();
el.innerHTML = "";
const data = [xs, ys];
const fmtY = opts.fmtY || ((v) => v == null ? "" : v.toFixed(2));
const u = new uPlot({
width: el.clientWidth, height: el.clientHeight,
cursor: { y: false, lock: false },
legend: { show: false },
cursor: { y: false, lock: false, focus: { prox: 16 } },
legend: { show: true, live: true },
series: [
{},
{
label: "time",
// Show ts as "Mon 17 Jun, 14:00" in the legend
value: (u, t) => t == null ? "" : new Date(t * 1000).toLocaleString("nl-NL", {
weekday: "short", day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit",
}),
},
{
label: axes[1],
stroke: opts.stroke || "#93c5fd",
fill: opts.fill,
width: 1.5,
points: { show: false },
value: (u, v) => fmtY(v),
},
],
axes: [
@ -426,7 +440,7 @@ function drawChart(key, axes, xs, ys, containerId, opts = {}) {
ctx.restore();
}],
} : {},
}, data, el);
}, [xs, ys], el);
charts[key] = u;
}

View file

@ -247,8 +247,11 @@ table#leaderboard tbody tr a { color: inherit; text-decoration: underline dotted
/* uPlot dark-theme tweaks */
.uplot { color: var(--text); }
.uplot .u-legend { color: var(--text); font-size: 0.75rem; }
.uplot .u-legend { color: var(--text); font-size: 0.75rem; padding: 0.15rem 0; }
.uplot .u-legend th, .uplot .u-legend td { color: var(--text); padding: 0 0.4rem; }
.uplot .u-legend .u-marker { display: inline-block; }
.uplot .u-axis { color: var(--muted); }
.uplot .u-cursor-x, .uplot .u-cursor-y { stroke: var(--accent); }
table#leaderboard tbody tr:hover { background: var(--panel2); }
table#leaderboard tbody tr:first-child { background: rgba(74, 222, 128, 0.08); }

View file

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Honest battery payback — vs thuisbatterijgids.nl</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uplot@1.6.31/dist/uPlot.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}?v=18">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}?v=19">
</head>
<body>
@ -194,6 +194,6 @@
</div>
<script src="https://cdn.jsdelivr.net/npm/uplot@1.6.31/dist/uPlot.iife.min.js"></script>
<script src="{{ url_for('static', filename='app.js') }}?v=18"></script>
<script src="{{ url_for('static', filename='app.js') }}?v=19"></script>
</body>
</html>

View file

@ -392,7 +392,12 @@ def timeline():
saldering = q.get("saldering", "false").lower() == "true"
eta = float(q.get("eta", DEFAULTS["eta"]))
raw_export = q.get("export_rate")
if raw_export in (None, "", "null"):
if raw_export is None:
# Param absent → use the configured default (matches the form's
# initial value). 'export_rate=' (explicit empty) still means
# 'fall back to saldering toggle'.
export_rate = DEFAULTS.get("export_rate")
elif raw_export in ("", "null"):
export_rate = None
else:
export_rate = float(raw_export)