From 3db761777578bc19144ea8d6dea37b74fda91be8 Mon Sep 17 00:00:00 2001 From: Alan Kavanagh Date: Mon, 10 Aug 2015 10:02:22 +0100 Subject: [PATCH 1/5] merged in changed src file from mtraynham/layer_mixin --- src/bar-chart.js | 446 +++++++++++++++++++------------------- src/layer-mixin.js | 116 ++++++++++ src/line-chart.js | 522 ++++++++++++++++++++------------------------- 3 files changed, 572 insertions(+), 512 deletions(-) create mode 100644 src/layer-mixin.js diff --git a/src/bar-chart.js b/src/bar-chart.js index 4655a83d2..f00ff9219 100644 --- a/src/bar-chart.js +++ b/src/bar-chart.js @@ -1,205 +1,34 @@ -/** -## Bar Chart -Includes: [Stack Mixin](#stack Mixin), [Coordinate Grid Mixin](#coordinate-grid-mixin) - -Concrete bar chart/histogram implementation. - -Examples: - -* [Nasdaq 100 Index](http://dc-js.github.com/dc.js/) -* [Canadian City Crime Stats](http://dc-js.github.com/dc.js/crime/index.html) -#### dc.barChart(parent[, chartGroup]) -Create a bar chart instance and attach it to the given parent element. - -Parameters: -* parent : string | node | selection | compositeChart - any valid - [d3 single selector](https://github.com/mbostock/d3/wiki/Selections#selecting-elements) 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 [Composite Chart](#composite-chart) then pass in the parent composite - chart instance. -* chartGroup : string (optional) - 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: -A newly created bar chart instance - -```js -// 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); -``` - -**/ dc.barChart = function (parent, chartGroup) { - var MIN_BAR_WIDTH = 1; - var DEFAULT_GAP_BETWEEN_BARS = 2; - - 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; - }); - - 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.'); - } - - _chart._render(); - }); - - _chart.plotData = function () { - var layers = _chart.chartBodyG().selectAll('g.stack') - .data(_chart.data()); - - calculateBarWidth(); - - layers - .enter() - .append('g') - .attr('class', function (d, i) { - return 'stack ' + '_' + i; - }); - - layers.each(function (d, i) { - var layer = d3.select(this); - - renderBars(layer, i, d); - }); - }; - - function barHeight(d) { - return dc.utils.safeNumber(Math.abs(_chart.y()(d.y + d.y0) - _chart.y()(d.y0))); - } - - 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('y', _chart.yAxisHeight()) - .attr('height', 0); - - if (_chart.renderTitle()) { - enter.append('title').text(dc.pluck('data', _chart.title(d.name))); - } - - if (_chart.isOrdinal()) { - bars.on('click', _chart.onClick); - } - - dc.transition(bars, _chart.transitionDuration()) - .attr('x', function (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); - }) - .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()) - .attr('height', 0) - .remove(); - } + var DEFAULT_GAP_BETWEEN_BARS = 2, + MIN_BAR_WIDTH = 1; - function calculateBarWidth() { - if (_barWidth === undefined) { - var numberOfBars = _chart.xUnitCount(); + var _chart = dc.layerMixin(dc.coordinateGridMixin({})); - // please can't we always use rangeBands for bar charts? - if (_chart.isOrdinal() && _gap === undefined) { - _barWidth = Math.floor(_chart.x().rangeBand()); - } 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 () { - var bars = _chart.chartBodyG().selectAll('rect.bar'); - var extent = _chart.brush().extent(); - - 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.brushIsEmpty(extent)) { - var start = extent[0]; - var end = extent[1]; - - bars.classed(dc.constants.DESELECTED_CLASS, function (d) { - return d.x < start || d.x >= end; - }); - } else { - bars.classed(dc.constants.DESELECTED_CLASS, false); - } - } - }; + var _alwaysUseRounding = false, + _centerBar = false, + _gap = DEFAULT_GAP_BETWEEN_BARS; /** - #### .centerBar(boolean) - Whether the bar chart will render each bar centered around the data position on x axis. Default: false + #### .alwaysUseRounding([boolean]) + Set or get whether rounding is enabled when bars are centered. Default: false. 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 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. + ```js + chart.round(function(n) {return Math.floor(n)+0.5}); + ``` **/ - _chart.centerBar = function (_) { + _chart.alwaysUseRounding = function (_) { if (!arguments.length) { - return _centerBar; + return _alwaysUseRounding; } - _centerBar = _; + _alwaysUseRounding = _; return _chart; }; - dc.override(_chart, 'onClick', function (d) { - _chart._onClick(d.data); - }); - /** #### .barPadding([padding]) Get or set the spacing between bars as a fraction of bar size. Valid values are between 0-1. @@ -216,18 +45,17 @@ dc.barChart = function (parent, chartGroup) { return _chart; }; - _chart._useOuterPadding = function () { - return _gap === undefined; - }; - /** - #### .outerPadding([padding]) - 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. - - Default: 0.5 + #### .centerBar(boolean) + Whether the bar chart will render each bar centered around the data position on x axis. Default: false **/ - _chart.outerPadding = _chart._outerRangeBandPadding; + _chart.centerBar = function (_) { + if (!arguments.length) { + return _centerBar; + } + _centerBar = _; + return _chart; + }; /** #### .gap(gapBetweenBars) @@ -244,44 +72,73 @@ dc.barChart = function (parent, chartGroup) { return _chart; }; - _chart.extendBrush = function () { - var extent = _chart.brush().extent(); - if (_chart.round() && (!_centerBar || _alwaysUseRounding)) { - extent[0] = extent.map(_chart.round())[0]; - extent[1] = extent.map(_chart.round())[1]; + _chart._useOuterPadding = function () { + return _gap === undefined; + }; - _chart.chartBodyG().select('.brush') - .call(_chart.brush().extent(extent)); + /** + #### .outerPadding([padding]) + 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. + + Default: 0.5 + **/ + _chart.outerPadding = _chart._outerRangeBandPadding; + + _chart.layerFunctor(dc.barChart.layerFn.standard); + + _chart.barWidth = function () { + var numberOfBars = _chart.xUnitCount(), + barWidth = MIN_BAR_WIDTH; + if (_chart.isOrdinal() && _gap === undefined) { + barWidth = Math.floor(_chart.x().rangeBand()); + } 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; } + return barWidth; + }; - return extent; + _chart.plotData = function () { + var g = _chart.chartBodyG(), + data = _chart.layerFn().data; + + var bars = g.selectAll('rect.bar') + .data(data, function (datum) { + return datum.key + datum.layer || ''; + }); + + bars.enter() + .append('rect') + .attr('class', 'bar') + .attr('fill', _chart.getColor) + .attr('height', 0); + + _chart.layerFn().render(_chart, dc.transition(bars, _chart.transitionDuration())); + + dc.transition(bars.exit(), _chart.transitionDuration()) + .attr('height', 0) + .remove(); }; - /** - #### .alwaysUseRounding([boolean]) - Set or get whether rounding is enabled when bars are centered. Default: false. 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 centered. + _chart.fadeDeselectedArea = function () {}; - 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. - ```js - chart.round(function(n) {return Math.floor(n)+0.5}); - ``` - **/ - _chart.alwaysUseRounding = function (_) { - if (!arguments.length) { - return _alwaysUseRounding; + _chart.extendBrush = function () { + var extent = _chart.brush().extent(); + if (_chart.round() && (!_centerBar || _alwaysUseRounding)) { + _chart.chartBodyG().select('.brush') + .call(_chart.brush().extent(extent.map(_chart.round()))); } - _alwaysUseRounding = _; - return _chart; + return extent; }; function colorFilter(color, inv) { return function () { - var item = d3.select(this); - var match = item.attr('fill') === color; + var match = d3.select(this).attr('fill') === color; return inv ? !match : match; }; } @@ -311,3 +168,138 @@ dc.barChart = function (parent, chartGroup) { return _chart.anchor(parent, chartGroup); }; + +dc.barChart.layerFn = { + standard: dc.layerMixin.layerFunctor(function (chart, data) { + data = dc.layerMixin.dataFn.standard(chart, data); + var xAxisExtent = d3.extent(data, dc.pluck('key')), + yAxisExtent = d3.extent(data, dc.pluck('values')); + return { + data: data, + xAxisMin: xAxisExtent[0] || 0, + xAxisMax: xAxisExtent[1] || 0, + yAxisMin: yAxisExtent[0] || 0, + yAxisMax: yAxisExtent[1] || 0, + render: function (chart, g) { + var _x = chart.x(), + _y = chart.y(), + bWidth = chart.barWidth(), + cHeight = chart.height(); + g.attr('x', function (d) { return _x(d.key); }) + .attr('y', function (d) { return _y(d.values); }) + .attr('width', bWidth) + .attr('height', function (d) { return cHeight - _y(d.values); }); + } + }; + }), + // {key: 'a', values:[{key: 'x', values: 1}, {key: 'y', values: 2}]} + stack: dc.layerMixin.layerFunctor(function (chart, data) { + data = dc.layerMixin.dataFn.key(chart, data); + var xAxisExtent = d3.extent(data, dc.pluck('key')); + var yAxisMax = data.reduce(function (extent, datum) { + var sum = d3.sum(datum.values, dc.pluck('values')); + return Math.max(extent, sum); + }, 0); + data = data.reduce(function (prev, datum) { + var key = datum.key; + return prev.concat(datum.values.reduce(function (previous, layerDatum) { + previous[0].push({ + key: key, + layer: layerDatum.key, + values0: previous[1], + values1: previous[1] += layerDatum.values + }); + return previous; + }, [[], 0])[0]); + }, []); + return { + data: data, + xAxisMin: xAxisExtent[0] || 0, + xAxisMax: xAxisExtent[1] || 0, + yAxisMin: 0, + yAxisMax: yAxisMax, + render: function (chart, g) { + var _x = chart.x(), + _y = chart.y(), + bWidth = chart.barWidth(); + g.attr('y', function (d) { return _y(d.values1); }) + .attr('height', function (d) { return _y(d.values0) - _y(d.values1); }) + .transition() + .attr('x', function (d) { return _x(d.key); }) + .attr('width', bWidth); + } + }; + }), + stack100: dc.layerMixin.layerFunctor(function (chart, data) { + data = dc.layerMixin.dataFn.key(chart, data); + var xAxisExtent = d3.extent(data, dc.pluck('key')); + data = data.reduce(function (prev, datum) { + var key = datum.key, + total = d3.sum(datum.values, dc.pluck('values')); + return prev.concat(datum.values.reduce(function (previous, layerDatum) { + previous[0].push({ + key: key, + layer: layerDatum.key, + values0: previous[1], + values1: previous[1] += (layerDatum.values / total) + }); + return previous; + }, [[], 0])[0]); + }, []); + return { + data: data, + xAxisMin: xAxisExtent[0] || 0, + xAxisMax: xAxisExtent[1] || 0, + yAxisMin: 0, + yAxisMax: 1, + render: function (chart, g) { + var _x = chart.x(), + _y = chart.y(), + bWidth = chart.barWidth(); + g.attr('y', function (d) { return _y(d.values1); }) + .attr('height', function (d) { return _y(d.values0) - _y(d.values1); }) + .transition() + .attr('x', function (d) { return _x(d.key); }) + .attr('width', bWidth); + } + }; + }), + group: dc.layerMixin.layerFunctor(function (chart, data) { + var standardData = dc.layerMixin.dataFn.standard(chart, data), + xAxisExtent = d3.extent(standardData, dc.pluck('key')), + yAxisExtent = d3.extent(data, dc.pluck('value')); + data = dc.layerMixin.dataFn.key(chart, data); + var totalLayers = d3.max(data, function (datum) { + return datum.values.length; + }); + data = data.reduce(function (previous, datum) { + var key = datum.key; + return previous.concat(datum.values.map(function (layerDatum, i) { + return { + key: key, + layer: layerDatum.key, + values: layerDatum.values, + index: i + }; + })); + }, []); + return { + data: data, + xAxisMin: xAxisExtent[0] || 0, + xAxisMax: xAxisExtent[1] || 0, + yAxisMin: yAxisExtent[0] || 0, + yAxisMax: yAxisExtent[1] || 0, + render: function (chart, g) { + var _x = chart.x(), + _y = chart.y(), + bWidth = chart.barWidth() / totalLayers, + cHeight = chart.height(); + g.attr('x', function (d) { return _x(d.key) + bWidth * d.index; }) + .attr('width', bWidth) + .transition() + .attr('y', function (d) { return _y(d.values); }) + .attr('height', function (d) { return cHeight - _y(d.values); }); + } + }; + }) +}; \ No newline at end of file diff --git a/src/layer-mixin.js b/src/layer-mixin.js new file mode 100644 index 000000000..c97ea1471 --- /dev/null +++ b/src/layer-mixin.js @@ -0,0 +1,116 @@ +dc.layerMixin = function (_chart) { + var _layerAccessor, + _layerFunctor, + _layerFn; + + _chart.colorAccessor(function (d) { + return d.layer || null; + }); + + _chart.layerAccessor = function (_) { + if (!arguments.length) { + return _layerAccessor; + } + _layerAccessor = _; + return _chart; + }; + + _chart.layerFunctor = function (_) { + if (!arguments.length) { + return _layerFunctor; + } + _layerFunctor = _; + _chart.expireCache(); + return _chart; + }; + + _chart.layerFn = function (_) { + if (!arguments.length) { + return _layerFn; + } + _layerFn = _; + return _chart; + }; + + _chart._preprocessData = function () { + _chart.layerFn(_chart.layerFunctor()(_chart, _chart.data())); + }; + + _chart.xAxisMax = function () { + return dc.utils.add(_chart.layerFn().xAxisMax(), _chart.xAxisPadding()); + }; + + _chart.xAxisMin = function () { + return dc.utils.subtract(_chart.layerFn().xAxisMin(), _chart.xAxisPadding()); + }; + + _chart.yAxisMax = function () { + return dc.utils.add(_chart.layerFn().yAxisMax(), _chart.yAxisPadding()); + }; + + _chart.yAxisMin = function () { + return dc.utils.subtract(_chart.layerFn().yAxisMin(), _chart.yAxisPadding()); + }; + + _chart._ordinalXDomain = function () { + return _chart.layerFn().input().map(dc.pluck('key')); + }; + return _chart; +}; + +dc.layerMixin.dataFn = { + standard: function (chart, data) { + return d3.nest() + .key(chart.keyAccessor()) + .rollup(function (datums) { + return d3.sum(datums, chart.valueAccessor()); + }).entries(data); + }, + key: function (chart, data) { + return d3.nest() + .key(chart.keyAccessor()) + .key(chart.layerAccessor() || function () { return 'all'; }) + .sortKeys(d3.ascending) + .rollup(function (datums) { + return d3.sum(datums, chart.valueAccessor()); + }).entries(data); + }, + layer: function (chart, data) { + return d3.nest() + .key(chart.layerAccessor() || function () { return 'all'; }) + .key(chart.keyAccessor()) + .sortKeys(d3.ascending) + .rollup(function (datums) { + return d3.sum(datums, chart.valueAccessor()); + }).entries(data); + } +}; + +dc.layerMixin.layerFunctor = function (layerFn) { + return function (chart, input) { + var output = layerFn(chart, input); + return { + input: function () { + return input; + }, + data: function () { + return output.data; + }, + xAxisMax: function () { + return output.xAxisMax; + }, + xAxisMin: function () { + return output.xAxisMin; + }, + yAxisMax: function () { + return output.yAxisMax; + }, + yAxisMin: function () { + return output.yAxisMin; + }, + render: function (chart, g) { + return output.render(chart, g); + } + }; + }; +}; \ No newline at end of file diff --git a/src/line-chart.js b/src/line-chart.js index 6c83d1f6a..dd05120f7 100644 --- a/src/line-chart.js +++ b/src/line-chart.js @@ -1,85 +1,34 @@ -/** -## Line Chart -Includes [Stack Mixin](#stack-mixin), [Coordinate Grid Mixin](#coordinate-grid-mixin) - -Concrete line/area chart implementation. - -Examples: -* [Nasdaq 100 Index](http://dc-js.github.com/dc.js/) -* [Canadian City Crime Stats](http://dc-js.github.com/dc.js/crime/index.html) -#### dc.lineChart(parent[, chartGroup]) -Create a line chart instance and attach it to the given parent element. - -Parameters: - -* parent : string | node | selection | compositeChart - any valid - [d3 single selector](https://github.com/mbostock/d3/wiki/Selections#selecting-elements) specifying - a dom block element such as a div; or a dom element or d3 selection. - If the line chart is a sub-chart in a [Composite Chart](#composite-chart) then pass in the parent composite - chart instance. - -* chartGroup : string (optional) - 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: -A newly created line chart instance - -```js -// create a line chart under #chart-container1 element using the default global chart group -var chart1 = dc.lineChart('#chart-container1'); -// create a line chart under #chart-container2 element using chart group A -var chart2 = dc.lineChart('#chart-container2', 'chartGroupA'); -// create a sub-chart under a composite parent chart -var chart3 = dc.lineChart(compositeChart); -``` - -**/ dc.lineChart = function (parent, chartGroup) { - var DEFAULT_DOT_RADIUS = 5; - var TOOLTIP_G_CLASS = 'dc-tooltip'; - var DOT_CIRCLE_CLASS = 'dot'; - var Y_AXIS_REF_LINE_CLASS = 'yRef'; - var X_AXIS_REF_LINE_CLASS = 'xRef'; - var DEFAULT_DOT_OPACITY = 1e-6; - - var _chart = dc.stackMixin(dc.coordinateGridMixin({})); - var _renderArea = false; - var _dotRadius = DEFAULT_DOT_RADIUS; - var _dataPointRadius = null; - var _dataPointFillOpacity = DEFAULT_DOT_OPACITY; - var _dataPointStrokeOpacity = DEFAULT_DOT_OPACITY; - var _interpolate = 'linear'; - var _tension = 0.7; - var _defined; - var _dashStyle; + var DEFAULT_DOT_RADIUS = 5, + DEFAULT_DOT_OPACITY = 1; + + var _chart = dc.layerMixin(dc.coordinateGridMixin({})), + _renderArea = false, + _dotRadius = DEFAULT_DOT_RADIUS, + _dataPointRadius = DEFAULT_DOT_RADIUS, + _dataPointFillOpacity = DEFAULT_DOT_OPACITY, + _dataPointStrokeOpacity = DEFAULT_DOT_OPACITY, + _interpolate = 'linear', + _tension = 0.7, + _defined, + _dashStyle, + _safePath = function (d) { return (!d || d.indexOf('NaN') >= 0) ? 'M0,0' : d; }, + _line = d3.svg.line() + .interpolate(_interpolate) + .tension(_tension), + _lineEnterExit = d3.svg.line() + .interpolate(_interpolate) + .tension(_tension), + _area = d3.svg.area() + .interpolate(_interpolate) + .tension(_tension), + _areaEnterExit = d3.svg.area() + .interpolate(_interpolate) + .tension(_tension); _chart.transitionDuration(500); _chart._rangeBandPadding(1); - _chart.plotData = function () { - var chartBody = _chart.chartBodyG(); - var layersList = chartBody.selectAll('g.stack-list'); - - if (layersList.empty()) { - layersList = chartBody.append('g').attr('class', 'stack-list'); - } - - var layers = layersList.selectAll('g.stack').data(_chart.data()); - - var layersEnter = layers - .enter() - .append('g') - .attr('class', function (d, i) { - return 'stack ' + '_' + i; - }); - - drawLine(layersEnter, layers); - - drawArea(layersEnter, layers); - - drawDots(chartBody, layers); - }; - /** #### .interpolate([value]) Gets or sets the interpolator to use for lines drawn, by string name, allowing e.g. step @@ -93,6 +42,10 @@ dc.lineChart = function (parent, chartGroup) { return _interpolate; } _interpolate = _; + _line.interpolate(_interpolate); + _lineEnterExit.interpolate(_interpolate); + _area.interpolate(_interpolate); + _areaEnterExit.interpolate(_interpolate); return _chart; }; @@ -108,6 +61,10 @@ dc.lineChart = function (parent, chartGroup) { return _tension; } _tension = _; + _line.tension(_tension); + _lineEnterExit.tension(_tension); + _area.tension(_tension); + _areaEnterExit.tension(_tension); return _chart; }; @@ -161,183 +118,6 @@ dc.lineChart = function (parent, chartGroup) { return _chart; }; - function colors(d, i) { - return _chart.getColor.call(d, d.values, i); - } - - function drawLine(layersEnter, layers) { - var line = d3.svg.line() - .x(function (d) { - return _chart.x()(d.x); - }) - .y(function (d) { - return _chart.y()(d.y + d.y0); - }) - .interpolate(_interpolate) - .tension(_tension); - if (_defined) { - line.defined(_defined); - } - - var path = layersEnter.append('path') - .attr('class', 'line') - .attr('stroke', colors); - if (_dashStyle) { - path.attr('stroke-dasharray', _dashStyle); - } - - dc.transition(layers.select('path.line'), _chart.transitionDuration()) - //.ease('linear') - .attr('stroke', colors) - .attr('d', function (d) { - return safeD(line(d.values)); - }); - } - - function drawArea(layersEnter, layers) { - if (_renderArea) { - var area = d3.svg.area() - .x(function (d) { - return _chart.x()(d.x); - }) - .y(function (d) { - return _chart.y()(d.y + d.y0); - }) - .y0(function (d) { - return _chart.y()(d.y0); - }) - .interpolate(_interpolate) - .tension(_tension); - if (_defined) { - area.defined(_defined); - } - - layersEnter.append('path') - .attr('class', 'area') - .attr('fill', colors) - .attr('d', function (d) { - return safeD(area(d.values)); - }); - - dc.transition(layers.select('path.area'), _chart.transitionDuration()) - //.ease('linear') - .attr('fill', colors) - .attr('d', function (d) { - return safeD(area(d.values)); - }); - } - } - - function safeD (d) { - return (!d || d.indexOf('NaN') >= 0) ? 'M0,0' : d; - } - - function drawDots(chartBody, layers) { - if (!_chart.brushOn()) { - var tooltipListClass = TOOLTIP_G_CLASS + '-list'; - var tooltips = chartBody.select('g.' + tooltipListClass); - - if (tooltips.empty()) { - tooltips = chartBody.append('g').attr('class', tooltipListClass); - } - - layers.each(function (d, layerIndex) { - var points = d.values; - if (_defined) { - points = points.filter(_defined); - } - - var g = tooltips.select('g.' + TOOLTIP_G_CLASS + '._' + layerIndex); - if (g.empty()) { - g = tooltips.append('g').attr('class', TOOLTIP_G_CLASS + ' _' + layerIndex); - } - - createRefLines(g); - - var dots = g.selectAll('circle.' + DOT_CIRCLE_CLASS) - .data(points, dc.pluck('x')); - - dots.enter() - .append('circle') - .attr('class', DOT_CIRCLE_CLASS) - .attr('r', getDotRadius()) - .style('fill-opacity', _dataPointFillOpacity) - .style('stroke-opacity', _dataPointStrokeOpacity) - .on('mousemove', function () { - var dot = d3.select(this); - showDot(dot); - showRefLines(dot, g); - }) - .on('mouseout', function () { - var dot = d3.select(this); - hideDot(dot); - hideRefLines(g); - }); - - dots - .attr('cx', function (d) { - return dc.utils.safeNumber(_chart.x()(d.x)); - }) - .attr('cy', function (d) { - return dc.utils.safeNumber(_chart.y()(d.y + d.y0)); - }) - .attr('fill', _chart.getColor) - .call(renderTitle, d); - - dots.exit().remove(); - }); - } - } - - function createRefLines(g) { - var yRefLine = g.select('path.' + Y_AXIS_REF_LINE_CLASS).empty() ? - g.append('path').attr('class', Y_AXIS_REF_LINE_CLASS) : g.select('path.' + Y_AXIS_REF_LINE_CLASS); - yRefLine.style('display', 'none').attr('stroke-dasharray', '5,5'); - - var xRefLine = g.select('path.' + X_AXIS_REF_LINE_CLASS).empty() ? - g.append('path').attr('class', X_AXIS_REF_LINE_CLASS) : g.select('path.' + X_AXIS_REF_LINE_CLASS); - xRefLine.style('display', 'none').attr('stroke-dasharray', '5,5'); - } - - function showDot(dot) { - dot.style('fill-opacity', 0.8); - dot.style('stroke-opacity', 0.8); - dot.attr('r', _dotRadius); - return dot; - } - - function showRefLines(dot, g) { - var x = dot.attr('cx'); - var y = dot.attr('cy'); - var yAxisX = (_chart._yAxisX() - _chart.margins().left); - var yAxisRefPathD = 'M' + yAxisX + ' ' + y + 'L' + (x) + ' ' + (y); - var xAxisRefPathD = 'M' + x + ' ' + _chart.yAxisHeight() + 'L' + x + ' ' + y; - g.select('path.' + Y_AXIS_REF_LINE_CLASS).style('display', '').attr('d', yAxisRefPathD); - g.select('path.' + X_AXIS_REF_LINE_CLASS).style('display', '').attr('d', xAxisRefPathD); - } - - function getDotRadius() { - return _dataPointRadius || _dotRadius; - } - - function hideDot(dot) { - dot.style('fill-opacity', _dataPointFillOpacity) - .style('stroke-opacity', _dataPointStrokeOpacity) - .attr('r', getDotRadius()); - } - - function hideRefLines(g) { - g.select('path.' + Y_AXIS_REF_LINE_CLASS).style('display', 'none'); - g.select('path.' + X_AXIS_REF_LINE_CLASS).style('display', 'none'); - } - - function renderTitle(dot, d) { - if (_chart.renderTitle()) { - dot.selectAll('title').remove(); - dot.append('title').text(dc.pluck('data', _chart.title(d.name))); - } - } - /** #### .dotRadius([dotRadius]) Get or set the radius (in px) for dots displayed on the data points. Default dot radius is 5. @@ -350,44 +130,117 @@ dc.lineChart = function (parent, chartGroup) { return _chart; }; - /** - #### .renderDataPoints([options]) - Always show individual dots for each datapoint. - - Options, if given, is an object that can contain the following: - - * fillOpacity (default 0.8) - * strokeOpacity (default 0.8) - * radius (default 2) - - If `options` is falsy, it disables data point rendering. + _chart.dataPointRadius = function (_) { + if (!arguments.length) { + return _dataPointRadius; + } + _dataPointRadius = _ || 2; + return _chart; + }; - If no `options` are provided, the current `options` values are instead returned. + _chart.dataPointFillOpacity = function (_) { + if (!arguments.length) { + return _dataPointFillOpacity; + } + _dataPointFillOpacity = _ || 0.8; + return _chart; + }; - Example: - ``` - chart.renderDataPoints({radius: 2, fillOpacity: 0.8, strokeOpacity: 0.8}) - ``` - **/ - _chart.renderDataPoints = function (options) { + _chart.dataPointStrokeOpacity = function (_) { if (!arguments.length) { - return { - fillOpacity: _dataPointFillOpacity, - strokeOpacity: _dataPointStrokeOpacity, - radius: _dataPointRadius - }; - } else if (!options) { - _dataPointFillOpacity = DEFAULT_DOT_OPACITY; - _dataPointStrokeOpacity = DEFAULT_DOT_OPACITY; - _dataPointRadius = null; - } else { - _dataPointFillOpacity = options.fillOpacity || 0.8; - _dataPointStrokeOpacity = options.strokeOpacity || 0.8; - _dataPointRadius = options.radius || 2; + return _dataPointStrokeOpacity; } + _dataPointStrokeOpacity = _ || 0.8; return _chart; }; + _chart.plotData = function () { + var _g = _chart.chartBodyG(), + _data = _chart.layerFn().data, + _x = _chart.x(), + _y = _chart.y(); + _line = d3.svg.line() + .x(function (d) { return _x(d.key); }) + .y(function (d) { return _y(d.values); }); + _lineEnterExit = d3.svg.line() + .x(function (d) { return _x(d.key); }) + .y(function () { return _y(0); }); + _area = d3.svg.area() + .x(function (d) { return _x(d.key); }) + .y(function (d) { return _y(d.values); }) + .y0(function (d) { return _y(d.values0); }); + _areaEnterExit = d3.svg.area() + .x(function (d) { return _x(d.key); }) + .y(function () { return _y(0); }) + .y0(function () { return _y(0); }); + + // Layers + var layers = _g.selectAll('g.layer') + .data(_data, function (datum, i) { return i; }), + layersEnter = layers.enter() + .append('g') + .attr('class', 'layer'), + layersExit = layers.exit(); + + // Lines + layersEnter.append('path') + .attr('class', 'line') + .attr('stroke', _chart.getColor) + .attr('stroke-dasharray', _dashStyle || '') + .attr('d', function (d) { return _safePath(_lineEnterExit(d.values)); }); + dc.transition(layers.select('path.line'), _chart.transitionDuration()) + .attr('stroke', _chart.getColor) + .attr('stroke-dasharray', _dashStyle || '') + .attr('d', function (d) { return _safePath(_line(d.values)); }); + dc.transition(layersExit.select('path.line'), _chart.transitionDuration()) + .attr('d', function (d) { return _safePath(_lineEnterExit(d.values)); }); + + // Area + layersEnter.append('path') + .attr('class', 'area') + .attr('fill', _chart.getColor) + .attr('visible', _chart.renderArea) + .attr('d', function (d) { return _safePath(_areaEnterExit(d.values)); }); + dc.transition(layers.select('path.area'), _chart.transitionDuration()) + .attr('fill', _chart.getColor) + .attr('d', function (d) { return _safePath(_area(d.values)); }); + dc.transition(layersExit.select('path.area'), _chart.transitionDuration()) + .attr('d', function (d) { return _safePath(_areaEnterExit(d.values)); }); + + // Points + var dots = layers.selectAll('circle') + .data(function (d) { return d.values; }); + dots.enter() + .append('circle') + .attr('cx', function (d) { return dc.utils.safeNumber(_x(d.key)); }) + .attr('cy', function () { return dc.utils.safeNumber(_y(0)); }) + .on('mousemove', function () { + d3.select(this) + .attr('r', _dotRadius) + .style('fill-opacity', 0.8) + .style('stroke-opacity', 0.8); + }) + .on('mouseout', function () { + d3.select(this) + .attr('r', _dataPointRadius) + .style('fill-opacity', _dataPointFillOpacity) + .style('stroke-opacity', _dataPointStrokeOpacity); + }) + .append('title').text(_chart.title()); + dc.transition(dots, _chart.transitionDuration()) + .attr('cx', function (d) { return dc.utils.safeNumber(_x(d.key)); }) + .attr('cy', function (d) { return dc.utils.safeNumber(_y(d.values)); }) + .attr('r', _dataPointRadius) + .attr('fill', function () { return _chart.getColor(this.parentNode.__data__); }) + .style('fill-opacity', _dataPointFillOpacity) + .style('stroke-opacity', _dataPointStrokeOpacity); + dc.transition(dots.exit(), _chart.transitionDuration()) + .attr('cx', function (d) { return dc.utils.safeNumber(_x(d.key)); }) + .attr('cy', function () { return dc.utils.safeNumber(_y(0)); }) + .remove(); + dc.transition(layersExit, _chart.transitionDuration()).remove(); + }; + function colorFilter(color, dashstyle, inv) { return function () { var item = d3.select(this); @@ -425,3 +278,102 @@ dc.lineChart = function (parent, chartGroup) { return _chart.anchor(parent, chartGroup); }; + +dc.lineChart.layerFn = { + standard: dc.layerMixin.layerFunctor(function (chart, data) { + var xAxisExtent = d3.extent(data, chart.keyAccessor()), + yAxisExtent = d3.extent(data, chart.valueAccessor()); + data = dc.layerMixin.dataFn.layer(chart, data); + data.forEach(function (datum) { + datum.values.forEach(function (keyDatum) { + keyDatum.values0 = 0; + }); + }); + return { + data: data, + xAxisMin: xAxisExtent[0] || 0, + xAxisMax: xAxisExtent[1] || 0, + yAxisMin: yAxisExtent[0] || 0, + yAxisMax: yAxisExtent[1] || 0 + }; + }), + stack: dc.layerMixin.layerFunctor(function (chart, data) { + var xAxisExtent = d3.extent(data, chart.keyAccessor()); + data = dc.layerMixin.dataFn.layer(chart, data); + var keyMap = {}, + yAxisMax = Number.NEGATIVE_INFINITY; + data.forEach(function (datum) { + datum.values.forEach(function (keyDatum) { + var key = keyDatum.key; + keyDatum.values0 = keyMap[key] = keyMap[key] || 0; + keyDatum.values = keyMap[key] += keyDatum.values; + yAxisMax = Math.max(yAxisMax, keyDatum.values); + }); + }); + return { + data: data, + xAxisMin: xAxisExtent[0] || 0, + xAxisMax: xAxisExtent[1] || 0, + yAxisMin: 0, + yAxisMax: yAxisMax === Number.NEGATIVE_INFINITY ? 0 : yAxisMax + }; + }), + stack100: dc.layerMixin.layerFunctor(function (chart, data) { + var keyMap = {}, + xAxisExtent = d3.extent(data, chart.keyAccessor()); + data = dc.layerMixin.dataFn.layer(chart, data); + data.forEach(function (datum) { + datum.values.forEach(function (keyDatum) { + var key = keyDatum.key; + keyDatum.values0 = keyMap[key] = keyMap[key] || 0; + keyDatum.values = keyMap[key] += keyDatum.values; + }); + }); + data.forEach(function (datum) { + datum.values.forEach(function (keyDatum) { + var keyTotal = keyMap[keyDatum.key]; + keyDatum.values0 /= keyTotal; + keyDatum.values /= keyTotal; + }); + }); + return { + data: data, + xAxisMin: xAxisExtent[0] || 0, + xAxisMax: xAxisExtent[1] || 0, + yAxisMin: 0, + yAxisMax: 1 + }; + }), + area: dc.layerMixin.layerFunctor(function (chart, data) { + var xAxisExtent = d3.extent(data, chart.keyAccessor()), + yAxisMin = Number.POSITIVE_INFINITY, + yAxisMax = Number.NEGATIVE_INFINITY; + data = dc.layerMixin.dataFn.key(chart, data); + data = data.reduce(function (previous, datum) { + var key = datum.key, + values = datum.values, + extent = d3.extent(values, dc.pluck('values')), + mean = d3.mean(values, dc.pluck('values')), + median = d3.median(values, dc.pluck('values')); + yAxisMin = Math.min(extent[0], yAxisMin); + yAxisMax = Math.max(extent[1], yAxisMax); + previous[0].values.push({key: key, values0: extent[0] || 0, values: extent[1] || 0}); + previous[1].values.push({key: key, values0: extent[1] || 0, values: extent[0] || 0}); + previous[2].values.push({key: key, values0: null, values: mean || 0}); + previous[3].values.push({key: key, values0: null, values: median || 0}); + return previous; + }, [ + {key: 'max', values: []}, + {key: 'min', values: []}, + {key: 'mean', values: []}, + {key: 'mean', values: []} + ]); + return { + data: data, + xAxisMin: xAxisExtent[0] || 0, + xAxisMax: xAxisExtent[1] || 0, + yAxisMin: yAxisMin === Number.POSITIVE_INFINITY ? 0 : yAxisMin, + yAxisMax: yAxisMax === Number.NEGATIVE_INFINITY ? 0 : yAxisMax + }; + }) +}; \ No newline at end of file From a38bb1fe2d372ea30e931c0869b5a8cd1950bc5c Mon Sep 17 00:00:00 2001 From: Alan Kavanagh Date: Mon, 10 Aug 2015 11:52:38 +0100 Subject: [PATCH 2/5] bring in current version of bar and line to implement new behaviour --- src/bar-chart.js | 345 +++++++++++++++++----------------------------- src/line-chart.js | 302 ++++++++++++---------------------------- 2 files changed, 219 insertions(+), 428 deletions(-) diff --git a/src/bar-chart.js b/src/bar-chart.js index c5d5992bb..ca0226d4a 100644 --- a/src/bar-chart.js +++ b/src/bar-chart.js @@ -1,3 +1,38 @@ +/** +## Bar Chart +Includes: [Stack Mixin](#stack Mixin), [Coordinate Grid Mixin](#coordinate-grid-mixin) + +Concrete bar chart/histogram implementation. + +Examples: + +* [Nasdaq 100 Index](http://dc-js.github.com/dc.js/) +* [Canadian City Crime Stats](http://dc-js.github.com/dc.js/crime/index.html) +#### dc.barChart(parent[, chartGroup]) +Create a bar chart instance and attach it to the given parent element. + +Parameters: +* parent : string | node | selection | compositeChart - any valid + [d3 single selector](https://github.com/mbostock/d3/wiki/Selections#selecting-elements) 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 [Composite Chart](#composite-chart) then pass in the parent composite + chart instance. +* chartGroup : string (optional) - 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: +A newly created bar chart instance + +```js +// 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); +``` + +**/ dc.barChart = function (parent, chartGroup) { var MIN_BAR_WIDTH = 1; var DEFAULT_GAP_BETWEEN_BARS = 2; @@ -100,33 +135,72 @@ dc.barChart = function (parent, chartGroup) { .remove(); } - var _chart = dc.layerMixin(dc.coordinateGridMixin({})); + 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().rangeBand()); + } 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 () { + var bars = _chart.chartBodyG().selectAll('rect.bar'); + var extent = _chart.brush().extent(); - var _alwaysUseRounding = false, - _centerBar = false, - _gap = DEFAULT_GAP_BETWEEN_BARS; + 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.brushIsEmpty(extent)) { + var start = extent[0]; + var end = extent[1]; + + bars.classed(dc.constants.DESELECTED_CLASS, function (d) { + return d.x < start || d.x >= end; + }); + } else { + bars.classed(dc.constants.DESELECTED_CLASS, false); + } + } + }; /** - #### .alwaysUseRounding([boolean]) - Set or get whether rounding is enabled when bars are centered. Default: false. 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 centered. + #### .centerBar(boolean) + Whether the bar chart will render each bar centered around the data position on x axis. Default: false - 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. - ```js - chart.round(function(n) {return Math.floor(n)+0.5}); - ``` **/ - _chart.alwaysUseRounding = function (_) { + _chart.centerBar = function (_) { if (!arguments.length) { - return _alwaysUseRounding; + return _centerBar; } - _alwaysUseRounding = _; + _centerBar = _; return _chart; }; + dc.override(_chart, 'onClick', function (d) { + _chart._onClick(d.data); + }); + /** #### .barPadding([padding]) Get or set the spacing between bars as a fraction of bar size. Valid values are between 0-1. @@ -143,17 +217,18 @@ dc.barChart = function (parent, chartGroup) { return _chart; }; + _chart._useOuterPadding = function () { + return _gap === undefined; + }; + /** - #### .centerBar(boolean) - Whether the bar chart will render each bar centered around the data position on x axis. Default: false + #### .outerPadding([padding]) + 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. + + Default: 0.5 **/ - _chart.centerBar = function (_) { - if (!arguments.length) { - return _centerBar; - } - _centerBar = _; - return _chart; - }; + _chart.outerPadding = _chart._outerRangeBandPadding; /** #### .gap(gapBetweenBars) @@ -170,73 +245,44 @@ dc.barChart = function (parent, chartGroup) { return _chart; }; - _chart._useOuterPadding = function () { - return _gap === undefined; - }; - - /** - #### .outerPadding([padding]) - 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. - - Default: 0.5 - **/ - _chart.outerPadding = _chart._outerRangeBandPadding; - - _chart.layerFunctor(dc.barChart.layerFn.standard); + _chart.extendBrush = function () { + var extent = _chart.brush().extent(); + if (_chart.round() && (!_centerBar || _alwaysUseRounding)) { + extent[0] = extent.map(_chart.round())[0]; + extent[1] = extent.map(_chart.round())[1]; - _chart.barWidth = function () { - var numberOfBars = _chart.xUnitCount(), - barWidth = MIN_BAR_WIDTH; - if (_chart.isOrdinal() && _gap === undefined) { - barWidth = Math.floor(_chart.x().rangeBand()); - } 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.chartBodyG().select('.brush') + .call(_chart.brush().extent(extent)); } - return barWidth; - }; - - _chart.plotData = function () { - var g = _chart.chartBodyG(), - data = _chart.layerFn().data; - var bars = g.selectAll('rect.bar') - .data(data, function (datum) { - return datum.key + datum.layer || ''; - }); - - bars.enter() - .append('rect') - .attr('class', 'bar') - .attr('fill', _chart.getColor) - .attr('height', 0); - - _chart.layerFn().render(_chart, dc.transition(bars, _chart.transitionDuration())); - - dc.transition(bars.exit(), _chart.transitionDuration()) - .attr('height', 0) - .remove(); + return extent; }; - _chart.fadeDeselectedArea = function () {}; + /** + #### .alwaysUseRounding([boolean]) + Set or get whether rounding is enabled when bars are centered. Default: false. 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 centered. - _chart.extendBrush = function () { - var extent = _chart.brush().extent(); - if (_chart.round() && (!_centerBar || _alwaysUseRounding)) { - _chart.chartBodyG().select('.brush') - .call(_chart.brush().extent(extent.map(_chart.round()))); + 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. + ```js + chart.round(function(n) {return Math.floor(n)+0.5}); + ``` + **/ + _chart.alwaysUseRounding = function (_) { + if (!arguments.length) { + return _alwaysUseRounding; } - return extent; + _alwaysUseRounding = _; + return _chart; }; function colorFilter(color, inv) { return function () { - var match = d3.select(this).attr('fill') === color; + var item = d3.select(this); + var match = item.attr('fill') === color; return inv ? !match : match; }; } @@ -266,138 +312,3 @@ dc.barChart = function (parent, chartGroup) { return _chart.anchor(parent, chartGroup); }; - -dc.barChart.layerFn = { - standard: dc.layerMixin.layerFunctor(function (chart, data) { - data = dc.layerMixin.dataFn.standard(chart, data); - var xAxisExtent = d3.extent(data, dc.pluck('key')), - yAxisExtent = d3.extent(data, dc.pluck('values')); - return { - data: data, - xAxisMin: xAxisExtent[0] || 0, - xAxisMax: xAxisExtent[1] || 0, - yAxisMin: yAxisExtent[0] || 0, - yAxisMax: yAxisExtent[1] || 0, - render: function (chart, g) { - var _x = chart.x(), - _y = chart.y(), - bWidth = chart.barWidth(), - cHeight = chart.height(); - g.attr('x', function (d) { return _x(d.key); }) - .attr('y', function (d) { return _y(d.values); }) - .attr('width', bWidth) - .attr('height', function (d) { return cHeight - _y(d.values); }); - } - }; - }), - // {key: 'a', values:[{key: 'x', values: 1}, {key: 'y', values: 2}]} - stack: dc.layerMixin.layerFunctor(function (chart, data) { - data = dc.layerMixin.dataFn.key(chart, data); - var xAxisExtent = d3.extent(data, dc.pluck('key')); - var yAxisMax = data.reduce(function (extent, datum) { - var sum = d3.sum(datum.values, dc.pluck('values')); - return Math.max(extent, sum); - }, 0); - data = data.reduce(function (prev, datum) { - var key = datum.key; - return prev.concat(datum.values.reduce(function (previous, layerDatum) { - previous[0].push({ - key: key, - layer: layerDatum.key, - values0: previous[1], - values1: previous[1] += layerDatum.values - }); - return previous; - }, [[], 0])[0]); - }, []); - return { - data: data, - xAxisMin: xAxisExtent[0] || 0, - xAxisMax: xAxisExtent[1] || 0, - yAxisMin: 0, - yAxisMax: yAxisMax, - render: function (chart, g) { - var _x = chart.x(), - _y = chart.y(), - bWidth = chart.barWidth(); - g.attr('y', function (d) { return _y(d.values1); }) - .attr('height', function (d) { return _y(d.values0) - _y(d.values1); }) - .transition() - .attr('x', function (d) { return _x(d.key); }) - .attr('width', bWidth); - } - }; - }), - stack100: dc.layerMixin.layerFunctor(function (chart, data) { - data = dc.layerMixin.dataFn.key(chart, data); - var xAxisExtent = d3.extent(data, dc.pluck('key')); - data = data.reduce(function (prev, datum) { - var key = datum.key, - total = d3.sum(datum.values, dc.pluck('values')); - return prev.concat(datum.values.reduce(function (previous, layerDatum) { - previous[0].push({ - key: key, - layer: layerDatum.key, - values0: previous[1], - values1: previous[1] += (layerDatum.values / total) - }); - return previous; - }, [[], 0])[0]); - }, []); - return { - data: data, - xAxisMin: xAxisExtent[0] || 0, - xAxisMax: xAxisExtent[1] || 0, - yAxisMin: 0, - yAxisMax: 1, - render: function (chart, g) { - var _x = chart.x(), - _y = chart.y(), - bWidth = chart.barWidth(); - g.attr('y', function (d) { return _y(d.values1); }) - .attr('height', function (d) { return _y(d.values0) - _y(d.values1); }) - .transition() - .attr('x', function (d) { return _x(d.key); }) - .attr('width', bWidth); - } - }; - }), - group: dc.layerMixin.layerFunctor(function (chart, data) { - var standardData = dc.layerMixin.dataFn.standard(chart, data), - xAxisExtent = d3.extent(standardData, dc.pluck('key')), - yAxisExtent = d3.extent(data, dc.pluck('value')); - data = dc.layerMixin.dataFn.key(chart, data); - var totalLayers = d3.max(data, function (datum) { - return datum.values.length; - }); - data = data.reduce(function (previous, datum) { - var key = datum.key; - return previous.concat(datum.values.map(function (layerDatum, i) { - return { - key: key, - layer: layerDatum.key, - values: layerDatum.values, - index: i - }; - })); - }, []); - return { - data: data, - xAxisMin: xAxisExtent[0] || 0, - xAxisMax: xAxisExtent[1] || 0, - yAxisMin: yAxisExtent[0] || 0, - yAxisMax: yAxisExtent[1] || 0, - render: function (chart, g) { - var _x = chart.x(), - _y = chart.y(), - bWidth = chart.barWidth() / totalLayers, - cHeight = chart.height(); - g.attr('x', function (d) { return _x(d.key) + bWidth * d.index; }) - .attr('width', bWidth) - .transition() - .attr('y', function (d) { return _y(d.values); }) - .attr('height', function (d) { return cHeight - _y(d.values); }); - } - }; - }) -}; diff --git a/src/line-chart.js b/src/line-chart.js index cb92882ab..1c1965171 100644 --- a/src/line-chart.js +++ b/src/line-chart.js @@ -1,3 +1,39 @@ +/** +## Line Chart +Includes [Stack Mixin](#stack-mixin), [Coordinate Grid Mixin](#coordinate-grid-mixin) + +Concrete line/area chart implementation. + +Examples: +* [Nasdaq 100 Index](http://dc-js.github.com/dc.js/) +* [Canadian City Crime Stats](http://dc-js.github.com/dc.js/crime/index.html) +#### dc.lineChart(parent[, chartGroup]) +Create a line chart instance and attach it to the given parent element. + +Parameters: + +* parent : string | node | selection | compositeChart - any valid + [d3 single selector](https://github.com/mbostock/d3/wiki/Selections#selecting-elements) specifying + a dom block element such as a div; or a dom element or d3 selection. + If the line chart is a sub-chart in a [Composite Chart](#composite-chart) then pass in the parent composite + chart instance. + +* chartGroup : string (optional) - 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: +A newly created line chart instance + +```js +// create a line chart under #chart-container1 element using the default global chart group +var chart1 = dc.lineChart('#chart-container1'); +// create a line chart under #chart-container2 element using chart group A +var chart2 = dc.lineChart('#chart-container2', 'chartGroupA'); +// create a sub-chart under a composite parent chart +var chart3 = dc.lineChart(compositeChart); +``` + +**/ dc.lineChart = function (parent, chartGroup) { var DEFAULT_DOT_RADIUS = 5; var TOOLTIP_G_CLASS = 'dc-tooltip'; @@ -21,6 +57,30 @@ dc.lineChart = function (parent, chartGroup) { _chart.transitionDuration(500); _chart._rangeBandPadding(1); + _chart.plotData = function () { + var chartBody = _chart.chartBodyG(); + var layersList = chartBody.selectAll('g.stack-list'); + + if (layersList.empty()) { + layersList = chartBody.append('g').attr('class', 'stack-list'); + } + + var layers = layersList.selectAll('g.stack').data(_chart.data()); + + var layersEnter = layers + .enter() + .append('g') + .attr('class', function (d, i) { + return 'stack ' + '_' + i; + }); + + drawLine(layersEnter, layers); + + drawArea(layersEnter, layers); + + drawDots(chartBody, layers); + }; + /** #### .interpolate([value]) Gets or sets the interpolator to use for lines drawn, by string name, allowing e.g. step @@ -34,10 +94,6 @@ dc.lineChart = function (parent, chartGroup) { return _interpolate; } _interpolate = _; - _line.interpolate(_interpolate); - _lineEnterExit.interpolate(_interpolate); - _area.interpolate(_interpolate); - _areaEnterExit.interpolate(_interpolate); return _chart; }; @@ -54,10 +110,6 @@ dc.lineChart = function (parent, chartGroup) { return _tension; } _tension = _; - _line.tension(_tension); - _lineEnterExit.tension(_tension); - _area.tension(_tension); - _areaEnterExit.tension(_tension); return _chart; }; @@ -314,117 +366,44 @@ dc.lineChart = function (parent, chartGroup) { return _chart; }; - _chart.dataPointRadius = function (_) { - if (!arguments.length) { - return _dataPointRadius; - } - _dataPointRadius = _ || 2; - return _chart; - }; + /** + #### .renderDataPoints([options]) + Always show individual dots for each datapoint. - _chart.dataPointFillOpacity = function (_) { - if (!arguments.length) { - return _dataPointFillOpacity; - } - _dataPointFillOpacity = _ || 0.8; - return _chart; - }; + Options, if given, is an object that can contain the following: + + * fillOpacity (default 0.8) + * strokeOpacity (default 0.8) + * radius (default 2) + + If `options` is falsy, it disables data point rendering. + + If no `options` are provided, the current `options` values are instead returned. - _chart.dataPointStrokeOpacity = function (_) { + Example: + ``` + chart.renderDataPoints({radius: 2, fillOpacity: 0.8, strokeOpacity: 0.8}) + ``` + **/ + _chart.renderDataPoints = function (options) { if (!arguments.length) { - return _dataPointStrokeOpacity; + return { + fillOpacity: _dataPointFillOpacity, + strokeOpacity: _dataPointStrokeOpacity, + radius: _dataPointRadius + }; + } else if (!options) { + _dataPointFillOpacity = DEFAULT_DOT_OPACITY; + _dataPointStrokeOpacity = DEFAULT_DOT_OPACITY; + _dataPointRadius = null; + } else { + _dataPointFillOpacity = options.fillOpacity || 0.8; + _dataPointStrokeOpacity = options.strokeOpacity || 0.8; + _dataPointRadius = options.radius || 2; } - _dataPointStrokeOpacity = _ || 0.8; return _chart; }; - _chart.plotData = function () { - var _g = _chart.chartBodyG(), - _data = _chart.layerFn().data, - _x = _chart.x(), - _y = _chart.y(); - _line = d3.svg.line() - .x(function (d) { return _x(d.key); }) - .y(function (d) { return _y(d.values); }); - _lineEnterExit = d3.svg.line() - .x(function (d) { return _x(d.key); }) - .y(function () { return _y(0); }); - _area = d3.svg.area() - .x(function (d) { return _x(d.key); }) - .y(function (d) { return _y(d.values); }) - .y0(function (d) { return _y(d.values0); }); - _areaEnterExit = d3.svg.area() - .x(function (d) { return _x(d.key); }) - .y(function () { return _y(0); }) - .y0(function () { return _y(0); }); - - // Layers - var layers = _g.selectAll('g.layer') - .data(_data, function (datum, i) { return i; }), - layersEnter = layers.enter() - .append('g') - .attr('class', 'layer'), - layersExit = layers.exit(); - - // Lines - layersEnter.append('path') - .attr('class', 'line') - .attr('stroke', _chart.getColor) - .attr('stroke-dasharray', _dashStyle || '') - .attr('d', function (d) { return _safePath(_lineEnterExit(d.values)); }); - dc.transition(layers.select('path.line'), _chart.transitionDuration()) - .attr('stroke', _chart.getColor) - .attr('stroke-dasharray', _dashStyle || '') - .attr('d', function (d) { return _safePath(_line(d.values)); }); - dc.transition(layersExit.select('path.line'), _chart.transitionDuration()) - .attr('d', function (d) { return _safePath(_lineEnterExit(d.values)); }); - - // Area - layersEnter.append('path') - .attr('class', 'area') - .attr('fill', _chart.getColor) - .attr('visible', _chart.renderArea) - .attr('d', function (d) { return _safePath(_areaEnterExit(d.values)); }); - dc.transition(layers.select('path.area'), _chart.transitionDuration()) - .attr('fill', _chart.getColor) - .attr('d', function (d) { return _safePath(_area(d.values)); }); - dc.transition(layersExit.select('path.area'), _chart.transitionDuration()) - .attr('d', function (d) { return _safePath(_areaEnterExit(d.values)); }); - - // Points - var dots = layers.selectAll('circle') - .data(function (d) { return d.values; }); - dots.enter() - .append('circle') - .attr('cx', function (d) { return dc.utils.safeNumber(_x(d.key)); }) - .attr('cy', function () { return dc.utils.safeNumber(_y(0)); }) - .on('mousemove', function () { - d3.select(this) - .attr('r', _dotRadius) - .style('fill-opacity', 0.8) - .style('stroke-opacity', 0.8); - }) - .on('mouseout', function () { - d3.select(this) - .attr('r', _dataPointRadius) - .style('fill-opacity', _dataPointFillOpacity) - .style('stroke-opacity', _dataPointStrokeOpacity); - }) - .append('title').text(_chart.title()); - dc.transition(dots, _chart.transitionDuration()) - .attr('cx', function (d) { return dc.utils.safeNumber(_x(d.key)); }) - .attr('cy', function (d) { return dc.utils.safeNumber(_y(d.values)); }) - .attr('r', _dataPointRadius) - .attr('fill', function () { return _chart.getColor(this.parentNode.__data__); }) - .style('fill-opacity', _dataPointFillOpacity) - .style('stroke-opacity', _dataPointStrokeOpacity); - dc.transition(dots.exit(), _chart.transitionDuration()) - .attr('cx', function (d) { return dc.utils.safeNumber(_x(d.key)); }) - .attr('cy', function () { return dc.utils.safeNumber(_y(0)); }) - .remove(); - dc.transition(layersExit, _chart.transitionDuration()).remove(); - }; - function colorFilter(color, dashstyle, inv) { return function () { var item = d3.select(this); @@ -462,102 +441,3 @@ dc.lineChart = function (parent, chartGroup) { return _chart.anchor(parent, chartGroup); }; - -dc.lineChart.layerFn = { - standard: dc.layerMixin.layerFunctor(function (chart, data) { - var xAxisExtent = d3.extent(data, chart.keyAccessor()), - yAxisExtent = d3.extent(data, chart.valueAccessor()); - data = dc.layerMixin.dataFn.layer(chart, data); - data.forEach(function (datum) { - datum.values.forEach(function (keyDatum) { - keyDatum.values0 = 0; - }); - }); - return { - data: data, - xAxisMin: xAxisExtent[0] || 0, - xAxisMax: xAxisExtent[1] || 0, - yAxisMin: yAxisExtent[0] || 0, - yAxisMax: yAxisExtent[1] || 0 - }; - }), - stack: dc.layerMixin.layerFunctor(function (chart, data) { - var xAxisExtent = d3.extent(data, chart.keyAccessor()); - data = dc.layerMixin.dataFn.layer(chart, data); - var keyMap = {}, - yAxisMax = Number.NEGATIVE_INFINITY; - data.forEach(function (datum) { - datum.values.forEach(function (keyDatum) { - var key = keyDatum.key; - keyDatum.values0 = keyMap[key] = keyMap[key] || 0; - keyDatum.values = keyMap[key] += keyDatum.values; - yAxisMax = Math.max(yAxisMax, keyDatum.values); - }); - }); - return { - data: data, - xAxisMin: xAxisExtent[0] || 0, - xAxisMax: xAxisExtent[1] || 0, - yAxisMin: 0, - yAxisMax: yAxisMax === Number.NEGATIVE_INFINITY ? 0 : yAxisMax - }; - }), - stack100: dc.layerMixin.layerFunctor(function (chart, data) { - var keyMap = {}, - xAxisExtent = d3.extent(data, chart.keyAccessor()); - data = dc.layerMixin.dataFn.layer(chart, data); - data.forEach(function (datum) { - datum.values.forEach(function (keyDatum) { - var key = keyDatum.key; - keyDatum.values0 = keyMap[key] = keyMap[key] || 0; - keyDatum.values = keyMap[key] += keyDatum.values; - }); - }); - data.forEach(function (datum) { - datum.values.forEach(function (keyDatum) { - var keyTotal = keyMap[keyDatum.key]; - keyDatum.values0 /= keyTotal; - keyDatum.values /= keyTotal; - }); - }); - return { - data: data, - xAxisMin: xAxisExtent[0] || 0, - xAxisMax: xAxisExtent[1] || 0, - yAxisMin: 0, - yAxisMax: 1 - }; - }), - area: dc.layerMixin.layerFunctor(function (chart, data) { - var xAxisExtent = d3.extent(data, chart.keyAccessor()), - yAxisMin = Number.POSITIVE_INFINITY, - yAxisMax = Number.NEGATIVE_INFINITY; - data = dc.layerMixin.dataFn.key(chart, data); - data = data.reduce(function (previous, datum) { - var key = datum.key, - values = datum.values, - extent = d3.extent(values, dc.pluck('values')), - mean = d3.mean(values, dc.pluck('values')), - median = d3.median(values, dc.pluck('values')); - yAxisMin = Math.min(extent[0], yAxisMin); - yAxisMax = Math.max(extent[1], yAxisMax); - previous[0].values.push({key: key, values0: extent[0] || 0, values: extent[1] || 0}); - previous[1].values.push({key: key, values0: extent[1] || 0, values: extent[0] || 0}); - previous[2].values.push({key: key, values0: null, values: mean || 0}); - previous[3].values.push({key: key, values0: null, values: median || 0}); - return previous; - }, [ - {key: 'max', values: []}, - {key: 'min', values: []}, - {key: 'mean', values: []}, - {key: 'mean', values: []} - ]); - return { - data: data, - xAxisMin: xAxisExtent[0] || 0, - xAxisMax: xAxisExtent[1] || 0, - yAxisMin: yAxisMin === Number.POSITIVE_INFINITY ? 0 : yAxisMin, - yAxisMax: yAxisMax === Number.NEGATIVE_INFINITY ? 0 : yAxisMax - }; - }) -}; From 18514fb34cd792aaf6f1d1551aaab526600b872e Mon Sep 17 00:00:00 2001 From: Alan Kavanagh Date: Mon, 10 Aug 2015 11:59:41 +0100 Subject: [PATCH 3/5] bar chart has not changed much, can go off mtraynhams source --- src/bar-chart.js | 447 +++++++++++++++++++++++------------------------ 1 file changed, 219 insertions(+), 228 deletions(-) diff --git a/src/bar-chart.js b/src/bar-chart.js index ca0226d4a..f00ff9219 100644 --- a/src/bar-chart.js +++ b/src/bar-chart.js @@ -1,206 +1,34 @@ -/** -## Bar Chart -Includes: [Stack Mixin](#stack Mixin), [Coordinate Grid Mixin](#coordinate-grid-mixin) - -Concrete bar chart/histogram implementation. - -Examples: - -* [Nasdaq 100 Index](http://dc-js.github.com/dc.js/) -* [Canadian City Crime Stats](http://dc-js.github.com/dc.js/crime/index.html) -#### dc.barChart(parent[, chartGroup]) -Create a bar chart instance and attach it to the given parent element. - -Parameters: -* parent : string | node | selection | compositeChart - any valid - [d3 single selector](https://github.com/mbostock/d3/wiki/Selections#selecting-elements) 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 [Composite Chart](#composite-chart) then pass in the parent composite - chart instance. -* chartGroup : string (optional) - 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: -A newly created bar chart instance - -```js -// 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); -``` - -**/ dc.barChart = function (parent, chartGroup) { - var MIN_BAR_WIDTH = 1; - var DEFAULT_GAP_BETWEEN_BARS = 2; - - 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.plotData = function () { - var layers = _chart.chartBodyG().selectAll('g.stack') - .data(_chart.data()); - - calculateBarWidth(); - - layers - .enter() - .append('g') - .attr('class', function (d, i) { - return 'stack ' + '_' + i; - }); - - layers.each(function (d, i) { - var layer = d3.select(this); - - renderBars(layer, i, d); - }); - }; - - function barHeight(d) { - return dc.utils.safeNumber(Math.abs(_chart.y()(d.y + d.y0) - _chart.y()(d.y0))); - } - - 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('y', _chart.yAxisHeight()) - .attr('height', 0); - - if (_chart.renderTitle()) { - enter.append('title').text(dc.pluck('data', _chart.title(d.name))); - } - - if (_chart.isOrdinal()) { - bars.on('click', _chart.onClick); - } - - dc.transition(bars, _chart.transitionDuration()) - .attr('x', function (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); - }) - .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()) - .attr('height', 0) - .remove(); - } + var DEFAULT_GAP_BETWEEN_BARS = 2, + MIN_BAR_WIDTH = 1; - function calculateBarWidth() { - if (_barWidth === undefined) { - var numberOfBars = _chart.xUnitCount(); + var _chart = dc.layerMixin(dc.coordinateGridMixin({})); - // please can't we always use rangeBands for bar charts? - if (_chart.isOrdinal() && _gap === undefined) { - _barWidth = Math.floor(_chart.x().rangeBand()); - } 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 () { - var bars = _chart.chartBodyG().selectAll('rect.bar'); - var extent = _chart.brush().extent(); - - 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.brushIsEmpty(extent)) { - var start = extent[0]; - var end = extent[1]; - - bars.classed(dc.constants.DESELECTED_CLASS, function (d) { - return d.x < start || d.x >= end; - }); - } else { - bars.classed(dc.constants.DESELECTED_CLASS, false); - } - } - }; + var _alwaysUseRounding = false, + _centerBar = false, + _gap = DEFAULT_GAP_BETWEEN_BARS; /** - #### .centerBar(boolean) - Whether the bar chart will render each bar centered around the data position on x axis. Default: false + #### .alwaysUseRounding([boolean]) + Set or get whether rounding is enabled when bars are centered. Default: false. 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 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. + ```js + chart.round(function(n) {return Math.floor(n)+0.5}); + ``` **/ - _chart.centerBar = function (_) { + _chart.alwaysUseRounding = function (_) { if (!arguments.length) { - return _centerBar; + return _alwaysUseRounding; } - _centerBar = _; + _alwaysUseRounding = _; return _chart; }; - dc.override(_chart, 'onClick', function (d) { - _chart._onClick(d.data); - }); - /** #### .barPadding([padding]) Get or set the spacing between bars as a fraction of bar size. Valid values are between 0-1. @@ -217,18 +45,17 @@ dc.barChart = function (parent, chartGroup) { return _chart; }; - _chart._useOuterPadding = function () { - return _gap === undefined; - }; - /** - #### .outerPadding([padding]) - 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. - - Default: 0.5 + #### .centerBar(boolean) + Whether the bar chart will render each bar centered around the data position on x axis. Default: false **/ - _chart.outerPadding = _chart._outerRangeBandPadding; + _chart.centerBar = function (_) { + if (!arguments.length) { + return _centerBar; + } + _centerBar = _; + return _chart; + }; /** #### .gap(gapBetweenBars) @@ -245,44 +72,73 @@ dc.barChart = function (parent, chartGroup) { return _chart; }; - _chart.extendBrush = function () { - var extent = _chart.brush().extent(); - if (_chart.round() && (!_centerBar || _alwaysUseRounding)) { - extent[0] = extent.map(_chart.round())[0]; - extent[1] = extent.map(_chart.round())[1]; + _chart._useOuterPadding = function () { + return _gap === undefined; + }; - _chart.chartBodyG().select('.brush') - .call(_chart.brush().extent(extent)); + /** + #### .outerPadding([padding]) + 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. + + Default: 0.5 + **/ + _chart.outerPadding = _chart._outerRangeBandPadding; + + _chart.layerFunctor(dc.barChart.layerFn.standard); + + _chart.barWidth = function () { + var numberOfBars = _chart.xUnitCount(), + barWidth = MIN_BAR_WIDTH; + if (_chart.isOrdinal() && _gap === undefined) { + barWidth = Math.floor(_chart.x().rangeBand()); + } 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; + } + return barWidth; + }; - return extent; + _chart.plotData = function () { + var g = _chart.chartBodyG(), + data = _chart.layerFn().data; + + var bars = g.selectAll('rect.bar') + .data(data, function (datum) { + return datum.key + datum.layer || ''; + }); + + bars.enter() + .append('rect') + .attr('class', 'bar') + .attr('fill', _chart.getColor) + .attr('height', 0); + + _chart.layerFn().render(_chart, dc.transition(bars, _chart.transitionDuration())); + + dc.transition(bars.exit(), _chart.transitionDuration()) + .attr('height', 0) + .remove(); }; - /** - #### .alwaysUseRounding([boolean]) - Set or get whether rounding is enabled when bars are centered. Default: false. 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 centered. + _chart.fadeDeselectedArea = function () {}; - 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. - ```js - chart.round(function(n) {return Math.floor(n)+0.5}); - ``` - **/ - _chart.alwaysUseRounding = function (_) { - if (!arguments.length) { - return _alwaysUseRounding; + _chart.extendBrush = function () { + var extent = _chart.brush().extent(); + if (_chart.round() && (!_centerBar || _alwaysUseRounding)) { + _chart.chartBodyG().select('.brush') + .call(_chart.brush().extent(extent.map(_chart.round()))); } - _alwaysUseRounding = _; - return _chart; + return extent; }; function colorFilter(color, inv) { return function () { - var item = d3.select(this); - var match = item.attr('fill') === color; + var match = d3.select(this).attr('fill') === color; return inv ? !match : match; }; } @@ -312,3 +168,138 @@ dc.barChart = function (parent, chartGroup) { return _chart.anchor(parent, chartGroup); }; + +dc.barChart.layerFn = { + standard: dc.layerMixin.layerFunctor(function (chart, data) { + data = dc.layerMixin.dataFn.standard(chart, data); + var xAxisExtent = d3.extent(data, dc.pluck('key')), + yAxisExtent = d3.extent(data, dc.pluck('values')); + return { + data: data, + xAxisMin: xAxisExtent[0] || 0, + xAxisMax: xAxisExtent[1] || 0, + yAxisMin: yAxisExtent[0] || 0, + yAxisMax: yAxisExtent[1] || 0, + render: function (chart, g) { + var _x = chart.x(), + _y = chart.y(), + bWidth = chart.barWidth(), + cHeight = chart.height(); + g.attr('x', function (d) { return _x(d.key); }) + .attr('y', function (d) { return _y(d.values); }) + .attr('width', bWidth) + .attr('height', function (d) { return cHeight - _y(d.values); }); + } + }; + }), + // {key: 'a', values:[{key: 'x', values: 1}, {key: 'y', values: 2}]} + stack: dc.layerMixin.layerFunctor(function (chart, data) { + data = dc.layerMixin.dataFn.key(chart, data); + var xAxisExtent = d3.extent(data, dc.pluck('key')); + var yAxisMax = data.reduce(function (extent, datum) { + var sum = d3.sum(datum.values, dc.pluck('values')); + return Math.max(extent, sum); + }, 0); + data = data.reduce(function (prev, datum) { + var key = datum.key; + return prev.concat(datum.values.reduce(function (previous, layerDatum) { + previous[0].push({ + key: key, + layer: layerDatum.key, + values0: previous[1], + values1: previous[1] += layerDatum.values + }); + return previous; + }, [[], 0])[0]); + }, []); + return { + data: data, + xAxisMin: xAxisExtent[0] || 0, + xAxisMax: xAxisExtent[1] || 0, + yAxisMin: 0, + yAxisMax: yAxisMax, + render: function (chart, g) { + var _x = chart.x(), + _y = chart.y(), + bWidth = chart.barWidth(); + g.attr('y', function (d) { return _y(d.values1); }) + .attr('height', function (d) { return _y(d.values0) - _y(d.values1); }) + .transition() + .attr('x', function (d) { return _x(d.key); }) + .attr('width', bWidth); + } + }; + }), + stack100: dc.layerMixin.layerFunctor(function (chart, data) { + data = dc.layerMixin.dataFn.key(chart, data); + var xAxisExtent = d3.extent(data, dc.pluck('key')); + data = data.reduce(function (prev, datum) { + var key = datum.key, + total = d3.sum(datum.values, dc.pluck('values')); + return prev.concat(datum.values.reduce(function (previous, layerDatum) { + previous[0].push({ + key: key, + layer: layerDatum.key, + values0: previous[1], + values1: previous[1] += (layerDatum.values / total) + }); + return previous; + }, [[], 0])[0]); + }, []); + return { + data: data, + xAxisMin: xAxisExtent[0] || 0, + xAxisMax: xAxisExtent[1] || 0, + yAxisMin: 0, + yAxisMax: 1, + render: function (chart, g) { + var _x = chart.x(), + _y = chart.y(), + bWidth = chart.barWidth(); + g.attr('y', function (d) { return _y(d.values1); }) + .attr('height', function (d) { return _y(d.values0) - _y(d.values1); }) + .transition() + .attr('x', function (d) { return _x(d.key); }) + .attr('width', bWidth); + } + }; + }), + group: dc.layerMixin.layerFunctor(function (chart, data) { + var standardData = dc.layerMixin.dataFn.standard(chart, data), + xAxisExtent = d3.extent(standardData, dc.pluck('key')), + yAxisExtent = d3.extent(data, dc.pluck('value')); + data = dc.layerMixin.dataFn.key(chart, data); + var totalLayers = d3.max(data, function (datum) { + return datum.values.length; + }); + data = data.reduce(function (previous, datum) { + var key = datum.key; + return previous.concat(datum.values.map(function (layerDatum, i) { + return { + key: key, + layer: layerDatum.key, + values: layerDatum.values, + index: i + }; + })); + }, []); + return { + data: data, + xAxisMin: xAxisExtent[0] || 0, + xAxisMax: xAxisExtent[1] || 0, + yAxisMin: yAxisExtent[0] || 0, + yAxisMax: yAxisExtent[1] || 0, + render: function (chart, g) { + var _x = chart.x(), + _y = chart.y(), + bWidth = chart.barWidth() / totalLayers, + cHeight = chart.height(); + g.attr('x', function (d) { return _x(d.key) + bWidth * d.index; }) + .attr('width', bWidth) + .transition() + .attr('y', function (d) { return _y(d.values); }) + .attr('height', function (d) { return cHeight - _y(d.values); }); + } + }; + }) +}; \ No newline at end of file From 71984a3bb400c79ec2f330cd88b8b0928a75b599 Mon Sep 17 00:00:00 2001 From: Alan Kavanagh Date: Tue, 11 Aug 2015 10:54:19 +0100 Subject: [PATCH 4/5] added layer-mixin.js to Grunt file --- Gruntfile.js | 1 + 1 file changed, 1 insertion(+) diff --git a/Gruntfile.js b/Gruntfile.js index 36ea97942..c97ec4049 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -398,6 +398,7 @@ module.exports.jsFiles = [ 'src/color-mixin.js', 'src/coordinate-grid-mixin.js', 'src/stack-mixin.js', + 'src/layer-mixin.js', 'src/cap-mixin.js', 'src/bubble-mixin.js', 'src/pie-chart.js', From ef2fdf17e788010eb9c5a921d2638c1ee615732e Mon Sep 17 00:00:00 2001 From: Alan Kavanagh Date: Tue, 11 Aug 2015 12:34:39 +0100 Subject: [PATCH 5/5] include current build of dc.js --- dc.js | 562 ++++++++++++++++++++++++++++++++++------------------------ 1 file changed, 334 insertions(+), 228 deletions(-) diff --git a/dc.js b/dc.js index fcd321ad0..2f94ce16c 100644 --- a/dc.js +++ b/dc.js @@ -3458,6 +3458,122 @@ dc.stackMixin = function (_chart) { return _chart; }; +dc.layerMixin = function (_chart) { + var _layerAccessor, + _layerFunctor, + _layerFn; + + _chart.colorAccessor(function (d) { + return d.layer || null; + }); + + _chart.layerAccessor = function (_) { + if (!arguments.length) { + return _layerAccessor; + } + _layerAccessor = _; + return _chart; + }; + + _chart.layerFunctor = function (_) { + if (!arguments.length) { + return _layerFunctor; + } + _layerFunctor = _; + _chart.expireCache(); + return _chart; + }; + + _chart.layerFn = function (_) { + if (!arguments.length) { + return _layerFn; + } + _layerFn = _; + return _chart; + }; + + _chart._preprocessData = function () { + _chart.layerFn(_chart.layerFunctor()(_chart, _chart.data())); + }; + + _chart.xAxisMax = function () { + return dc.utils.add(_chart.layerFn().xAxisMax(), _chart.xAxisPadding()); + }; + + _chart.xAxisMin = function () { + return dc.utils.subtract(_chart.layerFn().xAxisMin(), _chart.xAxisPadding()); + }; + + _chart.yAxisMax = function () { + return dc.utils.add(_chart.layerFn().yAxisMax(), _chart.yAxisPadding()); + }; + + _chart.yAxisMin = function () { + return dc.utils.subtract(_chart.layerFn().yAxisMin(), _chart.yAxisPadding()); + }; + + _chart._ordinalXDomain = function () { + return _chart.layerFn().input().map(dc.pluck('key')); + }; + return _chart; +}; + +dc.layerMixin.dataFn = { + standard: function (chart, data) { + return d3.nest() + .key(chart.keyAccessor()) + .rollup(function (datums) { + return d3.sum(datums, chart.valueAccessor()); + }).entries(data); + }, + key: function (chart, data) { + return d3.nest() + .key(chart.keyAccessor()) + .key(chart.layerAccessor() || function () { return 'all'; }) + .sortKeys(d3.ascending) + .rollup(function (datums) { + return d3.sum(datums, chart.valueAccessor()); + }).entries(data); + }, + layer: function (chart, data) { + return d3.nest() + .key(chart.layerAccessor() || function () { return 'all'; }) + .key(chart.keyAccessor()) + .sortKeys(d3.ascending) + .rollup(function (datums) { + return d3.sum(datums, chart.valueAccessor()); + }).entries(data); + } +}; + +dc.layerMixin.layerFunctor = function (layerFn) { + return function (chart, input) { + var output = layerFn(chart, input); + return { + input: function () { + return input; + }, + data: function () { + return output.data; + }, + xAxisMax: function () { + return output.xAxisMax; + }, + xAxisMin: function () { + return output.xAxisMin; + }, + yAxisMax: function () { + return output.yAxisMax; + }, + yAxisMin: function () { + return output.yAxisMin; + }, + render: function (chart, g) { + return output.render(chart, g); + } + }; + }; +}; /** ## Cap Mixin Cap is a mixin that groups small data elements below a _cap_ into an *others* grouping for both the @@ -4253,209 +4369,37 @@ dc.pieChart = function (parent, chartGroup) { return _chart.anchor(parent, chartGroup); }; -/** -## Bar Chart -Includes: [Stack Mixin](#stack Mixin), [Coordinate Grid Mixin](#coordinate-grid-mixin) - -Concrete bar chart/histogram implementation. - -Examples: - -* [Nasdaq 100 Index](http://dc-js.github.com/dc.js/) -* [Canadian City Crime Stats](http://dc-js.github.com/dc.js/crime/index.html) -#### dc.barChart(parent[, chartGroup]) -Create a bar chart instance and attach it to the given parent element. - -Parameters: -* parent : string | node | selection | compositeChart - any valid - [d3 single selector](https://github.com/mbostock/d3/wiki/Selections#selecting-elements) 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 [Composite Chart](#composite-chart) then pass in the parent composite - chart instance. -* chartGroup : string (optional) - 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: -A newly created bar chart instance - -```js -// 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); -``` - -**/ dc.barChart = function (parent, chartGroup) { - var MIN_BAR_WIDTH = 1; - var DEFAULT_GAP_BETWEEN_BARS = 2; - - 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; - }); + var DEFAULT_GAP_BETWEEN_BARS = 2, + MIN_BAR_WIDTH = 1; - 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.plotData = function () { - var layers = _chart.chartBodyG().selectAll('g.stack') - .data(_chart.data()); - - calculateBarWidth(); - - layers - .enter() - .append('g') - .attr('class', function (d, i) { - return 'stack ' + '_' + i; - }); - - layers.each(function (d, i) { - var layer = d3.select(this); - - renderBars(layer, i, d); - }); - }; + var _chart = dc.layerMixin(dc.coordinateGridMixin({})); - function barHeight(d) { - return dc.utils.safeNumber(Math.abs(_chart.y()(d.y + d.y0) - _chart.y()(d.y0))); - } - - 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('y', _chart.yAxisHeight()) - .attr('height', 0); - - if (_chart.renderTitle()) { - enter.append('title').text(dc.pluck('data', _chart.title(d.name))); - } - - if (_chart.isOrdinal()) { - bars.on('click', _chart.onClick); - } - - dc.transition(bars, _chart.transitionDuration()) - .attr('x', function (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); - }) - .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()) - .attr('height', 0) - .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().rangeBand()); - } 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 () { - var bars = _chart.chartBodyG().selectAll('rect.bar'); - var extent = _chart.brush().extent(); - - 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.brushIsEmpty(extent)) { - var start = extent[0]; - var end = extent[1]; - - bars.classed(dc.constants.DESELECTED_CLASS, function (d) { - return d.x < start || d.x >= end; - }); - } else { - bars.classed(dc.constants.DESELECTED_CLASS, false); - } - } - }; + var _alwaysUseRounding = false, + _centerBar = false, + _gap = DEFAULT_GAP_BETWEEN_BARS; /** - #### .centerBar(boolean) - Whether the bar chart will render each bar centered around the data position on x axis. Default: false + #### .alwaysUseRounding([boolean]) + Set or get whether rounding is enabled when bars are centered. Default: false. 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 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. + ```js + chart.round(function(n) {return Math.floor(n)+0.5}); + ``` **/ - _chart.centerBar = function (_) { + _chart.alwaysUseRounding = function (_) { if (!arguments.length) { - return _centerBar; + return _alwaysUseRounding; } - _centerBar = _; + _alwaysUseRounding = _; return _chart; }; - dc.override(_chart, 'onClick', function (d) { - _chart._onClick(d.data); - }); - /** #### .barPadding([padding]) Get or set the spacing between bars as a fraction of bar size. Valid values are between 0-1. @@ -4472,18 +4416,17 @@ dc.barChart = function (parent, chartGroup) { return _chart; }; - _chart._useOuterPadding = function () { - return _gap === undefined; - }; - /** - #### .outerPadding([padding]) - 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. - - Default: 0.5 + #### .centerBar(boolean) + Whether the bar chart will render each bar centered around the data position on x axis. Default: false **/ - _chart.outerPadding = _chart._outerRangeBandPadding; + _chart.centerBar = function (_) { + if (!arguments.length) { + return _centerBar; + } + _centerBar = _; + return _chart; + }; /** #### .gap(gapBetweenBars) @@ -4500,44 +4443,73 @@ dc.barChart = function (parent, chartGroup) { return _chart; }; - _chart.extendBrush = function () { - var extent = _chart.brush().extent(); - if (_chart.round() && (!_centerBar || _alwaysUseRounding)) { - extent[0] = extent.map(_chart.round())[0]; - extent[1] = extent.map(_chart.round())[1]; + _chart._useOuterPadding = function () { + return _gap === undefined; + }; - _chart.chartBodyG().select('.brush') - .call(_chart.brush().extent(extent)); + /** + #### .outerPadding([padding]) + 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. + + Default: 0.5 + **/ + _chart.outerPadding = _chart._outerRangeBandPadding; + + _chart.layerFunctor(dc.barChart.layerFn.standard); + + _chart.barWidth = function () { + var numberOfBars = _chart.xUnitCount(), + barWidth = MIN_BAR_WIDTH; + if (_chart.isOrdinal() && _gap === undefined) { + barWidth = Math.floor(_chart.x().rangeBand()); + } 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; + } + return barWidth; + }; - return extent; + _chart.plotData = function () { + var g = _chart.chartBodyG(), + data = _chart.layerFn().data; + + var bars = g.selectAll('rect.bar') + .data(data, function (datum) { + return datum.key + datum.layer || ''; + }); + + bars.enter() + .append('rect') + .attr('class', 'bar') + .attr('fill', _chart.getColor) + .attr('height', 0); + + _chart.layerFn().render(_chart, dc.transition(bars, _chart.transitionDuration())); + + dc.transition(bars.exit(), _chart.transitionDuration()) + .attr('height', 0) + .remove(); }; - /** - #### .alwaysUseRounding([boolean]) - Set or get whether rounding is enabled when bars are centered. Default: false. 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 centered. + _chart.fadeDeselectedArea = function () {}; - 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. - ```js - chart.round(function(n) {return Math.floor(n)+0.5}); - ``` - **/ - _chart.alwaysUseRounding = function (_) { - if (!arguments.length) { - return _alwaysUseRounding; + _chart.extendBrush = function () { + var extent = _chart.brush().extent(); + if (_chart.round() && (!_centerBar || _alwaysUseRounding)) { + _chart.chartBodyG().select('.brush') + .call(_chart.brush().extent(extent.map(_chart.round()))); } - _alwaysUseRounding = _; - return _chart; + return extent; }; function colorFilter(color, inv) { return function () { - var item = d3.select(this); - var match = item.attr('fill') === color; + var match = d3.select(this).attr('fill') === color; return inv ? !match : match; }; } @@ -4568,6 +4540,140 @@ dc.barChart = function (parent, chartGroup) { return _chart.anchor(parent, chartGroup); }; +dc.barChart.layerFn = { + standard: dc.layerMixin.layerFunctor(function (chart, data) { + data = dc.layerMixin.dataFn.standard(chart, data); + var xAxisExtent = d3.extent(data, dc.pluck('key')), + yAxisExtent = d3.extent(data, dc.pluck('values')); + return { + data: data, + xAxisMin: xAxisExtent[0] || 0, + xAxisMax: xAxisExtent[1] || 0, + yAxisMin: yAxisExtent[0] || 0, + yAxisMax: yAxisExtent[1] || 0, + render: function (chart, g) { + var _x = chart.x(), + _y = chart.y(), + bWidth = chart.barWidth(), + cHeight = chart.height(); + g.attr('x', function (d) { return _x(d.key); }) + .attr('y', function (d) { return _y(d.values); }) + .attr('width', bWidth) + .attr('height', function (d) { return cHeight - _y(d.values); }); + } + }; + }), + // {key: 'a', values:[{key: 'x', values: 1}, {key: 'y', values: 2}]} + stack: dc.layerMixin.layerFunctor(function (chart, data) { + data = dc.layerMixin.dataFn.key(chart, data); + var xAxisExtent = d3.extent(data, dc.pluck('key')); + var yAxisMax = data.reduce(function (extent, datum) { + var sum = d3.sum(datum.values, dc.pluck('values')); + return Math.max(extent, sum); + }, 0); + data = data.reduce(function (prev, datum) { + var key = datum.key; + return prev.concat(datum.values.reduce(function (previous, layerDatum) { + previous[0].push({ + key: key, + layer: layerDatum.key, + values0: previous[1], + values1: previous[1] += layerDatum.values + }); + return previous; + }, [[], 0])[0]); + }, []); + return { + data: data, + xAxisMin: xAxisExtent[0] || 0, + xAxisMax: xAxisExtent[1] || 0, + yAxisMin: 0, + yAxisMax: yAxisMax, + render: function (chart, g) { + var _x = chart.x(), + _y = chart.y(), + bWidth = chart.barWidth(); + g.attr('y', function (d) { return _y(d.values1); }) + .attr('height', function (d) { return _y(d.values0) - _y(d.values1); }) + .transition() + .attr('x', function (d) { return _x(d.key); }) + .attr('width', bWidth); + } + }; + }), + stack100: dc.layerMixin.layerFunctor(function (chart, data) { + data = dc.layerMixin.dataFn.key(chart, data); + var xAxisExtent = d3.extent(data, dc.pluck('key')); + data = data.reduce(function (prev, datum) { + var key = datum.key, + total = d3.sum(datum.values, dc.pluck('values')); + return prev.concat(datum.values.reduce(function (previous, layerDatum) { + previous[0].push({ + key: key, + layer: layerDatum.key, + values0: previous[1], + values1: previous[1] += (layerDatum.values / total) + }); + return previous; + }, [[], 0])[0]); + }, []); + return { + data: data, + xAxisMin: xAxisExtent[0] || 0, + xAxisMax: xAxisExtent[1] || 0, + yAxisMin: 0, + yAxisMax: 1, + render: function (chart, g) { + var _x = chart.x(), + _y = chart.y(), + bWidth = chart.barWidth(); + g.attr('y', function (d) { return _y(d.values1); }) + .attr('height', function (d) { return _y(d.values0) - _y(d.values1); }) + .transition() + .attr('x', function (d) { return _x(d.key); }) + .attr('width', bWidth); + } + }; + }), + group: dc.layerMixin.layerFunctor(function (chart, data) { + var standardData = dc.layerMixin.dataFn.standard(chart, data), + xAxisExtent = d3.extent(standardData, dc.pluck('key')), + yAxisExtent = d3.extent(data, dc.pluck('value')); + data = dc.layerMixin.dataFn.key(chart, data); + var totalLayers = d3.max(data, function (datum) { + return datum.values.length; + }); + data = data.reduce(function (previous, datum) { + var key = datum.key; + return previous.concat(datum.values.map(function (layerDatum, i) { + return { + key: key, + layer: layerDatum.key, + values: layerDatum.values, + index: i + }; + })); + }, []); + return { + data: data, + xAxisMin: xAxisExtent[0] || 0, + xAxisMax: xAxisExtent[1] || 0, + yAxisMin: yAxisExtent[0] || 0, + yAxisMax: yAxisExtent[1] || 0, + render: function (chart, g) { + var _x = chart.x(), + _y = chart.y(), + bWidth = chart.barWidth() / totalLayers, + cHeight = chart.height(); + g.attr('x', function (d) { return _x(d.key) + bWidth * d.index; }) + .attr('width', bWidth) + .transition() + .attr('y', function (d) { return _y(d.values); }) + .attr('height', function (d) { return cHeight - _y(d.values); }); + } + }; + }) +}; /** ## Line Chart Includes [Stack Mixin](#stack-mixin), [Coordinate Grid Mixin](#coordinate-grid-mixin)