From 4e4b8eb8a19d6e0d9bc71a7002784db9e9f9d131 Mon Sep 17 00:00:00 2001 From: Alex Bochkov Date: Mon, 24 Nov 2025 13:06:59 -0700 Subject: [PATCH 1/5] Replace OxyPlot with ScottPlot --- AxialSqlTools/AxialSqlTools.csproj | 4 +- AxialSqlTools/AxialSqlToolsPackage.cs | 15 - .../HealthDashboard_ServerControl.xaml | 46 +- .../HealthDashboard_ServerControl.xaml.cs | 431 ++++++------------ 4 files changed, 168 insertions(+), 328 deletions(-) diff --git a/AxialSqlTools/AxialSqlTools.csproj b/AxialSqlTools/AxialSqlTools.csproj index 24a643a..88fd96c 100644 --- a/AxialSqlTools/AxialSqlTools.csproj +++ b/AxialSqlTools/AxialSqlTools.csproj @@ -240,8 +240,8 @@ 14.0.0 - - 2.2.0 + + 4.1.73 4.3.4 diff --git a/AxialSqlTools/AxialSqlToolsPackage.cs b/AxialSqlTools/AxialSqlToolsPackage.cs index 21f71f8..7bf31f5 100644 --- a/AxialSqlTools/AxialSqlToolsPackage.cs +++ b/AxialSqlTools/AxialSqlToolsPackage.cs @@ -406,9 +406,6 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke _logger.Error(ex, "An exception occurred"); } - // needed for the OxyPlot library - AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve); - } #endregion @@ -511,18 +508,6 @@ public void LoadGlobalSnippets() } - // I don't understand the purpose, but it works - private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) - { - // add this into main module -> AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve); - - if (args.Name.Contains("OxyPlot")) - return AppDomain.CurrentDomain.Load(args.Name); - else return null; - } - //---------------- - - // This method aligns all numeric values to the right public static void SQLResultsControl_ScriptExecutionCompleted(object QEOLESQLExec, object b) { diff --git a/AxialSqlTools/HealthDashboards/HealthDashboard_ServerControl.xaml b/AxialSqlTools/HealthDashboards/HealthDashboard_ServerControl.xaml index ae8dc60..d9f75e7 100644 --- a/AxialSqlTools/HealthDashboards/HealthDashboard_ServerControl.xaml +++ b/AxialSqlTools/HealthDashboards/HealthDashboard_ServerControl.xaml @@ -4,7 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:vsshell="clr-namespace:Microsoft.VisualStudio.Shell;assembly=Microsoft.VisualStudio.Shell.15.0" - xmlns:oxy="http://oxyplot.org/wpf" + xmlns:sp="clr-namespace:ScottPlot.WPF;assembly=ScottPlot.WPF" mc:Ignorable="d" d:DesignHeight="500" d:DesignWidth="1000" x:Name="MyToolWindow"> @@ -178,8 +178,8 @@ - + @@ -187,8 +187,8 @@ x:Name="LabelInternalException" Margin="5"/> - + @@ -196,18 +196,18 @@ - - - - - - - - - - - - + + + + + + + + + + + + @@ -254,10 +254,10 @@ - - + + @@ -287,8 +287,8 @@ - + diff --git a/AxialSqlTools/HealthDashboards/HealthDashboard_ServerControl.xaml.cs b/AxialSqlTools/HealthDashboards/HealthDashboard_ServerControl.xaml.cs index 218333d..4917c5a 100644 --- a/AxialSqlTools/HealthDashboards/HealthDashboard_ServerControl.xaml.cs +++ b/AxialSqlTools/HealthDashboards/HealthDashboard_ServerControl.xaml.cs @@ -13,12 +13,8 @@ using System.Windows.Controls; using Microsoft.SqlServer.Management.UI.VSIntegration.Editors; using System.Net.Http; - using OxyPlot; - using OxyPlot.Axes; - using OxyPlot.Series; using System.Collections.Generic; using System.Windows.Input; - using OxyPlot.Legends; using System.Linq; using static HealthDashboardServerMetric; using System.Diagnostics; @@ -26,7 +22,11 @@ using DocumentFormat.OpenXml.Bibliography; using DocumentFormat.OpenXml.Spreadsheet; using static AxialSqlTools.HealthDashboard_ServerControl; - using MarkerType = OxyPlot.MarkerType; + using ScottPlot; + using ScottPlot.Plottable; + using ScottPlot.WPF; + using ScottPlot.Palettes; + using Color = System.Drawing.Color; /// /// Interaction logic for HealthDashboard_ServerControl. @@ -411,54 +411,39 @@ public void UpdateUI(int i, HealthDashboardServerMetric metrics, bool doEmpty) //-------------------------------------------------------------------- // Disk info graph - var barModel = new PlotModel { Title = "Volume(s) Utilization" }; + var diskPlot = DiskInfoModel.Plot; + diskPlot.Clear(); + diskPlot.Title("Volume(s) Utilization"); - var barSeries1 = new BarSeries - { - LabelPlacement = LabelPlacement.Inside, - LabelFormatString = "{0:0} Gb", // Adjust this to change how the labels are formatted - StrokeColor = OxyColors.Black, - StrokeThickness = 1, - IsStacked = true - }; - foreach (var disk in metrics.DisksInfo) - barSeries1.Items.Add(new BarItem { Value = disk.UsedSpaceGb, Color = OxyColors.LightPink }); - barModel.Series.Add(barSeries1); + double[] positions = Enumerable.Range(0, metrics.DisksInfo.Count).Select(i => (double)i).ToArray(); + double[] usedValues = metrics.DisksInfo.Select(disk => disk.UsedSpaceGb).ToArray(); + double[] freeValues = metrics.DisksInfo.Select(disk => disk.FreeSpaceGb).ToArray(); + string[] labels = metrics.DisksInfo.Select(disk => disk.VolumeDescription).ToArray(); - var barSeries2 = new BarSeries - { - LabelPlacement = LabelPlacement.Inside, - LabelFormatString = "{0:0} Gb", // Adjust this to change how the labels are formatted - StrokeColor = OxyColors.Black, - StrokeThickness = 1, - IsStacked = true - }; - foreach (var disk in metrics.DisksInfo) - barSeries2.Items.Add(new BarItem { Value = disk.FreeSpaceGb, Color = OxyColors.LightBlue }); - barModel.Series.Add(barSeries2); + var usedBars = diskPlot.AddBar(usedValues, positions); + usedBars.Horizontal = true; + usedBars.Label = "Used"; + usedBars.FillColor = Color.LightPink; + usedBars.BorderColor = Color.Black; + usedBars.ValueFormatter = x => $"{x:0} Gb"; + var freeBars = diskPlot.AddBar(freeValues, positions); + freeBars.Horizontal = true; + freeBars.Label = "Free"; + freeBars.FillColor = Color.LightBlue; + freeBars.BorderColor = Color.Black; + freeBars.ValueFormatter = x => $"{x:0} Gb"; - barModel.Axes.Add(new CategoryAxis - { - Position = AxisPosition.Left, - Key = "DiskAxis", - ItemsSource = metrics.DisksInfo.Select(disk => disk.VolumeDescription).ToList(), - IsZoomEnabled = false, - IsPanEnabled = false - }); - - barModel.Axes.Add(new LinearAxis - { - Position = AxisPosition.Bottom, - MinimumPadding = 0.1, - MaximumPadding = 0.1, - AbsoluteMinimum = 0, - Title = "Gb", - IsZoomEnabled = false, - IsPanEnabled = false - }); + double offset = usedBars.BarWidth / 2; + usedBars.PositionOffset = -offset; + freeBars.PositionOffset = offset; + + diskPlot.YTicks(positions, labels); + diskPlot.XLabel("Gb"); + diskPlot.SetAxisLimits(xMin: 0); + diskPlot.Legend(location: Alignment.UpperRight); - this.DiskInfoModel.Model = barModel; + DiskInfoModel.Refresh(); //-------------------------------------------------------------------- @@ -553,68 +538,37 @@ private void UpdateWaitStatsGraph(List previousWaitStats, Dictionary< { var sortedKeys = aggrData.Keys.OrderBy(k => k).ToList(); + var waitPlot = WaitStatsModel.Plot; + waitPlot.Clear(); + waitPlot.Title("Real-time Wait Stats"); - var barModelWS = new PlotModel { Title = "Real-time Wait Stats" }; - - var categoryAxis = new CategoryAxis { - Position = AxisPosition.Left, - IsZoomEnabled = false, - IsPanEnabled = false - }; - var valueAxis = new LinearAxis { - Position = AxisPosition.Bottom, - MinimumPadding = 0, - AbsoluteMinimum = 0, - IsZoomEnabled = false, - IsPanEnabled = false - }; - - barModelWS.Axes.Add(categoryAxis); - barModelWS.Axes.Add(valueAxis); - - foreach (var key in sortedKeys) - { - categoryAxis.Labels.Add(key.ToString("HH:mm")); - } - - var LegendWS = new Legend - { - LegendTitle = "Wait Stats", - LegendPosition = LegendPosition.RightTop, - LegendPlacement = LegendPlacement.Outside, - LegendOrientation = LegendOrientation.Vertical, - LegendBackground = OxyColor.FromAColor(200, OxyColors.White), - LegendBorder = OxyColors.Black - }; - LegendWS.LegendMaxWidth = 200; + double[] positions = Enumerable.Range(0, sortedKeys.Count).Select(i => (double)i).ToArray(); + waitPlot.YTicks(positions, sortedKeys.Select(k => k.ToString("HH:mm")).ToArray()); + waitPlot.SetAxisLimits(xMin: 0); + waitPlot.Legend(true); - // Legend configuration - barModelWS.IsLegendVisible = true; // Make the legend visible - barModelWS.Legends.Add(LegendWS); + var palette = new Category10(); + double offsetStep = 0.1; + int seriesIndex = 0; foreach (var previousWaitStat in previousWaitStats) { - var barSeriesWS = new BarSeries - { - //LabelPlacement = LabelPlacement.Inside, - //LabelFormatString = "{0:0}", // Adjust this to change how the labels are formatted - StrokeColor = OxyColors.Black, - StrokeThickness = 1, - IsStacked = true - }; - - barSeriesWS.Title = previousWaitStat.WaitName; - - foreach (var aggValue in aggrData) - { - foreach (WaitsInfo ws in aggValue.Value) - if (ws.WaitName == previousWaitStat.WaitName) - barSeriesWS.Items.Add(new BarItem { Value = (double)ws.WaitSec }); //, Color = OxyColors.LightPink }); - } - barModelWS.Series.Add(barSeriesWS); + double[] values = sortedKeys + .Select(key => aggrData[key].FirstOrDefault(ws => ws.WaitName == previousWaitStat.WaitName)?.WaitSec ?? 0) + .Select(v => (double)v) + .ToArray(); + + var barSeries = waitPlot.AddBar(values, positions); + barSeries.Horizontal = true; + barSeries.Label = previousWaitStat.WaitName; + barSeries.FillColor = palette.GetColor(seriesIndex % palette.Count); + barSeries.BorderColor = Color.Black; + barSeries.PositionOffset = (seriesIndex - (previousWaitStats.Count / 2.0)) * offsetStep; + + seriesIndex++; } - this.WaitStatsModel.Model = barModelWS; + WaitStatsModel.Refresh(); } private void AddPerformanceSample(HealthDashboardServerMetric metrics) @@ -644,66 +598,40 @@ private void AddPerformanceSample(HealthDashboardServerMetric metrics) UpdatePerformanceCharts(); } - private PlotModel CreateTimeSeriesModel(string title, string yAxisTitle, Func valueSelector, bool clampToZero = true) + private void UpdateTimeSeriesPlot(WpfPlot targetPlot, string title, string yAxisTitle, Func valueSelector, bool clampToZero = true) { - var model = new PlotModel { Title = title }; + var orderedSamples = _performanceSamples.OrderBy(s => s.Timestamp).ToList(); - model.Axes.Add(new DateTimeAxis - { - Position = AxisPosition.Bottom, - StringFormat = "HH:mm", - IntervalType = DateTimeIntervalType.Minutes, - IsZoomEnabled = false, - IsPanEnabled = false, - MinorIntervalType = DateTimeIntervalType.Minutes - }); - - var linearAxis = new LinearAxis - { - Position = AxisPosition.Left, - Title = yAxisTitle, - IsZoomEnabled = false, - IsPanEnabled = false - }; + double[] xs = orderedSamples.Select(s => s.Timestamp.ToOADate()).ToArray(); + double[] ys = orderedSamples.Select(valueSelector).ToArray(); - if (clampToZero) - { - linearAxis.Minimum = 0; - } - - model.Axes.Add(linearAxis); - - var series = new LineSeries - { - StrokeThickness = 2, - MarkerSize = 2, - MarkerType = MarkerType.Circle - }; + var plt = targetPlot.Plot; + plt.Clear(); + plt.Title(title); + plt.XAxis.DateTimeFormat(true); + plt.YLabel(yAxisTitle); + plt.AddScatter(xs, ys, markerSize: 2, lineWidth: 2); - foreach (var sample in _performanceSamples.OrderBy(s => s.Timestamp)) - { - series.Points.Add(DateTimeAxis.CreateDataPoint(sample.Timestamp, valueSelector(sample))); - } - - model.Series.Add(series); + if (clampToZero) + plt.SetAxisLimits(yMin: 0); - return model; + targetPlot.Refresh(); } private void UpdatePerformanceCharts() { - PerfChart_CpuUtilization.Model = CreateTimeSeriesModel("CPU Utilization (%)", "%", s => s.CpuUtilization); - PerfChart_UserConnections.Model = CreateTimeSeriesModel("User Connections", "connections", s => s.UserConnections); - PerfChart_BatchRequests.Model = CreateTimeSeriesModel("Batch Requests/sec", "requests/sec", s => s.BatchRequestsSec); - PerfChart_SqlCompilations.Model = CreateTimeSeriesModel("SQL Compilations/sec", "compilations/sec", s => s.SqlCompilationsSec); - PerfChart_PageLifeExpectancy.Model = CreateTimeSeriesModel("Page Life Expectancy", "seconds", s => s.PageLifeExpectancy, clampToZero: false); - PerfChart_PageReads.Model = CreateTimeSeriesModel("Page Reads/sec", "pages/sec", s => s.PageReadsSec); - PerfChart_PageWrites.Model = CreateTimeSeriesModel("Page Writes/sec", "pages/sec", s => s.PageWritesSec); - PerfChart_LogFlushes.Model = CreateTimeSeriesModel("Log Flushes/sec", "flushes/sec", s => s.LogFlushesSec); - PerfChart_Transactions.Model = CreateTimeSeriesModel("Transactions/sec", "transactions/sec", s => s.TransactionsSec); - PerfChart_LockWaits.Model = CreateTimeSeriesModel("Lock Waits/sec", "waits/sec", s => s.LockWaitsSec); - PerfChart_MemoryGrantsPending.Model = CreateTimeSeriesModel("Memory Grants Pending", "grants", s => s.MemoryGrantsPending); - PerfChart_TotalServerMemory.Model = CreateTimeSeriesModel("Total Server Memory", "MB", s => s.TotalServerMemoryMb); + UpdateTimeSeriesPlot(PerfChart_CpuUtilization, "CPU Utilization (%)", "%", s => s.CpuUtilization); + UpdateTimeSeriesPlot(PerfChart_UserConnections, "User Connections", "connections", s => s.UserConnections); + UpdateTimeSeriesPlot(PerfChart_BatchRequests, "Batch Requests/sec", "requests/sec", s => s.BatchRequestsSec); + UpdateTimeSeriesPlot(PerfChart_SqlCompilations, "SQL Compilations/sec", "compilations/sec", s => s.SqlCompilationsSec); + UpdateTimeSeriesPlot(PerfChart_PageLifeExpectancy, "Page Life Expectancy", "seconds", s => s.PageLifeExpectancy, clampToZero: false); + UpdateTimeSeriesPlot(PerfChart_PageReads, "Page Reads/sec", "pages/sec", s => s.PageReadsSec); + UpdateTimeSeriesPlot(PerfChart_PageWrites, "Page Writes/sec", "pages/sec", s => s.PageWritesSec); + UpdateTimeSeriesPlot(PerfChart_LogFlushes, "Log Flushes/sec", "flushes/sec", s => s.LogFlushesSec); + UpdateTimeSeriesPlot(PerfChart_Transactions, "Transactions/sec", "transactions/sec", s => s.TransactionsSec); + UpdateTimeSeriesPlot(PerfChart_LockWaits, "Lock Waits/sec", "waits/sec", s => s.LockWaitsSec); + UpdateTimeSeriesPlot(PerfChart_MemoryGrantsPending, "Memory Grants Pending", "grants", s => s.MemoryGrantsPending); + UpdateTimeSeriesPlot(PerfChart_TotalServerMemory, "Total Server Memory", "MB", s => s.TotalServerMemoryMb); } public static string FormatBytesToMB(long bytes) @@ -857,19 +785,11 @@ FROM msdb.dbo.backupset ORDER BY database_name DESC, backup_start_date;"; - var MyModel = new PlotModel { Title = "Database Backups: Frequency and Durations Analysis" }; - - MyModel.Axes.Add(new DateTimeAxis - { - Position = AxisPosition.Bottom, - StringFormat = "dd-MM-yyyy HH:mm", - Title = "Date", - IntervalType = DateTimeIntervalType.Hours, - MinorIntervalType = DateTimeIntervalType.Minutes, - IntervalLength = 80, - IsZoomEnabled = false, - IsPanEnabled = false - }); + var backupTimelinePlot = BackupTimelineModel.Plot; + backupTimelinePlot.Clear(); + backupTimelinePlot.Title("Database Backups: Frequency and Durations Analysis"); + backupTimelinePlot.XAxis.DateTimeFormat(true); + backupTimelinePlot.XLabel("Date"); var customLabels = new Dictionary(); var databaseIndex = new Dictionary(); @@ -902,71 +822,38 @@ ORDER BY customLabels.Add(dbIndex, databaseName); - var startDate = DateTimeAxis.ToDouble(reader.GetDateTime(1)); - var finishDate = DateTimeAxis.ToDouble(reader.GetDateTime(2)); + var startDate = reader.GetDateTime(1).ToOADate(); + var finishDate = reader.GetDateTime(2).ToOADate(); var backupType = (double)reader.GetDecimal(3); - var LineColor = OxyColors.Blue; + var LineColor = Color.Blue; if (backupType == 0.1) - LineColor = OxyColors.Green; + LineColor = Color.Green; if (backupType == 0.2) - LineColor = OxyColors.DeepPink; + LineColor = Color.DeepPink; dbIndex = dbIndex + backupType; - var scatterSeries = new ScatterSeries { - MarkerType = OxyPlot.MarkerType.Circle, - MarkerFill = LineColor, - MarkerStrokeThickness = 1 - }; - - // Add two points to the scatter series - scatterSeries.Points.Add(new ScatterPoint(startDate, dbIndex)); - scatterSeries.Points.Add(new ScatterPoint(finishDate, dbIndex)); - - // Add a line series to connect the dots - var lineSeries = new LineSeries() - { - Color = LineColor, - StrokeThickness = 2, - LineStyle = LineStyle.Solid - }; - lineSeries.Points.Add(new DataPoint(startDate, dbIndex)); - lineSeries.Points.Add(new DataPoint(finishDate, dbIndex)); - - MyModel.Series.Add(lineSeries); - MyModel.Series.Add(scatterSeries); + var scatter = backupTimelinePlot.AddScatter( + new double[] { startDate, finishDate }, + new double[] { dbIndex, dbIndex }, + color: LineColor, + markerShape: MarkerShape.filledCircle, + markerSize: 5, + lineWidth: 2); + scatter.MarkerLineWidth = 1; } } } } - var yAxis = new LinearAxis - { - Position = AxisPosition.Left, - Title = "Backup Duration", - MajorStep = 1, - MinorStep = 1, - IsZoomEnabled = false, - IsPanEnabled = false, - - LabelFormatter = value => - { - // Return the custom label if it exists; otherwise, return the default string representation - if (customLabels.TryGetValue(value, out var label)) - { - return label; - } - return value.ToString(); - } - }; - - MyModel.Axes.Add(yAxis); + backupTimelinePlot.YLabel("Backup Duration"); + backupTimelinePlot.YTicks(customLabels.Keys.ToArray(), customLabels.Values.ToArray()); + backupTimelinePlot.SetAxisLimits(yMin: -0.5, yMax: customLabels.Count + 0.5); - - this.BackupTimelineModel.Model = MyModel; + BackupTimelineModel.Refresh(); //--------------------------------------------------- @@ -984,9 +871,12 @@ FROM msdb.dbo.backupset GROUP BY database_name ORDER BY 2 DESC"; - var PieModel = new PlotModel { Title = "Database Backup Sizes"}; + var piePlot = BackupSizeModel.Plot; + piePlot.Clear(); + piePlot.Title("Database Backup Sizes"); - dynamic seriesP1 = new PieSeries { StrokeThickness = 2.0, InsideLabelPosition = 0.8, AngleSpan = 360, StartAngle = 0 }; + var pieLabels = new List(); + var pieSizes = new List(); using (SqlConnection sourceConn = new SqlConnection(connectionString)) { @@ -1005,17 +895,22 @@ GROUP BY database_name var databaseName = reader.GetString(0); var backupSize = (double)reader.GetDecimal(1); - seriesP1.Slices.Add(new PieSlice(databaseName, backupSize) { IsExploded = true }); + pieLabels.Add(databaseName); + pieSizes.Add(backupSize); } } } } + if (pieSizes.Count > 0) + { + var pie = piePlot.AddPie(pieSizes.ToArray()); + pie.SliceLabels = pieLabels.ToArray(); + pie.Explode = true; + piePlot.Legend(true); + } - - PieModel.Series.Add(seriesP1); - - this.BackupSizeModel.Model = PieModel; + BackupSizeModel.Refresh(); } @@ -1063,19 +958,11 @@ ON sysjobhistory(job_id, step_id, run_date, run_time) WITH (DATA_COMPRESSION = PAGE, ONLINE = ON, MAXDOP = 4); */ "; - var MyModel = new PlotModel { Title = "Agent Jobs: Frequency and Durations Analysis" }; - - MyModel.Axes.Add(new DateTimeAxis - { - Position = AxisPosition.Bottom, - StringFormat = "dd-MM-yyyy HH:mm", - Title = "Date", - IntervalType = DateTimeIntervalType.Hours, - MinorIntervalType = DateTimeIntervalType.Minutes, - IntervalLength = 80, - IsZoomEnabled = false, - IsPanEnabled = false - }); + var jobsPlot = AgentJobsTimelineModel.Plot; + jobsPlot.Clear(); + jobsPlot.Title("Agent Jobs: Frequency and Durations Analysis"); + jobsPlot.XAxis.DateTimeFormat(true); + jobsPlot.XLabel("Date"); var customLabels = new Dictionary(); var jobIndex = new Dictionary(); @@ -1105,72 +992,40 @@ ON sysjobhistory(job_id, step_id, run_date, run_time) customLabels.Add(jIndex, jobName); - var startDate = DateTimeAxis.ToDouble(reader.GetDateTime(1)); - var finishDate = DateTimeAxis.ToDouble(reader.GetDateTime(2)); + var startDate = reader.GetDateTime(1).ToOADate(); + var finishDate = reader.GetDateTime(2).ToOADate(); var resultType = reader.GetInt32(3); - var LineColor = OxyColors.DeepPink; + var LineColor = Color.DeepPink; if (resultType == 0) // Failure - LineColor = OxyColors.Red; + LineColor = Color.Red; else if (resultType == 1) // Success - LineColor = OxyColors.Green; + LineColor = Color.Green; else if (resultType == 2) // ?? - LineColor = OxyColors.DeepPink; + LineColor = Color.DeepPink; else if (resultType == 3) // Stopped manually - LineColor = OxyColors.Purple; - - var scatterSeries = new ScatterSeries - { - MarkerType = OxyPlot.MarkerType.Circle, - MarkerFill = LineColor, - MarkerStrokeThickness = 1 - }; - - // Add two points to the scatter series - scatterSeries.Points.Add(new ScatterPoint(startDate, jIndex)); - scatterSeries.Points.Add(new ScatterPoint(finishDate, jIndex)); - - // Add a line series to connect the dots - var lineSeries = new LineSeries() - { - Color = LineColor, - StrokeThickness = 2, - LineStyle = LineStyle.Solid - }; - lineSeries.Points.Add(new DataPoint(startDate, jIndex)); - lineSeries.Points.Add(new DataPoint(finishDate, jIndex)); + LineColor = Color.Purple; - MyModel.Series.Add(lineSeries); - MyModel.Series.Add(scatterSeries); + var scatter = jobsPlot.AddScatter( + new double[] { startDate, finishDate }, + new double[] { jIndex, jIndex }, + color: LineColor, + markerShape: MarkerShape.filledCircle, + markerSize: 5, + lineWidth: 2); + scatter.MarkerLineWidth = 1; } } } } - var yAxis = new LinearAxis - { - Position = AxisPosition.Left, - Title = "Job Duration", - MajorStep = 1, - MinorStep = 1, - IsZoomEnabled = false, - IsPanEnabled = false, - LabelFormatter = value => - { - // Return the custom label if it exists; otherwise, return the default string representation - if (customLabels.TryGetValue(value, out var label)) - { - return label; - } - return value.ToString(); - } - }; - - MyModel.Axes.Add(yAxis); + jobsPlot.YLabel("Job Duration"); + jobsPlot.YTicks(customLabels.Keys.ToArray(), customLabels.Values.ToArray()); + jobsPlot.SetAxisLimits(yMin: -0.5, yMax: customLabels.Count + 0.5); - this.AgentJobsTimelineModel.Model = MyModel; + AgentJobsTimelineModel.Refresh(); } From dad2c3c23702b8c139420e36467d2934479a7964 Mon Sep 17 00:00:00 2001 From: Alex Bochkov Date: Mon, 24 Nov 2025 13:18:33 -0700 Subject: [PATCH 2/5] Upgrade ScottPlot.WPF to v5 --- AxialSqlTools/AxialSqlTools.csproj | 2 +- .../HealthDashboard_ServerControl.xaml.cs | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/AxialSqlTools/AxialSqlTools.csproj b/AxialSqlTools/AxialSqlTools.csproj index 88fd96c..d57dfaa 100644 --- a/AxialSqlTools/AxialSqlTools.csproj +++ b/AxialSqlTools/AxialSqlTools.csproj @@ -241,7 +241,7 @@ 14.0.0 - 4.1.73 + 5.1.57 4.3.4 diff --git a/AxialSqlTools/HealthDashboards/HealthDashboard_ServerControl.xaml.cs b/AxialSqlTools/HealthDashboards/HealthDashboard_ServerControl.xaml.cs index 4917c5a..81774bf 100644 --- a/AxialSqlTools/HealthDashboards/HealthDashboard_ServerControl.xaml.cs +++ b/AxialSqlTools/HealthDashboards/HealthDashboard_ServerControl.xaml.cs @@ -23,7 +23,7 @@ using DocumentFormat.OpenXml.Spreadsheet; using static AxialSqlTools.HealthDashboard_ServerControl; using ScottPlot; - using ScottPlot.Plottable; + using ScottPlot.Plottables; using ScottPlot.WPF; using ScottPlot.Palettes; using Color = System.Drawing.Color; @@ -438,10 +438,10 @@ public void UpdateUI(int i, HealthDashboardServerMetric metrics, bool doEmpty) usedBars.PositionOffset = -offset; freeBars.PositionOffset = offset; - diskPlot.YTicks(positions, labels); + diskPlot.Axes.Left.SetTicks(positions, labels); diskPlot.XLabel("Gb"); diskPlot.SetAxisLimits(xMin: 0); - diskPlot.Legend(location: Alignment.UpperRight); + diskPlot.ShowLegend(Alignment.UpperRight); DiskInfoModel.Refresh(); @@ -543,9 +543,9 @@ private void UpdateWaitStatsGraph(List previousWaitStats, Dictionary< waitPlot.Title("Real-time Wait Stats"); double[] positions = Enumerable.Range(0, sortedKeys.Count).Select(i => (double)i).ToArray(); - waitPlot.YTicks(positions, sortedKeys.Select(k => k.ToString("HH:mm")).ToArray()); + waitPlot.Axes.Left.SetTicks(positions, sortedKeys.Select(k => k.ToString("HH:mm")).ToArray()); waitPlot.SetAxisLimits(xMin: 0); - waitPlot.Legend(true); + waitPlot.ShowLegend(); var palette = new Category10(); double offsetStep = 0.1; @@ -850,7 +850,7 @@ ORDER BY } backupTimelinePlot.YLabel("Backup Duration"); - backupTimelinePlot.YTicks(customLabels.Keys.ToArray(), customLabels.Values.ToArray()); + backupTimelinePlot.Axes.Left.SetTicks(customLabels.Keys.ToArray(), customLabels.Values.ToArray()); backupTimelinePlot.SetAxisLimits(yMin: -0.5, yMax: customLabels.Count + 0.5); BackupTimelineModel.Refresh(); @@ -907,7 +907,7 @@ GROUP BY database_name var pie = piePlot.AddPie(pieSizes.ToArray()); pie.SliceLabels = pieLabels.ToArray(); pie.Explode = true; - piePlot.Legend(true); + piePlot.ShowLegend(); } BackupSizeModel.Refresh(); @@ -1022,7 +1022,7 @@ ON sysjobhistory(job_id, step_id, run_date, run_time) } jobsPlot.YLabel("Job Duration"); - jobsPlot.YTicks(customLabels.Keys.ToArray(), customLabels.Values.ToArray()); + jobsPlot.Axes.Left.SetTicks(customLabels.Keys.ToArray(), customLabels.Values.ToArray()); jobsPlot.SetAxisLimits(yMin: -0.5, yMax: customLabels.Count + 0.5); AgentJobsTimelineModel.Refresh(); From 11bc665b1d81642971ec3bd323e5bc701a13d646 Mon Sep 17 00:00:00 2001 From: Alex Bochkov Date: Wed, 26 Nov 2025 10:30:05 -0700 Subject: [PATCH 3/5] wip --- AxialSqlTools/AxialSqlTools.csproj | 6 + .../HealthDashboard_ServerControl.xaml.cs | 177 +++++++++--------- 2 files changed, 95 insertions(+), 88 deletions(-) diff --git a/AxialSqlTools/AxialSqlTools.csproj b/AxialSqlTools/AxialSqlTools.csproj index d57dfaa..7b9977d 100644 --- a/AxialSqlTools/AxialSqlTools.csproj +++ b/AxialSqlTools/AxialSqlTools.csproj @@ -243,6 +243,12 @@ 5.1.57 + + 3.119.1 + + + 3.119.1 + 4.3.4 diff --git a/AxialSqlTools/HealthDashboards/HealthDashboard_ServerControl.xaml.cs b/AxialSqlTools/HealthDashboards/HealthDashboard_ServerControl.xaml.cs index 81774bf..f17c093 100644 --- a/AxialSqlTools/HealthDashboards/HealthDashboard_ServerControl.xaml.cs +++ b/AxialSqlTools/HealthDashboards/HealthDashboard_ServerControl.xaml.cs @@ -409,41 +409,41 @@ public void UpdateUI(int i, HealthDashboardServerMetric metrics, bool doEmpty) waitsStatsAggregator.UpdateWaitStats(metrics.WaitStatsInfo); UpdateWaitStatsGraph(waitsStatsAggregator.previousWaitStats, waitsStatsAggregator.GetAggregatedData()); - //-------------------------------------------------------------------- - // Disk info graph - var diskPlot = DiskInfoModel.Plot; - diskPlot.Clear(); - diskPlot.Title("Volume(s) Utilization"); - - double[] positions = Enumerable.Range(0, metrics.DisksInfo.Count).Select(i => (double)i).ToArray(); - double[] usedValues = metrics.DisksInfo.Select(disk => disk.UsedSpaceGb).ToArray(); - double[] freeValues = metrics.DisksInfo.Select(disk => disk.FreeSpaceGb).ToArray(); - string[] labels = metrics.DisksInfo.Select(disk => disk.VolumeDescription).ToArray(); - - var usedBars = diskPlot.AddBar(usedValues, positions); - usedBars.Horizontal = true; - usedBars.Label = "Used"; - usedBars.FillColor = Color.LightPink; - usedBars.BorderColor = Color.Black; - usedBars.ValueFormatter = x => $"{x:0} Gb"; - - var freeBars = diskPlot.AddBar(freeValues, positions); - freeBars.Horizontal = true; - freeBars.Label = "Free"; - freeBars.FillColor = Color.LightBlue; - freeBars.BorderColor = Color.Black; - freeBars.ValueFormatter = x => $"{x:0} Gb"; - - double offset = usedBars.BarWidth / 2; - usedBars.PositionOffset = -offset; - freeBars.PositionOffset = offset; - - diskPlot.Axes.Left.SetTicks(positions, labels); - diskPlot.XLabel("Gb"); - diskPlot.SetAxisLimits(xMin: 0); - diskPlot.ShowLegend(Alignment.UpperRight); - - DiskInfoModel.Refresh(); + ////-------------------------------------------------------------------- + //// Disk info graph + //var diskPlot = DiskInfoModel.Plot; + //diskPlot.Clear(); + //diskPlot.Title("Volume(s) Utilization"); + // + //double[] positions = Enumerable.Range(0, metrics.DisksInfo.Count).Select(ii => (double)ii).ToArray(); + //long[] usedValues = metrics.DisksInfo.Select(disk => disk.UsedSpaceGb).ToArray(); + //long[] freeValues = metrics.DisksInfo.Select(disk => disk.FreeSpaceGb).ToArray(); + //string[] labels = metrics.DisksInfo.Select(disk => disk.VolumeDescription).ToArray(); + // + //var usedBars = diskPlot.AddBar(usedValues, positions); + //usedBars.Horizontal = true; + //usedBars.Label = "Used"; + //usedBars.FillColor = Color.LightPink; + //usedBars.BorderColor = Color.Black; + //usedBars.ValueFormatter = x => $"{x:0} Gb"; + // + //var freeBars = diskPlot.AddBar(freeValues, positions); + //freeBars.Horizontal = true; + //freeBars.Label = "Free"; + //freeBars.FillColor = Color.LightBlue; + //freeBars.BorderColor = Color.Black; + //freeBars.ValueFormatter = x => $"{x:0} Gb"; + // + //double offset = usedBars.BarWidth / 2; + //usedBars.PositionOffset = -offset; + //freeBars.PositionOffset = offset; + // + //diskPlot.Axes.Left.SetTicks(positions, labels); + //diskPlot.XLabel("Gb"); + //diskPlot.SetAxisLimits(xMin: 0); + //diskPlot.ShowLegend(Alignment.UpperRight); + // + //DiskInfoModel.Refresh(); //-------------------------------------------------------------------- @@ -537,38 +537,38 @@ public void UpdateUI(int i, HealthDashboardServerMetric metrics, bool doEmpty) private void UpdateWaitStatsGraph(List previousWaitStats, Dictionary> aggrData) { - var sortedKeys = aggrData.Keys.OrderBy(k => k).ToList(); - var waitPlot = WaitStatsModel.Plot; - waitPlot.Clear(); - waitPlot.Title("Real-time Wait Stats"); - - double[] positions = Enumerable.Range(0, sortedKeys.Count).Select(i => (double)i).ToArray(); - waitPlot.Axes.Left.SetTicks(positions, sortedKeys.Select(k => k.ToString("HH:mm")).ToArray()); - waitPlot.SetAxisLimits(xMin: 0); - waitPlot.ShowLegend(); - - var palette = new Category10(); - double offsetStep = 0.1; - int seriesIndex = 0; - - foreach (var previousWaitStat in previousWaitStats) - { - double[] values = sortedKeys - .Select(key => aggrData[key].FirstOrDefault(ws => ws.WaitName == previousWaitStat.WaitName)?.WaitSec ?? 0) - .Select(v => (double)v) - .ToArray(); - - var barSeries = waitPlot.AddBar(values, positions); - barSeries.Horizontal = true; - barSeries.Label = previousWaitStat.WaitName; - barSeries.FillColor = palette.GetColor(seriesIndex % palette.Count); - barSeries.BorderColor = Color.Black; - barSeries.PositionOffset = (seriesIndex - (previousWaitStats.Count / 2.0)) * offsetStep; - - seriesIndex++; - } - - WaitStatsModel.Refresh(); + //var sortedKeys = aggrData.Keys.OrderBy(k => k).ToList(); + //var waitPlot = WaitStatsModel.Plot; + //waitPlot.Clear(); + //waitPlot.Title("Real-time Wait Stats"); + // + //double[] positions = Enumerable.Range(0, sortedKeys.Count).Select(i => (double)i).ToArray(); + //waitPlot.Axes.Left.SetTicks(positions, sortedKeys.Select(k => k.ToString("HH:mm")).ToArray()); + //waitPlot.SetAxisLimits(xMin: 0); + //waitPlot.ShowLegend(); + // + //var palette = new Category10(); + //double offsetStep = 0.1; + //int seriesIndex = 0; + // + //foreach (var previousWaitStat in previousWaitStats) + //{ + // double[] values = sortedKeys + // .Select(key => aggrData[key].FirstOrDefault(ws => ws.WaitName == previousWaitStat.WaitName)?.WaitSec ?? 0) + // .Select(v => (double)v) + // .ToArray(); + // + // var barSeries = waitPlot.AddBar(values, positions); + // barSeries.Horizontal = true; + // barSeries.Label = previousWaitStat.WaitName; + // barSeries.FillColor = palette.GetColor(seriesIndex % palette.Count); + // barSeries.BorderColor = Color.Black; + // barSeries.PositionOffset = (seriesIndex - (previousWaitStats.Count / 2.0)) * offsetStep; + // + // seriesIndex++; + //} + // + //WaitStatsModel.Refresh(); } private void AddPerformanceSample(HealthDashboardServerMetric metrics) @@ -600,22 +600,22 @@ private void AddPerformanceSample(HealthDashboardServerMetric metrics) private void UpdateTimeSeriesPlot(WpfPlot targetPlot, string title, string yAxisTitle, Func valueSelector, bool clampToZero = true) { - var orderedSamples = _performanceSamples.OrderBy(s => s.Timestamp).ToList(); - - double[] xs = orderedSamples.Select(s => s.Timestamp.ToOADate()).ToArray(); - double[] ys = orderedSamples.Select(valueSelector).ToArray(); - - var plt = targetPlot.Plot; - plt.Clear(); - plt.Title(title); - plt.XAxis.DateTimeFormat(true); - plt.YLabel(yAxisTitle); - plt.AddScatter(xs, ys, markerSize: 2, lineWidth: 2); - - if (clampToZero) - plt.SetAxisLimits(yMin: 0); - - targetPlot.Refresh(); + //var orderedSamples = _performanceSamples.OrderBy(s => s.Timestamp).ToList(); + // + //double[] xs = orderedSamples.Select(s => s.Timestamp.ToOADate()).ToArray(); + //double[] ys = orderedSamples.Select(valueSelector).ToArray(); + // + //var plt = targetPlot.Plot; + //plt.Clear(); + //plt.Title(title); + //plt.XAxis.DateTimeFormat(true); + //plt.YLabel(yAxisTitle); + //plt.AddScatter(xs, ys, markerSize: 2, lineWidth: 2); + // + //if (clampToZero) + // plt.SetAxisLimits(yMin: 0); + // + //targetPlot.Refresh(); } private void UpdatePerformanceCharts() @@ -760,7 +760,7 @@ private void buttonDetailedBackupInfo_Click(object sender, RoutedEventArgs e) private void BackupTimelineModelRefresh_Click(object sender, RoutedEventArgs e) { - + /* //var ci = ScriptFactoryAccess.GetCurrentConnectionInfo(); int BackupHistoryPeriod = 1; @@ -911,12 +911,13 @@ GROUP BY database_name } BackupSizeModel.Refresh(); + */ } private void AgentJobsTimelineModelRefresh_Click(object sender, RoutedEventArgs e) { - + /* int AgentJobHistoryPeriod = 1; //TODO int.TryParse(AgentJobsTimelinePeriodNumberTextBox.Text, out AgentJobHistoryPeriod); @@ -953,9 +954,9 @@ FROM msdb.dbo.sysjobs j ORDER BY StartTime DESC; - /* CREATE INDEX IDX_sysjobhistory_1 + / * CREATE INDEX IDX_sysjobhistory_1 ON sysjobhistory(job_id, step_id, run_date, run_time) - WITH (DATA_COMPRESSION = PAGE, ONLINE = ON, MAXDOP = 4); */ + WITH (DATA_COMPRESSION = PAGE, ONLINE = ON, MAXDOP = 4); * / "; var jobsPlot = AgentJobsTimelineModel.Plot; @@ -1026,7 +1027,7 @@ ON sysjobhistory(job_id, step_id, run_date, run_time) jobsPlot.SetAxisLimits(yMin: -0.5, yMax: customLabels.Count + 0.5); AgentJobsTimelineModel.Refresh(); - + */ } From b7acf3f0cf41ed87b47e11fd55c62bbb2962eb62 Mon Sep 17 00:00:00 2001 From: Alex Bochkov Date: Wed, 26 Nov 2025 12:35:08 -0700 Subject: [PATCH 4/5] wip --- AxialSqlTools/AxialSqlTools.csproj | 3 -- AxialSqlTools/AxialSqlToolsPackage.cs | 11 +++++++ .../HealthDashboard_ServerControl.xaml.cs | 33 ++++++++++--------- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/AxialSqlTools/AxialSqlTools.csproj b/AxialSqlTools/AxialSqlTools.csproj index 7b9977d..6ccfdf1 100644 --- a/AxialSqlTools/AxialSqlTools.csproj +++ b/AxialSqlTools/AxialSqlTools.csproj @@ -243,9 +243,6 @@ 5.1.57 - - 3.119.1 - 3.119.1 diff --git a/AxialSqlTools/AxialSqlToolsPackage.cs b/AxialSqlTools/AxialSqlToolsPackage.cs index 7bf31f5..bef36e3 100644 --- a/AxialSqlTools/AxialSqlToolsPackage.cs +++ b/AxialSqlTools/AxialSqlToolsPackage.cs @@ -406,6 +406,17 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke _logger.Error(ex, "An exception occurred"); } + AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve); + + } + + private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) + { + // add this into main module -> AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve); + + if (args.Name.Contains("OxyPlot") || args.Name.Contains("SkiaSharp")) + return AppDomain.CurrentDomain.Load(args.Name); + else return null; } #endregion diff --git a/AxialSqlTools/HealthDashboards/HealthDashboard_ServerControl.xaml.cs b/AxialSqlTools/HealthDashboards/HealthDashboard_ServerControl.xaml.cs index f17c093..f540962 100644 --- a/AxialSqlTools/HealthDashboards/HealthDashboard_ServerControl.xaml.cs +++ b/AxialSqlTools/HealthDashboards/HealthDashboard_ServerControl.xaml.cs @@ -561,7 +561,7 @@ private void UpdateWaitStatsGraph(List previousWaitStats, Dictionary< // var barSeries = waitPlot.AddBar(values, positions); // barSeries.Horizontal = true; // barSeries.Label = previousWaitStat.WaitName; - // barSeries.FillColor = palette.GetColor(seriesIndex % palette.Count); + // barSeries.FillColor = palette.GetColor(seriesIndex % palette.Count()); // barSeries.BorderColor = Color.Black; // barSeries.PositionOffset = (seriesIndex - (previousWaitStats.Count / 2.0)) * offsetStep; // @@ -600,22 +600,23 @@ private void AddPerformanceSample(HealthDashboardServerMetric metrics) private void UpdateTimeSeriesPlot(WpfPlot targetPlot, string title, string yAxisTitle, Func valueSelector, bool clampToZero = true) { - //var orderedSamples = _performanceSamples.OrderBy(s => s.Timestamp).ToList(); - // - //double[] xs = orderedSamples.Select(s => s.Timestamp.ToOADate()).ToArray(); - //double[] ys = orderedSamples.Select(valueSelector).ToArray(); - // - //var plt = targetPlot.Plot; - //plt.Clear(); - //plt.Title(title); - //plt.XAxis.DateTimeFormat(true); - //plt.YLabel(yAxisTitle); - //plt.AddScatter(xs, ys, markerSize: 2, lineWidth: 2); - // + var orderedSamples = _performanceSamples.OrderBy(s => s.Timestamp).ToList(); + + double[] xs = orderedSamples.Select(s => s.Timestamp.ToOADate()).ToArray(); + double[] ys = orderedSamples.Select(valueSelector).ToArray(); + + var plt = targetPlot.Plot; + plt.Clear(); + plt.Title(title); + // plt.XAxis.DateTimeFormat(true); + plt.YLabel(yAxisTitle); + // plt.AddScatter(xs, ys, markerSize: 2, lineWidth: 2); + plt.Add.Scatter(xs, ys); + //if (clampToZero) - // plt.SetAxisLimits(yMin: 0); - // - //targetPlot.Refresh(); + // plt.Axes.SetLimits(yMin: 0); + + targetPlot.Refresh(); } private void UpdatePerformanceCharts() From 960075d57269e7c390f20fa48cd11bd3375059e2 Mon Sep 17 00:00:00 2001 From: Alex Bochkov Date: Wed, 26 Nov 2025 13:08:54 -0700 Subject: [PATCH 5/5] got something working --- .../HealthDashboard_ServerControl.xaml.cs | 321 +++++++++++++----- 1 file changed, 231 insertions(+), 90 deletions(-) diff --git a/AxialSqlTools/HealthDashboards/HealthDashboard_ServerControl.xaml.cs b/AxialSqlTools/HealthDashboards/HealthDashboard_ServerControl.xaml.cs index f540962..5a0ab40 100644 --- a/AxialSqlTools/HealthDashboards/HealthDashboard_ServerControl.xaml.cs +++ b/AxialSqlTools/HealthDashboards/HealthDashboard_ServerControl.xaml.cs @@ -1,31 +1,33 @@ namespace AxialSqlTools { + using DocumentFormat.OpenXml.Bibliography; + using DocumentFormat.OpenXml.Spreadsheet; + using Microsoft.Identity.Client; using Microsoft.SqlServer.Management.Smo.RegSvrEnum; using Microsoft.SqlServer.Management.UI.VSIntegration; + using Microsoft.SqlServer.Management.UI.VSIntegration.Editors; using Microsoft.VisualStudio.Shell; + using ScottPlot; + using ScottPlot.Palettes; + using ScottPlot.Plottables; + using ScottPlot.TickGenerators; + using ScottPlot.WPF; using System; + using System.Collections.Generic; using System.Data.SqlClient; + using System.Diagnostics; using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Net.Http; using System.Threading; using System.Threading.Tasks; using System.Windows; - using System.Windows.Media; using System.Windows.Controls; - using Microsoft.SqlServer.Management.UI.VSIntegration.Editors; - using System.Net.Http; - using System.Collections.Generic; using System.Windows.Input; - using System.Linq; - using static HealthDashboardServerMetric; - using System.Diagnostics; + using System.Windows.Media; using static AxialSqlTools.AxialSqlToolsPackage; - using DocumentFormat.OpenXml.Bibliography; - using DocumentFormat.OpenXml.Spreadsheet; using static AxialSqlTools.HealthDashboard_ServerControl; - using ScottPlot; - using ScottPlot.Plottables; - using ScottPlot.WPF; - using ScottPlot.Palettes; + using static HealthDashboardServerMetric; using Color = System.Drawing.Color; /// @@ -409,41 +411,7 @@ public void UpdateUI(int i, HealthDashboardServerMetric metrics, bool doEmpty) waitsStatsAggregator.UpdateWaitStats(metrics.WaitStatsInfo); UpdateWaitStatsGraph(waitsStatsAggregator.previousWaitStats, waitsStatsAggregator.GetAggregatedData()); - ////-------------------------------------------------------------------- - //// Disk info graph - //var diskPlot = DiskInfoModel.Plot; - //diskPlot.Clear(); - //diskPlot.Title("Volume(s) Utilization"); - // - //double[] positions = Enumerable.Range(0, metrics.DisksInfo.Count).Select(ii => (double)ii).ToArray(); - //long[] usedValues = metrics.DisksInfo.Select(disk => disk.UsedSpaceGb).ToArray(); - //long[] freeValues = metrics.DisksInfo.Select(disk => disk.FreeSpaceGb).ToArray(); - //string[] labels = metrics.DisksInfo.Select(disk => disk.VolumeDescription).ToArray(); - // - //var usedBars = diskPlot.AddBar(usedValues, positions); - //usedBars.Horizontal = true; - //usedBars.Label = "Used"; - //usedBars.FillColor = Color.LightPink; - //usedBars.BorderColor = Color.Black; - //usedBars.ValueFormatter = x => $"{x:0} Gb"; - // - //var freeBars = diskPlot.AddBar(freeValues, positions); - //freeBars.Horizontal = true; - //freeBars.Label = "Free"; - //freeBars.FillColor = Color.LightBlue; - //freeBars.BorderColor = Color.Black; - //freeBars.ValueFormatter = x => $"{x:0} Gb"; - // - //double offset = usedBars.BarWidth / 2; - //usedBars.PositionOffset = -offset; - //freeBars.PositionOffset = offset; - // - //diskPlot.Axes.Left.SetTicks(positions, labels); - //diskPlot.XLabel("Gb"); - //diskPlot.SetAxisLimits(xMin: 0); - //diskPlot.ShowLegend(Alignment.UpperRight); - // - //DiskInfoModel.Refresh(); + UpdateDiskSpaceGraph(metrics); //-------------------------------------------------------------------- @@ -534,41 +502,194 @@ public void UpdateUI(int i, HealthDashboardServerMetric metrics, bool doEmpty) } + private void UpdateDiskSpaceGraph(HealthDashboardServerMetric metrics) + { + var diskPlot = DiskInfoModel.Plot; + diskPlot.Clear(); + diskPlot.Title("Volume(s) Utilization"); + + int count = metrics.DisksInfo.Count; + if (count == 0) + { + DiskInfoModel.Refresh(); + return; + } + + // group positions (one group per volume) + double[] groupPositions = Enumerable.Range(0, count) + .Select(i => (double)i) + .ToArray(); + + double[] usedValues = metrics.DisksInfo + .Select(disk => (double)disk.UsedSpaceGb) + .ToArray(); + + double[] freeValues = metrics.DisksInfo + .Select(disk => (double)disk.FreeSpaceGb) + .ToArray(); + + string[] labels = metrics.DisksInfo + .Select(disk => disk.VolumeDescription) + .ToArray(); + + // create grouped bars by offsetting positions, as in the Bar Positioning example + double groupOffset = 0.18; // half the distance between used/free inside a group + + List usedBarsList = new List(); + List freeBarsList = new List(); + + for (int i = 0; i < count; i++) + { + double center = groupPositions[i]; + + usedBarsList.Add(new ScottPlot.Bar + { + Position = center - groupOffset, + Value = usedValues[i], + FillColor = ScottPlot.Colors.Pink + }); + + freeBarsList.Add(new ScottPlot.Bar + { + Position = center + groupOffset, + Value = freeValues[i], + FillColor = ScottPlot.Colors.LightBlue + }); + } + + // add bar series + var usedBars = diskPlot.Add.Bars(usedBarsList); + usedBars.Horizontal = true; + usedBars.LegendText = "Used"; + + var freeBars = diskPlot.Add.Bars(freeBarsList); + freeBars.Horizontal = true; + freeBars.LegendText = "Free"; + + // style borders per bar (v5 styles each bar individually) + foreach (var bar in usedBars.Bars) + { + bar.LineWidth = 1; + bar.LineColor = ScottPlot.Colors.Black; + bar.Label = $"{bar.Value:0} Gb"; // optional value label + } + + foreach (var bar in freeBars.Bars) + { + bar.LineWidth = 1; + bar.LineColor = ScottPlot.Colors.Black; + bar.Label = $"{bar.Value:0} Gb"; // optional value label + } + + // style label text (borrowed from Bar with Value Labels examples) + usedBars.ValueLabelStyle.FontSize = 10; + freeBars.ValueLabelStyle.FontSize = 10; + + // horizontal bar layout: left axis has volume names + diskPlot.Axes.Left.SetTicks(groupPositions, labels); + + diskPlot.XLabel("Gb"); + + // remove padding on the left so bars start at the axis, like Horizontal Bar example + diskPlot.Axes.Margins(left: 0); + + // autoscale then clamp X to start at 0 + diskPlot.Axes.AutoScale(); + var limits = diskPlot.Axes.GetLimits(); + diskPlot.Axes.SetLimitsX(0, limits.Right); + + // legend (v5 style) + diskPlot.ShowLegend(ScottPlot.Alignment.UpperRight); + + DiskInfoModel.Refresh(); + + + } private void UpdateWaitStatsGraph(List previousWaitStats, Dictionary> aggrData) { - //var sortedKeys = aggrData.Keys.OrderBy(k => k).ToList(); - //var waitPlot = WaitStatsModel.Plot; - //waitPlot.Clear(); - //waitPlot.Title("Real-time Wait Stats"); - // - //double[] positions = Enumerable.Range(0, sortedKeys.Count).Select(i => (double)i).ToArray(); - //waitPlot.Axes.Left.SetTicks(positions, sortedKeys.Select(k => k.ToString("HH:mm")).ToArray()); - //waitPlot.SetAxisLimits(xMin: 0); - //waitPlot.ShowLegend(); - // - //var palette = new Category10(); - //double offsetStep = 0.1; - //int seriesIndex = 0; - // - //foreach (var previousWaitStat in previousWaitStats) - //{ - // double[] values = sortedKeys - // .Select(key => aggrData[key].FirstOrDefault(ws => ws.WaitName == previousWaitStat.WaitName)?.WaitSec ?? 0) - // .Select(v => (double)v) - // .ToArray(); - // - // var barSeries = waitPlot.AddBar(values, positions); - // barSeries.Horizontal = true; - // barSeries.Label = previousWaitStat.WaitName; - // barSeries.FillColor = palette.GetColor(seriesIndex % palette.Count()); - // barSeries.BorderColor = Color.Black; - // barSeries.PositionOffset = (seriesIndex - (previousWaitStats.Count / 2.0)) * offsetStep; - // - // seriesIndex++; - //} - // - //WaitStatsModel.Refresh(); + var plot = WaitStatsModel.Plot; + + plot.Clear(); + + // sort timestamps + var sortedKeys = aggrData.Keys.OrderBy(k => k).ToList(); + + // positions along the vertical axis (since we will use Horizontal = true) + double[] positions = Enumerable.Range(0, sortedKeys.Count) + .Select(i => (double)i) + .ToArray(); + + // time labels on the left axis + string[] timeLabels = sortedKeys + .Select(k => k.ToString("HH:mm")) + .ToArray(); + + plot.Axes.Left.SetTicks(positions, timeLabels); + plot.Axes.Margins(left: 0, bottom: 0, top: 0.2); + + // palette for wait types + var palette = new Category10(); + + // build legend manually - one item per wait type + plot.Legend.IsVisible = true; + plot.Legend.Orientation = ScottPlot.Orientation.Horizontal; + plot.Legend.Alignment = ScottPlot.Alignment.UpperRight; + + plot.Legend.ManualItems.Clear(); + for (int i = 0; i < previousWaitStats.Count; i++) + { + var wait = previousWaitStats[i]; + var color = palette.GetColor(i % palette.Count()); + + plot.Legend.ManualItems.Add(new LegendItem + { + LabelText = wait.WaitName, + FillColor = color + }); + } + + // for each timestamp, create a horizontal stack of bars + for (int t = 0; t < sortedKeys.Count; t++) + { + DateTime key = sortedKeys[t]; + var waitsAtTime = aggrData[key]; + + // one stacked group (all wait types) at this time position + List bars = new List(); + double valueBase = 0; + + for (int s = 0; s < previousWaitStats.Count; s++) + { + string waitName = previousWaitStats[s].WaitName; + + double value = (double)(waitsAtTime.FirstOrDefault(ws => ws.WaitName == waitName)?.WaitSec ?? 0); + + if (value <= 0) + continue; + + double top = valueBase + value; + + var bar = new ScottPlot.Bar + { + Position = positions[t], // vertical position (y) when Horizontal = true + ValueBase = valueBase, // start of this segment + Value = top, // end of this segment + FillColor = palette.GetColor(s % palette.Count()) + }; + + bars.Add(bar); + valueBase = top; + } + + if (bars.Count > 0) + { + var barPlot = plot.Add.Bars(bars); + barPlot.Horizontal = true; // make it a horizontal stacked bar + } + } + + WaitStatsModel.Refresh(); } private void AddPerformanceSample(HealthDashboardServerMetric metrics) @@ -600,21 +721,41 @@ private void AddPerformanceSample(HealthDashboardServerMetric metrics) private void UpdateTimeSeriesPlot(WpfPlot targetPlot, string title, string yAxisTitle, Func valueSelector, bool clampToZero = true) { + // order samples by time var orderedSamples = _performanceSamples.OrderBy(s => s.Timestamp).ToList(); - - double[] xs = orderedSamples.Select(s => s.Timestamp.ToOADate()).ToArray(); + + // ScottPlot 5 can take DateTime[] directly for X + DateTime[] xs = orderedSamples.Select(s => s.Timestamp).ToArray(); + double[] ys = orderedSamples.Select(valueSelector).ToArray(); - + var plt = targetPlot.Plot; plt.Clear(); - plt.Title(title); - // plt.XAxis.DateTimeFormat(true); + + // title and axis labels helpers (ScottPlot 5 quickstart) :contentReference[oaicite:1]{index=1} + plt.Title(title); plt.YLabel(yAxisTitle); - // plt.AddScatter(xs, ys, markerSize: 2, lineWidth: 2); - plt.Add.Scatter(xs, ys); - - //if (clampToZero) - // plt.Axes.SetLimits(yMin: 0); + // optional, but usually useful + plt.XLabel("Time"); + + // use a DateTime bottom axis (ScottPlot 5 DateTime axis quickstart) :contentReference[oaicite:2]{index=2} + var dtAxis = plt.Axes.DateTimeTicksBottom(); + var autoTicks = (ScottPlot.TickGenerators.DateTimeAutomatic)dtAxis.TickGenerator; + autoTicks.LabelFormatter = dt => dt.ToString("HH:mm"); + + // scatter with DateTime X values (ScottPlot 5 scatter examples) :contentReference[oaicite:3]{index=3} + var scatter = plt.Add.Scatter(xs, ys); + scatter.MarkerSize = 2; + scatter.LineWidth = 2; + + // autoscale to fit data, then clamp Y to zero if requested + plt.Axes.AutoScale(); + + if (clampToZero) + { + var limits = plt.Axes.GetLimits(); + plt.Axes.SetLimitsY(bottom: 0, top: limits.Top); + } targetPlot.Refresh(); }