Skip to content

Commit

Permalink
add below to scattermapbox + handle below:'traces' in layout layers
Browse files Browse the repository at this point in the history
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
  • Loading branch information
etpinard committed Jul 17, 2019
1 parent f51d66b commit 8a4e8f1
Show file tree
Hide file tree
Showing 15 changed files with 438 additions and 86 deletions.
29 changes: 26 additions & 3 deletions src/plots/mapbox/layers.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ function MapboxLayer(subplot, index) {
this.map = subplot.map;

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

this.idSource = 'source-' + this.uid;
this.idLayer = constants.layoutLayerPrefix + this.uid;
Expand Down Expand Up @@ -66,7 +67,7 @@ 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]
);
};

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

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

if(below === 'traces') {
var mapLayers = this.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({
Expand All @@ -102,8 +122,11 @@ 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) {
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
92 changes: 92 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,10 @@ proto.setOptions = function(id, methodName, opts) {
}
};

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

// 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
43 changes: 7 additions & 36 deletions src/traces/choroplethmapbox/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,16 @@ proto.updateOnSelect = function(calcTrace) {
proto._update = function(optsAll) {
var subplot = this.subplot;
var layerList = this.layerList;
var below = subplot.belowLookup['trace-' + this.uid];

subplot.map
.getSource(this.sourceId)
.setData(optsAll.geojson);

if(optsAll.below !== this.below) {
if(below !== this.below) {
this._removeLayers();
this._addLayers(optsAll);
this._addLayers(optsAll, below);
this.below = below;
}

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

proto._addLayers = function(optsAll) {
proto._addLayers = function(optsAll, below) {
var subplot = this.subplot;
var layerList = this.layerList;
var sourceId = this.sourceId;
var below = this.getBelow(optsAll);

for(var i = 0; i < layerList.length; i++) {
var item = layerList[i];
Expand All @@ -85,8 +86,6 @@ proto._addLayers = function(optsAll) {
paint: opts.paint
}, below);
}

this.below = below;
};

proto._removeLayers = function() {
Expand All @@ -104,47 +103,19 @@ proto.dispose = function() {
map.removeSource(this.sourceId);
};

proto.getBelow = function(optsAll) {
if(optsAll.below !== undefined) {
return optsAll.below;
}

var mapLayers = this.subplot.map.getStyle().layers;
var out = '';

// find layer just above top-most "water" layer
for(var i = 0; i < mapLayers.length; i++) {
var layerId = mapLayers[i].id;

if(typeof layerId === 'string') {
var isWaterLayer = layerId.indexOf('water') === 0;

if(out && !isWaterLayer) {
out = layerId;
break;
}
if(isWaterLayer) {
out = layerId;
}
}
}

return out;
};

module.exports = function createChoroplethMapbox(subplot, calcTrace) {
var trace = calcTrace[0].trace;
var choroplethMapbox = new ChoroplethMapbox(subplot, trace.uid);
var sourceId = choroplethMapbox.sourceId;

var optsAll = convert(calcTrace);
var below = choroplethMapbox.below = subplot.belowLookup['trace-' + trace.uid];

subplot.map.addSource(sourceId, {
type: 'geojson',
data: optsAll.geojson
});

choroplethMapbox._addLayers(optsAll);
choroplethMapbox._addLayers(optsAll, below);

// link ref for quick update during selections
calcTrace[0].trace._glTrace = choroplethMapbox;
Expand Down
1 change: 0 additions & 1 deletion src/traces/densitymapbox/convert.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ module.exports = function convert(calcTrace) {

opts.geojson = {type: 'FeatureCollection', features: features};
opts.heatmap.layout.visibility = 'visible';
opts.below = trace.below;

return opts;
};
16 changes: 16 additions & 0 deletions src/traces/densitymapbox/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,22 @@ module.exports = {
hoverPoints: require('./hover'),
eventData: require('./event_data'),

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

// find first layer with `type: 'symbol'`,
// that is not a plotly layer
for(var i = 0; i < mapLayers.length; i++) {
var layer = mapLayers[i];
var layerId = layer.id;
if(layer.type === 'symbol' &&
typeof layerId === 'string' && layerId.indexOf('plotly-') === -1
) {
return layerId;
}
}
},

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

0 comments on commit 8a4e8f1

Please sign in to comment.