| OLD | NEW |
| (Empty) |
| 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-pa
tches">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 Chara
cterData) | |
| 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.classLi
st.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.marginTo
p = y ? y + 'px' : null; } | |
| 295 GraphView.prototype._graphContainer = function () { return this._container.getEl
ementsByClassName('graph-container')[0]; } | |
| 296 GraphView.prototype._clearGraph = function () { return this._graphContainer().re
moveAllChildren(); } | |
| 297 | |
| 298 GraphView.prototype._numberOfPatches = function (dataItem) { | |
| 299 return dataItem.numberOfReviewedPatches + (dataItem.numberOfUnreviewedPatche
s !== 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].numberOfReview
s !== undefined ? data[label].numberOfReviews : 0); | |
| 306 })); | |
| 307 } | |
| 308 | |
| 309 GraphView.prototype._sortLabelsByNumberOfReviwsAndReviewedPatches = function(dat
a) { | |
| 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 !== undefin
ed ? dataItem.numberOfReviews : 0); | |
| 316 } | |
| 317 labels.sort(function (a, b) { return computeValue(data[b]) - computeValue(da
ta[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]), E
lement.create('td', {})]); | |
| 337 var valueCell = row.lastChild; | |
| 338 | |
| 339 if (item.numberOfReviews != undefined) { | |
| 340 valueCell.append( | |
| 341 Element.create('span', {'class': 'bar reviews', 'style': compute
StyleForBar(item.numberOfReviews) }), | |
| 342 Element.create('span', {'class': 'value-container'}, [item.numbe
rOfReviews]), | |
| 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-pa
tches', '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.revi
ews.total : 0; | |
| 382 contributor.numberOfReviewedPatches = contributor.patches ? contribu
tor.patches.reviewed : 0; | |
| 383 contributor.numberOfUnreviewedPatches = contributor.patches ? contri
butor.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> | |
| OLD | NEW |