From faf065cba1add4dcefd1d8098ecd68db5e4efff9 Mon Sep 17 00:00:00 2001
From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com>
Date: Thu, 28 May 2026 00:05:24 +0200
Subject: [PATCH 1/4] Feature: Migrate ping to live charts + add tooltip to
speedtest
---
.../LvlChartsDefaultInfo.cs | 12 +-
.../Controls/LiveChartsPingTimeTooltip.xaml | 46 ++++++
.../LiveChartsPingTimeTooltip.xaml.cs | 94 ++++++++++++
.../Controls/LiveChartsSpeedTestTooltip.xaml | 38 +++++
.../LiveChartsSpeedTestTooltip.xaml.cs | 67 +++++++++
.../Controls/LvlChartsPingTimeTooltip.xaml | 48 ------
.../Controls/LvlChartsPingTimeTooltip.xaml.cs | 35 -----
.../ViewModels/PingMonitorViewModel.cs | 141 +++++++++++++-----
.../ViewModels/SpeedTestWidgetViewModel.cs | 16 +-
.../NETworkManager/Views/PingMonitorView.xaml | 37 ++---
.../Views/SpeedTestWidgetView.xaml | 50 +++++--
11 files changed, 406 insertions(+), 178 deletions(-)
create mode 100644 Source/NETworkManager/Controls/LiveChartsPingTimeTooltip.xaml
create mode 100644 Source/NETworkManager/Controls/LiveChartsPingTimeTooltip.xaml.cs
create mode 100644 Source/NETworkManager/Controls/LiveChartsSpeedTestTooltip.xaml
create mode 100644 Source/NETworkManager/Controls/LiveChartsSpeedTestTooltip.xaml.cs
delete mode 100644 Source/NETworkManager/Controls/LvlChartsPingTimeTooltip.xaml
delete mode 100644 Source/NETworkManager/Controls/LvlChartsPingTimeTooltip.xaml.cs
diff --git a/Source/NETworkManager.Utilities/LvlChartsDefaultInfo.cs b/Source/NETworkManager.Utilities/LvlChartsDefaultInfo.cs
index 2761265120..974029e9ee 100644
--- a/Source/NETworkManager.Utilities/LvlChartsDefaultInfo.cs
+++ b/Source/NETworkManager.Utilities/LvlChartsDefaultInfo.cs
@@ -2,14 +2,8 @@
namespace NETworkManager.Utilities;
-public class LvlChartsDefaultInfo
+public class LvlChartsDefaultInfo(DateTime dateTime, double value)
{
- public LvlChartsDefaultInfo(DateTime dateTime, double value)
- {
- DateTime = dateTime;
- Value = value;
- }
-
- public DateTime DateTime { get; }
- public double Value { get; set; }
+ public DateTime DateTime { get; } = dateTime;
+ public double Value { get; } = value;
}
\ No newline at end of file
diff --git a/Source/NETworkManager/Controls/LiveChartsPingTimeTooltip.xaml b/Source/NETworkManager/Controls/LiveChartsPingTimeTooltip.xaml
new file mode 100644
index 0000000000..bfe13c1003
--- /dev/null
+++ b/Source/NETworkManager/Controls/LiveChartsPingTimeTooltip.xaml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/NETworkManager/Controls/LiveChartsPingTimeTooltip.xaml.cs b/Source/NETworkManager/Controls/LiveChartsPingTimeTooltip.xaml.cs
new file mode 100644
index 0000000000..8a802b9e48
--- /dev/null
+++ b/Source/NETworkManager/Controls/LiveChartsPingTimeTooltip.xaml.cs
@@ -0,0 +1,94 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Windows;
+using System.Windows.Controls.Primitives;
+using System.Windows.Media;
+using LiveChartsCore;
+using LiveChartsCore.Kernel;
+using LiveChartsCore.Kernel.Sketches;
+using LiveChartsCore.SkiaSharpView;
+using LiveChartsCore.SkiaSharpView.Painting;
+using NETworkManager.Utilities;
+using SkiaSharp;
+
+namespace NETworkManager.Controls;
+
+public partial class LiveChartsPingTimeTooltip : IChartTooltip, INotifyPropertyChanged
+{
+ private readonly Popup _popup;
+
+ public LiveChartsPingTimeTooltip()
+ {
+ InitializeComponent();
+ DataContext = this;
+ _popup = new Popup
+ {
+ AllowsTransparency = true,
+ Placement = PlacementMode.MousePoint,
+ StaysOpen = true,
+ Child = this
+ };
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public string HeaderText
+ {
+ get;
+ private set
+ {
+ if (field == value) return;
+ field = value;
+ OnPropertyChanged();
+ }
+ }
+
+ public ObservableCollection TooltipEntries { get; } = [];
+
+ public void Show(IEnumerable tooltipPoints, Chart chart)
+ {
+ var points = tooltipPoints.ToList();
+ if (points.Count == 0) return;
+
+ if (points[0].Context.DataSource is LvlChartsDefaultInfo firstInfo)
+ HeaderText = DateTimeHelper.DateTimeToTimeString(firstInfo.DateTime);
+
+ TooltipEntries.Clear();
+ foreach (var point in points)
+ {
+ var value = point.Context.DataSource is LvlChartsDefaultInfo info
+ ? $"{info.Value} ms"
+ : "-/-";
+ TooltipEntries.Add(new TooltipEntry(
+ SkColorToBrush(GetSeriesColor(point)),
+ value,
+ point.Context.Series.Name ?? string.Empty));
+ }
+
+ _popup.PlacementTarget = chart.View as FrameworkElement;
+ _popup.IsOpen = true;
+ }
+
+ public void Hide(Chart chart)
+ {
+ _popup.IsOpen = false;
+ }
+
+ protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+
+ private static SKColor GetSeriesColor(ChartPoint point)
+ {
+ if (point.Context.Series is LineSeries ls && ls.Stroke is SolidColorPaint paint)
+ return paint.Color;
+ return SKColors.Gray;
+ }
+
+ private static Brush SkColorToBrush(SKColor color)
+ => new SolidColorBrush(Color.FromArgb(color.Alpha, color.Red, color.Green, color.Blue));
+
+ public record TooltipEntry(Brush SeriesColor, string Value, string SeriesName);
+}
diff --git a/Source/NETworkManager/Controls/LiveChartsSpeedTestTooltip.xaml b/Source/NETworkManager/Controls/LiveChartsSpeedTestTooltip.xaml
new file mode 100644
index 0000000000..2b7151b0a6
--- /dev/null
+++ b/Source/NETworkManager/Controls/LiveChartsSpeedTestTooltip.xaml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/NETworkManager/Controls/LiveChartsSpeedTestTooltip.xaml.cs b/Source/NETworkManager/Controls/LiveChartsSpeedTestTooltip.xaml.cs
new file mode 100644
index 0000000000..46c5274518
--- /dev/null
+++ b/Source/NETworkManager/Controls/LiveChartsSpeedTestTooltip.xaml.cs
@@ -0,0 +1,67 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls.Primitives;
+using System.Windows.Media;
+using LiveChartsCore;
+using LiveChartsCore.Kernel;
+using LiveChartsCore.Kernel.Sketches;
+using LiveChartsCore.SkiaSharpView;
+using LiveChartsCore.SkiaSharpView.Painting;
+using SkiaSharp;
+
+namespace NETworkManager.Controls;
+
+public partial class LiveChartsSpeedTestTooltip : IChartTooltip
+{
+ private readonly Popup _popup;
+
+ public LiveChartsSpeedTestTooltip()
+ {
+ InitializeComponent();
+ DataContext = this;
+ _popup = new Popup
+ {
+ AllowsTransparency = true,
+ Placement = PlacementMode.MousePoint,
+ StaysOpen = true,
+ Child = this
+ };
+ }
+
+ public ObservableCollection TooltipEntries { get; } = [];
+
+ public void Show(IEnumerable tooltipPoints, Chart chart)
+ {
+ var points = tooltipPoints.ToList();
+ if (points.Count == 0) return;
+
+ TooltipEntries.Clear();
+ foreach (var point in points)
+ {
+ var value = point.Context.DataSource is double mbps ? $"{mbps:F1} Mbps" : "-/-";
+ TooltipEntries.Add(new TooltipEntry(
+ SkColorToBrush(GetSeriesColor(point)),
+ value,
+ point.Context.Series.Name ?? string.Empty));
+ }
+
+ _popup.PlacementTarget = chart.View as FrameworkElement;
+ _popup.IsOpen = true;
+ }
+
+ public void Hide(Chart chart)
+ {
+ _popup.IsOpen = false;
+ }
+
+ private static SKColor GetSeriesColor(ChartPoint point)
+ {
+ return point.Context.Series is LineSeries { Stroke: SolidColorPaint paint } ? paint.Color : SKColors.Gray;
+ }
+
+ private static Brush SkColorToBrush(SKColor color) => new SolidColorBrush(Color.FromArgb(color.Alpha, color.Red, color.Green, color.Blue));
+
+ public record TooltipEntry(Brush SeriesColor, string Value, string SeriesName);
+}
diff --git a/Source/NETworkManager/Controls/LvlChartsPingTimeTooltip.xaml b/Source/NETworkManager/Controls/LvlChartsPingTimeTooltip.xaml
deleted file mode 100644
index 99c1057773..0000000000
--- a/Source/NETworkManager/Controls/LvlChartsPingTimeTooltip.xaml
+++ /dev/null
@@ -1,48 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Source/NETworkManager/Controls/LvlChartsPingTimeTooltip.xaml.cs b/Source/NETworkManager/Controls/LvlChartsPingTimeTooltip.xaml.cs
deleted file mode 100644
index a35c43f0b8..0000000000
--- a/Source/NETworkManager/Controls/LvlChartsPingTimeTooltip.xaml.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using System.ComponentModel;
-using System.Runtime.CompilerServices;
-using LiveCharts;
-using LiveCharts.Wpf;
-
-namespace NETworkManager.Controls;
-
-public partial class LvlChartsPingTimeTooltip : IChartTooltip
-{
- public LvlChartsPingTimeTooltip()
- {
- InitializeComponent();
-
- DataContext = this;
- }
-
- public event PropertyChangedEventHandler PropertyChanged;
-
- public TooltipData Data
- {
- get;
- set
- {
- field = value;
- OnPropertyChanged();
- }
- }
-
- public TooltipSelectionMode? SelectionMode { get; set; }
-
- protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
- {
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
- }
-}
\ No newline at end of file
diff --git a/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs b/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs
index bbe3c3bdb8..a3318e8e59 100644
--- a/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs
+++ b/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs
@@ -1,6 +1,10 @@
-using LiveCharts;
-using LiveCharts.Configurations;
-using LiveCharts.Wpf;
+using LiveChartsCore;
+using LiveChartsCore.Drawing;
+using LiveChartsCore.Kernel;
+using LiveChartsCore.Kernel.Sketches;
+using LiveChartsCore.SkiaSharpView;
+using LiveChartsCore.SkiaSharpView.Painting;
+using LiveChartsCore.SkiaSharpView.Painting.Effects;
using log4net;
using MahApps.Metro.SimpleChildWindow;
using NETworkManager.Localization.Resources;
@@ -9,9 +13,11 @@
using NETworkManager.Settings;
using NETworkManager.Utilities;
using NETworkManager.Views;
+using SkiaSharp;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Threading;
@@ -60,6 +66,7 @@ public PingMonitorViewModel(Guid hostId, Action removeHostByGuid,
private static readonly ILog Log = LogManager.GetLogger(typeof(PingMonitorViewModel));
private CancellationTokenSource _cancellationTokenSource;
+ private int _maxPingValues;
public readonly Guid HostId;
private readonly Action _removeHostByGuid;
@@ -258,44 +265,86 @@ public long TimeMs
}
}
+ private ObservableCollection _pingValues;
+
///
/// Initializes the time chart configuration.
///
private void InitialTimeChart()
{
- var dayConfig = Mappers.Xy()
- .X(dayModel => (double)dayModel.DateTime.Ticks / TimeSpan.FromHours(1).Ticks)
- .Y(dayModel => dayModel.Value);
+ _pingValues = [];
- Series = new SeriesCollection(dayConfig)
- {
- new LineSeries
+ var chartColor = SKColor.Parse("#1ba1e2");
+
+ var labelColor = Application.Current.Resources["MahApps.Brushes.Gray5"] is System.Windows.Media.SolidColorBrush gray5
+ ? new SKColor(gray5.Color.R, gray5.Color.G, gray5.Color.B, gray5.Color.A)
+ : new SKColor(0x68, 0x68, 0x68);
+
+ var separatorColor = Application.Current.Resources["MahApps.Brushes.Gray8"] is System.Windows.Media.SolidColorBrush gray8
+ ? new SKColor(gray8.Color.R, gray8.Color.G, gray8.Color.B, gray8.Color.A)
+ : new SKColor(0x80, 0x80, 0x80);
+
+ Series =
+ [
+ new LineSeries
{
- Title = "Time",
- Values = new ChartValues(),
- PointGeometry = null
+ Name = Strings.Time,
+ Values = _pingValues,
+ Mapping = (info, _) => double.IsNaN(info.Value)
+ ? Coordinate.Empty
+ : new(info.DateTime.Ticks / (double)TimeSpan.FromHours(1).Ticks, info.Value),
+ GeometrySize = 0,
+ LineSmoothness = 0.3,
+ DataPadding = new LvcPoint(0, 0),
+ Stroke = new SolidColorPaint(chartColor) { StrokeThickness = 1.5f },
+ Fill = new SolidColorPaint(chartColor.WithAlpha(0x33))
}
- };
+ ];
- FormatterDate = value =>
- DateTimeHelper.DateTimeToTimeString(new DateTime((long)(value * TimeSpan.FromHours(1).Ticks)));
- FormatterPingTime = value => $"{value} ms";
+ PingXAxes =
+ [
+ new Axis
+ {
+ LabelsPaint = null,
+ SeparatorsPaint = null,
+ Padding = new Padding(0)
+ }
+ ];
+
+ PingYAxes =
+ [
+ new Axis
+ {
+ MinLimit = 0,
+ MinStep = 25,
+ ForceStepToMin = true,
+ Labeler = value => $"{value} ms",
+ TextSize = 11,
+ Padding = new Padding(4, 0),
+ LabelsPaint = new SolidColorPaint(labelColor),
+ SeparatorsPaint = new SolidColorPaint(separatorColor)
+ {
+ StrokeThickness = 1,
+ PathEffect = new DashEffect([10f, 10f])
+ }
+ }
+ ];
}
///
- /// Gets or sets the formatter for the date axis.
+ /// Gets or sets the series collection for the chart.
///
- public Func FormatterDate { get; set; }
+ public ISeries[] Series { get; private set; }
///
- /// Gets or sets the formatter for the ping time axis.
+ /// Gets the X-axes configuration for the ping time chart.
///
- public Func FormatterPingTime { get; set; }
+ public ICartesianAxis[] PingXAxes { get; private set; }
///
- /// Gets or sets the series collection for the chart.
+ /// Gets the Y-axes configuration for the ping time chart.
///
- public SeriesCollection Series { get; set; }
+ public ICartesianAxis[] PingYAxes { get; private set; }
///
/// Gets the error message if an error occurs.
@@ -403,6 +452,9 @@ public void Start()
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = new CancellationTokenSource();
+ // How many data points fit into the 60-second window at the configured interval.
+ _maxPingValues = (int)Math.Ceiling(60_000.0 / SettingsManager.Current.PingMonitor_WaitTime);
+
var ping = new Ping
{
Timeout = SettingsManager.Current.PingMonitor_Timeout,
@@ -434,21 +486,22 @@ public void Stop()
///
/// Resets the time chart with empty values.
///
+ private static readonly double _hourTicks = (double)TimeSpan.FromHours(1).Ticks;
+
private void ResetTimeChart()
{
- if (Series == null)
+ if (_pingValues == null)
return;
- Series[0].Values.Clear();
-
- var currentDateTime = DateTime.Now;
-
- for (var i = 30; i > 0; i--)
- {
- var bandwidthInfo = new LvlChartsDefaultInfo(currentDateTime.AddSeconds(-i), double.NaN);
+ _pingValues.Clear();
+ UpdateXAxisWindow(DateTime.Now);
+ }
- Series[0].Values.Add(bandwidthInfo);
- }
+ private void UpdateXAxisWindow(DateTime now)
+ {
+ var axis = (Axis)PingXAxes[0];
+ axis.MinLimit = now.AddSeconds(-60).Ticks / _hourTicks;
+ axis.MaxLimit = now.Ticks / _hourTicks;
}
///
@@ -543,14 +596,26 @@ private void Ping_PingReceived(object sender, PingReceivedArgs e)
PacketLoss = Math.Round((double)Lost / Transmitted * 100, 2);
TimeMs = e.Args.Time;
- // Null exception may occur when the application is closing
- Application.Current?.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate
+ // Null exception may occur when the application is closing
+ Application.Current?.Dispatcher.BeginInvoke(DispatcherPriority.Normal, () =>
{
- Series[0].Values.Add(timeInfo);
+ _pingValues.Add(timeInfo);
+ if (_pingValues.Count > _maxPingValues)
+ _pingValues.RemoveAt(0);
- if (Series[0].Values.Count > 59)
- Series[0].Values.RemoveAt(0);
- }));
+ UpdateXAxisWindow(timeInfo.DateTime);
+
+ // Compute step from a 20% padded max, then set MaxLimit = step * 3 so the
+ // 4th label lands exactly on MaxLimit and is never cut off by rounding.
+ var maxVal = _pingValues.Where(p => !double.IsNaN(p.Value)).Select(p => p.Value).DefaultIfEmpty(0).Max();
+ if (maxVal > 0)
+ {
+ var yAxis = (Axis)PingYAxes[0];
+ var step = Math.Ceiling(maxVal * 1.2 / 3.0);
+ yAxis.MinStep = step;
+ yAxis.MaxLimit = step * 3;
+ }
+ });
// Add to history
_pingInfoList.Add(e.Args);
diff --git a/Source/NETworkManager/ViewModels/SpeedTestWidgetViewModel.cs b/Source/NETworkManager/ViewModels/SpeedTestWidgetViewModel.cs
index 7fa4dce898..277706fd4c 100644
--- a/Source/NETworkManager/ViewModels/SpeedTestWidgetViewModel.cs
+++ b/Source/NETworkManager/ViewModels/SpeedTestWidgetViewModel.cs
@@ -177,15 +177,13 @@ public SpeedTestWidgetViewModel()
[
new LineSeries
{
+ Name = Strings.Download,
Values = DownloadSamples,
- GeometrySize = 3,
+ GeometrySize = 1.5f,
LineSmoothness = 0.3,
DataPadding = new LvcPoint(0, 0),
Stroke = new SolidColorPaint(downloadColor) { StrokeThickness = 1.5f },
- Fill = new SolidColorPaint(downloadColor.WithAlpha(0x33)),
- GeometryStroke = new SolidColorPaint(downloadColor) { StrokeThickness = 1.5f },
- GeometryFill = new SolidColorPaint(downloadColor),
- YToolTipLabelFormatter = point => $"{point.Model:F1} Mbps"
+ Fill = new SolidColorPaint(downloadColor.WithAlpha(0x33))
}
];
@@ -194,15 +192,13 @@ public SpeedTestWidgetViewModel()
[
new LineSeries
{
+ Name = Strings.Upload,
Values = UploadSamples,
- GeometrySize = 3,
+ GeometrySize = 1.5f,
LineSmoothness = 0.3,
DataPadding = new LvcPoint(0, 0),
Stroke = new SolidColorPaint(uploadColor) { StrokeThickness = 1.5f },
- Fill = new SolidColorPaint(uploadColor.WithAlpha(0x33)),
- GeometryStroke = new SolidColorPaint(uploadColor) { StrokeThickness = 1.5f },
- GeometryFill = new SolidColorPaint(uploadColor),
- YToolTipLabelFormatter = point => $"{point.Model:F1} Mbps"
+ Fill = new SolidColorPaint(uploadColor.WithAlpha(0x33))
}
];
}
diff --git a/Source/NETworkManager/Views/PingMonitorView.xaml b/Source/NETworkManager/Views/PingMonitorView.xaml
index c34ed071e5..c2e6e2686d 100644
--- a/Source/NETworkManager/Views/PingMonitorView.xaml
+++ b/Source/NETworkManager/Views/PingMonitorView.xaml
@@ -8,7 +8,7 @@
xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"
xmlns:viewModels="clr-namespace:NETworkManager.ViewModels"
xmlns:localization="clr-namespace:NETworkManager.Localization.Resources;assembly=NETworkManager.Localization"
- xmlns:liveChart="clr-namespace:LiveCharts.Wpf;assembly=LiveCharts.Wpf"
+ xmlns:lvc="clr-namespace:LiveChartsCore.SkiaSharpView.WPF;assembly=LiveChartsCore.SkiaSharpView.WPF"
mc:Ignorable="d" d:DataContext="{d:DesignInstance viewModels:PingMonitorViewModel}">
@@ -207,8 +207,7 @@
-
+
@@ -223,27 +222,17 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
diff --git a/Source/NETworkManager/Views/SpeedTestWidgetView.xaml b/Source/NETworkManager/Views/SpeedTestWidgetView.xaml
index 250a2313d5..ef82b999e0 100644
--- a/Source/NETworkManager/Views/SpeedTestWidgetView.xaml
+++ b/Source/NETworkManager/Views/SpeedTestWidgetView.xaml
@@ -6,6 +6,7 @@
xmlns:localization="clr-namespace:NETworkManager.Localization.Resources;assembly=NETworkManager.Localization"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:lvc="clr-namespace:LiveChartsCore.SkiaSharpView.WPF;assembly=LiveChartsCore.SkiaSharpView.WPF"
+ xmlns:controls="clr-namespace:NETworkManager.Controls"
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:NETworkManager.ViewModels"
@@ -233,8 +234,13 @@
Text="{Binding CurrentJitterMs, Converter={StaticResource NullableDoubleToStringConverter}, ConverterParameter='F1|ms', Mode=OneWay}" />
-
-
+
+
+
+
+
+
+
@@ -244,16 +250,28 @@
Style="{StaticResource MessageTextBlock}"
Margin="10,0,0,0" />
-
-
+ DrawMarginFrame="{x:Null}"
+ RenderOptions.BitmapScalingMode="NearestNeighbor">
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
@@ -263,13 +281,17 @@
Style="{StaticResource MessageTextBlock}"
Margin="10,0,0,0" />
-
-
+
+
+
+
+
+
From 5d652427c5b9b55ed67145242a57e98135de7d3d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 27 May 2026 22:16:47 +0000
Subject: [PATCH 2/4] Guard PingMonitor resource lookups when Application is
null
---
Source/NETworkManager/ViewModels/PingMonitorViewModel.cs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs b/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs
index a3318e8e59..e0ed70c9fe 100644
--- a/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs
+++ b/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs
@@ -276,11 +276,11 @@ private void InitialTimeChart()
var chartColor = SKColor.Parse("#1ba1e2");
- var labelColor = Application.Current.Resources["MahApps.Brushes.Gray5"] is System.Windows.Media.SolidColorBrush gray5
+ var labelColor = Application.Current?.TryFindResource("MahApps.Brushes.Gray5") is System.Windows.Media.SolidColorBrush gray5
? new SKColor(gray5.Color.R, gray5.Color.G, gray5.Color.B, gray5.Color.A)
: new SKColor(0x68, 0x68, 0x68);
- var separatorColor = Application.Current.Resources["MahApps.Brushes.Gray8"] is System.Windows.Media.SolidColorBrush gray8
+ var separatorColor = Application.Current?.TryFindResource("MahApps.Brushes.Gray8") is System.Windows.Media.SolidColorBrush gray8
? new SKColor(gray8.Color.R, gray8.Color.G, gray8.Color.B, gray8.Color.A)
: new SKColor(0x80, 0x80, 0x80);
@@ -659,4 +659,4 @@ private void Ping_PingException(object sender, PingExceptionArgs e)
}
#endregion
-}
\ No newline at end of file
+}
From 58c3dd89d2884370dbeee3e7049eabfd44d8d638 Mon Sep 17 00:00:00 2001
From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com>
Date: Thu, 28 May 2026 21:12:05 +0200
Subject: [PATCH 3/4] Feature: Migrate LiveChart to LiveCharts2
---
README.md | 1 +
.../LibraryManager.cs | 42 +++++++++----------
.../{LiveCharts.txt => LiveCharts2.txt} | 4 +-
.../NETworkManager.Documentation.csproj | 4 +-
.../Resources/Strings.resx | 2 +-
Website/docs/changelog/next-release.md | 6 +++
6 files changed, 33 insertions(+), 26 deletions(-)
rename Source/NETworkManager.Documentation/Licenses/{LiveCharts.txt => LiveCharts2.txt} (92%)
diff --git a/README.md b/README.md
index 3357827d79..062e8c98ab 100644
--- a/README.md
+++ b/README.md
@@ -227,6 +227,7 @@ A huge thank you to our supporters and contributors who make NETworkManager poss
- [Dragablz](https://dragablz.net/) - Tearable TabControl for WPF.
- [GongSolutions.Wpf.DragDrop](https://github.com/punker76/gong-wpf-dragdrop) - An easy to use drag'n'drop framework for WPF.
- [IPNetwork](https://github.com/lduchosal/ipnetwork) - .NET library for complex network, IP, and subnet calculations.
+ - [LiveCharts2](https://github.com/Live-Charts/LiveCharts2) - Beautiful, interactive charts, maps, and gauges.
- [LoadingIndicators.WPF](https://github.com/zeluisping/LoadingIndicators.WPF) - A collection of loading indicators for WPF.
- [MahApps.Metro](https://mahapps.com/) - UI toolkit for WPF applications.
- [MahApps.Metro.IconPacks](https://github.com/MahApps/MahApps.Metro.IconPacks) - Awesome icon packs for WPF and UWP in one library.
diff --git a/Source/NETworkManager.Documentation/LibraryManager.cs b/Source/NETworkManager.Documentation/LibraryManager.cs
index 6a6d788c33..5844e094ac 100644
--- a/Source/NETworkManager.Documentation/LibraryManager.cs
+++ b/Source/NETworkManager.Documentation/LibraryManager.cs
@@ -20,83 +20,83 @@ public static class LibraryManager
///
public static List List =>
[
- new LibraryInfo("#SNMP Library", "https://github.com/lextudio/sharpsnmplib",
+ new("#SNMP Library", "https://github.com/lextudio/sharpsnmplib",
Strings.Library_SharpSNMP_Description,
Strings.License_MITLicense,
"https://github.com/lextudio/sharpsnmplib/blob/master/LICENSE"),
- new LibraryInfo("AirspaceFixer", "https://github.com/chris84948/AirspaceFixer",
+ new("AirspaceFixer", "https://github.com/chris84948/AirspaceFixer",
Strings.Library_AirspaceFixer_Description,
Strings.License_MITLicense,
"https://github.com/chris84948/AirspaceFixer/blob/master/LICENSE"),
- new LibraryInfo("ControlzEx", "https://github.com/ControlzEx/ControlzEx",
+ new("ControlzEx", "https://github.com/ControlzEx/ControlzEx",
Strings.Library_ControlzEx_Description,
Strings.License_MITLicense,
"https://github.com/ButchersBoy/Dragablz/blob/master/LICENSE"),
- new LibraryInfo("DnsClient.NET", "https://github.com/MichaCo/DnsClient.NET",
+ new("DnsClient.NET", "https://github.com/MichaCo/DnsClient.NET",
Strings.Library_DnsClientNET_Description,
Strings.License_ApacheLicense2dot0,
"https://github.com/MichaCo/DnsClient.NET/blob/dev/LICENSE"),
- new LibraryInfo("Dragablz", "https://github.com/ButchersBoy/Dragablz",
+ new("Dragablz", "https://github.com/ButchersBoy/Dragablz",
Strings.Library_Dragablz_Description,
Strings.License_MITLicense,
"https://github.com/ButchersBoy/Dragablz/blob/master/LICENSE"),
- new LibraryInfo("GongSolutions.Wpf.DragDrop", "https://github.com/punker76/gong-wpf-dragdrop",
+ new("GongSolutions.Wpf.DragDrop", "https://github.com/punker76/gong-wpf-dragdrop",
Strings.Library_GongSolutionsWpfDragDrop_Description,
Strings.License_BDS3Clause,
"https://github.com/punker76/gong-wpf-dragdrop/blob/develop/LICENSE"),
- new LibraryInfo("IPNetwork", "https://github.com/lduchosal/ipnetwork",
+ new("IPNetwork", "https://github.com/lduchosal/ipnetwork",
Strings.Library_IPNetwork_Description,
Strings.License_BDS2Clause,
"https://github.com/lduchosal/ipnetwork/blob/master/LICENSE"),
- new LibraryInfo("LiveCharts", "https://github.com/Live-Charts/Live-Charts",
+ new("LiveCharts2", "https://github.com/Live-Charts/LiveCharts2",
Strings.Library_LiveCharts_Description,
Strings.License_MITLicense,
- "https://github.com/Live-Charts/Live-Charts/blob/master/LICENSE.TXT"),
- new LibraryInfo("LoadingIndicators.WPF", "https://github.com/zeluisping/LoadingIndicators.WPF",
+ "https://github.com/Live-Charts/LiveCharts2/blob/master/LICENSE"),
+ new("LoadingIndicators.WPF", "https://github.com/zeluisping/LoadingIndicators.WPF",
Strings.Library_LoadingIndicatorsWPF_Description,
Strings.License_Unlicense,
"https://github.com/zeluisping/LoadingIndicators.WPF/blob/master/LICENSE"),
- new LibraryInfo("log4net", "https://logging.apache.org/log4net/",
+ new("log4net", "https://logging.apache.org/log4net/",
Strings.Library_log4net_Description,
Strings.License_ApacheLicense2dot0,
"https://github.com/apache/logging-log4net/blob/master/LICENSE"),
- new LibraryInfo("MahApps.Metro", "https://github.com/mahapps/mahapps.metro",
+ new("MahApps.Metro", "https://github.com/mahapps/mahapps.metro",
Strings.Library_MahAppsMetro_Description,
Strings.License_MITLicense,
"https://github.com/MahApps/MahApps.Metro/blob/master/LICENSE"),
- new LibraryInfo("MahApps.Metro.IconPacks", "https://github.com/MahApps/MahApps.Metro.IconPacks",
+ new("MahApps.Metro.IconPacks", "https://github.com/MahApps/MahApps.Metro.IconPacks",
Strings.Library_MahAppsMetroIconPacks_Description,
Strings.License_MITLicense,
"https://github.com/MahApps/MahApps.Metro.IconPacks/blob/master/LICENSE"),
- new LibraryInfo("Microsoft.PowerShell.SDK", "https://github.com/PowerShell/PowerShell",
+ new("Microsoft.PowerShell.SDK", "https://github.com/PowerShell/PowerShell",
Strings.Library_PowerShellSDK_Description,
Strings.License_MITLicense,
"https://github.com/PowerShell/PowerShell/blob/master/LICENSE.txt"),
- new LibraryInfo("Microsoft.Web.WebView2", "https://docs.microsoft.com/en-us/microsoft-edge/webview2/",
+ new("Microsoft.Web.WebView2", "https://docs.microsoft.com/en-us/microsoft-edge/webview2/",
Strings.Library_WebView2_Description,
Strings.License_MicrosoftWebView2License,
"https://www.nuget.org/packages/Microsoft.Web.WebView2/1.0.824-prerelease/License"),
- new LibraryInfo("Microsoft.Windows.CsWinRT", "https://github.com/microsoft/cswinrt/tree/master/",
+ new("Microsoft.Windows.CsWinRT", "https://github.com/microsoft/cswinrt/tree/master/",
Strings.Library_CsWinRT_Description,
Strings.License_MITLicense,
"https://github.com/microsoft/CsWinRT/blob/master/LICENSE"),
- new LibraryInfo("Microsoft.Xaml.Behaviors.Wpf", "https://github.com/microsoft/XamlBehaviorsWpf",
+ new("Microsoft.Xaml.Behaviors.Wpf", "https://github.com/microsoft/XamlBehaviorsWpf",
Strings.Library_XamlBehaviorsWpf_Description,
Strings.License_MITLicense,
"https://github.com/microsoft/XamlBehaviorsWpf/blob/master/LICENSE"),
- new LibraryInfo("Newtonsoft.Json", "https://github.com/JamesNK/Newtonsoft.Json",
+ new("Newtonsoft.Json", "https://github.com/JamesNK/Newtonsoft.Json",
Strings.Library_NewtonsoftJson_Description,
Strings.License_MITLicense,
"https://github.com/JamesNK/Newtonsoft.Json/blob/master/LICENSE.md"),
- new LibraryInfo("nulastudio.NetBeauty", "https://github.com/nulastudio/NetBeauty2",
+ new("nulastudio.NetBeauty", "https://github.com/nulastudio/NetBeauty2",
Strings.Library_nulastudioNetBeauty_Description,
Strings.License_MITLicense,
"https://github.com/nulastudio/NetBeauty2/blob/master/LICENSE"),
- new LibraryInfo("Octokit", "https://github.com/octokit/octokit.net",
+ new("Octokit", "https://github.com/octokit/octokit.net",
Strings.Library_Octokit_Description,
Strings.License_MITLicense,
"https://github.com/octokit/octokit.net/blob/master/LICENSE.txt"),
- new LibraryInfo("PSDiscoveryProtocol", "https://github.com/lahell/PSDiscoveryProtocol",
+ new("PSDiscoveryProtocol", "https://github.com/lahell/PSDiscoveryProtocol",
Strings.Library_PSDicoveryProtocol_Description,
Strings.License_MITLicense,
"https://github.com/lahell/PSDiscoveryProtocol/blob/master/LICENSE")
diff --git a/Source/NETworkManager.Documentation/Licenses/LiveCharts.txt b/Source/NETworkManager.Documentation/Licenses/LiveCharts2.txt
similarity index 92%
rename from Source/NETworkManager.Documentation/Licenses/LiveCharts.txt
rename to Source/NETworkManager.Documentation/Licenses/LiveCharts2.txt
index 65c8efe372..bf5cc2e5ea 100644
--- a/Source/NETworkManager.Documentation/Licenses/LiveCharts.txt
+++ b/Source/NETworkManager.Documentation/Licenses/LiveCharts2.txt
@@ -1,6 +1,6 @@
-The MIT License (MIT)
+MIT License
-Copyright (c) 2016 Alberto Rodriguez & LiveCharts contributors
+Copyright (c) 2021 Alberto Rodriguez Orozco
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/Source/NETworkManager.Documentation/NETworkManager.Documentation.csproj b/Source/NETworkManager.Documentation/NETworkManager.Documentation.csproj
index 996f839fac..029e38d24c 100644
--- a/Source/NETworkManager.Documentation/NETworkManager.Documentation.csproj
+++ b/Source/NETworkManager.Documentation/NETworkManager.Documentation.csproj
@@ -20,7 +20,7 @@
-
+
@@ -61,7 +61,7 @@
PreserveNewest
-
+
PreserveNewest
diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx
index 9e99aee06d..4cb9414464 100644
--- a/Source/NETworkManager.Localization/Resources/Strings.resx
+++ b/Source/NETworkManager.Localization/Resources/Strings.resx
@@ -2845,7 +2845,7 @@ Error message:
C# library take care of complex network, IP, IPv4, IPv6, netmask, CIDR, subnet, subnetting, supernet, and supernetting calculation for .NET developers.
- Simple, flexible, interactive & powerful charts, maps and gauges for .Net
+ Beautiful, interactive charts, maps, and gauges.
A collection of loading indicators for WPF
diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md
index 28d1b77eef..d384d45df3 100644
--- a/Website/docs/changelog/next-release.md
+++ b/Website/docs/changelog/next-release.md
@@ -68,12 +68,17 @@ Release date: **xx.xx.2025**
**Dashboard**
- Added a **Refresh** button (with animated feedback) to the **Network Connection**, **IP Geolocation**, and **DNS Resolver** widgets. The global reload button in the Status Window has been removed, as each widget now has its own. [#3447](https://github.com/BornToBeRoot/NETworkManager/pull/3447)
+- Added a tooltip to the **Speed Test** widget chart showing download/upload speed values on hover. [#3449](https://github.com/BornToBeRoot/NETworkManager/pull/3449)
- Redesign Status Window to make it more compact. [#3359](https://github.com/BornToBeRoot/NETworkManager/pull/3359)
**Network Interface**
- Added Network Profile (domain, private, public) information to the Network Interface details view, if available. [#3383](https://github.com/BornToBeRoot/NETworkManager/pull/3383)
+**Ping Monitor**
+
+- Migrated charts from LiveCharts to LiveCharts2. Added a tooltip showing the ping time on hover. [#3449](https://github.com/BornToBeRoot/NETworkManager/pull/3449)
+
**Discovery Protocol**
- Added support for `F5` and `Enter` keys to start capturing network packets. [#3383](https://github.com/BornToBeRoot/NETworkManager/pull/3383)
@@ -111,6 +116,7 @@ Release date: **xx.xx.2025**
## Dependencies, Refactoring & Documentation
+- Migrated from `LiveCharts` to `LiveCharts2` (`LiveChartsCore.SkiaSharpView.WPF`) for chart rendering. [#3449](https://github.com/BornToBeRoot/NETworkManager/pull/3449)
- Fixed `CancellationTokenSource` leak in `IPScanner`, `PortScanner`, `Traceroute`, `PingMonitor`, `PingMonitorHost` and `SNMP` ViewModels. The previous instance was never disposed before being overwritten on each run, leaking the underlying `WaitHandle`. [#3448](https://github.com/BornToBeRoot/NETworkManager/pull/3448)
- Replace fire-and-forget `.ConfigureAwait(false)` calls with explicit discard assignments (`_ = SomeAsyncOperation()`) across command handlers, startup/load paths and profile callbacks. [#3441](https://github.com/BornToBeRoot/NETworkManager/pull/3441)
- Code cleanup & refactoring
From 835011cd2f4a850777837b7cb67f0242308a4293 Mon Sep 17 00:00:00 2001
From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com>
Date: Thu, 28 May 2026 21:31:27 +0200
Subject: [PATCH 4/4] Fix: Based on Claude feedback
---
.../NETworkManager/ViewModels/PingMonitorViewModel.cs | 10 ++++++++--
Source/NETworkManager/Views/SpeedTestWidgetView.xaml | 3 ++-
2 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs b/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs
index e0ed70c9fe..ada5083017 100644
--- a/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs
+++ b/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs
@@ -67,6 +67,7 @@ public PingMonitorViewModel(Guid hostId, Action removeHostByGuid,
private CancellationTokenSource _cancellationTokenSource;
private int _maxPingValues;
+ private int _sessionId;
public readonly Guid HostId;
private readonly Action _removeHostByGuid;
@@ -339,12 +340,12 @@ private void InitialTimeChart()
///
/// Gets the X-axes configuration for the ping time chart.
///
- public ICartesianAxis[] PingXAxes { get; private set; }
+ public Axis[] PingXAxes { get; private set; }
///
/// Gets the Y-axes configuration for the ping time chart.
///
- public ICartesianAxis[] PingYAxes { get; private set; }
+ public Axis[] PingYAxes { get; private set; }
///
/// Gets the error message if an error occurs.
@@ -447,6 +448,7 @@ public void Start()
PacketLoss = 0;
// Reset chart
+ _sessionId++;
ResetTimeChart();
_cancellationTokenSource?.Dispose();
@@ -597,8 +599,12 @@ private void Ping_PingReceived(object sender, PingReceivedArgs e)
TimeMs = e.Args.Time;
// Null exception may occur when the application is closing
+ var session = _sessionId;
Application.Current?.Dispatcher.BeginInvoke(DispatcherPriority.Normal, () =>
{
+ if (_sessionId != session)
+ return;
+
_pingValues.Add(timeInfo);
if (_pingValues.Count > _maxPingValues)
_pingValues.RemoveAt(0);
diff --git a/Source/NETworkManager/Views/SpeedTestWidgetView.xaml b/Source/NETworkManager/Views/SpeedTestWidgetView.xaml
index ef82b999e0..0760fc8ebc 100644
--- a/Source/NETworkManager/Views/SpeedTestWidgetView.xaml
+++ b/Source/NETworkManager/Views/SpeedTestWidgetView.xaml
@@ -286,7 +286,8 @@
Series="{Binding UploadSeries}"
XAxes="{Binding UploadXAxes}"
YAxes="{Binding UploadYAxes}"
- DrawMarginFrame="{x:Null}">
+ DrawMarginFrame="{x:Null}"
+ RenderOptions.BitmapScalingMode="NearestNeighbor">