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