| <!DOCTYPE html> |
| <!-- |
| Copyright (c) 2014 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/color_scheme.html"> |
| <link rel="import" href="/tracing/ui/analysis/analysis_link.html"> |
| <link rel="import" href="/tracing/ui/base/d3.html"> |
| <link rel="import" href="/tracing/ui/base/ui.html"> |
| |
| <dom-module id="tr-ui-b-chart-legend-key"> |
| <template> |
| <style> |
| #checkbox { |
| margin: 0; |
| visibility: hidden; |
| vertical-align: text-top; |
| } |
| #label, #link { |
| white-space: nowrap; |
| text-overflow: ellipsis; |
| overflow: hidden; |
| display: inline-block; |
| } |
| </style> |
| |
| <input type=checkbox id="checkbox" checked> |
| <tr-ui-a-analysis-link id="link"></tr-ui-a-analysis-link> |
| <label id="label"></label> |
| </template> |
| </dom-module> |
| |
| <script> |
| 'use strict'; |
| Polymer({ |
| is: 'tr-ui-b-chart-legend-key', |
| |
| ready: function() { |
| this.$.checkbox.addEventListener( |
| 'change', this.onCheckboxChange_.bind(this)); |
| }, |
| |
| /** |
| * Dispatch an event when the checkbox is toggled. |
| * The checkbox is visible when optional is set to true. |
| */ |
| onCheckboxChange_: function() { |
| tr.b.dispatchSimpleEvent(this, tr.ui.b.DataSeriesEnableChangeEventType, |
| true, false, |
| {key: Polymer.dom(this).textContent, enabled: this.enabled}); |
| }, |
| |
| set textContent(t) { |
| Polymer.dom(this.$.label).textContent = t; |
| Polymer.dom(this.$.link).textContent = t; |
| this.updateContents_(); |
| }, |
| |
| set width(w) { |
| w -= 20; // reserve 20px for the checkbox |
| this.$.link.style.width = w + 'px'; |
| this.$.label.style.width = w + 'px'; |
| }, |
| |
| get textContent() { |
| return Polymer.dom(this.$.label).textContent; |
| }, |
| |
| /** |
| * When a legend-key is "optional", then its checkbox is visible to allow |
| * the user to enable/disable the data series for the key. |
| * |
| * @param {boolean} optional |
| */ |
| set optional(optional) { |
| this.$.checkbox.style.visibility = optional ? 'visible' : 'hidden'; |
| }, |
| |
| get optional() { |
| return this.$.checkbox.style.visibility === 'visible'; |
| }, |
| |
| set enabled(enabled) { |
| this.$.checkbox.checked = enabled ? 'checked' : ''; |
| }, |
| |
| get enabled() { |
| return this.$.checkbox.checked; |
| }, |
| |
| set color(c) { |
| this.$.label.style.color = c; |
| this.$.link.color = c; |
| }, |
| |
| /** |
| * When target is defined, label is hidden and link is shown. |
| * When the link is clicked, then a RequestSelectionChangeEvent is |
| * dispatched containing the target. |
| * When target is undefined, label is shown and link is hidden, so that the |
| * link is not clickable. |
| */ |
| set target(target) { |
| this.$.link.setSelectionAndContent( |
| target, Polymer.dom(this.$.label).textContent); |
| this.updateContents_(); |
| }, |
| |
| get target() { |
| return this.$.link.selection; |
| }, |
| |
| updateContents_: function() { |
| this.$.link.style.display = this.target ? '' : 'none'; |
| this.$.label.style.display = this.target ? 'none' : ''; |
| this.$.label.htmlFor = this.optional ? 'checkbox' : ''; |
| } |
| }); |
| </script> |
| |
| <style> |
| * /deep/ .chart-base #title { |
| font-size: 16pt; |
| } |
| |
| * /deep/ .chart-base { |
| -webkit-user-select: none; |
| cursor: default; |
| } |
| |
| * /deep/ .chart-base .axis path, |
| * /deep/ .chart-base .axis line { |
| fill: none; |
| shape-rendering: crispEdges; |
| stroke: #000; |
| } |
| |
| * /deep/ .chart-base .legend body { |
| margin: 0; |
| } |
| </style> |
| |
| <template id="chart-base-template"> |
| <svg> <!-- svg tag is dropped by ChartBase.decorate. --> |
| <g xmlns="http://www.w3.org/2000/svg" id="chart-area"> |
| <g class="x axis"></g> |
| <g class="y axis"></g> |
| <text id="title"></text> |
| </g> |
| </svg> |
| </template> |
| |
| <script> |
| 'use strict'; |
| |
| tr.exportTo('tr.ui.b', function() { |
| var DataSeriesEnableChangeEventType = 'data-series-enabled-change'; |
| |
| var THIS_DOC = document.currentScript.ownerDocument; |
| |
| var svgNS = 'http://www.w3.org/2000/svg'; |
| var ColorScheme = tr.b.ColorScheme; |
| |
| function getColorOfKey(key, selected) { |
| var id = ColorScheme.getColorIdForGeneralPurposeString(key); |
| if (selected) |
| id += ColorScheme.properties.brightenedOffsets[0]; |
| return ColorScheme.colorsAsStrings[id]; |
| } |
| |
| function DataSeries(key) { |
| this.key_ = key; |
| this.target_ = undefined; |
| this.optional_ = false; |
| this.enabled_ = true; |
| this.color_ = getColorOfKey(key, false); |
| this.highlightedColor_ = getColorOfKey(key, true); |
| } |
| |
| DataSeries.prototype = { |
| get key() { |
| return this.key_; |
| }, |
| |
| get color() { |
| return this.color_; |
| }, |
| |
| set color(c) { |
| this.color_ = c; |
| }, |
| |
| get highlightedColor() { |
| return this.highlightedColor_; |
| }, |
| |
| set highlightedColor(c) { |
| this.highlightedColor_ = c; |
| }, |
| |
| get optional() { |
| return this.optional_; |
| }, |
| |
| set optional(optional) { |
| this.optional_ = optional; |
| }, |
| |
| get enabled() { |
| return this.enabled_; |
| }, |
| |
| set enabled(enabled) { |
| // If the caller is disabling a data series, but it wasn't optional, then |
| // force it to be optional. |
| if (!this.optional && !enabled) |
| this.optional = true; |
| this.enabled_ = enabled; |
| }, |
| |
| get target() { |
| return this.target_; |
| }, |
| |
| set target(t) { |
| this.target_ = t; |
| } |
| }; |
| |
| /** |
| * A virtual base class for basic charts that provides X and Y axes, if |
| * needed, a title, and legend. |
| * |
| * @constructor |
| */ |
| var ChartBase = tr.ui.b.define('svg', undefined, svgNS); |
| |
| ChartBase.prototype = { |
| __proto__: HTMLUnknownElement.prototype, |
| |
| getDataSeries: function(key) { |
| if (!this.seriesByKey_.has(key)) |
| this.seriesByKey_.set(key, new DataSeries(key)); |
| return this.seriesByKey_.get(key); |
| }, |
| |
| decorate: function() { |
| Polymer.dom(this).classList.add('chart-base'); |
| this.chartTitle_ = undefined; |
| this.seriesByKey_ = new Map(); |
| this.width_ = 400; |
| this.height_ = 300; |
| this.margin = {top: 20, right: 72, bottom: 30, left: 50}; |
| this.hideLegend_ = false; |
| |
| // This should use tr.ui.b.instantiateTemplate. However, creating |
| // svg-namespaced elements inside a template isn't possible. Thus, this |
| // hack. |
| var template = |
| Polymer.dom(THIS_DOC).querySelector('#chart-base-template'); |
| var svgEl = Polymer.dom(template.content).querySelector('svg'); |
| for (var i = 0; i < Polymer.dom(svgEl).children.length; i++) |
| Polymer.dom(this).appendChild( |
| Polymer.dom(svgEl.children[i]).cloneNode(true)); |
| |
| // svg likes to take over width & height properties for some reason. This |
| // works around it. |
| Object.defineProperty( |
| this, 'width', { |
| get: function() { |
| return this.width_; |
| }, |
| set: function(width) { |
| this.width_ = width; |
| this.updateContents_(); |
| } |
| }); |
| Object.defineProperty( |
| this, 'height', { |
| get: function() { |
| return this.height_; |
| }, |
| set: function(height) { |
| this.height_ = height; |
| this.updateContents_(); |
| } |
| }); |
| this.addEventListener(DataSeriesEnableChangeEventType, |
| this.onDataSeriesEnableChange_.bind(this)); |
| }, |
| |
| get hideLegend() { |
| return this.hideLegend_; |
| }, |
| |
| set hideLegend(h) { |
| this.hideLegend_ = h; |
| this.updateContents_(); |
| }, |
| |
| isSeriesEnabled: function(key) { |
| return this.getDataSeries(key).enabled; |
| }, |
| |
| onDataSeriesEnableChange_: function(event) { |
| this.getDataSeries(event.key).enabled = event.enabled; |
| this.updateContents_(); |
| }, |
| |
| get chartTitle() { |
| return this.chartTitle_; |
| }, |
| |
| set chartTitle(chartTitle) { |
| if (chartTitle && !this.chartTitle_) |
| this.margin.top += this.titleMarginPx; |
| else if (this.chartTitle_ && !chartTitle) |
| this.margin.top -= this.titleMarginPx; |
| |
| this.chartTitle_ = chartTitle; |
| this.updateContents_(); |
| }, |
| |
| get titleMarginPx() { |
| return 20; |
| }, |
| |
| get chartAreaElement() { |
| return Polymer.dom(this).querySelector('#chart-area'); |
| }, |
| |
| setSize: function(size) { |
| this.width_ = size.width; |
| this.height_ = size.height; |
| this.updateContents_(); |
| }, |
| |
| get chartAreaSize() { |
| return { |
| width: this.width_ - this.margin.left - this.margin.right, |
| height: this.height_ - this.margin.top - this.margin.bottom |
| }; |
| }, |
| |
| updateScales_: function() { |
| throw new Error('Not implemented'); |
| }, |
| |
| updateContents_: function() { |
| var thisSel = d3.select(this); |
| thisSel.attr('width', this.width_); |
| thisSel.attr('height', this.height_); |
| |
| var chartAreaSel = d3.select(this.chartAreaElement); |
| chartAreaSel.attr('transform', |
| 'translate(' + this.margin.left + ',' + this.margin.top + ')'); |
| |
| this.updateScales_(); |
| this.updateTitle_(chartAreaSel); |
| this.updateLegend_(); |
| }, |
| |
| updateTitle_: function(chartAreaSel) { |
| var titleSel = chartAreaSel.select('#title'); |
| if (!this.chartTitle_) { |
| titleSel.style('display', 'none'); |
| return; |
| } |
| var width = this.chartAreaSize.width; |
| titleSel.attr('transform', 'translate(' + width * 0.5 + ',-5)') |
| .style('display', undefined) |
| .style('text-anchor', 'middle') |
| .attr('class', 'title') |
| .attr('width', width) |
| .text(this.chartTitle_); |
| }, |
| |
| updateLegend_: function() { |
| var chartAreaSel = d3.select(this.chartAreaElement); |
| chartAreaSel.selectAll('.legend').remove(); |
| if (this.hideLegend) |
| return; |
| |
| var series = [...this.seriesByKey_.values()].reverse(); |
| var legendEntriesSel = chartAreaSel.selectAll('.legend').data(series); |
| |
| var width = this.margin.right - 2; |
| legendEntriesSel.enter() |
| .append('foreignObject') |
| .attr('class', 'legend') |
| .attr('x', this.chartAreaSize.width + 2) |
| .attr('width', width) |
| .attr('height', 18) |
| .attr('transform', function(series, i) { |
| return 'translate(0,' + i * 18 + ')'; |
| }) |
| .append('xhtml:body') |
| .append('tr-ui-b-chart-legend-key') |
| .property('color', function(series) { |
| if (this.currentHighlightedLegendKey === series.key) |
| return series.highlightedColor; |
| return series.color; |
| }.bind(this)) |
| .property('width', width) |
| .property('target', function(series) { return series.target; }) |
| .property('optional', function(series) { return series.optional; }) |
| .property('enabled', function(series) { return series.enabled; }) |
| .text(function(series) { return series.key; }); |
| legendEntriesSel.exit().remove(); |
| }, |
| |
| get highlightedLegendKey() { |
| return this.highlightedLegendKey_; |
| }, |
| |
| set highlightedLegendKey(highlightedLegendKey) { |
| this.highlightedLegendKey_ = highlightedLegendKey; |
| this.updateHighlight_(); |
| }, |
| |
| get currentHighlightedLegendKey() { |
| if (this.tempHighlightedLegendKey_) |
| return this.tempHighlightedLegendKey_; |
| return this.highlightedLegendKey_; |
| }, |
| |
| pushTempHighlightedLegendKey: function(key) { |
| if (this.tempHighlightedLegendKey_) |
| throw new Error('push cannot nest'); |
| this.tempHighlightedLegendKey_ = key; |
| this.updateHighlight_(); |
| }, |
| |
| popTempHighlightedLegendKey: function(key) { |
| if (this.tempHighlightedLegendKey_ != key) |
| throw new Error('pop cannot happen'); |
| this.tempHighlightedLegendKey_ = undefined; |
| this.updateHighlight_(); |
| }, |
| |
| updateHighlight_: function() { |
| // Update label colors. |
| var chartAreaSel = d3.select(this.chartAreaElement); |
| var legendEntriesSel = chartAreaSel.selectAll('.legend'); |
| |
| var that = this; |
| legendEntriesSel.each(function(key) { |
| var dataSeries = that.getDataSeries(key); |
| if (key === that.currentHighlightedLegendKey) { |
| this.style.fill = dataSeries.highlightedColor; |
| this.style.fontWeight = 'bold'; |
| } else { |
| this.style.fill = dataSeries.color; |
| this.style.fontWeight = ''; |
| } |
| }); |
| } |
| }; |
| |
| return { |
| DataSeriesEnableChangeEventType: DataSeriesEnableChangeEventType, |
| getColorOfKey: getColorOfKey, |
| ChartBase: ChartBase |
| }; |
| }); |
| </script> |