| <!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="/base/range.html"> |
| <link rel="import" href="/base/ui/d3.html"> |
| <link rel="import" href="/base/ui/chart_base.html"> |
| <link rel="import" href="/base/ui/mouse_tracker.html"> |
| <link rel="stylesheet" href="/base/ui/line_chart.css"> |
| <script> |
| 'use strict'; |
| |
| tr.exportTo('tr.b.ui', function() { |
| var ChartBase = tr.b.ui.ChartBase; |
| var getColorOfKey = tr.b.ui.getColorOfKey; |
| |
| function getSampleWidth(data, index, leftSide) { |
| var leftIndex, rightIndex; |
| if (leftSide) { |
| leftIndex = Math.max(index - 1, 0); |
| rightIndex = index; |
| } else { |
| leftIndex = index; |
| rightIndex = Math.min(index + 1, data.length - 1); |
| } |
| var leftWidth = data[index].x - data[leftIndex].x; |
| var rightWidth = data[rightIndex].x - data[index].x; |
| return leftWidth * 0.5 + rightWidth * 0.5; |
| } |
| |
| /** |
| * @constructor |
| */ |
| var LineChart = tr.b.ui.define('line-chart', ChartBase); |
| |
| LineChart.prototype = { |
| __proto__: ChartBase.prototype, |
| |
| decorate: function() { |
| ChartBase.prototype.decorate.call(this); |
| this.classList.add('line-chart'); |
| |
| this.brushedRange_ = new tr.b.Range(); |
| |
| this.xScale_ = d3.scale.linear(); |
| this.yScale_ = d3.scale.linear(); |
| d3.select(this.chartAreaElement) |
| .append('g') |
| .attr('id', 'brushes'); |
| d3.select(this.chartAreaElement) |
| .append('g') |
| .attr('id', 'series'); |
| |
| this.addEventListener('mousedown', this.onMouseDown_.bind(this)); |
| }, |
| |
| /** |
| * Sets the data array for the object |
| * |
| * @param {Array} data The data. Each element must be an object, with at |
| * least an x property. All other properties become series names in the |
| * chart. |
| */ |
| set data(data) { |
| if (data.length == 0) |
| throw new Error('Data must be nonzero. Pass undefined.'); |
| |
| var keys; |
| if (data !== undefined) { |
| var d = data[0]; |
| if (d.x === undefined) |
| throw new Error('Elements must have "x" fields'); |
| keys = d3.keys(data[0]); |
| keys.splice(keys.indexOf('x'), 1); |
| if (keys.length == 0) |
| throw new Error('Elements must have at least one other field than X'); |
| } else { |
| keys = undefined; |
| } |
| this.data_ = data; |
| this.seriesKeys_ = keys; |
| |
| this.updateContents_(); |
| }, |
| |
| // Note: range can only be set, not retrieved. It needs to be immutable |
| // or else odd data binding effects will result. |
| set brushedRange(range) { |
| this.brushedRange_.reset(); |
| this.brushedRange_.addRange(range); |
| this.updateContents_(); |
| }, |
| |
| computeBrushRangeFromIndices: function(indexA, indexB) { |
| var r = new tr.b.Range(); |
| var leftIndex = Math.min(indexA, indexB); |
| var rightIndex = Math.max(indexA, indexB); |
| leftIndex = Math.max(0, leftIndex); |
| rightIndex = Math.min(this.data_.length - 1, rightIndex); |
| r.addValue(this.data_[leftIndex].x - |
| getSampleWidth(this.data_, leftIndex, true)); |
| r.addValue(this.data_[rightIndex].x + |
| getSampleWidth(this.data_, rightIndex, false)); |
| return r; |
| }, |
| |
| getLegendKeys_: function() { |
| if (this.seriesKeys_ && |
| this.seriesKeys_.length > 1) |
| return this.seriesKeys_.slice(); |
| return []; |
| }, |
| |
| updateScales_: function(width, height) { |
| if (this.data_ === undefined) |
| return; |
| |
| // X. |
| this.xScale_.range([0, width]); |
| this.xScale_.domain(d3.extent(this.data_, function(d) { return d.x; })); |
| |
| // Y. |
| var yRange = new tr.b.Range(); |
| this.data_.forEach(function(d) { |
| this.seriesKeys_.forEach(function(k) { |
| yRange.addValue(d[k]); |
| }); |
| }, this); |
| |
| this.yScale_.range([height, 0]); |
| this.yScale_.domain([yRange.min, yRange.max]); |
| }, |
| |
| updateContents_: function() { |
| ChartBase.prototype.updateContents_.call(this); |
| if (!this.data_) |
| return; |
| |
| var chartAreaSel = d3.select(this.chartAreaElement); |
| |
| var brushes = this.brushedRange_.isEmpty ? [] : [this.brushedRange_]; |
| |
| var brushRectsSel = chartAreaSel.select('#brushes') |
| .selectAll('rect').data(brushes); |
| brushRectsSel.enter() |
| .append('rect'); |
| brushRectsSel.exit().remove(); |
| brushRectsSel |
| .attr('x', function(d) { |
| return this.xScale_(d.min); |
| }.bind(this)) |
| .attr('y', 0) |
| .attr('width', function(d) { |
| return this.xScale_(d.max) - this.xScale_(d.min); |
| }.bind(this)) |
| .attr('height', this.chartAreaSize.height); |
| |
| |
| var seriesSel = chartAreaSel.select('#series'); |
| var pathsSel = seriesSel.selectAll('path').data(this.seriesKeys_); |
| pathsSel.enter() |
| .append('path') |
| .attr('class', 'line') |
| .style('stroke', function(key) { |
| return getColorOfKey(key); |
| }) |
| .attr('d', function(key) { |
| var line = d3.svg.line() |
| .x(function(d) { return this.xScale_(d.x); }.bind(this)) |
| .y(function(d) { return this.yScale_(d[key]); }.bind(this)); |
| return line(this.data_); |
| }.bind(this)); |
| pathsSel.exit().remove(); |
| }, |
| |
| getDataIndexAtClientPoint_: function(clientX, clientY, clipToY) { |
| var rect = this.getBoundingClientRect(); |
| var margin = this.margin; |
| var chartAreaSize = this.chartAreaSize; |
| |
| var x = clientX - rect.left - margin.left; |
| var y = clientY - rect.top - margin.top; |
| |
| // Don't check width: let people select the left- and right-most data |
| // points. |
| if (clipToY) { |
| if (y < 0 || |
| y >= chartAreaSize.height) |
| return undefined; |
| } |
| |
| var dataX = this.xScale_.invert(x); |
| |
| var index; |
| if (this.data_) { |
| var bisect = d3.bisector(function(d) { return d.x; }).right; |
| index = bisect(this.data_, dataX) - 1; |
| } |
| |
| return index; |
| }, |
| |
| onMouseDown_: function(e) { |
| var index = this.getDataIndexAtClientPoint_(e.clientX, e.clientY, true); |
| |
| if (index !== undefined) { |
| tr.b.ui.trackMouseMovesUntilMouseUp( |
| this.onMouseMove_.bind(this, e.button), |
| this.onMouseUp_.bind(this, e.button)); |
| } |
| e.preventDefault(); |
| e.stopPropagation(); |
| |
| var event = new Event('item-mousedown'); |
| event.data = this.data_[index]; |
| event.index = index; |
| event.buttons = e.buttons; |
| this.dispatchEvent(event); |
| }, |
| |
| onMouseMove_: function(button, e) { |
| var index = this.getDataIndexAtClientPoint_(e.clientX, e.clientY, false); |
| if (e.buttons !== undefined) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| } |
| |
| var event = new Event('item-mousemove'); |
| event.data = this.data_[index]; |
| event.index = index; |
| event.button = button; |
| this.dispatchEvent(event); |
| }, |
| |
| onMouseUp_: function(button, e) { |
| var index = this.getDataIndexAtClientPoint_(e.clientX, e.clientY, false); |
| e.preventDefault(); |
| e.stopPropagation(); |
| |
| var event = new Event('item-mouseup'); |
| event.data = this.data_[index]; |
| event.index = index; |
| event.button = button; |
| this.dispatchEvent(event); |
| } |
| }; |
| |
| return { |
| LineChart: LineChart |
| }; |
| }); |
| </script> |