Skip to content

Commit

Permalink
Merge pull request #5827 from plotly/finalist-cluster-scattermapbox
Browse files Browse the repository at this point in the history
Add clustering options to `scattermapbox`
  • Loading branch information
archmoj authored Oct 14, 2022
2 parents e377c38 + 477d2a0 commit 2cc42e2
Show file tree
Hide file tree
Showing 13 changed files with 590 additions and 43 deletions.
2 changes: 2 additions & 0 deletions draftlogs/5827_add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Add clustering options to `scattermapbox` [[#5827](https://github.com/plotly/plotly.js/pull/5827)],
with thanks to @elben10 for the contribution!
45 changes: 45 additions & 0 deletions src/traces/scattermapbox/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ var colorScaleAttrs = require('../../components/colorscale/attributes');

var extendFlat = require('../../lib/extend').extendFlat;
var overrideAll = require('../../plot_api/edit_types').overrideAll;
var mapboxLayoutAtributes = require('../../plots/mapbox/layout_attributes');

var lineAttrs = scatterGeoAttrs.line;
var markerAttrs = scatterGeoAttrs.marker;
Expand All @@ -18,6 +19,50 @@ module.exports = overrideAll({
lon: scatterGeoAttrs.lon,
lat: scatterGeoAttrs.lat,

cluster: {
enabled: {
valType: 'boolean',
description: 'Determines whether clustering is enabled or disabled.'
},
maxzoom: extendFlat({}, mapboxLayoutAtributes.layers.maxzoom, {
description: [
'Sets the maximum zoom level.',
'At zoom levels equal to or greater than this, points will never be clustered.'
].join(' ')
}),
step: {
valType: 'number',
arrayOk: true,
dflt: -1,
min: -1,
description: [
'Sets how many points it takes to create a cluster or advance to the next cluster step.',
'Use this in conjunction with arrays for `size` and / or `color`.',
'If an integer, steps start at multiples of this number.',
'If an array, each step extends from the given value until one less than the next value.'
].join(' ')
},
size: {
valType: 'number',
arrayOk: true,
dflt: 20,
min: 0,
description: [
'Sets the size for each cluster step.'
].join(' ')
},
color: {
valType: 'color',
arrayOk: true,
description: [
'Sets the color for each cluster step.'
].join(' ')
},
opacity: extendFlat({}, markerAttrs.opacity, {
dflt: 1
})
},

// locations
// locationmode

Expand Down
57 changes: 51 additions & 6 deletions src/traces/scattermapbox/convert.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@ module.exports = function convert(gd, calcTrace) {
var hasText = subTypes.hasText(trace);
var hasCircles = (hasMarkers && trace.marker.symbol === 'circle');
var hasSymbols = (hasMarkers && trace.marker.symbol !== 'circle');
var hasCluster = trace.cluster && trace.cluster.enabled;

var fill = initContainer();
var line = initContainer();
var circle = initContainer();
var symbol = initContainer();
var fill = initContainer('fill');
var line = initContainer('line');
var circle = initContainer('circle');
var symbol = initContainer('symbol');

var opts = {
fill: fill,
Expand Down Expand Up @@ -74,6 +75,29 @@ module.exports = function convert(gd, calcTrace) {
var circleOpts = makeCircleOpts(calcTrace);
circle.geojson = circleOpts.geojson;
circle.layout.visibility = 'visible';
if(hasCluster) {
circle.filter = ['!', ['has', 'point_count']];
opts.cluster = {
type: 'circle',
filter: ['has', 'point_count'],
layout: {visibility: 'visible'},
paint: {
'circle-color': arrayifyAttribute(trace.cluster.color, trace.cluster.step),
'circle-radius': arrayifyAttribute(trace.cluster.size, trace.cluster.step),
'circle-opacity': arrayifyAttribute(trace.cluster.opacity, trace.cluster.step),
},
};
opts.clusterCount = {
type: 'symbol',
filter: ['has', 'point_count'],
paint: {},
layout: {
'text-field': '{point_count_abbreviated}',
'text-font': ['Open Sans Regular', 'Arial Unicode MS Regular'],
'text-size': 12
}
};
}

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

if(hasCircles && hasCluster) {
circle.filter = ['!', ['has', 'point_count']];
}

if(hasSymbols || hasText) {
symbol.geojson = makeSymbolGeoJSON(calcTrace, gd);

Expand Down Expand Up @@ -142,10 +170,12 @@ module.exports = function convert(gd, calcTrace) {
return opts;
};

function initContainer() {
function initContainer(type) {
return {
type: type,
geojson: geoJsonUtils.makeBlank(),
layout: { visibility: 'none' },
filter: null,
paint: {}
};
}
Expand Down Expand Up @@ -200,7 +230,8 @@ function makeCircleOpts(calcTrace) {

features.push({
type: 'Feature',
geometry: {type: 'Point', coordinates: lonlat},
id: i + 1,
geometry: { type: 'Point', coordinates: lonlat },
properties: props
});
}
Expand Down Expand Up @@ -323,3 +354,17 @@ function blankFillFunc() { return ''; }
function isBADNUM(lonlat) {
return lonlat[0] === BADNUM;
}

function arrayifyAttribute(values, step) {
var newAttribute;
if(Lib.isArrayOrTypedArray(values) && Lib.isArrayOrTypedArray(step)) {
newAttribute = ['step', ['get', 'point_count'], values[0]];

for(var idx = 1; idx < values.length; idx++) {
newAttribute.push(step[idx - 1], values[idx]);
}
} else {
newAttribute = values;
}
return newAttribute;
}
19 changes: 19 additions & 0 deletions src/traces/scattermapbox/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
return Lib.coerce(traceIn, traceOut, attributes, attr, dflt);
}

function coerce2(attr, dflt) {
return Lib.coerce2(traceIn, traceOut, attributes, attr, dflt);
}

var len = handleLonLatDefaults(traceIn, traceOut, coerce);
if(!len) {
traceOut.visible = false;
Expand Down Expand Up @@ -46,6 +50,21 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
}
}

var clusterMaxzoom = coerce2('cluster.maxzoom');
var clusterStep = coerce2('cluster.step');
var clusterColor = coerce2('cluster.color', (traceOut.marker && traceOut.marker.color) || defaultColor);
var clusterSize = coerce2('cluster.size');
var clusterOpacity = coerce2('cluster.opacity');

var clusterEnabledDflt =
clusterMaxzoom !== false ||
clusterStep !== false ||
clusterColor !== false ||
clusterSize !== false ||
clusterOpacity !== false;

coerce('cluster.enabled', clusterEnabledDflt);

if(subTypes.hasText(traceOut)) {
handleTextDefaults(traceIn, traceOut, layout, coerce, {noSelect: true});
}
Expand Down
10 changes: 10 additions & 0 deletions src/traces/scattermapbox/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,22 @@ var Lib = require('../../lib');
var getTraceColor = require('../scatter/get_trace_color');
var fillText = Lib.fillText;
var BADNUM = require('../../constants/numerical').BADNUM;
var LAYER_PREFIX = require('../../plots/mapbox/constants').traceLayerPrefix;

function hoverPoints(pointData, xval, yval) {
var cd = pointData.cd;
var trace = cd[0].trace;
var xa = pointData.xa;
var ya = pointData.ya;
var subplot = pointData.subplot;
var clusteredPointsIds = [];
var layer = LAYER_PREFIX + trace.uid + '-circle';
var hasCluster = trace.cluster && trace.cluster.enabled;

if(hasCluster) {
var elems = subplot.map.queryRenderedFeatures(null, {layers: [layer]});
clusteredPointsIds = elems.map(function(elem) {return elem.id;});
}

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

var lon = Lib.modHalf(lonlat[0], 360);
var lat = lonlat[1];
Expand Down
Loading

0 comments on commit 2cc42e2

Please sign in to comment.