/** * Concrete bar chart/histogram implementation. * * Examples: * - {@link http://dc-js.github.com/dc.js/ Nasdaq 100 Index} * - {@link http://dc-js.github.com/dc.js/crime/index.html Canadian City Crime Stats} * @class barChart * @memberof dc * @mixes dc.stackMixin * @mixes dc.coordinateGridMixin * @example * // create a bar chart under #chart-container1 element using the default global chart group * var chart1 = dc.barChart('#chart-container1'); * // create a bar chart under #chart-container2 element using chart group A * var chart2 = dc.barChart('#chart-container2', 'chartGroupA'); * // create a sub-chart under a composite parent chart * var chart3 = dc.barChart(compositeChart); * @param {String|node|d3.selection|dc.compositeChart} parent - Any valid * {@link https://github.com/d3/d3-selection/blob/master/README.md#select d3 single selector} * specifying a dom block element such as a div; or a dom element or d3 selection. If the bar * chart is a sub-chart in a {@link dc.compositeChart Composite Chart} then pass in the parent * composite chart instance instead. * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in. * Interaction with a chart will only trigger events and redraws within the chart's group. * @returns {dc.barChart} */ dc.barChart = function (parent, chartGroup) { var MIN_BAR_WIDTH = 1; var DEFAULT_GAP_BETWEEN_BARS = 2; var LABEL_PADDING = 3; var _chart = dc.stackMixin(dc.coordinateGridMixin({})); var _gap = DEFAULT_GAP_BETWEEN_BARS; var _centerBar = false; var _alwaysUseRounding = false; var _barWidth; dc.override(_chart, 'rescale', function () { _chart._rescale(); _barWidth = undefined; return _chart; }); dc.override(_chart, 'render', function () { if (_chart.round() && _centerBar && !_alwaysUseRounding) { dc.logger.warn('By default, brush rounding is disabled if bars are centered. ' + 'See dc.js bar chart API documentation for details.'); } return _chart._render(); }); _chart.label(function (d) { return dc.utils.printSingleValue(d.y0 + d.y); }, false); _chart.plotData = function () { var layers = _chart.chartBodyG().selectAll('g.stack') .data(_chart.data()); calculateBarWidth(); layers = layers .enter() .append('g') .attr('class', function (d, i) { return 'stack ' + '_' + i; }) .merge(layers); var last = layers.size() - 1; layers.each(function (d, i) { var layer = d3.select(this); renderBars(layer, i, d); if (_chart.renderLabel() && last === i) { renderLabels(layer, i, d); } }); }; function barHeight (d) { return dc.utils.safeNumber(Math.abs(_chart.y()(d.y + d.y0) - _chart.y()(d.y0))); } function labelXPos (d) { var x = _chart.x()(d.x); if (!_centerBar) { x += _barWidth / 2; } if (_chart.isOrdinal() && _gap !== undefined) { x += _gap / 2; } return dc.utils.safeNumber(x); } function labelYPos (d) { var y = _chart.y()(d.y + d.y0); if (d.y < 0) { y -= barHeight(d); } return dc.utils.safeNumber(y - LABEL_PADDING); } function renderLabels (layer, layerIndex, d) { var labels = layer.selectAll('text.barLabel') .data(d.values, dc.pluck('x')); var labelsEnterUpdate = labels .enter() .append('text') .attr('class', 'barLabel') .attr('text-anchor', 'middle') .attr('x', labelXPos) .attr('y', labelYPos) .merge(labels); if (_chart.isOrdinal()) { labelsEnterUpdate.on('click', _chart.onClick); labelsEnterUpdate.attr('cursor', 'pointer'); } dc.transition(labelsEnterUpdate, _chart.transitionDuration(), _chart.transitionDelay()) .attr('x', labelXPos) .attr('y', labelYPos) .text(function (d) { return _chart.label()(d); }); dc.transition(labels.exit(), _chart.transitionDuration(), _chart.transitionDelay()) .attr('height', 0) .remove(); } function barXPos (d) { var x = _chart.x()(d.x); if (_centerBar) { x -= _barWidth / 2; } if (_chart.isOrdinal() && _gap !== undefined) { x += _gap / 2; } return dc.utils.safeNumber(x); } function renderBars (layer, layerIndex, d) { var bars = layer.selectAll('rect.bar') .data(d.values, dc.pluck('x')); var enter = bars.enter() .append('rect') .attr('class', 'bar') .attr('fill', dc.pluck('data', _chart.getColor)) .attr('x', barXPos) .attr('y', _chart.yAxisHeight()) .attr('height', 0); var barsEnterUpdate = enter.merge(bars); if (_chart.renderTitle()) { enter.append('title').text(dc.pluck('data', _chart.title(d.name))); } if (_chart.isOrdinal()) { barsEnterUpdate.on('click', _chart.onClick); } dc.transition(barsEnterUpdate, _chart.transitionDuration(), _chart.transitionDelay()) .attr('x', barXPos) .attr('y', function (d) { var y = _chart.y()(d.y + d.y0); if (d.y < 0) { y -= barHeight(d); } return dc.utils.safeNumber(y); }) .attr('width', _barWidth) .attr('height', function (d) { return barHeight(d); }) .attr('fill', dc.pluck('data', _chart.getColor)) .select('title').text(dc.pluck('data', _chart.title(d.name))); dc.transition(bars.exit(), _chart.transitionDuration(), _chart.transitionDelay()) .attr('x', function (d) { return _chart.x()(d.x); }) .attr('width', _barWidth * 0.9) .remove(); } function calculateBarWidth () { if (_barWidth === undefined) { var numberOfBars = _chart.xUnitCount(); // please can't we always use rangeBands for bar charts? if (_chart.isOrdinal() && _gap === undefined) { _barWidth = Math.floor(_chart.x().bandwidth()); } else if (_gap) { _barWidth = Math.floor((_chart.xAxisLength() - (numberOfBars - 1) * _gap) / numberOfBars); } else { _barWidth = Math.floor(_chart.xAxisLength() / (1 + _chart.barPadding()) / numberOfBars); } if (_barWidth === Infinity || isNaN(_barWidth) || _barWidth < MIN_BAR_WIDTH) { _barWidth = MIN_BAR_WIDTH; } } } _chart.fadeDeselectedArea = function (brushSelection) { var bars = _chart.chartBodyG().selectAll('rect.bar'); if (_chart.isOrdinal()) { if (_chart.hasFilter()) { bars.classed(dc.constants.SELECTED_CLASS, function (d) { return _chart.hasFilter(d.x); }); bars.classed(dc.constants.DESELECTED_CLASS, function (d) { return !_chart.hasFilter(d.x); }); } else { bars.classed(dc.constants.SELECTED_CLASS, false); bars.classed(dc.constants.DESELECTED_CLASS, false); } } else if (_chart.brushOn() || _chart.parentBrushOn()) { if (!_chart.brushIsEmpty(brushSelection)) { var start = brushSelection[0]; var end = brushSelection[1]; bars.classed(dc.constants.DESELECTED_CLASS, function (d) { return d.x < start || d.x >= end; }); } else { bars.classed(dc.constants.DESELECTED_CLASS, false); } } }; /** * Whether the bar chart will render each bar centered around the data position on the x-axis. * @method centerBar * @memberof dc.barChart * @instance * @param {Boolean} [centerBar=false] * @returns {Boolean|dc.barChart} */ _chart.centerBar = function (centerBar) { if (!arguments.length) { return _centerBar; } _centerBar = centerBar; return _chart; }; dc.override(_chart, 'onClick', function (d) { _chart._onClick(d.data); }); /** * Get or set the spacing between bars as a fraction of bar size. Valid values are between 0-1. * Setting this value will also remove any previously set {@link dc.barChart#gap gap}. See the * {@link https://github.com/d3/d3-scale/blob/master/README.md#scaleBand d3 docs} * for a visual description of how the padding is applied. * @method barPadding * @memberof dc.barChart * @instance * @param {Number} [barPadding=0] * @returns {Number|dc.barChart} */ _chart.barPadding = function (barPadding) { if (!arguments.length) { return _chart._rangeBandPadding(); } _chart._rangeBandPadding(barPadding); _gap = undefined; return _chart; }; _chart._useOuterPadding = function () { return _gap === undefined; }; /** * Get or set the outer padding on an ordinal bar chart. This setting has no effect on non-ordinal charts. * Will pad the width by `padding * barWidth` on each side of the chart. * @method outerPadding * @memberof dc.barChart * @instance * @param {Number} [padding=0.5] * @returns {Number|dc.barChart} */ _chart.outerPadding = _chart._outerRangeBandPadding; /** * Manually set fixed gap (in px) between bars instead of relying on the default auto-generated * gap. By default the bar chart implementation will calculate and set the gap automatically * based on the number of data points and the length of the x axis. * @method gap * @memberof dc.barChart * @instance * @param {Number} [gap=2] * @returns {Number|dc.barChart} */ _chart.gap = function (gap) { if (!arguments.length) { return _gap; } _gap = gap; return _chart; }; _chart.extendBrush = function (brushSelection) { if (brushSelection && _chart.round() && (!_centerBar || _alwaysUseRounding)) { brushSelection[0] = _chart.round()(brushSelection[0]); brushSelection[1] = _chart.round()(brushSelection[1]); } return brushSelection; }; /** * Set or get whether rounding is enabled when bars are centered. If false, using * rounding with centered bars will result in a warning and rounding will be ignored. This flag * has no effect if bars are not {@link dc.barChart#centerBar centered}. * When using standard d3.js rounding methods, the brush often doesn't align correctly with * centered bars since the bars are offset. The rounding function must add an offset to * compensate, such as in the following example. * @method alwaysUseRounding * @memberof dc.barChart * @instance * @example * chart.round(function(n) { return Math.floor(n) + 0.5; }); * @param {Boolean} [alwaysUseRounding=false] * @returns {Boolean|dc.barChart} */ _chart.alwaysUseRounding = function (alwaysUseRounding) { if (!arguments.length) { return _alwaysUseRounding; } _alwaysUseRounding = alwaysUseRounding; return _chart; }; function colorFilter (color, inv) { return function () { var item = d3.select(this); var match = item.attr('fill') === color; return inv ? !match : match; }; } _chart.legendHighlight = function (d) { if (!_chart.isLegendableHidden(d)) { _chart.g().selectAll('rect.bar') .classed('highlight', colorFilter(d.color)) .classed('fadeout', colorFilter(d.color, true)); } }; _chart.legendReset = function () { _chart.g().selectAll('rect.bar') .classed('highlight', false) .classed('fadeout', false); }; dc.override(_chart, 'xAxisMax', function () { var max = this._xAxisMax(); if ('resolution' in _chart.xUnits()) { var res = _chart.xUnits().resolution; max += res; } return max; }); return _chart.anchor(parent, chartGroup); };