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
View File
@@ -1998,40 +1998,28 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
};
canvas._updateHover = updateHover;
canvas._points = points;
canvas.onmousemove = (event) => {
const rect = canvas.getBoundingClientRect();
const width = rect.width || 1;
const x = Math.max(0, Math.min(width, event.clientX - rect.left));
const fraction = x / width;
function syncHoverByX(srcCanvas, clientX) {
const rect = srcCanvas.getBoundingClientRect();
const x = Math.max(0, Math.min(rect.width || 1, clientX - rect.left));
const srcIdx = Math.max(0, Math.min(points.length - 1, Math.round(x / (rect.width || 1) * (points.length - 1))));
const hoveredTs = points[srcIdx][0];
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);
if (c === srcCanvas) { c._updateHover(srcIdx, true); return; }
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 => {
if (c._updateHover) c._updateHover(null, c === canvas);
if (c._updateHover) c._updateHover(null, c === srcCanvas);
});
};
canvas.ontouchstart = (event) => {
const touch = event.touches && event.touches[0];
if (!touch) return;
const rect = canvas.getBoundingClientRect();
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);
});
};
}
canvas.onmousemove = (e) => syncHoverByX(canvas, e.clientX);
canvas.onmouseleave = () => syncHoverClear(canvas);
canvas.ontouchstart = (e) => { const t = e.touches && e.touches[0]; if (t) syncHoverByX(canvas, t.clientX); };
canvas.ontouchend = () => syncHoverClear(canvas);
}
function setTrendCardState(key, title, value) {
const card = document.getElementById("trend-" + key);