Skip to content

Commit 399d03b

Browse files
authored
Merge pull request #3529 from plotly/offset-alignment-groups
Implement alignmentgroup and offsetgroup
2 parents 25fa0c2 + e746b08 commit 399d03b

34 files changed

+2165
-39
lines changed

src/plots/cartesian/axis_ids.js

+10
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,13 @@ exports.idSort = function(id1, id2) {
114114
if(letter1 !== letter2) return letter1 > letter2 ? 1 : -1;
115115
return +(id1.substr(1) || 1) - +(id2.substr(1) || 1);
116116
};
117+
118+
exports.getAxisGroup = function getAxisGroup(fullLayout, axId) {
119+
var matchGroups = fullLayout._axisMatchGroups;
120+
121+
for(var i = 0; i < matchGroups.length; i++) {
122+
var group = matchGroups[i];
123+
if(group[axId]) return 'g' + i;
124+
}
125+
return axId;
126+
};

src/plots/plots.js

+2
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,8 @@ plots.supplyDefaults = function(gd, opts) {
394394
newFullLayout._scatterStackOpts = {};
395395
// for the first scatter trace on each subplot (so it knows tonext->tozero)
396396
newFullLayout._firstScatter = {};
397+
// for grouped bar/box/violin trace to share config across traces
398+
newFullLayout._alignmentOpts = {};
397399

398400
// for traces to request a default rangeslider on their x axes
399401
// eg set `_requestRangeslider.x2 = true` for xaxis2

src/traces/bar/attributes.js

+24
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,30 @@ module.exports = {
174174

175175
marker: marker,
176176

177+
offsetgroup: {
178+
valType: 'string',
179+
role: 'info',
180+
dflt: '',
181+
editType: 'calc',
182+
description: [
183+
'Set several traces linked to the same position axis',
184+
'or matching axes to the same',
185+
'offsetgroup where bars of the same position coordinate will line up.'
186+
].join(' ')
187+
},
188+
alignmentgroup: {
189+
valType: 'string',
190+
role: 'info',
191+
dflt: '',
192+
editType: 'calc',
193+
description: [
194+
'Set several traces linked to the same position axis',
195+
'or matching axes to the same',
196+
'alignmentgroup. This controls whether bars compute their positional',
197+
'range dependently or independently.'
198+
].join(' ')
199+
},
200+
177201
selected: {
178202
marker: {
179203
opacity: scatterAttrs.selected.marker.opacity,

src/traces/bar/cross_trace_calc.js

+27-10
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ var BADNUM = require('../../constants/numerical').BADNUM;
1414

1515
var Registry = require('../../registry');
1616
var Axes = require('../../plots/cartesian/axes');
17+
var getAxisGroup = require('../../plots/cartesian/axis_ids').getAxisGroup;
1718
var Sieve = require('./sieve.js');
1819

1920
/*
@@ -279,26 +280,42 @@ function setOffsetAndWidthInGroupMode(gd, pa, sieve) {
279280
var distinctPositions = sieve.distinctPositions;
280281
var minDiff = sieve.minDiff;
281282
var calcTraces = sieve.traces;
283+
var nTraces = calcTraces.length;
282284

283285
// if there aren't any overlapping positions,
284286
// let them have full width even if mode is group
285287
var overlap = (positions.length !== distinctPositions.length);
286-
287-
var nTraces = calcTraces.length;
288288
var barGroupWidth = minDiff * (1 - bargap);
289-
var barWidthPlusGap = (overlap) ? barGroupWidth / nTraces : barGroupWidth;
290-
var barWidth = barWidthPlusGap * (1 - bargroupgap);
289+
290+
var groupId = getAxisGroup(fullLayout, pa._id) + calcTraces[0][0].trace.orientation;
291+
var alignmentGroups = fullLayout._alignmentOpts[groupId] || {};
291292

292293
for(var i = 0; i < nTraces; i++) {
293294
var calcTrace = calcTraces[i];
294-
var t = calcTrace[0].t;
295+
var trace = calcTrace[0].trace;
295296

296-
// computer bar group center and bar offset
297-
var offsetFromCenter = overlap ?
298-
((2 * i + 1 - nTraces) * barWidthPlusGap - barWidth) / 2 :
299-
-barWidth / 2;
297+
var alignmentGroupOpts = alignmentGroups[trace.alignmentgroup] || {};
298+
var nOffsetGroups = Object.keys(alignmentGroupOpts.offsetGroups || {}).length;
300299

301-
// store bar width and offset for this trace
300+
var barWidthPlusGap;
301+
if(nOffsetGroups) {
302+
barWidthPlusGap = barGroupWidth / nOffsetGroups;
303+
} else {
304+
barWidthPlusGap = overlap ? barGroupWidth / nTraces : barGroupWidth;
305+
}
306+
307+
var barWidth = barWidthPlusGap * (1 - bargroupgap);
308+
309+
var offsetFromCenter;
310+
if(nOffsetGroups) {
311+
offsetFromCenter = ((2 * trace._offsetIndex + 1 - nOffsetGroups) * barWidthPlusGap - barWidth) / 2;
312+
} else {
313+
offsetFromCenter = overlap ?
314+
((2 * i + 1 - nTraces) * barWidthPlusGap - barWidth) / 2 :
315+
-barWidth / 2;
316+
}
317+
318+
var t = calcTrace[0].t;
302319
t.barwidth = barWidth;
303320
t.poffset = offsetFromCenter;
304321
t.bargroupwidth = barGroupWidth;

src/traces/bar/defaults.js

+66-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
* LICENSE file in the root directory of this source tree.
77
*/
88

9-
109
'use strict';
1110

1211
var Lib = require('../../lib');
@@ -15,9 +14,10 @@ var Registry = require('../../registry');
1514

1615
var handleXYDefaults = require('../scatter/xy_defaults');
1716
var handleStyleDefaults = require('../bar/style_defaults');
17+
var getAxisGroup = require('../../plots/cartesian/axis_ids').getAxisGroup;
1818
var attributes = require('./attributes');
1919

20-
module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
20+
function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
2121
function coerce(attr, dflt) {
2222
return Lib.coerce(traceIn, traceOut, attributes, attr, dflt);
2323
}
@@ -78,4 +78,68 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
7878
errorBarsSupplyDefaults(traceIn, traceOut, lineColor || Color.defaultLine, {axis: 'x', inherit: 'y'});
7979

8080
Lib.coerceSelectionMarkerOpacity(traceOut, coerce);
81+
}
82+
83+
function handleGroupingDefaults(traceIn, traceOut, fullLayout, coerce) {
84+
var orientation = traceOut.orientation;
85+
// N.B. grouping is done across all trace trace types that support it
86+
var posAxId = traceOut[{v: 'x', h: 'y'}[orientation] + 'axis'];
87+
var groupId = getAxisGroup(fullLayout, posAxId) + orientation;
88+
89+
var alignmentOpts = fullLayout._alignmentOpts || {};
90+
var alignmentgroup = coerce('alignmentgroup');
91+
92+
var alignmentGroups = alignmentOpts[groupId];
93+
if(!alignmentGroups) alignmentGroups = alignmentOpts[groupId] = {};
94+
95+
var alignmentGroupOpts = alignmentGroups[alignmentgroup];
96+
97+
if(alignmentGroupOpts) {
98+
alignmentGroupOpts.traces.push(traceOut);
99+
} else {
100+
alignmentGroupOpts = alignmentGroups[alignmentgroup] = {
101+
traces: [traceOut],
102+
alignmentIndex: Object.keys(alignmentGroups).length,
103+
offsetGroups: {}
104+
};
105+
}
106+
107+
var offsetgroup = coerce('offsetgroup');
108+
var offsetGroups = alignmentGroupOpts.offsetGroups;
109+
var offsetGroupOpts = offsetGroups[offsetgroup];
110+
111+
if(offsetgroup) {
112+
if(!offsetGroupOpts) {
113+
offsetGroupOpts = offsetGroups[offsetgroup] = {
114+
offsetIndex: Object.keys(offsetGroups).length
115+
};
116+
}
117+
118+
traceOut._offsetIndex = offsetGroupOpts.offsetIndex;
119+
}
120+
}
121+
122+
function crossTraceDefaults(fullData, fullLayout) {
123+
var traceIn, traceOut;
124+
125+
function coerce(attr) {
126+
return Lib.coerce(traceOut._input, traceOut, attributes, attr);
127+
}
128+
129+
for(var i = 0; i < fullData.length; i++) {
130+
traceOut = fullData[i];
131+
132+
if(traceOut.type === 'bar') {
133+
traceIn = traceOut._input;
134+
if(fullLayout.barmode === 'group') {
135+
handleGroupingDefaults(traceIn, traceOut, fullLayout, coerce);
136+
}
137+
}
138+
}
139+
}
140+
141+
module.exports = {
142+
supplyDefaults: supplyDefaults,
143+
crossTraceDefaults: crossTraceDefaults,
144+
handleGroupingDefaults: handleGroupingDefaults
81145
};

src/traces/bar/index.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
* LICENSE file in the root directory of this source tree.
77
*/
88

9-
109
'use strict';
1110

1211
var Bar = {};
1312

1413
Bar.attributes = require('./attributes');
1514
Bar.layoutAttributes = require('./layout_attributes');
16-
Bar.supplyDefaults = require('./defaults');
15+
Bar.supplyDefaults = require('./defaults').supplyDefaults;
16+
Bar.crossTraceDefaults = require('./defaults').crossTraceDefaults;
1717
Bar.supplyLayoutDefaults = require('./layout_defaults');
1818
Bar.calc = require('./calc');
1919
Bar.crossTraceCalc = require('./cross_trace_calc').crossTraceCalc;

src/traces/bar/layout_defaults.js

-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
* LICENSE file in the root directory of this source tree.
77
*/
88

9-
109
'use strict';
1110

1211
var Registry = require('../../registry');
@@ -15,7 +14,6 @@ var Lib = require('../../lib');
1514

1615
var layoutAttributes = require('./layout_attributes');
1716

18-
1917
module.exports = function(layoutIn, layoutOut, fullData) {
2018
function coerce(attr, dflt) {
2119
return Lib.coerce(layoutIn, layoutOut, layoutAttributes, attr, dflt);

src/traces/box/attributes.js

+4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
'use strict';
1010

1111
var scatterAttrs = require('../scatter/attributes');
12+
var barAttrs = require('../bar/attributes');
1213
var colorAttrs = require('../../components/color/attributes');
1314
var extendFlat = require('../../lib/extend').extendFlat;
1415

@@ -250,6 +251,9 @@ module.exports = {
250251
},
251252
fillcolor: scatterAttrs.fillcolor,
252253

254+
offsetgroup: barAttrs.offsetgroup,
255+
alignmentgroup: barAttrs.alignmentgroup,
256+
253257
selected: {
254258
marker: scatterAttrs.selected.marker,
255259
editType: 'style'

src/traces/box/cross_trace_calc.js

+21-8
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
var Axes = require('../../plots/cartesian/axes');
1212
var Lib = require('../../lib');
13+
var getAxisGroup = require('../../plots/cartesian/axis_ids').getAxisGroup;
1314

1415
var orientations = ['v', 'h'];
1516

@@ -51,9 +52,6 @@ function setPositionOffset(traceType, gd, boxList, posAxis) {
5152
var axId = posAxis._id;
5253
var axLetter = axId.charAt(0);
5354

54-
// N.B. reused in violin
55-
var numKey = traceType === 'violin' ? '_numViolins' : '_numBoxes';
56-
5755
var i, j, calcTrace;
5856
var pointList = [];
5957
var shownPts = 0;
@@ -76,8 +74,9 @@ function setPositionOffset(traceType, gd, boxList, posAxis) {
7674
// check for forced minimum dtick
7775
Axes.minDtick(posAxis, boxdv.minDiff, boxdv.vals[0], true);
7876

79-
var num = fullLayout[numKey];
80-
var group = (fullLayout[traceType + 'mode'] === 'group' && num > 1);
77+
var numKey = traceType === 'violin' ? '_numViolins' : '_numBoxes';
78+
var numTotal = fullLayout[numKey];
79+
var group = fullLayout[traceType + 'mode'] === 'group' && numTotal > 1;
8180
var groupFraction = 1 - fullLayout[traceType + 'gap'];
8281
var groupGapFraction = 1 - fullLayout[traceType + 'groupgap'];
8382

@@ -104,9 +103,23 @@ function setPositionOffset(traceType, gd, boxList, posAxis) {
104103
bPos = 0;
105104
} else {
106105
dPos = dPos0;
107-
bdPos = dPos * groupFraction * groupGapFraction / (group ? num : 1);
108-
bPos = group ? 2 * dPos * (-0.5 + (t.num + 0.5) / num) * groupFraction : 0;
109-
wHover = dPos * (group ? groupFraction / num : 1);
106+
107+
if(group) {
108+
var groupId = getAxisGroup(fullLayout, posAxis._id) + trace.orientation;
109+
var alignmentGroups = fullLayout._alignmentOpts[groupId] || {};
110+
var alignmentGroupOpts = alignmentGroups[trace.alignmentgroup] || {};
111+
var nOffsetGroups = Object.keys(alignmentGroupOpts.offsetGroups || {}).length;
112+
var num = nOffsetGroups || numTotal;
113+
var shift = nOffsetGroups ? trace._offsetIndex : t.num;
114+
115+
bdPos = dPos * groupFraction * groupGapFraction / num;
116+
bPos = 2 * dPos * (-0.5 + (shift + 0.5) / num) * groupFraction;
117+
wHover = dPos * groupFraction / num;
118+
} else {
119+
bdPos = dPos * groupFraction * groupGapFraction;
120+
bPos = 0;
121+
wHover = dPos;
122+
}
110123
}
111124
t.dPos = dPos;
112125
t.bPos = bPos;

src/traces/box/defaults.js

+23-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
var Lib = require('../../lib');
1212
var Registry = require('../../registry');
1313
var Color = require('../../components/color');
14-
14+
var handleGroupingDefaults = require('../bar/defaults').handleGroupingDefaults;
1515
var attributes = require('./attributes');
1616

1717
function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
@@ -109,8 +109,30 @@ function handlePointsDefaults(traceIn, traceOut, coerce, opts) {
109109
Lib.coerceSelectionMarkerOpacity(traceOut, coerce);
110110
}
111111

112+
function crossTraceDefaults(fullData, fullLayout) {
113+
var traceIn, traceOut;
114+
115+
function coerce(attr) {
116+
return Lib.coerce(traceOut._input, traceOut, attributes, attr);
117+
}
118+
119+
for(var i = 0; i < fullData.length; i++) {
120+
traceOut = fullData[i];
121+
var traceType = traceOut.type;
122+
123+
if(traceType === 'box' || traceType === 'violin') {
124+
traceIn = traceOut._input;
125+
if(fullLayout[traceType + 'mode'] === 'group') {
126+
handleGroupingDefaults(traceIn, traceOut, fullLayout, coerce);
127+
}
128+
}
129+
}
130+
}
131+
112132
module.exports = {
113133
supplyDefaults: supplyDefaults,
134+
crossTraceDefaults: crossTraceDefaults,
135+
114136
handleSampleDefaults: handleSampleDefaults,
115137
handlePointsDefaults: handlePointsDefaults
116138
};

src/traces/box/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ var Box = {};
1313
Box.attributes = require('./attributes');
1414
Box.layoutAttributes = require('./layout_attributes');
1515
Box.supplyDefaults = require('./defaults').supplyDefaults;
16+
Box.crossTraceDefaults = require('./defaults').crossTraceDefaults;
1617
Box.supplyLayoutDefaults = require('./layout_defaults').supplyLayoutDefaults;
1718
Box.calc = require('./calc');
1819
Box.crossTraceCalc = require('./cross_trace_calc').crossTraceCalc;

src/traces/box/layout_defaults.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@ var Lib = require('../../lib');
1313
var layoutAttributes = require('./layout_attributes');
1414

1515
function _supply(layoutIn, layoutOut, fullData, coerce, traceType) {
16-
var hasTraceType;
1716
var category = traceType + 'Layout';
17+
var hasTraceType = false;
18+
1819
for(var i = 0; i < fullData.length; i++) {
19-
if(Registry.traceIs(fullData[i], category)) {
20+
var trace = fullData[i];
21+
22+
if(Registry.traceIs(trace, category)) {
2023
hasTraceType = true;
2124
break;
2225
}

src/traces/histogram/attributes.js

+3
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,9 @@ module.exports = {
188188

189189
marker: barAttrs.marker,
190190

191+
offsetgroup: barAttrs.offsetgroup,
192+
alignmentgroup: barAttrs.alignmentgroup,
193+
191194
selected: barAttrs.selected,
192195
unselected: barAttrs.unselected,
193196

0 commit comments

Comments
 (0)