diff --git a/package.json b/package.json index f7453c3b5..2ee6ca795 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dc", - "version": "3.0.6", + "version": "3.0.6-grouped-or-stacked", "license": "Apache-2.0", "copyright": "2017", "description": "A multi-dimensional charting library built to work natively with crossfilter and rendered using d3.js ", diff --git a/spec/bar-chart-spec.js b/spec/bar-chart-spec.js old mode 100644 new mode 100755 index ba7a3f9e5..4eb8560cb --- a/spec/bar-chart-spec.js +++ b/spec/bar-chart-spec.js @@ -77,17 +77,17 @@ describe('dc.barChart', function () { }); it('should generate labels with positions corresponding to their data', function () { - expect(nthStack(0).nthLabel(0).attr('x')).toBeWithinDelta(405, 1); - expect(nthStack(0).nthLabel(0).attr('y')).toBeWithinDelta(104, 1); - expect(nthStack(0).nthLabel(0).text()).toBe('1'); + expect(nthBarGroup(0).nthLabel(0).attr('x')).toBeWithinDelta(405, 1); + expect(nthBarGroup(0).nthLabel(0).attr('y')).toBeWithinDelta(104, 1); + expect(nthBarGroup(0).nthLabel(0).text()).toBe('1'); - expect(nthStack(0).nthLabel(3).attr('x')).toBeWithinDelta(509, 1); - expect(nthStack(0).nthLabel(3).attr('y')).toBeWithinDelta(104, 1); - expect(nthStack(0).nthLabel(3).text()).toBe('1'); + expect(nthBarGroup(3).nthLabel(0).attr('x')).toBeWithinDelta(509, 1); + expect(nthBarGroup(3).nthLabel(0).attr('y')).toBeWithinDelta(104, 1); + expect(nthBarGroup(3).nthLabel(0).text()).toBe('1'); - expect(nthStack(0).nthLabel(5).attr('x')).toBeWithinDelta(620, 1); - expect(nthStack(0).nthLabel(5).attr('y')).toBeWithinDelta(50, 1); - expect(nthStack(0).nthLabel(5).text()).toBe('2'); + expect(nthBarGroup(5).nthLabel(0).attr('x')).toBeWithinDelta(620, 1); + expect(nthBarGroup(5).nthLabel(0).attr('y')).toBeWithinDelta(50, 1); + expect(nthBarGroup(5).nthLabel(0).text()).toBe('2'); }); }); @@ -150,17 +150,17 @@ describe('dc.barChart', function () { return d3.select(chart.selectAll('g.y text').nodes()[n]); } it('should generate bars with positions corresponding to their data', function () { - expect(nthStack(0).nthBar(0).attr('x')).toBeWithinDelta(58, 1); - expect(nthStack(0).nthBar(0).attr('y')).toBeWithinDelta(84, 1); - expect(nthStack(0).nthBar(0).attr('height')).toBeWithinDelta(30, 1); + expect(nthBarGroup(0).nthBar(0).attr('x')).toBeWithinDelta(58, 1); + expect(nthBarGroup(0).nthBar(0).attr('y')).toBeWithinDelta(84, 1); + expect(nthBarGroup(0).nthBar(0).attr('height')).toBeWithinDelta(30, 1); - expect(nthStack(0).nthBar(3).attr('x')).toBeWithinDelta(492, 1); - expect(nthStack(0).nthBar(3).attr('y')).toBeWithinDelta(84, 1); - expect(nthStack(0).nthBar(3).attr('height')).toBeWithinDelta(23, 1); + expect(nthBarGroup(3).nthBar(0).attr('x')).toBeWithinDelta(492, 1); + expect(nthBarGroup(3).nthBar(0).attr('y')).toBeWithinDelta(84, 1); + expect(nthBarGroup(3).nthBar(0).attr('height')).toBeWithinDelta(23, 1); - expect(nthStack(0).nthBar(5).attr('x')).toBeWithinDelta(961, 1); - expect(nthStack(0).nthBar(5).attr('y')).toBeWithinDelta(61, 1); - expect(nthStack(0).nthBar(5).attr('height')).toBeWithinDelta(23, 1); + expect(nthBarGroup(5).nthBar(0).attr('x')).toBeWithinDelta(961, 1); + expect(nthBarGroup(5).nthBar(0).attr('y')).toBeWithinDelta(61, 1); + expect(nthBarGroup(5).nthBar(0).attr('height')).toBeWithinDelta(23, 1); }); it('should generate the y-axis domain dynamically', function () { @@ -203,16 +203,16 @@ describe('dc.barChart', function () { }); it('should position the bar based on the ordinal range', function () { - expect(nthStack(0).nthBar(0).attr('x')).toBeWithinDelta(16, 1); - expect(nthStack(0).nthBar(3).attr('x')).toBeWithinDelta(674, 1); - expect(nthStack(0).nthBar(5).attr('x')).toBeWithinDelta(509, 1); + expect(nthBarGroup(0).nthBar(0).attr('x')).toBeWithinDelta(16, 1); + expect(nthBarGroup(3).nthBar(0).attr('x')).toBeWithinDelta(674, 1); + expect(nthBarGroup(5).nthBar(0).attr('x')).toBeWithinDelta(509, 1); }); it('should fade deselected bars', function () { chart.filter('Ontario').filter('Colorado').redraw(); - expect(nthStack(0).nthBar(0).classed('deselected')).toBeTruthy(); - expect(nthStack(0).nthBar(1).classed('deselected')).toBeFalsy(); - expect(nthStack(0).nthBar(5).classed('deselected')).toBeFalsy(); + expect(nthBarGroup(0).nthBar(0).classed('deselected')).toBeTruthy(); + expect(nthBarGroup(1).nthBar(0).classed('deselected')).toBeFalsy(); + expect(nthBarGroup(5).nthBar(0).classed('deselected')).toBeFalsy(); expect(stateDimension.top(Infinity).length).toBe(3); }); @@ -220,9 +220,9 @@ describe('dc.barChart', function () { // Note that bar chart works differently from pie chart. The bar objects (the // actual DOM nodes) don't get reordered by the custom ordering, but they are // placed so that they are drawn in the order specified. - var ontarioXPos = nthStack(0).nthBar(5).attr('x'); - var mississippiXPos = nthStack(0).nthBar(3).attr('x'); - var oklahomaXPos = nthStack(0).nthBar(4).attr('x'); + var ontarioXPos = nthBarGroup(5).nthBar(0).attr('x'); + var mississippiXPos = nthBarGroup(3).nthBar(0).attr('x'); + var oklahomaXPos = nthBarGroup(4).nthBar(0).attr('x'); expect(ontarioXPos).toBeLessThan(mississippiXPos); expect(mississippiXPos).toBeLessThan(oklahomaXPos); @@ -264,9 +264,9 @@ describe('dc.barChart', function () { }); it('should position bars based on ordinal range', function () { - expect(nthStack(0).nthBar(0).attr('height')).toBe('1600'); - expect(nthStack(0).nthBar(1).attr('height')).toBe('1600'); - expect(nthStack(0).nthBar(2).attr('height')).toBe('1600'); + expect(nthBarGroup(0).nthBar(0).attr('height')).toBe('1600'); + expect(nthBarGroup(1).nthBar(0).attr('height')).toBe('1600'); + expect(nthBarGroup(2).nthBar(0).attr('height')).toBe('1600'); }); }); @@ -274,7 +274,7 @@ describe('dc.barChart', function () { it('causes other dimension to be filtered', function () { expect(dimension.top(Infinity).length).toEqual(10); // fake a click - var abar = chart.selectAll('rect.bar:nth-child(3)'); + var abar = chart.selectAll('g.bar-group:nth-child(3) rect.bar'); abar.on('click')(abar.datum()); expect(dimension.top(Infinity).length).toEqual(1); }); @@ -320,9 +320,9 @@ describe('dc.barChart', function () { expect(dc.logger.warn).toHaveBeenCalled(); expect(typeof chart.x().bandwidth).toEqual('function'); - expect(nthStack(0).nthBar(0).attr('x')).toBeWithinDelta(16, 1); - expect(nthStack(0).nthBar(3).attr('x')).toBeWithinDelta(674, 1); - expect(nthStack(0).nthBar(5).attr('x')).toBeWithinDelta(509, 1); + expect(nthBarGroup(0).nthBar(0).attr('x')).toBeWithinDelta(16, 1); + expect(nthBarGroup(3).nthBar(0).attr('x')).toBeWithinDelta(674, 1); + expect(nthBarGroup(5).nthBar(0).attr('x')).toBeWithinDelta(509, 1); }); }); @@ -351,9 +351,9 @@ describe('dc.barChart', function () { }); it('should position bars based on linear range', function () { - expect(nthStack(0).nthBar(0).attr('x')).toBeWithinDelta(40, 1); - expect(nthStack(0).nthBar(2).attr('x')).toBeWithinDelta(489, 1); - expect(nthStack(0).nthBar(4).attr('x')).toBeWithinDelta(938, 1); + expect(nthBarGroup(0).nthBar(0).attr('x')).toBeWithinDelta(40, 1); + expect(nthBarGroup(2).nthBar(0).attr('x')).toBeWithinDelta(489, 1); + expect(nthBarGroup(4).nthBar(0).attr('x')).toBeWithinDelta(938, 1); }); describe('with a custom click handler', function () { @@ -366,7 +366,7 @@ describe('dc.barChart', function () { }); it('clicking causes another dimension to be filtered', function () { expect(dimension.top(Infinity).length).toEqual(10); - var abar = chart.selectAll('rect.bar:nth-child(3)'); + var abar = chart.selectAll('g.bar-group:nth-child(3) rect.bar'); abar.on('click')(abar.datum()); expect(dimension.top(Infinity).length).toEqual(3); }); @@ -396,35 +396,60 @@ describe('dc.barChart', function () { expect(chart.y().domain()).toEqual([0, 152]); }); - it('should generate each stack using its associated group', function () { - expect(nthStack(0).selectAll('rect.bar').size()).toBe(6); - expect(nthStack(1).selectAll('rect.bar').size()).toBe(6); - expect(nthStack(2).selectAll('rect.bar').size()).toBe(6); + it('should generate the correct number of bar groups', function () { + expect(chart.selectAll('g.bar-group').size()).toBe(6); }); - it('should render the correct number of stacks', function () { - expect(chart.selectAll('.stack').size()).toBe(3); + it('should render the correct number of bars in each bar group', function () { + expect(nthBarGroup(0).selectAll('rect.bar').size()).toBe(3); + expect(nthBarGroup(1).selectAll('rect.bar').size()).toBe(3); + expect(nthBarGroup(2).selectAll('rect.bar').size()).toBe(3); + expect(nthBarGroup(3).selectAll('rect.bar').size()).toBe(3); + expect(nthBarGroup(4).selectAll('rect.bar').size()).toBe(3); + expect(nthBarGroup(5).selectAll('rect.bar').size()).toBe(3); }); - it('should display one label for each stack', function () { - expect(chart.selectAll('text.barLabel').size()).toBe(6); + it('should generate one label for each bar in bar group', function () { + expect(nthBarGroup(0).selectAll('text.barLabel').size()).toBe(3); + expect(nthBarGroup(1).selectAll('text.barLabel').size()).toBe(3); + expect(nthBarGroup(2).selectAll('text.barLabel').size()).toBe(3); + expect(nthBarGroup(3).selectAll('text.barLabel').size()).toBe(3); + expect(nthBarGroup(4).selectAll('text.barLabel').size()).toBe(3); + expect(nthBarGroup(5).selectAll('text.barLabel').size()).toBe(3); }); - it('should generate labels with total value of stack', function () { - expect(nthStack(2).nthLabel(0).text()).toBe('48'); - expect(nthStack(2).nthLabel(3).text()).toBe('51'); - expect(nthStack(2).nthLabel(5).text()).toBe('92'); + it('should generate empty labels for the lower bars in each bar group', function () { + expect(nthBarGroup(0).nthLabel(0).text()).toBe(''); + expect(nthBarGroup(0).nthLabel(1).text()).toBe(''); + expect(nthBarGroup(1).nthLabel(0).text()).toBe(''); + expect(nthBarGroup(1).nthLabel(1).text()).toBe(''); + expect(nthBarGroup(2).nthLabel(0).text()).toBe(''); + expect(nthBarGroup(2).nthLabel(1).text()).toBe(''); + expect(nthBarGroup(3).nthLabel(0).text()).toBe(''); + expect(nthBarGroup(3).nthLabel(1).text()).toBe(''); + expect(nthBarGroup(4).nthLabel(0).text()).toBe(''); + expect(nthBarGroup(4).nthLabel(1).text()).toBe(''); + expect(nthBarGroup(5).nthLabel(0).text()).toBe(''); + expect(nthBarGroup(5).nthLabel(1).text()).toBe(''); + + }); + + it('should generate labels with total value of stack on the top bar of each bar group', function () { + expect(nthBarGroup(0).nthLabel(2).text()).toBe('48'); + expect(nthBarGroup(3).nthLabel(2).text()).toBe('51'); + expect(nthBarGroup(5).nthLabel(2).text()).toBe('92'); }); it('should stack the bars', function () { - expect(+nthStack(0).nthBar(2).attr('y')).toBe(142); - expect(+nthStack(0).nthBar(4).attr('y')).toBe(144); + expect(+nthBarGroup(2).nthBar(0).attr('y')).toBe(142); + expect(+nthBarGroup(4).nthBar(0).attr('y')).toBe(144); - expect(+nthStack(1).nthBar(2).attr('y')).toBe(3); - expect(+nthStack(1).nthBar(4).attr('y')).toBe(86); + expect(+nthBarGroup(2).nthBar(1).attr('y')).toBe(3); + expect(+nthBarGroup(4).nthBar(1).attr('y')).toBe(86); + + expect(+nthBarGroup(2).nthBar(2).attr('y')).toBe(0); + expect(+nthBarGroup(4).nthBar(2).attr('y')).toBe(83); - expect(+nthStack(2).nthBar(2).attr('y')).toBe(0); - expect(+nthStack(2).nthBar(4).attr('y')).toBe(83); }); it('should have its own title accessor', function () { @@ -476,14 +501,14 @@ describe('dc.barChart', function () { }); it('should hide the stack', function () { - expect(nthStack(0).nthBar(0).attr('height')).toBe('52'); - expect(nthStack(0).nthBar(1).attr('height')).toBe('78'); + expect(nthBarGroup(0).nthBar(0).attr('height')).toBe('52'); + expect(nthBarGroup(1).nthBar(0).attr('height')).toBe('78'); }); it('should show the stack', function () { chart.showStack('stack 0').render(); - expect(nthStack(0).nthBar(0).attr('height')).toBe('1'); - expect(nthStack(0).nthBar(1).attr('height')).toBe('6'); + expect(nthBarGroup(0).nthBar(0).attr('height')).toBe('1'); + expect(nthBarGroup(1).nthBar(0).attr('height')).toBe('6'); }); }); @@ -494,14 +519,14 @@ describe('dc.barChart', function () { }); it('should hide the stack', function () { - expect(nthStack(1).nthBar(0).attr('height')).toBe('24'); - expect(nthStack(1).nthBar(1).attr('height')).toBe('24'); + expect(nthBarGroup(0).nthBar(1).attr('height')).toBe('24'); + expect(nthBarGroup(1).nthBar(1).attr('height')).toBe('24'); }); it('should show the stack', function () { chart.showStack('stack 1').render(); - expect(nthStack(1).nthBar(0).attr('height')).toBe('46'); - expect(nthStack(1).nthBar(1).attr('height')).toBe('70'); + expect(nthBarGroup(0).nthBar(1).attr('height')).toBe('46'); + expect(nthBarGroup(1).nthBar(1).attr('height')).toBe('70'); }); it('should still show the title for a visible stack', function () { @@ -555,31 +580,31 @@ describe('dc.barChart', function () { }); it('should generate negative bars for stack 0', function () { - expect(nthStack(0).nthBar(0).attr('x')).toBeWithinDelta(58, 1); - expect(nthStack(0).nthBar(0).attr('y')).toBeWithinDelta(73, 1); - expect(nthStack(0).nthBar(0).attr('height')).toBeWithinDelta(8, 1); + expect(nthBarGroup(0).nthBar(0).attr('x')).toBeWithinDelta(58, 1); + expect(nthBarGroup(0).nthBar(0).attr('y')).toBeWithinDelta(73, 1); + expect(nthBarGroup(0).nthBar(0).attr('height')).toBeWithinDelta(8, 1); - expect(nthStack(0).nthBar(3).attr('x')).toBeWithinDelta(492, 1); - expect(nthStack(0).nthBar(3).attr('y')).toBeWithinDelta(73, 1); - expect(nthStack(0).nthBar(3).attr('height')).toBeWithinDelta(6, 1); + expect(nthBarGroup(3).nthBar(0).attr('x')).toBeWithinDelta(492, 1); + expect(nthBarGroup(3).nthBar(0).attr('y')).toBeWithinDelta(73, 1); + expect(nthBarGroup(3).nthBar(0).attr('height')).toBeWithinDelta(6, 1); - expect(nthStack(0).nthBar(5).attr('x')).toBeWithinDelta(961, 1); - expect(nthStack(0).nthBar(5).attr('y')).toBeWithinDelta(67, 1); - expect(nthStack(0).nthBar(5).attr('height')).toBeWithinDelta(6, 1); + expect(nthBarGroup(5).nthBar(0).attr('x')).toBeWithinDelta(961, 1); + expect(nthBarGroup(5).nthBar(0).attr('y')).toBeWithinDelta(67, 1); + expect(nthBarGroup(5).nthBar(0).attr('height')).toBeWithinDelta(6, 1); }); it('should generate negative bar for stack 1', function () { - expect(nthStack(1).nthBar(0).attr('x')).toBeWithinDelta(58, 1); - expect(nthStack(1).nthBar(0).attr('y')).toBeWithinDelta(81, 1); - expect(nthStack(1).nthBar(0).attr('height')).toBeWithinDelta(7, 1); + expect(nthBarGroup(0).nthBar(1).attr('x')).toBeWithinDelta(58, 1); + expect(nthBarGroup(0).nthBar(1).attr('y')).toBeWithinDelta(81, 1); + expect(nthBarGroup(0).nthBar(1).attr('height')).toBeWithinDelta(7, 1); - expect(nthStack(1).nthBar(3).attr('x')).toBeWithinDelta(492, 1); - expect(nthStack(1).nthBar(3).attr('y')).toBeWithinDelta(79, 1); - expect(nthStack(1).nthBar(3).attr('height')).toBeWithinDelta(5, 1); + expect(nthBarGroup(3).nthBar(1).attr('x')).toBeWithinDelta(492, 1); + expect(nthBarGroup(3).nthBar(1).attr('y')).toBeWithinDelta(79, 1); + expect(nthBarGroup(3).nthBar(1).attr('height')).toBeWithinDelta(5, 1); - expect(nthStack(1).nthBar(5).attr('x')).toBeWithinDelta(961, 1); - expect(nthStack(1).nthBar(5).attr('y')).toBeWithinDelta(61, 1); - expect(nthStack(1).nthBar(5).attr('height')).toBeWithinDelta(6, 1); + expect(nthBarGroup(5).nthBar(1).attr('x')).toBeWithinDelta(961, 1); + expect(nthBarGroup(5).nthBar(1).attr('y')).toBeWithinDelta(61, 1); + expect(nthBarGroup(5).nthBar(1).attr('height')).toBeWithinDelta(6, 1); }); it('should generate y axis domain dynamically', function () { @@ -786,13 +811,13 @@ describe('dc.barChart', function () { }); it('should push unselected bars to the background', function () { - expect(nthStack(0).nthBar(0).classed('deselected')).toBeTruthy(); - expect(nthStack(0).nthBar(1).classed('deselected')).toBeFalsy(); - expect(nthStack(0).nthBar(3).classed('deselected')).toBeTruthy(); + expect(nthBarGroup(0).nthBar(0).classed('deselected')).toBeTruthy(); + expect(nthBarGroup(1).nthBar(0).classed('deselected')).toBeFalsy(); + expect(nthBarGroup(3).nthBar(0).classed('deselected')).toBeTruthy(); }); it('should push the selected bars to the foreground', function () { - expect(nthStack(0).nthBar(1).classed('deselected')).toBeFalsy(); + expect(nthBarGroup(1).nthBar(0).classed('deselected')).toBeFalsy(); }); describe('after reset', function () { @@ -888,8 +913,8 @@ describe('dc.barChart', function () { }); it('should not overlap bars', function () { var x = numAttr('x'), wid = numAttr('width'); - expect(x(nthStack(0).nthBar(0)) + wid(nthStack(0).nthBar(0))) - .toBeLessThan(x(nthStack(0).nthBar(1))); + expect(x(nthBarGroup(0).nthBar(0)) + wid(nthBarGroup(0).nthBar(0))) + .toBeLessThan(x(nthBarGroup(1).nthBar(0))); }); }); @@ -1322,6 +1347,26 @@ describe('dc.barChart', function () { return stack; } + function nthBarGroup (n) { + var stack = d3.select(chart.selectAll('.bar-group').nodes()[n]); + + stack.nthBar = function (n) { + return d3.select(this.selectAll('rect.bar').nodes()[n]); + }; + + stack.nthLabel = function (n) { + return d3.select(this.selectAll('text.barLabel').nodes()[n]); + }; + + stack.forEachBar = function (assertions) { + this.selectAll('rect.bar').each(function (d) { + assertions(d3.select(this), d); + }); + }; + + return stack; + } + function forEachBar (assertions) { chart.selectAll('rect.bar').each(function (d) { assertions(d3.select(this), d); @@ -1337,7 +1382,7 @@ describe('dc.barChart', function () { function checkBarOverlap (n) { var x = numAttr('x'), wid = numAttr('width'); - expect(x(nthStack(0).nthBar(n)) + wid(nthStack(0).nthBar(n))) - .toBeLessThan(x(nthStack(0).nthBar(n + 1))); + expect(x(nthBarGroup(n).nthBar(0)) + wid(nthBarGroup(n).nthBar(0))) + .toBeLessThan(x(nthBarGroup(n + 1).nthBar(0))); } }); diff --git a/spec/composite-chart-spec.js b/spec/composite-chart-spec.js old mode 100644 new mode 100755 index d64468d41..322f4bdae --- a/spec/composite-chart-spec.js +++ b/spec/composite-chart-spec.js @@ -179,21 +179,21 @@ describe('dc.compositeChart', function () { }); it('should generate sub bar charts', function () { - expect(chart.selectAll('g.sub g._0 rect').size()).toBe(6); + expect(chart.selectAll('g.sub g.bar-group rect.bar').size()).toBe(6); }); it('should render sub bar chart', function () { expect(chart.selectAll('g.sub rect.bar').size()).not.toBe(0); - chart.selectAll('g.sub rect.bar').each(function (d, i) { + chart.selectAll('g.sub g.bar-group rect.bar').each(function (d, i) { switch (i) { case 0: - expect(d3.select(this).attr('x')).toBeCloseTo('22.637931034482758', 3); + expect(d3.select(this).attr('x')).toBeCloseTo('22.224137931034484', 3); expect(d3.select(this).attr('y')).toBe('93'); expect(d3.select(this).attr('width')).toBe('3'); expect(d3.select(this).attr('height')).toBe('17'); break; case 5: - expect(d3.select(this).attr('x')).toBeCloseTo('394.3620689655172', 3); + expect(d3.select(this).attr('x')).toBeCloseTo('393.94827586206895', 3); expect(d3.select(this).attr('y')).toBe('80'); expect(d3.select(this).attr('width')).toBe('3'); expect(d3.select(this).attr('height')).toBe('30'); @@ -409,7 +409,7 @@ describe('dc.compositeChart', function () { }); it('should trigger the sub-chart renderlet', function () { - expect(d3.select(chart.selectAll('rect').nodes()[0]).attr('width')).toBe('10'); + expect(d3.select(chart.selectAll('rect.bar').nodes()[0]).attr('width')).toBe('10'); }); }); diff --git a/src/bar-chart.js b/src/bar-chart.js index 03d1581d6..5aec19b5f 100644 --- a/src/bar-chart.js +++ b/src/bar-chart.js @@ -27,19 +27,34 @@ dc.barChart = function (parent, chartGroup) { var MIN_BAR_WIDTH = 1; var DEFAULT_GAP_BETWEEN_BARS = 2; + var DEFAULT_GAP_BETWEEN_BAR_GROUPS = 10; var LABEL_PADDING = 3; + var DEFAULT_SENSOR_BAR = true; + var DEFAULT_SENSOR_BAR_COLOR = '#fffff'; + var DEFAULT_SENSOR_BAR_OPACITY = 0; var _chart = dc.stackMixin(dc.coordinateGridMixin({})); var _gap = DEFAULT_GAP_BETWEEN_BARS; + var _groupGap = DEFAULT_GAP_BETWEEN_BAR_GROUPS; + var _groupBars = false; var _centerBar = false; + var _sensorBars = DEFAULT_SENSOR_BAR; + var _sensorBarColor = DEFAULT_SENSOR_BAR_COLOR; + var _sensorBarOpacity = DEFAULT_SENSOR_BAR_OPACITY; var _alwaysUseRounding = false; + var _barPadding; var _barWidth; + var _sensorBarWidth; + var _sensorBarPadding; dc.override(_chart, 'rescale', function () { _chart._rescale(); _barWidth = undefined; + _barPadding = undefined; + _sensorBarWidth = undefined; + _sensorBarPadding = undefined; return _chart; }); @@ -53,72 +68,104 @@ dc.barChart = function (parent, chartGroup) { }); _chart.label(function (d) { - return dc.utils.printSingleValue(d.y0 + d.y); + if (_groupBars) { + return dc.utils.printSingleValue(d.y); + } else { + return dc.utils.printSingleValue(d.y0 + d.y); + } }, false); _chart.plotData = function () { - var layers = _chart.chartBodyG().selectAll('g.stack') - .data(_chart.data()); + calculateBarWidths(); + calculateSensorBarWidths(); + var chartData = _chart.data(), + firstStack = _chart.data()[0], + barData = [], + sensorBarData = []; + + // Pivot the data, so that each item in the barData array contains all bars for the same x-point. + // Also add attribute groupIndex, wich indicates the bars order within the group. + if (chartData.length > 0) { + firstStack.values.forEach(function (d, i) { + var values = []; + for (var j = 0; j < chartData.length; j++) { + var value = chartData[j].values[i]; + value.groupIndex = j; + values.push(value); + } + + barData.push({ + 'values': values + }); + // create an array containing "sensor bars", one bar for each group of bars. + sensorBarData.push({ + 'values': [{ + 'x': d.x, + 'y': 0, + 'y0': 0, + 'y1': 0, + 'groupIndex': -1, + 'layer': d.layer, + 'data': { + 'key': d.data.key, + 'value': [] + } + }] + }); + }); - calculateBarWidth(); + var barGroups = _chart.chartBodyG().selectAll('g.bar-group') + .data(barData); - layers = layers - .enter() + var barGroupsEnter = barGroups.enter() .append('g') - .attr('class', function (d, i) { - return 'stack ' + '_' + i; - }) - .merge(layers); + .attr('class', 'bar-group') + .merge(barGroups); - var last = layers.size() - 1; - layers.each(function (d, i) { - var layer = d3.select(this); + barGroups.exit() + .remove(); - renderBars(layer, i, d); + barGroupsEnter.each(function (d, i) { + var barGroup = d3.select(this); - if (_chart.renderLabel() && last === i) { - renderLabels(layer, i, d); - } - }); - }; + renderSensorBar(barGroup, i, sensorBarData[i]); + renderBars(barGroup, i, d); - function barHeight (d) { - return dc.utils.safeNumber(Math.abs(_chart.y()(d.y + d.y0) - _chart.y()(d.y0))); - } - - function labelXPos (d) { - var x = _chart.x()(d.x); - if (!_centerBar) { - x += _barWidth / 2; - } - if (_chart.isOrdinal() && _gap !== undefined) { - x += _gap / 2; + if (_chart.renderLabel()) { + renderLabels(barGroup, i, d); + } + }); } - return dc.utils.safeNumber(x); - } - - function labelYPos (d) { - var y = _chart.y()(d.y + d.y0); + }; - if (d.y < 0) { - y -= barHeight(d); + function barHeight (d) { + var height; + if (_groupBars) { + height = dc.utils.safeNumber(Math.abs(_chart.y()(d.y) - _chart.y()(0))); + } else { + height = dc.utils.safeNumber(Math.abs(_chart.y()(d.y + d.y0) - _chart.y()(d.y0))); } - - return dc.utils.safeNumber(y - LABEL_PADDING); + return height; } function renderLabels (layer, layerIndex, d) { + var labelValues = d.values.filter(function (d) {return d.groupIndex !== -1;}); + if (false && !_groupBars) { + labelValues = [labelValues.pop()]; + } var labels = layer.selectAll('text.barLabel') - .data(d.values, dc.pluck('x')); + .data(labelValues, dc.pluck('groupIndex')); var labelsEnterUpdate = labels .enter() .append('text') .attr('class', 'barLabel') .attr('text-anchor', 'middle') - .attr('x', labelXPos) - .attr('y', labelYPos) - .merge(labels); + .attr('x', function (d) { + return barXPos(d) + _barWidth / 2; + }) + .attr('y', _chart.yAxisHeight()) + .merge(labels); if (_chart.isOrdinal()) { labelsEnterUpdate.on('click', _chart.onClick); @@ -126,9 +173,16 @@ dc.barChart = function (parent, chartGroup) { } dc.transition(labelsEnterUpdate, _chart.transitionDuration(), _chart.transitionDelay()) - .attr('x', labelXPos) - .attr('y', labelYPos) + .attr('x', function (d) { + return dc.utils.safeNumber(barXPos(d) + _barWidth / 2); + }) + .attr('y', function (d) { + return dc.utils.safeNumber(barYPos(d) - LABEL_PADDING); + }) .text(function (d) { + if (!_groupBars && d.groupIndex !== labelValues.length - 1) { + return ''; + } return _chart.label()(d); }); @@ -139,77 +193,228 @@ dc.barChart = function (parent, chartGroup) { function barXPos (d) { var x = _chart.x()(d.x); - if (_centerBar) { - x -= _barWidth / 2; + + if (_chart.groupBars()) { + var nuberOfBarsInGroup = _chart.stack().length, + xAxistStepLength, + groupIndex = d.groupIndex, + offset; + + x += groupIndex * (_barWidth + _barPadding); + if (_chart.isOrdinal()) { + xAxistStepLength = _chart.x().step(); + offset = xAxistStepLength - _chart.x().bandwidth(); + + x += _chart.groupGap() / 2; + x += _barPadding / 2; + x -= offset / 2; + } else if (!_chart.isOrdinal() && _centerBar) { + x -= (_barWidth + _barPadding) * nuberOfBarsInGroup / 2; + x += _barPadding / 2; + } + + } else { + + if (_centerBar) { + x -= _barWidth / 2; + } + if (_chart.isOrdinal()) { + x += _barPadding / 2; + } + } + return dc.utils.safeNumber(x); + } + + function barYPos (d) { + var y; + if (_groupBars) { + y = _chart.yAxisHeight() - barHeight(d); + } else { + y = _chart.y()(d.y + d.y0); + } + if (d.y < 0) { + y -= barHeight(d); + } + return dc.utils.safeNumber(y); + } + + function sensorBarXPos (d) { + var x = _chart.x()(d.x); + + if (_chart.isOrdinal()) { + x += _barPadding / 2; } - if (_chart.isOrdinal() && _gap !== undefined) { - x += _gap / 2; + if (!_chart.isOrdinal() && _centerBar) { + x -= _sensorBarWidth / 2; } + + if (_chart.isOrdinal() && _groupBars) { + x += _chart.groupGap() / 2; + } + return dc.utils.safeNumber(x); } - function renderBars (layer, layerIndex, d) { - var bars = layer.selectAll('rect.bar') - .data(d.values, dc.pluck('x')); + function renderBars (barGroup, barGroupIndex, d) { + var bars, + barData = d.values; - var enter = bars.enter() + bars = barGroup.selectAll('rect.bar') + .data(barData, dc.pluck('groupIndex')); + + var barsEnter = bars.enter() .append('rect') .attr('class', 'bar') .attr('fill', dc.pluck('data', _chart.getColor)) .attr('x', barXPos) .attr('y', _chart.yAxisHeight()) - .attr('height', 0); + .attr('height', 0) + .attr('width', Math.floor(_barWidth)); + + var barsEnterUpdate = barsEnter.merge(bars); + + dc.transition(barsEnterUpdate, _chart.transitionDuration(), _chart.transitionDelay()) + .attr('x', barXPos) + .attr('y', barYPos) + .attr('width', Math.floor(_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))); - var barsEnterUpdate = enter.merge(bars); + dc.transition(bars.exit(), _chart.transitionDuration(), _chart.transitionDelay()) + .attr('x', function (d) {return _chart.x()(d.x);}) + .attr('width', Math.floor(_barWidth) * 0.9) + .remove(); if (_chart.renderTitle()) { - enter.append('title').text(dc.pluck('data', _chart.title(d.name))); + barsEnter.append('title').text(dc.pluck('data', _chart.title(d.name))); } if (_chart.isOrdinal()) { - barsEnterUpdate.on('click', _chart.onClick); + barsEnterUpdate.on('click', function (d, i) { + _chart.onClick(d, i); + }); + + barsEnterUpdate.on('mouseenter', function (d, i) { + d3.select(this.parentElement).classed('hoover', true); + }); + + barsEnterUpdate.on('mouseleave', function (d, i) { + d3.select(this.parentElement).classed('hoover', false); + }); } + } - dc.transition(barsEnterUpdate, _chart.transitionDuration(), _chart.transitionDelay()) - .attr('x', barXPos) - .attr('y', function (d) { - var y = _chart.y()(d.y + d.y0); + function renderSensorBar (barGroup, barGroupIndex, d) { + var bars, + barData = d.values; - if (d.y < 0) { - y -= barHeight(d); - } + bars = barGroup.selectAll('rect.sensor-bar') + .data(barData, dc.pluck('groupIndex')); - 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))); + var barsEnter = bars.enter() + .append('rect') + .attr('class', 'sensor-bar') + .attr('fill', _sensorBarColor) + .attr('fill-opacity', _sensorBarOpacity) + .attr('x', sensorBarXPos) + .attr('y', 0) + .attr('height', _chart.yAxisHeight()) + .attr('width', Math.floor(_sensorBarWidth)); + + var barsEnterUpdate = barsEnter.merge(bars); + + dc.transition(barsEnterUpdate, _chart.transitionDuration(), _chart.transitionDelay()) + .attr('x', sensorBarXPos) + .attr('height', _chart.yAxisHeight()) + .attr('width', Math.floor(_sensorBarWidth)); dc.transition(bars.exit(), _chart.transitionDuration(), _chart.transitionDelay()) .attr('x', function (d) { return _chart.x()(d.x); }) - .attr('width', _barWidth * 0.9) + .attr('width', Math.floor(_sensorBarWidth) * 0.9) .remove(); + + if (_chart.isOrdinal()) { + barsEnterUpdate.on('click', function (d, i) { + _chart.onClick(d, i); + }); + + barsEnterUpdate.on('mouseenter', function (d, i) { + d3.select(this.parentElement).classed('hoover', true); + }); + + barsEnterUpdate.on('mouseleave', function (d, i) { + d3.select(this.parentElement).classed('hoover', false); + }); + } } - function calculateBarWidth () { + function calculateBarWidths () { if (_barWidth === undefined) { - var numberOfBars = _chart.xUnitCount(); + var xUnits = _chart.xUnitCount(), + xAxistStepLength, + numberOfBarsInGroup, + groupWidth, + barPadding, + barWidth; + + // Width + if (_chart.isOrdinal()) { + if (_groupBars) { + xAxistStepLength = _chart.x().step(); + numberOfBarsInGroup = _chart.stack().length; + groupWidth = (xAxistStepLength - _chart.groupGap()); + barWidth = groupWidth / numberOfBarsInGroup; + } else { + groupWidth = _chart.x().bandwidth(); + barWidth = groupWidth; + } + } else { + if (_groupBars) { + numberOfBarsInGroup = _chart.stack().length; + groupWidth = (_chart.xAxisLength() / xUnits) - _chart.groupGap(); + barWidth = groupWidth / numberOfBarsInGroup; + } else { + groupWidth = _chart.xAxisLength() / xUnits; + barWidth = groupWidth; + } + } + + // Padding + if (_gap === undefined) { + barPadding = barWidth * (_chart.barPadding()); + } else { + barPadding = _gap; + } + + if (barWidth === Infinity || isNaN(barWidth) || barWidth < MIN_BAR_WIDTH) { + barWidth = MIN_BAR_WIDTH; + barPadding = 0; + } + + _barWidth = dc.utils.safeNumber(barWidth - barPadding); + _barPadding = dc.utils.safeNumber(barPadding); + } + } + + function calculateSensorBarWidths () { + if (_sensorBarWidth === undefined) { + var sensorBarWidth, + sensorBarPadding = 0; - // please can't we always use rangeBands for bar charts? - if (_chart.isOrdinal() && _gap === undefined) { - _barWidth = Math.floor(_chart.x().bandwidth()); - } else if (_gap) { - _barWidth = Math.floor((_chart.xAxisLength() - (numberOfBars - 1) * _gap) / numberOfBars); + if (_groupBars) { + sensorBarWidth = (_barWidth + _barPadding) * _chart.stack().length - _barPadding; } else { - _barWidth = Math.floor(_chart.xAxisLength() / (1 + _chart.barPadding()) / numberOfBars); + sensorBarWidth = _barWidth; } - if (_barWidth === Infinity || isNaN(_barWidth) || _barWidth < MIN_BAR_WIDTH) { - _barWidth = MIN_BAR_WIDTH; + if (sensorBarWidth === Infinity || isNaN(sensorBarWidth) || sensorBarWidth < MIN_BAR_WIDTH) { + sensorBarWidth = MIN_BAR_WIDTH; + sensorBarPadding = 0; } + + _sensorBarWidth = dc.utils.safeNumber(sensorBarWidth - sensorBarPadding); + _sensorBarPadding = dc.utils.safeNumber(sensorBarPadding); } } @@ -323,6 +528,95 @@ dc.barChart = function (parent, chartGroup) { return brushSelection; }; + /** + * Group bars instead of stacking them. By default bars added through the stack function is stacked + * on top of each other. By setting groupedBars = true, the bars will instead be placed next to each other. + * Use groupGap and barPadding to adjust the spacing between bars and group of bars. + * @name groupBars + * @memberof dc.barChart + * @instance + * @param {Boolean} [groupBars=false] + * @return {Boolean|dc.barChart} + */ + _chart.groupBars = function (groupBars) { + if (!arguments.length) { + return _groupBars; + } + _barWidth = undefined; + _barPadding = undefined; + _sensorBarWidth = undefined; + _sensorBarPadding = undefined; + _groupBars = groupBars; + return _chart; + }; + + /** + * Manually set fixed gap (in px) between bar groups instead of relying on the default auto-generated + * gap. Only applicable for grouped bar charts. + * @name groupGap + * @memberof dc.barChart + * @instance + * @param {Number} [groupGap=5] + * @return {Number|dc.barChart} + */ + _chart.groupGap = function (groupGap) { + if (!arguments.length) { + return _groupGap; + } + _groupGap = groupGap; + return _chart; + }; + + /** + * Set or get whether sensor bars is enabled. Sensor bars is placed behind the normal bars or groups of bars + * but has the same height as the chart. This enables selection of bars by hovering or clicking above them + * in the chart. Useful for instance when some of the bars are relativly short. + * @name sensorBars + * @memberof dc.barChart + * @instance + * @param {Boolean} [sensorBars=true] + * @return {Boolean|dc.barChart} + */ + _chart.sensorBars = function (sensorBars) { + if (!arguments.length) { + return _sensorBars; + } + _sensorBars = sensorBars; + return _chart; + }; + + /** + * Set or get the fill color of the sensor bars + * @name sensorBarColor + * @memberof dc.barChart + * @instance + * @param {String} [sensorBarColor="#fffff"] + * @return {String|dc.barChart} + */ + _chart.sensorBarColor = function (sensorBarColor) { + if (!arguments.length) { + return _sensorBarColor; + } + _sensorBarColor = sensorBarColor; + return _chart; + }; + + /** + * Set or get the fill color of the sensor bars + * @name sensorBarOpacity + * @memberof dc.barChart + * @instance + * @param {Number} [sensorBarOpacity=0] + * @return {Number|dc.barChart} + */ + _chart.sensorBarOpacity = function (sensorBarOpacity) { + if (!arguments.length) { + return _sensorBarOpacity; + } + _sensorBarOpacity = sensorBarOpacity; + return _chart; + }; + /** * Set or get whether rounding is enabled when bars are centered. If false, using * rounding with centered bars will result in a warning and rounding will be ignored. This flag @@ -377,5 +671,25 @@ dc.barChart = function (parent, chartGroup) { return max; }); + dc.override(_chart, 'yAxisMax', function () { + var max; + if (_groupBars) { + max = d3.max(flattenStack(), function (p) { + return p.y; + }); + } else { + max = d3.max(flattenStack(), function (p) { + return (p.y > 0) ? (p.y + p.y0) : p.y0; + }); + } + + return dc.utils.add(max, _chart.yAxisPadding()); + }); + + function flattenStack () { + var valueses = _chart.data().map(function (layer) { return layer.domainValues; }); + return Array.prototype.concat.apply([], valueses); + } + return _chart.anchor(parent, chartGroup); }; diff --git a/style/dc.scss b/style/dc.scss index 68b81bb22..1d0ba812b 100644 --- a/style/dc.scss +++ b/style/dc.scss @@ -34,14 +34,27 @@ div.dc-chart { } .dc-chart { - rect { - &.bar { - stroke: none; - cursor: pointer; - &:hover { + g.bar-group{ + cursor: pointer; + rect { + &.bar { + stroke: none; + &:hover { + fill-opacity: 1; + } + } + &.deselected { + stroke: none; + fill: $color_celeste; + } + } + &.hoover { + rect.bar { fill-opacity: .5; } } + } + rect { &.deselected { stroke: none; fill: $color_celeste; @@ -250,6 +263,9 @@ div.dc-chart { cursor: default; } } + circle.dot { + stroke: none; + } } .dc-data-count { diff --git a/web/examples/bar-grouped-or-stacked-center.html b/web/examples/bar-grouped-or-stacked-center.html new file mode 100644 index 000000000..30b3e298f --- /dev/null +++ b/web/examples/bar-grouped-or-stacked-center.html @@ -0,0 +1,100 @@ + + +
+