diff --git a/LibreHardwareMonitor.Windows.WinUI/Controls/PlotView.cs b/LibreHardwareMonitor.Windows.WinUI/Controls/PlotView.cs index bedf85b..5b01654 100644 --- a/LibreHardwareMonitor.Windows.WinUI/Controls/PlotView.cs +++ b/LibreHardwareMonitor.Windows.WinUI/Controls/PlotView.cs @@ -207,6 +207,9 @@ public sealed class PlotView : Grid foreach (PlotSeriesSample sample in samples) DrawPlotSeries(_canvas, bounds, axis, minTimestamp, maxTimestamp, sample); } + + if (VM.ShowPlotLegend) + DrawLegend(_canvas, bounds, visibleSeries); } private MenuFlyout BuildPlotContextMenu() @@ -214,6 +217,7 @@ public sealed class PlotView : Grid MenuFlyout flyout = new(); flyout.Items.Add(CreateToggleSettingItem("Stacked Axes", () => VM.PlotStackedAxes, value => VM.PlotStackedAxes = value)); flyout.Items.Add(CreateToggleSettingItem("Show Axes Labels", () => VM.ShowPlotAxisLabels, value => VM.ShowPlotAxisLabels = value)); + flyout.Items.Add(CreateToggleSettingItem("Show Legend", () => VM.ShowPlotLegend, value => VM.ShowPlotLegend = value)); MenuFlyoutSubItem timeAxis = new() { Text = "Time Axis" }; timeAxis.Items.Add(CreateToggleSettingItem("Enable Zoom", () => VM.PlotTimeAxisZoomEnabled, value => VM.PlotTimeAxisZoomEnabled = value)); @@ -475,6 +479,158 @@ public sealed class PlotView : Grid canvas.Children.Add(label); } + private const int LegendMaxRows = 12; + private const double LegendMinPlotWidth = 200; + private const double LegendSwatchSize = 12; + private const double LegendRowSpacing = 4; + private const double LegendColumnSpacing = 10; + private const double LegendPadding = 8; + private const double LegendInset = 8; + private const double LegendFontSize = 11; + + private void DrawLegend(Canvas canvas, PlotBounds bounds, IReadOnlyList visibleSeries) + { + if (visibleSeries.Count == 0 || bounds.Width < LegendMinPlotWidth) + return; + + PlotSeriesSample[] shown = visibleSeries.Take(LegendMaxRows).ToArray(); + int overflow = visibleSeries.Count - shown.Length; + SolidColorBrush textBrush = new(GetPlotTextColor()); + + TextBlock[] labelBlocks = new TextBlock[shown.Length]; + TextBlock[] valueBlocks = new TextBlock[shown.Length]; + double[] labelDesiredWidths = new double[shown.Length]; + double valueColumnWidth = 0; + double rowHeight = 0; + + for (int i = 0; i < shown.Length; i++) + { + PlotSeriesSample sample = shown[i]; + TextBlock label = CreatePlotLabel(sample.Series.Title, textBrush, LegendFontSize); + label.Measure(new global::Windows.Foundation.Size(double.PositiveInfinity, double.PositiveInfinity)); + labelBlocks[i] = label; + labelDesiredWidths[i] = label.DesiredSize.Width; + + string valueText = FormatLegendValue(sample.Points[^1].Value, sample.Series.Unit); + TextBlock value = CreatePlotLabel(valueText, textBrush, LegendFontSize, 600); + value.Measure(new global::Windows.Foundation.Size(double.PositiveInfinity, double.PositiveInfinity)); + valueBlocks[i] = value; + valueColumnWidth = Math.Max(valueColumnWidth, value.DesiredSize.Width); + + rowHeight = Math.Max(rowHeight, Math.Max(label.DesiredSize.Height, value.DesiredSize.Height)); + } + + TextBlock? overflowBlock = null; + if (overflow > 0) + { + overflowBlock = CreatePlotLabel($"+{overflow} more", textBrush, LegendFontSize); + overflowBlock.Measure(new global::Windows.Foundation.Size(double.PositiveInfinity, double.PositiveInfinity)); + rowHeight = Math.Max(rowHeight, overflowBlock.DesiredSize.Height); + } + + rowHeight = Math.Max(rowHeight, LegendSwatchSize); + + double fixedWidth = LegendPadding * 2 + LegendSwatchSize + LegendColumnSpacing + LegendColumnSpacing + valueColumnWidth; + double maxLabelColumnWidth = Math.Max(40, bounds.Width - LegendInset * 2 - fixedWidth); + + double labelColumnWidth = 0; + for (int i = 0; i < shown.Length; i++) + labelColumnWidth = Math.Max(labelColumnWidth, Math.Min(labelDesiredWidths[i], maxLabelColumnWidth)); + + bool clamped = false; + for (int i = 0; i < shown.Length; i++) + { + if (labelDesiredWidths[i] > labelColumnWidth + 0.5) + { + clamped = true; + break; + } + } + if (clamped) + { + foreach (TextBlock label in labelBlocks) + label.TextTrimming = TextTrimming.CharacterEllipsis; + } + + int totalRows = shown.Length + (overflowBlock != null ? 1 : 0); + double panelWidth = fixedWidth + labelColumnWidth; + double panelHeight = LegendPadding * 2 + totalRows * rowHeight + Math.Max(0, totalRows - 1) * LegendRowSpacing; + + if (panelHeight > bounds.Height - LegendInset * 2) + return; + + double panelLeft = bounds.Right - panelWidth - LegendInset; + double panelTop = bounds.Top + LegendInset; + + Rectangle background = new() + { + Width = panelWidth, + Height = panelHeight, + RadiusX = 4, + RadiusY = 4, + Fill = new SolidColorBrush(GetLegendBackgroundColor()), + Stroke = new SolidColorBrush(GetPlotBorderColor()), + StrokeThickness = 1 + }; + Canvas.SetLeft(background, panelLeft); + Canvas.SetTop(background, panelTop); + canvas.Children.Add(background); + + double rowTop = panelTop + LegendPadding; + double swatchLeft = panelLeft + LegendPadding; + double labelLeft = swatchLeft + LegendSwatchSize + LegendColumnSpacing; + double valueRight = panelLeft + panelWidth - LegendPadding; + + for (int i = 0; i < shown.Length; i++) + { + PlotSeriesSample sample = shown[i]; + global::Windows.UI.Color seriesColor = GetVisiblePlotColor(sample.Series.Color); + Rectangle swatch = new() + { + Width = LegendSwatchSize, + Height = LegendSwatchSize, + RadiusX = 2, + RadiusY = 2, + Fill = new SolidColorBrush(seriesColor) + }; + Canvas.SetLeft(swatch, swatchLeft); + Canvas.SetTop(swatch, rowTop + Math.Max(0, (rowHeight - LegendSwatchSize) / 2)); + canvas.Children.Add(swatch); + + TextBlock label = labelBlocks[i]; + label.MaxWidth = labelColumnWidth; + Canvas.SetLeft(label, labelLeft); + Canvas.SetTop(label, rowTop + Math.Max(0, (rowHeight - label.DesiredSize.Height) / 2)); + canvas.Children.Add(label); + + TextBlock value = valueBlocks[i]; + Canvas.SetLeft(value, valueRight - value.DesiredSize.Width); + Canvas.SetTop(value, rowTop + Math.Max(0, (rowHeight - value.DesiredSize.Height) / 2)); + canvas.Children.Add(value); + + rowTop += rowHeight + LegendRowSpacing; + } + + if (overflowBlock != null) + { + Canvas.SetLeft(overflowBlock, labelLeft); + Canvas.SetTop(overflowBlock, rowTop + Math.Max(0, (rowHeight - overflowBlock.DesiredSize.Height) / 2)); + canvas.Children.Add(overflowBlock); + } + } + + private global::Windows.UI.Color GetLegendBackgroundColor() + { + global::Windows.UI.Color background = GetPlotBackgroundColor(); + return global::Windows.UI.Color.FromArgb(220, background.R, background.G, background.B); + } + + private static string FormatLegendValue(double value, string unit) + { + string formatted = FormatPlotAxisValue(value); + return string.IsNullOrEmpty(unit) ? formatted : $"{formatted} {unit}"; + } + private static void GetValueRange(IReadOnlyList samples, out double minValue, out double maxValue) { minValue = double.MaxValue; diff --git a/LibreHardwareMonitor.Windows.WinUI/MainWindow.xaml b/LibreHardwareMonitor.Windows.WinUI/MainWindow.xaml index bdfd1c5..1943e20 100644 --- a/LibreHardwareMonitor.Windows.WinUI/MainWindow.xaml +++ b/LibreHardwareMonitor.Windows.WinUI/MainWindow.xaml @@ -43,6 +43,9 @@ + diff --git a/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs b/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs index bff804d..b0f91a7 100644 --- a/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs +++ b/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs @@ -103,6 +103,7 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable private bool _showMaxColumn; private bool _showMinColumn; private bool _showPlotAxisLabels; + private bool _showPlotLegend; private bool _showPlot; private bool _showValueColumn; private bool _throttleAtaUpdate; @@ -148,6 +149,7 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable _plotStrokeThickness = Math.Clamp(settings.GetValue("plotStroke", 1) + 1, 1, 4); _plotStackedAxes = settings.GetValue("stackedAxes", true); _showPlotAxisLabels = settings.GetValue("showAxesLabels", true); + _showPlotLegend = settings.GetValue("showPlotLegend", true); _plotTimeAxisZoomEnabled = settings.GetValue("timeAxisEnableZoom", true); _plotValueAxesZoomEnabled = settings.GetValue("yAxesEnableZoom", true); int plotTimeWindowDefault = GetPlotTimeWindowIndex(settings.GetValue("plotPanel.MaxTimeSpan", 10.0f * 60)); @@ -187,6 +189,8 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable public Visibility PlotVisibility => ShowPlot && PlotLocation != PlotLocation.Window ? Visibility.Visible : Visibility.Collapsed; + public Visibility ShowPlotLegendMenuVisibility => ShowPlot ? Visibility.Visible : Visibility.Collapsed; + public ObservableCollection RootItems { get; } = []; public SensorTreeItemViewModel? SelectedItem @@ -601,6 +605,19 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable } } + public bool ShowPlotLegend + { + get => _showPlotLegend; + set + { + if (!SetProperty(ref _showPlotLegend, value)) + return; + + Settings.SetValue("showPlotLegend", value); + PlotInvalidated?.Invoke(this, EventArgs.Empty); + } + } + public bool ShowPlot { get => _showPlot; @@ -611,6 +628,7 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable Settings.SetValue("plotMenuItem", value); OnPropertyChanged(nameof(PlotVisibility)); + OnPropertyChanged(nameof(ShowPlotLegendMenuVisibility)); OnPropertyChanged(nameof(IsPlotWindowVisible)); NotifyPlotLayoutChanged(); PlotInvalidated?.Invoke(this, EventArgs.Empty);