From a47bfe6ab3b4e00e4e0022b74aaf88dec67c7384 Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Thu, 19 Feb 2026 18:22:54 -0500 Subject: [PATCH] Fix unreliable chart tooltips - use X-axis proximity (#167) Tooltips used Euclidean pixel distance requiring the mouse to be within 50px of a data point marker. With sparse time-series data (points every 15 mins), large gaps between markers made tooltips unreliable or absent. Fix: use X-axis (time) proximity as primary filter (80px horizontal), Y-axis distance as tiebreaker for multi-series charts. Tooltips now appear reliably when hovering at any Y position near a data point's time. Added try-catch for robustness, reduced throttle to 30ms. Co-Authored-By: Claude Opus 4.6 --- Dashboard/Helpers/ChartHoverHelper.cs | 68 ++++++++++++++++----------- 1 file changed, 41 insertions(+), 27 deletions(-) diff --git a/Dashboard/Helpers/ChartHoverHelper.cs b/Dashboard/Helpers/ChartHoverHelper.cs index 048e0c37..dd0e7122 100644 --- a/Dashboard/Helpers/ChartHoverHelper.cs +++ b/Dashboard/Helpers/ChartHoverHelper.cs @@ -11,6 +11,7 @@ namespace PerformanceMonitorDashboard.Helpers; /// /// Adds mouse-hover tooltips to a ScottPlot chart with multiple scatter series. /// Shows the series name, value, and timestamp in a popup that follows the mouse. +/// Uses X-axis (time) proximity for reliable detection on time-series charts. /// internal sealed class ChartHoverHelper { @@ -64,47 +65,60 @@ private void OnMouseMove(object sender, MouseEventArgs e) { if (_scatters.Count == 0) return; var now = DateTime.UtcNow; - if ((now - _lastUpdate).TotalMilliseconds < 50) return; + if ((now - _lastUpdate).TotalMilliseconds < 30) return; _lastUpdate = now; - var pos = e.GetPosition(_chart); - var pixel = new ScottPlot.Pixel( - (float)(pos.X * _chart.DisplayScale), - (float)(pos.Y * _chart.DisplayScale)); - var mouseCoords = _chart.Plot.GetCoordinates(pixel); + try + { + var pos = e.GetPosition(_chart); + var pixel = new ScottPlot.Pixel( + (float)(pos.X * _chart.DisplayScale), + (float)(pos.Y * _chart.DisplayScale)); + var mouseCoords = _chart.Plot.GetCoordinates(pixel); - double bestDistance = double.MaxValue; - ScottPlot.DataPoint bestPoint = default; - string bestLabel = ""; + /* Use X-axis (time) proximity as the primary filter, Y-axis distance + as tiebreaker. This makes tooltips appear reliably when hovering at + any Y position near a data point's time — standard for time-series. */ + double bestYDistance = double.MaxValue; + ScottPlot.DataPoint bestPoint = default; + string bestLabel = ""; + bool found = false; - foreach (var (scatter, label) in _scatters) - { - var nearest = scatter.Data.GetNearest(mouseCoords, _chart.Plot.LastRender); - if (nearest.IsReal) + foreach (var (scatter, label) in _scatters) { + var nearest = scatter.Data.GetNearest(mouseCoords, _chart.Plot.LastRender); + if (!nearest.IsReal) continue; + var nearestPixel = _chart.Plot.GetPixel( new ScottPlot.Coordinates(nearest.X, nearest.Y)); - double dx = nearestPixel.X - pixel.X; - double dy = nearestPixel.Y - pixel.Y; - double dist = dx * dx + dy * dy; - if (dist < bestDistance) + double dx = Math.Abs(nearestPixel.X - pixel.X); + double dy = Math.Abs(nearestPixel.Y - pixel.Y); + + /* Must be within 80px horizontally (time axis). Among matches, + pick the series closest in Y (nearest line to cursor). */ + if (dx < 80 && dy < bestYDistance) { - bestDistance = dist; + bestYDistance = dy; bestPoint = nearest; bestLabel = label; + found = true; } } - } - if (bestPoint.IsReal && bestDistance < 2500) // ~50px radius - { - var time = DateTime.FromOADate(bestPoint.X); - _text.Text = $"{bestLabel}\n{bestPoint.Y:N1} {_unit}\n{time:HH:mm:ss}"; - _popup.HorizontalOffset = pos.X + 15; - _popup.VerticalOffset = pos.Y + 15; - _popup.IsOpen = true; + if (found) + { + var time = DateTime.FromOADate(bestPoint.X); + _text.Text = $"{bestLabel}\n{bestPoint.Y:N1} {_unit}\n{time:HH:mm:ss}"; + _popup.HorizontalOffset = pos.X + 15; + _popup.VerticalOffset = pos.Y + 15; + _popup.IsOpen = true; + } + else + { + _popup.IsOpen = false; + } } - else + catch { _popup.IsOpen = false; }