Skip to content

Commit e545415

Browse files
authored
Merge pull request #3005 from plotly/per-stack-first
Fix stacked area layering and deletion bugs
2 parents 5fa493a + 72c61c7 commit e545415

File tree

7 files changed

+162
-34
lines changed

7 files changed

+162
-34
lines changed

src/traces/scatter/attributes.js

+10-2
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,11 @@ module.exports = {
8585
'stacked. Stacking also turns `fill` on by default, using *tonexty*',
8686
'(*tonextx*) if `orientation` is *h* (*v*) and sets the default',
8787
'`mode` to *lines* irrespective of point count.',
88-
'You can only stack on a numeric (linear or log) axis.'
88+
'You can only stack on a numeric (linear or log) axis.',
89+
'Traces in a `stackgroup` will only fill to (or be filled to) other',
90+
'traces in the same group. With multiple `stackgroup`s or some',
91+
'traces stacked and some not, if fill-linked traces are not already',
92+
'consecutive, the later ones will be pushed down in the drawing order.'
8993
].join(' ')
9094
},
9195
orientation: {
@@ -299,7 +303,11 @@ module.exports = {
299303
'*tonext* fills the space between two traces if one completely',
300304
'encloses the other (eg consecutive contour lines), and behaves like',
301305
'*toself* if there is no trace before it. *tonext* should not be',
302-
'used if one trace does not enclose the other.'
306+
'used if one trace does not enclose the other.',
307+
'Traces in a `stackgroup` will only fill to (or be filled to) other',
308+
'traces in the same group. With multiple `stackgroup`s or some',
309+
'traces stacked and some not, if fill-linked traces are not already',
310+
'consecutive, the later ones will be pushed down in the drawing order.'
303311
].join(' ')
304312
},
305313
fillcolor: {

src/traces/scatter/calc.js

+9-3
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ function calcAxisExpansion(gd, trace, xa, ya, x, y, ppad) {
153153
var fullLayout = gd._fullLayout;
154154
var xId = xa._id;
155155
var yId = ya._id;
156-
var firstScatter = fullLayout._firstScatter[xId + yId + trace.type] === trace.uid;
156+
var firstScatter = fullLayout._firstScatter[firstScatterGroup(trace)] === trace.uid;
157157
var stackOrientation = (getStackOpts(trace, fullLayout, xa, ya) || {}).orientation;
158158
var fill = trace.fill;
159159

@@ -257,9 +257,15 @@ function calcMarkerSize(trace, serieslen) {
257257
* per-trace calc this will get confused.
258258
*/
259259
function setFirstScatter(fullLayout, trace) {
260-
var subplotAndType = trace.xaxis + trace.yaxis + trace.type;
260+
var group = firstScatterGroup(trace);
261261
var firstScatter = fullLayout._firstScatter;
262-
if(!firstScatter[subplotAndType]) firstScatter[subplotAndType] = trace.uid;
262+
if(!firstScatter[group]) firstScatter[group] = trace.uid;
263+
}
264+
265+
function firstScatterGroup(trace) {
266+
var stackGroup = trace.stackgroup;
267+
return trace.xaxis + trace.yaxis + trace.type +
268+
(stackGroup ? '-' + stackGroup : '');
263269
}
264270

265271
function getStackOpts(trace, fullLayout, xa, ya) {

src/traces/scatter/link_traces.js

+53-6
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,56 @@
88

99
'use strict';
1010

11+
var LINKEDFILLS = {tonextx: 1, tonexty: 1, tonext: 1};
12+
1113
module.exports = function linkTraces(gd, plotinfo, cdscatter) {
12-
var trace, i;
13-
var prevtrace = null;
14+
var trace, i, group, prevtrace, groupIndex;
1415

15-
for(i = 0; i < cdscatter.length; ++i) {
16+
// first sort traces to keep stacks & filled-together groups together
17+
var groupIndices = {};
18+
var needsSort = false;
19+
var prevGroupIndex = -1;
20+
var nextGroupIndex = 0;
21+
var prevUnstackedGroupIndex = -1;
22+
for(i = 0; i < cdscatter.length; i++) {
1623
trace = cdscatter[i][0].trace;
24+
group = trace.stackgroup || '';
25+
if(group) {
26+
if(group in groupIndices) {
27+
groupIndex = groupIndices[group];
28+
}
29+
else {
30+
groupIndex = groupIndices[group] = nextGroupIndex;
31+
nextGroupIndex++;
32+
}
33+
}
34+
else if(trace.fill in LINKEDFILLS && prevUnstackedGroupIndex >= 0) {
35+
groupIndex = prevUnstackedGroupIndex;
36+
}
37+
else {
38+
groupIndex = prevUnstackedGroupIndex = nextGroupIndex;
39+
nextGroupIndex++;
40+
}
41+
42+
if(groupIndex < prevGroupIndex) needsSort = true;
43+
trace._groupIndex = prevGroupIndex = groupIndex;
44+
}
45+
46+
var cdscatterSorted = cdscatter.slice();
47+
if(needsSort) {
48+
cdscatterSorted.sort(function(a, b) {
49+
var traceA = a[0].trace;
50+
var traceB = b[0].trace;
51+
return (traceA._groupIndex - traceB._groupIndex) ||
52+
(traceA.index - traceB.index);
53+
});
54+
}
55+
56+
// now link traces to each other
57+
var prevtraces = {};
58+
for(i = 0; i < cdscatterSorted.length; i++) {
59+
trace = cdscatterSorted[i][0].trace;
60+
group = trace.stackgroup || '';
1761

1862
// Note: The check which ensures all cdscatter here are for the same axis and
1963
// are either cartesian or scatterternary has been removed. This code assumes
@@ -22,17 +66,20 @@ module.exports = function linkTraces(gd, plotinfo, cdscatter) {
2266
if(trace.visible === true) {
2367
trace._nexttrace = null;
2468

25-
if(['tonextx', 'tonexty', 'tonext'].indexOf(trace.fill) !== -1) {
26-
trace._prevtrace = prevtrace;
69+
if(trace.fill in LINKEDFILLS) {
70+
prevtrace = prevtraces[group];
71+
trace._prevtrace = prevtrace || null;
2772

2873
if(prevtrace) {
2974
prevtrace._nexttrace = trace;
3075
}
3176
}
3277

33-
prevtrace = trace;
78+
prevtraces[group] = trace;
3479
} else {
3580
trace._prevtrace = trace._nexttrace = null;
3681
}
3782
}
83+
84+
return cdscatterSorted;
3885
};

src/traces/scatter/plot.js

+9-23
Original file line numberDiff line numberDiff line change
@@ -23,43 +23,29 @@ var linkTraces = require('./link_traces');
2323
var polygonTester = require('../../lib/polygon').tester;
2424

2525
module.exports = function plot(gd, plotinfo, cdscatter, scatterLayer, transitionOpts, makeOnCompleteCallback) {
26-
var i, uids, join, onComplete;
26+
var join, onComplete;
2727

2828
// If transition config is provided, then it is only a partial replot and traces not
2929
// updated are removed.
3030
var isFullReplot = !transitionOpts;
3131
var hasTransition = !!transitionOpts && transitionOpts.duration > 0;
3232

33+
// Link traces so the z-order of fill layers is correct
34+
var cdscatterSorted = linkTraces(gd, plotinfo, cdscatter);
35+
3336
join = scatterLayer.selectAll('g.trace')
34-
.data(cdscatter, function(d) { return d[0].trace.uid; });
37+
.data(cdscatterSorted, function(d) { return d[0].trace.uid; });
3538

3639
// Append new traces:
3740
join.enter().append('g')
3841
.attr('class', function(d) {
3942
return 'trace scatter trace' + d[0].trace.uid;
4043
})
4144
.style('stroke-miterlimit', 2);
42-
43-
// After the elements are created but before they've been draw, we have to perform
44-
// this extra step of linking the traces. This allows appending of fill layers so that
45-
// the z-order of fill layers is correct.
46-
linkTraces(gd, plotinfo, cdscatter);
45+
join.order();
4746

4847
createFills(gd, join, plotinfo);
4948

50-
// Sort the traces, once created, so that the ordering is preserved even when traces
51-
// are shown and hidden. This is needed since we're not just wiping everything out
52-
// and recreating on every update.
53-
for(i = 0, uids = {}; i < cdscatter.length; i++) {
54-
uids[cdscatter[i][0].trace.uid] = i;
55-
}
56-
57-
join.sort(function(a, b) {
58-
var idx1 = uids[a[0].trace.uid];
59-
var idx2 = uids[b[0].trace.uid];
60-
return idx1 - idx2;
61-
});
62-
6349
if(hasTransition) {
6450
if(makeOnCompleteCallback) {
6551
// If it was passed a callback to register completion, make a callback. If
@@ -82,12 +68,12 @@ module.exports = function plot(gd, plotinfo, cdscatter, scatterLayer, transition
8268
// Must run the selection again since otherwise enters/updates get grouped together
8369
// and these get executed out of order. Except we need them in order!
8470
scatterLayer.selectAll('g.trace').each(function(d, i) {
85-
plotOne(gd, i, plotinfo, d, cdscatter, this, transitionOpts);
71+
plotOne(gd, i, plotinfo, d, cdscatterSorted, this, transitionOpts);
8672
});
8773
});
8874
} else {
89-
scatterLayer.selectAll('g.trace').each(function(d, i) {
90-
plotOne(gd, i, plotinfo, d, cdscatter, this, transitionOpts);
75+
join.each(function(d, i) {
76+
plotOne(gd, i, plotinfo, d, cdscatterSorted, this, transitionOpts);
9177
});
9278
}
9379

40.4 KB
Loading
+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"data": [
3+
{
4+
"x": [0, 2, 4], "y": [6, 1, 6], "line": {"color": "#000"},
5+
"name": "bottom", "legendgroup": "l"
6+
}, {
7+
"x0": -0.5, "y": [1, 2, 3, 4, 5], "name": "a 1",
8+
"stackgroup": "a", "legendgroup": "a",
9+
"line": {"color": "#f00"}, "fillcolor": "rgba(255,0,0,0.8)"
10+
}, {
11+
"x0": 0.5, "y": [5, 4, 3, 2, 1], "name": "b 1",
12+
"stackgroup": "b", "legendgroup": "b",
13+
"line": {"color": "#00f"}, "fillcolor": "rgba(0,0,255,0.8)"
14+
}, {
15+
"x0": -0.5, "y": [1, 1, 1, 1, 1], "name": "a 2",
16+
"stackgroup": "a", "legendgroup": "a",
17+
"line": {"color": "#f80"}, "fillcolor": "rgba(255,136,0,0.8)"
18+
}, {
19+
"x0": 1, "y": [1, 2, 1], "name": "unstacked 1",
20+
"fill": "tozeroy", "legendgroup": "u",
21+
"line": {"color": "#888"}, "fillcolor": "rgba(136,136,136,0.8)"
22+
}, {
23+
"x0": -0.5, "y": [1, 1, 1, 1, 1], "name": "a 3",
24+
"stackgroup": "a", "legendgroup": "a",
25+
"line": {"color": "#ff0"}, "fillcolor": "rgba(255,255,0,0.8)"
26+
}, {
27+
"x0": 0.5, "y": [1, 1, 1, 1, 1], "name": "b 2",
28+
"stackgroup": "b", "legendgroup": "b",
29+
"line": {"color": "#80f"}, "fillcolor": "rgba(136,0,255,0.8)"
30+
}, {
31+
"x0": 1, "y": [5, 8, 5], "name": "unstacked 2",
32+
"fill": "tonexty", "legendgroup": "u",
33+
"line": {"color": "#ccc"}, "fillcolor": "rgba(204,204,204,0.8)"
34+
}, {
35+
"x": [0, 2, 4], "y": [7, 3, 7], "line": {"color": "#0c0"},
36+
"name": "top", "legendgroup": "l"
37+
}, {
38+
"x0": 0.5, "y": [1, 1, 1, 1, 1], "name": "b 3",
39+
"stackgroup": "b", "legendgroup": "b",
40+
"line": {"color": "#f0f"}, "fillcolor": "rgba(255,0,255,0.8)"
41+
}
42+
],
43+
"layout": {
44+
"width": 500,
45+
"height": 450,
46+
"title": "Stack groups and unstacked filled traces"
47+
}
48+
}

test/jasmine/tests/scatter_test.js

+33
Original file line numberDiff line numberDiff line change
@@ -1184,6 +1184,39 @@ describe('stacked area', function() {
11841184
.then(done);
11851185
});
11861186

1187+
it('can add/delete stack groups', function(done) {
1188+
var data01 = [
1189+
{mode: 'markers', y: [1, 2, -1, 2, 1], stackgroup: 'a'},
1190+
{mode: 'markers', y: [2, 3, 2, 3, 2], stackgroup: 'b'}
1191+
];
1192+
var data0 = [Lib.extendDeep({}, data01[0])];
1193+
var data1 = [Lib.extendDeep({}, data01[1])];
1194+
1195+
function _assert(yRange, nTraces) {
1196+
expect(gd._fullLayout.yaxis.range).toBeCloseToArray(yRange, 2);
1197+
expect(gd.querySelectorAll('g.trace.scatter').length).toBe(nTraces);
1198+
}
1199+
1200+
Plotly.newPlot(gd, data01)
1201+
.then(function() {
1202+
_assert([-1.293, 3.293], 2);
1203+
return Plotly.react(gd, data0);
1204+
})
1205+
.then(function() {
1206+
_assert([-1.220, 2.220], 1);
1207+
return Plotly.react(gd, data01);
1208+
})
1209+
.then(function() {
1210+
_assert([-1.293, 3.293], 2);
1211+
return Plotly.react(gd, data1);
1212+
})
1213+
.then(function() {
1214+
_assert([0, 3.205], 1);
1215+
})
1216+
.catch(failTest)
1217+
.then(done);
1218+
});
1219+
11871220
it('does not stack on date axes', function(done) {
11881221
Plotly.newPlot(gd, [
11891222
{y: ['2016-01-01', '2017-01-01'], stackgroup: 'a'},

0 commit comments

Comments
 (0)