Source file plots.ml

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
(*********************************************************************************)
(*                Statocaml                                                      *)
(*                                                                               *)
(*    Copyright (C) 2025 INRIA All rights reserved.                              *)
(*    Author: Maxence Guesdon (INRIA Saclay)                                     *)
(*      with Gabriel Scherer (INRIA Paris) and Florian Angeletti (INRIA Paris)   *)
(*                                                                               *)
(*    This program is free software; you can redistribute it and/or modify       *)
(*    it under the terms of the GNU General Public License as                    *)
(*    published by the Free Software Foundation, version 3 of the License.       *)
(*                                                                               *)
(*    This program is distributed in the hope that it will be useful,            *)
(*    but WITHOUT ANY WARRANTY; without even the implied warranty of             *)
(*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the               *)
(*    GNU General Public License for more details.                               *)
(*                                                                               *)
(*    You should have received a copy of the GNU General Public                  *)
(*    License along with this program; if not, write to the Free Software        *)
(*    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA                   *)
(*    02111-1307  USA                                                            *)
(*                                                                               *)
(*    As a special exception, you have permission to link this program           *)
(*    with the OCaml compiler and distribute executables, as long as you         *)
(*    follow the requirements of the GNU GPL in regard to all of the             *)
(*    software in the executable aside from the OCaml compiler.                  *)
(*                                                                               *)
(*    Contact: Maxence.Guesdon@inria.fr                                          *)
(*                                                                               *)
(*********************************************************************************)

(** *)

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 (* no values *) 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 () = Lwt_io.(with_file ~mode:Output
              (Printf.sprintf "/tmp/spider%d.gnuplot" p.id)
              (fun oc -> write oc (Plot.code gp)))
              in*)
           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) ->
        (*if p.P.id = 167 then Log.warn (fun m -> m "%a" P.pp_dated 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 () = Lwt_io.(with_file ~mode:Output
              (Printf.sprintf "/tmp/spider-%d-%d.gnuplot" p.id year)
              (fun oc -> write oc (Plot.code gp)))
              in*)
           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) ;
      (*prerr_endline (Plot.code gp);*)
      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
      (*Utils.classes_of_data pr_days ;
      Utils.classes_of_data issue_days ;*)
      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
          (*~classes:[5;4;1;1;1]*) 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
          (*~classes:[5;4;1;1;1]*) 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