blob: ec2f63b2adc682d266dc00c763f5138d0152f801 [file] [log] [blame]
<!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>