@@ -36,6 +36,7 @@ public ScottPlotExporter(int width = 1920, int height = 1080)
36
36
this . Width = width ;
37
37
this . Height = height ;
38
38
this . IncludeBarPlot = true ;
39
+ this . IncludeBoxPlot = true ;
39
40
this . RotateLabels = true ;
40
41
}
41
42
@@ -49,6 +50,16 @@ public ScottPlotExporter(int width = 1920, int height = 1080)
49
50
/// </summary>
50
51
public int Height { get ; set ; }
51
52
53
+ /// <summary>
54
+ /// Gets or sets the common font size for ticks, labels etc. (defaults to 14).
55
+ /// </summary>
56
+ public int FontSize { get ; set ; } = 14 ;
57
+
58
+ /// <summary>
59
+ /// Gets or sets the font size for the chart title. (defaults to 28).
60
+ /// </summary>
61
+ public int TitleFontSize { get ; set ; } = 28 ;
62
+
52
63
/// <summary>
53
64
/// Gets or sets a value indicating whether labels for Plot X-axis should be rotated.
54
65
/// This allows for longer labels at the expense of chart height.
@@ -61,6 +72,12 @@ public ScottPlotExporter(int width = 1920, int height = 1080)
61
72
/// </summary>
62
73
public bool IncludeBarPlot { get ; set ; }
63
74
75
+ /// <summary>
76
+ /// Gets or sets a value indicating whether a box plot or whisker plot for time-per-op
77
+ /// measurement values should be exported.
78
+ /// </summary>
79
+ public bool IncludeBoxPlot { get ; set ; }
80
+
64
81
/// <summary>
65
82
/// Not supported.
66
83
/// </summary>
@@ -98,7 +115,7 @@ from measurement in report.AllMeasurements
98
115
where measurement . Is ( IterationMode . Workload , IterationStage . Result )
99
116
let measurementValue = measurement . Nanoseconds / measurement . Operations
100
117
group measurementValue / timeScale by ( Target : report . BenchmarkCase . Descriptor . WorkloadMethodDisplayInfo , JobId : jobId ) into g
101
- select ( g . Key . Target , g . Key . JobId , Mean : g . Average ( ) , StdError : StandardError ( g . ToList ( ) ) ) ;
118
+ select new ChartStats ( g . Key . Target , g . Key . JobId , g . ToList ( ) ) ;
102
119
103
120
if ( this . IncludeBarPlot )
104
121
{
@@ -112,8 +129,19 @@ where measurement.Is(IterationMode.Workload, IterationStage.Result)
112
129
annotations ) ;
113
130
}
114
131
132
+ if ( this . IncludeBoxPlot )
133
+ {
134
+ // <BenchmarkName>-boxplot.png
135
+ yield return CreateBoxPlot (
136
+ $ "{ title } - { benchmarkName } ",
137
+ Path . Combine ( summary . ResultsDirectoryPath , $ "{ title } -{ benchmarkName } -boxplot.png") ,
138
+ $ "Time ({ timeUnit } )",
139
+ "Target" ,
140
+ timeStats ,
141
+ annotations ) ;
142
+ }
143
+
115
144
/* TODO: Rest of the RPlotExporter plots.
116
- <BenchmarkName>-boxplot.png
117
145
<BenchmarkName>-<MethodName>-density.png
118
146
<BenchmarkName>-<MethodName>-facetTimeline.png
119
147
<BenchmarkName>-<MethodName>-facetTimelineSmooth.png
@@ -161,12 +189,12 @@ private static double StandardError(IReadOnlyList<double> values)
161
189
return ( "ns" , 1d ) ;
162
190
}
163
191
164
- private string CreateBarPlot ( string title , string fileName , string yLabel , string xLabel , IEnumerable < ( string Target , string JobId , double Mean , double StdError ) > data , IReadOnlyList < Annotation > annotations )
192
+ private string CreateBarPlot ( string title , string fileName , string yLabel , string xLabel , IEnumerable < ChartStats > data , IReadOnlyList < Annotation > annotations )
165
193
{
166
194
Plot plt = new Plot ( ) ;
167
- plt . Title ( title , 28 ) ;
168
- plt . YLabel ( yLabel ) ;
169
- plt . XLabel ( xLabel ) ;
195
+ plt . Title ( title , this . TitleFontSize ) ;
196
+ plt . YLabel ( yLabel , this . FontSize ) ;
197
+ plt . XLabel ( xLabel , this . FontSize ) ;
170
198
171
199
var palette = new ScottPlot . Palettes . Category10 ( ) ;
172
200
@@ -177,6 +205,7 @@ private string CreateBarPlot(string title, string fileName, string yLabel, strin
177
205
178
206
plt . Legend . IsVisible = true ;
179
207
plt . Legend . Location = Alignment . UpperRight ;
208
+ plt . Legend . Font . Size = this . FontSize ;
180
209
var legend = data . Select ( d => d . JobId )
181
210
. Distinct ( )
182
211
. Select ( ( label , index ) => new LegendItem ( )
@@ -192,8 +221,11 @@ private string CreateBarPlot(string title, string fileName, string yLabel, strin
192
221
var ticks = data
193
222
. Select ( ( d , index ) => new Tick ( index , d . Target ) )
194
223
. ToArray ( ) ;
224
+
225
+ plt . Axes . Left . TickLabelStyle . FontSize = this . FontSize ;
195
226
plt . Axes . Bottom . TickGenerator = new ScottPlot . TickGenerators . NumericManual ( ticks ) ;
196
227
plt . Axes . Bottom . MajorTickStyle . Length = 0 ;
228
+ plt . Axes . Bottom . TickLabelStyle . FontSize = this . FontSize ;
197
229
198
230
if ( this . RotateLabels )
199
231
{
@@ -209,7 +241,7 @@ private string CreateBarPlot(string title, string fileName, string yLabel, strin
209
241
}
210
242
211
243
// ensure axis panels do not get smaller than the largest label
212
- plt . Axes . Bottom . MinimumSize = largestLabelWidth ;
244
+ plt . Axes . Bottom . MinimumSize = largestLabelWidth * 2 ;
213
245
plt . Axes . Right . MinimumSize = largestLabelWidth ;
214
246
}
215
247
@@ -232,6 +264,89 @@ private string CreateBarPlot(string title, string fileName, string yLabel, strin
232
264
return Path . GetFullPath ( fileName ) ;
233
265
}
234
266
267
+ private string CreateBoxPlot ( string title , string fileName , string yLabel , string xLabel , IEnumerable < ChartStats > data , IReadOnlyList < Annotation > annotations )
268
+ {
269
+ Plot plt = new Plot ( ) ;
270
+ plt . Title ( title , this . TitleFontSize ) ;
271
+ plt . YLabel ( yLabel , this . FontSize ) ;
272
+ plt . XLabel ( xLabel , this . FontSize ) ;
273
+
274
+ var palette = new ScottPlot . Palettes . Category10 ( ) ;
275
+
276
+ var legendPalette = data . Select ( d => d . JobId )
277
+ . Distinct ( )
278
+ . Select ( ( jobId , index ) => ( jobId , index ) )
279
+ . ToDictionary ( t => t . jobId , t => palette . GetColor ( t . index ) ) ;
280
+
281
+ plt . Legend . IsVisible = true ;
282
+ plt . Legend . Location = Alignment . UpperRight ;
283
+ plt . Legend . Font . Size = this . FontSize ;
284
+ var legend = data . Select ( d => d . JobId )
285
+ . Distinct ( )
286
+ . Select ( ( label , index ) => new LegendItem ( )
287
+ {
288
+ Label = label ,
289
+ FillColor = legendPalette [ label ]
290
+ } )
291
+ . ToList ( ) ;
292
+
293
+ plt . Legend . ManualItems . AddRange ( legend ) ;
294
+
295
+ var jobCount = plt . Legend . ManualItems . Count ;
296
+ var ticks = data
297
+ . Select ( ( d , index ) => new Tick ( index , d . Target ) )
298
+ . ToArray ( ) ;
299
+
300
+ plt . Axes . Left . TickLabelStyle . FontSize = this . FontSize ;
301
+ plt . Axes . Bottom . TickGenerator = new ScottPlot . TickGenerators . NumericManual ( ticks ) ;
302
+ plt . Axes . Bottom . MajorTickStyle . Length = 0 ;
303
+ plt . Axes . Bottom . TickLabelStyle . FontSize = this . FontSize ;
304
+
305
+ if ( this . RotateLabels )
306
+ {
307
+ plt . Axes . Bottom . TickLabelStyle . Rotation = 45 ;
308
+ plt . Axes . Bottom . TickLabelStyle . Alignment = Alignment . MiddleLeft ;
309
+
310
+ // determine the width of the largest tick label
311
+ float largestLabelWidth = 0 ;
312
+ foreach ( Tick tick in ticks )
313
+ {
314
+ PixelSize size = plt . Axes . Bottom . TickLabelStyle . Measure ( tick . Label ) ;
315
+ largestLabelWidth = Math . Max ( largestLabelWidth , size . Width ) ;
316
+ }
317
+
318
+ // ensure axis panels do not get smaller than the largest label
319
+ plt . Axes . Bottom . MinimumSize = largestLabelWidth * 2 ;
320
+ plt . Axes . Right . MinimumSize = largestLabelWidth ;
321
+ }
322
+
323
+ int globalIndex = 0 ;
324
+ foreach ( var ( targetGroup , targetGroupIndex ) in data . GroupBy ( s => s . Target ) . Select ( ( targetGroup , index ) => ( targetGroup , index ) ) )
325
+ {
326
+ var boxes = targetGroup . Select ( job => ( job . JobId , Stats : job . CalculateBoxPlotStatistics ( ) ) ) . Select ( ( j , jobIndex ) => new Box ( )
327
+ {
328
+ Position = ticks [ globalIndex ++ ] . Position ,
329
+ Fill = new FillStyle ( ) { Color = legendPalette [ j . JobId ] } ,
330
+ Stroke = new LineStyle ( ) { Color = Colors . Black } ,
331
+ BoxMin = j . Stats . Q1 ,
332
+ BoxMax = j . Stats . Q3 ,
333
+ WhiskerMin = j . Stats . Min ,
334
+ WhiskerMax = j . Stats . Max ,
335
+ BoxMiddle = j . Stats . Median
336
+ } )
337
+ . ToList ( ) ;
338
+ plt . Add . Boxes ( boxes ) ;
339
+ }
340
+
341
+ // Tell the plot to autoscale with a small padding below the boxes.
342
+ plt . Axes . Margins ( bottom : 0.05 , right : .2 ) ;
343
+
344
+ plt . PlottableList . AddRange ( annotations ) ;
345
+
346
+ plt . SavePng ( fileName , this . Width , this . Height ) ;
347
+ return Path . GetFullPath ( fileName ) ;
348
+ }
349
+
235
350
/// <summary>
236
351
/// Provides a list of annotations to put over the data area.
237
352
/// </summary>
@@ -255,5 +370,69 @@ private IReadOnlyList<Annotation> GetAnnotations(string version)
255
370
256
371
return new [ ] { versionAnnotation } ;
257
372
}
373
+
374
+ private class ChartStats
375
+ {
376
+ public ChartStats ( string Target , string JobId , IReadOnlyList < double > Values )
377
+ {
378
+ this . Target = Target ;
379
+ this . JobId = JobId ;
380
+ this . Values = Values ;
381
+ }
382
+
383
+ public string Target { get ; }
384
+
385
+ public string JobId { get ; }
386
+
387
+ public IReadOnlyList < double > Values { get ; }
388
+
389
+ public double Min => this . Values . DefaultIfEmpty ( 0d ) . Min ( ) ;
390
+
391
+ public double Max => this . Values . DefaultIfEmpty ( 0d ) . Max ( ) ;
392
+
393
+ public double Mean => this . Values . DefaultIfEmpty ( 0d ) . Average ( ) ;
394
+
395
+ public double StdError => StandardError ( this . Values ) ;
396
+
397
+
398
+ private static ( int MidPoint , double Median ) CalculateMedian ( ReadOnlySpan < double > values )
399
+ {
400
+ int n = values . Length ;
401
+ var midPoint = n / 2 ;
402
+
403
+ // Check if count is even, if so use average of the two middle values,
404
+ // otherwise take the middle value.
405
+ var median = n % 2 == 0 ? ( values [ midPoint - 1 ] + values [ midPoint ] ) / 2d : values [ midPoint ] ;
406
+ return ( midPoint , median ) ;
407
+ }
408
+
409
+ /// <summary>
410
+ /// Calculate the mid points.
411
+ /// </summary>
412
+ /// <returns></returns>
413
+ public ( double Min , double Q1 , double Median , double Q3 , double Max , double [ ] Outliers ) CalculateBoxPlotStatistics ( )
414
+ {
415
+ var values = this . Values . ToArray ( ) ;
416
+ Array . Sort ( values ) ;
417
+ var s = values . AsSpan ( ) ;
418
+ var ( midPoint , median ) = CalculateMedian ( s ) ;
419
+
420
+ var ( q1Index , q1 ) = midPoint > 0 ? CalculateMedian ( s . Slice ( 0 , midPoint ) ) : ( midPoint , median ) ;
421
+ var ( q3Index , q3 ) = midPoint + 1 < s . Length ? CalculateMedian ( s . Slice ( midPoint + 1 ) ) : ( midPoint , median ) ;
422
+ var iqr = q3 - q1 ;
423
+ var lowerFence = q1 - 1.5d * iqr ;
424
+ var upperFence = q3 + 1.5d * iqr ;
425
+ var outliers = values . Where ( v => v < lowerFence || v > upperFence ) . ToArray ( ) ;
426
+ var nonOutliers = values . Where ( v => v >= lowerFence && v <= upperFence ) . ToArray ( ) ;
427
+ return (
428
+ nonOutliers . FirstOrDefault ( ) ,
429
+ q1 ,
430
+ median ,
431
+ q3 ,
432
+ nonOutliers . LastOrDefault ( ) ,
433
+ outliers
434
+ ) ;
435
+ }
436
+ }
258
437
}
259
438
}
0 commit comments