Skip to content

Commit 2a0d960

Browse files
authored
Merge pull request #3680 from plotly/unhide-grouped-bars-within-trace
Unhide overlapping grouped bars within trace
2 parents 978303e + c019c5c commit 2a0d960

File tree

5 files changed

+200
-65
lines changed

5 files changed

+200
-65
lines changed

src/traces/bar/cross_trace_calc.js

Lines changed: 95 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -57,52 +57,57 @@ function setGroupPositions(gd, pa, sa, calcTraces) {
5757
if(!calcTraces.length) return;
5858

5959
var barmode = gd._fullLayout.barmode;
60-
var overlay = (barmode === 'overlay');
61-
var group = (barmode === 'group');
6260
var excluded;
6361
var included;
6462
var i, calcTrace, fullTrace;
6563

6664
initBase(gd, pa, sa, calcTraces);
6765

68-
if(overlay) {
69-
setGroupPositionsInOverlayMode(gd, pa, sa, calcTraces);
70-
} else if(group) {
71-
// exclude from the group those traces for which the user set an offset
72-
excluded = [];
73-
included = [];
74-
for(i = 0; i < calcTraces.length; i++) {
75-
calcTrace = calcTraces[i];
76-
fullTrace = calcTrace[0].trace;
77-
78-
if(fullTrace.offset === undefined) included.push(calcTrace);
79-
else excluded.push(calcTrace);
80-
}
66+
switch(barmode) {
67+
case 'overlay':
68+
setGroupPositionsInOverlayMode(gd, pa, sa, calcTraces);
69+
break;
70+
71+
case 'group':
72+
// exclude from the group those traces for which the user set an offset
73+
excluded = [];
74+
included = [];
75+
for(i = 0; i < calcTraces.length; i++) {
76+
calcTrace = calcTraces[i];
77+
fullTrace = calcTrace[0].trace;
78+
79+
if(fullTrace.offset === undefined) included.push(calcTrace);
80+
else excluded.push(calcTrace);
81+
}
8182

82-
if(included.length) {
83-
setGroupPositionsInGroupMode(gd, pa, sa, included);
84-
}
85-
if(excluded.length) {
86-
setGroupPositionsInOverlayMode(gd, pa, sa, excluded);
87-
}
88-
} else {
89-
// exclude from the stack those traces for which the user set a base
90-
excluded = [];
91-
included = [];
92-
for(i = 0; i < calcTraces.length; i++) {
93-
calcTrace = calcTraces[i];
94-
fullTrace = calcTrace[0].trace;
95-
96-
if(fullTrace.base === undefined) included.push(calcTrace);
97-
else excluded.push(calcTrace);
98-
}
83+
if(included.length) {
84+
setGroupPositionsInGroupMode(gd, pa, sa, included);
85+
}
86+
if(excluded.length) {
87+
setGroupPositionsInOverlayMode(gd, pa, sa, excluded);
88+
}
89+
break;
90+
91+
case 'stack':
92+
case 'relative':
93+
// exclude from the stack those traces for which the user set a base
94+
excluded = [];
95+
included = [];
96+
for(i = 0; i < calcTraces.length; i++) {
97+
calcTrace = calcTraces[i];
98+
fullTrace = calcTrace[0].trace;
99+
100+
if(fullTrace.base === undefined) included.push(calcTrace);
101+
else excluded.push(calcTrace);
102+
}
99103

100-
if(included.length) {
101-
setGroupPositionsInStackOrRelativeMode(gd, pa, sa, included);
102-
}
103-
if(excluded.length) {
104-
setGroupPositionsInOverlayMode(gd, pa, sa, excluded);
105-
}
104+
if(included.length) {
105+
setGroupPositionsInStackOrRelativeMode(gd, pa, sa, included);
106+
}
107+
if(excluded.length) {
108+
setGroupPositionsInOverlayMode(gd, pa, sa, excluded);
109+
}
110+
break;
106111
}
107112

108113
collectExtents(calcTraces, pa);
@@ -154,13 +159,15 @@ function initBase(gd, pa, sa, calcTraces) {
154159

155160
function setGroupPositionsInOverlayMode(gd, pa, sa, calcTraces) {
156161
var barnorm = gd._fullLayout.barnorm;
157-
var separateNegativeValues = false;
158-
var dontMergeOverlappingData = !barnorm;
159162

160163
// update position axis and set bar offsets and widths
161164
for(var i = 0; i < calcTraces.length; i++) {
162165
var calcTrace = calcTraces[i];
163-
var sieve = new Sieve([calcTrace], separateNegativeValues, dontMergeOverlappingData);
166+
167+
var sieve = new Sieve([calcTrace], {
168+
sepNegVal: false,
169+
overlapNoMerge: !barnorm
170+
});
164171

165172
// set bar offsets and widths, and update position axis
166173
setOffsetAndWidth(gd, pa, sieve);
@@ -182,13 +189,19 @@ function setGroupPositionsInOverlayMode(gd, pa, sa, calcTraces) {
182189
function setGroupPositionsInGroupMode(gd, pa, sa, calcTraces) {
183190
var fullLayout = gd._fullLayout;
184191
var barnorm = fullLayout.barnorm;
185-
var separateNegativeValues = false;
186-
var dontMergeOverlappingData = !barnorm;
187-
var sieve = new Sieve(calcTraces, separateNegativeValues, dontMergeOverlappingData);
192+
193+
var sieve = new Sieve(calcTraces, {
194+
sepNegVal: false,
195+
overlapNoMerge: !barnorm
196+
});
188197

189198
// set bar offsets and widths, and update position axis
190199
setOffsetAndWidthInGroupMode(gd, pa, sieve);
191200

201+
// relative-stack bars within the same trace that would otherwise
202+
// be hidden
203+
unhideBarsWithinTrace(gd, sa, sieve);
204+
192205
// set bar bases and sizes, and update size axis
193206
if(barnorm) {
194207
sieveBars(gd, sa, sieve);
@@ -201,12 +214,12 @@ function setGroupPositionsInGroupMode(gd, pa, sa, calcTraces) {
201214
function setGroupPositionsInStackOrRelativeMode(gd, pa, sa, calcTraces) {
202215
var fullLayout = gd._fullLayout;
203216
var barmode = fullLayout.barmode;
204-
var stack = barmode === 'stack';
205-
var relative = barmode === 'relative';
206217
var barnorm = fullLayout.barnorm;
207-
var separateNegativeValues = relative;
208-
var dontMergeOverlappingData = !(barnorm || stack || relative);
209-
var sieve = new Sieve(calcTraces, separateNegativeValues, dontMergeOverlappingData);
218+
219+
var sieve = new Sieve(calcTraces, {
220+
sepNegVal: barmode === 'relative',
221+
overlapNoMerge: !(barnorm || barmode === 'stack' || barmode === 'relative')
222+
});
210223

211224
// set bar offsets and widths, and update position axis
212225
setOffsetAndWidth(gd, pa, sieve);
@@ -563,7 +576,39 @@ function sieveBars(gd, sa, sieve) {
563576
for(var j = 0; j < calcTrace.length; j++) {
564577
var bar = calcTrace[j];
565578

566-
if(bar.s !== BADNUM) sieve.put(bar.p, bar.b + bar.s);
579+
if(bar.s !== BADNUM) {
580+
sieve.put(bar.p, bar.b + bar.s);
581+
}
582+
}
583+
}
584+
}
585+
586+
function unhideBarsWithinTrace(gd, sa, sieve) {
587+
var calcTraces = sieve.traces;
588+
589+
for(var i = 0; i < calcTraces.length; i++) {
590+
var calcTrace = calcTraces[i];
591+
var fullTrace = calcTrace[0].trace;
592+
593+
if(fullTrace.base === undefined) {
594+
var inTraceSieve = new Sieve([calcTrace], {
595+
sepNegVal: true,
596+
overlapNoMerge: true
597+
});
598+
599+
for(var j = 0; j < calcTrace.length; j++) {
600+
var bar = calcTrace[j];
601+
602+
if(bar.p !== BADNUM) {
603+
// stack current bar and get previous sum
604+
var barBase = inTraceSieve.put(bar.p, bar.b + bar.s);
605+
606+
// if previous sum if non-zero, this means:
607+
// multiple bars have same starting point are potentially hidden,
608+
// shift them vertically so that all bars are visible by default
609+
if(barBase) bar.b = barBase;
610+
}
611+
}
567612
}
568613
}
569614
}

src/traces/bar/sieve.js

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,20 @@ var BADNUM = require('../../constants/numerical').BADNUM;
1717
* Helper class to sieve data from traces into bins
1818
*
1919
* @class
20-
* @param {Array} traces
21-
* Array of calculated traces
22-
* @param {boolean} [separateNegativeValues]
23-
* If true, then split data at the same position into a bar
24-
* for positive values and another for negative values
25-
* @param {boolean} [dontMergeOverlappingData]
26-
* If true, then don't merge overlapping bars into a single bar
20+
*
21+
* @param {Array} traces
22+
* Array of calculated traces
23+
* @param {object} opts
24+
* - @param {boolean} [sepNegVal]
25+
* If true, then split data at the same position into a bar
26+
* for positive values and another for negative values
27+
* - @param {boolean} [overlapNoMerge]
28+
* If true, then don't merge overlapping bars into a single bar
2729
*/
28-
function Sieve(traces, separateNegativeValues, dontMergeOverlappingData) {
30+
function Sieve(traces, opts) {
2931
this.traces = traces;
30-
this.separateNegativeValues = separateNegativeValues;
31-
this.dontMergeOverlappingData = dontMergeOverlappingData;
32+
this.sepNegVal = opts.sepNegVal;
33+
this.overlapNoMerge = opts.overlapNoMerge;
3234

3335
// for single-bin histograms - see histogram/calc
3436
var width1 = Infinity;
@@ -79,7 +81,7 @@ Sieve.prototype.put = function put(position, value) {
7981
* @method
8082
* @param {number} position Position of datum
8183
* @param {number} [value] Value of datum
82-
* (required if this.separateNegativeValues is true)
84+
* (required if this.sepNegVal is true)
8385
* @returns {number} Current bin value
8486
*/
8587
Sieve.prototype.get = function put(position, value) {
@@ -93,14 +95,14 @@ Sieve.prototype.get = function put(position, value) {
9395
* @method
9496
* @param {number} position Position of datum
9597
* @param {number} [value] Value of datum
96-
* (required if this.separateNegativeValues is true)
98+
* (required if this.sepNegVal is true)
9799
* @returns {string} Bin label
98-
* (prefixed with a 'v' if value is negative and this.separateNegativeValues is
100+
* (prefixed with a 'v' if value is negative and this.sepNegVal is
99101
* true; otherwise prefixed with '^')
100102
*/
101103
Sieve.prototype.getLabel = function getLabel(position, value) {
102-
var prefix = (value < 0 && this.separateNegativeValues) ? 'v' : '^';
103-
var label = (this.dontMergeOverlappingData) ?
104+
var prefix = (value < 0 && this.sepNegVal) ? 'v' : '^';
105+
var label = (this.overlapNoMerge) ?
104106
position :
105107
Math.round(position / this.binWidth);
106108
return prefix + label;

test/image/baselines/bar_unhidden.png

8.42 KB
Loading

test/image/mocks/bar_unhidden.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"data": [
3+
{
4+
"type": "bar",
5+
"x": [ 0, 0, 0 ],
6+
"y": [ 1, 1, -1 ],
7+
"marker": {
8+
"color": [ "red", "green", "blue" ]
9+
}
10+
},
11+
{
12+
"type": "bar",
13+
"x": [ 0, 0, 0 ],
14+
"y": [ 1, 1, -1 ],
15+
"marker": {
16+
"color": [ "cyan", "magenta", "yellow" ]
17+
}
18+
}
19+
],
20+
"layout": {
21+
"margin": { "t": 30, "b": 30, "l": 30, "r": 30 },
22+
"width": 400,
23+
"height": 400,
24+
"showlegend": false,
25+
"hovermode": "closest"
26+
}
27+
}

test/jasmine/tests/bar_test.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -926,6 +926,67 @@ describe('Bar.crossTraceCalc (formerly known as setPositions)', function() {
926926
expect(gd._fullLayout.xaxis.type).toBe('multicategory');
927927
assertPointField(gd.calcdata, 'b', [[0, 0, 0, 0]]);
928928
});
929+
930+
describe('should relative-stack bar within the same trace that overlap under barmode=group', function() {
931+
it('- base case', function() {
932+
var gd = mockBarPlot([{
933+
x: [0, 0, 0],
934+
y: [1, -2, -1]
935+
}]);
936+
937+
assertPointField(gd.calcdata, 'b', [[0, 0, -2]]);
938+
assertPointField(gd.calcdata, 'y', [[1, -2, -3]]);
939+
});
940+
941+
it('- with blank positions', function() {
942+
var gd = mockBarPlot([{
943+
x: [0, null, 0, null, 0],
944+
y: [1, null, -2, null, -1]
945+
}]);
946+
947+
assertPointField(gd.calcdata, 'b', [[0, 0, 0, 0, -2]]);
948+
assertPointField(gd.calcdata, 'y', [[1, NaN, -2, NaN, -3]]);
949+
});
950+
951+
it('- with barnorm set', function() {
952+
var gd = mockBarPlot([{
953+
x: [0, 0, 0],
954+
y: [1, -2, -1],
955+
}], {
956+
barnorm: 'fraction'
957+
});
958+
959+
assertPointField(gd.calcdata, 'b', [[0, 0, -0.5]]);
960+
assertPointField(gd.calcdata, 'y', [[0.25, -0.5, -0.75]]);
961+
});
962+
963+
it('- skipped when base is set', function() {
964+
var gd = mockBarPlot([{
965+
x: [0, 0, 0],
966+
y: [1, -2, -1],
967+
base: 10
968+
}, {
969+
x: [0, 0, 0],
970+
y: [1, -2, -1],
971+
base: [1, 2, 1]
972+
}]);
973+
974+
assertPointField(gd.calcdata, 'b', [[10, 10, 10], [1, 2, 1]]);
975+
assertPointField(gd.calcdata, 'y', [[11, 8, 9], [2, 0, 0]]);
976+
});
977+
978+
it('- skipped when barmode=overlay', function() {
979+
var gd = mockBarPlot([{
980+
x: [0, 0, 0],
981+
y: [1, -2, -1]
982+
}], {
983+
barmode: 'overlay'
984+
});
985+
986+
assertPointField(gd.calcdata, 'b', [[0, 0, 0]]);
987+
assertPointField(gd.calcdata, 'y', [[1, -2, -1]]);
988+
});
989+
});
929990
});
930991

931992
describe('A bar plot', function() {

0 commit comments

Comments
 (0)