cartodb/lib/assets/javascripts/deep-insights/widgets/histogram/chart.js

1757 lines
51 KiB
JavaScript
Raw Normal View History

2020-06-15 10:58:47 +08:00
var $ = require('jquery');
var _ = require('underscore');
var d3 = require('d3');
var d3Interpolate = require('d3-interpolate');
var CoreModel = require('backbone/core-model');
var CoreView = require('backbone/core-view');
var formatter = require('../../formatter');
var timestampHelper = require('../../util/timestamp-helper');
var viewportUtils = require('../../viewport-utils');
var FILTERED_COLOR = '#2E3C43';
var UNFILTERED_COLOR = 'rgba(0, 0, 0, 0.06)';
var TIP_RECT_HEIGHT = 17;
var TIP_H_PADDING = 6;
var TRIANGLE_SIDE = 14;
var TRIANGLE_HEIGHT = 7;
// How much lower (based on height) will the triangle be on the right side
var TRIANGLE_RIGHT_FACTOR = 1.3;
var TOOLTIP_MARGIN = 2;
var DASH_WIDTH = 2;
var MOBILE_BAR_HEIGHT = 3;
var BEZIER_MARGIN_X = 0.1;
var BEZIER_MARGIN_Y = 1;
var trianglePath = function (x1, y1, x2, y2, x3, y3, yFactor) {
// Bezier Control point y
var cy = y3 + (yFactor * BEZIER_MARGIN_Y);
// Bezier Control point x 1
var cx1 = x3 + BEZIER_MARGIN_X;
var cx2 = x3 - BEZIER_MARGIN_X;
return 'M ' + x1 + ' ' + y1 + ' L ' + x2 + ' ' + y2 + ' C ' + cx1 + ' ' + cy + ' ' + cx2 + ' ' + cy + ' ' + x1 + ' ' + y1 + ' z';
};
module.exports = CoreView.extend({
options: {
// render the chart once the width is set as default, provide false value for this prop to disable this behavior
// e.g. for "mini" histogram behavior
showOnWidthChange: true,
chartBarColor: '#F2CC8F',
labelsMargin: 16, // px
hasAxisTip: false,
minimumBarHeight: 2,
animationSpeed: 750,
handleWidth: 8,
handleRadius: 3,
divisionWidth: 80,
animationBarDelay: function (d, i) {
return Math.random() * (100 + (i * 10));
},
transitionType: 'elastic'
},
initialize: function () {
this._originalData = this.options.originalData;
if (!_.isNumber(this.options.height)) throw new Error('height is required');
if (!this.options.dataviewModel) throw new Error('dataviewModel is required');
if (!this.options.layerModel) throw new Error('layerModel is required');
if (!this.options.type) throw new Error('type is required');
_.bindAll(this, '_selectBars', '_adjustBrushHandles', '_onBrushMove', '_onBrushEnd', '_onMouseMove', '_onMouseOut');
// Use this special setup for each view instance ot have its own debounced listener
// TODO in theory there's the possiblity that the callback is called before the view is rendered in the DOM,
// which would lead to the view not being visible until an explicit window resize.
// a wasAddedToDOM event would've been nice to have
this.forceResize = _.debounce(this._resizeToParentElement.bind(this), 50);
// using tagName: 'svg' doesn't work,
// and w/o class="" d3 won't instantiate properly
this.setElement($('<svg class=""></svg>')[0]);
this._widgetModel = this.options.widgetModel;
this._dataviewModel = this.options.dataviewModel;
this._layerModel = this.options.layerModel;
this.canvas = d3.select(this.el)
.style('overflow', 'visible')
.attr('width', 0)
.attr('height', this.options.height);
this.canvas
.append('g')
.attr('class', 'CDB-WidgetCanvas');
this._setupModel();
this._setupBindings();
this._setupDimensions();
this._setupD3Bindings();
this._setupFillColor();
this.hide(); // will be toggled on width change
this._tooltipFormatter = formatter.formatNumber; // Tooltips are always numbers
this._createFormatter();
},
render: function () {
this._generateChart();
this._generateChartContent();
return this;
},
replaceData: function (data) {
this.model.set({ data: data });
},
toggleLabels: function (show) {
this.model.set('showLabels', show);
},
chartWidth: function () {
var margin = this.model.get('margin');
// Get max because width might be negative initially
return Math.max(0, this.model.get('width') - margin.left - margin.right);
},
chartHeight: function () {
var m = this.model.get('margin');
var labelsMargin = this.model.get('showLabels')
? this.options.labelsMargin
: 0;
return this.model.get('height') - m.top - m.bottom - labelsMargin;
},
getSelectionExtent: function () {
if (this.brush && this.brush.extent()) {
var extent = this.brush.extent();
return extent[1] - extent[0];
}
return 0;
},
_resizeToParentElement: function () {
if (this.$el.parent()) {
// Hide this view temporarily to get actual size of the parent container
var wasHidden = this.isHidden();
this.hide();
var parent = this.$el.parent();
var grandParent = parent.parent && parent.parent() && parent.parent().length > 0
? parent.parent()
: null;
var width = parent.width() || 0;
if (this.model.get('animated')) {
// We could just substract 24, width of play/pause but imho this is more future proof
this.$el.siblings().each(function () {
width -= $(this).width();
});
}
if (grandParent && grandParent.outerWidth && this._isTabletViewport()) {
width -= grandParent.outerWidth(true) - grandParent.width();
}
if (wasHidden) {
this.hide();
} else {
this.show();
}
this.model.set('width', width);
}
},
_onChangeLeftAxisTip: function () {
this._updateAxisTip('left');
},
_onChangeRightAxisTip: function () {
this._updateAxisTip('right');
},
_overlap: function (first, second) {
var bFirst = first.node().getBoundingClientRect();
var bSecond = second.node().getBoundingClientRect();
return !(bFirst.right < bSecond.left ||
bFirst.left > bSecond.right ||
bFirst.bottom < bSecond.top ||
bFirst.top > bSecond.bottom);
},
_updateTriangle: function (isRight, triangle, start, center, rectWidth) {
var ySign = isRight && !(this._isTabletViewport() && this._isTimeSeries()) ? -1 : 1;
var transform = d3.transform(triangle.attr('transform'));
var side = Math.min(TRIANGLE_SIDE, rectWidth);
var translate = center - (side / 2);
var offset = isRight
? Math.min((start + rectWidth) - (translate + side), 0)
: Math.abs(Math.min(translate - start, 0));
var p0 = [0, 0];
var p1 = [side, 0];
var p2 = [side / 2 - offset, TRIANGLE_HEIGHT * ySign];
triangle.attr('d', trianglePath(p0[0], p0[1], p1[0], p1[1], p2[0], p2[1], ySign));
transform.translate[0] = center - (side / 2) + offset;
triangle.attr('transform', transform.toString());
},
_updateAxisTip: function (className) {
var leftTip = 'left_axis_tip';
var rightTip = 'right_axis_tip';
var attr = className + '_axis_tip';
var isRight = className === 'right';
var isLeft = !isRight;
var isWeek = this._dataviewModel.get('aggregation') === 'week';
var model = this.model.get(attr);
if (model === undefined) { return; }
var leftValue = this.model.get(leftTip);
var rightValue = this.model.get(rightTip);
var textLabel = this.chart.select('.CDB-Chart-axisTipText.CDB-Chart-axisTip-' + className);
var axisTip = this.chart.select('.CDB-Chart-axisTip.CDB-Chart-axisTip-' + className);
var rectLabel = this.chart.select('.CDB-Chart-axisTipRect.CDB-Chart-axisTip-' + className);
var handle = this.chart.select('.CDB-Chart-handle.CDB-Chart-handle-' + className);
var triangle = handle.select('.CDB-Chart-axisTipTriangle');
textLabel.data([model]).text(function (d) {
var text = this.formatter(d);
this._dataviewModel.trigger('on_update_axis_tip', {
attr: attr,
text: text
});
return text;
}.bind(this));
if (!textLabel.node()) {
return;
}
var textBBox = textLabel.node().getBBox();
var width = textBBox.width;
var rectWidth = width + TIP_H_PADDING;
var handleWidth = this.options.handleWidth;
var barWidth = this.barWidth;
var chartWidth = this.chartWidth();
rectLabel.attr('width', rectWidth);
textLabel.attr('dx', TIP_H_PADDING / 2);
textLabel.attr('dy', textBBox.height - Math.abs((textBBox.height - TIP_RECT_HEIGHT) / 2));
var parts = d3.transform(handle.attr('transform')).translate;
var xPos = +parts[0] + (this.options.handleWidth / 2);
var yPos = isRight && !(this._isMobileViewport() && this._isTimeSeries())
? this.chartHeight() + (TRIANGLE_HEIGHT * TRIANGLE_RIGHT_FACTOR) - 1
: -(TRIANGLE_HEIGHT + TIP_RECT_HEIGHT + TOOLTIP_MARGIN);
yPos = Math.floor(yPos);
// Align rect and bar centers
var rectCenter = rectWidth / 2;
var barCenter = (handleWidth + barWidth) / 2;
barCenter -= (isRight ? barWidth : 0); // right tip should center to the previous bin
if (!this._isDateTimeSeries() || isWeek) { // In numeric and week histograms, axis should point to the handler
barCenter = handleWidth / 2;
}
var translate = barCenter - rectCenter;
// Check if rect if out of bounds and clip translate if that happens
var leftPos = xPos + translate;
var rightPos = leftPos + rectWidth;
var translatedCenter = translate + rectCenter;
var rightExceed = rightPos - (chartWidth + handleWidth);
// Do we exceed left?
if (leftPos < 0) {
translate -= leftPos;
}
// Do we exceed right?
if (rightExceed > 0) {
translate -= rightExceed;
}
// Show / hide labels depending on their values
var showTip = isLeft
? leftValue <= rightValue
: (leftValue <= rightValue && !(leftValue === rightValue && this._isDateTimeSeries()));
this._showAxisTip(className, showTip);
// Translate axis tip
axisTip.attr('transform', 'translate(' + translate + ', ' + yPos + ')');
// Update triangle position
this._updateTriangle(isRight, triangle, translate, translatedCenter, rectWidth);
if (this.model.get('dragging') && this._isMobileViewport() && this._isTimeSeries()) {
this._showAxisTip(className, true);
}
},
_onChangeData: function () {
if (this.model.previous('data').length !== this.model.get('data').length) {
this.reset();
} else {
this.refresh();
}
this._setupFillColor();
this._refreshBarsColor();
},
_onChangeRange: function () {
var loIndex = this.model.get('lo_index');
var hiIndex = this.model.get('hi_index');
if ((loIndex === 0 && hiIndex === 0) || (loIndex === null && hiIndex === null)) {
return;
}
this.selectRange(loIndex, hiIndex);
this._adjustBrushHandles();
this._setAxisTipAccordingToBins();
this._selectBars();
this.trigger('on_brush_end', loIndex, hiIndex);
},
_onChangeWidth: function () {
var width = this.model.get('width');
this.canvas.attr('width', width);
this.chart.attr('width', width);
if (this.options.showOnWidthChange && width > 0) {
this.show();
}
this.reset();
var loBarIndex = this.model.get('lo_index');
var hiBarIndex = this.model.get('hi_index');
this.selectRange(loBarIndex, hiBarIndex);
this._updateAxisTip('left');
this._updateAxisTip('right');
},
_onChangeNormalized: function () {
// do not show shadow bars if they are not enabled
this.model.set('show_shadow_bars', !this.model.get('normalized'));
this._generateShadowBars();
this.updateYScale();
this.refresh();
},
_onChangeHeight: function () {
var height = this.model.get('height');
this.$el.height(height);
this.chart.attr('height', height);
this.leftHandle.attr('height', height);
this.rightHandle.attr('height', height);
this.updateYScale();
this.reset();
},
_onChangeShowLabels: function () {
this._axis.style('opacity', this.model.get('showLabels') ? 1 : 0);
},
_onChangePos: function () {
var pos = this.model.get('pos');
var margin = this.model.get('margin');
var x = +pos.x;
var y = +pos.y;
this.chart
.transition()
.duration(150)
.attr('transform', 'translate(' + (margin.left + x) + ', ' + (margin.top + y) + ')');
},
_onChangeDragging: function () {
this.chart.classed('is-dragging', this.model.get('dragging'));
if (!this.model.get('dragging') && this._isMobileViewport() && this._isTimeSeries()) {
this._showAxisTip('right', false);
this._showAxisTip('left', false);
}
},
_toggleAxisTip: function (className, show) {
var textLabel = this.chart.select('.CDB-Chart-axisTipText.CDB-Chart-axisTip-' + className);
var rectLabel = this.chart.select('.CDB-Chart-axisTipRect.CDB-Chart-axisTip-' + className);
var handle = this.chart.select('.CDB-Chart-handle.CDB-Chart-handle-' + className);
var triangle = handle.select('.CDB-Chart-axisTipTriangle');
var duration = 60;
if (textLabel) {
textLabel.transition().duration(duration).attr('opacity', show);
}
if (rectLabel) {
rectLabel.transition().duration(duration).attr('opacity', show);
}
if (triangle) {
triangle.transition().duration(duration).style('opacity', show);
}
},
_showAxisTip: function (className, show) {
this._toggleAxisTip(className, show ? 1 : 0);
},
_setAxisTipAccordingToBins: function () {
var left = this._getValueFromBinIndex(this._getLoBarIndex());
var right = this._getValueFromBinIndex(this._getHiBarIndex());
if (this._isDateTimeSeries()) {
right = timestampHelper.substractOneUnit(right, this._dataviewModel.get('aggregation'));
}
this._setAxisTip(left, right);
},
_setAxisTip: function (left, right) {
if (this.options.hasAxisTip) {
this.model.set({
left_axis_tip: left,
right_axis_tip: right
});
}
},
reset: function () {
this._removeChartContent();
this._setupDimensions();
this._calcBarWidth();
this._generateChartContent();
this._generateShadowBars();
},
refresh: function () {
this._createFormatter();
this._setupDimensions();
this._removeAxis();
this._generateAxis();
this._updateChart();
this._refreshBarsColor();
this.chart.select('.CDB-Chart-handles').moveToFront();
this.chart.select('.Brush').moveToFront();
},
resetIndexes: function () {
this.model.set({ lo_index: null, hi_index: null });
},
removeShadowBars: function () {
this.model.set('show_shadow_bars', false);
},
_removeShadowBars: function () {
this.chart.selectAll('.CDB-Chart-shadowBars').remove();
},
_removeBars: function () {
this.chart.selectAll('.CDB-Chart-bars').remove();
},
_removeBrush: function () {
this.chart.selectAll('.Brush').remove();
this.chart.classed('is-selectable', false);
this._axis.classed('is-disabled', false);
},
_removeLines: function () {
this.chart.select('.CDB-Chart-lines').remove();
this.chart.select('.CDB-Chart-line--bottom').remove();
},
_removeChartContent: function () {
this._removeBrush();
this._removeHandles();
this._removeBars();
this._removeAxis();
this._removeLines();
},
_generateChartContent: function () {
this._generateAxis();
if (!(this._isTabletViewport() && this._isTimeSeries())) {
this._generateLines();
}
this._generateBars();
if (!(this._isMobileViewport() && this._isTimeSeries())) {
this._generateBottomLine();
}
this._generateHandles();
this._setupBrush();
},
_generateLines: function () {
this._generateHorizontalLines();
this._generateVerticalLines();
},
_generateVerticalLines: function () {
var lines = this.chart.select('.CDB-Chart-lines');
lines.append('g')
.selectAll('.CDB-Chart-line')
.data(this.verticalRange.slice(1, this.verticalRange.length - 1))
.enter().append('svg:line')
.attr('class', 'CDB-Chart-line')
.attr('y1', 0)
.attr('x1', function (d) { return d; })
.attr('y2', this.chartHeight())
.attr('x2', function (d) { return d; });
},
_generateHorizontalLines: function () {
var lines = this.chart.append('g')
.attr('class', 'CDB-Chart-lines');
lines.append('g')
.attr('class', 'y')
.selectAll('.CDB-Chart-line')
.data(this.horizontalRange.slice(0, this.horizontalRange.length - 1))
.enter().append('svg:line')
.attr('class', 'CDB-Chart-line')
.attr('x1', 0)
.attr('y1', function (d) { return d; })
.attr('x2', this.chartWidth())
.attr('y2', function (d) { return d; });
},
_generateBottomLine: function () {
this.chart.append('line')
.attr('class', 'CDB-Chart-line CDB-Chart-line--bottom')
.attr('x1', 0)
.attr('y1', this.chartHeight() - 1)
.attr('x2', this.chartWidth() - 1)
.attr('y2', this.chartHeight() - 1);
},
_setupD3Bindings: function () { // TODO: move to a helper
d3.selection.prototype.moveToBack = function () {
return this.each(function () {
var firstChild = this.parentNode.firstChild;
if (firstChild) {
this.parentNode.insertBefore(this, firstChild);
}
});
};
d3.selection.prototype.moveToFront = function () {
return this.each(function () {
this.parentNode.appendChild(this);
});
};
},
_setupModel: function () {
this.model = new CoreModel({
bounded: false,
showLabels: true,
data: this.options.data,
height: this.options.height,
display: true,
show_shadow_bars: this.options.displayShadowBars,
margin: _.clone(this.options.margin),
width: 0, // will be set on resize listener
pos: { x: 0, y: 0 },
normalized: this.options.normalized,
local_timezone: this.options.local_timezone
});
},
_setupBindings: function () {
this.listenTo(this.model, 'change:data', this._onChangeData);
this.listenTo(this.model, 'change:display', this._onChangeDisplay);
this.listenTo(this.model, 'change:dragging', this._onChangeDragging);
this.listenTo(this.model, 'change:height', this._onChangeHeight);
this.listenTo(this.model, 'change:left_axis_tip', this._onChangeLeftAxisTip);
this.listenTo(this.model, 'change:lo_index change:hi_index', this._onChangeRange);
this.listenTo(this.model, 'change:pos', this._onChangePos);
this.listenTo(this.model, 'change:right_axis_tip', this._onChangeRightAxisTip);
this.listenTo(this.model, 'change:showLabels', this._onChangeShowLabels);
this.listenTo(this.model, 'change:show_shadow_bars', this._onChangeShowShadowBars);
this.listenTo(this.model, 'change:width', this._onChangeWidth);
this.listenTo(this.model, 'change:normalized', this._onChangeNormalized);
if (this._widgetModel) {
this.listenTo(this._widgetModel, 'change:autoStyle', this._refreshBarsColor);
this.listenTo(this._widgetModel, 'change:style', function () {
this._setupFillColor();
this._refreshBarsColor();
});
}
if (this._dataviewModel) {
this.listenTo(this._dataviewModel, 'change:offset change:localTimezone', function () {
this.refresh();
});
}
this.listenTo(this._layerModel, 'change:cartocss', function () {
if (!this._areGradientsAlreadyGenerated()) {
this._setupFillColor();
}
});
if (this._originalData) {
this.listenTo(this._originalData, 'change:data', function () {
this.updateYScale();
this._removeShadowBars();
this._generateShadowBars();
});
}
},
_setupDimensions: function () {
this._setupScales();
this._setupRanges();
this.forceResize();
},
_getData: function () {
return (this._originalData && this._originalData.getData()) || this.model.get('data');
},
_getMaxData: function (data) {
return d3.max(data, function (d) { return _.isEmpty(d) ? 0 : d.freq; });
},
_getXScale: function () {
return d3.scale.linear().domain([0, 100]).range([0, this.chartWidth()]);
},
_getYScale: function () {
var data = this.model.get('normalized') ? this.model.get('data') : this._getData();
return d3.scale.linear().domain([0, this._getMaxData(data)]).range([this.chartHeight(), 0]);
},
updateXScale: function () {
this.xScale = this._getXScale();
},
updateYScale: function () {
this.yScale = this._getYScale();
},
resetYScale: function () {
this.yScale = this._originalYScale;
},
_getDataForScales: function () {
if (!this.model.get('bounded') && this._originalData) {
return this._originalData.getData();
} else {
return this.model.get('data');
}
},
_setupScales: function () {
var data = this._getDataForScales();
this.updateXScale();
if (!this._originalYScale || this.model.get('normalized')) {
this._originalYScale = this.yScale = this._getYScale();
}
if (!data || !data.length) {
return;
}
var start = data[0].start;
var end = data[data.length - 1].end;
this.xAxisScale = d3.scale.linear().range([start, end]).domain([0, this.chartWidth()]);
},
_setupRanges: function () {
this.verticalRange = this._calculateVerticalRangeDivisions();
this.horizontalRange = d3.range(0, this.chartHeight() + this.chartHeight() / 2, this.chartHeight() / 2);
},
_calculateVerticalRangeDivisions: function () {
if (this._isDateTimeSeries() && this.model.get('data').length > 0) {
return this._calculateTimelySpacedDivisions();
}
return this._calculateEvenlySpacedDivisions();
},
_calculateTimelySpacedDivisions: function () {
this._calcBarWidth();
var divisions = Math.round(this.chartWidth() / this.options.divisionWidth);
var bucketsPerDivision = Math.ceil(this.model.get('data').length / divisions);
var range = [0];
var index = 0;
for (var i = 0; i < divisions; i++) {
index = (i < (divisions - 1)) ? index + bucketsPerDivision : this.model.get('data').length;
range.push(Math.ceil(this.xAxisScale.invert(this._getValueFromBinIndex(index))));
}
range = _.uniq(range);
// Sometimes the last two ticks are too close. In those cases, we get rid of the second to last
if (range.length >= 3) {
var lastTwo = _.last(range, 2);
if ((lastTwo[1] - lastTwo[0]) < this.options.divisionWidth) {
range = _.without(range, lastTwo[0]);
}
}
return range;
},
_calculateEvenlySpacedDivisions: function () {
var divisions = Math.round(this.chartWidth() / this.options.divisionWidth);
var step = this.chartWidth() / divisions;
var stop = this.chartWidth() + step;
var range = d3.range(0, stop, step).slice(0, divisions + 1);
return range;
},
_calcBarWidth: function () {
this.barWidth = this.chartWidth() / this.model.get('data').length;
},
_generateChart: function () {
var margin = this.model.get('margin');
this.chart = d3.select(this.el)
.selectAll('.CDB-WidgetCanvas')
.append('g')
.attr('class', 'CDB-Chart')
.attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
this.chart.classed(this.options.className || '', true);
},
_onChangeShowShadowBars: function () {
if (this.model.get('show_shadow_bars')) {
this._generateShadowBars();
} else {
this._removeShadowBars();
}
},
_onChangeDisplay: function () {
if (this.model.get('display')) {
this._show();
} else {
this._hide();
}
},
hide: function () {
this.model.set('display', false);
},
show: function () {
this.model.set('display', true);
},
_hide: function () {
this.$el.hide();
},
_show: function () {
this.$el.show();
},
isHidden: function () {
return !this.model.get('display');
},
_selectBars: function () {
this.chart
.selectAll('.CDB-Chart-bar')
.classed({
'is-selected': function (d, i) {
return this._isBarChartWithinFilter(i);
}.bind(this),
'is-filtered': function (d, i) {
return !this._isBarChartWithinFilter(i);
}.bind(this)
});
},
_isBarChartWithinFilter: function (i) {
var extent = this.brush.extent();
var lo = extent[0];
var hi = extent[1];
var a = Math.floor(i * this.barWidth);
var b = Math.floor(a + this.barWidth);
var LO = Math.floor(this.xScale(lo));
var HI = Math.floor(this.xScale(hi));
return (a > LO && a < HI) || (b > LO && b < HI) || (a <= LO && b >= HI);
},
_isDragging: function () {
return this.model.get('dragging');
},
setAnimated: function () {
return this.model.set('animated', true);
},
_isAnimated: function () {
return this.model.get('animated');
},
_move: function (pos) {
this.model.set({ pos: pos });
},
expand: function (height) {
this.canvas.attr('height', this.model.get('height') + height);
this._move({ x: 0, y: height });
},
contract: function (height) {
this.canvas.attr('height', height);
this._move({ x: 0, y: 0 });
},
resizeHeight: function (height) {
this.model.set('height', height);
},
setNormalized: function (normalized) {
this.model.set('normalized', !!normalized);
return this;
},
removeSelection: function () {
this.resetIndexes();
this.chart.selectAll('.CDB-Chart-bar').classed({'is-selected': false, 'is-filtered': false});
this._refreshBarsColor();
this._removeBrush();
this._setupBrush();
},
selectRange: function (loBarIndex, hiBarIndex) {
if (!loBarIndex && !hiBarIndex) {
return;
}
// -- HACK: Reset filter if any of the indexes is out of the scope
var data = this._dataviewModel.get('data');
if (!data[loBarIndex] || !data[hiBarIndex - 1]) {
return this.trigger('on_reset_filter');
}
var loPosition = this._getBarPosition(loBarIndex);
var hiPosition = this._getBarPosition(hiBarIndex);
this.model.set({ lo_index: loBarIndex, hi_index: hiBarIndex });
this._selectRange(loPosition, hiPosition);
},
_selectRange: function (loPosition, hiPosition) {
this.chart.select('.Brush').transition()
.duration(this.brush.empty() ? 0 : 150)
.call(this.brush.extent([loPosition, hiPosition]))
.call(this.brush.event);
},
_getLoBarIndex: function () {
var extent = this.brush.extent();
return Math.round(this.xScale(extent[0]) / this.barWidth);
},
_getHiBarIndex: function () {
var extent = this.brush.extent();
return Math.round(this.xScale(extent[1]) / this.barWidth);
},
_getBarIndex: function () {
var x = d3.event.sourceEvent.layerX;
return Math.floor(x / this.barWidth);
},
_getBarPosition: function (index) {
var data = this.model.get('data');
return index * (100 / data.length);
},
_setupBrush: function () {
// define brush control element and its events
var brush = d3.svg.brush()
.x(this.xScale)
.on('brush', this._onBrushMove)
.on('brushend', this._onBrushEnd);
// create svg group with class brush and call brush on it
var brushg = this.chart.append('g')
.attr('class', 'Brush')
.call(brush);
var height = this._isTabletViewport() && this._isTimeSeries() ? this.chartHeight() * 2 : this.chartHeight();
// set brush extent to rect and define objects height
brushg.selectAll('rect')
.attr('y', 0)
.attr('height', height);
// Only bind on the background element
brushg.selectAll('rect.background')
.on('mouseout', this._onMouseOut)
.on('mousemove', this._onMouseMove);
// Prevent scroll while touching selections
brushg.selectAll('rect')
.classed('ps-prevent-touchmove', true);
brushg.selectAll('g')
.classed('ps-prevent-touchmove', true);
this.brush = brush;
// Make grabby handles as big as the display handles
this.chart.selectAll('g.resize rect')
.attr('width', this.options.handleWidth)
.attr('x', -this.options.handleWidth / 2);
},
_onBrushMove: function () {
if (!this.brush.empty()) {
this.chart.classed('is-selectable', true);
this._axis.classed('is-disabled', true);
this.model.set({ dragging: true });
this._selectBars();
this._setupFillColor();
this._refreshBarsColor();
this._adjustBrushHandles();
this._updateAxisTip('left');
this._updateAxisTip('right');
}
},
_onBrushEnd: function () {
var data = this.model.get('data');
var brush = this.brush;
var loPosition, hiPosition;
var loBarIndex = this._getLoBarIndex();
var hiBarIndex = this._getHiBarIndex();
this.model.set({ dragging: false });
// click in animated histogram
if (brush.empty() && this._isAnimated()) {
// Send 0..1 factor of position of click in graph
this.trigger('on_brush_click', brush.extent()[0] / 100);
return;
} else {
loPosition = this._getBarPosition(loBarIndex);
hiPosition = this._getBarPosition(hiBarIndex);
// for some reason d3 launches several brushend events
if (!d3.event.sourceEvent) {
return;
}
// click in first and last indexes
if (loBarIndex === hiBarIndex) {
if (hiBarIndex >= data.length) {
loBarIndex = data.length - 1;
hiBarIndex = data.length;
} else {
hiBarIndex = hiBarIndex + 1;
}
}
this.model.set({ lo_index: loBarIndex, hi_index: hiBarIndex }, { silent: true });
// Maybe the indexes don't change, and the handlers end up stuck in the middle of the
// bucket because the event doesn't trigger, so let's trigger it manually
this.model.trigger('change:lo_index');
}
// click in non animated histogram
if (d3.event.sourceEvent && loPosition === undefined && hiPosition === undefined) {
var barIndex = this._getBarIndex();
this.model.set({ lo_index: barIndex, hi_index: barIndex + 1 });
}
this._setupFillColor();
this._refreshBarsColor();
},
_onMouseOut: function () {
var bars = this.chart.selectAll('.CDB-Chart-bar');
bars
.classed('is-highlighted', false)
.attr('fill', this._getFillColor.bind(this));
this.trigger('hover', { target: null });
},
_onMouseMove: function () {
var x = d3.event.offsetX - this.model.get('margin').left;
var barIndex = Math.floor(x / this.barWidth);
var data = this.model.get('data');
if (data[barIndex] === undefined || data[barIndex] === null) {
return;
}
var freq = data[barIndex].freq;
var hoverProperties = {};
var bar = this.chart.select('.CDB-Chart-bar:nth-child(' + (barIndex + 1) + ')');
if (bar && bar.node() && !bar.classed('is-selected')) {
var left = (barIndex * this.barWidth) + (this.barWidth / 2);
var top = this.yScale(freq);
var h = this.chartHeight() - this.yScale(freq);
if (h < this.options.minimumBarHeight && h > 0) {
top = this.chartHeight() - this.options.minimumBarHeight;
}
if (!this._isDragging() && freq > 0) {
var d = this.formatter(freq);
hoverProperties = { target: bar[0][0], top: top, left: left, data: d };
} else {
hoverProperties = null;
}
} else {
hoverProperties = null;
}
this.trigger('hover', hoverProperties);
this.chart.selectAll('.CDB-Chart-bar')
.classed('is-highlighted', false)
.attr('fill', this._getFillColor.bind(this));
if (bar && bar.node()) {
bar.attr('fill', function () {
return this._getHoverFillColor(data[barIndex], barIndex);
}.bind(this));
bar.classed('is-highlighted', true);
}
},
_adjustBrushHandles: function () {
var extent = this.brush.extent();
var loExtent = extent[0];
var hiExtent = extent[1];
this._moveHandle(loExtent, 'left');
this._moveHandle(hiExtent, 'right');
this._setAxisTipAccordingToBins();
},
_moveHandle: function (position, selector) {
var handle = this.chart.select('.CDB-Chart-handle-' + selector);
var fixedPosition = position.toFixed(5);
var x = this.xScale(fixedPosition) - this.options.handleWidth / 2;
var display = (fixedPosition >= 0 && fixedPosition <= 100) ? 'inline' : 'none';
handle
.style('display', display)
.attr('transform', 'translate(' + x + ', 0)');
},
_generateAxisTip: function (className) {
var handle = this.chart.select('.CDB-Chart-handle.CDB-Chart-handle-' + className);
var yPos = className === 'right' && !(this._isMobileViewport() && this._isTimeSeries())
? this.chartHeight() + (TRIANGLE_HEIGHT * TRIANGLE_RIGHT_FACTOR) : -(TRIANGLE_HEIGHT + TIP_RECT_HEIGHT + TOOLTIP_MARGIN);
yPos = Math.floor(yPos);
var yTriangle = className === 'right' && !(this._isMobileViewport() && this._isTimeSeries())
? this.chartHeight() + (TRIANGLE_HEIGHT * TRIANGLE_RIGHT_FACTOR) + 2 : -(TRIANGLE_HEIGHT + TOOLTIP_MARGIN) - 2;
var yFactor = className === 'right' ? -1 : 1;
var triangleHeight = TRIANGLE_HEIGHT * yFactor;
var axisTip = handle.selectAll('g')
.data([''])
.enter().append('g')
.attr('class', 'CDB-Chart-axisTip CDB-Chart-axisTip-' + className)
.attr('transform', 'translate(0,' + yPos + ')');
handle.append('path')
.attr('class', 'CDB-Chart-axisTipRect CDB-Chart-axisTipTriangle')
.attr('transform', 'translate(' + ((this.options.handleWidth / 2) - (TRIANGLE_SIDE / 2)) + ', ' + yTriangle + ')')
.attr('d', trianglePath(0, 0, TRIANGLE_SIDE, 0, (TRIANGLE_SIDE / 2), triangleHeight, yFactor))
.style('opacity', '1');
axisTip.append('rect')
.attr('class', 'CDB-Chart-axisTipRect CDB-Chart-axisTip-' + className)
.attr('rx', '2')
.attr('ry', '2')
.attr('opacity', '1')
.attr('height', TIP_RECT_HEIGHT);
axisTip.append('text')
.attr('class', 'CDB-Text CDB-Size-small CDB-Chart-axisTipText CDB-Chart-axisTip-' + className)
.attr('dy', '11')
.attr('dx', '0')
.attr('opacity', '1')
.text(function (d) { return d; });
},
_isTabletViewport: function () {
return viewportUtils.isTabletViewport();
},
_generateHandle: function (className) {
var height = this._isTabletViewport() && this._isTimeSeries() ? this.chartHeight() * 2 : this.chartHeight();
var opts = { width: this.options.handleWidth, height: height, radius: this.options.handleRadius };
var handle = this.chart.select('.CDB-Chart-handles')
.append('g')
.attr('class', 'CDB-Chart-handle CDB-Chart-handle-' + className);
if (this.options.hasAxisTip) {
this._generateAxisTip(className);
}
if (this.options.hasHandles) {
handle
.append('rect')
.attr('class', 'CDB-Chart-handleRect')
.attr('width', opts.width)
.attr('height', opts.height)
.attr('rx', opts.radius)
.attr('ry', opts.radius);
var y = this._isTabletViewport() && this._isTimeSeries() ? this.chartHeight() : this.chartHeight() / 2;
y -= 3;
var x1 = (opts.width - DASH_WIDTH) / 2;
for (var i = 0; i < 3; i++) {
handle
.append('line')
.attr('class', 'CDB-Chart-handleGrip')
.attr('x1', x1)
.attr('y1', y + i * 3)
.attr('x2', x1 + DASH_WIDTH)
.attr('y2', y + i * 3);
}
}
return handle;
},
_generateHandles: function () {
this.chart.append('g').attr('class', 'CDB-Chart-handles');
this.leftHandle = this._generateHandle('left');
this.rightHandle = this._generateHandle('right');
},
_removeHandles: function () {
this.chart.select('.CDB-Chart-handles').remove();
},
_removeAxis: function () {
this.canvas.select('.CDB-Chart-axis').remove();
},
_generateAdjustAnchorMethod: function (ticks) {
return function (d, i) {
if (i === 0) {
return 'start';
} else if (i === (ticks.length - 1)) {
return 'end';
} else {
return 'middle';
}
};
},
_generateAxis: function () {
this._axis = this._generateNumericAxis();
this._onChangeShowLabels();
},
_generateNumericAxis: function () {
var self = this;
var adjustTextAnchor = this._generateAdjustAnchorMethod(this.verticalRange);
var axis = this.chart.append('g')
.attr('class', 'CDB-Chart-axis CDB-Text CDB-Size-small');
function verticalToValue (d) {
return self.xAxisScale
? self.xAxisScale(d)
: null;
}
axis
.append('g')
.selectAll('.Label')
.data(this.verticalRange)
.enter().append('text')
.attr('x', function (d) {
return d;
})
.attr('y', function () { return self.chartHeight() + 15; })
.attr('text-anchor', adjustTextAnchor)
.text(function (d) {
var value = verticalToValue(d);
if (_.isFinite(value)) {
return self.formatter(value);
}
});
return axis;
},
_getMinValueFromBinIndex: function (binIndex) {
var data = this.model.get('data');
var dataBin = data[binIndex];
if (dataBin) {
return dataBin.min != null ? dataBin.min : dataBin.start;
} else {
return null;
}
},
_getMaxValueFromBinIndex: function (binIndex) {
var result = null;
var data = this.model.get('data');
var dataBin = data[binIndex];
if (dataBin) {
if (this._isDateTimeSeries() && !_.isUndefined(dataBin.next)) {
result = dataBin.next;
} else {
result = dataBin.min != null ? dataBin.max : dataBin.end;
}
}
return result;
},
_getValueFromBinIndex: function (index) {
if (!_.isNumber(index)) {
return null;
}
var result = null;
var fromStart = true;
var data = this.model.get('data');
if (index >= data.length) {
index = data.length - 1;
fromStart = false;
}
var dataBin = data[index];
if (dataBin) {
result = fromStart ? dataBin.start : _.isFinite(dataBin.next) ? dataBin.next : dataBin.end;
}
return result;
},
_getIndexFromValue: function (value) {
var index = _.findIndex(this.model.get('data'), function (bin) {
return bin.start <= value && value <= bin.end;
});
return index;
},
_getMaxFromData: function () {
return this.model.get('data').length > 0
? _.last(this.model.get('data')).end
: null;
},
// Calculates the domain ([ min, max ]) of the selected data. If there is no selection ongoing,
// it will take the first and last buckets with frequency.
_calculateDataDomain: function () {
var data = _.clone(this.model.get('data'));
var minBin;
var maxBin;
var minValue;
var maxValue;
if (!this._hasFilterApplied()) {
minValue = this._getMinValueFromBinIndex(0);
maxValue = this._getMaxValueFromBinIndex(data.length - 1);
minBin = _.find(data, function (d) {
return d.freq !== 0;
});
maxBin = _.find(data.reverse(), function (d) {
return d.freq !== 0;
});
} else {
var loBarIndex = this._getLoBarIndex();
var hiBarIndex = this._getHiBarIndex() - 1;
var filteredData = data.slice(loBarIndex, hiBarIndex);
if (_.isNaN(loBarIndex) || _.isNaN(hiBarIndex)) {
return [0, 0];
}
minValue = this._getMinValueFromBinIndex(loBarIndex);
maxValue = this._getMaxValueFromBinIndex(hiBarIndex);
if (data[loBarIndex] && data[loBarIndex].freq === 0) {
minBin = _.find(filteredData, function (d) {
return d.freq !== 0;
}, this);
}
if (data[hiBarIndex] && data[hiBarIndex].freq === 0) {
var reversedData = filteredData.reverse();
maxBin = _.find(reversedData, function (d) {
return d.freq !== 0;
}, this);
}
}
minValue = minBin ? (minBin.min != null ? minBin.min : minBin.start) : minValue;
maxValue = maxBin ? (maxBin.max != null ? maxBin.max : maxBin.end) : maxValue;
return [minValue, maxValue];
},
_removeFillGradients: function () {
var defs = d3.select(this.el).select('defs');
defs.remove();
delete this._linearGradients;
},
_areGradientsAlreadyGenerated: function () {
return !!this._linearGradients;
},
// Generate a linear-gradient with several stops for each bar
// in order to generate the proper colors ramp. It will depend
// of the domain of the selected data.
_generateFillGradients: function () {
if (!this._widgetModel || !this._widgetModel.isAutoStyleEnabled()) {
return false;
}
var obj = this._widgetModel.getAutoStyle();
if (_.isEmpty(obj) || _.isEmpty(obj.definition)) {
return false;
}
var self = this;
var geometryDefinition = obj.definition[Object.keys(obj.definition)[0]]; // Gets first definition by geometry
var colorsRange = geometryDefinition && geometryDefinition.color && geometryDefinition.color.range;
var interpolatedColors = d3Interpolate.interpolateRgbBasis(colorsRange);
var colorsRangeHover = _.map(colorsRange, function (color) {
return d3.rgb(color).darker(0.3).toString();
});
var interpolatedHoverColors = d3Interpolate.interpolateRgbBasis(colorsRangeHover);
var data = this.model.get('data');
var domain = this._calculateDataDomain();
var domainScale = d3.scale.linear().domain(domain).range([0, 1]);
var defs = d3.select(this.el).append('defs');
var stopsNumber = 4; // It is not necessary to create as many stops as colors
this._linearGradients = defs
.selectAll('.gradient')
.data(data)
.enter()
.append('linearGradient')
.attr('class', 'gradient')
.attr('id', function (d, i) {
// This is the scale for each bin, used in each stop within this gradient
this.__scale__ = d3.scale.linear()
.range([ self._getMinValueFromBinIndex(i), self._getMaxValueFromBinIndex(i) ])
.domain([0, 1]);
return 'bar-' + self.cid + '-' + i;
})
.attr('x1', '0%')
.attr('y1', '0%')
.attr('x2', '100%')
.attr('y2', '0%');
this._linearGradientsHover = defs
.selectAll('.gradient-hover')
.data(data)
.enter()
.append('linearGradient')
.attr('class', 'gradient-hover')
.attr('id', function (d, i) {
// This is the scale for each bin, used in each stop within this gradient
this.__scale__ = d3.scale.linear()
.range([self._getMinValueFromBinIndex(i), self._getMaxValueFromBinIndex(i)])
.domain([0, 1]);
return 'bar-' + self.cid + '-' + i + '-hover';
})
.attr('x1', '0%')
.attr('y1', '0%')
.attr('x2', '100%')
.attr('y2', '0%');
this._linearGradients
.selectAll('stop')
.data(d3.range(stopsNumber + 1))
.enter()
.append('stop')
.attr('offset', function (d, i) {
var offset = this.__offset__ = Math.floor(((i) / stopsNumber) * 100);
return (offset + '%');
})
.attr('stop-color', function () {
var localScale = this.parentNode.__scale__;
var interpolateValue = domainScale(localScale(this.__offset__ / 100));
return interpolatedColors(interpolateValue);
});
this._linearGradientsHover
.selectAll('stop')
.data(d3.range(stopsNumber + 1))
.enter()
.append('stop')
.attr('offset', function (d, i) {
var offset = this.__offset__ = Math.floor(((i) / stopsNumber) * 100);
return (offset + '%');
})
.attr('stop-color', function () {
var localScale = this.parentNode.__scale__;
var interpolateValue = domainScale(localScale(this.__offset__ / 100));
return interpolatedHoverColors(interpolateValue);
});
},
_setupFillColor: function () {
this._removeFillGradients();
this._generateFillGradients();
},
_getFillColor: function (d, i) {
if (this._widgetModel) {
if (this._widgetModel.isAutoStyle()) {
if (this._hasFilterApplied()) {
if (!this._isBarChartWithinFilter(i)) {
return UNFILTERED_COLOR;
}
}
return 'url(#bar-' + this.cid + '-' + i + ')';
} else {
if (this._hasFilterApplied()) {
if (this._isBarChartWithinFilter(i)) {
return FILTERED_COLOR;
} else {
return UNFILTERED_COLOR;
}
}
return this._widgetModel.getWidgetColor() || this.options.chartBarColor;
}
}
return this.options.chartBarColor;
},
_getHoverFillColor: function (d, i) {
var currentFillColor = this._getFillColor(d, i);
if (this._widgetModel) {
if (this._widgetModel.isAutoStyle()) {
return 'url(#bar-' + this.cid + '-' + i + '-hover)';
}
}
return d3.rgb(currentFillColor).darker(0.3).toString();
},
_updateChart: function () {
var self = this;
var data = this.model.get('data');
var bars = this.chart.selectAll('.CDB-Chart-bar')
.data(data);
bars
.enter()
.append('rect')
.attr('class', 'CDB-Chart-bar')
.attr('fill', this._getFillColor.bind(this))
.attr('x', function (d, i) {
return i * self.barWidth;
})
.attr('y', self.chartHeight())
.attr('height', 0)
.attr('width', Math.max(0, this.barWidth - 1));
bars
.attr('data-tooltip', function (d) {
return self._tooltipFormatter(d.freq);
})
.transition()
.duration(200)
.attr('height', function (d) {
if (_.isEmpty(d)) {
return 0;
}
if (self._isMobileViewport() && self._isTimeSeries()) {
return MOBILE_BAR_HEIGHT;
}
var h = self.chartHeight() - self.yScale(d.freq);
if (h < self.options.minimumBarHeight && h > 0) {
h = self.options.minimumBarHeight;
}
return h;
})
.attr('y', function (d) {
if (_.isEmpty(d)) {
return self.chartHeight();
}
if (self._isMobileViewport() && self._isTimeSeries()) {
return self.chartHeight() / 2 + MOBILE_BAR_HEIGHT;
}
var h = self.chartHeight() - self.yScale(d.freq);
if (h < self.options.minimumBarHeight && h > 0) {
return self.chartHeight() - self.options.minimumBarHeight;
} else {
return self.yScale(d.freq);
}
});
bars
.exit()
.transition()
.duration(200)
.attr('height', function () {
return 0;
})
.attr('y', function () {
return self.chartHeight();
});
},
_refreshBarsColor: function () {
this.chart
.selectAll('.CDB-Chart-bar')
.classed('is-highlighted', false)
.attr('fill', this._getFillColor.bind(this));
},
_isMobileViewport: function () {
return viewportUtils.isMobileViewport();
},
_generateBars: function () {
var self = this;
var data = this.model.get('data');
this._calcBarWidth();
// Remove spacing if not enough room for the smallest case, or mobile viewport
var spacing = ((((data.length * 2) - 1) > this.chartWidth() || this._isMobileViewport()) && this._isDateTimeSeries()) ? 0 : 1;
var bars = this.chart.append('g')
.attr('transform', 'translate(0, 0)')
.attr('class', 'CDB-Chart-bars')
.selectAll('.CDB-Chart-bar')
.data(data);
bars
.enter()
.append('rect')
.attr('class', 'CDB-Chart-bar')
.attr('fill', this._getFillColor.bind(self))
.attr('x', function (d, i) {
return i * self.barWidth;
})
.attr('y', self.chartHeight())
.attr('height', 0)
.attr('data-tooltip', function (d) {
return self._tooltipFormatter(d.freq);
})
.attr('width', Math.max(1, this.barWidth - spacing));
bars
.attr('data-tooltip', function (d) {
return self._tooltipFormatter(d.freq);
})
.transition()
.ease(this.options.transitionType)
.duration(this.options.animationSpeed)
.delay(this.options.animationBarDelay)
.transition()
.attr('height', function (d) {
if (_.isEmpty(d)) {
return 0;
}
if (self._isMobileViewport() && self._isTimeSeries()) {
return MOBILE_BAR_HEIGHT;
}
var h = self.chartHeight() - self.yScale(d.freq);
if (h < self.options.minimumBarHeight && h > 0) {
h = self.options.minimumBarHeight;
}
return h;
})
.attr('y', function (d) {
if (_.isEmpty(d)) {
return self.chartHeight();
}
if (self._isMobileViewport() && self._isTimeSeries()) {
return self.chartHeight() / 2 + MOBILE_BAR_HEIGHT;
}
var h = self.chartHeight() - self.yScale(d.freq);
if (h < self.options.minimumBarHeight && h > 0) {
return self.chartHeight() - self.options.minimumBarHeight;
} else {
return self.yScale(d.freq);
}
});
},
showShadowBars: function () {
if (this.options.displayShadowBars) {
this.model.set('show_shadow_bars', true);
}
},
_generateShadowBars: function () {
var data = this._getData();
if (!data || !data.length || !this.model.get('show_shadow_bars') || this.model.get('normalized')) {
this._removeShadowBars();
return;
}
this._removeShadowBars();
var self = this;
var yScale = d3.scale.linear().domain([0, this._getMaxData(data)]).range([this.chartHeight(), 0]);
var barWidth = this.chartWidth() / data.length;
this.chart.append('g')
.attr('transform', 'translate(0, 0)')
.attr('class', 'CDB-Chart-shadowBars')
.selectAll('.CDB-Chart-shadowBar')
.data(data)
.enter()
.append('rect')
.attr('class', 'CDB-Chart-shadowBar')
.attr('x', function (d, i) {
return i * barWidth;
})
.attr('y', function (d) {
if (_.isEmpty(d)) {
return self.chartHeight();
}
var h = self.chartHeight() - yScale(d.freq);
if (h < self.options.minimumBarHeight && h > 0) {
return self.chartHeight() - self.options.minimumBarHeight;
} else {
return yScale(d.freq);
}
})
.attr('width', Math.max(0.5, barWidth - 1))
.attr('height', function (d) {
if (_.isEmpty(d)) {
return 0;
}
var h = self.chartHeight() - yScale(d.freq);
if (h < self.options.minimumBarHeight && h > 0) {
h = self.options.minimumBarHeight;
}
return h;
});
// We need to explicitly move the lines of the grid behind the shadow bars
this.chart.selectAll('.CDB-Chart-shadowBars').moveToBack();
this.chart.selectAll('.CDB-Chart-lines').moveToBack();
},
_hasFilterApplied: function () {
return this.model.get('lo_index') != null && this.model.get('hi_index') != null;
},
_isTimeSeries: function () {
return this.options.type.indexOf('time') === 0;
},
_isDateTimeSeries: function () {
return this.options.type === 'time-date';
},
_calculateDivisionWithByAggregation: function (aggregation) {
switch (aggregation) {
case 'year':
return 50;
case 'quarter':
case 'month':
return 80;
case 'week':
case 'day':
return 120;
default:
return 140;
}
},
_createFormatter: function () {
this.formatter = formatter.formatNumber;
if (this._isDateTimeSeries()) {
this.formatter = formatter.timestampFactory(this._dataviewModel.get('aggregation'), this._dataviewModel.getCurrentOffset());
this.options.divisionWidth = this._calculateDivisionWithByAggregation(this._dataviewModel.get('aggregation'));
}
},
unsetBounds: function () {
this.model.set('bounded', false);
this.updateYScale();
this.contract(this.options.height);
this.resetIndexes();
this.removeSelection();
this._setupFillColor();
},
setBounds: function () {
this.model.set('bounded', true);
this.updateYScale();
this.expand(4);
this.removeShadowBars();
}
});