1<!DOCTYPE html>
2<!--
3Copyright (c) 2014 The Chromium Authors. All rights reserved.
4Use of this source code is governed by a BSD-style license that can be
5found in the LICENSE file.
6-->
7
8<link rel="import" href="/tracing/base/color_scheme.html">
9<link rel="import" href="/tracing/ui/base/d3.html">
10<link rel="import" href="/tracing/ui/base/ui.html">
11
12<style>
13  * /deep/ .chart-base #title {
14    font-size: 16pt;
15  }
16
17  * /deep/ .chart-base {
18    font-size: 12pt;
19    -webkit-user-select: none;
20    cursor: default;
21  }
22
23  * /deep/ .chart-base .axis path,
24  * /deep/ .chart-base .axis line {
25    fill: none;
26    shape-rendering: crispEdges;
27    stroke: #000;
28  }
29</style>
30
31<template id="chart-base-template">
32  <svg> <!-- svg tag is dropped by ChartBase.decorate. -->
33    <g xmlns="http://www.w3.org/2000/svg" id="chart-area">
34      <g class="x axis"></g>
35      <g class="y axis"></g>
36      <text id="title"></text>
37    </g>
38  </svg>
39</template>
40
41<script>
42'use strict';
43
44tr.exportTo('tr.ui.b', function() {
45  var THIS_DOC = document.currentScript.ownerDocument;
46
47  var svgNS = 'http://www.w3.org/2000/svg';
48  var ColorScheme = tr.b.ColorScheme;
49
50  function getColorOfKey(key, selected) {
51    var id = ColorScheme.getColorIdForGeneralPurposeString(key);
52    if (selected)
53      id += ColorScheme.properties.brightenedOffsets[0];
54    return ColorScheme.colorsAsStrings[id];
55  }
56
57  /**
58   * A virtual base class for basic charts that provides X and Y axes, if
59   * needed, a title, and legend.
60   *
61   * @constructor
62   */
63  var ChartBase = tr.ui.b.define('svg', undefined, svgNS);
64
65  ChartBase.prototype = {
66    __proto__: HTMLUnknownElement.prototype,
67
68    decorate: function() {
69      this.classList.add('chart-base');
70      this.chartTitle_ = undefined;
71      this.seriesKeys_ = undefined;
72      this.width_ = 400;
73      this.height_ = 300;
74
75      // This should use tr.ui.b.instantiateTemplate. However, creating
76      // svg-namespaced elements inside a template isn't possible. Thus, this
77      // hack.
78      var template = THIS_DOC.querySelector('#chart-base-template');
79      var svgEl = template.content.querySelector('svg');
80      for (var i = 0; i < svgEl.children.length; i++)
81        this.appendChild(svgEl.children[i].cloneNode(true));
82
83      // svg likes to take over width & height properties for some reason. This
84      // works around it.
85      Object.defineProperty(
86          this, 'width', {
87            get: function() {
88              return this.width_;
89            },
90            set: function(width) {
91              this.width_ = width;
92              this.updateContents_();
93            }
94          });
95      Object.defineProperty(
96          this, 'height', {
97            get: function() {
98              return this.height_;
99            },
100            set: function(height) {
101              this.height_ = height;
102              this.updateContents_();
103            }
104          });
105    },
106
107    get chartTitle() {
108      return this.chartTitle_;
109    },
110
111    set chartTitle(chartTitle) {
112      this.chartTitle_ = chartTitle;
113      this.updateContents_();
114    },
115
116    get chartAreaElement() {
117      return this.querySelector('#chart-area');
118    },
119
120    setSize: function(size) {
121      this.width_ = size.width;
122      this.height_ = size.height;
123      this.updateContents_();
124    },
125
126    getMargin_: function() {
127      var margin = {top: 20, right: 20, bottom: 30, left: 50};
128      if (this.chartTitle_)
129        margin.top += 20;
130      return margin;
131    },
132
133    get margin() {
134      return this.getMargin_();
135    },
136
137    get chartAreaSize() {
138      var margin = this.margin;
139      return {
140        width: this.width_ - margin.left - margin.right,
141        height: this.height_ - margin.top - margin.bottom
142      };
143    },
144
145    getLegendKeys_: function() {
146      throw new Error('Not implemented');
147    },
148
149    updateScales_: function() {
150      throw new Error('Not implemented');
151    },
152
153    updateContents_: function() {
154      var margin = this.margin;
155
156      var thisSel = d3.select(this);
157      thisSel.attr('width', this.width_);
158      thisSel.attr('height', this.height_);
159
160      var chartAreaSel = d3.select(this.chartAreaElement);
161      chartAreaSel.attr('transform',
162          'translate(' + margin.left + ',' + margin.top + ')');
163
164      this.updateScales_();
165      this.updateTitle_(chartAreaSel);
166      this.updateLegend_();
167    },
168
169    updateTitle_: function(chartAreaSel) {
170      var titleSel = chartAreaSel.select('#title');
171      if (!this.chartTitle_) {
172        titleSel.style('display', 'none');
173        return;
174      }
175      var width = this.chartAreaSize.width;
176      titleSel.attr('transform', 'translate(' + width * 0.5 + ',-5)')
177          .style('display', undefined)
178          .style('text-anchor', 'middle')
179          .attr('class', 'title')
180          .attr('width', width)
181          .text(this.chartTitle_);
182    },
183
184    // TODO(charliea): We should change updateLegend_ so that it ellipsizes the
185    // series names after a certain point. Otherwise, the series names start
186    // dipping below the x-axis and continue on outside of the viewport.
187    updateLegend_: function() {
188      var keys = this.getLegendKeys_();
189      if (keys === undefined)
190        return;
191
192      var chartAreaSel = d3.select(this.chartAreaElement);
193      var chartAreaSize = this.chartAreaSize;
194
195      var legendEntriesSel = chartAreaSel.selectAll('.legend')
196          .data(keys.slice().reverse());
197
198      legendEntriesSel.enter()
199          .append('g')
200          .attr('class', 'legend')
201          .attr('transform', function(d, i) {
202              return 'translate(0,' + i * 20 + ')';
203            })
204          .append('text').text(function(key) {
205              return key;
206            });
207      legendEntriesSel.exit().remove();
208
209      legendEntriesSel.attr('x', chartAreaSize.width - 18)
210          .attr('width', 18)
211          .attr('height', 18)
212          .style('fill', function(key) {
213            var selected = this.currentHighlightedLegendKey === key;
214            return getColorOfKey(key, selected);
215          }.bind(this));
216
217      legendEntriesSel.selectAll('text')
218        .attr('x', chartAreaSize.width - 24)
219        .attr('y', 9)
220        .attr('dy', '.35em')
221        .style('text-anchor', 'end')
222        .text(function(d) { return d; });
223    },
224
225    get highlightedLegendKey() {
226      return this.highlightedLegendKey_;
227    },
228
229    set highlightedLegendKey(highlightedLegendKey) {
230      this.highlightedLegendKey_ = highlightedLegendKey;
231      this.updateHighlight_();
232    },
233
234    get currentHighlightedLegendKey() {
235      if (this.tempHighlightedLegendKey_)
236        return this.tempHighlightedLegendKey_;
237      return this.highlightedLegendKey_;
238    },
239
240    pushTempHighlightedLegendKey: function(key) {
241      if (this.tempHighlightedLegendKey_)
242        throw new Error('push cannot nest');
243      this.tempHighlightedLegendKey_ = key;
244      this.updateHighlight_();
245    },
246
247    popTempHighlightedLegendKey: function(key) {
248      if (this.tempHighlightedLegendKey_ != key)
249        throw new Error('pop cannot happen');
250      this.tempHighlightedLegendKey_ = undefined;
251      this.updateHighlight_();
252    },
253
254    updateHighlight_: function() {
255      // Update label colors.
256      var chartAreaSel = d3.select(this.chartAreaElement);
257      var legendEntriesSel = chartAreaSel.selectAll('.legend');
258
259      var that = this;
260      legendEntriesSel.each(function(key) {
261        var highlighted = key == that.currentHighlightedLegendKey;
262        var color = getColorOfKey(key, highlighted);
263        this.style.fill = color;
264        if (highlighted)
265          this.style.fontWeight = 'bold';
266        else
267          this.style.fontWeight = '';
268      });
269    }
270  };
271
272  return {
273    getColorOfKey: getColorOfKey,
274    ChartBase: ChartBase
275  };
276});
277</script>
278