@@ -7,12 +7,15 @@ package devapp
7
7
import (
8
8
"encoding/json"
9
9
"fmt"
10
+ "html/template"
10
11
"image/color"
12
+ "io/ioutil"
11
13
"math"
12
14
"net/http"
13
15
"regexp"
14
16
"sort"
15
17
"strconv"
18
+ "strings"
16
19
"time"
17
20
18
21
"golang.org/x/build/godash"
@@ -69,6 +72,37 @@ func updateStats(ctx context.Context, w http.ResponseWriter, r *http.Request) er
69
72
return writeCache (ctx , "gzstats" , stats )
70
73
}
71
74
75
+ func release (ctx context.Context , w http.ResponseWriter , req * http.Request ) error {
76
+ req .ParseForm ()
77
+
78
+ tmpl , err := ioutil .ReadFile ("template/release.html" )
79
+ if err != nil {
80
+ return err
81
+ }
82
+
83
+ t , err := template .New ("main" ).Parse (string (tmpl ))
84
+ if err != nil {
85
+ return err
86
+ }
87
+
88
+ cycle , _ , err := argtoi (req , "cycle" )
89
+ if err != nil {
90
+ return err
91
+ }
92
+ if cycle == 0 {
93
+ data , err := loadData (ctx )
94
+ if err != nil {
95
+ return err
96
+ }
97
+ cycle = data .GoReleaseCycle
98
+ }
99
+
100
+ if err := t .Execute (w , struct { GoReleaseCycle int }{cycle }); err != nil {
101
+ return err
102
+ }
103
+ return nil
104
+ }
105
+
72
106
func rawHandler (w http.ResponseWriter , r * http.Request ) {
73
107
ctx := appengine .NewContext (r )
74
108
@@ -143,19 +177,23 @@ func (s countChangeSlice) Less(i, j int) bool { return s[i].t.Before(s[j].t) }
143
177
// were open at each time. This produces columns called "Time" and
144
178
// "Count".
145
179
type openCount struct {
146
- // ByRelease will add a Release column and provide counts per release.
147
- ByRelease bool
180
+ // By is the column to group by; if "" all issues will be
181
+ // grouped together. Only "Release" and "Milestone" are
182
+ // supported.
183
+ By string
148
184
}
149
185
150
186
func (o openCount ) F (input table.Grouping ) table.Grouping {
151
187
return table .MapTables (input , func (_ table.GroupID , t * table.Table ) * table.Table {
152
- releases := make (map [string ]countChangeSlice )
188
+ groups := make (map [string ]countChangeSlice )
153
189
add := func (milestone string , t time.Time , count int ) {
154
- r := milestoneToRelease (milestone )
155
- if r == "" {
156
- r = milestone
190
+ if o .By == "Release" {
191
+ r := milestoneToRelease (milestone )
192
+ if r != "" {
193
+ milestone = r
194
+ }
157
195
}
158
- releases [ r ] = append (releases [ r ], countChange {t , count })
196
+ groups [ milestone ] = append (groups [ milestone ], countChange {t , count })
159
197
}
160
198
161
199
created := t .MustColumn ("Created" ).([]time.Time )
@@ -189,9 +227,9 @@ func (o openCount) F(input table.Grouping) table.Grouping {
189
227
190
228
var times []time.Time
191
229
var counts []int
192
- if o .ByRelease {
230
+ if o .By != "" {
193
231
var names []string
194
- for name , s := range releases {
232
+ for name , s := range groups {
195
233
sort .Sort (s )
196
234
sum := 0
197
235
for _ , c := range s {
@@ -201,10 +239,10 @@ func (o openCount) F(input table.Grouping) table.Grouping {
201
239
counts = append (counts , sum )
202
240
}
203
241
}
204
- nt .Add ("Release" , names )
242
+ nt .Add (o . By , names )
205
243
} else {
206
244
var all countChangeSlice
207
- for _ , s := range releases {
245
+ for _ , s := range groups {
208
246
all = append (all , s ... )
209
247
}
210
248
sort .Sort (all )
@@ -308,25 +346,34 @@ func plot(w http.ResponseWriter, req *http.Request, stats table.Grouping) error
308
346
}
309
347
switch pivot := req .Form .Get ("pivot" ); pivot {
310
348
case "opencount" :
311
- byRelease := req .Form .Get ("group" ) == "release"
312
- plot .Stat (openCount {ByRelease : byRelease })
313
- if byRelease {
314
- plot .GroupBy ("Release" )
349
+ o := openCount {}
350
+ switch by := req .Form .Get ("group" ); by {
351
+ case "release" :
352
+ o .By = "Release"
353
+ case "milestone" :
354
+ o .By = "Milestone"
355
+ case "" :
356
+ default :
357
+ return fmt .Errorf ("unknown group %q" , by )
358
+ }
359
+ plot .Stat (o )
360
+ if o .By != "" {
361
+ plot .GroupBy (o .By )
315
362
}
316
363
plot .SortBy ("Time" )
317
364
lp := gg.LayerPaths {
318
365
X : "Time" ,
319
366
Y : "Count" ,
320
367
}
321
- if byRelease {
322
- lp .Color = "Release"
368
+ if o . By != "" {
369
+ lp .Color = o . By
323
370
}
324
371
plot .Add (gg.LayerSteps {LayerPaths : lp })
325
- if byRelease {
372
+ if o . By != "" {
326
373
plot .Add (gg.LayerTooltips {
327
374
X : "Time" ,
328
375
Y : "Count" ,
329
- Label : "Release" ,
376
+ Label : o . By ,
330
377
})
331
378
}
332
379
case "" :
@@ -456,3 +503,99 @@ func plot(w http.ResponseWriter, req *http.Request, stats table.Grouping) error
456
503
plot .WriteSVG (w , 1200 , 600 )
457
504
return nil
458
505
}
506
+
507
+ func releaseData (ctx context.Context , w http.ResponseWriter , req * http.Request ) error {
508
+ req .ParseForm ()
509
+
510
+ stats := & godash.Stats {}
511
+ if err := loadCache (ctx , "gzstats" , stats ); err != nil {
512
+ return err
513
+ }
514
+
515
+ cycle , _ , err := argtoi (req , "cycle" )
516
+ if err != nil {
517
+ return err
518
+ }
519
+ if cycle == 0 {
520
+ data , err := loadData (ctx )
521
+ if err != nil {
522
+ return err
523
+ }
524
+ cycle = data .GoReleaseCycle
525
+ }
526
+
527
+ prefix := fmt .Sprintf ("Go1.%d" , cycle )
528
+
529
+ w .Header ().Set ("Content-Type" , "application/javascript" )
530
+
531
+ g := gdstats .IssueStats (stats )
532
+ g = openCount {By : "Milestone" }.F (g )
533
+ g = table .Filter (g , func (m string ) bool { return strings .HasPrefix (m , prefix ) }, "Milestone" )
534
+ g = table .SortBy (g , "Time" )
535
+
536
+ // Dump data; remember that each row only affects one count, so we need to hold the counts from the previous row. Kind of like Pivot.
537
+ data := [][]interface {}{{"Date" }}
538
+ counts := make (map [string ]int )
539
+ var (
540
+ maxt time.Time
541
+ maxc int
542
+ )
543
+ for _ , gid := range g .Tables () {
544
+ // Find all the milestones that exist
545
+ ms := g .Table (gid ).MustColumn ("Milestone" ).([]string )
546
+ for _ , m := range ms {
547
+ counts [m ] = 0
548
+ }
549
+ // Find the peak of the graph
550
+ ts := g .Table (gid ).MustColumn ("Time" ).([]time.Time )
551
+ cs := g .Table (gid ).MustColumn ("Count" ).([]int )
552
+ for i , c := range cs {
553
+ if c > maxc {
554
+ maxc = c
555
+ maxt = ts [i ]
556
+ }
557
+ }
558
+ }
559
+
560
+ // Only show the most recent 6 months of data.
561
+ start := maxt .Add (time .Duration (- 6 * 30 * 24 * time .Hour ))
562
+ g = table .Filter (g , func (t time.Time ) bool { return t .After (start ) }, "Time" )
563
+
564
+ milestones := []string {prefix + "Early" , prefix , prefix + "Maybe" }
565
+ for m := range counts {
566
+ switch m {
567
+ case prefix + "Early" , prefix , prefix + "Maybe" :
568
+ default :
569
+ milestones = append (milestones , m )
570
+ }
571
+ }
572
+ for _ , m := range milestones {
573
+ data [0 ] = append (data [0 ], m )
574
+ }
575
+ for _ , gid := range g .Tables () {
576
+ t := g .Table (gid )
577
+ time := t .MustColumn ("Time" ).([]time.Time )
578
+ milestone := t .MustColumn ("Milestone" ).([]string )
579
+ count := t .MustColumn ("Count" ).([]int )
580
+ for i := range time {
581
+ counts [milestone [i ]] = count [i ]
582
+ row := []interface {}{time [i ].UnixNano () / 1e6 }
583
+ for _ , m := range milestones {
584
+ row = append (row , counts [m ])
585
+ }
586
+ data = append (data , row )
587
+ }
588
+ }
589
+ fmt .Fprintf (w , "var ReleaseData = " )
590
+ if err := json .NewEncoder (w ).Encode (data ); err != nil {
591
+ return err
592
+ }
593
+ fmt .Fprintf (w , ";\n " )
594
+ fmt .Fprintf (w , `
595
+ ReleaseData.map(function(row, i) {
596
+ if (i > 0) {
597
+ row[0] = new Date(row[0])
598
+ }
599
+ });` )
600
+ return nil
601
+ }
0 commit comments