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