chart_base_2d.html revision 8d2b206a675ec20ea07100c35df34e65ee1e45e8
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/range.html">
9<link rel="import" href="/tracing/ui/base/chart_base.html">
10<link rel="import" href="/tracing/ui/base/mouse_tracker.html">
11
12<style>
13  * /deep/ .chart-base-2d.updating-brushing-state #brushes > * {
14    fill: rgb(103, 199, 165)
15  }
16
17  * /deep/ .chart-base-2d #brushes {
18    fill: rgb(213, 236, 229)
19  }
20</style>
21
22<script>
23'use strict';
24
25tr.exportTo('tr.ui.b', function() {
26  var ChartBase = tr.ui.b.ChartBase;
27  var ChartBase2D = tr.ui.b.define('chart-base-2d', ChartBase);
28
29  ChartBase2D.prototype = {
30    __proto__: ChartBase.prototype,
31
32    decorate: function() {
33      ChartBase.prototype.decorate.call(this);
34      this.classList.add('chart-base-2d');
35      this.xScale_ = d3.scale.linear();
36      this.yScale_ = d3.scale.linear();
37
38      this.data_ = [];
39      this.seriesKeys_ = [];
40      this.leftMargin_ = 50;
41
42      d3.select(this.chartAreaElement)
43          .append('g')
44          .attr('id', 'brushes');
45      d3.select(this.chartAreaElement)
46          .append('g')
47          .attr('id', 'series');
48
49      this.addEventListener('mousedown', this.onMouseDown_.bind(this));
50    },
51
52    get data() {
53      return this.data_;
54    },
55
56    /**
57     * Sets the data array for the object
58     *
59     * @param {Array} data The data. Each element must be an object, with at
60     * least an x property. All other properties become series names in the
61     * chart. The data can be sparse (i.e. every x value does not have to
62     * contain data for every series).
63     */
64    set data(data) {
65      if (data === undefined)
66        throw new Error('data must be an Array');
67
68      this.data_ = data;
69      this.updateSeriesKeys_();
70      this.updateContents_();
71    },
72
73    getSampleWidth_: function(data, index, leftSide) {
74      var leftIndex, rightIndex;
75      if (leftSide) {
76        leftIndex = Math.max(index - 1, 0);
77        rightIndex = index;
78      } else {
79        leftIndex = index;
80        rightIndex = Math.min(index + 1, data.length - 1);
81      }
82      var leftWidth = this.getXForDatum_(data[index], index) -
83        this.getXForDatum_(data[leftIndex], leftIndex);
84      var rightWidth = this.getXForDatum_(data[rightIndex], rightIndex) -
85        this.getXForDatum_(data[index], index);
86      return leftWidth * 0.5 + rightWidth * 0.5;
87    },
88
89    getLegendKeys_: function() {
90      if (this.seriesKeys_ &&
91          this.seriesKeys_.length > 1)
92        return this.seriesKeys_.slice();
93      return [];
94    },
95
96    updateSeriesKeys_: function() {
97      // Accumulate the keys on each data point.
98      var keySet = {};
99      this.data_.forEach(function(datum) {
100        Object.keys(datum).forEach(function(key) {
101          if (this.isDatumFieldSeries_(key))
102            keySet[key] = true;
103        }, this);
104      }, this);
105      this.seriesKeys_ = Object.keys(keySet);
106    },
107
108    isDatumFieldSeries_: function(fieldName) {
109      throw new Error('Not implemented');
110    },
111
112    getXForDatum_: function(datum, index) {
113      throw new Error('Not implemented');
114    },
115
116    updateScales_: function() {
117      if (this.data_.length === 0)
118        return;
119
120      var width = this.chartAreaSize.width;
121      var height = this.chartAreaSize.height;
122
123      // X.
124      this.xScale_.range([0, width]);
125      this.xScale_.domain(d3.extent(this.data_, this.getXForDatum_.bind(this)));
126
127      // Y.
128      var yRange = new tr.b.Range();
129      this.data_.forEach(function(datum) {
130        this.seriesKeys_.forEach(function(key) {
131          // Allow for sparse data
132          if (datum[key] !== undefined)
133            yRange.addValue(datum[key]);
134        });
135      }, this);
136
137      this.yScale_.range([height, 0]);
138      this.yScale_.domain([yRange.min, yRange.max]);
139    },
140
141    updateBrushContents_: function(brushSel) {
142      brushSel.selectAll('*').remove();
143    },
144
145    updateXAxis_: function(xAxis) {
146      xAxis.selectAll('*').remove();
147      xAxis[0][0].style.opacity = 0;
148      xAxis.attr('transform', 'translate(0,' + this.chartAreaSize.height + ')')
149        .call(d3.svg.axis()
150              .scale(this.xScale_)
151              .orient('bottom'));
152      window.requestAnimationFrame(function() {
153        var previousRight = undefined;
154        xAxis.selectAll('.tick')[0].forEach(function(tick) {
155          var currentLeft = tick.transform.baseVal[0].matrix.e;
156          if ((previousRight === undefined) ||
157              (currentLeft > (previousRight + 3))) {
158            var currentWidth = tick.getBBox().width;
159            previousRight = currentLeft + currentWidth;
160          } else {
161            tick.style.opacity = 0;
162          }
163        });
164        xAxis[0][0].style.opacity = 1;
165      });
166    },
167
168    getMargin_: function() {
169      var margin = ChartBase.prototype.getMargin_.call(this);
170      margin.left = this.leftMargin_;
171      return margin;
172    },
173
174    updateYAxis_: function(yAxis) {
175      yAxis.selectAll('*').remove();
176      yAxis[0][0].style.opacity = 0;
177      yAxis.call(d3.svg.axis()
178        .scale(this.yScale_)
179        .orient('left'));
180      window.requestAnimationFrame(function() {
181        var previousTop = undefined;
182        var leftMargin = 0;
183        yAxis.selectAll('.tick')[0].forEach(function(tick) {
184          var bbox = tick.getBBox();
185          leftMargin = Math.max(leftMargin, bbox.width);
186          var currentTop = tick.transform.baseVal[0].matrix.f;
187          var currentBottom = currentTop + bbox.height;
188          if ((previousTop === undefined) ||
189              (previousTop > (currentBottom + 3))) {
190            previousTop = currentTop;
191          } else {
192            tick.style.opacity = 0;
193          }
194        });
195        if (leftMargin > this.leftMargin_) {
196          this.leftMargin_ = leftMargin;
197          this.updateContents_();
198        } else {
199          yAxis[0][0].style.opacity = 1;
200        }
201      }.bind(this));
202    },
203
204    updateContents_: function() {
205      ChartBase.prototype.updateContents_.call(this);
206      var chartAreaSel = d3.select(this.chartAreaElement);
207      this.updateXAxis_(chartAreaSel.select('.x.axis'));
208      this.updateYAxis_(chartAreaSel.select('.y.axis'));
209      this.updateBrushContents_(chartAreaSel.select('#brushes'));
210      this.updateDataContents_(chartAreaSel.select('#series'));
211    },
212
213    updateDataContents_: function(seriesSel) {
214      throw new Error('Not implemented');
215    },
216
217    /**
218     * Returns a map of series key to the data for that series.
219     *
220     * Example:
221     * // returns {y: [{x: 1, y: 1}, {x: 3, y: 3}], z: [{x: 2, z: 2}]}
222     * this.data_ = [{x: 1, y: 1}, {x: 2, z: 2}, {x: 3, y: 3}];
223     * this.getDataBySeriesKey_();
224     * @return {Object} A map of series data by series key.
225     */
226    getDataBySeriesKey_: function() {
227      var dataBySeriesKey = {};
228      this.seriesKeys_.forEach(function(seriesKey) {
229        dataBySeriesKey[seriesKey] = [];
230      });
231
232      this.data_.forEach(function(multiSeriesDatum, index) {
233        var x = this.getXForDatum_(multiSeriesDatum, index);
234
235        d3.keys(multiSeriesDatum).forEach(function(seriesKey) {
236          // Skip 'x' - it's not a series
237          if (seriesKey === 'x')
238            return;
239
240          if (multiSeriesDatum[seriesKey] === undefined)
241            return;
242
243          var singleSeriesDatum = {x: x};
244          singleSeriesDatum[seriesKey] = multiSeriesDatum[seriesKey];
245          dataBySeriesKey[seriesKey].push(singleSeriesDatum);
246        });
247      }, this);
248
249      return dataBySeriesKey;
250    },
251
252    getDataPointAtClientPoint_: function(clientX, clientY) {
253      var rect = this.getBoundingClientRect();
254      var margin = this.margin;
255      var x = clientX - rect.left - margin.left;
256      var y = clientY - rect.top - margin.top;
257      x = this.xScale_.invert(x);
258      y = this.yScale_.invert(y);
259      x = tr.b.clamp(x, this.xScale_.domain()[0], this.xScale_.domain()[1]);
260      y = tr.b.clamp(y, this.yScale_.domain()[0], this.yScale_.domain()[1]);
261      return {x: x, y: y};
262    },
263
264    prepareDataEvent_: function(mouseEvent, dataEvent) {
265      var dataPoint = this.getDataPointAtClientPoint_(
266          mouseEvent.clientX, mouseEvent.clientY);
267      dataEvent.x = dataPoint.x;
268      dataEvent.y = dataPoint.y;
269    },
270
271    onMouseDown_: function(mouseEvent) {
272      tr.ui.b.trackMouseMovesUntilMouseUp(
273          this.onMouseMove_.bind(this, mouseEvent.button),
274          this.onMouseUp_.bind(this, mouseEvent.button));
275      mouseEvent.preventDefault();
276      mouseEvent.stopPropagation();
277      var dataEvent = new tr.b.Event('item-mousedown');
278      dataEvent.button = mouseEvent.button;
279      this.classList.add('updating-brushing-state');
280      this.prepareDataEvent_(mouseEvent, dataEvent);
281      this.dispatchEvent(dataEvent);
282    },
283
284    onMouseMove_: function(button, mouseEvent) {
285      if (mouseEvent.buttons !== undefined) {
286        mouseEvent.preventDefault();
287        mouseEvent.stopPropagation();
288      }
289      var dataEvent = new tr.b.Event('item-mousemove');
290      dataEvent.button = button;
291      this.prepareDataEvent_(mouseEvent, dataEvent);
292      this.dispatchEvent(dataEvent);
293    },
294
295    onMouseUp_: function(button, mouseEvent) {
296      mouseEvent.preventDefault();
297      mouseEvent.stopPropagation();
298      var dataEvent = new tr.b.Event('item-mouseup');
299      dataEvent.button = button;
300      this.prepareDataEvent_(mouseEvent, dataEvent);
301      this.dispatchEvent(dataEvent);
302      this.classList.remove('updating-brushing-state');
303    }
304  };
305
306  return {
307    ChartBase2D: ChartBase2D
308  };
309});
310</script>
311