Skip to content

Commit

Permalink
Merge pull request #4058 from plotly/better-below
Browse files Browse the repository at this point in the history
Better mapbox trace / layout-layer *below*
  • Loading branch information
etpinard authored Jul 19, 2019
2 parents 9927079 + 39eff3a commit 66eacc3
Show file tree
Hide file tree
Showing 16 changed files with 582 additions and 125 deletions.
3 changes: 2 additions & 1 deletion src/plots/mapbox/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ module.exports = {
}]
},

controlContainerClassName: 'mapboxgl-control-container',
traceLayerPrefix: 'plotly-trace-layer-',
layoutLayerPrefix: 'plotly-layout-layer-',

wrongVersionErrorMsg: [
'Your custom plotly.js bundle is not using the correct mapbox-gl version',
Expand Down
59 changes: 41 additions & 18 deletions src/plots/mapbox/layers.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@

var Lib = require('../../lib');
var convertTextOpts = require('./convert_text_opts');
var constants = require('./constants');

function MapboxLayer(mapbox, index) {
this.mapbox = mapbox;
this.map = mapbox.map;
function MapboxLayer(subplot, index) {
this.subplot = subplot;

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

this.idSource = this.uid + '-source';
this.idLayer = this.uid + '-layer';
this.idSource = 'source-' + this.uid;
this.idLayer = constants.layoutLayerPrefix + this.uid;

// some state variable to check if a remove/add step is needed
this.sourceType = null;
Expand Down Expand Up @@ -65,12 +66,12 @@ proto.needsNewSource = function(opts) {
proto.needsNewLayer = function(opts) {
return (
this.layerType !== opts.type ||
this.below !== opts.below
this.below !== this.subplot.belowLookup['layout-' + this.index]
);
};

proto.updateSource = function(opts) {
var map = this.map;
var map = this.subplot.map;

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

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

proto.updateLayer = function(opts) {
var map = this.map;
var subplot = this.subplot;
var convertedOpts = convertOpts(opts);

var below = this.subplot.belowLookup['layout-' + this.index];
var _below;

if(below === 'traces') {
var mapLayers = subplot.getMapLayers();

// find id of first plotly trace layer
for(var i = 0; i < mapLayers.length; i++) {
var layerId = mapLayers[i].id;
if(typeof layerId === 'string' &&
layerId.indexOf(constants.traceLayerPrefix) === 0
) {
_below = layerId;
break;
}
}
} else {
_below = below;
}

this.removeLayer();
this.layerType = opts.type;

if(isVisible(opts)) {
map.addLayer({
subplot.addLayer({
id: this.idLayer,
source: this.idSource,
'source-layer': opts.sourcelayer || '',
Expand All @@ -101,27 +121,30 @@ proto.updateLayer = function(opts) {
maxzoom: opts.maxzoom,
layout: convertedOpts.layout,
paint: convertedOpts.paint
}, opts.below);
}, _below);
}

this.layerType = opts.type;
this.below = below;
};

proto.updateStyle = function(opts) {
if(isVisible(opts)) {
var convertedOpts = convertOpts(opts);
this.mapbox.setOptions(this.idLayer, 'setLayoutProperty', convertedOpts.layout);
this.mapbox.setOptions(this.idLayer, 'setPaintProperty', convertedOpts.paint);
this.subplot.setOptions(this.idLayer, 'setLayoutProperty', convertedOpts.layout);
this.subplot.setOptions(this.idLayer, 'setPaintProperty', convertedOpts.paint);
}
};

proto.removeLayer = function() {
var map = this.map;
var map = this.subplot.map;
if(map.getLayer(this.idLayer)) {
map.removeLayer(this.idLayer);
}
};

proto.dispose = function() {
var map = this.map;
var map = this.subplot.map;
map.removeLayer(this.idLayer);
map.removeSource(this.idSource);
};
Expand Down Expand Up @@ -222,8 +245,8 @@ function convertSourceOpts(opts) {
return sourceOpts;
}

module.exports = function createMapboxLayer(mapbox, index, opts) {
var mapboxLayer = new MapboxLayer(mapbox, index);
module.exports = function createMapboxLayer(subplot, index, opts) {
var mapboxLayer = new MapboxLayer(subplot, index);

mapboxLayer.update(opts);

Expand Down
1 change: 0 additions & 1 deletion src/plots/mapbox/layout_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,6 @@ var attrs = module.exports = overrideAll({
// attributes shared between all types
below: {
valType: 'string',
dflt: '',
role: 'info',
description: [
'Determines if the layer will be inserted',
Expand Down
122 changes: 122 additions & 0 deletions src/plots/mapbox/mapbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ function Mapbox(gd, id) {
this.styleObj = null;
this.traceHash = {};
this.layerList = [];
this.belowLookup = {};
}

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

Promise.all(promises).then(function() {
self.fillBelowLookup(calcData, fullLayout);
self.updateData(calcData);
self.updateLayout(fullLayout);
self.resolveOnRender(resolve);
Expand Down Expand Up @@ -191,12 +193,94 @@ proto.updateMap = function(calcData, fullLayout, resolve, reject) {
promises = promises.concat(self.fetchMapData(calcData, fullLayout));

Promise.all(promises).then(function() {
self.fillBelowLookup(calcData, fullLayout);
self.updateData(calcData);
self.updateLayout(fullLayout);
self.resolveOnRender(resolve);
}).catch(reject);
};

proto.fillBelowLookup = function(calcData, fullLayout) {
var opts = fullLayout[this.id];
var layers = opts.layers;
var i, val;

var belowLookup = this.belowLookup = {};
var hasTraceAtTop = false;

for(i = 0; i < calcData.length; i++) {
var trace = calcData[i][0].trace;
var _module = trace._module;

if(typeof trace.below === 'string') {
val = trace.below;
} else if(_module.getBelow) {
// 'smart' default that depend the map's base layers
val = _module.getBelow(trace, this);
}

if(val === '') {
hasTraceAtTop = true;
}

belowLookup['trace-' + trace.uid] = val || '';
}

for(i = 0; i < layers.length; i++) {
var item = layers[i];

if(typeof item.below === 'string') {
val = item.below;
} else if(hasTraceAtTop) {
// if one or more trace(s) set `below:''` and
// layers[i].below is unset,
// place layer below traces
val = 'traces';
} else {
val = '';
}

belowLookup['layout-' + i] = val;
}

// N.B. If multiple layers have the 'below' value,
// we must clear the stashed 'below' field in order
// to make `traceHash[k].update()` and `layerList[i].update()`
// remove/add the all those layers to have preserve
// the correct layer ordering
var val2list = {};
var k, id;

for(k in belowLookup) {
val = belowLookup[k];
if(val2list[val]) {
val2list[val].push(k);
} else {
val2list[val] = [k];
}
}

for(val in val2list) {
var list = val2list[val];
if(list.length > 1) {
for(i = 0; i < list.length; i++) {
k = list[i];
if(k.indexOf('trace-') === 0) {
id = k.split('trace-')[1];
if(this.traceHash[id]) {
this.traceHash[id].below = null;
}
} else if(k.indexOf('layout-') === 0) {
id = k.split('layout-')[1];
if(this.layerList[id]) {
this.layerList[id].below = null;
}
}
}
}
}
};

var traceType2orderIndex = {
choroplethmapbox: 0,
densitymapbox: 1,
Expand All @@ -207,6 +291,10 @@ proto.updateData = function(calcData) {
var traceHash = this.traceHash;
var traceObj, trace, i, j;

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

proto.getMapLayers = function() {
return this.map.getStyle().layers;
};

// convenience wrapper that first check in 'below' references
// a layer that exist and then add the layer to the map,
proto.addLayer = function(opts, below) {
var map = this.map;

if(typeof below === 'string') {
if(below === '') {
map.addLayer(opts, below);
return;
}

var mapLayers = this.getMapLayers();
for(var i = 0; i < mapLayers.length; i++) {
if(below === mapLayers[i].id) {
map.addLayer(opts, below);
return;
}
}

Lib.warn([
'Trying to add layer with *below* value',
below,
'referencing a layer that does not exist',
'or that does not yet exist.'
].join(' '));
}

map.addLayer(opts);
};

// convenience method to project a [lon, lat] array to pixel coords
proto.project = function(v) {
return this.map.project(new mapboxgl.LngLat(v[0], v[1]));
Expand Down
1 change: 0 additions & 1 deletion src/traces/choroplethmapbox/convert.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,6 @@ function convert(calcTrace) {
line.layout.visibility = 'visible';

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

convertOnSelect(calcTrace);

Expand Down
24 changes: 24 additions & 0 deletions src/traces/choroplethmapbox/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,30 @@ module.exports = {
}
},

getBelow: function(trace, subplot) {
var mapLayers = subplot.getMapLayers();

// find layer just above top-most "water" layer
// that is not a plotly layer
for(var i = mapLayers.length - 2; i >= 0; i--) {
var layerId = mapLayers[i].id;

if(typeof layerId === 'string' &&
layerId.indexOf('water') === 0
) {
for(var j = i + 1; j < mapLayers.length; j++) {
layerId = mapLayers[j].id;

if(typeof layerId === 'string' &&
layerId.indexOf('plotly-') === -1
) {
return layerId;
}
}
}
}
},

moduleType: 'trace',
name: 'choroplethmapbox',
basePlotModule: require('../../plots/mapbox'),
Expand Down
Loading

0 comments on commit 66eacc3

Please sign in to comment.