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:
parent
af55fb33dd
commit
e39f78fcc5
4 changed files with 39 additions and 17 deletions
|
|
@ -376,32 +376,46 @@ function paintTimeline(d) {
|
||||||
const grid = batteryOn ? d.hours.map(h => h.grid_with_bat)
|
const grid = batteryOn ? d.hours.map(h => h.grid_with_bat)
|
||||||
: d.hours.map(h => h.grid_no_bat);
|
: d.hours.map(h => h.grid_no_bat);
|
||||||
|
|
||||||
drawChart("irr", ["Time", "W/m²"], xs, irr, "tl-chart-irradiance",
|
drawChart("irr", ["Time", "W/m²"], xs, irr, "tl-chart-irradiance", {
|
||||||
{ stroke: "#fbbf24", fill: "rgba(251, 191, 36, 0.2)" });
|
stroke: "#fbbf24", fill: "rgba(251, 191, 36, 0.2)",
|
||||||
drawChart("soc", ["Time", "kWh"], xs, soc, "tl-chart-soc",
|
fmtY: (v) => v == null ? "" : `${v.toFixed(0)} W/m²`,
|
||||||
{ stroke: "#4ade80", fill: "rgba(74, 222, 128, 0.2)" });
|
});
|
||||||
drawChart("grid", ["Time", "kWh/h"], xs, grid, "tl-chart-grid",
|
drawChart("soc", ["Time", "kWh"], xs, soc, "tl-chart-soc", {
|
||||||
{ stroke: batteryOn ? "#93c5fd" : "#fb7185",
|
stroke: "#4ade80", fill: "rgba(74, 222, 128, 0.2)",
|
||||||
fill: batteryOn ? "rgba(147, 197, 253, 0.18)" : "rgba(251, 113, 133, 0.18)",
|
fmtY: (v) => v == null ? "" : `${v.toFixed(2)} kWh`,
|
||||||
zeroline: true });
|
});
|
||||||
|
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 = {}) {
|
function drawChart(key, axes, xs, ys, containerId, opts = {}) {
|
||||||
const el = document.getElementById(containerId);
|
const el = document.getElementById(containerId);
|
||||||
if (charts[key]) charts[key].destroy();
|
if (charts[key]) charts[key].destroy();
|
||||||
el.innerHTML = "";
|
el.innerHTML = "";
|
||||||
const data = [xs, ys];
|
const fmtY = opts.fmtY || ((v) => v == null ? "" : v.toFixed(2));
|
||||||
const u = new uPlot({
|
const u = new uPlot({
|
||||||
width: el.clientWidth, height: el.clientHeight,
|
width: el.clientWidth, height: el.clientHeight,
|
||||||
cursor: { y: false, lock: false },
|
cursor: { y: false, lock: false, focus: { prox: 16 } },
|
||||||
legend: { show: false },
|
legend: { show: true, live: true },
|
||||||
series: [
|
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",
|
stroke: opts.stroke || "#93c5fd",
|
||||||
fill: opts.fill,
|
fill: opts.fill,
|
||||||
width: 1.5,
|
width: 1.5,
|
||||||
points: { show: false },
|
points: { show: false },
|
||||||
|
value: (u, v) => fmtY(v),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
axes: [
|
axes: [
|
||||||
|
|
@ -426,7 +440,7 @@ function drawChart(key, axes, xs, ys, containerId, opts = {}) {
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}],
|
}],
|
||||||
} : {},
|
} : {},
|
||||||
}, data, el);
|
}, [xs, ys], el);
|
||||||
charts[key] = u;
|
charts[key] = u;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -247,8 +247,11 @@ table#leaderboard tbody tr a { color: inherit; text-decoration: underline dotted
|
||||||
|
|
||||||
/* uPlot dark-theme tweaks */
|
/* uPlot dark-theme tweaks */
|
||||||
.uplot { color: var(--text); }
|
.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-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:hover { background: var(--panel2); }
|
||||||
table#leaderboard tbody tr:first-child { background: rgba(74, 222, 128, 0.08); }
|
table#leaderboard tbody tr:first-child { background: rgba(74, 222, 128, 0.08); }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Honest battery payback — vs thuisbatterijgids.nl</title>
|
<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="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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
@ -194,6 +194,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/uplot@1.6.31/dist/uPlot.iife.min.js"></script>
|
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -392,7 +392,12 @@ def timeline():
|
||||||
saldering = q.get("saldering", "false").lower() == "true"
|
saldering = q.get("saldering", "false").lower() == "true"
|
||||||
eta = float(q.get("eta", DEFAULTS["eta"]))
|
eta = float(q.get("eta", DEFAULTS["eta"]))
|
||||||
raw_export = q.get("export_rate")
|
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
|
export_rate = None
|
||||||
else:
|
else:
|
||||||
export_rate = float(raw_export)
|
export_rate = float(raw_export)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue