| <!DOCTYPE html> |
| <!-- |
| Copyright 2016 The Chromium Authors. All rights reserved. |
| Use of this source code is governed by a BSD-style license that can be |
| found in the LICENSE file. |
| --> |
| |
| <link rel="import" href="/components/iron-collapse/iron-collapse.html"> |
| <link rel="import" href="/components/iron-flex-layout/iron-flex-layout-classes.html"> |
| <link rel="import" href="/components/iron-selector/iron-selector.html"> |
| <link rel="import" href="/components/paper-icon-button/paper-icon-button.html"> |
| <link rel="import" href="/components/paper-material/paper-material.html"> |
| <link rel="import" href="/components/paper-spinner/paper-spinner.html"> |
| |
| <link rel="import" href="/dashboard/elements/chart-legend-tooltip.html"> |
| |
| <dom-module id="chart-legend"> |
| <template> |
| <style include="iron-flex iron-flex-alignment"> |
| .row { |
| margin-bottom: 2px; |
| opacity: 1; |
| } |
| |
| .row[loading] { |
| opacity: 0.5; |
| } |
| |
| .last-important-test { |
| border-bottom: 1px solid #ebebeb; |
| margin-bottom: 10px; |
| } |
| |
| .series-set { |
| background: #f5f5f5; |
| box-sizing: border-box; |
| margin: 1px 1px 5px 1px; |
| padding: 3px; |
| width: 293px; |
| } |
| |
| iron-icon.info { |
| height: 15px; |
| width: 15px; |
| opacity: .75; |
| margin-left: 5px; |
| cursor: pointer; |
| } |
| |
| .close-icon { |
| cursor: pointer; |
| } |
| |
| .test-name { |
| font-weight: normal; |
| word-break: break-all; |
| width: 100%; |
| margin-right: 2px; |
| } |
| |
| .test-name[important] { |
| font-size: 105%; |
| font-weight: bolder; |
| } |
| |
| /* Checkboxes */ |
| input[type=checkbox]:checked::after { |
| font-size: 1.3em; |
| content: "✓"; |
| position: absolute; |
| top: -5px; |
| left: -1px; |
| } |
| |
| input[type=checkbox]:focus { |
| outline: none; |
| border-color: #4d90fe; |
| } |
| |
| input[type=checkbox] { |
| -webkit-appearance: none; |
| width: 13px; |
| height: 13px; |
| border: 1px solid #c6c6c6; |
| border-radius: 1px; |
| box-sizing: border-box; |
| cursor: default; |
| position: relative; |
| padding: 0 10px 0 0; |
| margin-right: 10px; |
| } |
| |
| .checkbox-container { |
| display: inline-table; |
| } |
| |
| .bottom-more-btn { |
| color: blue; |
| } |
| |
| .expand-link { |
| text-decoration: none; |
| } |
| |
| #rhs { |
| margin: 8px 10px 20px 5px; |
| padding: 16px 2px 0 16px; |
| box-shadow: 0 4px 16px rgba(0,0,0,0.2); |
| outline: 1px solid rgba(0,0,0,0.2); |
| font-size: 11px; |
| height: 246px; |
| width: 312px; |
| overflow-y: hidden; |
| } |
| |
| #rhs[compact] { |
| width: 125px; |
| } |
| |
| #rhs[collapse-legend] { |
| margin-top: 8px; |
| height: 25px; |
| width: 25px; |
| padding: 0; |
| } |
| |
| #expand-legend-btn { |
| position: absolute; |
| right: 2px; |
| top: 1px; |
| opacity: .75; |
| } |
| |
| #delta-off, #delta-drag { |
| margin-bottom: 5px; |
| } |
| |
| #traces { |
| margin-bottom: 10px; |
| } |
| |
| .trace-link { |
| text-decoration: none; |
| } |
| |
| #sg-container { |
| width: 312px; |
| height: 205px; |
| overflow: auto; |
| } |
| |
| #expand { |
| display: inline; |
| } |
| |
| paper-spinner { |
| width: 18px; |
| height: 18px; |
| } |
| |
| .sg-loading { |
| text-align:center; |
| padding-bottom: 2px; |
| } |
| </style> |
| |
| <div id="rhs" compact$="{{showCompact}}" collapse-legend$="{{collapseLegend}}"> |
| <paper-icon-button id="expand-legend-btn" icon="arrow-drop-down" |
| title="legend" role="button" |
| on-click="toggleLegend"></paper-icon-button> |
| |
| <iron-collapse id="collapsible-legend" opened$="{{!collapseLegend}}"> |
| |
| <template is="dom-if" if="{{!showDelta}}"> |
| <div id="delta-off">Click and drag graph to measure or zoom.</div> |
| </template> |
| <template is="dom-if" if="{{showDelta}}"> |
| Delta: {{deltaAbsolute}} or {{deltaPercent}}%.<br> |
| Click selected range to zoom. |
| </template> |
| |
| <div id="traces">Traces: |
| <a href="javascript:void(0);" |
| class="trace-link" |
| on-click="onSelectAll">select all</a> |
| | |
| <a href="javascript:void(0);" |
| class="trace-link" |
| on-click="onDeselectAll">deselect all</a> |
| | |
| <a href="javascript:void(0);" |
| class="trace-link" |
| on-click="onSelectCore">core only</a> |
| </div> |
| |
| <!-- List of series group boxes starts here. --> |
| <div id="sg-container"> |
| <template is="dom-repeat" items="{{seriesGroupList}}" as="seriesGroup" index-as="groupIndex" id="grouplist"> |
| |
| <paper-material elevation="1" |
| class="series-set" |
| draggable="true" |
| on-dragstart="onSeriesDragStart" |
| on-dragend="onSeriesDragEnd"> |
| |
| <div class="layout horizontal"> |
| <input type="checkbox" |
| on-change="onCheckAllCheckboxClicked" |
| checked="{{computeSelectionIsAll(seriesGroup)}}" |
| hidden?="{{!seriesGroup.tests.length}}"> |
| <span class="flex"></span> |
| <div class="close-icon" on-click="onCloseSeriesGroupClicked"> |
| ❌ <!-- cross mark U+274C --> |
| </div> |
| </div> |
| |
| <iron-selector class="list" selected="{{multiSelected}}" multi> |
| |
| <template is="dom-repeat" items="{{seriesGroup.tests}}" as="test"> |
| <div class="row layout horizontal" id="{{test.index}}" |
| loading$="{{computeIsUndefined(test.index)}}" |
| hidden$="{{test.hidden}}"> |
| <label class="layout horizontal center"> |
| <input type="checkbox" |
| checked="{{test.selected}}" |
| on-change="onCheckboxClicked" |
| disabled$="{{computeIsUndefined(test.index)}}"> |
| <span class="test-name" |
| important$="{{test.important}}" |
| style="color:{{test.color}};" |
| on-mouseover="seriesMouseover" |
| on-mouseout="seriesMouseout"> |
| {{test.name}} |
| </span> |
| </label> |
| <chart-legend-tooltip description={{test.description}} |
| direction={{test.direction}} |
| path={{test.path}} |
| units={{test.units}}></chart-legend> |
| </div> |
| </template> |
| </iron-selector> |
| |
| <div class="layout horizontal end-justified"> |
| <template is="dom-if" if="{{computeIsPositive(seriesGroup.*, 'numHidden')}}"> |
| <a href="javascript:void(0);" class="expand-link" |
| on-click="onExpandSeriesClicked">{{seriesGroup.numHidden}} more</a> |
| </template> |
| <template is="dom-if" if="{{!seriesGroup.numHidden}}"> |
| <a href="javascript:void(0);" class="expand-link" |
| on-click="onExpandSeriesClicked">less</a> |
| </template> |
| </div> |
| |
| <template is="dom-if" if="{{computeIsPositive(seriesGroup.*, 'numPendingRequests')}}"> |
| <div class="sg-loading"> |
| <paper-spinner active></paper-spinner> |
| </div> |
| </template> |
| </paper-material> |
| </template> |
| </div> |
| |
| |
| </iron-collapse> |
| </div> |
| |
| </template> |
| <script> |
| 'use strict'; |
| Polymer({ |
| |
| is: 'chart-legend', |
| properties: { |
| collapseLegend: { notify: true }, |
| deltaAbsolute: { notify: true }, |
| deltaPercent: { notify: true }, |
| indicesToGraph: { notify: true }, |
| seriesGroupList: { |
| notify: true, |
| type: Array, |
| value: () => [] |
| }, |
| showCompact: { notify: true }, |
| showDelta: { notify: true } |
| }, |
| |
| computeIsPositive: function(iterInfo, prop) { |
| return iterInfo.base[prop] > 0; |
| }, |
| computeIsUndefined: x => x === undefined, |
| computeSelectionIsAll: seriesGroup => seriesGroup.selection == 'all', |
| |
| getTestIndexInSeriesGroup: function(seriesGroupIndex, name) { |
| var tests = this.seriesGroupList[seriesGroupIndex].tests; |
| for (var i = 0; i < tests.length; i++) { |
| if (name == tests[i].name) { |
| return i; |
| } |
| } |
| return null; |
| }, |
| |
| /** |
| * Event handler for the change event of any of the checkboxes. |
| */ |
| onCheckboxClicked: function(event, detail) { |
| var test = event.model.test; |
| var seriesGroupIndex = this.$.grouplist.indexForElement(event.target); |
| var testIndex = this.getTestIndexInSeriesGroup( |
| seriesGroupIndex, test.name); |
| if (testIndex !== null) { |
| this.set('seriesGroupList.' + seriesGroupIndex + '.tests.' + |
| testIndex + '.selected', event.currentTarget.checked); |
| } |
| this.updateIndicesToGraph( |
| test.index, event.currentTarget.checked); |
| this.updateSeriesGroupCheckedState(seriesGroupIndex); |
| this.updateNumHidden(seriesGroupIndex); |
| this.fireChartStateChangedEvent(); |
| }, |
| |
| /** |
| * Updates seriesGroup based on its tests selection state. |
| * A series group is a dictionary that describe the selection state |
| * for a set of series within a test path. |
| * |
| * seriesGroup has the following properties: |
| * { |
| * 'path': 'ChromiumPerf/linux/dromaeo/Total', |
| * 'tests': [{ |
| * name: 'Total', |
| * direction: 'Lower is better', |
| * units: 'm/s', |
| * etc... |
| * }], |
| * 'selection': 'all', |
| * 'numHidden': null |
| * } |
| * |
| * @param {Object} seriesGroup A group of series. |
| */ |
| updateSeriesGroupCheckedState: function(seriesGroupIndex) { |
| var allSelected = []; |
| var allUnselected = []; |
| var seriesGroup = this.seriesGroupList[seriesGroupIndex]; |
| seriesGroup.tests.forEach(function(test) { |
| if (test.selected) { |
| allSelected.push(test); |
| } else { |
| allUnselected.push(test); |
| } |
| }.bind(this)); |
| |
| if (this.importantSelected(seriesGroup.tests)) { |
| this.set( |
| 'seriesGroupList.' + seriesGroupIndex + '.selection', |
| 'important'); |
| } else if (allSelected.length == seriesGroup.tests.length) { |
| this.set( |
| 'seriesGroupList.' + seriesGroupIndex + '.selection', 'all'); |
| } else if (allUnselected.length == seriesGroup.tests.length) { |
| this.set( |
| 'seriesGroupList.' + seriesGroupIndex + '.selection', 'none'); |
| } else { |
| this.set( |
| 'seriesGroupList.' + seriesGroupIndex + '.selection', null); |
| } |
| }, |
| |
| /** |
| * Returns true if only important series are selected. |
| */ |
| importantSelected: function(tests) { |
| var hasImportant = false; |
| for (var i = 0; i < tests.length; i++) { |
| var test = tests[i]; |
| if (test.important) { |
| if (!test.selected) { |
| return false; |
| } |
| hasImportant = true; |
| } else if (test.selected) { |
| return false; |
| } |
| } |
| return hasImportant; |
| }, |
| |
| /** |
| * Updates numHidden properties for a seriesGroup. This is to show the |
| * number of hidden series link. |
| */ |
| updateNumHidden: function(seriesGroupIndex) { |
| var numHidden = 0; |
| var numCanHide = 0; |
| var seriesGroup = this.seriesGroupList[seriesGroupIndex]; |
| seriesGroup.tests.forEach(function(test) { |
| if (test.hidden) { |
| numHidden++; |
| } else if (!test.important && !test.checked) { |
| numCanHide++; |
| } |
| }); |
| |
| // Don't show more/less link if the only series shown are the important. |
| if (numHidden > 0) { |
| this.set( |
| 'seriesGroupList.' + seriesGroupIndex + '.numHidden', numHidden); |
| } else if (numCanHide > 0) { |
| this.set( |
| 'seriesGroupList.' + seriesGroupIndex + '.numHidden', 0); |
| } else { |
| this.set( |
| 'seriesGroupList.' + seriesGroupIndex + '.numHidden', null); |
| } |
| }, |
| |
| /** |
| * Event handler for the change event of check all checkboxes. |
| */ |
| onCheckAllCheckboxClicked: function(event, detail) { |
| var sender = event.currentTarget; |
| var groupIndex = event.model.groupIndex; |
| |
| this.set( |
| 'seriesGroupList.' + groupIndex + '.selection', ( |
| sender.checked ? 'all' : 'none')); |
| var tests = this.seriesGroupList[groupIndex].tests; |
| for (var i = 0; i < tests.length; i++) { |
| if (tests[i].index != undefined) { |
| this.set( |
| 'seriesGroupList.' + groupIndex + '.tests.' + i + '.selected', |
| sender.checked); |
| this.updateIndicesToGraph(tests[i].index, sender.checked); |
| } |
| } |
| |
| this.fireChartStateChangedEvent(); |
| }, |
| |
| /** |
| * Event handler for series group close button clicked. |
| */ |
| onCloseSeriesGroupClicked: function(event, detail) { |
| var model = event.model; |
| this.fire('seriesgroupclosed', {'groupIndex': model.groupIndex}); |
| }, |
| |
| /** |
| * Event handler for click event of expand link. |
| */ |
| onExpandSeriesClicked: function(event, detail) { |
| var groupIndex = this.$.grouplist.modelForElement( |
| event.target).groupIndex; |
| var isCollapse = event.currentTarget.text == 'less' ? true : false; |
| var seriesGroup = this.seriesGroupList[groupIndex]; |
| seriesGroup.tests.forEach(function(test, index) { |
| if (isCollapse) { |
| if (!test.selected && !test.important) { |
| this.set( |
| 'seriesGroupList.' + groupIndex + '.tests.' + index + |
| '.hidden', |
| true); |
| } |
| } else { |
| this.set( |
| 'seriesGroupList.' + groupIndex + '.tests.' + index + |
| '.hidden', |
| false); |
| } |
| }.bind(this)); |
| this.updateNumHidden(groupIndex); |
| }, |
| |
| /** |
| * On series group box drag-start, set data to be transferred to on |
| * drop event. |
| */ |
| onSeriesDragStart: function(event, detail) { |
| var groupIndex = this.$.grouplist.modelForElement( |
| event.target).groupIndex; |
| var testPath = this.seriesGroupList[groupIndex].path; |
| var selectedTests = []; |
| var tests = this.seriesGroupList[groupIndex].tests; |
| for (var i = 0; i < tests.length; i++) { |
| if (tests[i].selected) { |
| selectedTests.push(tests[i].name); |
| } |
| } |
| event.dataTransfer.setData('type', 'seriesdnd'); |
| // chart-container takes a list of test path and selected tests pair. |
| event.dataTransfer.setData( |
| 'data', JSON.stringify([[testPath, selectedTests]])); |
| event.dataTransfer.effectAllowed = 'copy'; |
| }, |
| |
| /** |
| * On series group box drag-end, checks if drop target is valid, |
| * and remove series group. |
| */ |
| onSeriesDragEnd: function(event, detail) { |
| // Successful drop. |
| if (event.dataTransfer.dropEffect == 'copy') { |
| var groupIndex = this.$.grouplist.modelForElement( |
| event.target).groupIndex; |
| // Let chart-container handle removing this group series. |
| this.fire('seriesgroupclosed', {'groupIndex': groupIndex}); |
| } |
| }, |
| |
| fireChartStateChangedEvent: function() { |
| this.fire('chartstatechanged', { |
| target: this, |
| stateName: 'chartstatechanged', |
| state: this.seriesGroupList |
| }); |
| }, |
| |
| /** |
| * Handler for the click event of the select all traces button. |
| * Updates this.indicesToGraph to contain all traces. |
| * @param {Event=} opt_noEvent The click event, not used. |
| */ |
| onSelectAll: function(opt_noEvent) { |
| this.set('indicesToGraph', []); |
| for (var i = 0; i < this.seriesGroupList.length; i++) { |
| var group = this.seriesGroupList[i]; |
| for (var j = 0; j < group.tests.length; j++) { |
| this.set('seriesGroupList.' + i + '.tests.' + j + '.selected', |
| true); |
| this.push('indicesToGraph', group.tests[j].index); |
| } |
| this.updateSeriesGroupCheckedState(i); |
| this.updateNumHidden(i); |
| } |
| this.fireChartStateChangedEvent(); |
| }, |
| |
| /** |
| * Handler for the click event of the deselect all traces button. |
| * @param {Event=} opt_noEvent The click event, not used. |
| */ |
| onDeselectAll: function(opt_noEvent) { |
| this.set('indicesToGraph', []); |
| for (var i = 0; i < this.seriesGroupList.length; i++) { |
| var group = this.seriesGroupList[i]; |
| for (var j = 0; j < group.tests.length; j++) { |
| this.set('seriesGroupList.' + i + '.tests.' + j + '.selected', |
| false); |
| } |
| this.updateSeriesGroupCheckedState(i); |
| this.updateNumHidden(i); |
| } |
| this.fireChartStateChangedEvent(); |
| }, |
| |
| /** |
| * Handler for the click event of the select core traces button. |
| * Selects only the core traces (i.e. important and ref traces). |
| * Note: The property 'coreTraces' is set in graph.js. |
| * @param {Event=} opt_event The click event, not used. |
| */ |
| onSelectCore: function(opt_noEvent) { |
| this.set('indicesToGraph', []); |
| for (var i = 0; i < this.seriesGroupList.length; i++) { |
| var group = this.seriesGroupList[i]; |
| for (var j = 0; j < group.tests.length; j++) { |
| var test = group.tests[j]; |
| this.set('seriesGroupList.' + i + '.tests.' + j + '.selected', |
| test.important); |
| test.selected = test.important; |
| this.updateIndicesToGraph(test.index, test.selected); |
| } |
| this.updateSeriesGroupCheckedState(i); |
| this.updateNumHidden(i); |
| } |
| this.fireChartStateChangedEvent(); |
| }, |
| |
| seriesMouseover: function(event, detail) { |
| this.fire('seriesmouseover', { |
| 'index': event.model.index |
| }); |
| }, |
| |
| seriesMouseout: function(event, detail) { |
| this.fire('seriesmouseout', { |
| 'index': event.model.index |
| }); |
| }, |
| |
| /** |
| * Adds or removes a series index from |this.indicesToGraph|. |
| * @param {number} index The index to add or remove. |
| * @param {boolean} selected Whether to add the index. |
| */ |
| updateIndicesToGraph: function(index, selected) { |
| if (selected) { |
| if (this.indicesToGraph.indexOf(index) == -1) { |
| this.push('indicesToGraph', index); |
| } |
| } else { |
| if (this.indicesToGraph.indexOf(index) != -1) { |
| this.splice('indicesToGraph', |
| this.indicesToGraph.indexOf(index), 1); |
| } |
| } |
| }, |
| |
| /** |
| * Toggles legend window to collapse or expand. |
| */ |
| toggleLegend: function() { |
| this.$['collapsible-legend'].toggle(); |
| this.collapseLegend = !this.collapseLegend; |
| } |
| }); |
| </script> |
| </dom-module> |