Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 30 additions & 16 deletions PerformanceMonitor.Ui/ChartHoverHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ internal sealed class ChartHoverHelper
private readonly TextBlock _text;
private string _unit;
private DateTime _lastUpdate;
private bool _needsReanchor = true;

public ChartHoverHelper(ScottPlot.WPF.WpfPlot chart, string unit)
{
Expand Down Expand Up @@ -79,14 +80,17 @@ public void Dispose()
_barPlots.Clear();
}

private void OnChartVisibilityChanged(object sender, DependencyPropertyChangedEventArgs e) =>
_popup.IsOpen = false;

private void OnChartUnloaded(object sender, RoutedEventArgs e) =>
_popup.IsOpen = false;
private void OnChartVisibilityChanged(object sender, DependencyPropertyChangedEventArgs e) => ForceReanchor();
private void OnChartUnloaded(object sender, RoutedEventArgs e) => ForceReanchor();
private void OnChartLoaded(object sender, RoutedEventArgs e) => ForceReanchor();

private void OnChartLoaded(object sender, RoutedEventArgs e) =>
/* A tab visibility/load transition can wedge the popup open with a stale anchor; flag a
re-anchor so the next mouse move toggles it once instead of toggling on every move. */
private void ForceReanchor()
{
_popup.IsOpen = false;
_needsReanchor = true;
}

public void Clear()
{
Expand Down Expand Up @@ -151,14 +155,17 @@ private void FindNearestBar(ScottPlot.Pixel pixel, ref double bestYDistance,
{
foreach (var (barPlot, label) in _barPlots)
{
/* Bar width in pixels is the same for every bar on a linear axis, so compute it once
per plot (lazily on the first bar) instead of two GetPixel calls per bar. */
double? halfWidthPx = null;
foreach (var bar in barPlot.Bars)
{
halfWidthPx ??= Math.Abs(
_chart.Plot.GetPixel(new ScottPlot.Coordinates(bar.Size / 2, 0)).X
- _chart.Plot.GetPixel(new ScottPlot.Coordinates(0, 0)).X);
var topPixel = _chart.Plot.GetPixel(new ScottPlot.Coordinates(bar.Position, bar.Value));
double halfWidthPx = Math.Abs(
_chart.Plot.GetPixel(new ScottPlot.Coordinates(bar.Position + bar.Size / 2, bar.Value)).X
- topPixel.X);
double dx = Math.Abs(topPixel.X - pixel.X);
if (dx > halfWidthPx + 4) continue;
if (dx > halfWidthPx.Value + 4) continue;
double dy = Math.Abs(topPixel.Y - pixel.Y);
if (dy < bestYDistance)
{
Expand Down Expand Up @@ -229,12 +236,19 @@ pick the series closest in Y (nearest line to cursor). */
_text.Text = $"{bestLabel}\n{valueFormatted} {_unit}\n{time:HH:mm:ss}";
_popup.HorizontalOffset = pos.X + 15;
_popup.VerticalOffset = pos.Y + 15;
/* Toggle if already open so WPF re-evaluates the placement target.
Without this, a popup that was IsOpen = true when its TabItem was
unloaded stays "open" with a stale anchor and never appears on
return — the assignment below is a no-op. */
if (_popup.IsOpen) _popup.IsOpen = false;
_popup.IsOpen = true;
/* Updating the offsets above moves an already-open popup, so only toggle IsOpen
when a re-anchor is actually needed (a tab visibility/load transition wedged it).
Toggling every move tore down and recreated the popup's native window each frame. */
if (_needsReanchor)
{
if (_popup.IsOpen) _popup.IsOpen = false;
_popup.IsOpen = true;
_needsReanchor = false;
}
else if (!_popup.IsOpen)
{
_popup.IsOpen = true;
}
}
else
{
Expand Down
83 changes: 57 additions & 26 deletions PerformanceMonitor.Ui/CorrelatedCrosshairManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ internal sealed class CorrelatedCrosshairManager : IDisposable
private readonly Popup _tooltip;
private readonly TextBlock _tooltipText;
private DateTime _lastUpdate;
private double _lastPixelX = double.NaN;
private bool _needsReanchor = true;

public CorrelatedCrosshairManager()
{
Expand Down Expand Up @@ -89,14 +91,17 @@ public void AddLane(ScottPlot.WPF.WpfPlot chart, string label, string unit)
_lanes.Add(lane);
}

private void OnLaneVisibilityChanged(object sender, DependencyPropertyChangedEventArgs e) =>
_tooltip.IsOpen = false;

private void OnLaneUnloaded(object sender, RoutedEventArgs e) =>
_tooltip.IsOpen = false;
private void OnLaneVisibilityChanged(object sender, DependencyPropertyChangedEventArgs e) => ForceReanchor();
private void OnLaneUnloaded(object sender, RoutedEventArgs e) => ForceReanchor();
private void OnLaneLoaded(object sender, RoutedEventArgs e) => ForceReanchor();

private void OnLaneLoaded(object sender, RoutedEventArgs e) =>
/* A tab visibility/load transition can leave the popup wedged open with a stale anchor; flag a
re-anchor so the next mouse move toggles it once, instead of toggling on every move. */
private void ForceReanchor()
{
_tooltip.IsOpen = false;
_needsReanchor = true;
}

/// <summary>
/// Sets the expected baseline range for a lane (upper/lower bounds).
Expand Down Expand Up @@ -233,10 +238,15 @@ private void OnMouseMove(LaneInfo sourceLane, MouseEventArgs e)
if (sourceLane.VLine == null) return;

var now = DateTime.UtcNow;
if ((now - _lastUpdate).TotalMilliseconds < 16) return;
_lastUpdate = now;
if ((now - _lastUpdate).TotalMilliseconds < 33) return;

var pos = e.GetPosition(sourceLane.Chart);
/* The crosshair only tracks X; skip when the cursor hasn't moved a full pixel
horizontally so a near-still mouse doesn't re-render every lane ~30x/sec. */
if (!double.IsNaN(_lastPixelX) && Math.Abs(pos.X - _lastPixelX) < 1.0) return;
_lastUpdate = now;
_lastPixelX = pos.X;

var dpi = VisualTreeHelper.GetDpi(sourceLane.Chart);
var pixel = new ScottPlot.Pixel(
(float)(pos.X * dpi.DpiScaleX),
Expand All @@ -258,11 +268,10 @@ crosshair update rather than crashing to the dispatcher handler. */
if (_comparisonLabel != null)
_tooltipText.Inlines.Add(new Run($" (dashed = {_comparisonLabel})") { Foreground = DimBrush });

var defaultBrush = new SolidColorBrush(Color.FromRgb(0xE0, 0xE0, 0xE0));

foreach (var lane in _lanes)
{
if (lane.VLine == null) continue;
if (!lane.Chart.IsVisible) continue;

lane.VLine.IsVisible = true;
lane.VLine.X = xValue;
Expand All @@ -277,15 +286,15 @@ crosshair update rather than crashing to the dispatcher handler. */
var indicator = GetBaselineIndicator(lane, value.Value);

// Tooltip: value + arrow + "30d avg" context
_tooltipText.Inlines.Add(new Run($"\n{lane.Label}: {value.Value:N1} {lane.Unit}") { Foreground = defaultBrush });
_tooltipText.Inlines.Add(new Run($"\n{lane.Label}: {value.Value:N1} {lane.Unit}") { Foreground = DefaultBrush });
if (indicator != null)
{
_tooltipText.Inlines.Add(new Run($" {indicator.Value.Symbol}") { Foreground = indicator.Value.Brush });
}
}
else
{
_tooltipText.Inlines.Add(new Run($"\n{lane.Label}: —") { Foreground = defaultBrush });
_tooltipText.Inlines.Add(new Run($"\n{lane.Label}: —") { Foreground = DefaultBrush });
}
}
else if (lane.Series.Count > 1)
Expand All @@ -296,31 +305,38 @@ crosshair update rather than crashing to the dispatcher handler. */
string unit = series.Unit ?? lane.Unit;
if (value.HasValue)
{
_tooltipText.Inlines.Add(new Run($"\n{series.Name}: {value.Value:N0} {unit}") { Foreground = defaultBrush });
_tooltipText.Inlines.Add(new Run($"\n{series.Name}: {value.Value:N0} {unit}") { Foreground = DefaultBrush });
var indicator = GetBaselineIndicator(lane, value.Value);
if (indicator != null)
_tooltipText.Inlines.Add(new Run($" {indicator.Value.Symbol}") { Foreground = indicator.Value.Brush });
}
else
_tooltipText.Inlines.Add(new Run($"\n{series.Name}: —") { Foreground = defaultBrush });
_tooltipText.Inlines.Add(new Run($"\n{series.Name}: —") { Foreground = DefaultBrush });
}
}
else
{
_tooltipText.Inlines.Add(new Run($"\n{lane.Label}: —") { Foreground = defaultBrush });
_tooltipText.Inlines.Add(new Run($"\n{lane.Label}: —") { Foreground = DefaultBrush });
}

lane.Chart.Refresh();
}
_tooltip.PlacementTarget = sourceLane.Chart;
_tooltip.HorizontalOffset = pos.X + 15;
_tooltip.VerticalOffset = pos.Y + 15;
/* Toggle if already open so WPF re-evaluates the placement target.
Without this, a popup that was IsOpen = true when its TabItem was
unloaded stays "open" with a stale anchor and never appears on
return — the assignment below is a no-op. */
if (_tooltip.IsOpen) _tooltip.IsOpen = false;
_tooltip.IsOpen = true;
/* Updating the offsets above moves an already-open popup, so only toggle IsOpen when a
re-anchor is actually needed (a tab visibility/load transition wedged it with a stale
anchor). Toggling every move tore down and recreated the popup's native window each frame. */
if (_needsReanchor)
{
if (_tooltip.IsOpen) _tooltip.IsOpen = false;
_tooltip.IsOpen = true;
_needsReanchor = false;
}
else if (!_tooltip.IsOpen)
{
_tooltip.IsOpen = true;
}
}

private static double? FindNearestValue(DataSeries series, double targetX)
Expand Down Expand Up @@ -358,9 +374,19 @@ unloaded stays "open" with a stale anchor and never appears on
return val;
}

private static readonly SolidColorBrush RedBrush = new(Color.FromRgb(0xFF, 0x52, 0x52));
private static readonly SolidColorBrush GreenBrush = new(Color.FromRgb(0x69, 0xF0, 0x69));
private static readonly SolidColorBrush DimBrush = new(Color.FromRgb(0x90, 0x96, 0xA0));
private static readonly SolidColorBrush RedBrush = Frozen(0xFF, 0x52, 0x52);
private static readonly SolidColorBrush GreenBrush = Frozen(0x69, 0xF0, 0x69);
private static readonly SolidColorBrush DimBrush = Frozen(0x90, 0x96, 0xA0);
/* Reused on every mouse-move; cached + frozen so each move doesn't allocate a brush and they
carry no dispatcher-affinity / change-notification overhead. */
private static readonly SolidColorBrush DefaultBrush = Frozen(0xE0, 0xE0, 0xE0);

private static SolidColorBrush Frozen(byte r, byte g, byte b)
{
var brush = new SolidColorBrush(Color.FromRgb(r, g, b));
brush.Freeze();
return brush;
}

private record struct BaselineIndicator(string Symbol, SolidColorBrush Brush);

Expand Down Expand Up @@ -393,11 +419,16 @@ private record struct BaselineIndicator(string Symbol, SolidColorBrush Brush);
private void OnMouseLeave()
{
_tooltip.IsOpen = false;
_lastPixelX = double.NaN;
foreach (var lane in _lanes)
{
if (lane.VLine != null)
/* Only refresh lanes whose VLine was actually showing — crossing the stacked lanes
fires MouseLeave per boundary, so refreshing already-hidden lanes is wasted work. */
if (lane.VLine != null && lane.VLine.IsVisible)
{
lane.VLine.IsVisible = false;
lane.Chart.Refresh();
lane.Chart.Refresh();
}
}
}

Expand Down
Loading