| <!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> |