From 28ae9e5808743af705f8ee1c1581832ea26fab38 Mon Sep 17 00:00:00 2001 From: Alan Kavanagh Date: Wed, 12 Aug 2015 13:10:32 +0100 Subject: [PATCH 1/5] added basic implementation of grouped bar chart using stacks --- dc.js | 84 ++++++++++++++++++++++++++++++++---------------- src/bar-chart.js | 82 ++++++++++++++++++++++++++++++---------------- 2 files changed, 111 insertions(+), 55 deletions(-) diff --git a/dc.js b/dc.js index b06fd0896f..e08a8ffb1f 100644 --- a/dc.js +++ b/dc.js @@ -4195,9 +4195,11 @@ dc.barChart = function (parent, chartGroup) { var _gap = DEFAULT_GAP_BETWEEN_BARS; var _centerBar = false; var _alwaysUseRounding = false; - + var _barWidth; + _chart.renderType = "stack"; + dc.override(_chart, 'rescale', function () { _chart._rescale(); _barWidth = undefined; @@ -4232,10 +4234,14 @@ dc.barChart = function (parent, chartGroup) { }); }; - function barHeight(d) { + function stackBarHeight(d) { return dc.utils.safeNumber(Math.abs(_chart.y()(d.y + d.y0) - _chart.y()(d.y0))); } + function groupBarHeight(d){ + return dc.utils.safeNumber(Math.abs(_chart.height() - _chart.margins().top - _chart.margins().bottom - chart.y()(d.y))); + } + function renderBars(layer, layerIndex, d) { var bars = layer.selectAll('rect.bar') .data(d.values, dc.pluck('x')); @@ -4254,34 +4260,56 @@ dc.barChart = function (parent, chartGroup) { 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); + + if(_chart.renderType == "stack"){ + 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 -= stackBarHeight(d); + } + + return dc.utils.safeNumber(y); + }) + .attr('width', _barWidth) + .attr('height', function (d) { + return stackBarHeight(d); + }) + .attr('fill', dc.pluck('data', _chart.getColor)) + .select('title').text(dc.pluck('data', _chart.title(d.name))); + + } + else if(chart.renderType == "group"){ + var groups = chart.stack().map(function(d){ return d.name;}); + var groupRange = d3.scale.ordinal().domain(groups).rangePoints([0, _barWidth - _barWidth/2]); + dc.transition(bars, _chart.transitionDuration()) + .attr('x', function(d, i){ + var x = _chart.x()(d.x) + groupRange(d.layer); + 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('width', _barWidth/_chart.stack().length) + .attr('y', function(d) {return _chart.y()(d.y)}) + .attr('height', function(d){ + return groupBarHeight(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(); diff --git a/src/bar-chart.js b/src/bar-chart.js index 4655a83d2f..58223f3a7c 100644 --- a/src/bar-chart.js +++ b/src/bar-chart.js @@ -42,9 +42,11 @@ dc.barChart = function (parent, chartGroup) { var _gap = DEFAULT_GAP_BETWEEN_BARS; var _centerBar = false; var _alwaysUseRounding = false; - + var _barWidth; + _chart.renderType = "stack"; + dc.override(_chart, 'rescale', function () { _chart._rescale(); _barWidth = undefined; @@ -79,10 +81,14 @@ dc.barChart = function (parent, chartGroup) { }); }; - function barHeight(d) { + function stackBarHeight(d) { return dc.utils.safeNumber(Math.abs(_chart.y()(d.y + d.y0) - _chart.y()(d.y0))); } + function groupBarHeight(d){ + return dc.utils.safeNumber(Math.abs(_chart.height() - _chart.margins().top - _chart.margins().bottom - chart.y()(d.y))); + } + function renderBars(layer, layerIndex, d) { var bars = layer.selectAll('rect.bar') .data(d.values, dc.pluck('x')); @@ -101,34 +107,56 @@ dc.barChart = function (parent, chartGroup) { if (_chart.isOrdinal()) { bars.on('click', _chart.onClick); } + + if(_chart.renderType == "stack"){ + 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 -= stackBarHeight(d); + } + + return dc.utils.safeNumber(y); + }) + .attr('width', _barWidth) + .attr('height', function (d) { + return stackBarHeight(d); + }) + .attr('fill', dc.pluck('data', _chart.getColor)) + .select('title').text(dc.pluck('data', _chart.title(d.name))); - 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); + } + else if(chart.renderType == "group"){ + var groups = chart.stack().map(function(d){ return d.name;}); + var groupRange = d3.scale.ordinal().domain(groups).rangePoints([0, _barWidth - _barWidth/2]); + dc.transition(bars, _chart.transitionDuration()) + .attr('x', function(d, i){ + var x = _chart.x()(d.x) + groupRange(d.layer); + if (_centerBar) { + x -= _barWidth / 2; + } + if (_chart.isOrdinal() && _gap !== undefined) { + x += _gap / 2; + } + return dc.utils.safeNumber(x); }) - .attr('width', _barWidth) - .attr('height', function (d) { - return barHeight(d); + .attr('width', _barWidth/_chart.stack().length) + .attr('y', function(d) {return _chart.y()(d.y)}) + .attr('height', function(d){ + return groupBarHeight(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(); From d14127464f32602409dc684cb7159988c83746ef Mon Sep 17 00:00:00 2001 From: Alan Kavanagh Date: Wed, 12 Aug 2015 14:36:42 +0100 Subject: [PATCH 2/5] added getter/setter for renderType of stacked bar chart --- dc.js | 23 ++++++++++++++++------- src/bar-chart.js | 23 ++++++++++++++++------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/dc.js b/dc.js index e08a8ffb1f..59032beb1e 100644 --- a/dc.js +++ b/dc.js @@ -4198,7 +4198,7 @@ dc.barChart = function (parent, chartGroup) { var _barWidth; - _chart.renderType = "stack"; + var _renderType = 'stack'; dc.override(_chart, 'rescale', function () { _chart._rescale(); @@ -4239,7 +4239,8 @@ dc.barChart = function (parent, chartGroup) { } function groupBarHeight(d){ - return dc.utils.safeNumber(Math.abs(_chart.height() - _chart.margins().top - _chart.margins().bottom - chart.y()(d.y))); + var margin = _chart.margins(); + return dc.utils.safeNumber(Math.abs(_chart.height() - margin.top - margin.bottom - _chart.y()(d.y))); } function renderBars(layer, layerIndex, d) { @@ -4261,7 +4262,7 @@ dc.barChart = function (parent, chartGroup) { bars.on('click', _chart.onClick); } - if(_chart.renderType == "stack"){ + if(_renderType === 'stack'){ dc.transition(bars, _chart.transitionDuration()) .attr('x', function (d) { var x = _chart.x()(d.x); @@ -4290,8 +4291,8 @@ dc.barChart = function (parent, chartGroup) { .select('title').text(dc.pluck('data', _chart.title(d.name))); } - else if(chart.renderType == "group"){ - var groups = chart.stack().map(function(d){ return d.name;}); + else if(_renderType === 'group'){ + var groups = _chart.stack().map(function(d){ return d.name;}); var groupRange = d3.scale.ordinal().domain(groups).rangePoints([0, _barWidth - _barWidth/2]); dc.transition(bars, _chart.transitionDuration()) .attr('x', function(d, i){ @@ -4305,10 +4306,10 @@ dc.barChart = function (parent, chartGroup) { return dc.utils.safeNumber(x); }) .attr('width', _barWidth/_chart.stack().length) - .attr('y', function(d) {return _chart.y()(d.y)}) + .attr('y', function(d) {return _chart.y()(d.y);}) .attr('height', function(d){ return groupBarHeight(d); - }) + }); } dc.transition(bars.exit(), _chart.transitionDuration()) .attr('height', 0) @@ -4481,6 +4482,14 @@ dc.barChart = function (parent, chartGroup) { .classed('fadeout', false); }; + _chart.renderType = function(_rendtype){ + if(!arguments.length){ + return _renderType; + } + _renderType = _rendtype; + return _chart; + }; + dc.override(_chart, 'xAxisMax', function () { var max = this._xAxisMax(); if ('resolution' in _chart.xUnits()) { diff --git a/src/bar-chart.js b/src/bar-chart.js index 58223f3a7c..71859d0289 100644 --- a/src/bar-chart.js +++ b/src/bar-chart.js @@ -45,7 +45,7 @@ dc.barChart = function (parent, chartGroup) { var _barWidth; - _chart.renderType = "stack"; + var _renderType = 'stack'; dc.override(_chart, 'rescale', function () { _chart._rescale(); @@ -86,7 +86,8 @@ dc.barChart = function (parent, chartGroup) { } function groupBarHeight(d){ - return dc.utils.safeNumber(Math.abs(_chart.height() - _chart.margins().top - _chart.margins().bottom - chart.y()(d.y))); + var margin = _chart.margins(); + return dc.utils.safeNumber(Math.abs(_chart.height() - margin.top - margin.bottom - _chart.y()(d.y))); } function renderBars(layer, layerIndex, d) { @@ -108,7 +109,7 @@ dc.barChart = function (parent, chartGroup) { bars.on('click', _chart.onClick); } - if(_chart.renderType == "stack"){ + if(_renderType === 'stack'){ dc.transition(bars, _chart.transitionDuration()) .attr('x', function (d) { var x = _chart.x()(d.x); @@ -137,8 +138,8 @@ dc.barChart = function (parent, chartGroup) { .select('title').text(dc.pluck('data', _chart.title(d.name))); } - else if(chart.renderType == "group"){ - var groups = chart.stack().map(function(d){ return d.name;}); + else if(_renderType === 'group'){ + var groups = _chart.stack().map(function(d){ return d.name;}); var groupRange = d3.scale.ordinal().domain(groups).rangePoints([0, _barWidth - _barWidth/2]); dc.transition(bars, _chart.transitionDuration()) .attr('x', function(d, i){ @@ -152,10 +153,10 @@ dc.barChart = function (parent, chartGroup) { return dc.utils.safeNumber(x); }) .attr('width', _barWidth/_chart.stack().length) - .attr('y', function(d) {return _chart.y()(d.y)}) + .attr('y', function(d) {return _chart.y()(d.y);}) .attr('height', function(d){ return groupBarHeight(d); - }) + }); } dc.transition(bars.exit(), _chart.transitionDuration()) .attr('height', 0) @@ -328,6 +329,14 @@ dc.barChart = function (parent, chartGroup) { .classed('fadeout', false); }; + _chart.renderType = function(_rendtype){ + if(!arguments.length){ + return _renderType; + } + _renderType = _rendtype; + return _chart; + }; + dc.override(_chart, 'xAxisMax', function () { var max = this._xAxisMax(); if ('resolution' in _chart.xUnits()) { From 8851531a8723baa48df31c522f40c283630f2641 Mon Sep 17 00:00:00 2001 From: Alan Kavanagh Date: Wed, 12 Aug 2015 15:18:59 +0100 Subject: [PATCH 3/5] override yAxisMax so it changes for group renderType, duplicate flattenStack from stackMixin. Consider exposing flatten stack as dc.util or on chart object? --- dc.js | 20 +++++++++++++++++++- src/bar-chart.js | 20 +++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/dc.js b/dc.js index 59032beb1e..ec543ff5e1 100644 --- a/dc.js +++ b/dc.js @@ -4305,7 +4305,7 @@ dc.barChart = function (parent, chartGroup) { } return dc.utils.safeNumber(x); }) - .attr('width', _barWidth/_chart.stack().length) + .attr('width', (_barWidth/_chart.stack().length) - 3) .attr('y', function(d) {return _chart.y()(d.y);}) .attr('height', function(d){ return groupBarHeight(d); @@ -4499,6 +4499,24 @@ dc.barChart = function (parent, chartGroup) { return max; }); + dc.override(_chart, 'yAxisMax', function(){ + var max; + if(_renderType === 'stack'){ + max = d3.max(flattenStack(), function(p){ + return p.y + p.y0; + }); + } + else if(_renderType === 'group'){ + max = d3.max(flattenStack(), dc.pluck('y')); + } + return max; + }); + + // Not dry but need flattenStack here and don't want to expose it, possible move to util + function flattenStack() { + var valueses = _chart.data().map(function (layer) { return layer.values; }); + return Array.prototype.concat.apply([], valueses); + } return _chart.anchor(parent, chartGroup); }; diff --git a/src/bar-chart.js b/src/bar-chart.js index 71859d0289..c6c715fd49 100644 --- a/src/bar-chart.js +++ b/src/bar-chart.js @@ -152,7 +152,7 @@ dc.barChart = function (parent, chartGroup) { } return dc.utils.safeNumber(x); }) - .attr('width', _barWidth/_chart.stack().length) + .attr('width', (_barWidth/_chart.stack().length) - 3) .attr('y', function(d) {return _chart.y()(d.y);}) .attr('height', function(d){ return groupBarHeight(d); @@ -346,5 +346,23 @@ dc.barChart = function (parent, chartGroup) { return max; }); + dc.override(_chart, 'yAxisMax', function(){ + var max; + if(_renderType === 'stack'){ + max = d3.max(flattenStack(), function(p){ + return p.y + p.y0; + }); + } + else if(_renderType === 'group'){ + max = d3.max(flattenStack(), dc.pluck('y')); + } + return max; + }); + + // Not dry but need flattenStack here and don't want to expose it, possible move to util + function flattenStack() { + var valueses = _chart.data().map(function (layer) { return layer.values; }); + return Array.prototype.concat.apply([], valueses); + } return _chart.anchor(parent, chartGroup); }; From d97aa5ad7a012e53e1fa1e7a97560708ca6e469b Mon Sep 17 00:00:00 2001 From: Alan Kavanagh Date: Wed, 12 Aug 2015 15:46:51 +0100 Subject: [PATCH 4/5] fixed issue with negative heights on group bar charts, return dc.util.add instead of raw value in overrider yAxisMax function --- dc.js | 11 +++++++++-- src/bar-chart.js | 11 +++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/dc.js b/dc.js index ec543ff5e1..c4ab03c9b6 100644 --- a/dc.js +++ b/dc.js @@ -4306,7 +4306,14 @@ dc.barChart = function (parent, chartGroup) { return dc.utils.safeNumber(x); }) .attr('width', (_barWidth/_chart.stack().length) - 3) - .attr('y', function(d) {return _chart.y()(d.y);}) + .attr('y', function(d) { + var y = _chart.y()(d.y); + + if (d.y < 0) { + y -= groupBarHeight(d); + } + return dc.utils.safeNumber(y); + }) .attr('height', function(d){ return groupBarHeight(d); }); @@ -4509,7 +4516,7 @@ dc.barChart = function (parent, chartGroup) { else if(_renderType === 'group'){ max = d3.max(flattenStack(), dc.pluck('y')); } - return max; + return dc.utils.add(max, _chart.yAxisPadding()); }); // Not dry but need flattenStack here and don't want to expose it, possible move to util diff --git a/src/bar-chart.js b/src/bar-chart.js index c6c715fd49..b6975f569b 100644 --- a/src/bar-chart.js +++ b/src/bar-chart.js @@ -153,7 +153,14 @@ dc.barChart = function (parent, chartGroup) { return dc.utils.safeNumber(x); }) .attr('width', (_barWidth/_chart.stack().length) - 3) - .attr('y', function(d) {return _chart.y()(d.y);}) + .attr('y', function(d) { + var y = _chart.y()(d.y); + + if (d.y < 0) { + y -= groupBarHeight(d); + } + return dc.utils.safeNumber(y); + }) .attr('height', function(d){ return groupBarHeight(d); }); @@ -356,7 +363,7 @@ dc.barChart = function (parent, chartGroup) { else if(_renderType === 'group'){ max = d3.max(flattenStack(), dc.pluck('y')); } - return max; + return dc.utils.add(max, _chart.yAxisPadding()); }); // Not dry but need flattenStack here and don't want to expose it, possible move to util From df43aace3c2b8167edb08d756b49806e50ba97fe Mon Sep 17 00:00:00 2001 From: Alan Kavanagh Date: Tue, 1 Sep 2015 11:11:09 +0100 Subject: [PATCH 5/5] add 'overlay render type' --- dc.js | 27 ++++++++++++++++++++++++++- src/bar-chart.js | 27 ++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/dc.js b/dc.js index c4ab03c9b6..75d9f2c52e 100644 --- a/dc.js +++ b/dc.js @@ -4318,6 +4318,31 @@ dc.barChart = function (parent, chartGroup) { return groupBarHeight(d); }); } + else if(_renderType === 'overlap'){ + dc.transition(bars, _chart.transitionDuration()) + .attr('x', function(d, i){ + 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); + + if (d.y < 0) { + y -= groupBarHeight(d); + } + return dc.utils.safeNumber(y); + }) + .attr('width', _barWidth) + .attr('height', function(d){ + return groupBarHeight(d); + }); + } dc.transition(bars.exit(), _chart.transitionDuration()) .attr('height', 0) .remove(); @@ -4513,7 +4538,7 @@ dc.barChart = function (parent, chartGroup) { return p.y + p.y0; }); } - else if(_renderType === 'group'){ + else if(_renderType === 'group' || _renderType === 'overlap'){ max = d3.max(flattenStack(), dc.pluck('y')); } return dc.utils.add(max, _chart.yAxisPadding()); diff --git a/src/bar-chart.js b/src/bar-chart.js index b6975f569b..e1a9df0043 100644 --- a/src/bar-chart.js +++ b/src/bar-chart.js @@ -165,6 +165,31 @@ dc.barChart = function (parent, chartGroup) { return groupBarHeight(d); }); } + else if(_renderType === 'overlap'){ + dc.transition(bars, _chart.transitionDuration()) + .attr('x', function(d, i){ + 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); + + if (d.y < 0) { + y -= groupBarHeight(d); + } + return dc.utils.safeNumber(y); + }) + .attr('width', _barWidth) + .attr('height', function(d){ + return groupBarHeight(d); + }); + } dc.transition(bars.exit(), _chart.transitionDuration()) .attr('height', 0) .remove(); @@ -360,7 +385,7 @@ dc.barChart = function (parent, chartGroup) { return p.y + p.y0; }); } - else if(_renderType === 'group'){ + else if(_renderType === 'group' || _renderType === 'overlap'){ max = d3.max(flattenStack(), dc.pluck('y')); } return dc.utils.add(max, _chart.yAxisPadding());