Skip to content

Commit 8a4e8f1

Browse files
committed
add below to scattermapbox + handle below:'traces' in layout layers
We now handle below logic (mostly) at the subplot level in subplot.fillBelowLookup() and subplot.belowLookup. This allows us to handle many edges cases in a cleaner way. In brief, + we collect set trace/layout-layer below value, OR find their "smart" default value + each trace/layout-layer stash a below state value + if new below doesn't match old before, we must remove/add layer + if many traces/layout have same below value, we place choroplethmapbox, then densitymapbox, scattermapbox and finally layout layers in that order. + on update, if many traces/layout-layer have same below, we must in general remove/add all those layers to have the correct ordering Additional notes: - we no longer need to handle below in convert.js routines - getBelow is now a trace _module.getBelow method, so that it can get called before mapbox-trace object creation - new subplot.getMapLayer method to DRY things up
1 parent f51d66b commit 8a4e8f1

File tree

15 files changed

+438
-86
lines changed

15 files changed

+438
-86
lines changed

Diff for: src/plots/mapbox/layers.js

+26-3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ function MapboxLayer(subplot, index) {
1717
this.map = subplot.map;
1818

1919
this.uid = subplot.uid + '-' + index;
20+
this.index = index;
2021

2122
this.idSource = 'source-' + this.uid;
2223
this.idLayer = constants.layoutLayerPrefix + this.uid;
@@ -66,7 +67,7 @@ proto.needsNewSource = function(opts) {
6667
proto.needsNewLayer = function(opts) {
6768
return (
6869
this.layerType !== opts.type ||
69-
this.below !== opts.below
70+
this.below !== this.subplot.belowLookup['layout-' + this.index]
7071
);
7172
};
7273

@@ -89,8 +90,27 @@ proto.updateLayer = function(opts) {
8990
var map = this.map;
9091
var convertedOpts = convertOpts(opts);
9192

93+
var below = this.subplot.belowLookup['layout-' + this.index];
94+
var _below;
95+
96+
if(below === 'traces') {
97+
var mapLayers = this.subplot.getMapLayers();
98+
99+
// find id of first plotly trace layer
100+
for(var i = 0; i < mapLayers.length; i++) {
101+
var layerId = mapLayers[i].id;
102+
if(typeof layerId === 'string' &&
103+
layerId.indexOf(constants.traceLayerPrefix) === 0
104+
) {
105+
_below = layerId;
106+
break;
107+
}
108+
}
109+
} else {
110+
_below = below;
111+
}
112+
92113
this.removeLayer();
93-
this.layerType = opts.type;
94114

95115
if(isVisible(opts)) {
96116
map.addLayer({
@@ -102,8 +122,11 @@ proto.updateLayer = function(opts) {
102122
maxzoom: opts.maxzoom,
103123
layout: convertedOpts.layout,
104124
paint: convertedOpts.paint
105-
}, opts.below);
125+
}, _below);
106126
}
127+
128+
this.layerType = opts.type;
129+
this.below = below;
107130
};
108131

109132
proto.updateStyle = function(opts) {

Diff for: src/plots/mapbox/layout_attributes.js

-1
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,6 @@ var attrs = module.exports = overrideAll({
158158
// attributes shared between all types
159159
below: {
160160
valType: 'string',
161-
dflt: '',
162161
role: 'info',
163162
description: [
164163
'Determines if the layer will be inserted',

Diff for: src/plots/mapbox/mapbox.js

+92
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ function Mapbox(gd, id) {
4848
this.styleObj = null;
4949
this.traceHash = {};
5050
this.layerList = [];
51+
this.belowLookup = {};
5152
}
5253

5354
var proto = Mapbox.prototype;
@@ -126,6 +127,7 @@ proto.createMap = function(calcData, fullLayout, resolve, reject) {
126127
promises = promises.concat(self.fetchMapData(calcData, fullLayout));
127128

128129
Promise.all(promises).then(function() {
130+
self.fillBelowLookup(calcData, fullLayout);
129131
self.updateData(calcData);
130132
self.updateLayout(fullLayout);
131133
self.resolveOnRender(resolve);
@@ -191,12 +193,94 @@ proto.updateMap = function(calcData, fullLayout, resolve, reject) {
191193
promises = promises.concat(self.fetchMapData(calcData, fullLayout));
192194

193195
Promise.all(promises).then(function() {
196+
self.fillBelowLookup(calcData, fullLayout);
194197
self.updateData(calcData);
195198
self.updateLayout(fullLayout);
196199
self.resolveOnRender(resolve);
197200
}).catch(reject);
198201
};
199202

203+
proto.fillBelowLookup = function(calcData, fullLayout) {
204+
var opts = fullLayout[this.id];
205+
var layers = opts.layers;
206+
var i, val;
207+
208+
var belowLookup = this.belowLookup = {};
209+
var hasTraceAtTop = false;
210+
211+
for(i = 0; i < calcData.length; i++) {
212+
var trace = calcData[i][0].trace;
213+
var _module = trace._module;
214+
215+
if(typeof trace.below === 'string') {
216+
val = trace.below;
217+
} else if(_module.getBelow) {
218+
// 'smart' default that depend the map's base layers
219+
val = _module.getBelow(trace, this);
220+
}
221+
222+
if(val === '') {
223+
hasTraceAtTop = true;
224+
}
225+
226+
belowLookup['trace-' + trace.uid] = val || '';
227+
}
228+
229+
for(i = 0; i < layers.length; i++) {
230+
var item = layers[i];
231+
232+
if(typeof item.below === 'string') {
233+
val = item.below;
234+
} else if(hasTraceAtTop) {
235+
// if one or more trace(s) set `below:''` and
236+
// layers[i].below is unset,
237+
// place layer below traces
238+
val = 'traces';
239+
} else {
240+
val = '';
241+
}
242+
243+
belowLookup['layout-' + i] = val;
244+
}
245+
246+
// N.B. If multiple layers have the 'below' value,
247+
// we must clear the stashed 'below' field in order
248+
// to make `traceHash[k].update()` and `layerList[i].update()`
249+
// remove/add the all those layers to have preserve
250+
// the correct layer ordering
251+
var val2list = {};
252+
var k, id;
253+
254+
for(k in belowLookup) {
255+
val = belowLookup[k];
256+
if(val2list[val]) {
257+
val2list[val].push(k);
258+
} else {
259+
val2list[val] = [k];
260+
}
261+
}
262+
263+
for(val in val2list) {
264+
var list = val2list[val];
265+
if(list.length > 1) {
266+
for(i = 0; i < list.length; i++) {
267+
k = list[i];
268+
if(k.indexOf('trace-') === 0) {
269+
id = k.split('trace-')[1];
270+
if(this.traceHash[id]) {
271+
this.traceHash[id].below = null;
272+
}
273+
} else if(k.indexOf('layout-') === 0) {
274+
id = k.split('layout-')[1];
275+
if(this.layerList[id]) {
276+
this.layerList[id].below = null;
277+
}
278+
}
279+
}
280+
}
281+
}
282+
};
283+
200284
var traceType2orderIndex = {
201285
choroplethmapbox: 0,
202286
densitymapbox: 1,
@@ -207,6 +291,10 @@ proto.updateData = function(calcData) {
207291
var traceHash = this.traceHash;
208292
var traceObj, trace, i, j;
209293

294+
// Need to sort here by trace type here,
295+
// in case traces with different `type` have the same
296+
// below value, but sorting we ensure that
297+
// e.g. choroplethmapbox traces will be below scattermapbox traces
210298
var calcDataSorted = calcData.slice().sort(function(a, b) {
211299
return (
212300
traceType2orderIndex[a[0].trace.type] -
@@ -598,6 +686,10 @@ proto.setOptions = function(id, methodName, opts) {
598686
}
599687
};
600688

689+
proto.getMapLayers = function() {
690+
return this.map.getStyle().layers;
691+
};
692+
601693
// convenience method to project a [lon, lat] array to pixel coords
602694
proto.project = function(v) {
603695
return this.map.project(new mapboxgl.LngLat(v[0], v[1]));

Diff for: src/traces/choroplethmapbox/convert.js

-1
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,6 @@ function convert(calcTrace) {
176176
line.layout.visibility = 'visible';
177177

178178
opts.geojson = {type: 'FeatureCollection', features: featuresOut};
179-
opts.below = trace.below;
180179

181180
convertOnSelect(calcTrace);
182181

Diff for: src/traces/choroplethmapbox/index.js

+24
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,30 @@ module.exports = {
2525
}
2626
},
2727

28+
getBelow: function(trace, subplot) {
29+
var mapLayers = subplot.getMapLayers();
30+
31+
// find layer just above top-most "water" layer
32+
// that is not a plotly layer
33+
for(var i = mapLayers.length - 2; i >= 0; i--) {
34+
var layerId = mapLayers[i].id;
35+
36+
if(typeof layerId === 'string' &&
37+
layerId.indexOf('water') === 0
38+
) {
39+
for(var j = i + 1; j < mapLayers.length; j++) {
40+
layerId = mapLayers[j].id;
41+
42+
if(typeof layerId === 'string' &&
43+
layerId.indexOf('plotly-') === -1
44+
) {
45+
return layerId;
46+
}
47+
}
48+
}
49+
}
50+
},
51+
2852
moduleType: 'trace',
2953
name: 'choroplethmapbox',
3054
basePlotModule: require('../../plots/mapbox'),

Diff for: src/traces/choroplethmapbox/plot.js

+7-36
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,16 @@ proto.updateOnSelect = function(calcTrace) {
4242
proto._update = function(optsAll) {
4343
var subplot = this.subplot;
4444
var layerList = this.layerList;
45+
var below = subplot.belowLookup['trace-' + this.uid];
4546

4647
subplot.map
4748
.getSource(this.sourceId)
4849
.setData(optsAll.geojson);
4950

50-
if(optsAll.below !== this.below) {
51+
if(below !== this.below) {
5152
this._removeLayers();
52-
this._addLayers(optsAll);
53+
this._addLayers(optsAll, below);
54+
this.below = below;
5355
}
5456

5557
for(var i = 0; i < layerList.length; i++) {
@@ -66,11 +68,10 @@ proto._update = function(optsAll) {
6668
}
6769
};
6870

69-
proto._addLayers = function(optsAll) {
71+
proto._addLayers = function(optsAll, below) {
7072
var subplot = this.subplot;
7173
var layerList = this.layerList;
7274
var sourceId = this.sourceId;
73-
var below = this.getBelow(optsAll);
7475

7576
for(var i = 0; i < layerList.length; i++) {
7677
var item = layerList[i];
@@ -85,8 +86,6 @@ proto._addLayers = function(optsAll) {
8586
paint: opts.paint
8687
}, below);
8788
}
88-
89-
this.below = below;
9089
};
9190

9291
proto._removeLayers = function() {
@@ -104,47 +103,19 @@ proto.dispose = function() {
104103
map.removeSource(this.sourceId);
105104
};
106105

107-
proto.getBelow = function(optsAll) {
108-
if(optsAll.below !== undefined) {
109-
return optsAll.below;
110-
}
111-
112-
var mapLayers = this.subplot.map.getStyle().layers;
113-
var out = '';
114-
115-
// find layer just above top-most "water" layer
116-
for(var i = 0; i < mapLayers.length; i++) {
117-
var layerId = mapLayers[i].id;
118-
119-
if(typeof layerId === 'string') {
120-
var isWaterLayer = layerId.indexOf('water') === 0;
121-
122-
if(out && !isWaterLayer) {
123-
out = layerId;
124-
break;
125-
}
126-
if(isWaterLayer) {
127-
out = layerId;
128-
}
129-
}
130-
}
131-
132-
return out;
133-
};
134-
135106
module.exports = function createChoroplethMapbox(subplot, calcTrace) {
136107
var trace = calcTrace[0].trace;
137108
var choroplethMapbox = new ChoroplethMapbox(subplot, trace.uid);
138109
var sourceId = choroplethMapbox.sourceId;
139-
140110
var optsAll = convert(calcTrace);
111+
var below = choroplethMapbox.below = subplot.belowLookup['trace-' + trace.uid];
141112

142113
subplot.map.addSource(sourceId, {
143114
type: 'geojson',
144115
data: optsAll.geojson
145116
});
146117

147-
choroplethMapbox._addLayers(optsAll);
118+
choroplethMapbox._addLayers(optsAll, below);
148119

149120
// link ref for quick update during selections
150121
calcTrace[0].trace._glTrace = choroplethMapbox;

Diff for: src/traces/densitymapbox/convert.js

-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,6 @@ module.exports = function convert(calcTrace) {
109109

110110
opts.geojson = {type: 'FeatureCollection', features: features};
111111
opts.heatmap.layout.visibility = 'visible';
112-
opts.below = trace.below;
113112

114113
return opts;
115114
};

Diff for: src/traces/densitymapbox/index.js

+16
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,22 @@ module.exports = {
1717
hoverPoints: require('./hover'),
1818
eventData: require('./event_data'),
1919

20+
getBelow: function(trace, subplot) {
21+
var mapLayers = subplot.getMapLayers();
22+
23+
// find first layer with `type: 'symbol'`,
24+
// that is not a plotly layer
25+
for(var i = 0; i < mapLayers.length; i++) {
26+
var layer = mapLayers[i];
27+
var layerId = layer.id;
28+
if(layer.type === 'symbol' &&
29+
typeof layerId === 'string' && layerId.indexOf('plotly-') === -1
30+
) {
31+
return layerId;
32+
}
33+
}
34+
},
35+
2036
moduleType: 'trace',
2137
name: 'densitymapbox',
2238
basePlotModule: require('../../plots/mapbox'),

0 commit comments

Comments
 (0)