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
(** *)
module S = Statocaml
module GH = Statocaml_github
module Log = S.Log
module T = Statocaml_profile.T
module Make (P:T.S) =
struct
let bar_chart = Utils.bar_chart
let bar_chart_svg = Utils.bar_chart_svg
let gen_activity_graph title ?(w=600) ?(h=350) fdata profile =
let%lwt svg_file = Utils.lwt_temp_file ~suffix:".svg" () in
let gp = Plot.create ~terminal:"svg" ~title ~title_font_size:18 ~w ~h svg_file in
let p fmt = Plot.p gp fmt in
let by_year = P.dateds_by_year profile in
p "set style histogram clustered gap 2" ;
let xtics = let n = ref (-1) in
S.Imap.fold (fun y _ acc -> incr n; (Printf.sprintf "\"%d\" %d" y !n) :: acc)
by_year []
in
p "set xtics %s(%s)" (if List.length xtics > 10 then "rotate " else "")
(String.concat ", " (List.rev xtics));
p "set key bottom center outside";
let _, vmin, vmax, hists = List.fold_left
(fun (i, vmin, vmax, hists) (f, label, col) ->
let var = Printf.sprintf "data%d" i in
let l = S.Imap.fold
(fun _y dated acc -> f dated :: acc) by_year []
in
Plot.define_float_data gp var (List.rev l);
let h = Printf.sprintf
"$%s using 1 with histogram fs solid lc %S title '%s'" var col label
in
(i+1,
List.fold_left min vmin l,
List.fold_left max vmax l,
h :: hists)
) (0, max_float, min_float, []) fdata
in
let hists = List.rev hists in
if vmin = max_float then
Lwt.return []
else
(
let yrange =
if vmax -. vmin < 0.0001 then
match vmin with
| 0. -> None
| _ -> Some (if vmin < 0. then (vmin, 0.) else (0., vmax))
else
Some (vmin, vmax)
in
match yrange with
| None -> Lwt.return []
| Some (vmin, vmax) ->
p "set yrange [%d:%d]" (truncate vmin) (truncate(ceil vmax));
p "plot %s" (String.concat ", " hists);
let%lwt () = Plot.run gp in
Utils.svg_load_and_delete svg_file
)
let spider_title =
"Max value on each axis is the maximum value encountered for a contributor on the period"
let gen_profile_spider_subsys (stats:P.gstats) (max:P.max) =
let axe_of_ss ss (axes, ss_list) =
match P.Subs.Map.find_opt ss stats.g_subsys_commits with
| None -> (axes, ss_list)
| Some max_v ->
let hi =
match P.Subs.Map.find_opt ss max.max_subsys_commits with
| None -> assert false
| Some (v,_) -> float v
in
let axe = (P.Subs.to_string ss, (0., hi), max_v) in
axe :: axes, ss :: ss_list
in
let axes, ss_list = List.fold_right axe_of_ss P.Subs.list ([], []) in
let value_of_ss dated ss =
match P.Subs.Map.find_opt ss dated.P.subsys_commits with
| None -> 0.
| Some v -> v
in
fun _p (dated:P.dated) ->
let values = List.map (value_of_ss dated) ss_list in
if List.for_all ((=) 0.) values then
Lwt.return []
else
(
let%lwt svg_file = Utils.lwt_temp_file ~suffix:".svg" () in
let gp = Plot.create ~terminal:"svg" ~title:"Commits in subsystems"
~title_font_size:16 ~w:400 ~h:400 svg_file
in
Plot.spider_plot gp axes values;
let%lwt () = Plot.run gp in
Utils.svg_load_and_delete ~gnuplot_title:spider_title svg_file
)
let gen_profile_spider_activity (stats:P.gstats) (max:P.max) =
let sum_snd l = List.fold_right (fun (_,n) acc -> acc + n) l 0 in
let axes =
[
"Issues created", (0., float (fst max.max_issue_creator)), S.Imap.cardinal stats.g_issues ;
"Issues comments", (0., float (fst max.max_issue_commenter)), stats.g_issue_comments ;
"Issues reviewed", (0., float (fst max.max_issue_reviewer)), sum_snd stats.g_issue_reviewers;
"PRs created", (0., float (fst max.max_pr_creator)), S.Imap.cardinal stats.g_prs ;
"PRs comments", (0., float (fst max.max_pr_commenter)), stats.g_pr_comments ;
"PRs reviewed\\n(Github)", (0., float (fst max.max_pr_reviewer)), sum_snd stats.g_pr_reviewers ;
"PRs reviewed\\n(Changelog)",
(0., float (fst max.max_pr_reviewer_from_changelog)), sum_snd stats.g_pr_reviewers_from_changelog ;
"Commits", (0., float (fst max.max_committer)), S.Smap.cardinal stats.g_commits ;
]
in
let axes = List.map
(fun (t,(lo,hi),total) -> (t,(lo,hi),float total))
axes
in
fun _p (dated:P.dated) ->
let%lwt svg_file = Utils.lwt_temp_file ~suffix:".svg" () in
let values = [
float (S.Iset.cardinal dated.issues_created) ;
float (S.Iset.cardinal dated.issue_comments) ;
float (S.Iset.cardinal dated.issues_reviewed) ;
float (S.Iset.cardinal dated.prs_created) ;
float (S.Iset.cardinal dated.pr_comments) ;
float (S.Iset.cardinal dated.prs_reviewed) ;
float (S.Iset.cardinal dated.prs_reviewed_from_changelog) ;
float (S.Sset.cardinal dated.commits) ;
]
in
let axes, values = List.fold_right2
(fun ((_title,(_,hi),_) as axe) v ((axes,values) as acc) ->
match hi > 0. with
| true -> (axe::axes, v::values)
| _ -> acc
)
axes values ([], [])
in
if List.length axes < 3 then
Lwt.return []
else
(
let gp = Plot.create ~terminal:"svg" ~title:"Activity"
~title_font_size:16 ~w:400 ~h:400 svg_file
in
Plot.spider_plot gp axes values;
let%lwt () = Plot.run gp in
Utils.svg_load_and_delete ~gnuplot_title:spider_title svg_file
)
type monthly_activity_param =
{ after: Ptime.t option [@ocf Ocf.Wrapper.option Statocaml.Types.ptime_date_wrapper, None];
before: Ptime.t option [@ocf Ocf.Wrapper.option Statocaml.Types.ptime_date_wrapper, None];
show_commits : bool [@ocf Ocf.Wrapper.bool, true] ;
show_issues : bool [@ocf Ocf.Wrapper.bool, true] ;
show_prs : bool [@ocf Ocf.Wrapper.bool, true] ;
sliding_period : int option [@ocf Ocf.Wrapper.(option int), None ];
} [@@ocf]
let monthly_activity gp ?after ?before ?sliding_period
?(show_prs=true) ?(show_issues=true) ?(show_commits=true) (data:P.t) =
let open GH.Types in
let stats = T.Period.Map.(find All data.P.stats) in
let commits = List.sort (fun c1 c2 ->
Ptime.compare c1.git_commit.author.cu_date c2.git_commit.author.cu_date)
(List.map snd (S.Smap.bindings stats.g_commits))
in
let issues = List.sort (fun i1 i2 -> Ptime.compare i1.created_at i2.created_at)
(List.map snd (S.Imap.bindings stats.g_issues))
in
let prs = List.sort
(fun p1 p2 -> Ptime.compare p1.P.issue.created_at p2.issue.created_at)
(List.map snd (S.Imap.bindings stats.g_prs))
in
let month_in_interval = Plot.month_in_interval ?after ?before () in
let comp_closed i1 i2 =
match i1.closed_at, i2.closed_at with
| Some d1, Some d2 -> Ptime.compare d1 d2
| Some _, None -> -1
| None, Some _ -> 1
| _ -> 0
in
let closed_issues =
let l = List.filter
(fun i -> match i.state with `Closed -> true | `Open -> false)
issues
in
List.sort comp_closed l
in
let closed_prs =
let l = List.filter
(fun p -> match p.P.issue.state with `Closed -> true | `Open -> false)
prs
in
List.sort (fun p1 p2 -> comp_closed p1.P.issue p2.issue) l
in
let mk_by_month =
let f month acc i =
match month i with
| None -> acc
| Some m when not (month_in_interval m) -> acc
| Some (y,m) ->
match Plot.Mmap.find_opt (y,m) acc with
| None -> Plot.Mmap.add (y,m) 1 acc
| Some cpt -> Plot.Mmap.add (y, m) (cpt+1) acc
in
fun month list ->
List.fold_left (f month) Plot.Mmap.empty list
in
let commits_month = mk_by_month
(fun c -> let (y,m,_) = Ptime.to_date c.git_commit.author.cu_date in Some (y,m))
commits
in
let issues_month = mk_by_month
(fun i -> let (y,m,_) = Ptime.to_date i.created_at in Some (y,m))
issues
in
let prs_month = mk_by_month
(fun p -> let (y,m,_) = Ptime.to_date p.P.issue.created_at in Some (y,m))
prs
in
let closed_issues_month = mk_by_month
(fun i -> match i.closed_at with
| None -> None
| Some d -> let (y,m,_) = Ptime.to_date d in Some (y,m))
closed_issues
in
let closed_prs_month = mk_by_month
(fun p -> match p.P.issue.closed_at with
| None -> None
| Some d -> let (y,m,_) = Ptime.to_date d in Some (y,m))
closed_prs
in
let (min_month, max_month) = Plot.min_max_of_month_maps
[ commits_month ; issues_month ; closed_issues_month ; prs_month; closed_prs_month ]
in
let commits_month = Plot.add_missing_months ~default:0 min_month max_month commits_month in
let issues_month = Plot.add_missing_months ~default:0 min_month max_month issues_month in
let prs_month = Plot.add_missing_months ~default:0 min_month max_month prs_month in
let closed_issues_month =
Plot.add_missing_months ~default:0 min_month max_month closed_issues_month
in
let closed_prs_month =
Plot.add_missing_months ~default:0 min_month max_month closed_prs_month
in
let mk_year_steps = match sliding_period with
| None -> Utils.mk_year_steps ~div12:true
| Some p -> Utils.mk_sliding_sums ~div:true ~window_size:p
in
let commits_year_steps = mk_year_steps commits_month in
let issues_year_steps = mk_year_steps issues_month in
let closed_issues_year_steps = mk_year_steps closed_issues_month in
let prs_year_steps = mk_year_steps prs_month in
let closed_prs_year_steps = mk_year_steps closed_prs_month in
let commits_values = List.map (fun (_,v) -> float v) (Plot.Mmap.bindings commits_month) in
let issues_values = List.map (fun (_,v) -> float v) (Plot.Mmap.bindings issues_month) in
let closed_issues_values =
List.map (fun (_,v) -> float v) (Plot.Mmap.bindings closed_issues_month)
in
let prs_values = List.map (fun (_,v) -> float v) (Plot.Mmap.bindings prs_month) in
let closed_prs_values =
List.map (fun (_,v) -> float v) (Plot.Mmap.bindings closed_prs_month)
in
let all_year_steps =
List.map2 (+.)
(if show_commits then commits_year_steps else List.map (fun _ -> 0.) commits_year_steps)
(List.map2 (+.)
(if show_issues then
List.map2 (+.) issues_year_steps closed_issues_year_steps
else
List.map (fun _ -> 0.) issues_year_steps
)
(if show_prs then
List.map2 (+.) prs_year_steps closed_prs_year_steps
else
List.map (fun _ -> 0.) prs_year_steps
)
)
in
if show_commits then
(Plot.define_float_data gp "commits" commits_values ;
Plot.define_float_data gp "commits_yearsteps" commits_year_steps;
);
if show_issues then
(Plot.define_float_data gp "issues_open" issues_values ;
Plot.define_float_data gp "issues_closed" closed_issues_values ;
Plot.define_float_data gp "issues_yearsteps" issues_year_steps;
Plot.define_float_data gp "closed_issues_yearsteps" closed_issues_year_steps
);
if show_prs then
(
Plot.define_float_data gp "prs_open" prs_values ;
Plot.define_float_data gp "prs_closed" closed_prs_values ;
Plot.define_float_data gp "prs_yearsteps" prs_year_steps;
Plot.define_float_data gp "closed_prs_yearsteps" closed_prs_year_steps
);
Plot.define_float_data gp "all_yearsteps" all_year_steps;
let () = Utils.add_events_by_month gp min_month max_month data.orig_events in
Plot.gnuplot_init_for_months gp issues_month ;
Plot.p gp "set style histogram clustered gap 2" ;
let div = match sliding_period with None -> 12 | Some n -> n in
let sp = Printf.sprintf in
let plotted =
(if show_commits then
[ "$commits using 1 with histogram fs solid lc \"red\" title 'Commits'" ;
sp "$commits_yearsteps using 1 with histeps lc \"red\" lw 2 title 'Commits by year (/%d)'" div ;
]
else
[]
) @
(if show_issues then
[ "$issues_open using 1 with histogram fs solid lc \"blue\" title 'Issues open'" ;
"$issues_closed using 1 with histogram fs solid lc \"light-blue\" title 'Issues closed'" ;
sp "$issues_yearsteps using 1 with histeps lc \"blue\" lw 2 title 'Issues open by year (/%d)'" div;
sp "$closed_issues_yearsteps using 1 with histeps lc \"light-blue\" lw 2 title 'Issues closed by year (/%d)'" div;
]
else
[]
) @
(if show_prs then
[ "$prs_open using 1 with histogram fs solid lc \"dark-green\" title 'PRs open'" ;
"$prs_closed using 1 with histogram fs solid lc \"green\" title 'PRs closed'" ;
sp "$prs_yearsteps using 1 with histeps lc \"dark-green\" lw 2 title 'PRs open by year (/%d)'" div;
sp "$closed_prs_yearsteps using 1 with histeps lc \"green\" lw 2 title 'PRs closed by year (/%d)'" div;
]
else
[]
) @
[ sp "$all_yearsteps using 1 with histeps lc \"goldenrod\" lw 2 title 'All by year (/%d)'" div]
in
Plot.p gp "plot %s ;" (String.concat ", " plotted) ;
let%lwt () = Plot.run gp in
Lwt.return_unit
let monthly_activity_json =
let f gp p =
monthly_activity gp ?after:p.after ?before:p.before ?sliding_period:p.sliding_period
~show_commits:p.show_commits ~show_issues:p.show_issues ~show_prs:p.show_prs
in
Json.to_plotter monthly_activity_param_wrapper f
let date_title_complement ?after ?before () =
let to_str () (y,m,d) = Printf.sprintf "%04d/%02d/%02d" y m d in
match after, before with
| None, None -> None
| Some d, None -> Some (Printf.sprintf " (after %a)" to_str d)
| None, Some d -> Some (Printf.sprintf " (before %a)" to_str d)
| Some a, Some b ->
Some (Printf.sprintf " (between %a and %a)" to_str a to_str b)
let plot_activity_by_month ~outdir ?after ?before ?(prefix="") ?(events=[]) (issues:GH.Types.issue list) commits =
let open GH.Types in
let commits = List.sort (fun c1 c2 ->
Ptime.compare c1.git_commit.author.cu_date c2.git_commit.author.cu_date) commits in
let issues = List.sort (fun i1 i2 -> Ptime.compare i1.created_at i2.created_at) issues in
let month_in_interval = Plot.month_in_interval ?after ?before () in
let closed_issues =
let l = List.filter
(fun i -> match i.state with `Closed -> true | `Open -> false)
issues
in
List.sort (fun i1 i2 ->
match i1.closed_at, i2.closed_at with
| Some d1, Some d2 -> Ptime.compare d1 d2
| Some _, None -> -1
| None, Some _ -> 1
| _ -> 0)
l
in
let mk_by_month =
let f month acc i =
match month i with
| None -> acc
| Some m when not (month_in_interval m) -> acc
| Some (y,m) ->
match Plot.Mmap.find_opt (y,m) acc with
| None -> Plot.Mmap.add (y,m) 1 acc
| Some cpt -> Plot.Mmap.add (y, m) (cpt+1) acc
in
fun month list ->
List.fold_left (f month) Plot.Mmap.empty list
in
let commits_month = mk_by_month
(fun c -> let (y,m,_) = Ptime.to_date c.git_commit.author.cu_date in Some (y,m))
commits
in
let issues_month = mk_by_month
(fun i -> let (y,m,_) = Ptime.to_date i.created_at in Some (y,m))
issues
in
let closed_issues_month = mk_by_month
(fun i -> match i.closed_at with
| None -> None
| Some d -> let (y,m,_) = Ptime.to_date d in Some (y,m))
closed_issues
in
let (min_month, max_month) = Plot.min_max_of_month_maps
[ commits_month ; issues_month ; closed_issues_month ]
in
let commits_month = Plot.add_missing_months ~default:0 min_month max_month commits_month in
let issues_month = Plot.add_missing_months ~default:0 min_month max_month issues_month in
let closed_issues_month =
Plot.add_missing_months ~default:0 min_month max_month closed_issues_month
in
Log.debug (fun m -> m "min_month=(%d,%d), max_month=(%d,%d), issues_month:%d, closed_issues_month:%d"
(fst min_month) (snd min_month)
(fst max_month) (snd max_month)
(Plot.Mmap.cardinal issues_month)
(Plot.Mmap.cardinal closed_issues_month)
);
let commits_year_steps = Utils.mk_year_steps ~div12:true commits_month in
let issues_year_steps = Utils.mk_year_steps ~div12:true issues_month in
let closed_issues_year_steps = Utils.mk_year_steps ~div12:true closed_issues_month in
let commits_values = List.map (fun (_,v) -> float v) (Plot.Mmap.bindings commits_month) in
let issues_values = List.map (fun (_,v) -> float v) (Plot.Mmap.bindings issues_month) in
let closed_issues_values =
List.map (fun (_,v) -> float v) (Plot.Mmap.bindings closed_issues_month)
in
let all_year_steps =
List.map2 (+.) commits_year_steps
(List.map2 (+.) issues_year_steps closed_issues_year_steps)
in
let width = 30 * (Plot.Mmap.cardinal issues_month) in
let outfile = prefix ^ "activity.png" in
Log.info (fun m -> m "Creating %s" outfile);
let gp = Plot.create ~w:width ~h:1000 ~title:"Activity" (Filename.concat outdir outfile) in
Plot.define_float_data gp "commits" commits_values ;
Plot.define_float_data gp "issues_open" issues_values ;
Plot.define_float_data gp "issues_closed" closed_issues_values ;
Plot.define_float_data gp "commits_yearsteps" commits_year_steps;
Plot.define_float_data gp "issues_yearsteps" issues_year_steps;
Plot.define_float_data gp "closed_issues_yearsteps" closed_issues_year_steps;
Plot.define_float_data gp "all_yearsteps" all_year_steps;
let () = Utils.add_events_by_month gp min_month max_month events in
Plot.gnuplot_init_for_months gp issues_month ;
Plot.p gp "set style histogram clustered gap 2" ;
Plot.p gp "plot $commits using 1 with histogram fs solid lc \"red\" title 'Commits', $issues_open using 1 with histogram fs solid lc \"blue\" title 'Issues open', $issues_closed using 1 with histogram fs solid lc \"green\" title 'Issues closed', $commits_yearsteps using 1 with histeps lc \"red\" lw 2 title 'Commits by year (/12)', $issues_yearsteps using 1 with histeps lc \"blue\" lw 2 title 'Issues open by year (/12)', $closed_issues_yearsteps using 1 with histeps lc \"green\" lw 2 title 'Issues closed by year (/12)', $all_yearsteps using 1 with histeps lc \"goldenrod\" lw 2 title 'All by year (/12)'";
let%lwt () = Plot.run gp in
Lwt.return ("Activity", outfile)
let plot_issue_closing_delays ~outdir ?after ?before ?(prefix="") ?(events=[]) (issues:GH.Types.issue list) =
let open GH.Types in
let month_in_interval = Plot.month_in_interval ?after ?before () in
let issues = List.filter
(fun i ->
let (y,m,_) = Ptime.to_date i.created_at in
month_in_interval (y,m)
)
issues
in
let issues =
List.sort (fun i1 i2 -> Ptime.compare i1.created_at i2.created_at) issues
in
let _issues_open, issues_closed =
List.partition
(fun i ->
match i.state, i.closed_at with
| `Open, _
| _, None -> true
| `Closed, Some _ -> false
)
issues
in
let issues_delays =
List.map (fun i ->
match i.closed_at with
| None -> assert false
| Some d_closed ->
let span = Ptime.diff d_closed i.created_at in
let delay = Ptime.Span.to_float_s span /. (24. *. 3600.) in
(i, delay)
)
issues_closed
in
let map =
let f acc (i, delay) =
let (y,m,_) = Ptime.to_date i.created_at in
match Plot.Mmap.find_opt (y,m) acc with
| None -> Plot.Mmap.add (y,m) [delay] acc
| Some delays -> Plot.Mmap.add (y, m) (delay :: delays) acc
in
List.fold_left f Plot.Mmap.empty issues_delays
in
let map_open =
let f acc i =
let (y,m,_) = Ptime.to_date i.created_at in
match Plot.Mmap.find_opt (y,m) acc with
| None -> Plot.Mmap.add (y,m) 1 acc
| Some cpt -> Plot.Mmap.add (y, m) (cpt+1) acc
in
List.fold_left f Plot.Mmap.empty issues_closed
in
let (min_month, max_month) = Plot.min_max_of_month_maps [map] in
let map = Plot.add_missing_months ~default:[] min_month max_month map in
let map_open = Plot.add_missing_months ~default:0 min_month max_month map_open in
Log.debug (fun m -> m "min_month=(%d,%d), max_month=(%d,%d), map:%d"
(fst min_month) (snd min_month)
(fst max_month) (snd max_month)
(Plot.Mmap.cardinal map)
);
let map = Plot.Mmap.map
(function
| [] -> 0.
| l ->
let sum = List.fold_left (+.) 0. l in
sum /. (float (List.length l))
)
map
in
let width = 20 * (Plot.Mmap.cardinal map) in
let outfile = prefix ^ "closing_delays.png" in
Log.info (fun m -> m "Creating %s" outfile);
let gp = Plot.create ~w:width ~h:1000
~title:"Average delay (in days) to close an issue, by opening month"
(Filename.concat outdir outfile)
in
let () = Utils.add_events_by_month gp min_month max_month events in
Plot.gnuplot_init_for_months gp map ;
Plot.define_float_data gp "delays" (List.map snd (Plot.Mmap.bindings map));
Plot.define_float_data gp "issues_not_closed"
(List.map (fun (_,v) -> float v) (Plot.Mmap.bindings map_open));
Plot.p gp "plot $delays using 1 with histogram fs solid lc \"red\" title 'Delay to close issue', $issues_not_closed using 1 with histogram fs solid lc \"blue\" title 'Issues not closed yet'";
let%lwt () = Plot.run gp in
Lwt.return ("Issues delays", outfile)
let plot_first_commit_by_month ~outdir ?after ?before ?(prefix="") ?(events=[]) first_user_commit =
let open GH.Types in
let dates = List.sort Stdlib.compare
(List.map (fun (_, date) ->
let (y,m,_) = Ptime.to_date date in (y,m))
(S.Smap.bindings first_user_commit))
in
let month_in_interval = Plot.month_in_interval ?after ?before () in
let dates = List.filter month_in_interval dates in
let by_month =
let f acc month =
match Plot.Mmap.find_opt month acc with
| None -> Plot.Mmap.add month 1 acc
| Some cpt -> Plot.Mmap.add month (cpt+1) acc
in
List.fold_left f Plot.Mmap.empty dates
in
let (min_month, max_month) = Plot.min_max_of_month_maps [ by_month ] in
let by_month = Plot.add_missing_months ~default:0 min_month max_month by_month in
Log.debug (fun m -> m "min_month=(%d,%d), max_month=(%d,%d), by_month:%d"
(fst min_month) (snd min_month)
(fst max_month) (snd max_month)
(Plot.Mmap.cardinal by_month)
);
let values = List.map (fun (_,v) -> float v) (Plot.Mmap.bindings by_month) in
let (_, sums) = List.fold_left
(fun (sum, acc) v -> let sum = v+.sum in (sum, sum::acc))
(0., []) values
in
let sums = List.rev sums in
let year_steps = Utils.mk_year_steps by_month in
let width = 14 * (Plot.Mmap.cardinal by_month) in
let outfile = prefix ^ "first_commit_by_month.png" in
let gp = Plot.create ~w:width ~h:500 ~title:"Number of first commit authors"
(Filename.concat outdir outfile)
in
Log.info (fun m -> m "Creating %s" outfile);
let () = Utils.add_events_by_month gp min_month max_month events in
Plot.gnuplot_init_for_months gp ~fontsize:10 by_month ;
Plot.define_float_data gp "values" values ;
let y_max = List.fold_left max 0. year_steps in
let y2_max = List.fold_left max 0. sums in
Plot.p gp "set yrange [0:%d]" (truncate y_max + 1);
Plot.p gp "set y2range [0:%d]" (truncate y2_max + 1);
Plot.p gp "set y2label 'Total committers'";
Plot.p gp "set y2tics auto";
Plot.define_float_data gp "sums" sums;
Plot.define_float_data gp "yearsteps" year_steps;
Plot.p gp "plot $sums axes x1y2 with lines lc \"red\" title \"Total of new authors\", $values using 1 with histogram fs solid lc \"green\" title \"Number of first commit authors\", $yearsteps using 1 with histeps lc \"blue\" title \"Number of first commit authors by year\"";
let%lwt () = Plot.run gp in
Lwt.return ("Number of first commit authors", outfile)
module Closing = Closing.Make(P)
let plot_closing_delay_distribution ~outdir ?(prefix="") data =
let pr_days =
let map = Closing.prs_closing_days data in
List.map snd (S.Imap.bindings map)
in
let pr_days = List.sort Float.compare pr_days in
let issue_days =
let map = Closing.issues_closing_days data in
List.map snd (S.Imap.bindings map)
in
let issue_days = List.sort Float.compare issue_days in
let outfile = prefix ^ "closing_delay_distribution.png" in
let gp = Plot.create ~w:2500 ~h:2500 ~title:"Distributions of issues/PRs closing delay"
(Filename.concat outdir outfile)
in
Plot.p gp "set ylabel %S" "Closing delay (in days)";
Plot.define_float_data gp "prs" pr_days ;
Plot.define_float_data gp "issues" issue_days ;
let pr_mean = Option.value ~default:0. (Utils.float_mean pr_days) in
let pr_median = Option.value ~default:0. (Utils.float_median ~sorted:true pr_days) in
let issue_mean = Option.value ~default:0. (Utils.float_mean issue_days) in
let issue_median = Option.value ~default:0. (Utils.float_median ~sorted:true issue_days) in
Plot.p gp "plot \
$prs title \"Days to close PR (mean=%.1f, median=%.1f)\",\
$issues title \"Days to close issue (mean=%.1f, median=%.1f)\""
pr_mean pr_median issue_mean issue_median;
let%lwt () = Plot.run gp in
let%lwt pr_chart =
let title = "Number of PRs by closing delay" in
let file = prefix ^ "pr_closing_delay_quantiles.svg" in
let gp = Plot.create ~terminal:"svg" ~title ~w:600 ~h:600
(Filename.concat outdir file)
in
let%lwt () = Closing.plot_closing_delays_by_kind ~kind:`PR gp
data
in
Lwt.return (title, file)
in
let%lwt issue_chart =
let title = "Number of issues by closing delay" in
let file = prefix ^ "issue_closing_delay_quantiles.svg" in
let gp = Plot.create ~terminal:"svg" ~title ~w:600 ~h:600
(Filename.concat outdir file)
in
let%lwt () = Closing.plot_closing_delays_by_kind ~kind:`Issue gp
data
in
Lwt.return (title, file)
in
Lwt.return ["Issue/PRs closing delays", outfile ; pr_chart ; issue_chart ]
module Contrib = Contrib.Make(P)
module R = Json.Register(P)
let plotters = [
"pr_closing_delays", Closing.plot_closing_delays_json ~kind:`PR ;
"issue_closing_delays", Closing.plot_closing_delays_json ~kind:`Issue ;
"pr_cohorts", Closing.plot_pr_cohorts_json ;
"issue_cohorts", Closing.plot_issue_cohorts_json ;
"monthly_activity", monthly_activity_json ;
"reviewers_cloud", Contrib.plot_json ;
]
let () = List.iter (fun (name, f) -> R.add_plotter name f) plotters
let generate_from_json ?dir json data =
match json with
| `List l -> Lwt_list.map_s (fun json -> R.plot_from_json ?dir json data) l
| `Assoc _ -> let%lwt x = R.plot_from_json ?dir json data in Lwt.return [x]
| _ -> failwith (Printf.sprintf "Invalid json to generate plots: %s" (Yojson.Safe.pretty_to_string json))
end