CartoDB/cartodb20

View on GitHub
lib/assets/javascripts/deep-insights/widgets/histogram/chart.js

Summary

Maintainability
F
2 wks
Test Coverage
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();
  }
});