-
Notifications
You must be signed in to change notification settings - Fork 0
/
view.html
2477 lines (2315 loc) · 97.8 KB
/
view.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html>
<head><title>plot metrics</title>
<script src="chart.js-3.6.0/dist/chart.js"></script>
<script src="sgratzl-chartjs-chart-boxplot/build/index.umd.js"></script>
<script type="text/javascript">
//register box plots as Chart.js type -- apparently not necessary?
//Chart.register(ChartBoxPlot.BoxPlotController);
const BoxPlotChartTypeId = ChartBoxPlot.BoxPlotChart.id;
/* leftover rendering test code; TODO: remove
function fun1(x) {return Math.sin(x); }
function fun2(x) {return Math.cos(3*x);}
function drawTest() {
var canvas = document.getElementById("canvas");
if (null==canvas || !canvas.getContext) return;
var axes={}, ctx=canvas.getContext("2d");
axes.x0 = .5 + .5*canvas.width; // x0 pixels from left to x=0
axes.y0 = .5 + .5*canvas.height; // y0 pixels from top to y=0
axes.scale = 40; // 40 pixels from x=0 to x=1
axes.doNegativeX = true;
showAxes(ctx,axes);
funGraph(ctx,axes,fun1,"rgb(11,153,11)",1);
funGraph(ctx,axes,fun2,"rgb(66,44,255)",2);
}
function funGraph (ctx,axes,func,color,thick) {
var xx, yy, dx=4, x0=axes.x0, y0=axes.y0, scale=axes.scale;
var iMax = Math.round((ctx.canvas.width-x0)/dx);
var iMin = axes.doNegativeX ? Math.round(-x0/dx) : 0;
ctx.beginPath();
ctx.lineWidth = thick;
ctx.strokeStyle = color;
for (var i=iMin;i<=iMax;i++) {
xx = dx*i; yy = scale*func(xx/scale);
if (i==iMin) ctx.moveTo(x0+xx,y0-yy);
else ctx.lineTo(x0+xx,y0-yy);
}
ctx.stroke();
}
function showAxes(ctx,axes) {
var x0=axes.x0, w=ctx.canvas.width;
var y0=axes.y0, h=ctx.canvas.height;
var xmin = axes.doNegativeX ? 0 : x0;
ctx.beginPath();
ctx.strokeStyle = "rgb(128,128,128)";
ctx.moveTo(xmin,y0); ctx.lineTo(w,y0); // X axis
ctx.moveTo(x0,0); ctx.lineTo(x0,h); // Y axis
ctx.stroke();
}*/
// global program state variable section
//holds all metric data currently loaded from opened files,
//as a flat list of objects/dictionaries each representing one entry ('data point')
var metric_entries = [];
//holds all encountered commit hashes (values of metric entries with names starting with 'REPO-GITCOMMITHASH-'),
//indexed by repository name, in the form:
// Map (repository_name: string) -> (Map (encountered commit hash: string) -> (true))
var commit_hashes_in_metrics_lookup_by_repository_name = new Map();
//holds overview information for all properties of all metrics currently loaded from opened files,
//indexed by property names, containing objects of the form:
// {
// types: {
// only: ?string (type name or null),
// (["has_"+string (type name)+"s"] = true)...,
// },
// values: {
// first_unique: (first unique value),
// second_unique: (second unique value => undefined if all values are equal),
// min: (minimum value, by '<'),
// max: (maximum value, by '>'),
// },
// }
var property_overview = [];
//holds the current plotting pipeline (configuration info)
//as a flat array containing objects of the form:
// {
// row_id: string (HTML element id of the table row (tr element), based on property_key),
// property_key: string (the corresponding property's name),
// operation: string (operation name from a supported set),
// }
var plotting_pipeline = [];
//holds the number of commit strands that have been loaded for a particular repository
//(not decremented on deletion, initially undefined meaning 0)
const next_strand_id_per_repository = {};
//holds the total number of commit strands that have been loaded
//(not decremented on deletion)
var next_commit_strand_id = 0;
//holds all currently loaded (non-removed) commit strands in the same ordering as the ui displays,
//as a flat array containing objects of the form:
// {
// row_id: string (HTML element id of the table row (tr element)),
// strand_name: string (the strand's assigned name),
// commit_hashes: []string,
// commit_display_names: Map((hash: string) -> (display_name: string)), //NOTE: it's important that this is and stays an ordered map, returning .values() and .keys() in the same order as .commit_hashes!
// }
var loaded_commit_strands = [];
//holds all currently loaded (non-removed) commit strands by the repository name they pertain to,
//in no particular order (-> loading/insertion order)
var loaded_commit_strands_by_repository_name = new Map();
//holds a lookup map from a single commit to an array of all strands it is a part of (.get(unknown) -> undefined)
const loaded_commit_strands_by_commit_by_repository_name = new Map();
//holds the commit hashes that are present in metric_entries but not loaded strand, in the form:
// Map ((repository_name: string) -> (commit_hashes: []string))
//updated by updateCommitStrandCoverage after loading metrics and/or commit strands
var loaded_commit_strand_coverage_not_in_strands_by_repository_name;
//holds the commit hashes that are present in loaded strands but not metric_entries, in the form:
// Map ((repository_name: string) ->
// Map ((strand: object) -> (commit_hashes: []string))
// )
//updated by updateCommitStrandCoverage after loading metrics and/or commit strands
var loaded_commit_strand_coverage_not_in_metrics_by_strand_by_repository_name;
//holds extra info of every commit indexed by its hash, grouped by repository:
// Map ((repository_name: string) ->
// Map ((commit_hash: string) -> {
// tags: []string,
// tag_lookup: Map (string -> true),
// commit_expressions: []string,
// commit_expression_lookup: Map (string -> true),
// })
// )
const loaded_commit_info_by_hash_by_repository = new Map();
// function section
//ensures that the given field holds an array and returns it
function ensure_field_array(object, key){
var array = object[key];
if(array === undefined){
array = [];
object[key] = array;
}
return array;
}
//ensures that the given map entry holds an array and returns it
function ensure_entry_array(map, key){
var array = map.get(key);
if(array === undefined){
array = [];
map.set(key, array);
}
return array;
}
//ensures that the given field holds a map and returns it
function ensure_field_map(object, key){
var map = object[key];
if(map === undefined){
map = new Map();
object[key] = map;
}
return map;
}
//ensures that the given map entry holds a map and returns it
function ensure_entry_map(map, key){
var submap = map.get(key);
if(submap === undefined){
submap = new Map();
map.set(key, submap);
}
return submap;
}
//ensures that the given map entry holds an object and returns it
function ensure_entry_object(map, key){
var object = map.get(key);
if(object === undefined){
object = {};
map.set(key, object);
}
return object;
}
//escapes the given string (can be any text) to be insertable into the html doctree
function escapeForHTML(s){
return s
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
//reads all metric entries of the given metrics files
//into global variables metric_entries and property_overview (clearing all previous values),
//then (if successful) calls thenDo(...thenDoArgs), continuation-style
function readMetricsThen(files, thenDo, ...thenDoArgs) {
metric_entries = [];
commit_hashes_in_metrics_lookup_by_repository_name = new Map()
property_overview = [];
var line_pattern = /^([^\n=]*)=(.*)$/gm;
//how to parse each file, called recursively, continuation-style
function readFile(index) {
// base case / completion
if(index >= files.length){
if(thenDo !== undefined){
return thenDo(...thenDoArgs);
}
return;
}
var reader = new FileReader();
var file = files[index];
reader.onload = function(e) {
//get file contents
var contents = e.target.result;
//aggregates non-separated lines into entries
var current_entry = null;
// parse a single line per iteration
var matches = null;
while(matches = line_pattern.exec(contents)){
var name = matches[1]
var value = matches[2]
if(name.length == 0){ // irregular line
if(value.startsWith("=")){ // entry separator
if(current_entry !== null){
metric_entries.push(current_entry)
current_entry = null
}
}else{ // otherwise the entry has no name
alert("Found entry with no name in file '" + file.name + "'; value='"+value+"'")
}
}else{ // regular line, add as entry
// add to the current entry
if(current_entry === null){
current_entry = {}
}
current_entry[name] = value;
// ensure the property exists in the overview
var property_info = property_overview[name];
var mtypes, mvalues;
if(property_info === undefined){
mtypes = {};
mvalues = {};
property_info = {types: mtypes, values: mvalues};
property_overview[name] = property_info;
// if this is a git commit hash property, add it to commit_hashes_in_metrics_lookup_by_repository_name, which we tally in updateCommitStrandCoverage
if(name.startsWith("REPO-GITCOMMITHASH-")){
const repository_name = name.substring("REPO-GITCOMMITHASH-".length)
const commit_hashes_lookup = ensure_entry_map(commit_hashes_in_metrics_lookup_by_repository_name, repository_name); //probably never set here?
property_info.value_lookup = commit_hashes_lookup;
}
}else{
mtypes = property_info.types;
mvalues = property_info.values;
}
if(property_info.value_lookup !== undefined){
property_info.value_lookup.set(value, true);
}
// check value type (number or string)
var value_type
var value_as_number = Number(value)
if(value_as_number == value_as_number){ //is a number
value = value_as_number
value_type = 'number'
}else{ //is not a number => is a string
value_type = 'string'
}
// update property info
if(mtypes.only === undefined){ // first instance of this property
mtypes.only = value_type;
mvalues.first_unique = value;
mvalues.min = value;
mvalues.max = value;
} else { // subsequent instances
if(mtypes.only !== value_type){
mtypes.only = null;
}
if(mvalues.second_unique === undefined && value !== mvalues.first_unique){
mvalues.second_unique = value;
}
if(value < mvalues.min){
mvalues.min = value;
}else if(value > mvalues.max){
mvalues.max = value;
}
}
mtypes["has_"+value_type+"s"] = true;
}
}
if(current_entry !== null){ // add last, non-terminated entry, if one exists
metric_entries.push(current_entry)
}
// recursively call to parse the next file (or call the continuation and exit)
return readFile(index+1);
}
reader.readAsBinaryString(file);
}
return readFile(0);
}
//updates the total entry count in the ui,
//then calls thenDo(...thenDoArgs), continuation-style
function updateEntryCountOutputThen(thenDo, ...thenDoArgs){
document.getElementById("ui_metric_entry_count_output").innerHTML = metric_entries.length+" entries loaded"
if(thenDo !== undefined){
return thenDo(...thenDoArgs);
}
}
//LEFTOVER/UNUSED; TODO: remove
//replaces the property overview table in the ui
//with the data from the global variable property_overview,
//then calls thenDo(...thenDoArgs), continuation-style
function updateEntriesOutputThen(thenDo, ...thenDoArgs){
var new_table_rows_html = "<thead><tr><th>property</th><th>type</th><th>first unique value</th><th>second unique value</th>";
//new_table_rows_html += "<th>min</th><th>max</th>";
new_table_rows_html += "</tr></thead><tbody>";
for(var key in property_overview){ // append one row per property
var info = property_overview[key];
new_table_rows_html+="<tr>";
new_table_rows_html+="<td>"+key+"</td>";
var only_type = info.types.only;
new_table_rows_html+="<td>"+(only_type === null ? "mixed" : only_type)+"</td>";
var first_unique = info.values.first_unique;
new_table_rows_html+="<td>"+first_unique+"</td>";
var second_unique = info.values.second_unique;
new_table_rows_html+="<td>"+(second_unique === undefined ? "" : second_unique)+"</td>";
//new_table_rows_html+="<td>"+"(min)"+"</td>";
//new_table_rows_html+="<td>"+"(max)"+"</td>";
new_table_rows_html+="</tr>";
}
new_table_rows_html += "</tbody>";
document.getElementById("ui_property_overview_output").innerHTML = new_table_rows_html;
if(thenDo !== undefined){
return thenDo(...thenDoArgs);
}
}
//returns the default operation (name) for the given property_key,
// based on currently loaded data in global variable metric_entries
function defaultOperation(property_key){
const all_values_equal = property_overview[property_key].values.second_unique === undefined;
return all_values_equal
? 'assert_uniform'
: property_key.startsWith("REPO-GITCOMMITHASH-")
? 'split_figures_and_order_x_commit_strands'
: 'split_groups';
}
//lookup table associating operations (by name) with CSS styles (background colour)
const operation_styles = {
assert_uniform: "background: #DFD;",
split_figures: "background: #FFB;",
split_groups: "background: #FEC;",
plot_x: "background: #FBF;",
split_figures_and_order_x_commit_strands: "background: #FDE",
plot_y: "background: #CCF;",
plot_y_list: "background: #CBF;",
plot_y_aggregate_2: "background: #CAF;",
discard_merge: "background: #FAA;",
}
//returns the corresponding CSS style for a given operation (name)
function operationStyle(operation){
var style = operation_styles[operation];
return style === undefined
? ""
: style;
}
function queryPipelineOperationWarningForProperty(property_key, selected_operation){
const overview = property_overview[property_key];
switch(selected_operation){
case 'assert_uniform':
if(overview.values.second_unique !== undefined){
return "property holds non-uniform values";
}
break;
case 'plot_x':
case 'plot_y':
if(overview.types.only !== 'number'){
return "property holds non-numeric values";
}
break;
case 'plot_y_list':
case 'plot_y_aggregate_2':
if(overview.types.only !== 'string'){
return "property holds non-string (=> non-JSON-array) values";
}
break;
}
return undefined;
}
function construct_plotting_pipeline_operation(property_key, selected_operation){
// every row is referrable by unique id based on its property name
const row_id = "pipeline-ui-row-"+property_key;
// check whether the operation gels for this property in the currently loaded data, but continue even in case of warning
const operation_warning = queryPipelineOperationWarningForProperty(property_key, selected_operation);
// construct the logical element object
const plotting_pipeline_element = {row_id: row_id, property_key: property_key, operation: selected_operation};
const row_element = document.createElement('tr');
row_element.setAttribute('id', row_id);
// style element according to selected operation
row_element.setAttribute('style', operationStyle(selected_operation));
// buttons for manual reordering
const reordering_td = document.createElement('td')
const swap_downwards_button = document.createElement('button');
swap_downwards_button.onclick = function() {reorderPipelineStepMoveStably(row_id, 1)}
swap_downwards_button.textContent = "v";
reordering_td.appendChild(swap_downwards_button);
const swap_upwards_button = document.createElement('button');
swap_upwards_button.onclick = function() {reorderPipelineStepMoveStably(row_id, -1)}
swap_upwards_button.textContent = "^";
reordering_td.appendChild(swap_upwards_button);
row_element.appendChild(reordering_td);
//row type column
const type_td = document.createElement('td');
type_td.textContent = "operation";
row_element.appendChild(type_td);
//property name (value type) column
const name_type_td = document.createElement('td');
const overview = property_overview[property_key];
const only_type = overview.types.only;
const type_suffix = " (" + (only_type === null ? "mixed" : only_type) + ")";
name_type_td.textContent = property_key + type_suffix;
//hover text gives overview of values
const overview_values = overview.values;
const first_unique = overview_values.first_unique, second_unique = overview_values.second_unique;
const value_description = second_unique === undefined
? "only value: "+first_unique
: "first unique value: "+first_unique+"\nsecond unique value: "+second_unique+"\nmin value: "+overview_values.min+"\nmax value: "+overview_values.max;
name_type_td.setAttribute('title', value_description);
row_element.appendChild(name_type_td);
//operation kind selections
const operation_kind_td = document.createElement('td');
operation_kind_td.setAttribute('style', "font-size: small;")
const input_identity = "operation_kind-"+row_id;
const radio_button_onchange = function() {setPlottingOperation(row_id, this.value);}
const add_radio_button = function(value, text){
const radio_button = document.createElement('input');
radio_button.setAttribute('type', 'radio');
radio_button.setAttribute('name', input_identity); //the input group within which the activation of one radio button deactivates all others
const radio_button_id = input_identity+"-"+value;
radio_button.setAttribute('id', radio_button_id);
if(value === selected_operation){
radio_button.setAttribute('checked', "");
}
radio_button.setAttribute('value', value);
radio_button.onchange = radio_button_onchange;
operation_kind_td.appendChild(radio_button);
const label = document.createElement('label');
label.setAttribute('for', radio_button_id);
label.textContent = text;
operation_kind_td.appendChild(label);
}
add_radio_button('assert_uniform', "assert uniform");
add_radio_button('split_figures', "split figures");
add_radio_button('split_groups', "split into groups");
add_radio_button('plot_x', "(broken) use as x axis");
add_radio_button('split_figures_and_order_x_commit_strands', "split figures, order commits oldest-left on x axis")
add_radio_button('plot_y', "use as y axis");
add_radio_button('plot_y_list', "boxplot");
add_radio_button('plot_y_aggregate_2', "derive y axes (advanced)");
add_radio_button('discard_merge', "discard");
row_element.appendChild(operation_kind_td);
return {
plotting_pipeline_element: plotting_pipeline_element,
html_tr_element: row_element,
operation_warning: operation_warning,
};
}
//reconstructs the plotting pipeline
//based on the global variable property_overview
//into the global variable plotting_pipeline (clearing its previous values),
//then calls thenDo(...thenDoArgs), continuation-style
function rebuildPlottingPipelineThen(thenDo, ...thenDoArgs){
document.getElementById("ui_plotting_pipeline_table").hidden = false;
plotting_pipeline = [];
const plotting_pipeline_operations = document.getElementById("ui_plotting_pipeline_operations");
while(plotting_pipeline_operations.firstChild){ // remove all previous rows
plotting_pipeline_operations.removeChild(plotting_pipeline_operations.firstChild);
}
for(const key in property_overview){ // append one row per property in property_overview
const default_operation = defaultOperation(key); //determine default operation based on loaded data
const constructed = construct_plotting_pipeline_operation(key, default_operation);
plotting_pipeline.push(constructed.plotting_pipeline_element);
plotting_pipeline_operations.appendChild(constructed.html_tr_element);
if(constructed.operation_warning !== undefined){
alert("Default pipeline operation '"+default_operation+"' for property '"+key+"' triggered warning:\n"+constructed.operation_warning);
}
}
if(thenDo !== undefined){
return thenDo(...thenDoArgs);
}
}
//TODO: document
function replacePlottingPipelineFromConfigFile(file){
const reader = new FileReader();
reader.onload = function(e){
const config_json = e.target.result;
const new_config = JSON.parse(config_json);
// check for incongruence of properties between global property_overview and parsed new_config
const properties_only_before_lookup = new Map();
for(const key in property_overview){
properties_only_before_lookup.set(key, true);
}
const properties_only_after_lookup = new Map();
for(const new_config_operation of new_config){
const key = new_config_operation.property_key;
if(properties_only_before_lookup.get(key) === undefined){
properties_only_after_lookup.set(key, true);
}else{
properties_only_before_lookup.delete(key);
}
}
const incongruence_messages = [];
if(properties_only_before_lookup.size > 0){
incongruence_messages.push("The following keys are not present in the config file:\n\t"+Array.from(properties_only_before_lookup.keys()).join("\n\t"));
}
if(properties_only_after_lookup.size > 0){
incongruence_messages.push("The following keys are not present in the loaded metrics data:\n\t"+Array.from(properties_only_after_lookup.keys()).join("\n\t"));
}
if(incongruence_messages.length > 0){
alert("Import error:\n"+incongruence_messages.join("\n"));
return;
}
// replace the pipeline
plotting_pipeline = [];
const plotting_pipeline_operations = document.getElementById("ui_plotting_pipeline_operations");
while(plotting_pipeline_operations.firstChild){ // remove all previous rows
plotting_pipeline_operations.removeChild(plotting_pipeline_operations.firstChild);
}
for(const config_entry of new_config){
const key = config_entry.property_key;
if(config_entry.row_id !== "pipeline-ui-row-"+key){
alert("info: normalizing unusual row_id from pipeline configuration\nexpected: pipeline-ui-row-"+key+"\nreceived: "+config_entry.row_id);
}
const operation = config_entry.operation;
const constructed = construct_plotting_pipeline_operation(key, operation);
if(constructed.operation_warning !== undefined){
alert("Warning: Operation '"+operation+"' for property '"+key+"' triggered warning on loaded data:\n"+constructed.operation_warning);
}
plotting_pipeline.push(constructed.plotting_pipeline_element);
plotting_pipeline_operations.appendChild(constructed.html_tr_element);
}
};
reader.readAsBinaryString(file);
}
//sets the plotting operation of pipeline entry with the given .row_id to the given operation (name string)
function setPlottingOperation(row_id, new_operation){
for(var i = 0; i < plotting_pipeline.length; ++i){
var element = plotting_pipeline[i];
if(element.row_id === row_id){
element.operation = new_operation;
document.getElementById(row_id).setAttribute('style', operationStyle(new_operation));
return;
}
}
throw "could not find row_id '"+row_id+"' in plotting_pipeline";
}
//swaps the plotting pipeline entry at the two given indices
function reorderPipelineStepsSwap(first_index, second_index){
if(first_index < 0)
throw "first index invalid (negative)";
if(second_index < 0)
throw "second index invalid (negative)";
if(first_index >= plotting_pipeline.length)
throw "first index invalid (>= plotting_pipeline.length)";
if(second_index >= plotting_pipeline.length)
throw "second index invalid (>= plotting_pipeline.length)";
if(first_index == second_index) return;
//swap the logical in plotting_pipeline
const element_a = plotting_pipeline[first_index];
const element_b = plotting_pipeline[second_index];
plotting_pipeline[first_index] = element_b;
plotting_pipeline[second_index] = element_a;
//swap the HTML table rows
const row_a = document.getElementById(element_a.row_id);
const next_after_a = row_a.nextSibling;
const row_b = document.getElementById(element_b.row_id);
const next_after_b = row_b.nextSibling;
const table = document.getElementById("ui_plotting_pipeline_operations");
//.insertBefore(x, y) inserts (-> moves) x before y (the order is weird, I know);
//if y is null, it moves x to the very end, which is just what we want here.
table.insertBefore(row_b, next_after_a);
table.insertBefore(row_a, next_after_b);
}
//moves the plotting pipeline entry with the given .row_id by delta positions (given in indices, so -1 moves up)
function reorderPipelineStepMoveStably(row_id, delta){
for(var index_of_element_to_move = 0; index_of_element_to_move < plotting_pipeline.length; ++index_of_element_to_move){
var element = plotting_pipeline[index_of_element_to_move];
if(element.row_id === row_id){
break;
}
}
if(index_of_element_to_move == plotting_pipeline.length)
throw "could not find row_id '"+row_id+"' in plotting_pipeline";
while(delta <= -1 || delta >= 1){
var swappedElementIndex;
if(delta < 0){
if(index_of_element_to_move === 0){
return;
}
swappedElementIndex = index_of_element_to_move-1;
++delta;
}else{
if(index_of_element_to_move === plotting_pipeline.length-1){
return;
}
swappedElementIndex = index_of_element_to_move+1;
--delta;
}
reorderPipelineStepsSwap(index_of_element_to_move, swappedElementIndex);
index_of_element_to_move = swappedElementIndex;
}
}
const operations_ordering = {
assert_uniform: 0,
split_figures: 1,
split_groups: 2,
plot_x: 3,
split_figures_and_order_x_commit_strands: 4,
plot_y: 5,
plot_y_list: 6,
plot_y_aggregate_2: 7,
discard_merge: 8,
_length: 9,
};
//stably reorders the pipeline according to each step's operation, by their ordering in operations_ordered (implemented as stable counting sort)
function reorderPipelineAllAutoByOperation(){
//step one: count the number of pipeline steps with each available operation, and build a list pipeline_index -> bucket_index
const in_bucket_counts = [];
for(var operation_i = 0; operation_i < operations_ordering._length; ++operation_i){
in_bucket_counts[operation_i] = 0;
}
const to_bucket_list = [];
for(var pipeline_i = 0; pipeline_i < plotting_pipeline.length; ++pipeline_i){
var element = plotting_pipeline[pipeline_i];
var bucket_i = operations_ordering[element.operation];
if(bucket_i === undefined)
throw "unexpected operation '"+element.operation+"' at plotting_pipeline["+pipeline_i+"]"
to_bucket_list[pipeline_i] = bucket_i;
++in_bucket_counts[bucket_i];
}
//step two: sum individual counts to running totals
const running_total_counts = [];
var running_total_count = 0;
for(var bucket_i = 0; bucket_i < operations_ordering._length; ++bucket_i){
running_total_counts[bucket_i] = running_total_count;
running_total_count += in_bucket_counts[bucket_i];
}
//step three: calculate target positions
const to_list = [];
for(var pipeline_i = 0; pipeline_i < plotting_pipeline.length; ++pipeline_i){
const bucket_i = to_bucket_list[pipeline_i];
to_list[pipeline_i] = running_total_counts[bucket_i];
++running_total_counts[bucket_i];
}
//step four: swap elements until all are in their target positions
for(var pipeline_i = 0; pipeline_i < plotting_pipeline.length;){
const result_index = to_list[pipeline_i];
if(pipeline_i == result_index){
++pipeline_i;
}else{
reorderPipelineStepsSwap(pipeline_i, result_index);
to_list[pipeline_i] = to_list[result_index];
to_list[result_index] = result_index;
}
}
}
//asserts that only one repository has non-uniform commit hashes
//TODO: get rid of this?
function assertAllButOneRepoUseUniformCommits(metric_entries, property_overview, sort_by_repo_name){
for(var key in property_overview){
if(key.startsWith("REPO-GITCOMMITHASH-")){
var repo_name = key.substring(0, "REPO-GITCOMMITHASH-".length)
if(repo_name != sort_by_repo_name){
if(property_overview[key].values.second_unique !== undefined){
throw "non-excluded non-uniform git commit found in repository '"+repo_name+"'";
}
}
}
}
}
//sorts the given metric_entries by their REPO-GITCOMMITHASH-(sort_by_repo_name) values
//TODO: implement actual git history sorting instead of this (lexicographic sorting)
function sortByUniqueGitHistory(metric_entries, property_overview, sort_by_repo_name){
assertAllButOneRepoUseUniformCommits(metric_entries, property_overview, sort_by_repo_name);
gitcommithash_key = "REPO-GITCOMMITHASH-"+sort_by_repo_name;
metric_entries.sort(function(a, b) {
var commit_a = a[gitcommithash_key];
var commit_b = b[gitcommithash_key];
//TODO: Implement the actual sort we want
if(commit_a < commit_b)
return -1;
if(commit_a > commit_b)
return 1;
if(commit_a === commit_b)
return 0;
throw "uncomparable commits encountered: '"+commit_a+"' and '"+commit_b+"'";
});
return metric_entries;
}
//helper function; TODO: document
function alreadyOrJsonParseArray(value) {
const array = (typeof value) === 'string'
? JSON.parse(value)
: value;
if(!Array.isArray(array)){
throw "parsing JSON returned unexpected non-array: '"+array+"'";
}
return array;
}
function number_array_average(numbers) {
var total = 0;
for(number of numbers){
total += number;
}
return total/numbers.length;
}
function median(values) {
const number_tuples = [];
var tuple_index = 0;
for(const value of values){
const number = Number(value);
if(number === null) throw "failed to find median: list contained non-number";
if(number !== number) throw "failed to find median: list contained NaN";
number_tuples.push([number, tuple_index]);
++tuple_index;
}
number_tuples.sort(function(a, b){
if(a[0] < b[0]) return -1;
else if(a[0] > b[0]) return 1;
else return 0;
});
const half_len = number_tuples.length/2;
if(number_tuples.length % 2 == 0){
//we have to do math, can't keep it a string
return (number_tuples[half_len-1][0] + number_tuples[half_len][0])/2;
}else{
//keep it a string (in case it was one) if we can
return values[number_tuples[Math.floor(half_len)][1]];
}
}
//execute the plotting pipeline stored in the global variable plotting_pipeline on data (in-place);
//data has the following structure:
// {
// unattended_properties: [string] (a list of all properties not yet processed by the pipeline),
// plot_axis_x_ordering_property: string (the name of the property within entries the value of which is used to order the x position,
// presumably initially undefined),
// plot_axis_y_property_interfaces: [{name: string, get_for: fn(self, entry) value}]
// (a name and an interface to obtain the values of properties within entries, which are used as y value, presumably initially undefined),
// groups_by_figure: (a list with data of groups for each figure) [{
// prefix_string: string (the criteria identifying this figure among others),
// entry_groups: (a list of objects describing each group) [{
// prefix_string: string (the criteria identifying this group's entries within the figure among others),
// entries: [object] (the list of entries, are property dictionary objects, which make up this group),
// }]
// }],
// }
function executePipelineOperations(data){
for(var i = 0; i < plotting_pipeline.length; ++i){
var element = plotting_pipeline[i];
var operation = element.operation;
if(operation === undefined){
throw "non-operation element in pipeline at index "+i;
}
//check for duplicate keys
var key = element.property_key;
var unattended_index = data.unattended_properties.indexOf(key);
if(unattended_index === -1){
throw "key '"+key+"' has more than one operation in pipeline";
}
//mark this property as having been attended to
data.unattended_properties.splice(unattended_index, 1);
//do the respective operation
switch(operation){
case 'assert_uniform':
if(property_overview[key].values.second_unique !== undefined)
throw "value of key '"+key+"' not uniform";
break;
case 'split_figures':{
const original_figures_data = data.groups_by_figure;
var new_groups_by_figure = [];
for(var figure_i = 0; figure_i < original_figures_data.length; ++figure_i){
var original_figure = original_figures_data[figure_i];
var new_figures_by_value = {};
for(var group_i = 0; group_i < original_figure.entry_groups.length; ++group_i){
var new_groups_by_value = {};
var original_group = original_figure.entry_groups[group_i];
for(var entry_i = 0; entry_i < original_group.entries.length; ++entry_i){
var entry = original_group.entries[entry_i];
var value = entry[key];
var group = new_groups_by_value[value];
if(group === undefined){
group = {prefix_string: original_group.prefix_string, entries: []};
new_groups_by_value[value] = group;
var figure = new_figures_by_value[value];
if(figure === undefined){
figure = {
prefix_string: original_figure.prefix_string+key+"="+value+";",
entry_groups: [],
x_label: original_figure.x_label,
x_axis_commit_strand: original_figure.x_axis_commit_strand,
};
new_figures_by_value[value] = figure;
new_groups_by_figure.push(figure);
}
figure.entry_groups.push(group);
}
group.entries.push(entry);
}
}
}
data.groups_by_figure = new_groups_by_figure;
break;
}
case 'split_groups':
const group_index_lookup_by_value = new Map(); // we want to avoid a different ordering for the same value groups across graphs
var next_group_index = 0;
const original_figures_data = data.groups_by_figure;
for(const original_figure of original_figures_data){
const new_group_bucket_by_value = [];
for(const original_group of original_figure.entry_groups){
const new_groups_by_value = new Map();
for(const entry of original_group.entries){
const value = entry[key];
var group = new_groups_by_value.get(value);
if(group === undefined){
group = {prefix_string: original_group.prefix_string+key+"="+value+";", entries: []};
new_groups_by_value.set(value, group);
var bucket_index = group_index_lookup_by_value.get(value)
if(bucket_index === undefined){
bucket_index = next_group_index;
group_index_lookup_by_value.set(value, bucket_index);
++next_group_index;
}
var bucket = new_group_bucket_by_value[bucket_index];
if(bucket === undefined){
bucket = [];
new_group_bucket_by_value[bucket_index] = bucket;
}
bucket.push(group);
}
group.entries.push(entry);
}
}
original_figure.entry_groups = new_group_bucket_by_value.flat();
}
break;
case 'plot_x':
throw "unimplemented (todo: implement as order_x, plot_x is probably unnecessary)"
if(data.plot_axis_x_ordering_property !== undefined)
throw "plot already has key '"+data.plot_axis_x_ordering_property+"' set as x axis!";
data.plot_axis_x_ordering_property = key;
break;
case 'split_figures_and_order_x_commit_strands':
if(data.plot_axis_x_ordering_property !== undefined)
throw "plot already has key '"+data.plot_axis_x_ordering_property+"' set as x axis!";
data.plot_axis_x_ordering_property = key;
const expected_prefix = "REPO-GITCOMMITHASH-";
if(!key.startsWith(expected_prefix))
throw "unimplemented: key '"+key+"' does not start with 'REPO-GITCOMMITHASH-', don't know how to parse repository name";
const repository_name = key.substring(expected_prefix.length);
const commit_strands = loaded_commit_strands_by_repository_name.get(repository_name);
if((commit_strands === undefined) || (commit_strands.length === 0))
throw "no commit strands loaded for repository '"+repository_name+"' => no resulting figures!";
const loaded_commit_strands_by_commit = loaded_commit_strands_by_commit_by_repository_name.get(repository_name)
if(loaded_commit_strands_by_commit === undefined)
throw "unreachable: found commit strands, but not indexed by commit (should have happened upon loading commit strands)";
const loaded_commit_strand_coverage_not_in_strands = loaded_commit_strand_coverage_not_in_strands_by_repository_name.get(repository_name)
if(loaded_commit_strand_coverage_not_in_strands !== undefined){
alert("metric entries contain "+loaded_commit_strand_coverage_not_in_strands.count+" commits of repository '"+repository_name+"' not present in any loaded strands!\nThese will not appear in any plots:\n"+JSON.stringify(loaded_commit_strand_coverage_not_in_strands));
}
const loaded_commit_strand_coverage_not_in_metrics_by_strand = loaded_commit_strand_coverage_not_in_metrics_by_strand_by_repository_name.get(repository_name)
if(loaded_commit_strand_coverage_not_in_metrics_by_strand !== undefined){
for(var strand of loaded_commit_strand_coverage_not_in_metrics_by_strand.keys()){
const hashes = loaded_commit_strand_coverage_not_in_metrics_by_strand.get(strand)
alert("commit strand "+strand.strand_name+" of repository '"+repository_name+"' contains "+hashes.length+" commits not present in the loaded metrics:\n"+hashes.join("\n"));
}
}
//split figures, for each commit strand, by whether the commit is included
//and order groups by these strands
var new_groups_by_figure = [];
for(const original_figure of data.groups_by_figure){
if(original_figure.x_axis_commit_strand !== undefined){
alert("Error: figure already has a commit strand set as x axis: "+original_figure.x_axis_commit_strand);
}
var new_figures_by_strand = new Map();
for(const original_group of original_figure.entry_groups){
const new_groups_by_strand = new Map();
//splitting
for(var entry_i = 0; entry_i < original_group.entries.length; ++entry_i){
const entry = original_group.entries[entry_i];
const commit_hash = entry[key];
var strands = loaded_commit_strands_by_commit.get(commit_hash);
if(strands === undefined){
continue;
}
for(var strand_i = 0; strand_i < strands.length; ++strand_i){
const strand = strands[strand_i];
var group = new_groups_by_strand.get(strand);
if(group === undefined){
group = {prefix_string: original_group.prefix_string, entries: []};
new_groups_by_strand.set(strand, group);
var figure = new_figures_by_strand.get(strand);
if(figure === undefined){
figure = {
prefix_string: original_figure.prefix_string+"REPO-COMMITSTRAND-"+repository_name+"="+strand.strand_name+";",
entry_groups: [],
x_labels: Array.from(strand.commit_display_names.values()),
x_axis_commit_strand: strand,
};
new_figures_by_strand.set(strand, figure);
new_groups_by_figure.push(figure);
}
figure.entry_groups.push(group);
}
group.entries.push(entry);
}
}
}
}
data.groups_by_figure = new_groups_by_figure;
break;
case 'plot_y':
const standard_property_get_for = function(self, entry) {
return entry[self.name];
};
ensure_field_array(data, 'plot_axis_y_property_interfaces').push({name: key, get_for: standard_property_get_for});
break;
case 'plot_y_list':{
const base_name = key.endsWith("-list")
? key.slice(0, key.length - ("-list".length))
: key;
const shared_aggregate_data = {
key: key,
ensure_computed_for: function(entry) {
if(this.computed_for === entry) return;
const list_of_numbers = [];
try {
const input = entry[this.key];
const outer_parsed = alreadyOrJsonParseArray(input);
for(const value of outer_parsed) {
const number = Number(value);
if(number !== number){ //not a number
throw "non-number encountered of entry y to boxplot: "+value;
}
list_of_numbers.push(number);
}
} catch(exception) {