Skip to content

Commit 4d7e0aa

Browse files
committed
implement *below* in choroplethmapbox traces
- where by default chroroplethmapbox traces are placed above the top most "water" layer - N.B. we need to remove/add again layers when *below* is updated
1 parent 1359307 commit 4d7e0aa

File tree

5 files changed

+213
-26
lines changed

5 files changed

+213
-26
lines changed

src/traces/choroplethmapbox/attributes.js

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,22 @@ module.exports = extendFlat({
5353
].join(' ')
5454
},
5555

56-
// TODO agree on name
57-
// 'below' is used currently for layout.mapbox.layers, it's not very
58-
// plotly-esque though
59-
// https://docs.mapbox.com/mapbox-gl-js/example/geojson-layer-in-stack/
56+
// TODO agree on name / behaviour
57+
//
58+
// 'below' is used currently for layout.mapbox.layers,
59+
// even though it's not very plotly-esque.
60+
//
61+
// Note also, that the mapbox-gl style don't all have the same layers,
62+
// see https://codepen.io/etpinard/pen/ydVMwM for full list
6063
below: {
6164
valType: 'string',
62-
dflt: '',
6365
role: 'info',
66+
editType: 'plot',
6467
description: [
6568
'Determines if the choropleth polygons will be inserted',
6669
'before the layer with the specified ID.',
67-
'If omitted or set to \'\',',
70+
'By default, choroplethmapbox traces are placed above the water layers.',
71+
'If set to \'\',',
6872
'the layer will be inserted above every existing layer.'
6973
].join(' ')
7074
},
@@ -78,7 +82,7 @@ module.exports = extendFlat({
7882
width: extendFlat({}, choroplethAttrs.marker.line.width, {editType: 'plot'}),
7983
editType: 'calc'
8084
},
81-
// TODO maybe having a dflt less than 1 would be better?
85+
// TODO maybe having a dflt less than 1, together with `below:''` would be better?
8286
opacity: extendFlat({}, choroplethAttrs.marker.opacity, {editType: 'plot'}),
8387
editType: 'calc'
8488
},

src/traces/choroplethmapbox/convert.js

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

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

180181
convertOnSelect(calcTrace);
181182

src/traces/choroplethmapbox/defaults.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
3131

3232
traceOut._length = Math.min(locations.length, z.length);
3333

34-
// TODO
35-
// coerce('below');
34+
coerce('below');
3635

3736
coerce('text');
3837
coerce('hovertext');

src/traces/choroplethmapbox/plot.js

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ function ChoroplethMapbox(subplot, uid) {
2222
['fill', uid + '-layer-fill'],
2323
['line', uid + '-layer-line']
2424
];
25+
26+
// previous 'below' value,
27+
// need this to update it properly
28+
this.below = null;
2529
}
2630

2731
var proto = ChoroplethMapbox.prototype;
@@ -42,6 +46,11 @@ proto._update = function(optsAll) {
4246
.getSource(this.sourceId)
4347
.setData(optsAll.geojson);
4448

49+
if(optsAll.below !== this.below) {
50+
this._removeLayers();
51+
this._addLayers(optsAll);
52+
}
53+
4554
for(var i = 0; i < layerList.length; i++) {
4655
var item = layerList[i];
4756
var k = item[0];
@@ -56,21 +65,76 @@ proto._update = function(optsAll) {
5665
}
5766
};
5867

59-
proto.dispose = function() {
68+
proto._addLayers = function(optsAll) {
69+
var subplot = this.subplot;
70+
var layerList = this.layerList;
71+
var sourceId = this.sourceId;
72+
var below = this.getBelow(optsAll);
73+
74+
for(var i = 0; i < layerList.length; i++) {
75+
var item = layerList[i];
76+
var k = item[0];
77+
var opts = optsAll[k];
78+
79+
subplot.map.addLayer({
80+
type: k,
81+
id: item[1],
82+
source: sourceId,
83+
layout: opts.layout,
84+
paint: opts.paint
85+
}, below);
86+
}
87+
88+
this.below = below;
89+
};
90+
91+
proto._removeLayers = function() {
6092
var map = this.subplot.map;
6193
var layerList = this.layerList;
6294

6395
for(var i = 0; i < layerList.length; i++) {
64-
map.removeLayer(layerList[i][0]);
96+
map.removeLayer(layerList[i][1]);
6597
}
98+
};
99+
100+
proto.dispose = function() {
101+
var map = this.subplot.map;
102+
this._removeLayers();
66103
map.removeSource(this.sourceId);
67104
};
68105

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

75139
var optsAll = convert(calcTrace);
76140

@@ -79,19 +143,7 @@ module.exports = function createChoroplethMapbox(subplot, calcTrace) {
79143
data: optsAll.geojson
80144
});
81145

82-
for(var i = 0; i < layerList.length; i++) {
83-
var item = layerList[i];
84-
var k = item[0];
85-
var opts = optsAll[k];
86-
87-
subplot.map.addLayer({
88-
type: k,
89-
id: item[1],
90-
source: sourceId,
91-
layout: opts.layout,
92-
paint: opts.paint
93-
});
94-
}
146+
choroplethMapbox._addLayers(optsAll);
95147

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

test/jasmine/tests/choroplethmapbox_test.js

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ var Plots = require('@src/plots/plots');
33
var Lib = require('@src/lib');
44

55
var convertModule = require('@src/traces/choroplethmapbox/convert');
6+
var MAPBOX_ACCESS_TOKEN = require('@build/credentials.json').MAPBOX_ACCESS_TOKEN;
67

78
var createGraphDiv = require('../assets/create_graph_div');
89
var destroyGraphDiv = require('../assets/destroy_graph_div');
@@ -519,7 +520,7 @@ describe('@noCI Test choroplethmapbox hover:', function() {
519520

520521
if(!fig.layout) fig.layout = {};
521522
if(!fig.layout.mapbox) fig.layout.mapbox = {};
522-
fig.layout.mapbox.accesstoken = require('@build/credentials.json').MAPBOX_ACCESS_TOKEN;
523+
fig.layout.mapbox.accesstoken = MAPBOX_ACCESS_TOKEN;
523524

524525
var pos = s.pos || [270, 220];
525526

@@ -579,3 +580,133 @@ describe('@noCI Test choroplethmapbox hover:', function() {
579580
});
580581
});
581582
});
583+
584+
describe('@noCI Test choroplethmapbox interactions:', function() {
585+
var gd;
586+
587+
var geojson = {
588+
type: 'Feature',
589+
id: 'AL',
590+
geometry: {
591+
type: 'Polygon',
592+
coordinates: [[
593+
[-87.359296, 35.00118 ], [-85.606675, 34.984749 ], [-85.431413, 34.124869 ], [-85.184951, 32.859696 ],
594+
[-85.069935, 32.580372 ], [-84.960397, 32.421541 ], [-85.004212, 32.322956 ], [-84.889196, 32.262709 ],
595+
[-85.058981, 32.13674 ], [-85.053504, 32.01077 ], [-85.141136, 31.840985 ], [-85.042551, 31.539753 ],
596+
[-85.113751, 31.27686 ], [-85.004212, 31.003013 ], [-85.497137, 30.997536 ], [-87.600282, 30.997536 ],
597+
[-87.633143, 30.86609 ], [-87.408589, 30.674397 ], [-87.446927, 30.510088 ], [-87.37025, 30.427934 ],
598+
[-87.518128, 30.280057 ], [-87.655051, 30.247195 ], [-87.90699, 30.411504 ], [-87.934375, 30.657966 ],
599+
[-88.011052, 30.685351 ], [-88.10416, 30.499135 ], [-88.137022, 30.318396 ], [-88.394438, 30.367688 ],
600+
[-88.471115, 31.895754 ], [-88.241084, 33.796253 ], [-88.098683, 34.891641 ], [-88.202745, 34.995703 ],
601+
[-87.359296, 35.00118 ]
602+
]]
603+
}
604+
};
605+
606+
beforeEach(function() {
607+
gd = createGraphDiv();
608+
});
609+
610+
afterEach(function(done) {
611+
Plotly.purge(gd);
612+
destroyGraphDiv();
613+
setTimeout(done, 200);
614+
});
615+
616+
it('@gl should be able to add and remove traces', function(done) {
617+
function _assert(msg, exp) {
618+
var map = gd._fullLayout.mapbox._subplot.map;
619+
var layers = map.getStyle().layers;
620+
621+
expect(layers.length).toBe(exp.layerCnt, 'total # of layers |' + msg);
622+
}
623+
624+
var trace0 = {
625+
type: 'choroplethmapbox',
626+
locations: ['AL'],
627+
z: [10],
628+
geojson: geojson
629+
};
630+
631+
var trace1 = {
632+
type: 'choroplethmapbox',
633+
locations: ['AL'],
634+
z: [1],
635+
geojson: geojson,
636+
marker: {opacity: 0.3}
637+
};
638+
639+
Plotly.plot(gd,
640+
[trace0, trace1],
641+
{mapbox: {style: 'basic'}},
642+
{mapboxAccessToken: MAPBOX_ACCESS_TOKEN}
643+
)
644+
.then(function() {
645+
_assert('base', { layerCnt: 24 });
646+
})
647+
.then(function() { return Plotly.deleteTraces(gd, [0]); })
648+
.then(function() {
649+
_assert('w/o trace0', { layerCnt: 22 });
650+
})
651+
.then(function() { return Plotly.addTraces(gd, [trace0]); })
652+
.then(function() {
653+
_assert('after adding trace0', { layerCnt: 24 });
654+
})
655+
.catch(failTest)
656+
.then(done);
657+
});
658+
659+
it('@gl should be able to restyle *below*', function(done) {
660+
function getLayerIds() {
661+
var subplot = gd._fullLayout.mapbox._subplot;
662+
var layers = subplot.map.getStyle().layers;
663+
var layerIds = layers.map(function(l) { return l.id; });
664+
return layerIds;
665+
}
666+
667+
Plotly.plot(gd, [{
668+
type: 'choroplethmapbox',
669+
locations: ['AL'],
670+
z: [10],
671+
geojson: geojson,
672+
uid: 'a'
673+
}])
674+
.then(function() {
675+
expect(getLayerIds()).withContext('default *below*').toEqual([
676+
'background', 'landuse_overlay_national_park', 'landuse_park',
677+
'waterway', 'water',
678+
'a-layer-fill', 'a-layer-line',
679+
'building', 'tunnel_minor', 'tunnel_major', 'road_minor', 'road_major',
680+
'bridge_minor case', 'bridge_major case', 'bridge_minor', 'bridge_major',
681+
'admin_country', 'poi_label', 'road_major_label',
682+
'place_label_other', 'place_label_city', 'country_label'
683+
]);
684+
})
685+
.then(function() { return Plotly.restyle(gd, 'below', ''); })
686+
.then(function() {
687+
expect(getLayerIds()).withContext('default *below*').toEqual([
688+
'background', 'landuse_overlay_national_park', 'landuse_park',
689+
'waterway', 'water',
690+
'building', 'tunnel_minor', 'tunnel_major', 'road_minor', 'road_major',
691+
'bridge_minor case', 'bridge_major case', 'bridge_minor', 'bridge_major',
692+
'admin_country', 'poi_label', 'road_major_label',
693+
'place_label_other', 'place_label_city', 'country_label',
694+
'a-layer-fill', 'a-layer-line'
695+
]);
696+
})
697+
.then(function() { return Plotly.restyle(gd, 'below', 'place_label_other'); })
698+
.then(function() {
699+
expect(getLayerIds()).withContext('default *below*').toEqual([
700+
'background', 'landuse_overlay_national_park', 'landuse_park',
701+
'waterway', 'water',
702+
'building', 'tunnel_minor', 'tunnel_major', 'road_minor', 'road_major',
703+
'bridge_minor case', 'bridge_major case', 'bridge_minor', 'bridge_major',
704+
'admin_country', 'poi_label', 'road_major_label',
705+
'a-layer-fill', 'a-layer-line',
706+
'place_label_other', 'place_label_city', 'country_label',
707+
]);
708+
})
709+
.catch(failTest)
710+
.then(done);
711+
}, 5 * jasmine.DEFAULT_TIMEOUT_INTERVAL);
712+
});

0 commit comments

Comments
 (0)