Skip to content

Commit f034c62

Browse files
authored
Merge pull request #2978 from plotly/scatter-layer-order
fix layer ordering of scatter subtypes
2 parents 2a667d0 + 6988818 commit f034c62

File tree

9 files changed

+242
-95
lines changed

9 files changed

+242
-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

+56-67
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') {
@@ -412,11 +414,10 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
412414
return false;
413415
}
414416

415-
function makePoints(d) {
417+
function makePoints(points, text, cdscatter) {
416418
var join, selection, hasNode;
417419

418-
var trace = d[0].trace;
419-
var s = d3.select(this);
420+
var trace = cdscatter[0].trace;
420421
var showMarkers = subTypes.hasMarkers(trace);
421422
var showText = subTypes.hasText(trace);
422423

@@ -425,7 +426,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
425426
var textFilter = hideFilter;
426427

427428
if(showMarkers || showText) {
428-
var showFilter = Lib.identity;
429+
var showFilter = identity;
429430
// if we're stacking, "infer zero" gap mode gets markers in the
430431
// gap points - because we've inferred a zero there - but other
431432
// modes (currently "interpolate", later "interrupt" hopefully)
@@ -446,7 +447,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
446447

447448
// marker points
448449

449-
selection = s.selectAll('path.point');
450+
selection = points.selectAll('path.point');
450451

451452
join = selection.data(markerFilter, keyFunc);
452453

@@ -498,7 +499,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
498499
}
499500

500501
// text points
501-
selection = s.selectAll('g');
502+
selection = text.selectAll('g');
502503
join = selection.data(textFilter, keyFunc);
503504

504505
// each text needs to go in its own 'g' in case
@@ -537,28 +538,16 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
537538
join.exit().remove();
538539
}
539540

540-
// NB: selectAll is evaluated on instantiation:
541-
var pointSelection = tr.selectAll('.points');
542-
543-
// Join with new data
544-
join = pointSelection.data([cdscatter]);
545-
546-
// Transition existing, but don't defer this to an async .transition since
547-
// there's no timing involved:
548-
pointSelection.each(makePoints);
549-
550-
join.enter().append('g')
551-
.classed('points', true)
552-
.each(makePoints);
553-
554-
join.exit().remove();
541+
points.datum(cdscatter);
542+
text.datum(cdscatter);
543+
makePoints(points, text, cdscatter);
555544

556545
// lastly, clip points groups of `cliponaxis !== false` traces
557546
// on `plotinfo._hasClipOnAxisFalse === true` subplots
558-
join.each(function(d) {
559-
var hasClipOnAxisFalse = d[0].trace.cliponaxis === false;
560-
Drawing.setClipUrl(d3.select(this), hasClipOnAxisFalse ? null : plotinfo.layerClipId);
561-
});
547+
var hasClipOnAxisFalse = trace.cliponaxis === false;
548+
var clipUrl = hasClipOnAxisFalse ? null : plotinfo.layerClipId;
549+
Drawing.setClipUrl(points, clipUrl);
550+
Drawing.setClipUrl(text, clipUrl);
562551
}
563552

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

src/traces/scatter/style.js

+11
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,11 +58,13 @@ 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

5565
module.exports = {
5666
style: style,
5767
stylePoints: stylePoints,
68+
styleText: styleText,
5869
styleOnSelect: styleOnSelect
5970
};

src/traces/scattergeo/style.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ var d3 = require('d3');
1212
var Drawing = require('../../components/drawing');
1313
var Color = require('../../components/color');
1414

15-
var stylePoints = require('../scatter/style').stylePoints;
15+
var scatterStyle = require('../scatter/style');
16+
var stylePoints = scatterStyle.stylePoints;
17+
var styleText = scatterStyle.styleText;
1618

1719
module.exports = function style(gd, calcTrace) {
1820
if(calcTrace) styleTrace(gd, calcTrace);
@@ -25,6 +27,7 @@ function styleTrace(gd, calcTrace) {
2527
s.style('opacity', calcTrace[0].trace.opacity);
2628

2729
stylePoints(s, trace, gd);
30+
styleText(s, trace, gd);
2831

2932
// this part is incompatible with Drawing.lineGroupStyle
3033
s.selectAll('path.js-line')

test/jasmine/assets/custom_assertions.js

+22
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,22 @@ exports.assertNodeOrder = function(selectorBehind, selectorInFront, msg) {
312312
}
313313
};
314314

315+
/**
316+
* Ordering test for any number of nodes - calls assertNodeOrder n-1 times.
317+
* Note that we only take the first matching node for each selector, and it's
318+
* not necessary that the nodes be siblings or at the same level of nesting.
319+
*
320+
* @param {Array[string]} selectorArray: css selectors in the order they should
321+
* appear in the document, from back to front.
322+
* @param {string} msg: context for debugging
323+
*/
324+
exports.assertMultiNodeOrder = function(selectorArray, msg) {
325+
for(var i = 0; i < selectorArray.length - 1; i++) {
326+
var msgi = (msg ? msg + ' - ' : '') + 'entries ' + i + ' and ' + (i + 1);
327+
exports.assertNodeOrder(selectorArray[i], selectorArray[i + 1], msgi);
328+
}
329+
};
330+
315331
function getParents(node) {
316332
var parent = node.parentNode;
317333
if(parent) return getParents(parent).concat(node);
@@ -324,3 +340,9 @@ function collectionToArray(collection) {
324340
for(var i = 0; i < len; i++) a[i] = collection[i];
325341
return a;
326342
}
343+
344+
exports.assertD3Data = function(selection, expectedData) {
345+
var data = [];
346+
selection.each(function(d) { data.push(d); });
347+
expect(data).toEqual(expectedData);
348+
};

test/jasmine/assets/transitions.js

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use strict';
2+
3+
/**
4+
* Given n states (denoted by their indices 0..n-1) this routine produces
5+
* a sequence of indices such that you efficiently execute each transition
6+
* from any state to any other state.
7+
*/
8+
module.exports = function transitions(n) {
9+
var out = [0];
10+
var nextStates = [];
11+
var i;
12+
for(i = 0; i < n; i++) nextStates[i] = (i + 1) % n;
13+
var finishedStates = 0;
14+
var thisState = 0;
15+
var nextState;
16+
while(finishedStates < n) {
17+
nextState = nextStates[thisState];
18+
if(nextState === thisState) {
19+
// I don't actually know how to prove that this algorithm works,
20+
// but I've never seen it fail for n>1
21+
// For prime n it's the same sequence as the one I started with
22+
// (n transitions of +1 index, then n transitions +2 etc...)
23+
// but this one works for non-prime n as well.
24+
throw new Error('your transitions algo failed.');
25+
}
26+
nextStates[thisState] = (nextStates[thisState] + 1) % n;
27+
if(nextStates[thisState] === thisState) finishedStates++;
28+
out.push(nextState);
29+
thisState = nextState;
30+
}
31+
if(out.length !== n * (n - 1) + 1) {
32+
throw new Error('your transitions algo failed.');
33+
}
34+
return out;
35+
};

0 commit comments

Comments
 (0)