Web: sync graph hover by timestamp instead of relative position

When hovering over a sparkline, the same X fraction was applied to
all other canvases to compute the highlighted point index.  Because
different series can have different numbers of valid samples (some
values are filtered out), the same fraction mapped to different
indices and therefore different timestamps on each graph.

Fix by reading the uptime_secs timestamp (point[0]) of the hovered
point on the source canvas and finding the closest-timestamp point
in each other series, instead of using the fractional position.

Also de-duplicate the near-identical onmousemove/ontouchstart and
onmouseleave/ontouchend handlers into syncHoverByX() and
syncHoverClear() helpers, reducing the block from 34 lines to 16.
This commit is contained in:
Valentin V. Bartenev
2026-05-16 16:28:04 +03:00
parent 7f17c3aeab
commit d5d4311e0e
+17 -29
Vedi File
@@ -1998,40 +1998,28 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
}; };
canvas._updateHover = updateHover; canvas._updateHover = updateHover;
canvas._points = points; canvas._points = points;
canvas.onmousemove = (event) => { function syncHoverByX(srcCanvas, clientX) {
const rect = canvas.getBoundingClientRect(); const rect = srcCanvas.getBoundingClientRect();
const width = rect.width || 1; const x = Math.max(0, Math.min(rect.width || 1, clientX - rect.left));
const x = Math.max(0, Math.min(width, event.clientX - rect.left)); const srcIdx = Math.max(0, Math.min(points.length - 1, Math.round(x / (rect.width || 1) * (points.length - 1))));
const fraction = x / width; const hoveredTs = points[srcIdx][0];
document.querySelectorAll('#statsTrends canvas').forEach(c => { document.querySelectorAll('#statsTrends canvas').forEach(c => {
if (!c._updateHover || !c._points) return; if (!c._updateHover || !c._points) return;
const idx = Math.max(0, Math.min(c._points.length - 1, Math.round(fraction * (c._points.length - 1)))); if (c === srcCanvas) { c._updateHover(srcIdx, true); return; }
c._updateHover(idx, c === canvas); let bestIdx = 0, bestDiff = Infinity;
c._points.forEach((pt, i) => { const d = Math.abs(pt[0] - hoveredTs); if (d < bestDiff) { bestDiff = d; bestIdx = i; } });
c._updateHover(bestIdx, false);
}); });
}; }
canvas.onmouseleave = () => { function syncHoverClear(srcCanvas) {
document.querySelectorAll('#statsTrends canvas').forEach(c => { document.querySelectorAll('#statsTrends canvas').forEach(c => {
if (c._updateHover) c._updateHover(null, c === canvas); if (c._updateHover) c._updateHover(null, c === srcCanvas);
}); });
}; }
canvas.ontouchstart = (event) => { canvas.onmousemove = (e) => syncHoverByX(canvas, e.clientX);
const touch = event.touches && event.touches[0]; canvas.onmouseleave = () => syncHoverClear(canvas);
if (!touch) return; canvas.ontouchstart = (e) => { const t = e.touches && e.touches[0]; if (t) syncHoverByX(canvas, t.clientX); };
const rect = canvas.getBoundingClientRect(); canvas.ontouchend = () => syncHoverClear(canvas);
const width = rect.width || 1;
const x = Math.max(0, Math.min(width, touch.clientX - rect.left));
const fraction = x / width;
document.querySelectorAll('#statsTrends canvas').forEach(c => {
if (!c._updateHover || !c._points) return;
const idx = Math.max(0, Math.min(c._points.length - 1, Math.round(fraction * (c._points.length - 1))));
c._updateHover(idx, c === canvas);
});
};
canvas.ontouchend = () => {
document.querySelectorAll('#statsTrends canvas').forEach(c => {
if (c._updateHover) c._updateHover(null, c === canvas);
});
};
} }
function setTrendCardState(key, title, value) { function setTrendCardState(key, title, value) {
const card = document.getElementById("trend-" + key); const card = document.getElementById("trend-" + key);