blob: abf80d84fd20be35b7e9fb06d35fdc6fa8a2e8cf [file] [log] [blame]
<!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>