1 <!DOCTYPE html> 2 <!-- 3 Copyright 2016 The Chromium Authors. All rights reserved. 4 Use of this source code is governed by a BSD-style license that can be 5 found in the LICENSE file. 6 --> 7 8 <link rel="import" href="/components/core-icon-button/core-icon-button.html"> 9 <link rel="import" href="/components/core-selector/core-selector.html"> 10 <link rel="import" href="/components/paper-icon-button/paper-icon-button.html"> 11 <link rel="import" href="/components/paper-shadow/paper-shadow.html"> 12 <link rel="import" href="/components/paper-spinner/paper-spinner.html"> 13 14 <link rel="import" href="/dashboard/elements/tooltip-test-description.html"> 15 16 <polymer-element name="chart-legend" 17 attributes="seriesGroupList indicesToGraph showCompact 18 collapseLegend deltaAbsolute deltaPercent showDelta"> 19 <template> 20 <style> 21 .row { 22 margin-bottom: 2px; 23 opacity: 1; 24 } 25 26 .row:hover core-icon.info { 27 visibility: visible; 28 } 29 30 .row[loading] { 31 opacity: 0.5; 32 } 33 34 .last-important-test { 35 border-bottom: 1px solid #ebebeb; 36 margin-bottom: 10px; 37 } 38 39 .series-set { 40 background: #f5f5f5; 41 box-sizing: border-box; 42 margin: 1px 1px 5px 1px; 43 padding: 3px; 44 width: 293px; 45 } 46 47 core-icon.info { 48 height: 15px; 49 width: 15px; 50 opacity: .75; 51 margin-left: 5px; 52 cursor: pointer; 53 visibility: hidden; 54 } 55 56 .close-icon { 57 cursor: pointer; 58 } 59 60 .test-name { 61 font-weight: normal; 62 word-break: break-all; 63 width: 100%; 64 margin-right: 2px; 65 } 66 67 .test-name[important] { 68 font-size: 105%; 69 font-weight: bolder; 70 } 71 72 /* Checkboxes */ 73 input[type=checkbox]:checked::after { 74 font-size: 1.3em; 75 content: ""; 76 position: absolute; 77 top: -5px; 78 left: -1px; 79 } 80 81 input[type=checkbox]:focus { 82 outline: none; 83 border-color: #4d90fe; 84 } 85 86 input[type=checkbox] { 87 -webkit-appearance: none; 88 width: 13px; 89 height: 13px; 90 border: 1px solid #c6c6c6; 91 border-radius: 1px; 92 box-sizing: border-box; 93 cursor: default; 94 position: relative; 95 padding: 0 10px 0 0; 96 margin-right: 10px; 97 } 98 99 .checkbox-container { 100 display: inline-table; 101 } 102 103 .bottom-more-btn { 104 color: blue; 105 } 106 107 .expand-link { 108 text-decoration: none; 109 } 110 111 #rhs { 112 margin: 8px 10px 20px 5px; 113 padding: 16px 2px 0 16px; 114 box-shadow: 0 4px 16px rgba(0,0,0,0.2); 115 outline: 1px solid rgba(0,0,0,0.2); 116 font-size: 11px; 117 height: 246px; 118 width: 312px; 119 overflow-y: hidden; 120 } 121 122 #rhs[compact] { 123 width: 125px; 124 } 125 126 #rhs[collapse-legend] { 127 margin-top: 8px; 128 height: 25px; 129 width: 25px; 130 padding: 0; 131 } 132 133 #expand-legend-btn { 134 position: absolute; 135 right: 2px; 136 top: 1px; 137 opacity: .75; 138 } 139 140 #delta-off, #delta-drag { 141 margin-bottom: 5px; 142 } 143 144 #traces { 145 margin-bottom: 10px; 146 } 147 148 .trace-link { 149 text-decoration: none; 150 } 151 152 #sg-container { 153 width: 312px; 154 height: 205px; 155 overflow: auto; 156 } 157 158 #expand { 159 display: inline; 160 } 161 162 paper-spinner { 163 width: 18px; 164 height: 18px; 165 } 166 167 .sg-loading { 168 text-align:center; 169 padding-bottom: 2px; 170 } 171 </style> 172 173 <div id="rhs" compact?="{{showCompact}}" collapse-legend?="{{collapseLegend}}"> 174 <paper-icon-button id="expand-legend-btn" icon="arrow-drop-down" 175 title="legend" role="button" 176 on-click="{{toggleLegend}}"></paper-icon-button> 177 178 <core-collapse id="collapsible-legend" opened?="{{!collapseLegend}}"> 179 180 <template bind if="{{!showDelta}}"> 181 <div id="delta-off">Click and drag graph to measure or zoom.</div> 182 </template> 183 <template bind if="{{showDelta}}"> 184 Delta: {{deltaAbsolute}} or {{deltaPercent}}%.<br> 185 Click selected range to zoom. 186 </template> 187 188 <div id="traces">Traces: 189 <a href="javascript:void(0);" 190 class="trace-link" 191 on-click="{{onSelectAll}}">select all</a> 192 | 193 <a href="javascript:void(0);" 194 class="trace-link" 195 on-click="{{onDeselectAll}}">deselect all</a> 196 | 197 <a href="javascript:void(0);" 198 class="trace-link" 199 on-click="{{onSelectCore}}">core only</a> 200 </div> 201 202 <!-- List of series group boxes starts here. --> 203 <div id="sg-container"> 204 <template repeat="{{seriesGroup, groupIndex in seriesGroupList}}"> 205 206 <paper-shadow z="1" 207 class="series-set" 208 draggable="true" 209 on-dragstart="{{onSeriesDragStart}}" 210 on-dragend="{{onSeriesDragEnd}}"> 211 212 <div horizontal layout> 213 <input type="checkbox" 214 on-change="{{onCheckAllCheckboxClicked}}" 215 checked="{{seriesGroup.selection == 'all'}}" 216 hidden?="{{seriesGroup.tests.length == 0}}"> 217 <span flex four></span> 218 <div class="close-icon" on-click="{{onCloseSeriesGroupClicked}}"> 219 <!-- cross mark U+274C --> 220 </div> 221 </div> 222 223 <core-selector class="list" selected="{{multiSelected}}" multi> 224 225 <template repeat="{{test, testIndex in seriesGroup.tests}}"> 226 <div class="row" id="{{test.index}}" 227 loading?="{{test.index == undefined}}" 228 hidden?="{{test.hidden}}"> 229 230 <label horizontal layout center> 231 232 <input type="checkbox" 233 checked="{{test.selected}}" 234 on-change="{{onCheckboxClicked}}" 235 disabled?="{{test.index == undefined}}"> 236 <span class="test-name" 237 important?="{{test.important}}" 238 style="color:{{test.color}};" 239 on-mouseover='{{seriesMouseover}}' 240 on-mouseout='{{seriesMouseout}}'> 241 {{test.name}} 242 <core-icon icon="info-outline" class="info" 243 on-click="{{toggleDescription}}"></core-icon> 244 </span> 245 </label> 246 247 </div> 248 249 </template> 250 251 </core-selector> 252 253 <div horizontal end-justified layout> 254 <template if="{{seriesGroup.numHidden > 0}}"> 255 <a href="javascript:void(0);" class="expand-link" 256 on-click="{{onExpandSeriesClicked}}">{{seriesGroup.numHidden}} more</a> 257 </template> 258 <template if="{{seriesGroup.numHidden == 0}}"> 259 <a href="javascript:void(0);" class="expand-link" 260 on-click="{{onExpandSeriesClicked}}">less</a> 261 </template> 262 </div> 263 264 <template if="{{seriesGroup.numPendingRequests > 0}}"> 265 <div class="sg-loading"> 266 <paper-spinner active></paper-spinner> 267 </div> 268 </template> 269 </paper-shadow> 270 </template> 271 </div> 272 273 274 </core-collapse> 275 </div> 276 277 </template> 278 <script> 279 'use strict'; 280 Polymer('chart-legend', { 281 282 /** 283 * Shows or hides the detailed description for an item in the legend. 284 */ 285 toggleDescription: function(event, detail, sender) { 286 event.preventDefault(); 287 var model = event.target.templateInstance.model; 288 var seriesGroup = this.seriesGroupList[model.groupIndex]; 289 var test = seriesGroup.tests[model.testIndex]; 290 if (test.index == undefined) { 291 return; 292 } 293 294 var description = document.createElement('tooltip-test-description'); 295 description.test = test; 296 // This assumes that the tooltip element is present at the top-level. 297 // See https://github.com/catapult-project/catapult/issues/2172. 298 document.getElementById('legend-details-tooltip').set( 299 description, event.pageX, event.pageY); 300 }, 301 302 /** 303 * Event handler for the change event of any of the checkboxes. 304 */ 305 onCheckboxClicked: function(event, detail, sender) { 306 var model = event.target.templateInstance.model; 307 this.updateIndicesToGraph(model.test.index, sender.checked); 308 this.updateSeriesGroupCheckedState(model.seriesGroup); 309 this.updateNumHidden(model.seriesGroup); 310 this.fireChartStateChangedEvent(); 311 }, 312 313 /** 314 * Updates seriesGroup based on its tests selection state. 315 * A series group is a dictionary that describe the selection state 316 * for a set of series within a test path. 317 * 318 * seriesGroup has the following properties: 319 * { 320 * 'path': 'ChromiumPerf/linux/dromaeo/Total', 321 * 'tests': [{ 322 * name: 'Total', 323 * direction: 'Lower is better', 324 * units: 'm/s', 325 * etc... 326 * }], 327 * 'selection': 'all', 328 * 'numHidden': null 329 * } 330 * 331 * @param {Object} seriesGroup A group of series. 332 */ 333 updateSeriesGroupCheckedState: function(seriesGroup) { 334 var allSelected = []; 335 var allUnselected = []; 336 seriesGroup.tests.forEach(function(test) { 337 if (test.selected) { 338 allSelected.push(test); 339 } else { 340 allUnselected.push(test); 341 } 342 }); 343 344 if (this.importantSelected(seriesGroup.tests)) { 345 seriesGroup.selection = 'important'; 346 } else if (allSelected.length == seriesGroup.tests.length) { 347 seriesGroup.selection = 'all'; 348 } else if (allUnselected.length == seriesGroup.tests.length) { 349 seriesGroup.selection = 'none'; 350 } else { 351 seriesGroup.selection = null; 352 } 353 }, 354 355 /** 356 * Returns true if only important series are selected. 357 */ 358 importantSelected: function(tests) { 359 var hasImportant = false; 360 for (var i = 0; i < tests.length; i++) { 361 var test = tests[i]; 362 if (test.important) { 363 if (!test.selected) { 364 return false; 365 } 366 hasImportant = true; 367 } else if (test.selected) { 368 return false; 369 } 370 } 371 return hasImportant; 372 }, 373 374 /** 375 * Updates numHidden properties for a seriesGroup. This is to show the 376 * number of hidden series link. 377 */ 378 updateNumHidden: function(seriesGroup) { 379 var numHidden = 0; 380 var numCanHide = 0; 381 seriesGroup.tests.forEach(function(test) { 382 if (test.hidden) { 383 numHidden++; 384 } else if (!test.important && !test.checked) { 385 numCanHide++; 386 } 387 }); 388 389 // Don't show more/less link if the only series shown are the important. 390 if (numHidden > 0) { 391 seriesGroup.numHidden = numHidden; 392 } else if (numCanHide > 0) { 393 seriesGroup.numHidden = 0; 394 } else { 395 seriesGroup.numHidden = null; 396 } 397 }, 398 399 /** 400 * Event handler for the change event of check all checkboxes. 401 */ 402 onCheckAllCheckboxClicked: function(event, detail, sender) { 403 var model = event.target.templateInstance.model; 404 var groupIndex = model.groupIndex; 405 406 this.seriesGroupList[groupIndex].selection = ( 407 sender.checked ? 'all' : 'none'); 408 var tests = this.seriesGroupList[groupIndex].tests; 409 for (var i = 0; i < tests.length; i++) { 410 if (tests[i].index != undefined) { 411 tests[i].selected = sender.checked; 412 this.updateIndicesToGraph(tests[i].index, sender.checked); 413 } 414 } 415 416 this.fireChartStateChangedEvent(); 417 }, 418 419 /** 420 * Event handler for series group close button clicked. 421 */ 422 onCloseSeriesGroupClicked: function(event, detail, sender) { 423 var model = event.target.templateInstance.model; 424 this.fire('seriesgroupclosed', {'groupIndex': model.groupIndex}); 425 }, 426 427 /** 428 * Event handler for click event of expand link. 429 */ 430 onExpandSeriesClicked: function(event, detail, sender) { 431 var groupIndex = event.target.templateInstance.model.groupIndex; 432 var isCollapse = sender.text == 'less' ? true : false; 433 var seriesGroup = this.seriesGroupList[groupIndex]; 434 seriesGroup.tests.forEach(function(test) { 435 if (isCollapse) { 436 if (!test.selected && !test.important) { 437 test.hidden = true; 438 } 439 } else { 440 test.hidden = false; 441 } 442 }); 443 this.updateNumHidden(seriesGroup); 444 }, 445 446 /** 447 * On series group box drag-start, set data to be transferred to on 448 * drop event. 449 */ 450 onSeriesDragStart: function(event, detail, sender) { 451 var groupIndex = event.target.templateInstance.model.groupIndex; 452 var testPath = this.seriesGroupList[groupIndex].path; 453 var selectedTests = []; 454 var tests = this.seriesGroupList[groupIndex].tests; 455 for (var i = 0; i < tests.length; i++) { 456 if (tests[i].selected) { 457 selectedTests.push(tests[i].name); 458 } 459 } 460 event.dataTransfer.setData('type', 'seriesdnd'); 461 // chart-container takes a list of test path and selected tests pair. 462 event.dataTransfer.setData( 463 'data', JSON.stringify([[testPath, selectedTests]])); 464 event.dataTransfer.effectAllowed = 'copy'; 465 }, 466 467 /** 468 * On series group box drag-end, checks if drop target is valid, 469 * and remove series group. 470 */ 471 onSeriesDragEnd: function(event, detail, sender) { 472 // Successful drop. 473 if (event.dataTransfer.dropEffect == 'copy') { 474 var model = event.target.templateInstance.model; 475 // Let chart-container handle removing this group series. 476 this.fire('seriesgroupclosed', {'groupIndex': model.groupIndex}); 477 } 478 }, 479 480 fireChartStateChangedEvent: function() { 481 this.fire('chartstatechanged', { 482 target: this, 483 stateName: 'chartstatechanged', 484 state: this.seriesGroupList 485 }); 486 }, 487 488 /** 489 * Handler for the click event of the select all traces button. 490 * Updates this.indicesToGraph to contain all traces. 491 * @param {Event=} opt_noEvent The click event, not used. 492 */ 493 onSelectAll: function(opt_noEvent) { 494 this.indicesToGraph = []; 495 for (var i = 0; i < this.seriesGroupList.length; i++) { 496 var group = this.seriesGroupList[i]; 497 for (var i = 0; i < group.tests.length; i++) { 498 group.tests[i].selected = true; 499 this.indicesToGraph.push(group.tests[i].index); 500 } 501 this.updateSeriesGroupCheckedState(group); 502 this.updateNumHidden(group); 503 } 504 this.fireChartStateChangedEvent(); 505 }, 506 507 /** 508 * Handler for the click event of the deselect all traces button. 509 * @param {Event=} opt_noEvent The click event, not used. 510 */ 511 onDeselectAll: function(opt_noEvent) { 512 this.indicesToGraph = []; 513 for (var i = 0; i < this.seriesGroupList.length; i++) { 514 var group = this.seriesGroupList[i]; 515 for (var i = 0; i < group.tests.length; i++) { 516 group.tests[i].selected = false; 517 } 518 this.updateSeriesGroupCheckedState(group); 519 this.updateNumHidden(group); 520 } 521 this.fireChartStateChangedEvent(); 522 }, 523 524 /** 525 * Handler for the click event of the select core traces button. 526 * Selects only the core traces (i.e. important and ref traces). 527 * Note: The property 'coreTraces' is set in graph.js. 528 * @param {Event=} opt_event The click event, not used. 529 */ 530 onSelectCore: function(opt_noEvent) { 531 this.indicesToGraph = []; 532 for (var i = 0; i < this.seriesGroupList.length; i++) { 533 var group = this.seriesGroupList[i]; 534 for (var i = 0; i < group.tests.length; i++) { 535 var test = group.tests[i]; 536 test.selected = test.important; 537 this.updateIndicesToGraph(test.index, test.selected); 538 } 539 this.updateSeriesGroupCheckedState(group); 540 this.updateNumHidden(group); 541 } 542 this.fireChartStateChangedEvent(); 543 }, 544 545 seriesMouseover: function(event, detail, sender) { 546 var model = event.target.templateInstance.model; 547 this.fire('seriesmouseover', { 548 'index': model.test.index 549 }); 550 }, 551 552 seriesMouseout: function(event, detail, sender) { 553 var model = event.target.templateInstance.model; 554 this.fire('seriesmouseout', { 555 'index': model.test.index 556 }); 557 }, 558 559 /** 560 * Adds or removes a series index from |this.indicesToGraph|. 561 * @param {number} index The index to add or remove. 562 * @param {boolean} selected Whether to add the index. 563 */ 564 updateIndicesToGraph: function(index, selected) { 565 if (selected) { 566 if (this.indicesToGraph.indexOf(index) == -1) { 567 this.indicesToGraph.push(index); 568 } 569 } else { 570 if (this.indicesToGraph.indexOf(index) != -1) { 571 this.indicesToGraph.splice(this.indicesToGraph.indexOf(index), 1); 572 } 573 } 574 }, 575 576 /** 577 * Toggles legend window to collapse or expand. 578 */ 579 toggleLegend: function() { 580 this.$['collapsible-legend'].toggle(); 581 this.collapseLegend = !this.collapseLegend; 582 } 583 }); 584 585 586