blob: f68ca8e7a6251c91fd4b8f2d35512047cf2f6161 [file] [log] [blame]
<!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="/ui/base/d3.html">
<link rel="import" href="/ui/base/chart_base.html">
<link rel="import" href="/ui/base/mouse_tracker.html">
<link rel="stylesheet" href="/ui/base/line_chart.css">
<script>
'use strict';
tr.exportTo('tr.ui.b', function() {
var ChartBase = tr.ui.b.ChartBase;
var getColorOfKey = tr.ui.b.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.ui.b.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. The data can be sparse (i.e. every x value does not have to
* contain data for every series).
*/
set data(data) {
if (data.length == 0)
throw new Error('Data must be nonzero. Pass undefined.');
this.data_ = data;
this.seriesKeys_ = this.getSeriesKeys_();
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 [];
},
getSeriesKeys_: function() {
if (this.data_ === undefined)
return undefined;
// Accumulate the keys on each data point.
var keySet = {};
this.data_.forEach(function(datum) {
if (datum.x === undefined)
throw new Error('Elements must have "x" fields.');
Object.keys(datum).forEach(function(key) {
// Don't count 'x': it's the domain, not an actual series.
if (key === 'x')
return;
keySet[key] = true;
});
});
return Object.keys(keySet);
},
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(datum) {
this.seriesKeys_.forEach(function(key) {
// Allow for sparse data
if (datum[key] !== undefined)
yRange.addValue(datum[key]);
});
}, 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 dataBySeriesKey = this.getDataBySeriesKey_();
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(dataBySeriesKey[key]);
}.bind(this));
pathsSel.exit().remove();
},
/**
* Returns a map of series key to the data for that series.
*
* Example:
* // returns {y: [{x: 1, y: 1}, {x: 3, y: 3}], z: [{x: 2, z: 2}]}
* this.data_ = [{x: 1, y: 1}, {x: 2, z: 2}, {x: 3, y: 3}];
* this.getDataBySeriesKey_();
* @return {Object} A map of series data by series key.
*/
getDataBySeriesKey_: function() {
var dataBySeriesKey = {};
this.seriesKeys_.forEach(function(seriesKey) {
dataBySeriesKey[seriesKey] = [];
});
this.data_.forEach(function(multiSeriesDatum) {
var x = multiSeriesDatum.x;
d3.keys(multiSeriesDatum).forEach(function(seriesKey) {
// Skip 'x' - it's not a series
if (seriesKey === 'x')
return;
if (multiSeriesDatum[seriesKey] === undefined)
return;
var singleSeriesDatum = {x: x};
singleSeriesDatum[seriesKey] = multiSeriesDatum[seriesKey];
dataBySeriesKey[seriesKey].push(singleSeriesDatum);
});
});
return dataBySeriesKey;
},
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.ui.b.trackMouseMovesUntilMouseUp(
this.onMouseMove_.bind(this, e.button),
this.onMouseUp_.bind(this, e.button));
}
e.preventDefault();
e.stopPropagation();
var event = new tr.b.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 tr.b.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 tr.b.Event('item-mouseup');
event.data = this.data_[index];
event.index = index;
event.button = button;
this.dispatchEvent(event);
}
};
return {
LineChart: LineChart
};
});
</script>