• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1<!DOCTYPE html>
2<html>
3<head>
4<title>ChangeLog Analysis</title>
5<style type="text/css">
6
7body {
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
20h1, h2, h3 {
21    font-weight: 200;
22}
23
24h1 {
25    margin: 0 0 1em 0;
26}
27
28h2 {
29    font-size: 1.2em;
30    text-align: center;
31    margin-bottom: 1em;
32}
33
34h3 {
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
187if (!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
200if (!Node.prototype.remove) {
201    Node.prototype.remove = function () {
202        this.parentNode.removeChild(this);
203        return this;
204    }
205}
206
207if (!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
227if (!Node.prototype.removeAllChildren) {
228    Node.prototype.removeAllChildren = function () {
229        while (this.firstChild)
230            this.firstChild.remove();
231        return this;
232    }
233}
234
235Element.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
241function 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
251function GraphView(container) {
252    this._container = container;
253    this._defaultData = null;
254}
255
256GraphView.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
265GraphView.prototype.setDefaultData = function(data) {
266    this._defaultData = data;
267    this.setData(data);
268}
269
270GraphView.prototype.reset = function () {
271    this.setMarginTop();
272    this.setData(this._defaultData);
273}
274
275GraphView.prototype.isConstrained = function () { return this._container.classList.contains('constrained'); }
276
277GraphView.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
289GraphView.prototype.selectRow = function (row) {
290    this._container.removeClassNameFromAllElements('selected');
291    row.classList.add('selected');
292}
293
294GraphView.prototype.setMarginTop = function (y) { this._container.style.marginTop = y ? y + 'px' : null; }
295GraphView.prototype._graphContainer = function () { return this._container.getElementsByClassName('graph-container')[0]; }
296GraphView.prototype._clearGraph = function () { return this._graphContainer().removeAllChildren(); }
297
298GraphView.prototype._numberOfPatches = function (dataItem) {
299    return dataItem.numberOfReviewedPatches + (dataItem.numberOfUnreviewedPatches !== undefined ? dataItem.numberOfUnreviewedPatches : 0);
300}
301
302GraphView.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
309GraphView.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
321GraphView.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
361var contributorsView = new GraphView(document.querySelector('#contributors'));
362var areasView = new GraphView(document.querySelector('#areas'));
363
364getJSON('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
377getJSON('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
388getJSON('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
397function 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
412function 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
423var mouseTimer = 0;
424window.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