From 066d54260b0f34eedd538670b11431a015e07b4c Mon Sep 17 00:00:00 2001 From: Amr Mohammed El-Sheraey <141947355+AmrMsCLL@users.noreply.github.com> Date: Thu, 5 Nov 2026 13:19:00 +0200 Subject: [PATCH] fix: chart hover finds the nearest point, so the live range shows its tooltip The tooltip looked for a point exactly on the pointer's time rounded to the step. History points sit on step boundaries, but live frames carry the millisecond they were sampled, so none ever matched. It now takes the nearest point within half a step, and the newest second no longer falls off the edge. --- app/src/web/components/Chart.tsx | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/app/src/web/components/Chart.tsx b/app/src/web/components/Chart.tsx index e18efba..4b10160 100644 --- a/app/src/web/components/Chart.tsx +++ b/app/src/web/components/Chart.tsx @@ -45,9 +45,19 @@ function useWidth() { return { ref, width }; } -function Breakdown({ lines, at, metric, total, format }: { lines: Series[]; at: number; metric: Metric; total: number; format: (value: number) => string }) { +function near(points: Series['points'], at: number, step: number) { + let best: Series['points'][number] | undefined; + for (const point of points) { + if (Math.abs(point.at - at) <= step / 2 && (!best || Math.abs(point.at - at) < Math.abs(best.at - at))) { + best = point; + } + } + return best; +} + +function Breakdown({ lines, at, step, metric, total, format }: { lines: Series[]; at: number; step: number; metric: Metric; total: number; format: (value: number) => string }) { const parts = lines - .map((line) => ({ name: line.name, value: line.points.find((point) => point.at === at)?.[metric] ?? 0 })) + .map((line) => ({ name: line.name, value: near(line.points, at, step)?.[metric] ?? 0 })) .filter((part) => part.value > 0) .sort((a, b) => b.value - a.value); const rest = total - parts.reduce((sum, part) => sum + part.value, 0); @@ -94,7 +104,7 @@ export function Chart({ label, series, metric, span, step, now, format, max, bre const values = hover ? plotted - .map((line) => ({ name: line.name, point: line.points.find((point) => point.at === hover.at) })) + .map((line) => ({ name: line.name, point: near(line.points, hover.at, step) })) .filter((entry): entry is { name: string; point: Series['points'][number] } => entry.point !== undefined) .sort((a, b) => b.point[metric] - a.point[metric]) : []; @@ -105,10 +115,10 @@ export function Chart({ label, series, metric, span, step, now, format, max, bre const pointerY = event.clientY - bounds.top; const at = Math.round((start + ((pointerX - pad.left) / Math.max(1, width - pad.left - pad.right)) * span) / step) * step; const nearest = plotted - .map((line) => ({ name: line.name, point: line.points.find((point) => point.at === at) })) + .map((line) => ({ name: line.name, point: near(line.points, at, step) })) .filter((entry) => entry.point) .sort((a, b) => Math.abs(y(a.point![metric]) - pointerY) - Math.abs(y(b.point![metric]) - pointerY))[0]; - setHover(at >= start && at <= now ? { at, name: nearest?.name, x: x(at) } : undefined); + setHover(at >= start - step / 2 && at <= now + step / 2 ? { at, name: nearest?.name, x: x(at) } : undefined); }; const ticks = (width < 480 ? [0, 0.5, 1] : [0, 0.25, 0.5, 0.75, 1]).map((fraction) => start + fraction * span); @@ -185,7 +195,7 @@ export function Chart({ label, series, metric, span, step, now, format, max, bre {format(point[metric])}

))} - {breakdown && } + {breakdown && } )} {series.length > 1 && (