Skip to content

Commit ca48461

Browse files
authoredJun 28, 2019
Merge pull request #3993 from plotly/densitymapbox-pr
Introducing densitymapbox traces
2 parents fe6782d + 1add90e commit ca48461

21 files changed

+37801
-5
lines changed
 

‎lib/densitymapbox.js

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Copyright 2012-2019, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
module.exports = require('../src/traces/densitymapbox');

‎lib/index-mapbox.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ var Plotly = require('./core');
1212

1313
Plotly.register([
1414
require('./scattermapbox'),
15-
require('./choroplethmapbox')
15+
require('./choroplethmapbox'),
16+
require('./densitymapbox')
1617
]);
1718

1819
module.exports = Plotly;

‎lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ Plotly.register([
5151

5252
require('./scattermapbox'),
5353
require('./choroplethmapbox'),
54+
require('./densitymapbox'),
5455

5556
require('./sankey'),
5657

‎src/plots/mapbox/mapbox.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,9 @@ proto.updateMap = function(calcData, fullLayout, resolve, reject) {
199199
};
200200

201201
var traceType2orderIndex = {
202-
'choroplethmapbox': 0,
203-
'scattermapbox': 1
202+
choroplethmapbox: 0,
203+
densitymapbox: 1,
204+
scattermapbox: 2
204205
};
205206

206207
proto.updateData = function(calcData) {

‎src/traces/choroplethmapbox/plot.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ proto._removeLayers = function() {
9292
var map = this.subplot.map;
9393
var layerList = this.layerList;
9494

95-
for(var i = 0; i < layerList.length; i++) {
95+
for(var i = layerList.length - 1; i >= 0; i--) {
9696
map.removeLayer(layerList[i][1]);
9797
}
9898
};
+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* Copyright 2012-2019, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
var colorScaleAttrs = require('../../components/colorscale/attributes');
12+
var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes');
13+
var plotAttrs = require('../../plots/attributes');
14+
var scatterMapboxAttrs = require('../scattermapbox/attributes');
15+
16+
var extendFlat = require('../../lib/extend').extendFlat;
17+
18+
/*
19+
* - https://docs.mapbox.com/help/tutorials/make-a-heatmap-with-mapbox-gl-js/
20+
* - https://docs.mapbox.com/mapbox-gl-js/example/heatmap-layer/
21+
* - https://docs.mapbox.com/mapbox-gl-js/style-spec/#layers-heatmap
22+
* - https://blog.mapbox.com/introducing-heatmaps-in-mapbox-gl-js-71355ada9e6c
23+
*
24+
* Gotchas:
25+
* - https://github.com/mapbox/mapbox-gl-js/issues/6463
26+
* - https://github.com/mapbox/mapbox-gl-js/issues/6112
27+
*/
28+
29+
/*
30+
*
31+
* In mathematical terms, Mapbox GL heatmaps are a bivariate (2D) kernel density
32+
* estimation with a Gaussian kernel. It means that each data point has an area
33+
* of “influence” around it (called a kernel) where the numerical value of
34+
* influence (which we call density) decreases as you go further from the point.
35+
* If we sum density values of all points in every pixel of the screen, we get a
36+
* combined density value which we then map to a heatmap color.
37+
*
38+
*/
39+
40+
module.exports = extendFlat({
41+
lon: scatterMapboxAttrs.lon,
42+
lat: scatterMapboxAttrs.lat,
43+
44+
z: {
45+
valType: 'data_array',
46+
editType: 'calc',
47+
description: [
48+
'Sets the points\' weight.',
49+
'For example, a value of 10 would be equivalent to having 10 points of weight 1',
50+
'in the same spot'
51+
].join(' ')
52+
},
53+
54+
radius: {
55+
valType: 'number',
56+
role: 'info',
57+
editType: 'plot',
58+
arrayOk: true,
59+
min: 1,
60+
dflt: 30,
61+
description: [
62+
'Sets the radius of influence of one `lon` / `lat` point in pixels.',
63+
'Increasing the value makes the densitymapbox trace smoother, but less detailed.'
64+
].join(' ')
65+
},
66+
67+
below: {
68+
valType: 'string',
69+
role: 'info',
70+
editType: 'plot',
71+
description: [
72+
'Determines if the densitymapbox trace will be inserted',
73+
'before the layer with the specified ID.',
74+
'By default, densitymapbox traces are placed below the first',
75+
'layer of type symbol',
76+
'If set to \'\',',
77+
'the layer will be inserted above every existing layer.'
78+
].join(' ')
79+
},
80+
81+
text: scatterMapboxAttrs.text,
82+
hovertext: scatterMapboxAttrs.hovertext,
83+
84+
hoverinfo: extendFlat({}, plotAttrs.hoverinfo, {
85+
flags: ['lon', 'lat', 'z', 'text', 'name']
86+
}),
87+
hovertemplate: hovertemplateAttrs()
88+
},
89+
colorScaleAttrs('', {
90+
cLetter: 'z',
91+
editTypeOverride: 'calc'
92+
})
93+
);

‎src/traces/densitymapbox/calc.js

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* Copyright 2012-2019, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
var isNumeric = require('fast-isnumeric');
12+
13+
var isArrayOrTypedArray = require('../../lib').isArrayOrTypedArray;
14+
var BADNUM = require('../../constants/numerical').BADNUM;
15+
16+
var colorscaleCalc = require('../../components/colorscale/calc');
17+
var _ = require('../../lib')._;
18+
19+
module.exports = function calc(gd, trace) {
20+
var len = trace._length;
21+
var calcTrace = new Array(len);
22+
var z = trace.z;
23+
var hasZ = isArrayOrTypedArray(z) && z.length;
24+
25+
for(var i = 0; i < len; i++) {
26+
var cdi = calcTrace[i] = {};
27+
28+
var lon = trace.lon[i];
29+
var lat = trace.lat[i];
30+
31+
cdi.lonlat = isNumeric(lon) && isNumeric(lat) ?
32+
[+lon, +lat] :
33+
[BADNUM, BADNUM];
34+
35+
if(hasZ) {
36+
var zi = z[i];
37+
cdi.z = isNumeric(zi) ? zi : BADNUM;
38+
}
39+
}
40+
41+
colorscaleCalc(gd, trace, {
42+
vals: hasZ ? z : [0, 1],
43+
containerStr: '',
44+
cLetter: 'z'
45+
});
46+
47+
if(len) {
48+
calcTrace[0].t = {
49+
labels: {
50+
lat: _(gd, 'lat:') + ' ',
51+
lon: _(gd, 'lon:') + ' '
52+
}
53+
};
54+
}
55+
56+
return calcTrace;
57+
};

‎src/traces/densitymapbox/convert.js

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/**
2+
* Copyright 2012-2019, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
var isNumeric = require('fast-isnumeric');
12+
13+
var Lib = require('../../lib');
14+
var Color = require('../../components/color');
15+
var Colorscale = require('../../components/colorscale');
16+
17+
var BADNUM = require('../../constants/numerical').BADNUM;
18+
var makeBlank = require('../../lib/geojson_utils').makeBlank;
19+
20+
module.exports = function convert(calcTrace) {
21+
var trace = calcTrace[0].trace;
22+
var isVisible = (trace.visible === true && trace._length !== 0);
23+
24+
var heatmap = {
25+
layout: {visibility: 'none'},
26+
paint: {}
27+
};
28+
29+
var opts = trace._opts = {
30+
heatmap: heatmap,
31+
geojson: makeBlank()
32+
};
33+
34+
// early return if not visible or placeholder
35+
if(!isVisible) return opts;
36+
37+
var features = [];
38+
var i;
39+
40+
var z = trace.z;
41+
var radius = trace.radius;
42+
var hasZ = Lib.isArrayOrTypedArray(z) && z.length;
43+
var hasArrayRadius = Lib.isArrayOrTypedArray(radius);
44+
45+
for(i = 0; i < calcTrace.length; i++) {
46+
var cdi = calcTrace[i];
47+
var lonlat = cdi.lonlat;
48+
49+
if(lonlat[0] !== BADNUM) {
50+
var props = {};
51+
52+
if(hasZ) {
53+
var zi = cdi.z;
54+
props.z = zi !== BADNUM ? zi : 0;
55+
}
56+
if(hasArrayRadius) {
57+
props.r = (isNumeric(radius[i]) && radius[i] > 0) ? +radius[i] : 0;
58+
}
59+
60+
features.push({
61+
type: 'Feature',
62+
geometry: {type: 'Point', coordinates: lonlat},
63+
properties: props
64+
});
65+
}
66+
}
67+
68+
var cOpts = Colorscale.extractOpts(trace);
69+
var scl = cOpts.reversescale ?
70+
Colorscale.flipScale(cOpts.colorscale) :
71+
cOpts.colorscale;
72+
73+
// Add alpha channel to first colorscale step.
74+
// If not, we would essentially color the entire map.
75+
// See https://docs.mapbox.com/mapbox-gl-js/example/heatmap-layer/
76+
var scl01 = scl[0][1];
77+
var color0 = Color.opacity(scl01) < 1 ? scl01 : Color.addOpacity(scl01, 0);
78+
79+
var heatmapColor = [
80+
'interpolate', ['linear'],
81+
['heatmap-density'],
82+
0, color0
83+
];
84+
for(i = 1; i < scl.length; i++) {
85+
heatmapColor.push(scl[i][0], scl[i][1]);
86+
}
87+
88+
// Those "weights" have to be in [0, 1], we can do this either:
89+
// - as here using a mapbox-gl expression
90+
// - or, scale the 'z' property in the feature loop
91+
var zExp = [
92+
'interpolate', ['linear'],
93+
['get', 'z'],
94+
cOpts.min, 0,
95+
cOpts.max, 1
96+
];
97+
98+
Lib.extendFlat(opts.heatmap.paint, {
99+
'heatmap-weight': hasZ ? zExp : 1 / (cOpts.max - cOpts.min),
100+
101+
'heatmap-color': heatmapColor,
102+
103+
'heatmap-radius': hasArrayRadius ?
104+
{type: 'identity', property: 'r'} :
105+
trace.radius,
106+
107+
'heatmap-opacity': trace.opacity
108+
});
109+
110+
opts.geojson = {type: 'FeatureCollection', features: features};
111+
opts.heatmap.layout.visibility = 'visible';
112+
opts.below = trace.below;
113+
114+
return opts;
115+
};

‎src/traces/densitymapbox/defaults.js

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* Copyright 2012-2019, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
var Lib = require('../../lib');
12+
var colorscaleDefaults = require('../../components/colorscale/defaults');
13+
var attributes = require('./attributes');
14+
15+
module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
16+
function coerce(attr, dflt) {
17+
return Lib.coerce(traceIn, traceOut, attributes, attr, dflt);
18+
}
19+
20+
var lon = coerce('lon') || [];
21+
var lat = coerce('lat') || [];
22+
23+
var len = Math.min(lon.length, lat.length);
24+
if(!len) {
25+
traceOut.visible = false;
26+
return;
27+
}
28+
29+
traceOut._length = len;
30+
31+
coerce('z');
32+
coerce('radius');
33+
coerce('below');
34+
35+
coerce('text');
36+
coerce('hovertext');
37+
coerce('hovertemplate');
38+
39+
colorscaleDefaults(traceIn, traceOut, layout, coerce, {prefix: '', cLetter: 'z'});
40+
};
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* Copyright 2012-2019, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
module.exports = function eventData(out, pt) {
12+
out.lon = pt.lon;
13+
out.lat = pt.lat;
14+
out.z = pt.z;
15+
return out;
16+
};

‎src/traces/densitymapbox/hover.js

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/**
2+
* Copyright 2012-2019, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
var Lib = require('../../lib');
12+
var Axes = require('../../plots/cartesian/axes');
13+
var scatterMapboxHoverPoints = require('../scattermapbox/hover');
14+
15+
module.exports = function hoverPoints(pointData, xval, yval) {
16+
var pts = scatterMapboxHoverPoints(pointData, xval, yval);
17+
if(!pts) return;
18+
19+
var newPointData = pts[0];
20+
var cd = newPointData.cd;
21+
var trace = cd[0].trace;
22+
var di = cd[newPointData.index];
23+
24+
// let Fx.hover pick the color
25+
delete newPointData.color;
26+
27+
if('z' in di) {
28+
var ax = newPointData.subplot.mockAxis;
29+
newPointData.z = di.z;
30+
newPointData.zLabel = Axes.tickText(ax, ax.c2l(di.z), 'hover').text;
31+
}
32+
33+
newPointData.extraText = getExtraText(trace, di, cd[0].t.labels);
34+
35+
return [newPointData];
36+
};
37+
38+
function getExtraText(trace, di, labels) {
39+
if(trace.hovertemplate) return;
40+
41+
var hoverinfo = di.hi || trace.hoverinfo;
42+
var parts = hoverinfo.split('+');
43+
var isAll = parts.indexOf('all') !== -1;
44+
var hasLon = parts.indexOf('lon') !== -1;
45+
var hasLat = parts.indexOf('lat') !== -1;
46+
var lonlat = di.lonlat;
47+
var text = [];
48+
49+
function format(v) {
50+
return v + '\u00B0';
51+
}
52+
53+
if(isAll || (hasLon && hasLat)) {
54+
text.push('(' + format(lonlat[0]) + ', ' + format(lonlat[1]) + ')');
55+
} else if(hasLon) {
56+
text.push(labels.lon + format(lonlat[0]));
57+
} else if(hasLat) {
58+
text.push(labels.lat + format(lonlat[1]));
59+
}
60+
61+
if(isAll || parts.indexOf('text') !== -1) {
62+
Lib.fillText(di, trace, text);
63+
}
64+
65+
return text.join('<br>');
66+
}

‎src/traces/densitymapbox/index.js

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* Copyright 2012-2019, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
module.exports = {
12+
attributes: require('./attributes'),
13+
supplyDefaults: require('./defaults'),
14+
colorbar: require('../heatmap/colorbar'),
15+
calc: require('./calc'),
16+
plot: require('./plot'),
17+
hoverPoints: require('./hover'),
18+
eventData: require('./event_data'),
19+
20+
moduleType: 'trace',
21+
name: 'densitymapbox',
22+
basePlotModule: require('../../plots/mapbox'),
23+
categories: ['mapbox', 'gl'],
24+
meta: {
25+
hr_name: 'density_mapbox',
26+
description: [
27+
'Draws a bivariate kernel density estimation with a Gaussian kernel',
28+
'from `lon` and `lat` coordinates and optional `z` values using a colorscale.'
29+
].join(' ')
30+
}
31+
};

‎src/traces/densitymapbox/plot.js

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/**
2+
* Copyright 2012-2019, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
var convert = require('./convert');
12+
13+
function DensityMapbox(subplot, uid) {
14+
this.subplot = subplot;
15+
this.uid = uid;
16+
17+
this.sourceId = uid + '-source';
18+
19+
this.layerList = [
20+
['heatmap', uid + '-layer-heatmap']
21+
];
22+
23+
// previous 'below' value,
24+
// need this to update it properly
25+
this.below = null;
26+
}
27+
28+
var proto = DensityMapbox.prototype;
29+
30+
proto.update = function(calcTrace) {
31+
var subplot = this.subplot;
32+
var layerList = this.layerList;
33+
var optsAll = convert(calcTrace);
34+
35+
subplot.map
36+
.getSource(this.sourceId)
37+
.setData(optsAll.geojson);
38+
39+
if(optsAll.below !== this.below) {
40+
this._removeLayers();
41+
this._addLayers(optsAll);
42+
}
43+
44+
for(var i = 0; i < layerList.length; i++) {
45+
var item = layerList[i];
46+
var k = item[0];
47+
var id = item[1];
48+
var opts = optsAll[k];
49+
50+
subplot.setOptions(id, 'setLayoutProperty', opts.layout);
51+
52+
if(opts.layout.visibility === 'visible') {
53+
subplot.setOptions(id, 'setPaintProperty', opts.paint);
54+
}
55+
}
56+
};
57+
58+
proto._addLayers = function(optsAll) {
59+
var subplot = this.subplot;
60+
var layerList = this.layerList;
61+
var sourceId = this.sourceId;
62+
var below = this.getBelow(optsAll);
63+
64+
for(var i = 0; i < layerList.length; i++) {
65+
var item = layerList[i];
66+
var k = item[0];
67+
var opts = optsAll[k];
68+
69+
subplot.map.addLayer({
70+
type: k,
71+
id: item[1],
72+
source: sourceId,
73+
layout: opts.layout,
74+
paint: opts.paint
75+
}, below);
76+
}
77+
78+
this.below = below;
79+
};
80+
81+
proto._removeLayers = function() {
82+
var map = this.subplot.map;
83+
var layerList = this.layerList;
84+
85+
for(var i = layerList.length - 1; i >= 0; i--) {
86+
map.removeLayer(layerList[i][1]);
87+
}
88+
};
89+
90+
proto.dispose = function() {
91+
var map = this.subplot.map;
92+
this._removeLayers();
93+
map.removeSource(this.sourceId);
94+
};
95+
96+
proto.getBelow = function(optsAll) {
97+
if(optsAll.below !== undefined) {
98+
return optsAll.below;
99+
}
100+
101+
var mapLayers = this.subplot.map.getStyle().layers;
102+
var out = '';
103+
104+
// find first layer with `type: 'symbol'`
105+
for(var i = 0; i < mapLayers.length; i++) {
106+
var layer = mapLayers[i];
107+
if(layer.type === 'symbol') {
108+
out = layer.id;
109+
break;
110+
}
111+
}
112+
113+
return out;
114+
};
115+
116+
module.exports = function createDensityMapbox(subplot, calcTrace) {
117+
var trace = calcTrace[0].trace;
118+
var densityMapbox = new DensityMapbox(subplot, trace.uid);
119+
var sourceId = densityMapbox.sourceId;
120+
121+
var optsAll = convert(calcTrace);
122+
123+
subplot.map.addSource(sourceId, {
124+
type: 'geojson',
125+
data: optsAll.geojson
126+
});
127+
128+
densityMapbox._addLayers(optsAll);
129+
130+
return densityMapbox;
131+
};
90.8 KB
Loading
57.5 KB
Loading
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"data": [{
3+
"type": "densitymapbox",
4+
"name": "w/ reversescale:true",
5+
"lon": [10, 20, 30],
6+
"lat": [15, 25, 35],
7+
"z": [1, 3, 2],
8+
"zmid": 0,
9+
"reversescale": true,
10+
"radius": 50,
11+
"below": "",
12+
"colorbar": {
13+
"y": 1,
14+
"yanchor": "top",
15+
"len": 0.45
16+
}
17+
}, {
18+
"type": "densitymapbox",
19+
"name": "w/0 z data",
20+
"lon": [-10, -20, -30],
21+
"lat": [15, 25, 35],
22+
"zmin": 0,
23+
"zauto": false,
24+
"radius": [50, 100, 10],
25+
"colorbar": {
26+
"y": 0,
27+
"yanchor": "bottom",
28+
"len": 0.45
29+
}
30+
}],
31+
"layout": {
32+
"mapbox": {
33+
"style": "light",
34+
"center": {"lat": 20}
35+
},
36+
"width": 600,
37+
"height": 400
38+
}
39+
}

‎test/image/mocks/mapbox_density0.json

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"data": [{
3+
"type": "densitymapbox",
4+
"lon": [10, 20, 30],
5+
"lat": [15, 25, 35],
6+
"z": [1, 3, 2]
7+
}],
8+
"layout": {
9+
"width": 600,
10+
"height": 400
11+
}
12+
}

‎test/image/mocks/mapbox_earthquake-density.json

+36,694
Large diffs are not rendered by default.

‎test/jasmine/assets/mock_lists.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ var glMockList = [
6868

6969
var mapboxMockList = [
7070
['scattermapbox', require('@mocks/mapbox_bubbles-text.json')],
71-
['choroplethmapbox', require('@mocks/mapbox_choropleth0.json')]
71+
['choroplethmapbox', require('@mocks/mapbox_choropleth0.json')],
72+
['densitymapbox', require('@mocks/mapbox_density0.json')]
7273
];
7374

7475
module.exports = {

‎test/jasmine/tests/densitymapbox_test.js

+487
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.