blob: 7dd9d1cd7cc7886c075ea52b0ce3e700d353567e [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="/tracing/base/range.html">
<link rel="import" href="/tracing/ui/base/chart_base.html">
<link rel="import" href="/tracing/ui/base/mouse_tracker.html">
<style>
* /deep/ .chart-base-2d.updating-brushing-state #brushes > * {
fill: rgb(103, 199, 165)
}
* /deep/ .chart-base-2d #brushes {
fill: rgb(213, 236, 229)
}
</style>
<script>
'use strict';
tr.exportTo('tr.ui.b', function() {
var ChartBase = tr.ui.b.ChartBase;
var ChartBase2D = tr.ui.b.define('chart-base-2d', ChartBase);
ChartBase2D.prototype = {
__proto__: ChartBase.prototype,
decorate: function() {
ChartBase.prototype.decorate.call(this);
this.classList.add('chart-base-2d');
this.xScale_ = d3.scale.linear();
this.yScale_ = d3.scale.linear();
this.data_ = [];
this.seriesKeys_ = [];
this.leftMargin_ = 50;
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));
},
get data() {
return this.data_;
},
/**
* 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 === undefined)
throw new Error('data must be an Array');
this.data_ = data;
this.updateSeriesKeys_();
this.updateContents_();
},
getSampleWidth_: function(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 = this.getXForDatum_(data[index], index) -
this.getXForDatum_(data[leftIndex], leftIndex);
var rightWidth = this.getXForDatum_(data[rightIndex], rightIndex) -
this.getXForDatum_(data[index], index);
return leftWidth * 0.5 + rightWidth * 0.5;
},
getLegendKeys_: function() {
if (this.seriesKeys_ &&
this.seriesKeys_.length > 1)
return this.seriesKeys_.slice();
return [];
},
updateSeriesKeys_: function() {
// Accumulate the keys on each data point.
var keySet = {};
this.data_.forEach(function(datum) {
Object.keys(datum).forEach(function(key) {
if (this.isDatumFieldSeries_(key))
keySet[key] = true;
}, this);
}, this);
this.seriesKeys_ = Object.keys(keySet);
},
isDatumFieldSeries_: function(fieldName) {
throw new Error('Not implemented');
},
getXForDatum_: function(datum, index) {
throw new Error('Not implemented');
},
updateScales_: function() {
if (this.data_.length === 0)
return;
var width = this.chartAreaSize.width;
var height = this.chartAreaSize.height;
// X.
this.xScale_.range([0, width]);
this.xScale_.domain(d3.extent(this.data_, this.getXForDatum_.bind(this)));
// 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]);
},
updateBrushContents_: function(brushSel) {
brushSel.selectAll('*').remove();
},
updateXAxis_: function(xAxis) {
xAxis.selectAll('*').remove();
xAxis[0][0].style.opacity = 0;
xAxis.attr('transform', 'translate(0,' + this.chartAreaSize.height + ')')
.call(d3.svg.axis()
.scale(this.xScale_)
.orient('bottom'));
window.requestAnimationFrame(function() {
var previousRight = undefined;
xAxis.selectAll('.tick')[0].forEach(function(tick) {
var currentLeft = tick.transform.baseVal[0].matrix.e;
if ((previousRight === undefined) ||
(currentLeft > (previousRight + 3))) {
var currentWidth = tick.getBBox().width;
previousRight = currentLeft + currentWidth;
} else {
tick.style.opacity = 0;
}
});
xAxis[0][0].style.opacity = 1;
});
},
getMargin_: function() {
var margin = ChartBase.prototype.getMargin_.call(this);
margin.left = this.leftMargin_;
return margin;
},
updateYAxis_: function(yAxis) {
yAxis.selectAll('*').remove();
yAxis[0][0].style.opacity = 0;
yAxis.call(d3.svg.axis()
.scale(this.yScale_)
.orient('left'));
window.requestAnimationFrame(function() {
var previousTop = undefined;
var leftMargin = 0;
yAxis.selectAll('.tick')[0].forEach(function(tick) {
var bbox = tick.getBBox();
leftMargin = Math.max(leftMargin, bbox.width);
var currentTop = tick.transform.baseVal[0].matrix.f;
var currentBottom = currentTop + bbox.height;
if ((previousTop === undefined) ||
(previousTop > (currentBottom + 3))) {
previousTop = currentTop;
} else {
tick.style.opacity = 0;
}
});
if (leftMargin > this.leftMargin_) {
this.leftMargin_ = leftMargin;
this.updateContents_();
} else {
yAxis[0][0].style.opacity = 1;
}
}.bind(this));
},
updateContents_: function() {
ChartBase.prototype.updateContents_.call(this);
var chartAreaSel = d3.select(this.chartAreaElement);
this.updateXAxis_(chartAreaSel.select('.x.axis'));
this.updateYAxis_(chartAreaSel.select('.y.axis'));
this.updateBrushContents_(chartAreaSel.select('#brushes'));
this.updateDataContents_(chartAreaSel.select('#series'));
},
updateDataContents_: function(seriesSel) {
throw new Error('Not implemented');
},
/**
* 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, index) {
var x = this.getXForDatum_(multiSeriesDatum, index);
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);
});
}, this);
return dataBySeriesKey;
},
getDataPointAtClientPoint_: function(clientX, clientY) {
var rect = this.getBoundingClientRect();
var margin = this.margin;
var x = clientX - rect.left - margin.left;
var y = clientY - rect.top - margin.top;
x = this.xScale_.invert(x);
y = this.yScale_.invert(y);
x = tr.b.clamp(x, this.xScale_.domain()[0], this.xScale_.domain()[1]);
y = tr.b.clamp(y, this.yScale_.domain()[0], this.yScale_.domain()[1]);
return {x: x, y: y};
},
prepareDataEvent_: function(mouseEvent, dataEvent) {
var dataPoint = this.getDataPointAtClientPoint_(
mouseEvent.clientX, mouseEvent.clientY);
dataEvent.x = dataPoint.x;
dataEvent.y = dataPoint.y;
},
onMouseDown_: function(mouseEvent) {
tr.ui.b.trackMouseMovesUntilMouseUp(
this.onMouseMove_.bind(this, mouseEvent.button),
this.onMouseUp_.bind(this, mouseEvent.button));
mouseEvent.preventDefault();
mouseEvent.stopPropagation();
var dataEvent = new tr.b.Event('item-mousedown');
dataEvent.button = mouseEvent.button;
this.classList.add('updating-brushing-state');
this.prepareDataEvent_(mouseEvent, dataEvent);
this.dispatchEvent(dataEvent);
},
onMouseMove_: function(button, mouseEvent) {
if (mouseEvent.buttons !== undefined) {
mouseEvent.preventDefault();
mouseEvent.stopPropagation();
}
var dataEvent = new tr.b.Event('item-mousemove');
dataEvent.button = button;
this.prepareDataEvent_(mouseEvent, dataEvent);
this.dispatchEvent(dataEvent);
},
onMouseUp_: function(button, mouseEvent) {
mouseEvent.preventDefault();
mouseEvent.stopPropagation();
var dataEvent = new tr.b.Event('item-mouseup');
dataEvent.button = button;
this.prepareDataEvent_(mouseEvent, dataEvent);
this.dispatchEvent(dataEvent);
this.classList.remove('updating-brushing-state');
}
};
return {
ChartBase2D: ChartBase2D
};
});
</script>