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