Home | History | Annotate | Download | only in data
      1 <!DOCTYPE html>
      2 <html>
      3 <head>
      4 <title>ChangeLog Analysis</title>
      5 <style type="text/css">
      6 
      7 body {
      8     font-family: 'Helvetica' 'Segoe UI Light' sans-serif;
      9     font-weight: 200;
     10     padding: 20px;
     11     min-width: 1200px;
     12 }
     13 
     14 * {
     15     padding: 0px;
     16     margin: 0px;
     17     border: 0px;
     18 }
     19 
     20 h1, h2, h3 {
     21     font-weight: 200;
     22 }
     23 
     24 h1 {
     25     margin: 0 0 1em 0;
     26 }
     27 
     28 h2 {
     29     font-size: 1.2em;
     30     text-align: center;
     31     margin-bottom: 1em;
     32 }
     33 
     34 h3 {
     35     font-size: 1em;
     36 }
     37 
     38 .view {
     39     margin: 0px;
     40     width: 600px;
     41     float: left;
     42 }
     43 
     44 .graph-container p {
     45     width: 200px;
     46     text-align: right;
     47     margin: 20px 0 20px 0;
     48     padding: 5px;
     49     border-right: solid 1px black;
     50 }
     51 
     52 .graph-container table {
     53     width: 100%;
     54 }
     55 
     56 .graph-container table, .graph-container td {
     57     border-collapse: collapse;
     58     border: none;
     59 }
     60 
     61 .graph-container td {
     62     padding: 5px;
     63     vertical-align: center;
     64 }
     65 
     66 .graph-container td:first-child {
     67     width: 200px;
     68     text-align: right;
     69     border-right: solid 1px black;
     70 }
     71 
     72 .graph-container .selected {
     73     background: #eee;
     74 }
     75 
     76 #reviewers .selected td:first-child {
     77     border-radius: 10px 0px 0px 10px;
     78 }
     79 
     80 #areas .selected td:last-child {
     81     border-radius: 0px 10px 10px 0px;
     82 }
     83 
     84 .graph-container .bar {
     85     display: inline-block;
     86     min-height: 1em;
     87     background: #9f6;
     88     margin-right: 0.4ex;
     89 }
     90 
     91 .graph-container .reviewed-patches {
     92     background: #3cf;
     93     margin-right: 1px;
     94 }
     95 
     96 .graph-container .unreviewed-patches {
     97     background: #f99;
     98 }
     99 
    100 .constrained {
    101     background: #eee;
    102     border-radius: 10px;
    103 }
    104 
    105 .constrained .vertical-bar {
    106     border-right: solid 1px #eee;
    107 }
    108 
    109 #header {
    110     border-spacing: 5px;
    111 }
    112 
    113 #header section {
    114     display: table-cell;
    115     width: 200px;
    116     vertical-align: top;
    117     border: solid 2px #ccc;
    118     border-collapse: collapse;
    119     padding: 5px;
    120     font-size: 0.8em;
    121 }
    122 
    123 #header dt {
    124     float: left;
    125 }
    126 
    127 #header dt:after {
    128     content: ': ';
    129 }
    130 
    131 #header .legend {
    132     width: 600px;
    133 }
    134 
    135 .legend .bar {
    136     width: 15ex;
    137     padding: 2px;
    138 }
    139 
    140 .legend .reviews {
    141     width: 25ex;
    142 }
    143 
    144 .legend td:first-child {
    145     width: 18ex;
    146 }
    147 
    148 </style>
    149 </head>
    150 <body>
    151 <h1>ChangeLog Analysis</h1>
    152 
    153 <section id="header">
    154 <section id="summary">
    155 <h2>Summary</h2>
    156 </section>
    157 
    158 <section class="legend">
    159 <h2>Legend</h2>
    160 <div class="graph-container">
    161 <table>
    162 <tbody>
    163 <tr><td>Contributor's name</td>
    164 <td><span class="bar reviews">Reviews</span> <span class="value-container">(# of reviews)</span><br>
    165 <span class="bar reviewed-patches">Reviewed</span><span class="bar unreviewed-patches">Unreviewed</span>
    166 <span class="value-container">(# of reviewed):(# of unreviewed)</span></td></tr>
    167 </tbody>
    168 </table>
    169 </div>
    170 </section>
    171 </section>
    172 
    173 <section id="contributors" class="view">
    174 <h2 id="contributors-title">Contributors</h2>
    175 <div class="graph-container"></div>
    176 </section>
    177 
    178 <section id="areas" class="view">
    179 <h2 id="areas-title">Areas of contributions</h2>
    180 <div class="graph-container"></div>
    181 </section>
    182 
    183 <script>
    184 
    185 // Naive implementation of element extensions discussed on public-webapps
    186 
    187 if (!Element.prototype.append) {
    188     Element.prototype.append = function () {
    189         for (var i = 0; i < arguments.length; i++) {
    190             // FIXME: Take care of other node types
    191             if (arguments[i] instanceof Element || arguments[i] instanceof CharacterData)
    192                 this.appendChild(arguments[i]);
    193             else
    194                 this.appendChild(document.createTextNode(arguments[i]));
    195         }
    196         return this;
    197     }
    198 }
    199 
    200 if (!Node.prototype.remove) {
    201     Node.prototype.remove = function () {
    202         this.parentNode.removeChild(this);
    203         return this;
    204     }
    205 }
    206 
    207 if (!Element.create) {
    208     Element.create = function () {
    209         if (arguments.length < 1)
    210             return null;
    211         var element = document.createElement(arguments[0]);
    212         if (arguments.length == 1)
    213             return element;
    214 
    215         // FIXME: the second argument can be content or IDL attributes
    216         var attributes = arguments[1];
    217         for (attribute in attributes)
    218             element.setAttribute(attribute, attributes[attribute]);
    219 
    220         if (arguments.length >= 3)
    221             element.append.apply(element, arguments[2]);
    222 
    223         return element;
    224     }
    225 }
    226 
    227 if (!Node.prototype.removeAllChildren) {
    228     Node.prototype.removeAllChildren = function () {
    229         while (this.firstChild)
    230             this.firstChild.remove();
    231         return this;
    232     }
    233 }
    234 
    235 Element.prototype.removeClassNameFromAllElements = function (className) {
    236     var elements = this.getElementsByClassName(className);
    237     for (var i = 0; i < elements.length; i++)
    238         elements[i].classList.remove(className);
    239 }
    240 
    241 function getJSON(url, callback) {
    242     var xhr = new XMLHttpRequest();
    243     xhr.open('GET', url, true);
    244     xhr.onreadystatechange = function () {
    245         if (this.readyState == 4)
    246             callback(JSON.parse(xhr.responseText));
    247     }
    248     xhr.send();
    249 }
    250 
    251 function GraphView(container) {
    252     this._container = container;
    253     this._defaultData = null;
    254 }
    255 
    256 GraphView.prototype.setData = function(data, constrained) {
    257     if (constrained)
    258         this._container.classList.add('constrained');
    259     else
    260         this._container.classList.remove('constrained');
    261     this._clearGraph();
    262     this._constructGraph(data);
    263 }
    264 
    265 GraphView.prototype.setDefaultData = function(data) {
    266     this._defaultData = data;
    267     this.setData(data);
    268 }
    269 
    270 GraphView.prototype.reset = function () {
    271     this.setMarginTop();
    272     this.setData(this._defaultData);
    273 }
    274 
    275 GraphView.prototype.isConstrained = function () { return this._container.classList.contains('constrained'); }
    276 
    277 GraphView.prototype.targetRow = function (node) {
    278     var target = null;
    279 
    280     while (node && node != this._container) {
    281         if (node.localName == 'tr')
    282             target = node;
    283         node = node.parentNode;
    284     }
    285 
    286     return node && target;
    287 }
    288 
    289 GraphView.prototype.selectRow = function (row) {
    290     this._container.removeClassNameFromAllElements('selected');
    291     row.classList.add('selected');
    292 }
    293 
    294 GraphView.prototype.setMarginTop = function (y) { this._container.style.marginTop = y ? y + 'px' : null; }
    295 GraphView.prototype._graphContainer = function () { return this._container.getElementsByClassName('graph-container')[0]; }
    296 GraphView.prototype._clearGraph = function () { return this._graphContainer().removeAllChildren(); }
    297 
    298 GraphView.prototype._numberOfPatches = function (dataItem) {
    299     return dataItem.numberOfReviewedPatches + (dataItem.numberOfUnreviewedPatches !== undefined ? dataItem.numberOfUnreviewedPatches : 0);
    300 }
    301 
    302 GraphView.prototype._maximumValue = function (labels, data) {
    303     var numberOfPatches = this._numberOfPatches;
    304     return Math.max.apply(null, labels.map(function (label) {
    305         return Math.max(numberOfPatches(data[label]), data[label].numberOfReviews !== undefined ? data[label].numberOfReviews : 0);
    306     }));
    307 }
    308 
    309 GraphView.prototype._sortLabelsByNumberOfReviwsAndReviewedPatches = function(data) {
    310     var labels = Object.keys(data);
    311     if (!labels.length)
    312         return null;
    313     var numberOfPatches = this._numberOfPatches;
    314     var computeValue = function (dataItem) {
    315         return numberOfPatches(dataItem) + (dataItem.numberOfReviews !== undefined ? dataItem.numberOfReviews : 0);
    316     }
    317     labels.sort(function (a, b) { return computeValue(data[b]) - computeValue(data[a]); });
    318     return labels;
    319 }
    320 
    321 GraphView.prototype._constructGraph = function (data) {
    322     var element = this._graphContainer();
    323     var labels = this._sortLabelsByNumberOfReviwsAndReviewedPatches(data);
    324     if (!labels) {
    325         element.append(Element.create('p', {}, ['None']));
    326         return;
    327     }
    328 
    329     var maxValue = this._maximumValue(labels, data);
    330     var computeStyleForBar = function (value) { return 'width:' + (value * 85.0 / maxValue) + '%' }
    331 
    332     var table = Element.create('table', {}, [Element.create('tbody')]);
    333     for (var i = 0; i < labels.length; i++) {
    334         var label = labels[i];
    335         var item = data[label];
    336         var row = Element.create('tr', {}, [Element.create('td', {}, [label]), Element.create('td', {})]);
    337         var valueCell = row.lastChild;
    338 
    339         if (item.numberOfReviews != undefined) {
    340             valueCell.append(
    341                 Element.create('span', {'class': 'bar reviews', 'style': computeStyleForBar(item.numberOfReviews) }),
    342                 Element.create('span', {'class': 'value-container'}, [item.numberOfReviews]),
    343                 Element.create('br')
    344             );
    345         }
    346 
    347         valueCell.append(Element.create('span', {'class': 'bar reviewed-patches', 'style': computeStyleForBar(item.numberOfReviewedPatches) }));
    348         if (item.numberOfUnreviewedPatches !== undefined)
    349             valueCell.append(Element.create('span', {'class': 'bar unreviewed-patches', 'style': computeStyleForBar(item.numberOfUnreviewedPatches) }));
    350 
    351         valueCell.append(Element.create('span', {'class': 'value-container'},
    352             [item.numberOfReviewedPatches + (item.numberOfUnreviewedPatches !== undefined ? ':' + item.numberOfUnreviewedPatches : '')]));
    353 
    354         table.firstChild.append(row);
    355         row.label = label;
    356         row.data = item;
    357     }
    358     element.append(table);
    359 }
    360 
    361 var contributorsView = new GraphView(document.querySelector('#contributors'));
    362 var areasView = new GraphView(document.querySelector('#areas'));
    363 
    364 getJSON('summary.json',
    365     function (summary) {
    366         var summaryContainer = document.querySelector('#summary');
    367         summaryContainer.append(Element.create('dl', {}, [
    368             Element.create('dt', {}, ['Total entries (reviewed)']),
    369             Element.create('dd', {}, [(summary['reviewed'] + summary['unreviewed']) + ' (' + summary['reviewed'] + ')']),
    370             Element.create('dt', {}, ['Total contributors']),
    371             Element.create('dd', {}, [summary['contributors']]),
    372             Element.create('dt', {}, ['Contributors who reviewed']),
    373             Element.create('dd', {}, [summary['contributors_with_reviews']]),
    374         ]));
    375     });
    376 
    377 getJSON('contributors.json',
    378     function (contributors) {
    379         for (var contributor in contributors) {
    380             contributor = contributors[contributor];
    381             contributor.numberOfReviews = contributor.reviews ? contributor.reviews.total : 0;
    382             contributor.numberOfReviewedPatches = contributor.patches ? contributor.patches.reviewed : 0;
    383             contributor.numberOfUnreviewedPatches = contributor.patches ? contributor.patches.unreviewed : 0;
    384         }
    385         contributorsView.setDefaultData(contributors);
    386     });
    387 
    388 getJSON('areas.json',
    389     function (areas) {
    390         for (var area in areas) {
    391             areas[area].numberOfReviewedPatches = areas[area].reviewed;
    392             areas[area].numberOfUnreviewedPatches = areas[area].unreviewed;
    393         }
    394         areasView.setDefaultData(areas);
    395     });
    396 
    397 function contributorAreas(contributorData) {
    398     var areas = new Object;
    399     for (var area in contributorData.reviews.areas) {
    400         if (!areas[area])
    401             areas[area] = {'numberOfReviewedPatches': 0};
    402         areas[area].numberOfReviews = contributorData.reviews.areas[area];
    403     }
    404     for (var area in contributorData.patches.areas) {
    405         if (!areas[area])
    406             areas[area] = {'numberOfReviews': 0};
    407         areas[area].numberOfReviewedPatches = contributorData.patches.areas[area];
    408     }
    409     return areas;
    410 }
    411 
    412 function areaContributors(areaData) {
    413     var contributors = areaData['contributors'];
    414     for (var contributor in contributors) {
    415         contributor = contributors[contributor];
    416         contributor.numberOfReviews = contributor.reviews;
    417         contributor.numberOfReviewedPatches = contributor.reviewed;
    418         contributor.numberOfUnreviewedPatches = contributor.unreviewed;
    419     }
    420     return contributors;
    421 }
    422 
    423 var mouseTimer = 0;
    424 window.onmouseover = function (event) {
    425     clearTimeout(mouseTimer);
    426 
    427     var row = contributorsView.targetRow(event.target);
    428     if (row) {
    429         if (!contributorsView.isConstrained()) {
    430             contributorsView.selectRow(row);
    431             areasView.setMarginTop(row.firstChild.offsetTop);
    432             areasView.setData(contributorAreas(row.data), 'constrained');
    433         }
    434         return;
    435     }
    436 
    437     row = areasView.targetRow(event.target);
    438     if (row) {
    439         if (!areasView.isConstrained()) {
    440             areasView.selectRow(row);
    441             contributorsView.setMarginTop(row.firstChild.offsetTop);
    442             contributorsView.setData(areaContributors(row.data), 'constrained');
    443         }
    444         return;
    445     }
    446 
    447     mouseTimer = setTimeout(function () {
    448         contributorsView.reset();
    449         areasView.reset();
    450     }, 500);
    451 }
    452 
    453 </script>
    454 </body>
    455 </html>
    456