Skip to content

Commit ba6b865

Browse files
CopilotPressacco
andcommitted
Add metrics table feature to graph dialog with unit tests
Co-authored-by: Pressacco <5507864+Pressacco@users.noreply.github.com>
1 parent ee76906 commit ba6b865

File tree

4 files changed

+414
-2
lines changed

4 files changed

+414
-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: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public class GraphViewModel : INotifyPropertyChanged
4848
private IEnumerable<ISeries> _series;
4949
private IEnumerable<ICartesianAxis> _xAxes;
5050
private IEnumerable<ICartesianAxis> _yAxes;
51+
private ObservableCollection<SeriesMetrics> _seriesMetrics;
5152

5253
private string _xAxisLabel;
5354
private string _yAxisLabel;
@@ -76,6 +77,7 @@ public GraphViewModel(ImmutableArray<IRecord> records, string regularExpression,
7677
this.SourceFilePath = sourceFilePath ?? string.Empty;
7778
this.TooltipWidth = 10;
7879
this.RegularExpression = regularExpression ?? string.Empty;
80+
this.SeriesMetrics = new ObservableCollection<SeriesMetrics>();
7981

8082
this.SampleData = records.Any()
8183
? _records[0].Content
@@ -331,8 +333,20 @@ public string Series4Axis
331333
}
332334
}
333335

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

348+
public ICommand CopyMetricsCommand => new UiBoundCommand(() => CopyMetricsToClipboard());
349+
336350
private string GetDetectedData(string regularExpression, string inputString)
337351
{
338352
var result = string.Empty;
@@ -448,6 +462,9 @@ private void Update(bool isInitializing)
448462
string axisName = allNames.Any() ? string.Join(" / ", allNames) : DefaultYAxisLabel;
449463
this.YAxes = GetYAxes(axisName);
450464
}
465+
466+
// Calculate and update metrics
467+
this.SeriesMetrics = CalculateSeriesMetrics(this.Series);
451468
}
452469
catch (MatchCountException e)
453470
{
@@ -828,5 +845,126 @@ private static IEnumerable<ISeries> GetSeries(ImmutableArray<IRecord> records, s
828845

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

0 commit comments

Comments
 (0)