blob: deaf44a55bf214f9e263e2225ec804ba2f93118f [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/ui/base/chart_base_2d_brushable_x.html">
<script>
'use strict';
tr.exportTo('tr.ui.b', function() {
var ColorScheme = tr.b.ColorScheme;
var ChartBase2DBrushX = tr.ui.b.ChartBase2DBrushX;
function getSVGTextWidth(parentNode, text) {
var textNode = document.createElementNS(
'http://www.w3.org/2000/svg', 'text');
textNode.setAttributeNS(null, 'x', 0);
textNode.setAttributeNS(null, 'y', 0);
textNode.setAttributeNS(null, 'fill', 'black');
textNode.appendChild(document.createTextNode(text));
parentNode.appendChild(textNode);
var widthPx = textNode.getComputedTextLength();
parentNode.removeChild(textNode);
return widthPx;
}
// @constructor
var ColumnChart = tr.ui.b.define('column-chart', ChartBase2DBrushX);
ColumnChart.prototype = {
__proto__: ChartBase2DBrushX.prototype,
decorate: function() {
ChartBase2DBrushX.prototype.decorate.call(this);
Polymer.dom(this).classList.add('column-chart');
// ColumnChart allows bars to have arbitrary, non-uniform widths. Bars
// need not all be the same width. The width of each bar is automatically
// computed from the bar's x-coordinate and that of the next bar, which
// can not define the width of the last bar. This is the width (in the
// xScale's domain (as opposed to the xScale's range (which is measured in
// pixels))) of the last bar. When there are at least 2 bars, this is
// computed as the average width of the bars. When there is a single bar,
// this must default to a non-zero number so that the width of the only
// bar will not be zero.
this.xCushion_ = 1;
this.isStacked_ = false;
},
set isStacked(stacked) {
this.isStacked_ = true;
this.updateContents_();
},
get isStacked() {
return this.isStacked_;
},
isDatumFieldSeries_: function(fieldName) {
return fieldName != 'x';
},
getXForDatum_: function(datum, index) {
return datum.x;
},
updateScales_: function() {
if (this.data_.length === 0)
return;
var xDifferences = 0;
var currentX = undefined;
var previousX = undefined;
var yRange = new tr.b.Range();
this.data_.forEach(function(datum, index) {
previousX = currentX;
currentX = this.getXForDatum_(datum, index);
if (previousX !== undefined) {
xDifferences += currentX - previousX;
}
for (var [key, series] of this.seriesByKey_) {
// Allow for sparse data
if (datum[key] !== undefined)
yRange.addValue(datum[key]);
}
}, this);
// X.
// Leave a cushion on the right so that the last rect doesn't
// exceed the chart boundaries. The last rect's width is set to the
// average width of the rects, which is chart.width / data.length.
var width = this.chartAreaSize.width;
this.xScale_.range([0, width]);
var domain = d3.extent(this.data_, this.getXForDatum_.bind(this));
if (this.data_.length > 1)
this.xCushion_ = xDifferences / (this.data_.length - 1);
this.xScale_.domain([domain[0], domain[1] + this.xCushion_]);
// Y.
this.yScale_.range([this.chartAreaSize.height, 0]);
this.yScale_.domain(this.getYScaleDomain_(yRange.min, yRange.max));
},
getYScaleDomain_: function(minValue, maxValue) {
if (!this.isStacked) {
return ChartBase2DBrushX.prototype.getYScaleDomain_.call(
this, minValue, maxValue);
}
var range = new tr.b.Range();
range.addValue(0);
this.data_.forEach(function(datum, index) {
var sum = 0;
for (var [key, series] of this.seriesByKey_) {
if (datum[key] === undefined)
continue;
sum += datum[key];
}
range.addValue(sum);
}, this);
return [range.min, range.max];
},
getStackedRectsForDatum_: function(datum, index) {
var stacks = [];
var bottom = this.yScale_.range()[0];
var sum = 0;
for (var [key, series] of this.seriesByKey_) {
if (datum[key] === undefined || !this.isSeriesEnabled(key))
continue;
sum += datum[key];
var heightPx = bottom - this.yScale_(sum);
bottom -= heightPx;
stacks.push({
key: key,
value: datum[key],
color: this.getDataSeries(key).color,
heightPx: heightPx,
topPx: bottom
});
}
return stacks;
},
getRectsForDatum_: function(datum, index) {
if (this.isStacked)
return this.getStackedRectsForDatum_(datum, index);
var stacks = [];
for (var [key, series] of this.seriesByKey_) {
if (datum[key] === undefined || !this.isSeriesEnabled(key))
continue;
var topPx = this.yScale_(Math.max(datum[key], this.getYScaleMin_()));
stacks.push({
key: key,
value: datum[key],
topPx: topPx,
heightPx: this.yScale_.range()[0] - topPx,
color: this.getDataSeries(key).color
});
}
stacks.sort(function(a, b) {
return b.topPx - a.topPx;
});
return stacks;
},
drawHoverValueBox_: function(rect) {
var seriesKeys = [...this.seriesByKey_.keys()];
var chartAreaSel = d3.select(this.chartAreaElement);
chartAreaSel.selectAll('.hover').remove();
var keyWidthPx = 0;
var keyHeightPx = 0;
if (seriesKeys.length > 1) {
keyWidthPx = getSVGTextWidth(this.chartAreaElement, rect.key) + 5;
keyHeightPx = 16;
}
var valueWidthPx = getSVGTextWidth(
this.chartAreaElement, rect.value) + 5;
var valueHeightPx = 16;
var hoverLeftPx = rect.leftPx + (rect.widthPx / 2);
chartAreaSel
.append('rect')
.attr('class', 'hover')
.attr('fill', 'white')
.attr('x', hoverLeftPx)
.attr('y', rect.topPx)
.attr('width', Math.max(keyWidthPx, valueWidthPx))
.attr('height', keyHeightPx + valueHeightPx);
if (seriesKeys.length > 1) {
chartAreaSel
.append('text')
.attr('class', 'hover')
.attr('fill', rect.color)
.attr('x', hoverLeftPx + 2)
.attr('y', rect.topPx + keyHeightPx - 3)
.text(rect.key);
}
chartAreaSel
.append('text')
.attr('class', 'hover')
.attr('fill', rect.color)
.attr('x', hoverLeftPx + 2)
.attr('y', rect.topPx + keyHeightPx + valueHeightPx - 3)
.text(rect.value);
},
clearHoverValueBox_: function() {
d3.select(this.chartAreaElement).selectAll('.hover').remove();
},
drawRect_: function(rect, sel) {
sel.append('rect')
.attr('fill', rect.color)
.attr('x', rect.leftPx)
.attr('y', rect.topPx)
.attr('width', rect.widthPx)
.attr('height', rect.heightPx)
.on('mouseenter', this.drawHoverValueBox_.bind(this, rect))
.on('mouseleave', this.clearHoverValueBox_.bind(this));
},
updateDataContents_: function(dataSel) {
dataSel.selectAll('*').remove();
var chartAreaSel = d3.select(this.chartAreaElement);
var seriesKeys = [...this.seriesByKey_.keys()];
var rectsSel = dataSel.selectAll('path').data(seriesKeys);
this.data_.forEach(function(datum, index) {
var currentX = this.getXForDatum_(datum, index);
var width = undefined;
if (index < (this.data_.length - 1)) {
var nextX = this.getXForDatum_(this.data_[index + 1], index + 1);
width = nextX - currentX;
} else {
width = this.xCushion_;
}
for (var rect of this.getRectsForDatum_(datum, index)) {
rect.leftPx = this.xScale_(currentX);
rect.rightPx = this.xScale_(currentX + width);
rect.widthPx = rect.rightPx - rect.leftPx;
this.drawRect_(rect, rectsSel.enter());
}
}, this);
rectsSel.exit().remove();
}
};
return {
ColumnChart: ColumnChart,
getSVGTextWidth: getSVGTextWidth
};
});
</script>