Skip to content

Commit 66eacc3

Browse files
authored
Merge pull request #4058 from plotly/better-below
Better mapbox trace / layout-layer *below*
2 parents 9927079 + 39eff3a commit 66eacc3

File tree

16 files changed

+582
-125
lines changed

16 files changed

+582
-125
lines changed

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ module.exports = {
4242
}]
4343
},
4444

45-
controlContainerClassName: 'mapboxgl-control-container',
45+
traceLayerPrefix: 'plotly-trace-layer-',
46+
layoutLayerPrefix: 'plotly-layout-layer-',
4647

4748
wrongVersionErrorMsg: [
4849
'Your custom plotly.js bundle is not using the correct mapbox-gl version',

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

+41-18
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,16 @@
1010

1111
var Lib = require('../../lib');
1212
var convertTextOpts = require('./convert_text_opts');
13+
var constants = require('./constants');
1314

14-
function MapboxLayer(mapbox, index) {
15-
this.mapbox = mapbox;
16-
this.map = mapbox.map;
15+
function MapboxLayer(subplot, index) {
16+
this.subplot = subplot;
1717

18-
this.uid = mapbox.uid + '-' + 'layer' + index;
18+
this.uid = subplot.uid + '-' + index;
19+
this.index = index;
1920

20-
this.idSource = this.uid + '-source';
21-
this.idLayer = this.uid + '-layer';
21+
this.idSource = 'source-' + this.uid;
22+
this.idLayer = constants.layoutLayerPrefix + this.uid;
2223

2324
// some state variable to check if a remove/add step is needed
2425
this.sourceType = null;
@@ -65,12 +66,12 @@ proto.needsNewSource = function(opts) {
6566
proto.needsNewLayer = function(opts) {
6667
return (
6768
this.layerType !== opts.type ||
68-
this.below !== opts.below
69+
this.below !== this.subplot.belowLookup['layout-' + this.index]
6970
);
7071
};
7172

7273
proto.updateSource = function(opts) {
73-
var map = this.map;
74+
var map = this.subplot.map;
7475

7576
if(map.getSource(this.idSource)) map.removeSource(this.idSource);
7677

@@ -85,14 +86,33 @@ proto.updateSource = function(opts) {
8586
};
8687

8788
proto.updateLayer = function(opts) {
88-
var map = this.map;
89+
var subplot = this.subplot;
8990
var convertedOpts = convertOpts(opts);
9091

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

94114
if(isVisible(opts)) {
95-
map.addLayer({
115+
subplot.addLayer({
96116
id: this.idLayer,
97117
source: this.idSource,
98118
'source-layer': opts.sourcelayer || '',
@@ -101,27 +121,30 @@ proto.updateLayer = function(opts) {
101121
maxzoom: opts.maxzoom,
102122
layout: convertedOpts.layout,
103123
paint: convertedOpts.paint
104-
}, opts.below);
124+
}, _below);
105125
}
126+
127+
this.layerType = opts.type;
128+
this.below = below;
106129
};
107130

108131
proto.updateStyle = function(opts) {
109132
if(isVisible(opts)) {
110133
var convertedOpts = convertOpts(opts);
111-
this.mapbox.setOptions(this.idLayer, 'setLayoutProperty', convertedOpts.layout);
112-
this.mapbox.setOptions(this.idLayer, 'setPaintProperty', convertedOpts.paint);
134+
this.subplot.setOptions(this.idLayer, 'setLayoutProperty', convertedOpts.layout);
135+
this.subplot.setOptions(this.idLayer, 'setPaintProperty', convertedOpts.paint);
113136
}
114137
};
115138

116139
proto.removeLayer = function() {
117-
var map = this.map;
140+
var map = this.subplot.map;
118141
if(map.getLayer(this.idLayer)) {
119142
map.removeLayer(this.idLayer);
120143
}
121144
};
122145

123146
proto.dispose = function() {
124-
var map = this.map;
147+
var map = this.subplot.map;
125148
map.removeLayer(this.idLayer);
126149
map.removeSource(this.idSource);
127150
};
@@ -222,8 +245,8 @@ function convertSourceOpts(opts) {
222245
return sourceOpts;
223246
}
224247

225-
module.exports = function createMapboxLayer(mapbox, index, opts) {
226-
var mapboxLayer = new MapboxLayer(mapbox, index);
248+
module.exports = function createMapboxLayer(subplot, index, opts) {
249+
var mapboxLayer = new MapboxLayer(subplot, index);
227250

228251
mapboxLayer.update(opts);
229252

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

+122
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,40 @@ proto.setOptions = function(id, methodName, opts) {
598686
}
599687
};
600688

689+
proto.getMapLayers = function() {
690+
return this.map.getStyle().layers;
691+
};
692+
693+
// convenience wrapper that first check in 'below' references
694+
// a layer that exist and then add the layer to the map,
695+
proto.addLayer = function(opts, below) {
696+
var map = this.map;
697+
698+
if(typeof below === 'string') {
699+
if(below === '') {
700+
map.addLayer(opts, below);
701+
return;
702+
}
703+
704+
var mapLayers = this.getMapLayers();
705+
for(var i = 0; i < mapLayers.length; i++) {
706+
if(below === mapLayers[i].id) {
707+
map.addLayer(opts, below);
708+
return;
709+
}
710+
}
711+
712+
Lib.warn([
713+
'Trying to add layer with *below* value',
714+
below,
715+
'referencing a layer that does not exist',
716+
'or that does not yet exist.'
717+
].join(' '));
718+
}
719+
720+
map.addLayer(opts);
721+
};
722+
601723
// convenience method to project a [lon, lat] array to pixel coords
602724
proto.project = function(v) {
603725
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'),

0 commit comments

Comments
 (0)