Index: tracing/tracing/ui/base/chart_base.html |
diff --git a/tracing/tracing/ui/base/chart_base.html b/tracing/tracing/ui/base/chart_base.html |
index 8c8c9d60d53c907b8792fccceec1c2a386c84b27..e793d83841c148ede38380d1d10d4e142adc4540 100644 |
--- a/tracing/tracing/ui/base/chart_base.html |
+++ b/tracing/tracing/ui/base/chart_base.html |
@@ -1,4 +1,4 @@ |
-<!DOCTYPE html> |
+ <!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 |
@@ -6,16 +6,127 @@ 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. |
+ * See ChartBase.customizeOptionalSeries(). |
+ * |
+ * @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. |
+ * See ChartBase.customizeLegendTargets(). |
+ */ |
+ 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 { |
- font-size: 12pt; |
-webkit-user-select: none; |
cursor: default; |
} |
@@ -26,6 +137,10 @@ found in the LICENSE file. |
shape-rendering: crispEdges; |
stroke: #000; |
} |
+ |
+ * /deep/ .chart-base .legend body { |
+ margin: 0; |
+ } |
</style> |
<template id="chart-base-template"> |
@@ -42,6 +157,8 @@ found in the LICENSE file. |
'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'; |
@@ -54,6 +171,47 @@ tr.exportTo('tr.ui.b', function() { |
return ColorScheme.colorsAsStrings[id]; |
} |
+ function DataSeries(key) { |
+ this.key_ = key; |
+ this.target_ = undefined; |
+ this.optional_ = false; |
+ this.enabled_ = true; |
+ } |
+ |
+ DataSeries.prototype = { |
+ get key() { |
+ return this.key_; |
+ }, |
+ |
+ 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. |
@@ -63,23 +221,32 @@ tr.exportTo('tr.ui.b', function() { |
var ChartBase = tr.ui.b.define('svg', undefined, svgNS); |
ChartBase.prototype = { |
- __proto__: HTMLDivElement.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.seriesKeys_ = 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 template = |
+ Polymer.dom(THIS_DOC).querySelector('#chart-base-template'); |
var svgEl = Polymer.dom(template.content).querySelector('svg'); |
- for (var i = 0; i < svgEl.children.length; i++) |
- Polymer.dom(this).appendChild(svgEl.children[i].cloneNode(true)); |
+ 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. |
@@ -103,6 +270,26 @@ tr.exportTo('tr.ui.b', function() { |
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() { |
@@ -110,10 +297,19 @@ tr.exportTo('tr.ui.b', function() { |
}, |
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'); |
}, |
@@ -124,27 +320,64 @@ tr.exportTo('tr.ui.b', function() { |
this.updateContents_(); |
}, |
- getMargin_: function() { |
- var margin = {top: 20, right: 20, bottom: 30, left: 50}; |
- if (this.chartTitle_) |
- margin.top += 20; |
- return margin; |
+ get chartAreaSize() { |
+ return { |
+ width: this.width_ - this.margin.left - this.margin.right, |
+ height: this.height_ - this.margin.top - this.margin.bottom |
+ }; |
}, |
- get margin() { |
- return this.getMargin_(); |
+ /** |
+ * Legend keys can be clickable links instead of plain text. |
+ * When a legend key link is clicked, a RequestSelectionChangeEvent is |
+ * dispatched containing arbitrary data. ChartBase calls that arbitrary data |
+ * the "target" of the legend key link. |
+ * In order to turn the legend key for the 'foo' data series into a |
+ * clickable link, call customizeLegendTargets({foo: target}). When the user |
+ * clicks on the legend key link for 'foo', then a |
+ * RequestSelectionChangeEvent will be dispatched, and its |selection| field |
+ * will be the |target| value for the 'foo' key in |delta|. |
+ * |
+ * @param {!Object} delta |
+ */ |
+ customizeLegendTargets: function(delta) { |
+ tr.b.iterItems(delta, function(key, value) { |
+ this.getDataSeries(key).target = value; |
+ }, this); |
}, |
- get chartAreaSize() { |
- var margin = this.margin; |
- return { |
- width: this.width_ - margin.left - margin.right, |
- height: this.height_ - margin.top - margin.bottom |
- }; |
+ /** |
+ * Optional data series can be enabled and disabled using checkboxes. |
+ * In order to allow the user to enable/disabled the 'foo' data series, |
+ * call customizeOptionalSeries({foo: true}). This will show a checkbox |
+ * next to the 'foo' legend key. When the user toggles the checkbox, then a |
+ * DataSeriesEnableChangeEvent will be dispatched with its |key| = 'foo'. |
+ * ChartBase listens for that event and updates |isSeriesEnabled('foo')| to |
+ * reflect the state of that checkbox, and calls updateContents_(). |
+ * Subclasses are responsible for implementing updateContents_() in order to |
+ * hiding disabled data series -- see BarChart. |
+ * |
+ * @param {!Object} delta |
+ */ |
+ customizeOptionalSeries: function(delta) { |
+ tr.b.iterItems(delta, function(key, value) { |
+ this.getDataSeries(key).optional = value; |
+ }, this); |
}, |
- getLegendKeys_: function() { |
- throw new Error('Not implemented'); |
+ /** |
+ * Data series can be enabled and disabled. |
+ * See customizeOptionalSeries() in order to allow the user to |
+ * enable/disable data series manually. Callers may call |
+ * customizeEnabledSeries({foo: false}) in order to automatically |
+ * disable the 'foo' data series, for example. |
+ * |
+ * @param {!Object} delta |
+ */ |
+ customizeEnabledSeries: function(delta) { |
+ tr.b.iterItems(delta, function(key, value) { |
+ this.getDataSeries(key).enabled = value; |
+ }, this); |
}, |
updateScales_: function() { |
@@ -152,15 +385,13 @@ tr.exportTo('tr.ui.b', function() { |
}, |
updateContents_: function() { |
- var margin = this.margin; |
- |
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(' + margin.left + ',' + margin.top + ')'); |
+ 'translate(' + this.margin.left + ',' + this.margin.top + ')'); |
this.updateScales_(); |
this.updateTitle_(chartAreaSel); |
@@ -182,45 +413,37 @@ tr.exportTo('tr.ui.b', function() { |
.text(this.chartTitle_); |
}, |
- // TODO(charliea): We should change updateLegend_ so that it ellipsizes the |
- // series names after a certain point. Otherwise, the series names start |
- // dipping below the x-axis and continue on outside of the viewport. |
updateLegend_: function() { |
- var keys = this.getLegendKeys_(); |
- if (keys === undefined) |
- return; |
- |
var chartAreaSel = d3.select(this.chartAreaElement); |
- var chartAreaSize = this.chartAreaSize; |
+ chartAreaSel.selectAll('.legend').remove(); |
+ if (this.hideLegend) |
+ return; |
- var legendEntriesSel = chartAreaSel.selectAll('.legend') |
- .data(keys.slice().reverse()); |
+ var series = [...this.seriesByKey_.values()].reverse(); |
+ var legendEntriesSel = chartAreaSel.selectAll('.legend').data(series); |
+ var width = this.margin.right - 2; |
legendEntriesSel.enter() |
- .append('g') |
+ .append('foreignObject') |
.attr('class', 'legend') |
- .attr('transform', function(d, i) { |
- return 'translate(0,' + i * 20 + ')'; |
+ .attr('x', this.chartAreaSize.width + 2) |
+ .attr('width', width) |
+ .attr('height', 18) |
+ .attr('transform', function(series, i) { |
+ return 'translate(0,' + i * 18 + ')'; |
}) |
- .append('text').text(function(key) { |
- return key; |
- }); |
+ .append('xhtml:body') |
+ .append('tr-ui-b-chart-legend-key') |
+ .property('color', function(series) { |
+ var selected = this.currentHighlightedLegendKey === series.key; |
+ return getColorOfKey(series.key, selected); |
+ }.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(); |
- |
- legendEntriesSel.attr('x', chartAreaSize.width - 18) |
- .attr('width', 18) |
- .attr('height', 18) |
- .style('fill', function(key) { |
- var selected = this.currentHighlightedLegendKey === key; |
- return getColorOfKey(key, selected); |
- }.bind(this)); |
- |
- legendEntriesSel.selectAll('text') |
- .attr('x', chartAreaSize.width - 24) |
- .attr('y', 9) |
- .attr('dy', '.35em') |
- .style('text-anchor', 'end') |
- .text(function(d) { return d; }); |
}, |
get highlightedLegendKey() { |
@@ -271,6 +494,7 @@ tr.exportTo('tr.ui.b', function() { |
}; |
return { |
+ DataSeriesEnableChangeEventType: DataSeriesEnableChangeEventType, |
getColorOfKey: getColorOfKey, |
ChartBase: ChartBase |
}; |