You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@community.apache.org by hu...@apache.org on 2019/08/06 09:02:11 UTC

svn commit: r1864472 [2/2] - in /comdev/reporter.apache.org/trunk/site/wizard/js: source/statistics.js source/statistics_generator.js wizard.js

Modified: comdev/reporter.apache.org/trunk/site/wizard/js/wizard.js
URL: http://svn.apache.org/viewvc/comdev/reporter.apache.org/trunk/site/wizard/js/wizard.js?rev=1864472&r1=1864471&r2=1864472&view=diff
==============================================================================
--- comdev/reporter.apache.org/trunk/site/wizard/js/wizard.js (original)
+++ comdev/reporter.apache.org/trunk/site/wizard/js/wizard.js Tue Aug  6 09:02:11 2019
@@ -2091,51 +2091,49 @@ function toggleView(id) {
  Fetched from source/statistics_generator.js
 ******************************************/
 
-
-
 function statistics_roster(pdata) {
     // PMC age
-    let founded = moment((pdata.pmcdates[project].pmc[2]||pdata.pmcdates[project].pmc[1]) * 1000.0);
+    let founded = moment((pdata.pmcdates[project].pmc[2] || pdata.pmcdates[project].pmc[1]) * 1000.0);
     let age = founded.fromNow();
-    
+
     // PMC and committer count
     let no_com = pdata.count[project][1];
     let no_pmc = pdata.count[project][0];
-    
+
     let y1 = no_com;
     let y2 = no_pmc;
     let cpr = "";
-    
+
     // See if we can get a clean ratio
     let k = gcd(y1, y2);
     y1 /= k;
     y2 /= k;
-    if (y1 < 10 && y2 < 10) cpr = "%u:%u".format(y1,y2);
-    
+    if (y1 < 10 && y2 < 10) cpr = "%u:%u".format(y1, y2);
+
     // Nope, let's rough it up a bit.
     else {
-      // While >= 10 committers, halven both committers and pmc
-      // to get a simpler number to "mathify".
-      while (y1 >= 10) {
-          y1 = Math.round(y1/2)
-          y2 = Math.round(y2/2);
-      }
-      // round up/down
-      y1 = Math.round(y1);
-      y2 = Math.round(y2);
-      
-      // find greatest common divisor and make the final fraction
-      let k = gcd(y1, y2);
-      y1 /= k;
-      y2 /= k;
-      cpr = "roughly %u:%u".format(y1,y2);
+        // While >= 10 committers, halven both committers and pmc
+        // to get a simpler number to "mathify".
+        while (y1 >= 10) {
+            y1 = Math.round(y1 / 2)
+            y2 = Math.round(y2 / 2);
+        }
+        // round up/down
+        y1 = Math.round(y1);
+        y2 = Math.round(y2);
+
+        // find greatest common divisor and make the final fraction
+        let k = gcd(y1, y2);
+        y1 /= k;
+        y2 /= k;
+        cpr = "roughly %u:%u".format(y1, y2);
     }
-    
-    let txt = "<h3>Project Composition:</h3>";
+
+    let txt = "<h4>Project Composition:</h4>";
     txt += "<ul><li>There are currently %u committers and %u PMC members in this project.</li><li>The Committer-to-PMC ratio is %s.</li></ul>".format(no_com, no_pmc, cpr);
-    
-    
-    
+
+
+
     // Last PMC addition
     let changes = pdata.pmcdates[project].roster;
     let now = moment();
@@ -2146,8 +2144,8 @@ function statistics_roster(pdata) {
     for (var availid in changes) {
         let change = changes[availid];
         let name = change[0];
-        let added = moment(change[1]*1000.0);
-        if (!last_added || last_added[1] < change[1])  {
+        let added = moment(change[1] * 1000.0);
+        if (!last_added || last_added[1] < change[1]) {
             last_added = change;
         }
         if (added.isAfter(three_months_ago) && added.format('YYYY-MM-DD') != founded.format('YYYY-MM-DD')) {
@@ -2155,19 +2153,18 @@ function statistics_roster(pdata) {
             txt += "<li>%s was added to the PMC on %s</li>".format(name, added.format('YYYY-MM-DD'));
         }
     }
-    
+
     if (!no_added) {
         if (founded.isAfter(three_months_ago)) {
-          txt += "<li>-No new PMC members (project graduated recently).</li>";
-        }
-        else if (last_added) {
-            txt += "<li>No new PMC members. Last addition was %s on %s.</li>".format(last_added[0], moment(last_added[1]*1000.0).format('YYYY-MM-DD'));
+            txt += "<li>-No new PMC members (project graduated recently).</li>";
+        } else if (last_added) {
+            txt += "<li>No new PMC members. Last addition was %s on %s.</li>".format(last_added[0], moment(last_added[1] * 1000.0).format('YYYY-MM-DD'));
         } else {
-          txt += "<li>No new PMC members were added.</li>";
+            txt += "<li>No new PMC members were added.</li>";
         }
     }
-    
-    
+
+
     // Last Committer addition
     changes = pdata.changes[project].committer;
     now = moment();
@@ -2177,8 +2174,8 @@ function statistics_roster(pdata) {
     for (var availid in changes) {
         let change = changes[availid];
         let name = change[0];
-        let added = moment(change[1]*1000.0);
-        if (!last_added || last_added[1] < change[1])  {
+        let added = moment(change[1] * 1000.0);
+        if (!last_added || last_added[1] < change[1]) {
             last_added = change;
         }
         if (added.isAfter(three_months_ago)) {
@@ -2186,12 +2183,12 @@ function statistics_roster(pdata) {
             txt += "<li>%s was added as committer on %s</li>".format(name, added.format('YYYY-MM-DD'));
         }
     }
-    
+
     if (!no_added) {
         if (last_added) {
-            txt += "<li>No new committers. Last addition was %s on %s.</li>".format(last_added[0], moment(last_added[1]*1000.0).format('YYYY-MM-DD'));
+            txt += "<li>No new committers. Last addition was %s on %s.</li>".format(last_added[0], moment(last_added[1] * 1000.0).format('YYYY-MM-DD'));
         } else {
-          txt += "<li>No new committers were added.</li>";
+            txt += "<li>No new committers were added.</li>";
         }
     }
     txt += "</ul>";
@@ -2199,326 +2196,736 @@ function statistics_roster(pdata) {
 }
 
 function statistics_meta(data) {
-    let founded = moment((data.pmcdates[project].pmc[2]||data.pmcdates[project].pmc[1]) * 1000.0);
+    let txt = "<h4>Base Data:</h4>";
+    let founded = moment((data.pmcdates[project].pmc[2] || data.pmcdates[project].pmc[1]) * 1000.0);
     let age = founded.fromNow();
-    let txt = "<b>Founded: </b>%s (%s)<br/>".format(founded.format('YYYY-MM-DD'), age);
+    txt += "<b>Founded: </b>%s (%s)<br/>".format(founded.format('YYYY-MM-DD'), age);
     txt += "<b>Chair: </b> %s<br/>".format(data.pdata[project].chair);
     txt += getReportDate(cycles, project);
-    
+
     return txt;
 }
 
 
 function statistics_health(data) {
-    let html= new HTML('div', {style: {position: 'relative', clear: 'both'}});
-    html.inject(new HTML('h3', {}, "Community Health Metrics:"))
+    let html = new HTML('div', {
+        style: {
+            position: 'relative',
+            clear: 'both'
+        }
+    });
+    html.inject(new HTML('h4', {}, "Community Health Metrics:"))
     document.body.inject(html);
     let txt = "";
     // Mailing list changes
     for (var ml in data.delivery[project]) {
-      
-        let xhtml = new HTML('div', {style: {position: 'relative', clear: 'both'}});
+
+        let xhtml = new HTML('div', {
+            style: {
+                position: 'relative',
+                clear: 'both'
+            }
+        });
         let mldata = data.delivery[project][ml];
         let a = ml.split('-', 2);
         ml = "%s@%s.apache.org".format(a[1], a[0]);
         if (a[1].match(/commits|cvs|announce/)) { // we already count commits, so...
-          continue;
+            continue;
         }
         txt = "";
-        let pct_change =Math.floor( 100 * ( (mldata.quarterly[0] - mldata.quarterly[1]) / (mldata.quarterly[1]*1.0) ));
+        let pct_change = Math.floor(100 * ((mldata.quarterly[0] - mldata.quarterly[1]) / (mldata.quarterly[1] * 1.0)));
         let pct_change_txt = "%u%".format(Math.abs(pct_change));
         if (isNaN(pct_change) || !isFinite(pct_change)) {
             pct_change_txt = 'big';
         }
         if (pct_change > 25 && mldata.quarterly[0] > 5) {
             txt += "<h6 style='color: #080'>%s had a %s increase in traffic in the past quarter (%u emails compared to %u):</h6>".format(ml, pct_change_txt, mldata.quarterly[0], mldata.quarterly[1]);
-        }
-        else if (pct_change < -25  && mldata.quarterly[1] > 5) {
+        } else if (pct_change < -25 && mldata.quarterly[1] > 5) {
             txt += "<h6 style='color: #800'>%s had a %s decrease in traffic in the past quarter (%u emails compared to %u):</h6>".format(ml, pct_change_txt, mldata.quarterly[0], mldata.quarterly[1]);
         }
-        
+
         xhtml.innerHTML = txt;
         html.inject(xhtml);
-        
+
         if (txt.length > 0) {
-          let cols = [
-                      ['x'],
-                      ['%s@'.format(a[1])]
-                     ];
-          for (var i = 0; i < 24; i++) {
-             let date = moment.utc().subtract(i, 'weeks').startOf('week').weekday(4);
-             cols[0].push(date);
-             console.log(date.unix())
-             cols[1].push(mldata.weekly[date.unix()]||0);
-          }
-          let chartdiv = new HTML('div', { style: {clear: 'both', width: '620px', height: '220px', position: 'relative', background: '#FFF', borderRadius: '5px', border: '0.75px solid #333'}});
-          xhtml.inject(chartdiv);
-          let chart = c3.generate({
-              bindto: chartdiv,
-              axis: { x: { type: 'timeseries', tick: {count: 12, format: (x) => {return moment(x).format('MMM D, YYYY');}}}},
-              data: {
-                x: 'x',
-                type: 'bar',
-                columns: cols
-              },
-              bar: {
-                width: {
-                    ratio: 0.35
-                }
-              },
-              tooltip: {
-                format: {
-                    value: (x) => '%u emails'.format(x),
-                    title: (x) => 'Week %s'.format(moment(x).format('W, YYYY'))
-                }
-              }
-          });
-          xhtml.inject(new HTML('br'))
+            let cols = [
+                ['x'],
+                ['%s@'.format(a[1])]
+            ];
+            for (var i = 0; i < 24; i++) {
+                let date = moment.utc().subtract(i, 'weeks').startOf('week').weekday(4);
+                cols[0].push(date);
+                cols[1].push(mldata.weekly[date.unix()] || 0);
+            }
+            let cutoff = moment.utc().subtract(12, 'weeks').startOf('week').weekday(4);
+            let chartdiv = new HTML('div', {
+                style: {
+                    clear: 'both',
+                    width: '620px',
+                    height: '220px',
+                    position: 'relative',
+                    background: '#FFF',
+                    borderRadius: '5px',
+                    border: '0.75px solid #333'
+                }
+            });
+            xhtml.inject(chartdiv);
+            let chart = c3.generate({
+                bindto: chartdiv,
+                axis: {
+                    x: {
+                        type: 'timeseries',
+                        tick: {
+                            count: 12,
+                            format: (x) => {
+                                return moment(x).format('MMM D, YYYY');
+                            }
+                        }
+                    }
+                },
+                data: {
+                    x: 'x',
+                    type: 'bar',
+                    columns: cols,
+                    color: (color, d) => {
+                        return d.index < 12 ? '#9639' : (pct_change < 0 ? '#900' : '#090');
+                    }
+                },
+                bar: {
+                    width: {
+                        ratio: 0.35
+                    }
+                },
+                tooltip: {
+                    format: {
+                        value: (x) => '%u emails'.format(x),
+                        title: (x) => 'Week %s'.format(moment(x).format('W, YYYY'))
+                    }
+                }
+            });
+            xhtml.inject(new HTML('br'))
         }
-        
+
     }
-    
+
     txt = "";
     // Bugzilla changes
     let bz = data.bugzilla[project];
     if (bz[0] || bz[1]) txt += "<li>%u BugZilla tickets opened and %u closed in the past quarter.</li>".format(bz[0], bz[1]);
-    
+
     // JIRA changes
-    if (data.kibble) {
-      let color = 'black';
-      let ctxt = data.kibble.jira.change.opened;
-      let pct = parseInt(ctxt);
-      if (pct > 0) {
-        if (pct > 10) color = 'green';
-        ctxt += ' increase';
-      } else if (pct < 0) {
-        if (pct < -10) color = 'maroon';
-        ctxt += ' decrease';
-      } else {
-        ctxt = 'no change';
-      }
-      let s = data.kibble.jira.after.opened == 1 ? '' : 's';
-      if (! (ctxt == 'no change' && data.kibble.jira.after.opened == 0)) {
-        txt += "<li style='color: %s;'>%u issue%s opened in JIRA, past quarter (%s)</li>".format(color, data.kibble.jira.after.opened, s, ctxt);
-      }
-    }
-    if (data.kibble) {
-      let color = 'black';
-      let ctxt = data.kibble.jira.change.closed;
-      let pct = parseInt(ctxt);
-      if (pct > 0) {
-        if (pct > 10) color = 'green';
-        ctxt += ' increase';
-      } else if (pct < 0) {
-        if (pct < -10) color = 'maroon';
-        ctxt += ' decrease';
-      } else {
-        ctxt = 'no change';
-      }
-      let s = data.kibble.jira.after.closed == 1 ? '' : 's';
-      if (! (ctxt == 'no change' && data.kibble.jira.after.closed == 0)) {
-        txt += "<li style='color: %s;'>%u issue%s closed in JIRA, past quarter (%s)</li>".format(color, data.kibble.jira.after.closed, s, ctxt);
-      }
+    if (data.kibble.jira) {
+        let xhtml = new HTML('div', {
+            style: {
+                position: 'relative',
+                clear: 'both'
+            }
+        });
+        let txt = "<h5>JIRA Activity:</h5>";
+        // Opened tickets
+        let color = 'black';
+        let ctxt = data.kibble.jira.change.opened;
+        let pct = parseInt(ctxt);
+        if (pct > 0) {
+            if (pct > 10) color = 'green';
+            ctxt += ' increase';
+        } else if (pct < 0) {
+            if (pct < -10) color = 'maroon';
+            ctxt += ' decrease';
+        } else {
+            ctxt = 'no change';
+        }
+        let s = data.kibble.jira.after.opened == 1 ? '' : 's';
+        if (!(ctxt == 'no change' && data.kibble.jira.after.opened == 0)) {
+            txt += "<h6 style='color: %s;'>%u issue%s opened in JIRA, past quarter (%s)</hi>".format(color, data.kibble.jira.after.opened, s, ctxt);
+        }
+        
+        // Closed tickets
+        color = 'black';
+        ctxt = data.kibble.jira.change.closed;
+        pct = parseInt(ctxt);
+        if (pct > 0) {
+            if (pct > 10) color = 'green';
+            ctxt += ' increase';
+        } else if (pct < 0) {
+            if (pct < -10) color = 'maroon';
+            ctxt += ' decrease';
+        } else {
+            ctxt = 'no change';
+        }
+        s = data.kibble.jira.after.closed == 1 ? '' : 's';
+        if (!(ctxt == 'no change' && data.kibble.jira.after.closed == 0)) {
+            txt += "<h6 style='color: %s;'>%u issue%s closed in JIRA, past quarter (%s)</h6>".format(color, data.kibble.jira.after.closed, s, ctxt);
+        }
+
+        xhtml.innerHTML = txt;
+        html.inject(xhtml);
+        if (data.kibble.timeseries.jira && data.kibble.timeseries.jira.length > 0) {
+
+            let cols = [
+                ['x'],
+                ['Tickets opened'],
+                ['Tickets closed']
+            ];
+            for (var i = 0; i < 24; i++) {
+                let date = moment.utc().subtract(i, 'weeks').startOf('week').weekday(1);
+                let c = 0;
+                let o = 0;
+                for (var n = 0; n < data.kibble.timeseries.jira.length; n++) {
+                    let el = data.kibble.timeseries.jira[n];
+                    if (el.date == date.unix()) {
+                        c = el['issues closed']
+                        o = el['issues opened']
+                    }
+                }
+                cols[0].push(date);
+                cols[1].push(c);
+                cols[2].push(o);
+            }
+            let cutoff = moment.utc().subtract(12, 'weeks').startOf('week').weekday(4);
+            let chartdiv = new HTML('div', {
+                style: {
+                    clear: 'both',
+                    width: '620px',
+                    height: '220px',
+                    position: 'relative',
+                    background: '#FFF',
+                    borderRadius: '5px',
+                    border: '0.75px solid #333'
+                }
+            });
+            xhtml.inject(chartdiv);
+            let chart = c3.generate({
+                bindto: chartdiv,
+                axis: {
+                    x: {
+                        type: 'timeseries',
+                        tick: {
+                            count: 12,
+                            format: (x) => {
+                                return moment(x).format('MMM D, YYYY');
+                            }
+                        }
+                    }
+                },
+                data: {
+                    x: 'x',
+                    type: 'bar',
+                    columns: cols,
+                    color: (color, d) => {
+                        return (d.index < 12 ? color + '44': color +'FF');
+                    }
+                },
+                bar: {
+                    width: {
+                        ratio: 0.25
+                    }
+                },
+                tooltip: {
+                    format: {
+                        title: (x) => 'Week %s'.format(moment(x).format('W, YYYY'))
+                    }
+                }
+            });
+            xhtml.inject(new HTML('br'));
+        }
+        html.inject(new HTML('hr'));
+        
     }
-    
-    
+
     // Commits and contributors
-    if (data.kibble) {
-      let color = 'black';
-      let ctxt = data.kibble.commits.change.commits
-      let pct = parseInt(ctxt);
-      if (pct > 0) {
-        if (pct > 10) color = 'green';
-        ctxt += ' increase';
-      } else if (pct < 0) {
-        if (pct < -10) color = 'maroon';
-        ctxt += ' decrease';
-      } else {
-        ctxt = 'no change';
-      }
-      let s = data.kibble.commits.after.commits == 1 ? '' : 's';
-      txt += "<li style='color: %s;'>%u commit%s in the past quarter (%s)</li>".format(color, data.kibble.commits.after.commits, s, ctxt);
-    }
-    
-    if (data.kibble) {
-      let color = 'black';
-      let ctxt = data.kibble.commits.change.authors
-      let pct = parseInt(ctxt);
-      if (pct > 0) {
-        if (pct > 10) color = 'green';
-        ctxt += ' increase';
-      } else if (pct < 0) {
-        if (pct < -10) color = 'maroon';
-        ctxt += ' decrease';
-      } else {
-        ctxt = 'no change';
-      }
-      let s = data.kibble.commits.after.authors == 1 ? '' : 's';
-      txt += "<li style='color: %s;'>%u code contributor%s in the past quarter (%s)</li>".format(color, data.kibble.commits.after.authors, s, ctxt);
+    if (data.kibble.commits) {
+        let xhtml = new HTML('div', {
+            style: {
+                position: 'relative',
+                clear: 'both'
+            }
+        });
+        html.inject(xhtml);
+        let txt = "<h5>Commit activity:</h5>"
+        let color = 'black';
+        let ctxt = data.kibble.commits.change.commits
+        let pct = parseInt(ctxt);
+        if (pct > 0) {
+            if (pct > 10) color = 'green';
+            ctxt += ' increase';
+        } else if (pct < 0) {
+            if (pct < -10) color = 'maroon';
+            ctxt += ' decrease';
+        } else {
+            ctxt = 'no change';
+        }
+        let s = data.kibble.commits.after.commits == 1 ? '' : 's';
+        txt += "<h6 style='color: %s;'>%u commit%s in the past quarter (%s)</h6>".format(color, data.kibble.commits.after.commits, s, ctxt);
+        
+        // committers
+        color = 'black';
+        ctxt = data.kibble.commits.change.authors;
+        pct = parseInt(ctxt);
+        if (pct > 0) {
+            if (pct > 10) color = 'green';
+            ctxt += ' increase';
+        } else if (pct < 0) {
+            if (pct < -10) color = 'maroon';
+            ctxt += ' decrease';
+        } else {
+            ctxt = 'no change';
+        }
+        s = data.kibble.commits.after.authors == 1 ? '' : 's';
+        txt += "<h6 style='color: %s;'>%u code contributor%s in the past quarter (%s)</h6>".format(color, data.kibble.commits.after.authors, s, ctxt);
+        
+        xhtml.innerHTML = txt;
+        html.inject(xhtml);
+        if (data.kibble.timeseries.commits && data.kibble.timeseries.commits.length > 0) {
+
+            let cols = [
+                ['x'],
+                ['Commits']
+            ];
+            for (var i = 0; i < 24; i++) {
+                let date = moment.utc().subtract(i, 'weeks').startOf('week').weekday(1);
+                let c = 0;
+                for (var n = 0; n < data.kibble.timeseries.commits.length; n++) {
+                    let el = data.kibble.timeseries.commits[n];
+                    if (el.date == date.unix()) {
+                        c = el['commits']
+                    }
+                }
+                cols[0].push(date);
+                cols[1].push(c);
+            }
+            let cutoff = moment.utc().subtract(12, 'weeks').startOf('week').weekday(4);
+            let chartdiv = new HTML('div', {
+                style: {
+                    clear: 'both',
+                    width: '620px',
+                    height: '220px',
+                    position: 'relative',
+                    background: '#FFF',
+                    borderRadius: '5px',
+                    border: '0.75px solid #333'
+                }
+            });
+            xhtml.inject(chartdiv);
+            let chart = c3.generate({
+                bindto: chartdiv,
+                axis: {
+                    x: {
+                        type: 'timeseries',
+                        tick: {
+                            count: 12,
+                            format: (x) => {
+                                return moment(x).format('MMM D, YYYY');
+                            }
+                        }
+                    }
+                },
+                data: {
+                    x: 'x',
+                    type: 'bar',
+                    columns: cols,
+                    color: (color, d) => {
+                        return (d.index < 12 ? color + '44': color +'FF');
+                    }
+                },
+                bar: {
+                    width: {
+                        ratio: 0.25
+                    }
+                },
+                tooltip: {
+                    format: {
+                        title: (x) => 'Week %s'.format(moment(x).format('W, YYYY'))
+                    }
+                }
+            });
+            xhtml.inject(new HTML('br'))
+        }
+        html.inject(new HTML('hr'));
+        
     }
     
+
     // GitHub: PRs
-    if (data.kibble) {
-      let color = 'black';
-      let ctxt = data.kibble.prs.change.opened
-      let pct = parseInt(ctxt);
-      if (pct > 0) {
-        if (pct > 10) color = 'green';
-        ctxt += ' increase';
-      } else if (pct < 0) {
-        if (pct < -10) color = 'maroon';
-        ctxt += ' decrease';
-      } else {
-        ctxt = 'no change';
-      }
-      let s = data.kibble.prs.after.opened == 1 ? '' : 's';
-      if (! (ctxt == 'no change' && data.kibble.prs.after.opened == 0)) {
-        txt += "<li style='color: %s;'>%u PR%s opened on GitHub, past quarter (%s)</li>".format(color, data.kibble.prs.after.opened, s, ctxt);
-      }
-    }
-    
-    if (data.kibble) {
-      let color = 'black';
-      let ctxt = data.kibble.prs.change.closed
-      let pct = parseInt(ctxt);
-      if (pct > 0) {
-        if (pct > 10) color = 'green';
-        ctxt += ' increase';
-      } else if (pct < 0) {
-        if (pct < -10) color = 'maroon';
-        ctxt += ' decrease';
-      } else {
-        ctxt = 'no change';
-      }
-      let s = data.kibble.prs.after.closed == 1 ? '' : 's';
-      if (! (ctxt == 'no change' && data.kibble.prs.after.closed == 0)) {
-        txt += "<li style='color: %s;'>%u PR%s closed on GitHub, past quarter (%s)</li>".format(color, data.kibble.prs.after.closed, s, ctxt);
-      }
-    }
-    
-    // GitHub: Issues
-    if (data.kibble) {
-      let color = 'black';
-      let ctxt = data.kibble.issues.change.opened
-      let pct = parseInt(ctxt);
-      if (pct > 0) {
-        if (pct > 10) color = 'green';
-        ctxt += ' increase';
-      } else if (pct < 0) {
-        if (pct < -10) color = 'maroon';
-        ctxt += ' decrease';
-      } else {
-        ctxt = 'no change';
-      }
-      let s = data.kibble.issues.after.opened == 1 ? '' : 's';
-      if (! (ctxt == 'no change' && data.kibble.issues.after.opened == 0)) {
-        txt += "<li style='color: %s;'>%u issue%s opened on GitHub, past quarter (%s)</li>".format(color, data.kibble.issues.after.opened, s, ctxt);
-      }
+    if (data.kibble && data.kibble.prs) {
+        
+        let xhtml = new HTML('div', {
+            style: {
+                position: 'relative',
+                clear: 'both'
+            }
+        });
+        html.inject(xhtml);
+        let txt = "<h5>GitHub activity:</h5>";
+        
+        let color = 'black';
+        let ctxt = data.kibble.prs.change.opened
+        let pct = parseInt(ctxt);
+        if (pct > 0) {
+            if (pct > 10) color = 'green';
+            ctxt += ' increase';
+        } else if (pct < 0) {
+            if (pct < -10) color = 'maroon';
+            ctxt += ' decrease';
+        } else {
+            ctxt = 'no change';
+        }
+        let s = data.kibble.prs.after.opened == 1 ? '' : 's';
+        if (!(ctxt == 'no change' && data.kibble.prs.after.opened == 0)) {
+            txt += "<h6 style='color: %s;'>%u PR%s opened on GitHub, past quarter (%s)</h6>".format(color, data.kibble.prs.after.opened, s, ctxt);
+        }
+        
+        
+        color = 'black';
+        ctxt = data.kibble.prs.change.closed;
+        pct = parseInt(ctxt);
+        if (pct > 0) {
+            if (pct > 10) color = 'green';
+            ctxt += ' increase';
+        } else if (pct < 0) {
+            if (pct < -10) color = 'maroon';
+            ctxt += ' decrease';
+        } else {
+            ctxt = 'no change';
+        }
+        s = data.kibble.prs.after.closed == 1 ? '' : 's';
+        if (!(ctxt == 'no change' && data.kibble.prs.after.closed == 0)) {
+            txt += "<h6 style='color: %s;'>%u PR%s closed on GitHub, past quarter (%s)</h6>".format(color, data.kibble.prs.after.closed, s, ctxt);
+        }
+        
+        
+        xhtml.innerHTML = txt;
+        html.inject(xhtml);
+        if (data.kibble.timeseries.github && data.kibble.timeseries.github.length > 0) {
+
+            let cols = [
+                ['x'],
+                ['PRs opened'],
+                ['PRs closed']
+            ];
+            for (var i = 0; i < 24; i++) {
+                let date = moment.utc().subtract(i, 'weeks').startOf('week').weekday(1);
+                let c = 0;
+                let o = 0;
+                for (var n = 0; n < data.kibble.timeseries.github.length; n++) {
+                    let el = data.kibble.timeseries.github[n];
+                    if (el.date == date.unix()) {
+                        c = el['pull requests closed']
+                        o = el['pull requests opened']
+                    }
+                }
+                cols[0].push(date);
+                cols[1].push(c);
+                cols[2].push(o);
+            }
+            let cutoff = moment.utc().subtract(12, 'weeks').startOf('week').weekday(4);
+            let chartdiv = new HTML('div', {
+                style: {
+                    clear: 'both',
+                    width: '620px',
+                    height: '220px',
+                    position: 'relative',
+                    background: '#FFF',
+                    borderRadius: '5px',
+                    border: '0.75px solid #333'
+                }
+            });
+            xhtml.inject(chartdiv);
+            let chart = c3.generate({
+                bindto: chartdiv,
+                axis: {
+                    x: {
+                        type: 'timeseries',
+                        tick: {
+                            count: 12,
+                            format: (x) => {
+                                return moment(x).format('MMM D, YYYY');
+                            }
+                        }
+                    }
+                },
+                data: {
+                    x: 'x',
+                    type: 'bar',
+                    columns: cols,
+                    colors: {
+                        'PRs opened': '#008800',
+                        'PRs closed': '#993322'
+                    },
+                    color: (color, d) => {
+                        return (d.index < 12 ? color + '44': color +'FF');
+                    }
+                },
+                bar: {
+                    width: {
+                        ratio: 0.25
+                    }
+                },
+                tooltip: {
+                    format: {
+                        title: (x) => 'Week %s'.format(moment(x).format('W, YYYY'))
+                    }
+                }
+            });
+            xhtml.inject(new HTML('br'));
+        }
+        html.inject(new HTML('hr'));
+        
     }
+
     
-    if (data.kibble) {
-      let color = 'black';
-      let ctxt = data.kibble.issues.change.closed
-      let pct = parseInt(ctxt);
-      if (pct > 0) {
-        if (pct > 10) color = 'green';
-        ctxt += ' increase';
-      } else if (pct < 0) {
-        if (pct < -10) color = 'maroon';
-        ctxt += ' decrease';
-      } else {
-        ctxt = 'no change';
-      }
-      let s = data.kibble.issues.after.closed == 1 ? '' : 's';
-      if (! (ctxt == 'no change' && data.kibble.issues.after.closed == 0)) {
-        txt += "<li style='color: %s;'>%u issue%s closed on GitHub, past quarter (%s)</li>".format(color, data.kibble.issues.after.closed, s, ctxt);
-      }
+    // GitHub: issues
+    if (data.kibble && data.kibble.issues) {
+        
+        let xhtml = new HTML('div', {
+            style: {
+                position: 'relative',
+                clear: 'both'
+            }
+        });
+        html.inject(xhtml);
+        let txt = "<h5>GitHub issues:</h5>";
+        
+        let color = 'black';
+        let ctxt = data.kibble.issues.change.opened;
+        let pct = parseInt(ctxt);
+        if (pct > 0) {
+            if (pct > 10) color = 'green';
+            ctxt += ' increase';
+        } else if (pct < 0) {
+            if (pct < -10) color = 'maroon';
+            ctxt += ' decrease';
+        } else {
+            ctxt = 'no change';
+        }
+        let s = data.kibble.issues.after.opened == 1 ? '' : 's';
+        if (!(ctxt == 'no change' && data.kibble.issues.after.opened == 0)) {
+            txt += "<h6 style='color: %s;'>%u issue%s opened on GitHub, past quarter (%s)</h6>".format(color, data.kibble.issues.after.opened, s, ctxt);
+        }
+        
+        
+        color = 'black';
+        ctxt = data.kibble.issues.change.closed;
+        pct = parseInt(ctxt);
+        if (pct > 0) {
+            if (pct > 10) color = 'green';
+            ctxt += ' increase';
+        } else if (pct < 0) {
+            if (pct < -10) color = 'maroon';
+            ctxt += ' decrease';
+        } else {
+            ctxt = 'no change';
+        }
+        s = data.kibble.issues.after.closed == 1 ? '' : 's';
+        if (!(ctxt == 'no change' && data.kibble.issues.after.closed == 0)) {
+            txt += "<h6 style='color: %s;'>%u issue%s closed on GitHub, past quarter (%s)</h6>".format(color, data.kibble.issues.after.closed, s, ctxt);
+        }
+        
+        
+        xhtml.innerHTML = txt;
+        html.inject(xhtml);
+        if (data.kibble.timeseries.github && data.kibble.timeseries.github.length > 0) {
+
+            let cols = [
+                ['x'],
+                ['issues opened'],
+                ['issues closed']
+            ];
+            for (var i = 0; i < 24; i++) {
+                let date = moment.utc().subtract(i, 'weeks').startOf('week').weekday(1);
+                let c = 0;
+                let o = 0;
+                for (var n = 0; n < data.kibble.timeseries.github.length; n++) {
+                    let el = data.kibble.timeseries.github[n];
+                    if (el.date == date.unix()) {
+                        c = el['issues closed']
+                        o = el['issues opened']
+                    }
+                }
+                cols[0].push(date);
+                cols[1].push(c);
+                cols[2].push(o);
+            }
+            let cutoff = moment.utc().subtract(12, 'weeks').startOf('week').weekday(4);
+            let chartdiv = new HTML('div', {
+                style: {
+                    clear: 'both',
+                    width: '620px',
+                    height: '220px',
+                    position: 'relative',
+                    background: '#FFF',
+                    borderRadius: '5px',
+                    border: '0.75px solid #333'
+                }
+            });
+            xhtml.inject(chartdiv);
+            let chart = c3.generate({
+                bindto: chartdiv,
+                axis: {
+                    x: {
+                        type: 'timeseries',
+                        tick: {
+                            count: 12,
+                            format: (x) => {
+                                return moment(x).format('MMM D, YYYY');
+                            }
+                        }
+                    }
+                },
+                data: {
+                    x: 'x',
+                    type: 'bar',
+                    columns: cols,
+                    colors: {
+                        'issues opened': '#008800',
+                        'issues closed': '#993322'
+                    },
+                    color: (color, d) => {
+                        return (d.index < 12 ? color + '44': color +'FF');
+                    }
+                },
+                bar: {
+                    width: {
+                        ratio: 0.25
+                    }
+                },
+                tooltip: {
+                    format: {
+                        title: (x) => 'Week %s'.format(moment(x).format('W, YYYY'))
+                    }
+                }
+            });
+            xhtml.inject(new HTML('br'));
+        }
+        html.inject(new HTML('hr'));
+        
     }
-    
+
+
     // Busiest topics
     if (data.kibble) {
-      let showit = false;
-      let busiest = new HTML('li', {}, "Busiest topics (click to pop up): ");
-      if (data.kibble.busiest.email.length > 0) {
-        showit = true;
-        let ul = new HTML('ul');
-        let arr = data.kibble.busiest.email;
-        for (var i = 0; i < arr.length; i++) {
-          let ml = arr[i].source.split('?')[1];
-          let li = new HTML('li', {}, [
-                                       new HTML("kbd", {}, ml),
-                                       new HTML('i', {style: {display: 'inline-block', textIndent: '10px'}}, arr[i].name),
-                                       new HTML('span', { style: {display: 'inline-block', textIndent: '10px'}}, "(%u emails)".format(arr[i].count))
-                                      ]);
-          ul.inject(li);
-        }
-        busiest_html['email'] = ul.outerHTML;
-        let a = new HTML('a', {href: '#', onclick: 'show_busiest("email");', style: {marginLeft: '10px'}}, 'email');
-        busiest.inject(a);
-      }
-      
-      
-      if (data.kibble.busiest.github.length > 0) {
-        showit = true;
-        let ul = new HTML('ul');
-        let arr = data.kibble.busiest.github;
-        for (var i = 0; i < arr.length; i++) {
-          let li = new HTML('li', {}, [
-                                       new HTML("a", {href: arr[i].url}, arr[i].url.replace('https://github.com/apache/', '')),
-                                       new HTML('i', {style: {display: 'inline-block', textIndent: '10px'}}, arr[i].subject),
-                                       new HTML('span', { style: {display: 'inline-block', textIndent: '10px'}}, "(%u comments)".format(arr[i].count))
-                                      ]);
-          ul.inject(li);
-        }
-        busiest_html['github'] = ul.outerHTML;
-        let a = new HTML('a', {href: '#', onclick: 'show_busiest("github");', style: {marginLeft: '10px'}}, 'GitHub');
-        busiest.inject(a);
-      }
-      
-      if (data.kibble.busiest.jira.length > 0) {
-        showit = true;
-        let ul = new HTML('ul');
-        let arr = data.kibble.busiest.jira;
-        for (var i = 0; i < arr.length; i++) {
-          let li = new HTML('li', {}, [
-                                       new HTML("a", {href: arr[i].url}, arr[i].key),
-                                       new HTML('i', {style: {display: 'inline-block', textIndent: '10px'}}, arr[i].subject),
-                                       new HTML('span', { style: {display: 'inline-block', textIndent: '10px'}}, "(%u comments)".format(arr[i].count))
-                                      ]);
-          ul.inject(li);
-        }
-        busiest_html['jira'] = ul.outerHTML;
-        let a = new HTML('a', {href: '#', onclick: 'show_busiest("jira");', style: {marginLeft: '10px'}}, 'JIRA');
-        busiest.inject(a);
-      }
-      
-      if (showit) {
-        txt += busiest.outerHTML;
-      }
+        let txt = "";
+        let showit = false;
+        let busiest = new HTML('li', {}, "Busiest topics (click to pop up): ");
+        if (data.kibble.busiest.email.length > 0) {
+            txt += "<h5>Busiest email threads:</h5>";
+            showit = true;
+            let ul = new HTML('ul');
+            let arr = data.kibble.busiest.email;
+            for (var i = 0; i < arr.length; i++) {
+                let ml = arr[i].source.split('?')[1];
+                let li = new HTML('li', {}, [
+                    new HTML("kbd", {}, ml),
+                    new HTML('i', {
+                        style: {
+                            display: 'inline-block',
+                            textIndent: '10px'
+                        }
+                    }, arr[i].name),
+                    new HTML('span', {
+                        style: {
+                            display: 'inline-block',
+                            textIndent: '10px'
+                        }
+                    }, "(%u emails)".format(arr[i].count))
+                ]);
+                ul.inject(li);
+            }
+            txt += ul.outerHTML;
+            
+        }
+
+
+        if (data.kibble.busiest.github.length > 0) {
+            showit = true;
+            txt += "<h5>Busiest GitHub issues/PRs:</h5>";
+            let ul = new HTML('ul');
+            let arr = data.kibble.busiest.github;
+            for (var i = 0; i < arr.length; i++) {
+                let li = new HTML('li', {}, [
+                    new HTML("a", {
+                        href: arr[i].url
+                    }, arr[i].url.replace('https://github.com/apache/', '')),
+                    new HTML('i', {
+                        style: {
+                            display: 'inline-block',
+                            textIndent: '10px'
+                        }
+                    }, arr[i].subject),
+                    new HTML('span', {
+                        style: {
+                            display: 'inline-block',
+                            textIndent: '10px'
+                        }
+                    }, "(%u comments)".format(arr[i].count))
+                ]);
+                ul.inject(li);
+            }
+            txt += ul.outerHTML;
+            
+        }
+
+        if (data.kibble.busiest.jira.length > 0) {
+            showit = true;
+            txt += "<h5>Busiest JIRA tickets:</h5>";
+            let ul = new HTML('ul');
+            let arr = data.kibble.busiest.jira;
+            for (var i = 0; i < arr.length; i++) {
+                let li = new HTML('li', {}, [
+                    new HTML("a", {
+                        href: arr[i].url
+                    }, arr[i].key),
+                    new HTML('i', {
+                        style: {
+                            display: 'inline-block',
+                            textIndent: '10px'
+                        }
+                    }, arr[i].subject),
+                    new HTML('span', {
+                        style: {
+                            display: 'inline-block',
+                            textIndent: '10px'
+                        }
+                    }, "(%u comments)".format(arr[i].count))
+                ]);
+                ul.inject(li);
+            }
+            txt += ul.outerHTML;
+            
+        }
+
+        if (showit) {
+            txt += busiest.outerHTML;
+        }
+        if (txt.length > 0) {
+            let twrap = new HTML('div');
+            twrap.innerHTML = txt;
+            html.inject(twrap);
+        }
     }
     
-    // Append header IF there is data, otherwise nah.
-    if (txt.length > 0) {
-      txt = "<h3>Community Health Metrics:</h3><ul>" + txt + "</ul>";
+    headers = $(html).find("h5");
+    let toc = "<ul>";
+    for (var i = 0; i < headers.length; i++) {
+        let t = headers[i].innerText.replace(/:.*$/, '');
+        let id = t.replace(/\s+/g, '').toLowerCase();
+        headers[i].setAttribute('id', id);
+        toc += "<li><a href='#%s'>%s</a></li>".format(id, t);
     }
+    toc += "</ul>";
     let twrap = new HTML('div');
-    twrap.innerHTML = txt;
-    html.inject(twrap);
+    twrap.innerHTML = toc;
+    html.insertBefore(twrap, html.childNodes[1]);
+    
     return html;
-}
 
-function show_busiest(t) {
-  if (busiest_html[t]) {
-    let thtml = "<p>These figures are approximate and automatically generated. We advise that you also do your own research if you intend to use this for reports.</p>";
-    thtml += busiest_html[t];
-    modal(thtml, "Busiest topics:");
-  }
+   
 }
 
 function statistics_releases(data) {
     let three_months_ago = moment().subtract(3, 'months');
     let txt = "";
-    
+
     // Releases
     let rtxt = "";
     let new_releases = 0;
@@ -2526,40 +2933,40 @@ function statistics_releases(data) {
     for (var rel in data.releases[project]) {
         let reldate = moment(data.releases[project][rel] * 1000.0);
         if (reldate > three_months_ago) {
-          new_releases++;
+            new_releases++;
         }
         ages.push(reldate.unix());
     }
     ages.sort().reverse();
-    ages = ages.splice(0,new_releases >= 3 ? new_releases : 3);
+    ages = ages.splice(0, new_releases >= 3 ? new_releases : 3);
     let to_show = ages.length;
     let releases_shown = 0;
     while (ages.length) {
-      let ts = ages.shift();
-      for (var rel in data.releases[project]) {
-        if (releases_shown == to_show) break;
-          let reldate = moment(data.releases[project][rel] * 1000.0);
-          if (ts == reldate.unix()) {
-              rtxt += "<li>%s was released on %s.</li>".format(rel, reldate.format('YYYY-MM-DD'));
-              releases_shown++;
-          }
-      }
+        let ts = ages.shift();
+        for (var rel in data.releases[project]) {
+            if (releases_shown == to_show) break;
+            let reldate = moment(data.releases[project][rel] * 1000.0);
+            if (ts == reldate.unix()) {
+                rtxt += "<li>%s was released on %s.</li>".format(rel, reldate.format('YYYY-MM-DD'));
+                releases_shown++;
+            }
+        }
     }
     if (rtxt != '') {
         rtxt = "<h6>Recent releases: </h6><ul>" + rtxt + "</ul>";
-        rtxt += new HTML('a', {target: '_blank', href: 'https://reporter.apache.org/addrelease.html?%s'.format(project)}, 'Manage release data').outerHTML;
+        rtxt += new HTML('a', {
+            target: '_blank',
+            href: 'https://reporter.apache.org/addrelease.html?%s'.format(project)
+        }, 'Manage release data').outerHTML;
     }
-    
-    
+
+
     // Put it all together
     txt += rtxt;
     if (txt) txt = "<h3>Project Release Activity:</h3>" + txt
     return txt;
 }
 
-
-
-
 /******************************************
  Fetched from source/statistics.js
 ******************************************/
@@ -2581,6 +2988,20 @@ function StatisticsPage(layout, pdata) {
             wrapper.inject(thtml);
         }
     }
+    
+    headers = $(wrapper).find("h4");
+    let toc = "<ul style='background: #3333;'>";
+    for (var i = 0; i < headers.length; i++) {
+        let t = headers[i].innerText.replace(/:.*$/, '');
+        let id = t.replace(/\s+/g, '').toLowerCase();
+        headers[i].setAttribute('id', id);
+        toc += "<li style='display: inline-block; margin-left: 24px;'><a href='#%s'>%s</a></li>".format(id, t);
+    }
+    toc += "</ul>";
+    let twrap = new HTML('div');
+    twrap.innerHTML = toc;
+    wrapper.insertBefore(twrap, wrapper.childNodes[0]);
+    
 }