Skip to content

Commit 0c7fc5e

Browse files
jqnatividadandrei-ng
authored andcommitted
feat: add native point clustering to ScatterMap
- add a `Cluster` option to the MapLibre `ScatterMap` trace, exposing plotly.js's `scattermap.cluster` attributes (`enabled`, `color`, `maxzoom`, `opacity`, `size`, `step`). Clustering groups nearby markers client-side (MapLibre supercluster) into bubbles that expand into individual points on zoom, decluttering dense point maps without dropping any data. This is a `scattermap`-only feature; the deprecated `ScatterMapbox` has no equivalent. Stacked on the ScatterMap trace addition (#417). Signed-off-by: Andrei Gherghescu <8067229+andrei-ng@users.noreply.github.com>
1 parent f3d1299 commit 0c7fc5e

2 files changed

Lines changed: 79 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
1313
- [[#412](https://github.com/plotly/plotly.rs/pull/412)] Add `Violin` trace type with box, mean line, KDE span, and split/grouped support
1414
- [[#414](https://github.com/plotly/plotly.rs/issues/414)] Add `DensityMap` (MapLibre `map` subplot) trace type — density heatmaps with full color-scale and hover support
1515
- [[#417](https://github.com/plotly/plotly.rs/issues/417)] Add `ScatterMap` (MapLibre `map` subplot) trace type — the modern counterpart to `ScatterMapbox`
16+
- [[#418](https://github.com/plotly/plotly.rs/issues/418)] Add native point clustering to `ScatterMap` via a `Cluster` option
1617

1718
### Changed
1819

plotly/src/traces/scatter_map.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,56 @@ impl Selection {
5454
}
5555
}
5656

57+
/// Native point-clustering options for a [`ScatterMap`] trace.
58+
///
59+
/// When enabled, nearby markers are grouped client-side (by MapLibre's
60+
/// supercluster) into cluster bubbles that progressively expand into individual
61+
/// points as the map is zoomed in. This declutters dense point maps without
62+
/// dropping any data — every point still ships in the trace and becomes
63+
/// individually visible/hoverable at higher zoom levels.
64+
///
65+
/// This is a `scattermap`-only feature: the deprecated Mapbox-based
66+
/// [`ScatterMapbox`](crate::ScatterMapbox) has no equivalent.
67+
///
68+
/// # Examples
69+
///
70+
/// ```
71+
/// use plotly::{traces::scatter_map::Cluster, ScatterMap};
72+
///
73+
/// let trace = ScatterMap::new(vec![45.5017], vec![-73.5673])
74+
/// .cluster(Cluster::new().enabled(true));
75+
/// ```
76+
#[serde_with::skip_serializing_none]
77+
#[derive(Serialize, Clone, Debug, FieldSetter)]
78+
pub struct Cluster {
79+
/// Determines whether clustering is enabled or disabled. Defaults to
80+
/// `false` unless `color`, `size`, or `step` is set, in which case it
81+
/// defaults to `true`.
82+
enabled: Option<bool>,
83+
/// Sets the color for each cluster step. May be a single color or an array
84+
/// of colors (one per step).
85+
color: Option<Dim<Box<dyn Color>>>,
86+
/// Sets the maximum zoom level (0-24) at which clustering applies. Past
87+
/// this zoom, clusters expand into their individual points.
88+
#[serde(rename = "maxzoom")]
89+
max_zoom: Option<f64>,
90+
/// Sets the marker opacity of the cluster bubbles.
91+
opacity: Option<f64>,
92+
/// Sets the size for each cluster step. May be a single size or an array
93+
/// (one per step).
94+
size: Option<Dim<usize>>,
95+
/// Sets how many points it takes to create a cluster or advance to the next
96+
/// cluster step. May be a single number or an array (one per step). A value
97+
/// of `-1` (the default) lets MapLibre choose the step thresholds.
98+
step: Option<Dim<i32>>,
99+
}
100+
101+
impl Cluster {
102+
pub fn new() -> Self {
103+
Default::default()
104+
}
105+
}
106+
57107
/// Construct a scatter trace drawn on the MapLibre `map` subplot
58108
/// (configured via [`LayoutMap`](crate::layout::LayoutMap)).
59109
///
@@ -206,6 +256,11 @@ where
206256
/// Line display properties.
207257
line: Option<Line>,
208258

259+
/// Native point-clustering options. When enabled, nearby markers are
260+
/// grouped into clusters that expand into individual points as the map is
261+
/// zoomed in.
262+
cluster: Option<Cluster>,
263+
209264
/// Sets the text font.
210265
#[serde(rename = "textfont")]
211266
text_font: Option<Font>,
@@ -320,6 +375,27 @@ mod tests {
320375
assert_eq!(to_value(selection).unwrap(), expected);
321376
}
322377

378+
#[test]
379+
fn serialize_cluster() {
380+
let cluster = Cluster::new()
381+
.enabled(true)
382+
.color("#112233")
383+
.max_zoom(12.0)
384+
.opacity(0.8)
385+
.size(20)
386+
.step(50);
387+
let expected = json!({
388+
"enabled": true,
389+
"color": "#112233",
390+
"maxzoom": 12.0,
391+
"opacity": 0.8,
392+
"size": 20,
393+
"step": 50,
394+
});
395+
396+
assert_eq!(to_value(cluster).unwrap(), expected);
397+
}
398+
323399
#[test]
324400
fn serialize_scatter_map() {
325401
let scatter_map = ScatterMap::new(vec![45.5017], vec![-73.5673])
@@ -348,6 +424,7 @@ mod tests {
348424
.subplot("map")
349425
.marker(Marker::new())
350426
.line(Line::new())
427+
.cluster(Cluster::new().enabled(true))
351428
.text_font(Font::new())
352429
.selected_points(vec![0])
353430
.selected(Selection::new().color("#111111"))
@@ -382,6 +459,7 @@ mod tests {
382459
"subplot": "map",
383460
"marker": {},
384461
"line": {},
462+
"cluster": {"enabled": true},
385463
"textfont": {},
386464
"selectedpoints": [0],
387465
"selected": {"marker": {"color": "#111111"}},

0 commit comments

Comments
 (0)