blob: 5e32e3b95d516626f049096402066625d52e7041 [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/iteration_helpers.html">
<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.isYLogScale_ = false;
this.yLogScaleMin_ = undefined;
this.dataRange_ = new tr.b.Range();
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.updateDataRange_();
this.updateContents_();
},
set isYLogScale(logScale) {
if (logScale)
this.yScale_ = d3.scale.log(10);
else
this.yScale_ = d3.scale.linear();
this.isYLogScale_ = logScale;
},
getYScaleMin_: function() {
return this.isYLogScale_ ? this.yLogScaleMin_ : 0;
},
getYScaleDomain_: function(minValue, maxValue) {
if (this.isYLogScale_)
return [this.getYScaleMin_(), maxValue];
return [Math.min(minValue, this.getYScaleMin_()), maxValue];
},
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;
},
updateDataRange_: function() {
var dataBySeriesKey = this.getDataBySeriesKey_();
this.dataRange_.reset();
tr.b.iterItems(dataBySeriesKey, function(series, values) {
for (var i = 0; i < values.length; i++) {
this.dataRange_.addValue(values[i][series]);
}
}, this);
// Choose the closest power of 10, rounded down, as the smallest tick
// to display.
this.yLogScaleMin_ = undefined;
if (this.dataRange_.min !== undefined) {
var minValue = this.dataRange_.min;
if (minValue == 0)
minValue = 1;
var onePowerLess = Math.floor(
Math.log(minValue) / Math.log(10)) - 1;
this.yLogScaleMin_ = Math.pow(10, onePowerLess);
}
},
updateYAxis_: function(yAxis) {
yAxis.selectAll('*').remove();
yAxis[0][0].style.opacity = 0;
var axisModifier = d3.svg.axis()
.scale(this.yScale_)
.orient('left');
if (this.isYLogScale_) {
if (this.yLogScaleMin_ === undefined)
return;
var minValue = this.dataRange_.min;
if (minValue == 0)
minValue = 1;
var largestPower = Math.ceil(
Math.log(this.dataRange_.max) / Math.log(10)) + 1;
var smallestPower = Math.floor(
Math.log(minValue) / Math.log(10));
var tickValues = [];
for (var i = smallestPower; i < largestPower; i++) {
tickValues.push(Math.pow(10, i));
}
axisModifier = axisModifier
.tickValues(tickValues)
.tickFormat(function(d) {
return d;
});
}
yAxis.call(axisModifier);
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;
if (!this.isDatumFieldSeries_(seriesKey))
return;
var singleSeriesDatum = {x: x};
singleSeriesDatum[seriesKey] = multiSeriesDatum[seriesKey];
dataBySeriesKey[seriesKey].push(singleSeriesDatum);
}, this);
}, 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>