| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>ChangeLog Analysis</title> | |
| <style type="text/css"> | |
| body { | |
| font-family: 'Helvetica' 'Segoe UI Light' sans-serif; | |
| font-weight: 200; | |
| padding: 20px; | |
| min-width: 1200px; | |
| } | |
| * { | |
| padding: 0px; | |
| margin: 0px; | |
| border: 0px; | |
| } | |
| h1, h2, h3 { | |
| font-weight: 200; | |
| } | |
| h1 { | |
| margin: 0 0 1em 0; | |
| } | |
| h2 { | |
| font-size: 1.2em; | |
| text-align: center; | |
| margin-bottom: 1em; | |
| } | |
| h3 { | |
| font-size: 1em; | |
| } | |
| .view { | |
| margin: 0px; | |
| width: 600px; | |
| float: left; | |
| } | |
| .graph-container p { | |
| width: 200px; | |
| text-align: right; | |
| margin: 20px 0 20px 0; | |
| padding: 5px; | |
| border-right: solid 1px black; | |
| } | |
| .graph-container table { | |
| width: 100%; | |
| } | |
| .graph-container table, .graph-container td { | |
| border-collapse: collapse; | |
| border: none; | |
| } | |
| .graph-container td { | |
| padding: 5px; | |
| vertical-align: center; | |
| } | |
| .graph-container td:first-child { | |
| width: 200px; | |
| text-align: right; | |
| border-right: solid 1px black; | |
| } | |
| .graph-container .selected { | |
| background: #eee; | |
| } | |
| #reviewers .selected td:first-child { | |
| border-radius: 10px 0px 0px 10px; | |
| } | |
| #areas .selected td:last-child { | |
| border-radius: 0px 10px 10px 0px; | |
| } | |
| .graph-container .bar { | |
| display: inline-block; | |
| min-height: 1em; | |
| background: #9f6; | |
| margin-right: 0.4ex; | |
| } | |
| .graph-container .reviewed-patches { | |
| background: #3cf; | |
| margin-right: 1px; | |
| } | |
| .graph-container .unreviewed-patches { | |
| background: #f99; | |
| } | |
| .constrained { | |
| background: #eee; | |
| border-radius: 10px; | |
| } | |
| .constrained .vertical-bar { | |
| border-right: solid 1px #eee; | |
| } | |
| #header { | |
| border-spacing: 5px; | |
| } | |
| #header section { | |
| display: table-cell; | |
| width: 200px; | |
| vertical-align: top; | |
| border: solid 2px #ccc; | |
| border-collapse: collapse; | |
| padding: 5px; | |
| font-size: 0.8em; | |
| } | |
| #header dt { | |
| float: left; | |
| } | |
| #header dt:after { | |
| content: ': '; | |
| } | |
| #header .legend { | |
| width: 600px; | |
| } | |
| .legend .bar { | |
| width: 15ex; | |
| padding: 2px; | |
| } | |
| .legend .reviews { | |
| width: 25ex; | |
| } | |
| .legend td:first-child { | |
| width: 18ex; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>ChangeLog Analysis</h1> | |
| <section id="header"> | |
| <section id="summary"> | |
| <h2>Summary</h2> | |
| </section> | |
| <section class="legend"> | |
| <h2>Legend</h2> | |
| <div class="graph-container"> | |
| <table> | |
| <tbody> | |
| <tr><td>Contributor's name</td> | |
| <td><span class="bar reviews">Reviews</span> <span class="value-container">(# of reviews)</span><br> | |
| <span class="bar reviewed-patches">Reviewed</span><span class="bar unreviewed-patches">Unreviewed</span> | |
| <span class="value-container">(# of reviewed):(# of unreviewed)</span></td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </section> | |
| </section> | |
| <section id="contributors" class="view"> | |
| <h2 id="contributors-title">Contributors</h2> | |
| <div class="graph-container"></div> | |
| </section> | |
| <section id="areas" class="view"> | |
| <h2 id="areas-title">Areas of contributions</h2> | |
| <div class="graph-container"></div> | |
| </section> | |
| <script> | |
| // Naive implementation of element extensions discussed on public-webapps | |
| if (!Element.prototype.append) { | |
| Element.prototype.append = function () { | |
| for (var i = 0; i < arguments.length; i++) { | |
| // FIXME: Take care of other node types | |
| if (arguments[i] instanceof Element || arguments[i] instanceof CharacterData) | |
| this.appendChild(arguments[i]); | |
| else | |
| this.appendChild(document.createTextNode(arguments[i])); | |
| } | |
| return this; | |
| } | |
| } | |
| if (!Node.prototype.remove) { | |
| Node.prototype.remove = function () { | |
| this.parentNode.removeChild(this); | |
| return this; | |
| } | |
| } | |
| if (!Element.create) { | |
| Element.create = function () { | |
| if (arguments.length < 1) | |
| return null; | |
| var element = document.createElement(arguments[0]); | |
| if (arguments.length == 1) | |
| return element; | |
| // FIXME: the second argument can be content or IDL attributes | |
| var attributes = arguments[1]; | |
| for (attribute in attributes) | |
| element.setAttribute(attribute, attributes[attribute]); | |
| if (arguments.length >= 3) | |
| element.append.apply(element, arguments[2]); | |
| return element; | |
| } | |
| } | |
| if (!Node.prototype.removeAllChildren) { | |
| Node.prototype.removeAllChildren = function () { | |
| while (this.firstChild) | |
| this.firstChild.remove(); | |
| return this; | |
| } | |
| } | |
| Element.prototype.removeClassNameFromAllElements = function (className) { | |
| var elements = this.getElementsByClassName(className); | |
| for (var i = 0; i < elements.length; i++) | |
| elements[i].classList.remove(className); | |
| } | |
| function getJSON(url, callback) { | |
| var xhr = new XMLHttpRequest(); | |
| xhr.open('GET', url, true); | |
| xhr.onreadystatechange = function () { | |
| if (this.readyState == 4) | |
| callback(JSON.parse(xhr.responseText)); | |
| } | |
| xhr.send(); | |
| } | |
| function GraphView(container) { | |
| this._container = container; | |
| this._defaultData = null; | |
| } | |
| GraphView.prototype.setData = function(data, constrained) { | |
| if (constrained) | |
| this._container.classList.add('constrained'); | |
| else | |
| this._container.classList.remove('constrained'); | |
| this._clearGraph(); | |
| this._constructGraph(data); | |
| } | |
| GraphView.prototype.setDefaultData = function(data) { | |
| this._defaultData = data; | |
| this.setData(data); | |
| } | |
| GraphView.prototype.reset = function () { | |
| this.setMarginTop(); | |
| this.setData(this._defaultData); | |
| } | |
| GraphView.prototype.isConstrained = function () { return this._container.classList.contains('constrained'); } | |
| GraphView.prototype.targetRow = function (node) { | |
| var target = null; | |
| while (node && node != this._container) { | |
| if (node.localName == 'tr') | |
| target = node; | |
| node = node.parentNode; | |
| } | |
| return node && target; | |
| } | |
| GraphView.prototype.selectRow = function (row) { | |
| this._container.removeClassNameFromAllElements('selected'); | |
| row.classList.add('selected'); | |
| } | |
| GraphView.prototype.setMarginTop = function (y) { this._container.style.marginTop = y ? y + 'px' : null; } | |
| GraphView.prototype._graphContainer = function () { return this._container.getElementsByClassName('graph-container')[0]; } | |
| GraphView.prototype._clearGraph = function () { return this._graphContainer().removeAllChildren(); } | |
| GraphView.prototype._numberOfPatches = function (dataItem) { | |
| return dataItem.numberOfReviewedPatches + (dataItem.numberOfUnreviewedPatches !== undefined ? dataItem.numberOfUnreviewedPatches : 0); | |
| } | |
| GraphView.prototype._maximumValue = function (labels, data) { | |
| var numberOfPatches = this._numberOfPatches; | |
| return Math.max.apply(null, labels.map(function (label) { | |
| return Math.max(numberOfPatches(data[label]), data[label].numberOfReviews !== undefined ? data[label].numberOfReviews : 0); | |
| })); | |
| } | |
| GraphView.prototype._sortLabelsByNumberOfReviwsAndReviewedPatches = function(data) { | |
| var labels = Object.keys(data); | |
| if (!labels.length) | |
| return null; | |
| var numberOfPatches = this._numberOfPatches; | |
| var computeValue = function (dataItem) { | |
| return numberOfPatches(dataItem) + (dataItem.numberOfReviews !== undefined ? dataItem.numberOfReviews : 0); | |
| } | |
| labels.sort(function (a, b) { return computeValue(data[b]) - computeValue(data[a]); }); | |
| return labels; | |
| } | |
| GraphView.prototype._constructGraph = function (data) { | |
| var element = this._graphContainer(); | |
| var labels = this._sortLabelsByNumberOfReviwsAndReviewedPatches(data); | |
| if (!labels) { | |
| element.append(Element.create('p', {}, ['None'])); | |
| return; | |
| } | |
| var maxValue = this._maximumValue(labels, data); | |
| var computeStyleForBar = function (value) { return 'width:' + (value * 85.0 / maxValue) + '%' } | |
| var table = Element.create('table', {}, [Element.create('tbody')]); | |
| for (var i = 0; i < labels.length; i++) { | |
| var label = labels[i]; | |
| var item = data[label]; | |
| var row = Element.create('tr', {}, [Element.create('td', {}, [label]), Element.create('td', {})]); | |
| var valueCell = row.lastChild; | |
| if (item.numberOfReviews != undefined) { | |
| valueCell.append( | |
| Element.create('span', {'class': 'bar reviews', 'style': computeStyleForBar(item.numberOfReviews) }), | |
| Element.create('span', {'class': 'value-container'}, [item.numberOfReviews]), | |
| Element.create('br') | |
| ); | |
| } | |
| valueCell.append(Element.create('span', {'class': 'bar reviewed-patches', 'style': computeStyleForBar(item.numberOfReviewedPatches) })); | |
| if (item.numberOfUnreviewedPatches !== undefined) | |
| valueCell.append(Element.create('span', {'class': 'bar unreviewed-patches', 'style': computeStyleForBar(item.numberOfUnreviewedPatches) })); | |
| valueCell.append(Element.create('span', {'class': 'value-container'}, | |
| [item.numberOfReviewedPatches + (item.numberOfUnreviewedPatches !== undefined ? ':' + item.numberOfUnreviewedPatches : '')])); | |
| table.firstChild.append(row); | |
| row.label = label; | |
| row.data = item; | |
| } | |
| element.append(table); | |
| } | |
| var contributorsView = new GraphView(document.querySelector('#contributors')); | |
| var areasView = new GraphView(document.querySelector('#areas')); | |
| getJSON('summary.json', | |
| function (summary) { | |
| var summaryContainer = document.querySelector('#summary'); | |
| summaryContainer.append(Element.create('dl', {}, [ | |
| Element.create('dt', {}, ['Total entries (reviewed)']), | |
| Element.create('dd', {}, [(summary['reviewed'] + summary['unreviewed']) + ' (' + summary['reviewed'] + ')']), | |
| Element.create('dt', {}, ['Total contributors']), | |
| Element.create('dd', {}, [summary['contributors']]), | |
| Element.create('dt', {}, ['Contributors who reviewed']), | |
| Element.create('dd', {}, [summary['contributors_with_reviews']]), | |
| ])); | |
| }); | |
| getJSON('contributors.json', | |
| function (contributors) { | |
| for (var contributor in contributors) { | |
| contributor = contributors[contributor]; | |
| contributor.numberOfReviews = contributor.reviews ? contributor.reviews.total : 0; | |
| contributor.numberOfReviewedPatches = contributor.patches ? contributor.patches.reviewed : 0; | |
| contributor.numberOfUnreviewedPatches = contributor.patches ? contributor.patches.unreviewed : 0; | |
| } | |
| contributorsView.setDefaultData(contributors); | |
| }); | |
| getJSON('areas.json', | |
| function (areas) { | |
| for (var area in areas) { | |
| areas[area].numberOfReviewedPatches = areas[area].reviewed; | |
| areas[area].numberOfUnreviewedPatches = areas[area].unreviewed; | |
| } | |
| areasView.setDefaultData(areas); | |
| }); | |
| function contributorAreas(contributorData) { | |
| var areas = new Object; | |
| for (var area in contributorData.reviews.areas) { | |
| if (!areas[area]) | |
| areas[area] = {'numberOfReviewedPatches': 0}; | |
| areas[area].numberOfReviews = contributorData.reviews.areas[area]; | |
| } | |
| for (var area in contributorData.patches.areas) { | |
| if (!areas[area]) | |
| areas[area] = {'numberOfReviews': 0}; | |
| areas[area].numberOfReviewedPatches = contributorData.patches.areas[area]; | |
| } | |
| return areas; | |
| } | |
| function areaContributors(areaData) { | |
| var contributors = areaData['contributors']; | |
| for (var contributor in contributors) { | |
| contributor = contributors[contributor]; | |
| contributor.numberOfReviews = contributor.reviews; | |
| contributor.numberOfReviewedPatches = contributor.reviewed; | |
| contributor.numberOfUnreviewedPatches = contributor.unreviewed; | |
| } | |
| return contributors; | |
| } | |
| var mouseTimer = 0; | |
| window.onmouseover = function (event) { | |
| clearTimeout(mouseTimer); | |
| var row = contributorsView.targetRow(event.target); | |
| if (row) { | |
| if (!contributorsView.isConstrained()) { | |
| contributorsView.selectRow(row); | |
| areasView.setMarginTop(row.firstChild.offsetTop); | |
| areasView.setData(contributorAreas(row.data), 'constrained'); | |
| } | |
| return; | |
| } | |
| row = areasView.targetRow(event.target); | |
| if (row) { | |
| if (!areasView.isConstrained()) { | |
| areasView.selectRow(row); | |
| contributorsView.setMarginTop(row.firstChild.offsetTop); | |
| contributorsView.setData(areaContributors(row.data), 'constrained'); | |
| } | |
| return; | |
| } | |
| mouseTimer = setTimeout(function () { | |
| contributorsView.reset(); | |
| areasView.reset(); | |
| }, 500); | |
| } | |
| </script> | |
| </body> | |
| </html> |