diff --git a/src/plots/mapbox/layers.js b/src/plots/mapbox/layers.js index f6b71bc38b8..ef4a512cee7 100644 --- a/src/plots/mapbox/layers.js +++ b/src/plots/mapbox/layers.js @@ -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; @@ -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] ); }; @@ -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({ @@ -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) { diff --git a/src/plots/mapbox/layout_attributes.js b/src/plots/mapbox/layout_attributes.js index e8be572252c..51b4131968f 100644 --- a/src/plots/mapbox/layout_attributes.js +++ b/src/plots/mapbox/layout_attributes.js @@ -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', diff --git a/src/plots/mapbox/mapbox.js b/src/plots/mapbox/mapbox.js index 3cedd49b098..2b73a09b7a6 100644 --- a/src/plots/mapbox/mapbox.js +++ b/src/plots/mapbox/mapbox.js @@ -48,6 +48,7 @@ function Mapbox(gd, id) { this.styleObj = null; this.traceHash = {}; this.layerList = []; + this.belowLookup = {}; } var proto = Mapbox.prototype; @@ -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); @@ -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, @@ -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] - @@ -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])); diff --git a/src/traces/choroplethmapbox/convert.js b/src/traces/choroplethmapbox/convert.js index e73132f744c..8c1b00fb566 100644 --- a/src/traces/choroplethmapbox/convert.js +++ b/src/traces/choroplethmapbox/convert.js @@ -176,7 +176,6 @@ function convert(calcTrace) { line.layout.visibility = 'visible'; opts.geojson = {type: 'FeatureCollection', features: featuresOut}; - opts.below = trace.below; convertOnSelect(calcTrace); diff --git a/src/traces/choroplethmapbox/index.js b/src/traces/choroplethmapbox/index.js index 9da1ee7b0f5..4e9db4c81dd 100644 --- a/src/traces/choroplethmapbox/index.js +++ b/src/traces/choroplethmapbox/index.js @@ -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'), diff --git a/src/traces/choroplethmapbox/plot.js b/src/traces/choroplethmapbox/plot.js index 3233e613054..a41972cdfe9 100644 --- a/src/traces/choroplethmapbox/plot.js +++ b/src/traces/choroplethmapbox/plot.js @@ -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++) { @@ -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]; @@ -85,8 +86,6 @@ proto._addLayers = function(optsAll) { paint: opts.paint }, below); } - - this.below = below; }; proto._removeLayers = function() { @@ -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; diff --git a/src/traces/densitymapbox/convert.js b/src/traces/densitymapbox/convert.js index 2f2bf9997f2..0e7169c200a 100644 --- a/src/traces/densitymapbox/convert.js +++ b/src/traces/densitymapbox/convert.js @@ -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; }; diff --git a/src/traces/densitymapbox/index.js b/src/traces/densitymapbox/index.js index dea0f3d3e47..be982898737 100644 --- a/src/traces/densitymapbox/index.js +++ b/src/traces/densitymapbox/index.js @@ -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'), diff --git a/src/traces/densitymapbox/plot.js b/src/traces/densitymapbox/plot.js index d4fea837f7d..68a6ae88d8b 100644 --- a/src/traces/densitymapbox/plot.js +++ b/src/traces/densitymapbox/plot.js @@ -32,14 +32,16 @@ proto.update = function(calcTrace) { var subplot = this.subplot; var layerList = this.layerList; var optsAll = convert(calcTrace); + 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++) { @@ -56,11 +58,10 @@ proto.update = function(calcTrace) { } }; -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]; @@ -75,8 +76,6 @@ proto._addLayers = function(optsAll) { paint: opts.paint }, below); } - - this.below = below; }; proto._removeLayers = function() { @@ -94,39 +93,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 first layer with `type: 'symbol'` - for(var i = 0; i < mapLayers.length; i++) { - var layer = mapLayers[i]; - if(layer.type === 'symbol') { - out = layer.id; - break; - } - } - - return out; -}; - module.exports = function createDensityMapbox(subplot, calcTrace) { var trace = calcTrace[0].trace; var densityMapbox = new DensityMapbox(subplot, trace.uid); var sourceId = densityMapbox.sourceId; - var optsAll = convert(calcTrace); + var below = densityMapbox.below = subplot.belowLookup['trace-' + trace.uid]; subplot.map.addSource(sourceId, { type: 'geojson', data: optsAll.geojson }); - densityMapbox._addLayers(optsAll); + densityMapbox._addLayers(optsAll, below); return densityMapbox; }; diff --git a/src/traces/scattermapbox/attributes.js b/src/traces/scattermapbox/attributes.js index 876e9e4f9c2..3da24fa9a78 100644 --- a/src/traces/scattermapbox/attributes.js +++ b/src/traces/scattermapbox/attributes.js @@ -99,6 +99,18 @@ module.exports = overrideAll({ textfont: mapboxAttrs.layers.symbol.textfont, textposition: mapboxAttrs.layers.symbol.textposition, + below: { + valType: 'string', + role: 'info', + description: [ + 'Determines if this scattermapbox trace\'s layers are inserted', + 'before the layer with the specified ID.', + 'By default, scattermapbox layers are inserted', + 'above all the base layers.', + 'To place the scattermapbox layers above every other layer, set `below` to *\'\'*.' + ].join(' ') + }, + selected: { marker: scatterAttrs.selected.marker }, diff --git a/src/traces/scattermapbox/defaults.js b/src/traces/scattermapbox/defaults.js index f3b5b6d084f..4708202f393 100644 --- a/src/traces/scattermapbox/defaults.js +++ b/src/traces/scattermapbox/defaults.js @@ -32,6 +32,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('hovertext'); coerce('hovertemplate'); coerce('mode'); + coerce('below'); if(subTypes.hasLines(traceOut)) { handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce, {noDash: true}); diff --git a/src/traces/scattermapbox/plot.js b/src/traces/scattermapbox/plot.js index 43ca533153a..efdb925c1b8 100644 --- a/src/traces/scattermapbox/plot.js +++ b/src/traces/scattermapbox/plot.js @@ -10,6 +10,7 @@ var convert = require('./convert'); var LAYER_PREFIX = require('../../plots/mapbox/constants').traceLayerPrefix; +var ORDER = ['fill', 'line', 'circle', 'symbol']; function ScatterMapbox(subplot, uid) { this.subplot = subplot; @@ -29,11 +30,13 @@ function ScatterMapbox(subplot, uid) { symbol: LAYER_PREFIX + uid + '-symbol' }; - this.order = ['fill', 'line', 'circle', 'symbol']; - // We could merge the 'fill' source with the 'line' source and // the 'circle' source with the 'symbol' source if ever having // for up-to 4 sources per 'scattermapbox' traces becomes a problem. + + // previous 'below' value, + // need this to update it properly + this.below = null; } var proto = ScatterMapbox.prototype; @@ -51,23 +54,42 @@ proto.setSourceData = function(k, opts) { .setData(opts.geojson); }; -proto.addLayer = function(k, opts) { - this.subplot.map.addLayer({ +proto.addLayer = function(k, opts, below) { + var subplot = this.subplot; + + subplot.map.addLayer({ type: k, id: this.layerIds[k], source: this.sourceIds[k], layout: opts.layout, paint: opts.paint - }); + }, below); }; proto.update = function update(calcTrace) { var subplot = this.subplot; + var map = subplot.map; var optsAll = convert(calcTrace); + var below = subplot.belowLookup['trace-' + this.uid]; + var i, k, opts; + + if(below !== this.below) { + // console.log('update below', [below, this.below]) + for(i = ORDER.length - 1; i >= 0; i--) { + k = ORDER[i]; + map.removeLayer(this.layerIds[k]); + } + for(i = 0; i < ORDER.length; i++) { + k = ORDER[i]; + opts = optsAll[k]; + this.addLayer(k, opts, below); + } + this.below = below; + } - for(var i = 0; i < this.order.length; i++) { - var k = this.order[i]; - var opts = optsAll[k]; + for(i = 0; i < ORDER.length; i++) { + k = ORDER[i]; + opts = optsAll[k]; subplot.setOptions(this.layerIds[k], 'setLayoutProperty', opts.layout); @@ -84,8 +106,8 @@ proto.update = function update(calcTrace) { proto.dispose = function dispose() { var map = this.subplot.map; - for(var i = 0; i < this.order.length; i++) { - var k = this.order[i]; + for(var i = ORDER.length - 1; i >= 0; i--) { + var k = ORDER[i]; map.removeLayer(this.layerIds[k]); map.removeSource(this.sourceIds[k]); } @@ -95,13 +117,13 @@ module.exports = function createScatterMapbox(subplot, calcTrace) { var trace = calcTrace[0].trace; var scatterMapbox = new ScatterMapbox(subplot, trace.uid); var optsAll = convert(calcTrace); + var below = scatterMapbox.below = subplot.belowLookup['trace-' + trace.uid]; - for(var i = 0; i < scatterMapbox.order.length; i++) { - var k = scatterMapbox.order[i]; + for(var i = 0; i < ORDER.length; i++) { + var k = ORDER[i]; var opts = optsAll[k]; - scatterMapbox.addSource(k, opts); - scatterMapbox.addLayer(k, opts); + scatterMapbox.addLayer(k, opts, below); } // link ref for quick update during selections diff --git a/test/jasmine/tests/choroplethmapbox_test.js b/test/jasmine/tests/choroplethmapbox_test.js index 91222679706..68293b2e8d1 100644 --- a/test/jasmine/tests/choroplethmapbox_test.js +++ b/test/jasmine/tests/choroplethmapbox_test.js @@ -701,7 +701,7 @@ describe('@noCI Test choroplethmapbox interactions:', function() { }) .then(function() { return Plotly.restyle(gd, 'below', ''); }) .then(function() { - expect(getLayerIds()).withContext('default *below*').toEqual([ + expect(getLayerIds()).withContext('*below* set to \'\'').toEqual([ 'background', 'landuse_overlay_national_park', 'landuse_park', 'waterway', 'water', 'building', 'tunnel_minor', 'tunnel_major', 'road_minor', 'road_major', @@ -713,7 +713,7 @@ describe('@noCI Test choroplethmapbox interactions:', function() { }) .then(function() { return Plotly.restyle(gd, 'below', 'place_label_other'); }) .then(function() { - expect(getLayerIds()).withContext('default *below*').toEqual([ + expect(getLayerIds()).withContext('*below* set to same base layer').toEqual([ 'background', 'landuse_overlay_national_park', 'landuse_park', 'waterway', 'water', 'building', 'tunnel_minor', 'tunnel_major', 'road_minor', 'road_major', @@ -723,6 +723,18 @@ describe('@noCI Test choroplethmapbox interactions:', function() { 'place_label_other', 'place_label_city', 'country_label', ]); }) + .then(function() { return Plotly.restyle(gd, 'below', null); }) + .then(function() { + expect(getLayerIds()).withContext('back to default *below*').toEqual([ + 'background', 'landuse_overlay_national_park', 'landuse_park', + 'waterway', 'water', + 'plotly-trace-layer-a-fill', 'plotly-trace-layer-a-line', + 'building', 'tunnel_minor', 'tunnel_major', 'road_minor', 'road_major', + 'bridge_minor case', 'bridge_major case', 'bridge_minor', 'bridge_major', + 'admin_country', 'poi_label', 'road_major_label', + 'place_label_other', 'place_label_city', 'country_label' + ]); + }) .catch(failTest) .then(done); }, 5 * jasmine.DEFAULT_TIMEOUT_INTERVAL); diff --git a/test/jasmine/tests/densitymapbox_test.js b/test/jasmine/tests/densitymapbox_test.js index 6d4b96baaba..9503d3ab506 100644 --- a/test/jasmine/tests/densitymapbox_test.js +++ b/test/jasmine/tests/densitymapbox_test.js @@ -481,6 +481,19 @@ describe('@noCI Test densitymapbox interactions:', function() { 'place_label_other', 'place_label_city', 'country_label', ]); }) + .then(function() { return Plotly.restyle(gd, 'below', null); }) + .then(function() { + expect(getLayerIds()).withContext('back to default *below*').toEqual([ + 'background', 'landuse_overlay_national_park', 'landuse_park', + 'waterway', 'water', + 'building', 'tunnel_minor', 'tunnel_major', 'road_minor', 'road_major', + 'bridge_minor case', 'bridge_major case', 'bridge_minor', 'bridge_major', + 'admin_country', + 'plotly-trace-layer-a-heatmap', + 'poi_label', 'road_major_label', + 'place_label_other', 'place_label_city', 'country_label' + ]); + }) .catch(failTest) .then(done); }, 5 * jasmine.DEFAULT_TIMEOUT_INTERVAL); diff --git a/test/jasmine/tests/mapbox_test.js b/test/jasmine/tests/mapbox_test.js index 4cd0b241378..58c334068ae 100644 --- a/test/jasmine/tests/mapbox_test.js +++ b/test/jasmine/tests/mapbox_test.js @@ -1333,6 +1333,196 @@ describe('@noCI, mapbox plots', function() { } }); +describe('@noCI test mapbox trace/layout *below* interactions', function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(function(done) { + Plotly.purge(gd); + destroyGraphDiv(); + setTimeout(done, 200); + }); + + function getLayerIds() { + var subplot = gd._fullLayout.mapbox._subplot; + var layers = subplot.map.getStyle().layers; + var layerIds = layers.map(function(l) { return l.id; }); + return layerIds; + } + + it('@gl should be able to update *below* - scattermapbox + layout layer case', function(done) { + function _assert(msg, exp) { + var layersIds = getLayerIds(); + var tracePrefix = 'plotly-trace-layer-' + gd._fullData[0].uid; + + expect(layersIds.indexOf(tracePrefix + '-fill')).toBe(exp.trace[0], msg + '| fill'); + expect(layersIds.indexOf(tracePrefix + '-line')).toBe(exp.trace[1], msg + '| line'); + expect(layersIds.indexOf(tracePrefix + '-circle')).toBe(exp.trace[2], msg + '| circle'); + expect(layersIds.indexOf(tracePrefix + '-symbol')).toBe(exp.trace[3], msg + '| symbol'); + + var layoutLayerId = ['plotly-layout-layer', gd._fullLayout._uid, 'mapbox-0'].join('-'); + expect(layersIds.indexOf(layoutLayerId)).toBe(exp.layout, msg + '| layout layer'); + } + + Plotly.plot(gd, [{ + type: 'scattermapbox', + lon: [10, 20, 30], + lat: [15, 25, 35], + uid: 'a' + }], { + mapbox: { + style: 'basic', + layers: [{ + sourcetype: 'vector', + source: 'mapbox://mapbox.mapbox-terrain-v2', + sourcelayer: 'contour', + type: 'line' + }] + } + }) + .then(function() { + _assert('default *below*', { + trace: [20, 21, 22, 23], + layout: 24 + }); + }) + .then(function() { return Plotly.relayout(gd, 'mapbox.layers[0].below', 'traces'); }) + .then(function() { + _assert('with layout layer *below:traces*', { + trace: [21, 22, 23, 24], + layout: 20 + }); + }) + .then(function() { return Plotly.relayout(gd, 'mapbox.layers[0].below', null); }) + .then(function() { + _assert('back to default *below* (1)', { + trace: [20, 21, 22, 23], + layout: 24 + }); + }) + .then(function() { return Plotly.restyle(gd, 'below', ''); }) + .then(function() { + _assert('with trace *below:""*', { + trace: [21, 22, 23, 24], + layout: 20 + }); + }) + .then(function() { return Plotly.restyle(gd, 'below', null); }) + .then(function() { + _assert('back to default *below* (2)', { + trace: [20, 21, 22, 23], + layout: 24 + }); + }) + .then(function() { return Plotly.restyle(gd, 'below', 'water'); }) + .then(function() { + _assert('with trace *below:water*', { + trace: [4, 5, 6, 7], + layout: 24 + }); + }) + .then(function() { return Plotly.relayout(gd, 'mapbox.layers[0].below', 'water'); }) + .then(function() { + _assert('with trace AND layout layer *below:water*', { + trace: [4, 5, 6, 7], + layout: 8 + }); + }) + .then(function() { return Plotly.relayout(gd, 'mapbox.layers[0].below', ''); }) + .then(function() { + _assert('with trace *below:water* and layout layer *below:""*', { + trace: [4, 5, 6, 7], + layout: 24 + }); + }) + .then(function() { return Plotly.restyle(gd, 'below', ''); }) + .then(function() { + _assert('with trace AND layout layer *below:water*', { + trace: [20, 21, 22, 23], + layout: 24 + }); + }) + .then(function() { return Plotly.update(gd, {below: null}, {'mapbox.layers[0].below': null}); }) + .then(function() { + _assert('back to default *below* (3)', { + trace: [20, 21, 22, 23], + layout: 24 + }); + }) + .catch(failTest) + .then(done); + }, 8 * jasmine.DEFAULT_TIMEOUT_INTERVAL); + + it('@gl should be able to update *below* - scattermapbox + choroplethmapbox + densitymapbox case', function(done) { + function _assert(msg, exp) { + var layersIds = getLayerIds(); + var tracePrefix = 'plotly-trace-layer-'; + + var scatterPrefix = tracePrefix + 'scatter'; + expect(layersIds.indexOf(scatterPrefix + '-fill')).toBe(exp.scatter[0], msg + '| scatter fill'); + expect(layersIds.indexOf(scatterPrefix + '-line')).toBe(exp.scatter[1], msg + '| scatter line'); + expect(layersIds.indexOf(scatterPrefix + '-circle')).toBe(exp.scatter[2], msg + '| scatter circle'); + expect(layersIds.indexOf(scatterPrefix + '-symbol')).toBe(exp.scatter[3], msg + '| scatter symbol'); + + var densityPrefix = tracePrefix + 'density'; + expect(layersIds.indexOf(densityPrefix + '-heatmap')).toBe(exp.density[0], msg + '| density heatmap'); + + var choroplethPrefix = tracePrefix + 'choropleth'; + expect(layersIds.indexOf(choroplethPrefix + '-fill')).toBe(exp.choropleth[0], msg + '| choropleth fill'); + expect(layersIds.indexOf(choroplethPrefix + '-line')).toBe(exp.choropleth[1], msg + '| choropleth line'); + } + + Plotly.plot(gd, [{ + type: 'scattermapbox', + lon: [10, 20, 30], + lat: [15, 25, 35], + uid: 'scatter' + }, { + type: 'densitymapbox', + lon: [10, 20, 30], + lat: [15, 25, 35], + z: [1, 20, 5], + uid: 'density' + }, { + type: 'choroplethmapbox', + geojson: 'https://raw.githubusercontent.com/python-visualization/folium/master/examples/data/us-states.json', + locations: ['AL'], + z: [10], + uid: 'choropleth' + }], { + mapbox: {style: 'basic'} + }) + .then(function() { + _assert('base', { + scatter: [23, 24, 25, 26], + density: [17], + choropleth: [5, 6] + }); + }) + .then(function() { return Plotly.restyle(gd, 'below', ''); }) + .then(function() { + _assert('all traces *below:""', { + scatter: [23, 24, 25, 26], + density: [22], + choropleth: [20, 21] + }); + }) + .then(function() { return Plotly.restyle(gd, 'below', null); }) + .then(function() { + _assert('back to base', { + scatter: [23, 24, 25, 26], + density: [17], + choropleth: [5, 6] + }); + }) + .catch(failTest) + .then(done); + }, 8 * jasmine.DEFAULT_TIMEOUT_INTERVAL); +}); + describe('@noCI Test mapbox GeoJSON fetching:', function() { var gd;