Skip to content

Commit ca48461

Browse files
authored
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+
};

0 commit comments

Comments
 (0)