Skip to content

Commit df89581

Browse files
authored
Merge pull request #660 from BlueDotBrigade/copilot/add-metrics-table-to-graph
2 parents 78b1136 + 3cb8b48 commit df89581

File tree

4 files changed

+440
-2
lines changed

4 files changed

+440
-2
lines changed

Src/BlueDotBrigade.Weevil.Gui/Analysis/GraphDialog.xaml

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
<RowDefinition Height="*" />
2828
<RowDefinition Height="Auto" />
2929
<RowDefinition Height="Auto" />
30+
<RowDefinition Height="Auto" />
3031
</Grid.RowDefinitions>
3132
<Grid.ColumnDefinitions>
3233
<ColumnDefinition Width="*" />
@@ -39,7 +40,49 @@
3940
TooltipPosition="Bottom">
4041
</lvc:CartesianChart>
4142

42-
<Expander Grid.Row="1" Grid.Column="0" ExpandDirection="Down" IsExpanded="False">
43+
<!-- Metrics Table -->
44+
<Grid Grid.Row="1" Grid.Column="0" Margin="5">
45+
<Grid.RowDefinitions>
46+
<RowDefinition Height="Auto" />
47+
<RowDefinition Height="Auto" />
48+
</Grid.RowDefinitions>
49+
<Grid.ColumnDefinitions>
50+
<ColumnDefinition Width="*" />
51+
<ColumnDefinition Width="Auto" />
52+
</Grid.ColumnDefinitions>
53+
54+
<TextBlock Grid.Row="0" Grid.Column="0" Text="Series Metrics"
55+
FontWeight="Bold" Margin="5,0,0,5" />
56+
<Button Grid.Row="0" Grid.Column="1" Content="Copy All"
57+
Command="{Binding CopyMetricsCommand}"
58+
Margin="5,0,5,5"
59+
ToolTip="Copy metrics to clipboard as tab-delimited text" />
60+
61+
<DataGrid Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
62+
ItemsSource="{Binding SeriesMetrics}"
63+
AutoGenerateColumns="False"
64+
IsReadOnly="True"
65+
SelectionMode="Extended"
66+
CanUserAddRows="False"
67+
CanUserDeleteRows="False"
68+
HeadersVisibility="Column"
69+
GridLinesVisibility="All"
70+
MaxHeight="200"
71+
VerticalScrollBarVisibility="Auto">
72+
<DataGrid.Columns>
73+
<DataGridTextColumn Header="Series Name" Binding="{Binding SeriesName}" Width="*" />
74+
<DataGridTextColumn Header="Count" Binding="{Binding Count}" Width="Auto" />
75+
<DataGridTextColumn Header="Min" Binding="{Binding MinFormatted}" Width="Auto" />
76+
<DataGridTextColumn Header="Max" Binding="{Binding MaxFormatted}" Width="Auto" />
77+
<DataGridTextColumn Header="Mean" Binding="{Binding MeanFormatted}" Width="Auto" />
78+
<DataGridTextColumn Header="Median" Binding="{Binding MedianFormatted}" Width="Auto" />
79+
<DataGridTextColumn Header="Range Start" Binding="{Binding RangeStartFormatted}" Width="Auto" />
80+
<DataGridTextColumn Header="Range End" Binding="{Binding RangeEndFormatted}" Width="Auto" />
81+
</DataGrid.Columns>
82+
</DataGrid>
83+
</Grid>
84+
85+
<Expander Grid.Row="2" Grid.Column="0" ExpandDirection="Down" IsExpanded="False">
4386
<StackPanel Orientation="Vertical">
4487
<TextBox
4588
x:Name="RegularExpressionTextBox" Grid.Column="1"
@@ -150,7 +193,7 @@
150193
</StackPanel>
151194
</Expander>
152195

153-
<StatusBar Grid.Row="2" Grid.Column="0"
196+
<StatusBar Grid.Row="3" Grid.Column="0"
154197
Background="{StaticResource MaterialDesignPaper}"
155198
Foreground="{StaticResource TextForeground}"
156199
TextElement.FontWeight="Regular"

Src/BlueDotBrigade.Weevil.Gui/Analysis/GraphViewModel.cs

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public class GraphViewModel : INotifyPropertyChanged
3030
private static readonly string FloatFormat = "0.000";
3131
private static readonly int MaxSeriesCount = 4;
3232
private static readonly string DefaultSeries2Suffix = " 2";
33+
private static readonly int MetricsPrecision = 3;
3334

3435
// Y-Axis position options for each series
3536
public static readonly string YAxisLeft = "Left";
@@ -48,6 +49,7 @@ public class GraphViewModel : INotifyPropertyChanged
4849
private IEnumerable<ISeries> _series;
4950
private IEnumerable<ICartesianAxis> _xAxes;
5051
private IEnumerable<ICartesianAxis> _yAxes;
52+
private ObservableCollection<SeriesMetrics> _seriesMetrics;
5153

5254
private string _xAxisLabel;
5355
private string _yAxisLabel;
@@ -76,6 +78,7 @@ public GraphViewModel(ImmutableArray<IRecord> records, string regularExpression,
7678
this.SourceFilePath = sourceFilePath ?? string.Empty;
7779
this.TooltipWidth = 10;
7880
this.RegularExpression = regularExpression ?? string.Empty;
81+
this.SeriesMetrics = new ObservableCollection<SeriesMetrics>();
7982

8083
this.SampleData = records.Any()
8184
? _records[0].Content
@@ -331,8 +334,20 @@ public string Series4Axis
331334
}
332335
}
333336

337+
public ObservableCollection<SeriesMetrics> SeriesMetrics
338+
{
339+
get => _seriesMetrics;
340+
set
341+
{
342+
_seriesMetrics = value;
343+
RaisePropertyChanged(nameof(this.SeriesMetrics));
344+
}
345+
}
346+
334347
public ICommand UpdateCommand => new UiBoundCommand(() => Update(false));
335348

349+
public ICommand CopyMetricsCommand => new UiBoundCommand(() => CopyMetricsToClipboard());
350+
336351
private string GetDetectedData(string regularExpression, string inputString)
337352
{
338353
var result = string.Empty;
@@ -448,6 +463,9 @@ private void Update(bool isInitializing)
448463
string axisName = allNames.Any() ? string.Join(" / ", allNames) : DefaultYAxisLabel;
449464
this.YAxes = GetYAxes(axisName);
450465
}
466+
467+
// Calculate and update metrics
468+
this.SeriesMetrics = CalculateSeriesMetrics(this.Series);
451469
}
452470
catch (MatchCountException e)
453471
{
@@ -828,5 +846,148 @@ private static IEnumerable<ISeries> GetSeries(ImmutableArray<IRecord> records, s
828846

829847
return seriesList;
830848
}
849+
850+
/// <summary>
851+
/// Calculates statistical metrics for all series.
852+
/// </summary>
853+
private static ObservableCollection<SeriesMetrics> CalculateSeriesMetrics(
854+
IEnumerable<ISeries> series)
855+
{
856+
var metricsList = new ObservableCollection<SeriesMetrics>();
857+
858+
foreach (var s in series)
859+
{
860+
if (s is LineSeries<DateTimePoint> lineSeries)
861+
{
862+
var points = lineSeries.Values.Cast<DateTimePoint>().ToList();
863+
864+
if (points.Any())
865+
{
866+
// Filter out null values to avoid skewing statistics
867+
var values = points
868+
.Where(p => p.Value.HasValue)
869+
.Select(p => p.Value.Value)
870+
.ToList();
871+
var timestamps = points.Select(p => p.DateTime).ToList();
872+
873+
if (values.Any())
874+
{
875+
var count = values.Count;
876+
var min = values.Min();
877+
var max = values.Max();
878+
var mean = values.Average();
879+
880+
// Calculate median
881+
var sortedValues = values.OrderBy(v => v).ToList();
882+
var mid = sortedValues.Count / 2;
883+
var median = (sortedValues.Count % 2 == 0)
884+
? (sortedValues[mid - 1] + sortedValues[mid]) / 2.0
885+
: sortedValues[mid];
886+
887+
var rangeStart = timestamps.Min();
888+
var rangeEnd = timestamps.Max();
889+
890+
var metrics = new SeriesMetrics(
891+
lineSeries.Name ?? "Unknown",
892+
count,
893+
min,
894+
max,
895+
Math.Round(mean, MetricsPrecision),
896+
Math.Round(median, MetricsPrecision),
897+
rangeStart,
898+
rangeEnd);
899+
900+
metricsList.Add(metrics);
901+
}
902+
else
903+
{
904+
// All values were null
905+
var metrics = new SeriesMetrics(
906+
lineSeries.Name ?? "Unknown",
907+
0,
908+
null,
909+
null,
910+
null,
911+
null,
912+
null,
913+
null);
914+
915+
metricsList.Add(metrics);
916+
}
917+
}
918+
else
919+
{
920+
// Empty series
921+
var metrics = new SeriesMetrics(
922+
lineSeries.Name ?? "Unknown",
923+
0,
924+
null,
925+
null,
926+
null,
927+
null,
928+
null,
929+
null);
930+
931+
metricsList.Add(metrics);
932+
}
933+
}
934+
}
935+
936+
return metricsList;
937+
}
938+
939+
/// <summary>
940+
/// Serializes the metrics data as tab-delimited text suitable for clipboard copying.
941+
/// </summary>
942+
public string SerializeMetrics()
943+
{
944+
if (this.SeriesMetrics == null || !this.SeriesMetrics.Any())
945+
{
946+
return string.Empty;
947+
}
948+
949+
var lines = new List<string>();
950+
951+
// Header row
952+
lines.Add("Series Name\tCount\tMin\tMax\tMean\tMedian\tRange Start\tRange End");
953+
954+
// Data rows
955+
foreach (var metrics in this.SeriesMetrics)
956+
{
957+
var line = string.Join("\t",
958+
metrics.SeriesName,
959+
metrics.Count.ToString(),
960+
metrics.MinFormatted,
961+
metrics.MaxFormatted,
962+
metrics.MeanFormatted,
963+
metrics.MedianFormatted,
964+
metrics.RangeStartFormatted,
965+
metrics.RangeEndFormatted);
966+
967+
lines.Add(line);
968+
}
969+
970+
return string.Join(Environment.NewLine, lines);
971+
}
972+
973+
/// <summary>
974+
/// Copies the metrics data to the clipboard.
975+
/// </summary>
976+
private void CopyMetricsToClipboard()
977+
{
978+
try
979+
{
980+
var serializedData = SerializeMetrics();
981+
if (!string.IsNullOrEmpty(serializedData))
982+
{
983+
Clipboard.SetData(DataFormats.UnicodeText, serializedData);
984+
}
985+
}
986+
catch (Exception e)
987+
{
988+
Log.Default.Write(LogSeverityType.Error, e, "Failed to copy metrics to clipboard.");
989+
MessageBox.Show("Failed to copy metrics to clipboard.", "Copy Error", MessageBoxButton.OK, MessageBoxImage.Error);
990+
}
991+
}
831992
}
832993
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
namespace BlueDotBrigade.Weevil.Gui.Analysis
2+
{
3+
using System;
4+
5+
/// <summary>
6+
/// Represents statistical metrics for a graph series.
7+
/// </summary>
8+
public class SeriesMetrics
9+
{
10+
private const string NumericFormat = "0.000";
11+
private const string DateTimeFormat = "yyyy-MM-dd HH:mm:ss";
12+
13+
public SeriesMetrics(
14+
string seriesName,
15+
int count,
16+
double? min,
17+
double? max,
18+
double? mean,
19+
double? median,
20+
DateTime? rangeStart,
21+
DateTime? rangeEnd)
22+
{
23+
this.SeriesName = seriesName;
24+
this.Count = count;
25+
this.Min = min;
26+
this.Max = max;
27+
this.Mean = mean;
28+
this.Median = median;
29+
this.RangeStart = rangeStart;
30+
this.RangeEnd = rangeEnd;
31+
}
32+
33+
public string SeriesName { get; }
34+
public int Count { get; }
35+
public double? Min { get; }
36+
public double? Max { get; }
37+
public double? Mean { get; }
38+
public double? Median { get; }
39+
public DateTime? RangeStart { get; }
40+
public DateTime? RangeEnd { get; }
41+
42+
/// <summary>
43+
/// Returns a formatted string representation suitable for display.
44+
/// </summary>
45+
public string MinFormatted => Min.HasValue ? Min.Value.ToString(NumericFormat) : "N/A";
46+
public string MaxFormatted => Max.HasValue ? Max.Value.ToString(NumericFormat) : "N/A";
47+
public string MeanFormatted => Mean.HasValue ? Mean.Value.ToString(NumericFormat) : "N/A";
48+
public string MedianFormatted => Median.HasValue ? Median.Value.ToString(NumericFormat) : "N/A";
49+
public string RangeStartFormatted => RangeStart.HasValue ? RangeStart.Value.ToString(DateTimeFormat) : "N/A";
50+
public string RangeEndFormatted => RangeEnd.HasValue ? RangeEnd.Value.ToString(DateTimeFormat) : "N/A";
51+
}
52+
}

0 commit comments

Comments
 (0)