| <!DOCTYPE html> |
| <!-- |
| Copyright (c) 2015 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="/tracing/base/math/statistics.html"> |
| <link rel="import" href="/tracing/ui/base/box_chart.html"> |
| <link rel="import" href="/tracing/ui/base/drag_handle.html"> |
| <link rel="import" href="/tracing/ui/base/name_bar_chart.html"> |
| <link rel="import" href="/tracing/value/ui/diagnostic_map_table.html"> |
| <link rel="import" href="/tracing/value/ui/diagnostic_span.html"> |
| <link rel="import" href="/tracing/value/ui/scalar_map_table.html"> |
| |
| <dom-module id='tr-v-ui-histogram-span'> |
| <template> |
| <style> |
| #container { |
| display: flex; |
| flex-direction: row; |
| justify-content: space-between; |
| } |
| #chart { |
| flex-grow: 1; |
| display: none; |
| } |
| #drag_handle, #sample_diagnostics_container { |
| display: none; |
| } |
| #chart svg { |
| display: block; |
| } |
| </style> |
| |
| <div id="container"> |
| <div id="chart"></div> |
| <div id="stats_container"> |
| <tr-v-ui-scalar-map-table id="stats"></tr-v-ui-scalar-map-table> |
| </div> |
| </div> |
| <tr-ui-b-drag-handle id="drag_handle"></tr-ui-b-drag-handle> |
| |
| <tr-v-ui-diagnostic-map-table id="histogram_diagnostics"></tr-v-ui-diagnostic-map-table> |
| |
| <div id="sample_diagnostics_container"> |
| <div id="merge_sample_diagnostics_container"> |
| <input type="checkbox" id="merge_sample_diagnostics" checked on-change="updateDiagnostics_"> |
| <label for="merge_sample_diagnostics">Merge Sample Diagnostics</label> |
| </div> |
| <tr-v-ui-diagnostic-map-table id="sample_diagnostics"></tr-v-ui-diagnostic-map-table> |
| </div> |
| </template> |
| </dom-module> |
| |
| <script> |
| 'use strict'; |
| tr.exportTo('tr.v.ui', function() { |
| const DEFAULT_BAR_HEIGHT_PX = 5; |
| const TRUNCATE_BIN_MARGIN = 0.15; |
| const IGNORE_DELTA_STATISTICS_NAMES = [ |
| `${tr.v.DELTA}min`, |
| `%${tr.v.DELTA}min`, |
| `${tr.v.DELTA}max`, |
| `%${tr.v.DELTA}max`, |
| `${tr.v.DELTA}sum`, |
| `%${tr.v.DELTA}sum`, |
| `${tr.v.DELTA}count`, |
| `%${tr.v.DELTA}count`, |
| ]; |
| |
| Polymer({ |
| is: 'tr-v-ui-histogram-span', |
| |
| created() { |
| this.histogram_ = undefined; |
| this.referenceHistogram_ = undefined; |
| this.graphWidth_ = undefined; |
| this.graphHeight_ = undefined; |
| this.mouseDownBin_ = undefined; |
| this.brushedBins_ = []; |
| this.prevBrushedBins_ = []; |
| this.canMergeSampleDiagnostics_ = true; |
| }, |
| |
| ready() { |
| this.$.drag_handle.target = this.$.container; |
| this.$.drag_handle.addEventListener( |
| 'drag-handle-resize', this.onResize_.bind(this)); |
| }, |
| |
| attached() { |
| if (this.histogram_ !== undefined) this.updateContents_(); |
| }, |
| |
| get canMergeSampleDiagnostics() { |
| return this.canMergeSampleDiagnostics_; |
| }, |
| |
| set canMergeSampleDiagnostics(merge) { |
| this.canMergeSampleDiagnostics_ = merge; |
| this.$.merge_sample_diagnostics_container.style.display = ( |
| merge ? '' : 'none'); |
| }, |
| |
| onResize_(event) { |
| event.stopPropagation(); |
| let heightPx = parseInt(this.$.container.style.height); |
| if (heightPx < this.defaultGraphHeight) { |
| heightPx = this.defaultGraphHeight; |
| this.$.container.style.height = this.defaultGraphHeight + 'px'; |
| } |
| this.chart_.graphHeight = heightPx - (this.chart_.margin.top + |
| this.chart_.margin.bottom); |
| }, |
| |
| /** |
| * Get the width in pixels of the widest bar in the bar chart, not the total |
| * bar chart svg tag, which includes margins containing axes and legend. |
| * |
| * @return {number} |
| */ |
| get graphWidth() { |
| return this.graphWidth_ || this.defaultGraphWidth; |
| }, |
| |
| /** |
| * Set the width in pixels of the widest bar in the bar chart, not the total |
| * bar chart svg tag, which includes margins containing axes and legend. |
| * |
| * @param {number} width |
| */ |
| set graphWidth(width) { |
| this.graphWidth_ = width; |
| }, |
| |
| /** |
| * Get the height in pixels of the bars in the bar chart, not the total |
| * bar chart svg tag, which includes margins containing axes and legend. |
| * |
| * @return {number} |
| */ |
| get graphHeight() { |
| return this.graphHeight_ || this.defaultGraphHeight; |
| }, |
| |
| /** |
| * Set the height in pixels of the bars in the bar chart, not the total |
| * bar chart svg tag, which includes margins containing axes and legend. |
| * |
| * @param {number} height |
| */ |
| set graphHeight(height) { |
| this.graphHeight_ = height; |
| }, |
| |
| /** |
| * Get the height in pixels of one bar in the bar chart. |
| * |
| * @return {number} |
| */ |
| get barHeight() { |
| return this.chart_.barHeight; |
| }, |
| |
| /** |
| * Set the height in pixels of one bar in the bar chart. |
| * |
| * @param {number} px |
| */ |
| set barHeight(px) { |
| this.graphHeight = this.computeChartHeight_(px); |
| }, |
| |
| computeChartHeight_(barHeightPx) { |
| return (this.chart_.margin.top + |
| this.chart_.margin.bottom + |
| (barHeightPx * this.histogram.allBins.length)); |
| }, |
| |
| get defaultGraphHeight() { |
| if (this.histogram && this.histogram.allBins.length === 1) { |
| return 150; |
| } |
| return this.computeChartHeight_(DEFAULT_BAR_HEIGHT_PX); |
| }, |
| |
| get defaultGraphWidth() { |
| if (this.histogram.allBins.length === 1) { |
| return 100; |
| } |
| return 300; |
| }, |
| |
| get brushedBins() { |
| return this.brushedBins_; |
| }, |
| |
| set brushedBins(bins) { |
| this.brushedBins_ = bins; |
| let brushedBinIndices = new tr.b.math.Range(); |
| for (let bin of bins) { |
| brushedBinIndices.addValue(this.histogram.allBins.indexOf(bin)); |
| } |
| this.chart_.brushedRange = brushedBinIndices; |
| this.updateDiagnostics_(); |
| }, |
| |
| updateBrushedRange_(binIndex) { |
| let brushedBinIndices = new tr.b.math.Range(); |
| brushedBinIndices.addValue(tr.b.math.clamp( |
| this.mouseDownBinIndex_, 0, this.histogram.allBins.length - 1)); |
| brushedBinIndices.addValue(tr.b.math.clamp( |
| binIndex, 0, this.histogram.allBins.length - 1)); |
| brushedBinIndices.max += 1; |
| this.chart_.brushedRange = brushedBinIndices; |
| |
| this.brushedBins_ = []; |
| for (let i = brushedBinIndices.min; i < brushedBinIndices.max; ++i) { |
| this.brushedBins.push(this.histogram.allBins[i]); |
| } |
| }, |
| |
| onMouseDown_(chartEvent) { |
| chartEvent.stopPropagation(); |
| if (!this.histogram) { |
| return; |
| } |
| this.prevBrushedBins_ = this.brushedBins_; |
| this.mouseDownBinIndex_ = chartEvent.y; |
| this.updateBrushedRange_(chartEvent.y); |
| }, |
| |
| onMouseMove_(chartEvent) { |
| chartEvent.stopPropagation(); |
| if (!this.histogram) { |
| return; |
| } |
| this.updateBrushedRange_(chartEvent.y); |
| }, |
| |
| onMouseUp_(chartEvent) { |
| chartEvent.stopPropagation(); |
| if (!this.histogram) { |
| return; |
| } |
| this.updateBrushedRange_(chartEvent.y); |
| if (this.prevBrushedBins_.length === 1 && |
| this.brushedBins_.length === 1 && |
| this.prevBrushedBins_[0] === this.brushedBins_[0]) { |
| this.brushedBins_ = []; |
| this.chart_.brushedRange = new tr.b.math.Range(); |
| } |
| this.updateDiagnostics_(); |
| this.mouseDownBinIndex_ = undefined; |
| }, |
| |
| updateDiagnostics_() { |
| let maps = []; |
| for (let bin of this.brushedBins) { |
| for (let map of bin.diagnosticMaps) { |
| maps.push(map); |
| } |
| } |
| |
| if (maps.length === 0) { |
| this.$.sample_diagnostics_container.style.display = 'none'; |
| return; |
| } |
| |
| if (this.canMergeSampleDiagnostics && |
| this.$.merge_sample_diagnostics.checked) { |
| let merged = new tr.v.d.DiagnosticMap(); |
| for (let map of maps) { |
| merged.addDiagnostics(map); |
| } |
| maps = [merged]; |
| } |
| |
| this.$.sample_diagnostics_container.style.display = 'block'; |
| this.$.sample_diagnostics.diagnosticMaps = maps; |
| }, |
| |
| get histogram() { |
| return this.histogram_; |
| }, |
| |
| set histogram(histogram) { |
| if (histogram === this.histogram_) return; |
| this.histogram_ = histogram; |
| if (this.isAttached) this.updateContents_(); |
| }, |
| |
| get referenceHistogram() { |
| return this.referenceHistogram_; |
| }, |
| |
| set referenceHistogram(histogram) { |
| if (histogram === this.referenceHistogram_) { |
| return; |
| } |
| this.referenceHistogram_ = histogram; |
| if (this.histogram) this.updateContents_(); |
| }, |
| |
| getDeltaScalars_(statNames, scalarMap) { |
| if (!this.histogram.canCompare(this.referenceHistogram)) return; |
| |
| let mwu = tr.b.math.Statistics.mwu( |
| this.histogram.sampleValues, this.referenceHistogram.sampleValues); |
| |
| for (let deltaStatName of tr.v.Histogram.getDeltaStatisticsNames( |
| statNames)) { |
| if (IGNORE_DELTA_STATISTICS_NAMES.includes(deltaStatName)) continue; |
| let scalar = this.histogram.getStatisticScalar( |
| deltaStatName, this.referenceHistogram, mwu); |
| if (scalar === undefined) continue; |
| scalarMap.set(deltaStatName, scalar); |
| } |
| |
| if (this.histogram.unit.improvementDirection !== |
| tr.b.ImprovementDirection.DONT_CARE) { |
| this.$.stats.setSignificanceForKey( |
| `${tr.v.DELTA}avg`, mwu.significance); |
| } |
| }, |
| |
| set isYLogScale(logScale) { |
| this.chart_.isYLogScale = logScale; |
| }, |
| |
| updateContents_() { |
| this.$.chart.style.display = 'none'; |
| this.$.drag_handle.style.display = 'none'; |
| this.$.sample_diagnostics_container.style.display = 'none'; |
| this.$.container.style.justifyContent = ''; |
| this.brushedBins_ = []; |
| |
| while (Polymer.dom(this.$.chart).lastChild) { |
| Polymer.dom(this.$.chart).removeChild( |
| Polymer.dom(this.$.chart).lastChild); |
| } |
| |
| if (!this.histogram) return; |
| this.$.container.style.display = ''; |
| |
| let scalarMap = new Map(); |
| this.getDeltaScalars_(this.histogram.statisticsNames, scalarMap); |
| for (let [name, scalar] of this.histogram.statisticsScalars) { |
| scalarMap.set(name, scalar); |
| } |
| this.$.stats.scalarMap = scalarMap; |
| |
| if (this.histogram.diagnostics.size > 0) { |
| let diagnosticMap = new tr.v.d.DiagnosticMap(); |
| for (let [key, diagnostic] of this.histogram.diagnostics) { |
| // Hide the 'merged from' and 'merged to' diagnostics, which are |
| // implementation details. |
| if (key !== tr.v.d.MERGED_FROM_DIAGNOSTIC_KEY && |
| key !== tr.v.d.MERGED_TO_DIAGNOSTIC_KEY) { |
| diagnosticMap.set(key, diagnostic); |
| } |
| } |
| this.$.histogram_diagnostics.diagnosticMaps = [diagnosticMap]; |
| this.$.histogram_diagnostics.style.display = 'block'; |
| } else { |
| this.$.histogram_diagnostics.style.display = 'none'; |
| } |
| |
| if (this.histogram.numValues <= 1) { |
| this.brushedBins_ = this.histogram.allBins; |
| this.updateDiagnostics_(); |
| this.$.container.style.justifyContent = 'flex-end'; |
| return; |
| } |
| |
| this.$.chart.style.display = 'block'; |
| this.$.drag_handle.style.display = 'block'; |
| |
| if (this.histogram.allBins.length === 1) { |
| if (this.histogram.min !== this.histogram.max) { |
| this.chart_ = new tr.ui.b.BoxChart(); |
| Polymer.dom(this.$.chart).appendChild(this.chart_); |
| this.chart_.graphWidth = this.graphWidth; |
| this.chart_.graphHeight = this.graphHeight; |
| this.chart_.hideXAxis = true; |
| this.chart_.data = [ |
| { |
| x: '', |
| color: 'blue', |
| percentile_0: this.histogram.running.min, |
| percentile_25: this.histogram.getApproximatePercentile(0.25), |
| percentile_50: this.histogram.getApproximatePercentile(0.5), |
| percentile_75: this.histogram.getApproximatePercentile(0.75), |
| percentile_100: this.histogram.running.max, |
| } |
| ]; |
| } |
| this.brushedBins_ = this.histogram.allBins; |
| this.updateDiagnostics_(); |
| return; |
| } |
| |
| this.chart_ = new tr.ui.b.NameBarChart(); |
| Polymer.dom(this.$.chart).appendChild(this.chart_); |
| this.chart_.graphWidth = this.graphWidth; |
| this.chart_.graphHeight = this.graphHeight; |
| this.chart_.addEventListener('item-mousedown', |
| this.onMouseDown_.bind(this)); |
| this.chart_.addEventListener('item-mousemove', |
| this.onMouseMove_.bind(this)); |
| this.chart_.addEventListener('item-mouseup', |
| this.onMouseUp_.bind(this)); |
| this.chart_.hideLegend = true; |
| this.chart_.getDataSeries('y').color = 'blue'; |
| this.chart_.xAxisLabel = '#'; |
| this.chart_.brushedRange = new tr.b.math.Range(); |
| |
| let chartData = []; |
| let binCounts = []; |
| for (let bin of this.histogram.allBins) { |
| let x = bin.range.min; |
| if (x === -Number.MAX_VALUE) { |
| x = '<' + new tr.b.Scalar( |
| this.histogram.unit, bin.range.max).toString(); |
| } else { |
| x = new tr.b.Scalar(this.histogram.unit, x).toString(); |
| } |
| chartData.push({x: x, y: bin.count}); |
| binCounts.push(bin.count); |
| } |
| |
| // If the largest 1 or 2 bins are more than twice as large as the next |
| // largest bin, then set the dataRange max to TRUNCATE_BIN_MARGIN% more |
| // than that next largest bin. |
| binCounts.sort((x, y) => y - x); |
| let dataRange = tr.b.math.Range.fromExplicitRange(0, binCounts[0]); |
| if (binCounts[1] > 0 && binCounts[0] > (binCounts[1] * 2)) { |
| dataRange.max = binCounts[1] * (1 + TRUNCATE_BIN_MARGIN); |
| } |
| if (binCounts[2] > 0 && binCounts[1] > (binCounts[2] * 2)) { |
| dataRange.max = binCounts[2] * (1 + TRUNCATE_BIN_MARGIN); |
| } |
| this.chart_.overrideDataRange = dataRange; |
| |
| this.chart_.data = chartData; |
| } |
| }); |
| }); |
| </script> |