Skip to content

Commit aeef7f7

Browse files
committed
fix layer ordering of scatter subtypes
1 parent 33e1acf commit aeef7f7

File tree

7 files changed

+209
-95
lines changed

7 files changed

+209
-95
lines changed

src/components/legend/style.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,9 @@ module.exports = function style(s, gd) {
208208

209209
var pts = ptgroup.selectAll('path.scatterpts')
210210
.data(showMarkers ? dMod : []);
211-
pts.enter().append('path').classed('scatterpts', true)
211+
// make sure marker is on the bottom, in case it enters after text
212+
pts.enter().insert('path', ':first-child')
213+
.classed('scatterpts', true)
212214
.attr('transform', 'translate(20,0)');
213215
pts.exit().remove();
214216
pts.call(Drawing.pointStyle, tMod, gd);

src/traces/scatter/plot.js

+57-68
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ var d3 = require('d3');
1313

1414
var Registry = require('../../registry');
1515
var Lib = require('../../lib');
16+
var ensureSingle = Lib.ensureSingle;
17+
var identity = Lib.identity;
1618
var Drawing = require('../../components/drawing');
1719

1820
var subTypes = require('./subtypes');
@@ -43,7 +45,7 @@ module.exports = function plot(gd, plotinfo, cdscatter, scatterLayer, transition
4345
// the z-order of fill layers is correct.
4446
linkTraces(gd, plotinfo, cdscatter);
4547

46-
createFills(gd, scatterLayer, plotinfo);
48+
createFills(gd, join, plotinfo);
4749

4850
// Sort the traces, once created, so that the ordering is preserved even when traces
4951
// are shown and hidden. This is needed since we're not just wiping everything out
@@ -52,10 +54,10 @@ module.exports = function plot(gd, plotinfo, cdscatter, scatterLayer, transition
5254
uids[cdscatter[i][0].trace.uid] = i;
5355
}
5456

55-
scatterLayer.selectAll('g.trace').sort(function(a, b) {
57+
join.sort(function(a, b) {
5658
var idx1 = uids[a[0].trace.uid];
5759
var idx2 = uids[b[0].trace.uid];
58-
return idx1 > idx2 ? 1 : -1;
60+
return idx1 - idx2;
5961
});
6062

6163
if(hasTransition) {
@@ -97,51 +99,45 @@ module.exports = function plot(gd, plotinfo, cdscatter, scatterLayer, transition
9799
scatterLayer.selectAll('path:not([d])').remove();
98100
};
99101

100-
function createFills(gd, scatterLayer, plotinfo) {
101-
var trace;
102+
function createFills(gd, traceJoin, plotinfo) {
103+
traceJoin.each(function(d) {
104+
var fills = ensureSingle(d3.select(this), 'g', 'fills');
105+
Drawing.setClipUrl(fills, plotinfo.layerClipId);
102106

103-
scatterLayer.selectAll('g.trace').each(function(d) {
104-
var tr = d3.select(this);
105-
106-
// Loop only over the traces being redrawn:
107-
trace = d[0].trace;
107+
var trace = d[0].trace;
108108

109-
// make the fill-to-next path now for the NEXT trace, so it shows
110-
// behind both lines.
109+
var fillData = [];
110+
if(trace.fill && (trace.fill.substr(0, 6) === 'tozero' || trace.fill === 'toself' ||
111+
(trace.fill.substr(0, 2) === 'to' && !trace._prevtrace))
112+
) {
113+
fillData = ['_ownFill'];
114+
}
111115
if(trace._nexttrace) {
112-
trace._nextFill = tr.select('.js-fill.js-tonext');
113-
if(!trace._nextFill.size()) {
116+
// make the fill-to-next path now for the NEXT trace, so it shows
117+
// behind both lines.
118+
fillData.push('_nextFill');
119+
}
114120

115-
// If there is an existing tozero fill, we must insert this *after* that fill:
116-
var loc = ':first-child';
117-
if(tr.select('.js-fill.js-tozero').size()) {
118-
loc += ' + *';
119-
}
121+
var fillJoin = fills.selectAll('g')
122+
.data(fillData, identity);
120123

121-
trace._nextFill = tr.insert('path', loc).attr('class', 'js-fill js-tonext');
122-
}
123-
} else {
124-
tr.selectAll('.js-fill.js-tonext').remove();
125-
trace._nextFill = null;
126-
}
124+
fillJoin.enter().append('g');
127125

128-
if(trace.fill && (trace.fill.substr(0, 6) === 'tozero' || trace.fill === 'toself' ||
129-
(trace.fill.substr(0, 2) === 'to' && !trace._prevtrace))) {
130-
trace._ownFill = tr.select('.js-fill.js-tozero');
131-
if(!trace._ownFill.size()) {
132-
trace._ownFill = tr.insert('path', ':first-child').attr('class', 'js-fill js-tozero');
133-
}
134-
} else {
135-
tr.selectAll('.js-fill.js-tozero').remove();
136-
trace._ownFill = null;
137-
}
126+
fillJoin.exit()
127+
.each(function(d) { trace[d] = null; })
128+
.remove();
138129

139-
tr.selectAll('.js-fill').call(Drawing.setClipUrl, plotinfo.layerClipId);
130+
fillJoin.order().each(function(d) {
131+
// make a path element inside the fill group, just so
132+
// we can give it its own data later on and the group can
133+
// keep its simple '_*Fill' data
134+
trace[d] = ensureSingle(d3.select(this), 'path', 'js-fill');
135+
});
140136
});
141137
}
142138

143139
function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transitionOpts) {
144-
var join, i;
140+
var i;
145141

146142
// Since this has been reorganized and we're executing this on individual traces,
147143
// we need to pass it the full list of cdscatter as well as this trace's index (idx)
@@ -157,12 +153,17 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
157153
var xa = plotinfo.xaxis,
158154
ya = plotinfo.yaxis;
159155

160-
var trace = cdscatter[0].trace,
161-
line = trace.line,
162-
tr = d3.select(element);
156+
var trace = cdscatter[0].trace;
157+
var line = trace.line;
158+
var tr = d3.select(element);
159+
160+
var errorBarGroup = ensureSingle(tr, 'g', 'errorbars');
161+
var lines = ensureSingle(tr, 'g', 'lines');
162+
var points = ensureSingle(tr, 'g', 'points');
163+
var text = ensureSingle(tr, 'g', 'text');
163164

164165
// error bars are at the bottom
165-
Registry.getComponentMethod('errorbars', 'plot')(tr, plotinfo, transitionOpts);
166+
Registry.getComponentMethod('errorbars', 'plot')(errorBarGroup, plotinfo, transitionOpts);
166167

167168
if(trace.visible !== true) return;
168169

@@ -303,7 +304,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
303304
};
304305
}
305306

306-
var lineJoin = tr.selectAll('.js-line').data(segments);
307+
var lineJoin = lines.selectAll('.js-line').data(segments);
307308

308309
transition(lineJoin.exit())
309310
.style('opacity', 0)
@@ -325,6 +326,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
325326

326327
if(segments.length) {
327328
if(ownFillEl3) {
329+
ownFillEl3.datum(cdscatter);
328330
if(pt0 && pt1) {
329331
if(ownFillDir) {
330332
if(ownFillDir === 'y') {
@@ -404,11 +406,10 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
404406
return false;
405407
}
406408

407-
function makePoints(d) {
409+
function makePoints(points, text, cdscatter) {
408410
var join, selection, hasNode;
409411

410-
var trace = d[0].trace;
411-
var s = d3.select(this);
412+
var trace = cdscatter[0].trace;
412413
var showMarkers = subTypes.hasMarkers(trace);
413414
var showText = subTypes.hasText(trace);
414415

@@ -417,16 +418,16 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
417418
var textFilter = hideFilter;
418419

419420
if(showMarkers) {
420-
markerFilter = (trace.marker.maxdisplayed || trace._needsCull) ? visFilter : Lib.identity;
421+
markerFilter = (trace.marker.maxdisplayed || trace._needsCull) ? visFilter : identity;
421422
}
422423

423424
if(showText) {
424-
textFilter = (trace.marker.maxdisplayed || trace._needsCull) ? visFilter : Lib.identity;
425+
textFilter = (trace.marker.maxdisplayed || trace._needsCull) ? visFilter : identity;
425426
}
426427

427428
// marker points
428429

429-
selection = s.selectAll('path.point');
430+
selection = points.selectAll('path.point');
430431

431432
join = selection.data(markerFilter, keyFunc);
432433

@@ -478,7 +479,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
478479
}
479480

480481
// text points
481-
selection = s.selectAll('g');
482+
selection = text.selectAll('g');
482483
join = selection.data(textFilter, keyFunc);
483484

484485
// each text needs to go in its own 'g' in case
@@ -517,28 +518,16 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
517518
join.exit().remove();
518519
}
519520

520-
// NB: selectAll is evaluated on instantiation:
521-
var pointSelection = tr.selectAll('.points');
522-
523-
// Join with new data
524-
join = pointSelection.data([cdscatter]);
525-
526-
// Transition existing, but don't defer this to an async .transition since
527-
// there's no timing involved:
528-
pointSelection.each(makePoints);
529-
530-
join.enter().append('g')
531-
.classed('points', true)
532-
.each(makePoints);
533-
534-
join.exit().remove();
521+
points.datum(cdscatter);
522+
text.datum(cdscatter);
523+
makePoints(points, text, cdscatter);
535524

536525
// lastly, clip points groups of `cliponaxis !== false` traces
537526
// on `plotinfo._hasClipOnAxisFalse === true` subplots
538-
join.each(function(d) {
539-
var hasClipOnAxisFalse = d[0].trace.cliponaxis === false;
540-
Drawing.setClipUrl(d3.select(this), hasClipOnAxisFalse ? null : plotinfo.layerClipId);
541-
});
527+
var hasClipOnAxisFalse = trace.cliponaxis === false;
528+
var clipUrl = hasClipOnAxisFalse ? null : plotinfo.layerClipId;
529+
Drawing.setClipUrl(points, clipUrl);
530+
Drawing.setClipUrl(text, clipUrl);
542531
}
543532

544533
function selectMarkers(gd, idx, plotinfo, cdscatter, cdscatterAll) {

src/traces/scatter/style.js

+10
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ function style(gd, cd) {
2626
stylePoints(sel, trace, gd);
2727
});
2828

29+
s.selectAll('g.text').each(function(d) {
30+
var sel = d3.select(this);
31+
var trace = d.trace || d[0].trace;
32+
styleText(sel, trace, gd);
33+
});
34+
2935
s.selectAll('g.trace path.js-line')
3036
.call(Drawing.lineGroupStyle);
3137

@@ -37,6 +43,9 @@ function style(gd, cd) {
3743

3844
function stylePoints(sel, trace, gd) {
3945
Drawing.pointStyle(sel.selectAll('path.point'), trace, gd);
46+
}
47+
48+
function styleText(sel, trace, gd) {
4049
Drawing.textPointStyle(sel.selectAll('text'), trace, gd);
4150
}
4251

@@ -49,6 +58,7 @@ function styleOnSelect(gd, cd) {
4958
Drawing.selectedTextStyle(s.selectAll('text'), trace);
5059
} else {
5160
stylePoints(s, trace, gd);
61+
styleText(s, trace, gd);
5262
}
5363
}
5464

test/jasmine/assets/custom_assertions.js

+22
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,22 @@ exports.assertNodeOrder = function(selectorBehind, selectorInFront, msg) {
298298
}
299299
};
300300

301+
/**
302+
* Ordering test for any number of nodes - calls assertNodeOrder n-1 times.
303+
* Note that we only take the first matching node for each selector, and it's
304+
* not necessary that the nodes be siblings or at the same level of nesting.
305+
*
306+
* @param {Array[string]} selectorArray: css selectors in the order they should
307+
* appear in the document, from back to front.
308+
* @param {string} msg: context for debugging
309+
*/
310+
exports.assertMultiNodeOrder = function(selectorArray, msg) {
311+
for(var i = 0; i < selectorArray.length - 1; i++) {
312+
var msgi = (msg ? msg + ' - ' : '') + 'entries ' + i + ' and ' + (i + 1);
313+
exports.assertNodeOrder(selectorArray[i], selectorArray[i + 1], msgi);
314+
}
315+
};
316+
301317
function getParents(node) {
302318
var parent = node.parentNode;
303319
if(parent) return getParents(parent).concat(node);
@@ -310,3 +326,9 @@ function collectionToArray(collection) {
310326
for(var i = 0; i < len; i++) a[i] = collection[i];
311327
return a;
312328
}
329+
330+
exports.assertD3Data = function(selection, expectedData) {
331+
var data = [];
332+
selection.each(function(d) { data.push(d); });
333+
expect(data).toEqual(expectedData);
334+
};

test/jasmine/tests/cartesian_test.js

+19-21
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ var Drawing = require('@src/components/drawing');
77
var createGraphDiv = require('../assets/create_graph_div');
88
var destroyGraphDiv = require('../assets/destroy_graph_div');
99
var failTest = require('../assets/fail_test');
10+
var assertD3Data = require('../assets/custom_assertions').assertD3Data;
1011

1112
describe('restyle', function() {
1213
describe('scatter traces', function() {
@@ -21,37 +22,35 @@ describe('restyle', function() {
2122
it('reuses SVG fills', function(done) {
2223
var fills, firstToZero, secondToZero, firstToNext, secondToNext;
2324
var mock = Lib.extendDeep({}, require('@mocks/basic_area.json'));
25+
function getFills() {
26+
return d3.selectAll('g.trace.scatter .fills>g');
27+
}
2428

2529
Plotly.plot(gd, mock.data, mock.layout).then(function() {
26-
// Assert there are two fills:
27-
fills = d3.selectAll('g.trace.scatter .js-fill')[0];
30+
fills = getFills();
2831

29-
// First is tozero, second is tonext:
30-
expect(d3.selectAll('g.trace.scatter .js-fill').size()).toEqual(2);
31-
expect(fills[0]).toBeClassed(['js-fill', 'js-tozero']);
32-
expect(fills[1]).toBeClassed(['js-fill', 'js-tonext']);
32+
// Assert there are two fills, first is tozero, second is tonext
33+
assertD3Data(fills, ['_ownFill', '_nextFill']);
34+
35+
firstToZero = fills[0][0];
36+
firstToNext = fills[0][1];
3337

34-
firstToZero = fills[0];
35-
firstToNext = fills[1];
36-
}).then(function() {
3738
return Plotly.restyle(gd, {visible: [false]}, [1]);
3839
}).then(function() {
40+
fills = getFills();
3941
// Trace 1 hidden leaves only trace zero's tozero fill:
40-
expect(d3.selectAll('g.trace.scatter .js-fill').size()).toEqual(1);
41-
expect(fills[0]).toBeClassed(['js-fill', 'js-tozero']);
42-
}).then(function() {
42+
assertD3Data(fills, ['_ownFill']);
43+
4344
return Plotly.restyle(gd, {visible: [true]}, [1]);
4445
}).then(function() {
45-
// Reshow means two fills again AND order is preserved:
46-
fills = d3.selectAll('g.trace.scatter .js-fill')[0];
46+
fills = getFills();
4747

48+
// Reshow means two fills again AND order is preserved
4849
// First is tozero, second is tonext:
49-
expect(d3.selectAll('g.trace.scatter .js-fill').size()).toEqual(2);
50-
expect(fills[0]).toBeClassed(['js-fill', 'js-tozero']);
51-
expect(fills[1]).toBeClassed(['js-fill', 'js-tonext']);
50+
assertD3Data(fills, ['_ownFill', '_nextFill']);
5251

53-
secondToZero = fills[0];
54-
secondToNext = fills[1];
52+
secondToZero = fills[0][0];
53+
secondToNext = fills[0][1];
5554

5655
// The identity of the first is retained:
5756
expect(firstToZero).toBe(secondToZero);
@@ -61,8 +60,7 @@ describe('restyle', function() {
6160

6261
return Plotly.restyle(gd, 'visible', false);
6362
}).then(function() {
64-
expect(d3.selectAll('g.trace.scatter').size()).toEqual(0);
65-
63+
expect(d3.selectAll('g.trace.scatter').size()).toBe(0);
6664
})
6765
.catch(failTest)
6866
.then(done);

0 commit comments

Comments
 (0)