diff --git a/CHANGELOG.md b/CHANGELOG.md index 09b4e97b..480e84b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a - [[#412](https://github.com/plotly/plotly.rs/pull/412)] Add `Violin` trace type with box, mean line, KDE span, and split/grouped support - [[#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 - [[#417](https://github.com/plotly/plotly.rs/issues/417)] Add `ScatterMap` (MapLibre `map` subplot) trace type — the modern counterpart to `ScatterMapbox` +- [[#418](https://github.com/plotly/plotly.rs/issues/418)] Add native point clustering to `ScatterMap` via a `Cluster` option ### Changed diff --git a/plotly/src/traces/scatter_map.rs b/plotly/src/traces/scatter_map.rs index 5b213fff..d9f8708f 100644 --- a/plotly/src/traces/scatter_map.rs +++ b/plotly/src/traces/scatter_map.rs @@ -54,6 +54,56 @@ impl Selection { } } +/// Native point-clustering options for a [`ScatterMap`] trace. +/// +/// When enabled, nearby markers are grouped client-side (by MapLibre's +/// supercluster) into cluster bubbles that progressively expand into individual +/// points as the map is zoomed in. This declutters dense point maps without +/// dropping any data — every point still ships in the trace and becomes +/// individually visible/hoverable at higher zoom levels. +/// +/// This is a `scattermap`-only feature: the deprecated Mapbox-based +/// [`ScatterMapbox`](crate::ScatterMapbox) has no equivalent. +/// +/// # Examples +/// +/// ``` +/// use plotly::{traces::scatter_map::Cluster, ScatterMap}; +/// +/// let trace = ScatterMap::new(vec![45.5017], vec![-73.5673]) +/// .cluster(Cluster::new().enabled(true)); +/// ``` +#[serde_with::skip_serializing_none] +#[derive(Serialize, Clone, Debug, FieldSetter)] +pub struct Cluster { + /// Determines whether clustering is enabled or disabled. Defaults to + /// `false` unless `color`, `size`, or `step` is set, in which case it + /// defaults to `true`. + enabled: Option, + /// Sets the color for each cluster step. May be a single color or an array + /// of colors (one per step). + color: Option>>, + /// Sets the maximum zoom level (0-24) at which clustering applies. Past + /// this zoom, clusters expand into their individual points. + #[serde(rename = "maxzoom")] + max_zoom: Option, + /// Sets the marker opacity of the cluster bubbles. + opacity: Option, + /// Sets the size for each cluster step. May be a single size or an array + /// (one per step). + size: Option>, + /// Sets how many points it takes to create a cluster or advance to the next + /// cluster step. May be a single number or an array (one per step). A value + /// of `-1` (the default) lets MapLibre choose the step thresholds. + step: Option>, +} + +impl Cluster { + pub fn new() -> Self { + Default::default() + } +} + /// Construct a scatter trace drawn on the MapLibre `map` subplot /// (configured via [`LayoutMap`](crate::layout::LayoutMap)). /// @@ -206,6 +256,11 @@ where /// Line display properties. line: Option, + /// Native point-clustering options. When enabled, nearby markers are + /// grouped into clusters that expand into individual points as the map is + /// zoomed in. + cluster: Option, + /// Sets the text font. #[serde(rename = "textfont")] text_font: Option, @@ -320,6 +375,27 @@ mod tests { assert_eq!(to_value(selection).unwrap(), expected); } + #[test] + fn serialize_cluster() { + let cluster = Cluster::new() + .enabled(true) + .color("#112233") + .max_zoom(12.0) + .opacity(0.8) + .size(20) + .step(50); + let expected = json!({ + "enabled": true, + "color": "#112233", + "maxzoom": 12.0, + "opacity": 0.8, + "size": 20, + "step": 50, + }); + + assert_eq!(to_value(cluster).unwrap(), expected); + } + #[test] fn serialize_scatter_map() { let scatter_map = ScatterMap::new(vec![45.5017], vec![-73.5673]) @@ -348,6 +424,7 @@ mod tests { .subplot("map") .marker(Marker::new()) .line(Line::new()) + .cluster(Cluster::new().enabled(true)) .text_font(Font::new()) .selected_points(vec![0]) .selected(Selection::new().color("#111111")) @@ -382,6 +459,7 @@ mod tests { "subplot": "map", "marker": {}, "line": {}, + "cluster": {"enabled": true}, "textfont": {}, "selectedpoints": [0], "selected": {"marker": {"color": "#111111"}},