Home | History | Annotate | Download | only in elements
      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           &#124;
    193           <a href="javascript:void(0);"
    194              class="trace-link"
    195              on-click="{{onDeselectAll}}">deselect all</a>
    196           &#124;
    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