3 Copyright (c) 2014 The Chromium Authors. All rights reserved.
4 Use of this source code is governed by a BSD-style license that can be
5 found in the LICENSE file.
7 <link rel="import" href="/base/range.html">
8 <link rel="import" href="/base/ui/d3.html">
9 <link rel="import" href="/base/ui/chart_base.html">
10 <link rel="import" href="/base/ui/mouse_tracker.html">
11 <link rel="stylesheet" href="/base/ui/line_chart.css">
15 tv.exportTo('tv.ui', function() {
16 var ChartBase = tv.ui.ChartBase;
17 var getColorOfKey = tv.ui.getColorOfKey;
19 function getSampleWidth(data, index, leftSide) {
20 var leftIndex, rightIndex;
22 leftIndex = Math.max(index - 1, 0);
26 rightIndex = Math.min(index + 1, data.length - 1);
28 var leftWidth = data[index].x - data[leftIndex].x;
29 var rightWidth = data[rightIndex].x - data[index].x;
30 return leftWidth * 0.5 + rightWidth * 0.5;
36 var LineChart = tv.ui.define('line-chart', ChartBase);
38 LineChart.prototype = {
39 __proto__: ChartBase.prototype,
41 decorate: function() {
42 ChartBase.prototype.decorate.call(this);
43 this.classList.add('line-chart');
45 this.brushedRange_ = new tv.Range();
47 this.xScale_ = d3.scale.linear();
48 this.yScale_ = d3.scale.linear();
49 d3.select(this.chartAreaElement)
51 .attr('id', 'brushes');
52 d3.select(this.chartAreaElement)
54 .attr('id', 'series');
56 this.addEventListener('mousedown', this.onMouseDown_.bind(this));
60 * Sets the data array for the object
62 * @param {Array} data The data. Each element must be an object, with at
63 * least an x property. All other properties become series names in the
68 throw new Error('Data must be nonzero. Pass undefined.');
71 if (data !== undefined) {
73 if (d.x === undefined)
74 throw new Error('Elements must have "x" fields');
75 keys = d3.keys(data[0]);
76 keys.splice(keys.indexOf('x'), 1);
78 throw new Error('Elements must have at least one other field than X');
83 this.seriesKeys_ = keys;
85 this.updateContents_();
88 // Note: range can only be set, not retrieved. It needs to be immutable
89 // or else odd data binding effects will result.
90 set brushedRange(range) {
91 this.brushedRange_.reset();
92 this.brushedRange_.addRange(range);
93 this.updateContents_();
96 computeBrushRangeFromIndices: function(indexA, indexB) {
97 var r = new tv.Range();
98 var leftIndex = Math.min(indexA, indexB);
99 var rightIndex = Math.max(indexA, indexB);
100 leftIndex = Math.max(0, leftIndex);
101 rightIndex = Math.min(this.data_.length - 1, rightIndex);
102 r.addValue(this.data_[leftIndex].x -
103 getSampleWidth(this.data_, leftIndex, true));
104 r.addValue(this.data_[rightIndex].x +
105 getSampleWidth(this.data_, rightIndex, false));
109 getLegendKeys_: function() {
110 if (this.seriesKeys_ &&
111 this.seriesKeys_.length > 1)
112 return this.seriesKeys_.slice();
116 updateScales_: function(width, height) {
117 if (this.data_ === undefined)
121 this.xScale_.range([0, width]);
122 this.xScale_.domain(d3.extent(this.data_, function(d) { return d.x; }));
125 var yRange = new tv.Range();
126 this.data_.forEach(function(d) {
127 this.seriesKeys_.forEach(function(k) {
128 yRange.addValue(d[k]);
132 this.yScale_.range([height, 0]);
133 this.yScale_.domain([yRange.min, yRange.max]);
136 updateContents_: function() {
137 ChartBase.prototype.updateContents_.call(this);
141 var chartAreaSel = d3.select(this.chartAreaElement);
143 var brushes = this.brushedRange_.isEmpty ? [] : [this.brushedRange_];
145 var brushRectsSel = chartAreaSel.select('#brushes')
146 .selectAll('rect').data(brushes);
147 brushRectsSel.enter()
149 brushRectsSel.exit().remove();
151 .attr('x', function(d) {
152 return this.xScale_(d.min);
155 .attr('width', function(d) {
156 return this.xScale_(d.max) - this.xScale_(d.min);
158 .attr('height', this.chartAreaSize.height);
161 var seriesSel = chartAreaSel.select('#series');
162 var pathsSel = seriesSel.selectAll('path').data(this.seriesKeys_);
165 .attr('class', 'line')
166 .style('stroke', function(key) {
167 return getColorOfKey(key);
169 .attr('d', function(key) {
170 var line = d3.svg.line()
171 .x(function(d) { return this.xScale_(d.x); }.bind(this))
172 .y(function(d) { return this.yScale_(d[key]); }.bind(this));
173 return line(this.data_);
175 pathsSel.exit().remove();
178 getDataIndexAtClientPoint_: function(clientX, clientY, clipToY) {
179 var rect = this.getBoundingClientRect();
180 var margin = this.margin;
181 var chartAreaSize = this.chartAreaSize;
183 var x = clientX - rect.left - margin.left;
184 var y = clientY - rect.top - margin.top;
186 // Don't check width: let people select the left- and right-most data
190 y >= chartAreaSize.height)
194 var dataX = this.xScale_.invert(x);
198 var bisect = d3.bisector(function(d) { return d.x; }).right;
199 index = bisect(this.data_, dataX) - 1;
205 onMouseDown_: function(e) {
206 var index = this.getDataIndexAtClientPoint_(e.clientX, e.clientY, true);
208 if (index !== undefined) {
209 tv.ui.trackMouseMovesUntilMouseUp(
210 this.onMouseMove_.bind(this, e.button),
211 this.onMouseUp_.bind(this, e.button));
216 var event = new Event('item-mousedown');
217 event.data = this.data_[index];
219 event.buttons = e.buttons;
220 this.dispatchEvent(event);
223 onMouseMove_: function(button, e) {
224 var index = this.getDataIndexAtClientPoint_(e.clientX, e.clientY, false);
225 if (e.buttons !== undefined) {
230 var event = new Event('item-mousemove');
231 event.data = this.data_[index];
233 event.button = button;
234 this.dispatchEvent(event);
237 onMouseUp_: function(button, e) {
238 var index = this.getDataIndexAtClientPoint_(e.clientX, e.clientY, false);
242 var event = new Event('item-mouseup');
243 event.data = this.data_[index];
245 event.button = button;
246 this.dispatchEvent(event);