diff --git a/Dashboard/ServerTab.xaml.cs b/Dashboard/ServerTab.xaml.cs
index 29fe02b0..027631bc 100644
--- a/Dashboard/ServerTab.xaml.cs
+++ b/Dashboard/ServerTab.xaml.cs
@@ -457,17 +457,21 @@ private async void ServerTab_KeyDown(object sender, System.Windows.Input.KeyEven
private void SetupChartContextMenus()
{
// Resource Overview charts
- SetupChartSaveMenu(ResourceOverviewCpuChart, "CPU_Utilization", "collect.cpu_utilization_stats");
- SetupChartSaveMenu(ResourceOverviewMemoryChart, "Memory_Utilization", "collect.memory_stats");
- SetupChartSaveMenu(ResourceOverviewIoChart, "IO_Latency", "collect.file_io_stats");
- SetupChartSaveMenu(ResourceOverviewWaitChart, "Wait_Stats", "collect.wait_stats");
+ Helpers.TabHelpers.SetupChartContextMenu(ResourceOverviewCpuChart, "CPU_Utilization", "collect.cpu_utilization_stats");
+ Helpers.TabHelpers.SetupChartContextMenu(ResourceOverviewMemoryChart, "Memory_Utilization", "collect.memory_stats");
+ Helpers.TabHelpers.SetupChartContextMenu(ResourceOverviewIoChart, "IO_Latency", "collect.file_io_stats");
+ Helpers.TabHelpers.SetupChartContextMenu(ResourceOverviewWaitChart, "Wait_Stats", "collect.wait_stats");
// Blocking Stats charts
- SetupChartSaveMenu(LockWaitStatsChart, "Lock_Wait_Stats", "collect.wait_stats");
- SetupChartSaveMenu(BlockingStatsBlockingEventsChart, "Blocking_Events", "collect.blocking_deadlock_stats");
- SetupChartSaveMenu(BlockingStatsDurationChart, "Blocking_Duration", "collect.blocking_deadlock_stats");
- SetupChartSaveMenu(BlockingStatsDeadlocksChart, "Deadlocks", "collect.blocking_deadlock_stats");
- SetupChartSaveMenu(BlockingStatsDeadlockWaitTimeChart, "Deadlock_Wait_Time", "collect.blocking_deadlock_stats");
+ Helpers.TabHelpers.SetupChartContextMenu(LockWaitStatsChart, "Lock_Wait_Stats", "collect.wait_stats");
+ Helpers.TabHelpers.SetupChartContextMenu(BlockingStatsBlockingEventsChart, "Blocking_Events", "collect.blocking_deadlock_stats");
+ Helpers.TabHelpers.SetupChartContextMenu(BlockingStatsDurationChart, "Blocking_Duration", "collect.blocking_deadlock_stats");
+ Helpers.TabHelpers.SetupChartContextMenu(BlockingStatsDeadlocksChart, "Deadlocks", "collect.blocking_deadlock_stats");
+ Helpers.TabHelpers.SetupChartContextMenu(BlockingStatsDeadlockWaitTimeChart, "Deadlock_Wait_Time", "collect.blocking_deadlock_stats");
+
+ // Current Waits charts
+ Helpers.TabHelpers.SetupChartContextMenu(CurrentWaitsDurationChart, "Current_Waits_Duration", "collect.waiting_tasks");
+ Helpers.TabHelpers.SetupChartContextMenu(CurrentWaitsBlockedChart, "Current_Waits_Blocked", "collect.waiting_tasks");
// Query Performance Trends charts now handled by QueryPerformanceContent UserControl
@@ -477,279 +481,6 @@ private void SetupChartContextMenus()
// Memory Analysis charts now handled by MemoryContent UserControl
}
- private void SetupChartSaveMenu(WpfPlot chart, string chartName, string? dataSource = null)
- {
- // Create native WPF context menu (simpler - no zoom items since double-click handles reset)
- var contextMenu = new ContextMenu();
-
- var copyItem = new MenuItem { Header = "Copy Image", Icon = new TextBlock { Text = "📋" } };
- copyItem.Click += (s, e) =>
- {
- var tempFile = Path.Combine(Path.GetTempPath(), $"chart_copy_{Guid.NewGuid()}.png");
- try
- {
- chart.Plot.SavePng(tempFile, (int)chart.ActualWidth, (int)chart.ActualHeight);
- var bitmap = new System.Windows.Media.Imaging.BitmapImage();
- bitmap.BeginInit();
- bitmap.CacheOption = System.Windows.Media.Imaging.BitmapCacheOption.OnLoad;
- bitmap.UriSource = new Uri(tempFile);
- bitmap.EndInit();
- bitmap.Freeze();
- /* Use SetDataObject with copy=false to avoid WPF's problematic Clipboard.Flush() */
- Clipboard.SetDataObject(new System.Windows.DataObject(System.Windows.DataFormats.Bitmap, bitmap), false);
- }
- finally
- {
- if (File.Exists(tempFile)) File.Delete(tempFile);
- }
- };
- contextMenu.Items.Add(copyItem);
-
- var saveItem = new MenuItem { Header = "Save Image As...", Icon = new TextBlock { Text = "💾" } };
- saveItem.Click += (s, e) =>
- {
- var timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss", CultureInfo.InvariantCulture);
- var defaultFileName = $"{chartName}_{timestamp}.png";
- var saveDialog = new SaveFileDialog
- {
- Filter = "PNG Image|*.png|JPEG Image|*.jpg|BMP Image|*.bmp",
- FileName = defaultFileName,
- DefaultExt = ".png"
- };
- if (saveDialog.ShowDialog() == true)
- {
- chart.Plot.SavePng(saveDialog.FileName, (int)chart.ActualWidth, (int)chart.ActualHeight);
- }
- };
- contextMenu.Items.Add(saveItem);
-
- var openWindowItem = new MenuItem { Header = "Open in New Window", Icon = new TextBlock { Text = "🗗" } };
- openWindowItem.Click += (s, e) =>
- {
- var newWindow = new Window
- {
- Title = chartName.Replace("_", " ", StringComparison.Ordinal),
- Width = 800,
- Height = 600
- };
- var tempFile = Path.Combine(Path.GetTempPath(), $"chart_temp_{Guid.NewGuid()}.png");
- try
- {
- chart.Plot.SavePng(tempFile, 800, 600);
- var image = new System.Windows.Controls.Image();
- var bitmap = new System.Windows.Media.Imaging.BitmapImage();
- bitmap.BeginInit();
- bitmap.CacheOption = System.Windows.Media.Imaging.BitmapCacheOption.OnLoad;
- bitmap.UriSource = new Uri(tempFile);
- bitmap.EndInit();
- bitmap.Freeze();
- image.Source = bitmap;
- newWindow.Content = image;
- }
- finally
- {
- if (File.Exists(tempFile)) File.Delete(tempFile);
- }
- newWindow.Show();
- };
- contextMenu.Items.Add(openWindowItem);
-
- contextMenu.Items.Add(new Separator());
-
- var autoscaleItem = new MenuItem { Header = "Revert (or double-click)", Icon = new TextBlock { Text = "↩" } };
- autoscaleItem.Click += (s, e) =>
- {
- chart.Plot.Axes.AutoScale();
- chart.Refresh();
- };
- contextMenu.Items.Add(autoscaleItem);
-
- contextMenu.Items.Add(new Separator());
-
- var exportCsvItem = new MenuItem { Header = "Export Data to CSV...", Icon = new TextBlock { Text = "📊" } };
- exportCsvItem.Click += (s, e) =>
- {
- var timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss", CultureInfo.InvariantCulture);
- var defaultFileName = $"{chartName}_data_{timestamp}.csv";
- var saveDialog = new SaveFileDialog
- {
- Filter = "CSV Files|*.csv|All Files|*.*",
- FileName = defaultFileName,
- DefaultExt = ".csv"
- };
- if (saveDialog.ShowDialog() == true)
- {
- try
- {
- var sb = new StringBuilder();
- sb.AppendLine("DateTime,Series,Value");
-
- var plottables = chart.Plot.GetPlottables();
- int seriesIndex = 1;
- foreach (var plottable in plottables)
- {
- if (plottable is ScottPlot.Plottables.Scatter scatter)
- {
- var seriesName = scatter.LegendText ?? $"Series{seriesIndex}";
- var points = scatter.Data.GetScatterPoints();
-
- foreach (var point in points)
- {
- var dateTime = DateTime.FromOADate(point.X);
- sb.AppendLine(CultureInfo.InvariantCulture, $"{dateTime:yyyy-MM-dd HH:mm:ss},{TabHelpers.EscapeCsvField(seriesName)},{point.Y}");
- }
- seriesIndex++;
- }
- }
-
- File.WriteAllText(saveDialog.FileName, sb.ToString());
- MessageBox.Show($"Data exported to:\n{saveDialog.FileName}", "Export Complete", MessageBoxButton.OK, MessageBoxImage.Information);
- }
- catch (Exception ex)
- {
- MessageBox.Show($"Error exporting data:\n\n{ex.Message}", "Export Error", MessageBoxButton.OK, MessageBoxImage.Error);
- }
- }
- };
- contextMenu.Items.Add(exportCsvItem);
-
- // Show Data Source (if provided)
- if (!string.IsNullOrEmpty(dataSource))
- {
- contextMenu.Items.Add(new Separator());
-
- var dataSourceItem = new MenuItem { Header = "Show Data Source", Icon = new TextBlock { Text = "ℹ" } };
- dataSourceItem.Click += (s, e) =>
- {
- MessageBox.Show(
- $"Data Source:\n\n{dataSource}",
- "Chart Data Source",
- MessageBoxButton.OK,
- MessageBoxImage.Information);
- };
- contextMenu.Items.Add(dataSourceItem);
- }
-
- // Disable ScottPlot's default right-click context menu handling
- chart.UserInputProcessor.UserActionResponses.RemoveAll(r =>
- r.GetType().Name.Contains("Context", StringComparison.Ordinal) ||
- r.GetType().Name.Contains("RightClick", StringComparison.Ordinal) ||
- r.GetType().Name.Contains("Menu", StringComparison.Ordinal));
-
- // Use PreviewMouseRightButtonDown to show context menu before ScottPlot handles it
- chart.PreviewMouseRightButtonDown += (s, e) =>
- {
- e.Handled = true; // Prevent ScottPlot from handling
- contextMenu.PlacementTarget = chart;
- contextMenu.Placement = System.Windows.Controls.Primitives.PlacementMode.MousePoint;
- contextMenu.IsOpen = true;
- };
-
- // Disable ALL of ScottPlot's default double-click behaviors
- chart.UserInputProcessor.UserActionResponses.RemoveAll(r =>
- r.GetType().Name.Contains("DoubleClick", StringComparison.Ordinal));
-
- // Use PreviewMouseDoubleClick to intercept before ScottPlot
- chart.PreviewMouseDoubleClick += (s, e) =>
- {
- e.Handled = true;
- _isAutoScaling = true;
-
- Dispatcher.BeginInvoke(new Action(async () =>
- {
- try
- {
- if (_isZoomed)
- {
- await ResetToOriginalRange();
- }
- else
- {
- chart.Plot.Axes.AutoScale();
- chart.Refresh();
- }
- }
- finally
- {
- await Task.Delay(200);
- _isAutoScaling = false;
- }
- }), System.Windows.Threading.DispatcherPriority.Background);
- };
-
- // Add AxisLimitsChanged handler for auto-zoom-out detection
- chart.Plot.RenderManager.AxisLimitsChanged += (s, e) =>
- {
- if (_isApplyingZoom || _isAutoScaling) return; // Avoid recursion
-
- // Debounce - reset timer on each change
- _lastZoomedChart = chart;
-
- if (_chartZoomDebounceTimer == null)
- {
- _chartZoomDebounceTimer = new DispatcherTimer
- {
- Interval = TimeSpan.FromMilliseconds(800)
- };
- _chartZoomDebounceTimer.Tick += ChartZoomDebounce_Tick;
- }
-
- _chartZoomDebounceTimer.Stop();
- _chartZoomDebounceTimer.Start();
- };
- }
-
- private async void ChartZoomDebounce_Tick(object? sender, EventArgs e)
- {
- _chartZoomDebounceTimer?.Stop();
-
- if (_lastZoomedChart == null || _isApplyingZoom) return;
-
- try
- {
- var limits = _lastZoomedChart.Plot.Axes.GetLimits();
- var fromDate = DateTime.FromOADate(limits.Left);
- var toDate = DateTime.FromOADate(limits.Right);
-
- // Validate dates
- if (fromDate >= toDate || fromDate.Year < 2000 || toDate.Year > 2100)
- return;
-
- // Get current data range
- DateTime currentFrom, currentTo;
- if (_globalFromDate.HasValue && _globalToDate.HasValue)
- {
- currentFrom = _globalFromDate.Value;
- currentTo = _globalToDate.Value;
- }
- else
- {
- currentTo = Helpers.ServerTimeHelper.ServerNow;
- currentFrom = currentTo.AddHours(-_globalHoursBack);
- }
-
- // Check if zoomed significantly beyond current data range (more than 10% wider)
- var currentSpan = (currentTo - currentFrom).TotalMinutes;
- var newSpan = (toDate - fromDate).TotalMinutes;
-
- bool zoomedOut = fromDate < currentFrom.AddMinutes(-currentSpan * 0.1) ||
- toDate > currentTo.AddMinutes(currentSpan * 0.1);
-
- if (zoomedOut && newSpan > currentSpan * 1.1)
- {
- // Auto-apply the zoomed-out range to fetch more data
- _isApplyingZoom = true;
- await ZoomToTimeRange(fromDate, toDate);
- _isApplyingZoom = false;
- }
- }
- catch (Exception ex)
- {
- // Log but don't show error for zoom operations - they're non-critical
- Logger.Warning($"Chart zoom error: {ex.Message}");
- }
- }
-
private async void ServerTab_Loaded(object sender, RoutedEventArgs e)
{
try
@@ -814,12 +545,6 @@ private async void RefreshButton_Click(object sender, RoutedEventArgs e)
private DateTime? _originalToDate = null;
private bool _isZoomed = false;
- // Debounce timer for auto-applying chart zoom
- private DispatcherTimer? _chartZoomDebounceTimer;
- private WpfPlot? _lastZoomedChart;
- private bool _isApplyingZoom = false;
- private bool _isAutoScaling = false;
-
private async void GlobalTimeRange_Click(object sender, RoutedEventArgs e)
{
try
diff --git a/Lite/Controls/ServerTab.xaml.cs b/Lite/Controls/ServerTab.xaml.cs
index dda0609c..57592db7 100644
--- a/Lite/Controls/ServerTab.xaml.cs
+++ b/Lite/Controls/ServerTab.xaml.cs
@@ -179,6 +179,29 @@ public ServerTab(ServerConnection server, DuckDbInitializer duckDb, CredentialSe
_currentWaitsDurationHover = new Helpers.ChartHoverHelper(CurrentWaitsDurationChart, "ms");
_currentWaitsBlockedHover = new Helpers.ChartHoverHelper(CurrentWaitsBlockedChart, "sessions");
+ /* Chart context menus (right-click save/export) */
+ Helpers.ContextMenuHelper.SetupChartContextMenu(WaitStatsChart, "Wait_Stats");
+ Helpers.ContextMenuHelper.SetupChartContextMenu(QueryDurationTrendChart, "Query_Duration_Trends");
+ Helpers.ContextMenuHelper.SetupChartContextMenu(ProcDurationTrendChart, "Procedure_Duration_Trends");
+ Helpers.ContextMenuHelper.SetupChartContextMenu(QueryStoreDurationTrendChart, "QueryStore_Duration_Trends");
+ Helpers.ContextMenuHelper.SetupChartContextMenu(ExecutionCountTrendChart, "Execution_Count_Trends");
+ Helpers.ContextMenuHelper.SetupChartContextMenu(CpuChart, "CPU_Usage");
+ Helpers.ContextMenuHelper.SetupChartContextMenu(MemoryChart, "Memory_Usage");
+ Helpers.ContextMenuHelper.SetupChartContextMenu(MemoryClerksChart, "Memory_Clerks");
+ Helpers.ContextMenuHelper.SetupChartContextMenu(MemoryGrantSizingChart, "Memory_Grant_Sizing");
+ Helpers.ContextMenuHelper.SetupChartContextMenu(MemoryGrantActivityChart, "Memory_Grant_Activity");
+ Helpers.ContextMenuHelper.SetupChartContextMenu(FileIoReadChart, "File_IO_Read_Latency");
+ Helpers.ContextMenuHelper.SetupChartContextMenu(FileIoWriteChart, "File_IO_Write_Latency");
+ Helpers.ContextMenuHelper.SetupChartContextMenu(TempDbChart, "TempDB_Stats");
+ Helpers.ContextMenuHelper.SetupChartContextMenu(TempDbFileIoChart, "TempDB_File_IO");
+ Helpers.ContextMenuHelper.SetupChartContextMenu(LockWaitTrendChart, "Lock_Wait_Trends");
+ Helpers.ContextMenuHelper.SetupChartContextMenu(BlockingTrendChart, "Blocking_Trends");
+ Helpers.ContextMenuHelper.SetupChartContextMenu(DeadlockTrendChart, "Deadlock_Trends");
+ Helpers.ContextMenuHelper.SetupChartContextMenu(CurrentWaitsDurationChart, "Current_Waits_Duration");
+ Helpers.ContextMenuHelper.SetupChartContextMenu(CurrentWaitsBlockedChart, "Current_Waits_Blocked");
+ Helpers.ContextMenuHelper.SetupChartContextMenu(PerfmonChart, "Perfmon_Counters");
+ Helpers.ContextMenuHelper.SetupChartContextMenu(CollectorDurationChart, "Collector_Duration");
+
/* Initial load is triggered by MainWindow.ConnectToServer calling RefreshData()
after collectors finish - no Loaded handler needed */
}
diff --git a/Lite/Helpers/ContextMenuHelper.cs b/Lite/Helpers/ContextMenuHelper.cs
index fd97c1b2..01bfe4de 100644
--- a/Lite/Helpers/ContextMenuHelper.cs
+++ b/Lite/Helpers/ContextMenuHelper.cs
@@ -13,16 +13,18 @@
using System.Text;
using System.Windows;
using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Media;
using Microsoft.Win32;
+using ScottPlot.WPF;
namespace PerformanceMonitorLite.Helpers;
///
-/// Shared context menu helpers for DataGrid copy/export operations.
+/// Shared context menu helpers for DataGrid copy/export and chart save/export operations.
/// Used by standalone windows (history, collection log, manage servers, settings)
-/// that don't have the full ServerTab context menu infrastructure.
+/// and all ScottPlot chart controls.
///
public static class ContextMenuHelper
{
@@ -174,4 +176,198 @@ private static string CsvEscape(string value, string separator)
}
return value;
}
+
+ ///
+ /// Sets up a context menu for a ScottPlot chart with standard options:
+ /// Copy Image, Save Image As, Open in New Window, Revert, Export Data to CSV.
+ ///
+ public static void SetupChartContextMenu(WpfPlot chart, string chartName, string? dataSource = null)
+ {
+ var contextMenu = new ContextMenu();
+
+ // Copy Image
+ var copyItem = new MenuItem { Header = "Copy Image", Icon = new TextBlock { Text = "\U0001f4cb" } };
+ copyItem.Click += (s, e) =>
+ {
+ var tempFile = Path.Combine(Path.GetTempPath(), $"chart_copy_{Guid.NewGuid()}.png");
+ try
+ {
+ chart.Plot.SavePng(tempFile, (int)chart.ActualWidth, (int)chart.ActualHeight);
+ var bitmap = new System.Windows.Media.Imaging.BitmapImage();
+ bitmap.BeginInit();
+ bitmap.CacheOption = System.Windows.Media.Imaging.BitmapCacheOption.OnLoad;
+ bitmap.UriSource = new Uri(tempFile);
+ bitmap.EndInit();
+ bitmap.Freeze();
+ Clipboard.SetDataObject(new DataObject(DataFormats.Bitmap, bitmap), false);
+ }
+ finally
+ {
+ if (File.Exists(tempFile)) File.Delete(tempFile);
+ }
+ };
+ contextMenu.Items.Add(copyItem);
+
+ // Save Image As
+ var saveItem = new MenuItem { Header = "Save Image As...", Icon = new TextBlock { Text = "\U0001f4be" } };
+ saveItem.Click += (s, e) =>
+ {
+ var timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss", CultureInfo.InvariantCulture);
+ var defaultFileName = $"{chartName}_{timestamp}.png";
+ var saveDialog = new SaveFileDialog
+ {
+ Filter = "PNG Image|*.png|JPEG Image|*.jpg|BMP Image|*.bmp",
+ FileName = defaultFileName,
+ DefaultExt = ".png"
+ };
+ if (saveDialog.ShowDialog() == true)
+ {
+ chart.Plot.SavePng(saveDialog.FileName, (int)chart.ActualWidth, (int)chart.ActualHeight);
+ }
+ };
+ contextMenu.Items.Add(saveItem);
+
+ // Open in New Window
+ var openWindowItem = new MenuItem { Header = "Open in New Window", Icon = new TextBlock { Text = "\U0001f5d7" } };
+ openWindowItem.Click += (s, e) =>
+ {
+ var newWindow = new Window
+ {
+ Title = chartName.Replace("_", " ", StringComparison.Ordinal),
+ Width = 800,
+ Height = 600
+ };
+ var tempFile = Path.Combine(Path.GetTempPath(), $"chart_temp_{Guid.NewGuid()}.png");
+ try
+ {
+ chart.Plot.SavePng(tempFile, 800, 600);
+ var image = new System.Windows.Controls.Image();
+ var bitmap = new System.Windows.Media.Imaging.BitmapImage();
+ bitmap.BeginInit();
+ bitmap.CacheOption = System.Windows.Media.Imaging.BitmapCacheOption.OnLoad;
+ bitmap.UriSource = new Uri(tempFile);
+ bitmap.EndInit();
+ bitmap.Freeze();
+ image.Source = bitmap;
+ newWindow.Content = image;
+ }
+ finally
+ {
+ if (File.Exists(tempFile)) File.Delete(tempFile);
+ }
+ newWindow.Show();
+ };
+ contextMenu.Items.Add(openWindowItem);
+
+ contextMenu.Items.Add(new Separator());
+
+ // Revert (Autoscale)
+ var autoscaleItem = new MenuItem { Header = "Revert (or double-click)", Icon = new TextBlock { Text = "\u21a9" } };
+ autoscaleItem.Click += (s, e) =>
+ {
+ chart.Plot.Axes.AutoScale();
+ chart.Refresh();
+ };
+ contextMenu.Items.Add(autoscaleItem);
+
+ contextMenu.Items.Add(new Separator());
+
+ // Export Data to CSV
+ var exportCsvItem = new MenuItem { Header = "Export Data to CSV...", Icon = new TextBlock { Text = "\U0001f4ca" } };
+ exportCsvItem.Click += (s, e) =>
+ {
+ var timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss", CultureInfo.InvariantCulture);
+ var defaultFileName = $"{chartName}_data_{timestamp}.csv";
+ var saveDialog = new SaveFileDialog
+ {
+ Filter = "CSV Files|*.csv|All Files|*.*",
+ FileName = defaultFileName,
+ DefaultExt = ".csv"
+ };
+ if (saveDialog.ShowDialog() == true)
+ {
+ try
+ {
+ var sb = new StringBuilder();
+ var sep = App.CsvSeparator;
+ sb.AppendLine(string.Join(sep, new[] { "DateTime", "Series", "Value" }));
+
+ var plottables = chart.Plot.GetPlottables();
+ int seriesIndex = 1;
+ foreach (var plottable in plottables)
+ {
+ if (plottable is ScottPlot.Plottables.Scatter scatter)
+ {
+ var seriesName = scatter.LegendText ?? $"Series{seriesIndex}";
+ var points = scatter.Data.GetScatterPoints();
+
+ foreach (var point in points)
+ {
+ var dateTime = DateTime.FromOADate(point.X);
+ sb.AppendLine(string.Join(sep, new[]
+ {
+ dateTime.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture),
+ CsvEscape(seriesName, sep),
+ point.Y.ToString(CultureInfo.InvariantCulture)
+ }));
+ }
+ seriesIndex++;
+ }
+ }
+
+ File.WriteAllText(saveDialog.FileName, sb.ToString(), Encoding.UTF8);
+ MessageBox.Show($"Data exported to:\n{saveDialog.FileName}", "Export Complete", MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show($"Error exporting data:\n\n{ex.Message}", "Export Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+ };
+ contextMenu.Items.Add(exportCsvItem);
+
+ // Show Data Source (if provided)
+ if (!string.IsNullOrEmpty(dataSource))
+ {
+ contextMenu.Items.Add(new Separator());
+
+ var dataSourceItem = new MenuItem { Header = "Show Data Source", Icon = new TextBlock { Text = "\u2139" } };
+ dataSourceItem.Click += (s, e) =>
+ {
+ MessageBox.Show(
+ $"Data Source:\n\n{dataSource}",
+ "Chart Data Source",
+ MessageBoxButton.OK,
+ MessageBoxImage.Information);
+ };
+ contextMenu.Items.Add(dataSourceItem);
+ }
+
+ // Disable ScottPlot's default right-click context menu handling
+ chart.UserInputProcessor.UserActionResponses.RemoveAll(r =>
+ r.GetType().Name.Contains("Context", StringComparison.Ordinal) ||
+ r.GetType().Name.Contains("RightClick", StringComparison.Ordinal) ||
+ r.GetType().Name.Contains("Menu", StringComparison.Ordinal));
+
+ // Use PreviewMouseRightButtonDown to show context menu before ScottPlot handles it
+ chart.PreviewMouseRightButtonDown += (s, e) =>
+ {
+ e.Handled = true;
+ contextMenu.PlacementTarget = chart;
+ contextMenu.Placement = PlacementMode.MousePoint;
+ contextMenu.IsOpen = true;
+ };
+
+ // Disable ScottPlot's default double-click behaviors
+ chart.UserInputProcessor.UserActionResponses.RemoveAll(r =>
+ r.GetType().Name.Contains("DoubleClick", StringComparison.Ordinal));
+
+ // Use PreviewMouseDoubleClick for revert/autoscale
+ chart.PreviewMouseDoubleClick += (s, e) =>
+ {
+ e.Handled = true;
+ chart.Plot.Axes.AutoScale();
+ chart.Refresh();
+ };
+ }
}