Skip to content

Commit 2cc42e2

Browse files
authored
Merge pull request #5827 from plotly/finalist-cluster-scattermapbox
Add clustering options to `scattermapbox`
2 parents e377c38 + 477d2a0 commit 2cc42e2

13 files changed

+590
-43
lines changed

draftlogs/5827_add.md

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- Add clustering options to `scattermapbox` [[#5827](https://github.com/plotly/plotly.js/pull/5827)],
2+
with thanks to @elben10 for the contribution!

src/traces/scattermapbox/attributes.js

+45
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ var colorScaleAttrs = require('../../components/colorscale/attributes');
1010

1111
var extendFlat = require('../../lib/extend').extendFlat;
1212
var overrideAll = require('../../plot_api/edit_types').overrideAll;
13+
var mapboxLayoutAtributes = require('../../plots/mapbox/layout_attributes');
1314

1415
var lineAttrs = scatterGeoAttrs.line;
1516
var markerAttrs = scatterGeoAttrs.marker;
@@ -18,6 +19,50 @@ module.exports = overrideAll({
1819
lon: scatterGeoAttrs.lon,
1920
lat: scatterGeoAttrs.lat,
2021

22+
cluster: {
23+
enabled: {
24+
valType: 'boolean',
25+
description: 'Determines whether clustering is enabled or disabled.'
26+
},
27+
maxzoom: extendFlat({}, mapboxLayoutAtributes.layers.maxzoom, {
28+
description: [
29+
'Sets the maximum zoom level.',
30+
'At zoom levels equal to or greater than this, points will never be clustered.'
31+
].join(' ')
32+
}),
33+
step: {
34+
valType: 'number',
35+
arrayOk: true,
36+
dflt: -1,
37+
min: -1,
38+
description: [
39+
'Sets how many points it takes to create a cluster or advance to the next cluster step.',
40+
'Use this in conjunction with arrays for `size` and / or `color`.',
41+
'If an integer, steps start at multiples of this number.',
42+
'If an array, each step extends from the given value until one less than the next value.'
43+
].join(' ')
44+
},
45+
size: {
46+
valType: 'number',
47+
arrayOk: true,
48+
dflt: 20,
49+
min: 0,
50+
description: [
51+
'Sets the size for each cluster step.'
52+
].join(' ')
53+
},
54+
color: {
55+
valType: 'color',
56+
arrayOk: true,
57+
description: [
58+
'Sets the color for each cluster step.'
59+
].join(' ')
60+
},
61+
opacity: extendFlat({}, markerAttrs.opacity, {
62+
dflt: 1
63+
})
64+
},
65+
2166
// locations
2267
// locationmode
2368

src/traces/scattermapbox/convert.js

+51-6
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,12 @@ module.exports = function convert(gd, calcTrace) {
2626
var hasText = subTypes.hasText(trace);
2727
var hasCircles = (hasMarkers && trace.marker.symbol === 'circle');
2828
var hasSymbols = (hasMarkers && trace.marker.symbol !== 'circle');
29+
var hasCluster = trace.cluster && trace.cluster.enabled;
2930

30-
var fill = initContainer();
31-
var line = initContainer();
32-
var circle = initContainer();
33-
var symbol = initContainer();
31+
var fill = initContainer('fill');
32+
var line = initContainer('line');
33+
var circle = initContainer('circle');
34+
var symbol = initContainer('symbol');
3435

3536
var opts = {
3637
fill: fill,
@@ -74,6 +75,29 @@ module.exports = function convert(gd, calcTrace) {
7475
var circleOpts = makeCircleOpts(calcTrace);
7576
circle.geojson = circleOpts.geojson;
7677
circle.layout.visibility = 'visible';
78+
if(hasCluster) {
79+
circle.filter = ['!', ['has', 'point_count']];
80+
opts.cluster = {
81+
type: 'circle',
82+
filter: ['has', 'point_count'],
83+
layout: {visibility: 'visible'},
84+
paint: {
85+
'circle-color': arrayifyAttribute(trace.cluster.color, trace.cluster.step),
86+
'circle-radius': arrayifyAttribute(trace.cluster.size, trace.cluster.step),
87+
'circle-opacity': arrayifyAttribute(trace.cluster.opacity, trace.cluster.step),
88+
},
89+
};
90+
opts.clusterCount = {
91+
type: 'symbol',
92+
filter: ['has', 'point_count'],
93+
paint: {},
94+
layout: {
95+
'text-field': '{point_count_abbreviated}',
96+
'text-font': ['Open Sans Regular', 'Arial Unicode MS Regular'],
97+
'text-size': 12
98+
}
99+
};
100+
}
77101

78102
Lib.extendFlat(circle.paint, {
79103
'circle-color': circleOpts.mcc,
@@ -82,6 +106,10 @@ module.exports = function convert(gd, calcTrace) {
82106
});
83107
}
84108

109+
if(hasCircles && hasCluster) {
110+
circle.filter = ['!', ['has', 'point_count']];
111+
}
112+
85113
if(hasSymbols || hasText) {
86114
symbol.geojson = makeSymbolGeoJSON(calcTrace, gd);
87115

@@ -142,10 +170,12 @@ module.exports = function convert(gd, calcTrace) {
142170
return opts;
143171
};
144172

145-
function initContainer() {
173+
function initContainer(type) {
146174
return {
175+
type: type,
147176
geojson: geoJsonUtils.makeBlank(),
148177
layout: { visibility: 'none' },
178+
filter: null,
149179
paint: {}
150180
};
151181
}
@@ -200,7 +230,8 @@ function makeCircleOpts(calcTrace) {
200230

201231
features.push({
202232
type: 'Feature',
203-
geometry: {type: 'Point', coordinates: lonlat},
233+
id: i + 1,
234+
geometry: { type: 'Point', coordinates: lonlat },
204235
properties: props
205236
});
206237
}
@@ -323,3 +354,17 @@ function blankFillFunc() { return ''; }
323354
function isBADNUM(lonlat) {
324355
return lonlat[0] === BADNUM;
325356
}
357+
358+
function arrayifyAttribute(values, step) {
359+
var newAttribute;
360+
if(Lib.isArrayOrTypedArray(values) && Lib.isArrayOrTypedArray(step)) {
361+
newAttribute = ['step', ['get', 'point_count'], values[0]];
362+
363+
for(var idx = 1; idx < values.length; idx++) {
364+
newAttribute.push(step[idx - 1], values[idx]);
365+
}
366+
} else {
367+
newAttribute = values;
368+
}
369+
return newAttribute;
370+
}

src/traces/scattermapbox/defaults.js

+19
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
1414
return Lib.coerce(traceIn, traceOut, attributes, attr, dflt);
1515
}
1616

17+
function coerce2(attr, dflt) {
18+
return Lib.coerce2(traceIn, traceOut, attributes, attr, dflt);
19+
}
20+
1721
var len = handleLonLatDefaults(traceIn, traceOut, coerce);
1822
if(!len) {
1923
traceOut.visible = false;
@@ -46,6 +50,21 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
4650
}
4751
}
4852

53+
var clusterMaxzoom = coerce2('cluster.maxzoom');
54+
var clusterStep = coerce2('cluster.step');
55+
var clusterColor = coerce2('cluster.color', (traceOut.marker && traceOut.marker.color) || defaultColor);
56+
var clusterSize = coerce2('cluster.size');
57+
var clusterOpacity = coerce2('cluster.opacity');
58+
59+
var clusterEnabledDflt =
60+
clusterMaxzoom !== false ||
61+
clusterStep !== false ||
62+
clusterColor !== false ||
63+
clusterSize !== false ||
64+
clusterOpacity !== false;
65+
66+
coerce('cluster.enabled', clusterEnabledDflt);
67+
4968
if(subTypes.hasText(traceOut)) {
5069
handleTextDefaults(traceIn, traceOut, layout, coerce, {noSelect: true});
5170
}

src/traces/scattermapbox/hover.js

+10
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,22 @@ var Lib = require('../../lib');
55
var getTraceColor = require('../scatter/get_trace_color');
66
var fillText = Lib.fillText;
77
var BADNUM = require('../../constants/numerical').BADNUM;
8+
var LAYER_PREFIX = require('../../plots/mapbox/constants').traceLayerPrefix;
89

910
function hoverPoints(pointData, xval, yval) {
1011
var cd = pointData.cd;
1112
var trace = cd[0].trace;
1213
var xa = pointData.xa;
1314
var ya = pointData.ya;
1415
var subplot = pointData.subplot;
16+
var clusteredPointsIds = [];
17+
var layer = LAYER_PREFIX + trace.uid + '-circle';
18+
var hasCluster = trace.cluster && trace.cluster.enabled;
19+
20+
if(hasCluster) {
21+
var elems = subplot.map.queryRenderedFeatures(null, {layers: [layer]});
22+
clusteredPointsIds = elems.map(function(elem) {return elem.id;});
23+
}
1524

1625
// compute winding number about [-180, 180] globe
1726
var winding = (xval >= 0) ?
@@ -25,6 +34,7 @@ function hoverPoints(pointData, xval, yval) {
2534
function distFn(d) {
2635
var lonlat = d.lonlat;
2736
if(lonlat[0] === BADNUM) return Infinity;
37+
if(hasCluster && clusteredPointsIds.indexOf(d.i + 1) === -1) return Infinity;
2838

2939
var lon = Lib.modHalf(lonlat[0], 360);
3040
var lat = lonlat[1];

0 commit comments

Comments
 (0)