blob: 5f9c29e8c5d7a08b77557cd9734490a2e450883b [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/raf.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);
Polymer.dom(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.hideXAxis_ = false;
this.hideYAxis_ = false;
this.data_ = [];
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 hideXAxis() {
return this.hideXAxis_;
},
set hideXAxis(h) {
this.hideXAxis_ = h;
this.updateContents_();
},
get hideYAxis() {
return this.hideYAxis_;
},
set hideYAxis(h) {
this.hideYAxis_ = h;
this.updateContents_();
},
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;
},
updateSeriesKeys_: function() {
// Don't clear seriesByKey_; the caller might have put state in it using
// getDataSeries() before setting data.
this.data_.forEach(function(datum) {
Object.keys(datum).forEach(function(key) {
if (this.isDatumFieldSeries_(key))
this.getDataSeries(key);
}, this);
}, this);
},
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();
for (var i = 0; i < this.data_.length; i++) {
for (var key in this.data_[i]) {
if (!isNaN(Math.max(this.data_[i][key])))
yRange.addValue(this.data_[i][key]);
}
}
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;
if (this.hideXAxis)
return;
this.drawXAxis_(xAxis);
},
drawXAxis_: function(xAxis) {
xAxis.attr('transform', 'translate(0,' + this.chartAreaSize.height + ')')
.call(d3.svg.axis()
.scale(this.xScale_)
.orient('bottom'));
tr.b.requestAnimationFrame(this.drawXAxisTicks_.bind(this, xAxis));
},
drawXAxisTicks_: function(xAxis) {
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;
},
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;
if (this.hideYAxis)
return;
this.drawYAxis_(yAxis);
},
drawYAxis_: function(yAxis) {
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);
tr.b.requestAnimationFrame(this.drawYAxisTicks_.bind(this, yAxis));
},
drawYAxisTicks_: function(yAxis) {
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;
}
});
leftMargin = parseInt(Math.ceil(leftMargin));
if (leftMargin > this.margin.left) {
this.margin.left = leftMargin;
this.updateContents_();
} else {
yAxis[0][0].style.opacity = 1;
}
},
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 = {};
for (var [key, series] of this.seriesByKey_) {
dataBySeriesKey[key] = [];
}
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;
},
getChartPointAtClientPoint_: function(clientPoint) {
var rect = this.getBoundingClientRect();
return {
x: clientPoint.x - rect.left - this.margin.left,
y: clientPoint.y - rect.top - this.margin.top
};
},
getDataPointAtChartPoint_: function(chartPoint) {
return {
x: tr.b.clamp(this.xScale_.invert(chartPoint.x),
this.xScale_.domain()[0], this.xScale_.domain()[1]),
y: tr.b.clamp(this.yScale_.invert(chartPoint.y),
this.yScale_.domain()[0], this.yScale_.domain()[1])
};
},
getDataPointAtClientPoint_: function(clientX, clientY) {
var chartPoint = this.getChartPointAtClientPoint_(
{x: clientX, y: clientY});
return this.getDataPointAtChartPoint_(chartPoint);
},
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;
Polymer.dom(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);
Polymer.dom(this).classList.remove('updating-brushing-state');
}
};
return {
ChartBase2D: ChartBase2D
};
});
</script>