Skip to content

Commit 45e613d

Browse files
jqnatividadclaude
andauthored
feat: add DensityMap trace for the MapLibre map subplot (#416)
Add the modern MapLibre-based `densitymap` trace type, the density counterpart to `ChoroplethMap`. It draws a weighted kernel-density heatmap from lat/lon points (optional per-point weight z) onto the `layout.map` subplot, with full color-scale (colorscale, colorbar, showscale, reversescale, autocolorscale, coloraxis, zmin/zmax/zmid) and hover (text, hovertext, hoverinfo, hovertemplate, hoverlabel, customdata) support. Unlike ChoroplethMap it has no marker/selection. Registers the trace: DensityMap variant on PlotType (serializes to "densitymap"), module + type re-exports in traces/mod.rs and lib.rs. Includes a serialize_density_map unit test and a doctest. Signed-off-by: Andrei Gherghescu <8067229+andrei-ng@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 3743d96 commit 45e613d

5 files changed

Lines changed: 216 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
1111
- [[#410](https://github.com/plotly/plotly.rs/pull/410)] Add `Choropleth` (geo subplot) and `ChoroplethMap` (MapLibre `map` subplot) trace types, with a `LocationMode` enum and a dedicated `choropleth::Marker`; add the MapLibre `map` subplot via `LayoutMap`/`MapStyle`/`MapBounds`
1212
- [[#410](https://github.com/plotly/plotly.rs/pull/410)] Add `LayoutGeo` options: `fitbounds` (`GeoFitBounds`), `resolution` (`GeoResolution`, 1:110M/1:50M base-layer detail), and `bgcolor`
1313
- [[#412](https://github.com/plotly/plotly.rs/pull/412)] Add `Violin` trace type with box, mean line, KDE span, and split/grouped support
14+
- [[#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
1415

1516
### Changed
1617

plotly/src/common/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ pub enum PlotType {
230230
Sankey,
231231
Surface,
232232
DensityMapbox,
233+
DensityMap,
233234
Table,
234235
Pie,
235236
Treemap,

plotly/src/lib.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,14 @@ pub use layout::Layout;
6060
pub use plot::{Plot, Trace, Traces};
6161
// Also provide easy access to modules which contain additional trace-specific types
6262
pub use traces::{
63-
box_plot, choropleth, choropleth_map, contour, heat_map, histogram, image, mesh3d, sankey,
64-
scatter, scatter3d, scatter_mapbox, sunburst, surface, treemap, violin,
63+
box_plot, choropleth, choropleth_map, contour, density_map, heat_map, histogram, image, mesh3d,
64+
sankey, scatter, scatter3d, scatter_mapbox, sunburst, surface, treemap, violin,
6565
};
6666
// Bring the different trace types into the top-level scope
6767
pub use traces::{
68-
Bar, BoxPlot, Candlestick, Choropleth, ChoroplethMap, Contour, DensityMapbox, HeatMap,
69-
Histogram, Image, Mesh3D, Ohlc, Pie, Sankey, Scatter, Scatter3D, ScatterGeo, ScatterMapbox,
70-
ScatterPolar, Sunburst, Surface, Table, Treemap, Violin,
68+
Bar, BoxPlot, Candlestick, Choropleth, ChoroplethMap, Contour, DensityMap, DensityMapbox,
69+
HeatMap, Histogram, Image, Mesh3D, Ohlc, Pie, Sankey, Scatter, Scatter3D, ScatterGeo,
70+
ScatterMapbox, ScatterPolar, Sunburst, Surface, Table, Treemap, Violin,
7171
};
7272

7373
pub trait Restyle: serde::Serialize {}

plotly/src/traces/density_map.rs

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
//! Density heatmap trace for the MapLibre `map` subplot.
2+
3+
use plotly_derive::FieldSetter;
4+
use serde::Serialize;
5+
6+
use crate::common::{
7+
ColorBar, ColorScale, Dim, HoverInfo, Label, LegendGroupTitle, PlotType, Visible,
8+
};
9+
use crate::private::{NumOrString, NumOrStringCollection};
10+
use crate::Trace;
11+
12+
/// Construct a density heatmap trace drawn on the MapLibre `map` subplot
13+
/// (configured via [`LayoutMap`](crate::layout::LayoutMap)).
14+
///
15+
/// `DensityMap` is the modern MapLibre-based counterpart to
16+
/// [`DensityMapbox`](crate::DensityMapbox): it draws a weighted kernel-density
17+
/// heatmap from `lat`/`lon` points (with an optional per-point weight `z`) onto
18+
/// the `layout.map` subplot.
19+
///
20+
/// # Examples
21+
///
22+
/// ```
23+
/// use plotly::DensityMap;
24+
///
25+
/// let trace = DensityMap::new(vec![45.5017], vec![-73.5673], vec![1.0])
26+
/// .radius(20)
27+
/// .name("montreal");
28+
/// ```
29+
#[serde_with::skip_serializing_none]
30+
#[derive(Serialize, Clone, Debug, FieldSetter)]
31+
#[field_setter(box_self, kind = "trace")]
32+
pub struct DensityMap<Lat, Lon, Z>
33+
where
34+
Lat: Serialize + Clone,
35+
Lon: Serialize + Clone,
36+
Z: Serialize + Clone,
37+
{
38+
#[field_setter(default = "PlotType::DensityMap")]
39+
r#type: PlotType,
40+
/// Sets the trace name. The trace name appears as the legend item and on
41+
/// hover.
42+
name: Option<String>,
43+
/// Determines whether or not this trace is visible.
44+
visible: Option<Visible>,
45+
/// Determines whether or not an item corresponding to this trace is shown
46+
/// in the legend.
47+
#[serde(rename = "showlegend")]
48+
show_legend: Option<bool>,
49+
/// Sets the legend rank for this trace.
50+
#[serde(rename = "legendrank")]
51+
legend_rank: Option<usize>,
52+
/// Sets the legend group for this trace.
53+
#[serde(rename = "legendgroup")]
54+
legend_group: Option<String>,
55+
/// Set and style the title to appear for the legend group.
56+
#[serde(rename = "legendgrouptitle")]
57+
legend_group_title: Option<LegendGroupTitle>,
58+
/// Assigns id labels to each datum.
59+
ids: Option<Vec<String>>,
60+
61+
/// Sets the latitude coordinates (in degrees North).
62+
lat: Option<Vec<Lat>>,
63+
/// Sets the longitude coordinates (in degrees East).
64+
lon: Option<Vec<Lon>>,
65+
/// Sets the points' weight. For example, a value of 10 would be equivalent
66+
/// to having 10 points of weight 1 in the same spot.
67+
z: Option<Vec<Z>>,
68+
/// Sets the radius of influence of one `lat`/`lon` point in pixels.
69+
/// Increasing the value makes the density map smoother, but less detailed.
70+
radius: Option<u8>,
71+
/// Sets the opacity of the trace.
72+
opacity: Option<f64>,
73+
74+
/// Sets the text elements associated with each (lat,lon,z) triplet.
75+
text: Option<Dim<String>>,
76+
/// Sets the hover text elements associated with each (lat,lon,z) triplet.
77+
#[serde(rename = "hovertext")]
78+
hover_text: Option<Dim<String>>,
79+
/// Determines which trace information appears on hover.
80+
#[serde(rename = "hoverinfo")]
81+
hover_info: Option<HoverInfo>,
82+
/// Template string used for rendering the information that appears on the
83+
/// hover box.
84+
#[serde(rename = "hovertemplate")]
85+
hover_template: Option<Dim<String>>,
86+
#[serde(rename = "hovertemplatefallback")]
87+
hover_template_fallback: Option<Dim<String>>,
88+
/// Properties of the hover label.
89+
#[serde(rename = "hoverlabel")]
90+
hover_label: Option<Label>,
91+
92+
/// Determines whether or not the color domain is computed with respect to
93+
/// the input data (here in `z`) or the bounds set in `zmin` and `zmax`.
94+
#[serde(rename = "zauto")]
95+
z_auto: Option<bool>,
96+
/// Sets the lower bound of the color domain.
97+
#[serde(rename = "zmin")]
98+
z_min: Option<f64>,
99+
/// Sets the upper bound of the color domain.
100+
#[serde(rename = "zmax")]
101+
z_max: Option<f64>,
102+
/// Sets the mid-point of the color domain.
103+
#[serde(rename = "zmid")]
104+
z_mid: Option<f64>,
105+
/// Sets the colorscale.
106+
#[serde(rename = "colorscale")]
107+
color_scale: Option<ColorScale>,
108+
/// Determines whether the colorscale is a default palette.
109+
#[serde(rename = "autocolorscale")]
110+
auto_color_scale: Option<bool>,
111+
/// Reverses the color mapping if true.
112+
#[serde(rename = "reversescale")]
113+
reverse_scale: Option<bool>,
114+
/// Determines whether or not a colorbar is displayed for this trace.
115+
#[serde(rename = "showscale")]
116+
show_scale: Option<bool>,
117+
/// Properties of the colorbar.
118+
#[serde(rename = "colorbar")]
119+
color_bar: Option<ColorBar>,
120+
/// Sets a reference to a shared color axis.
121+
#[serde(rename = "coloraxis")]
122+
color_axis: Option<String>,
123+
124+
/// Sets a reference to the `map` subplot this trace is drawn on. Defaults
125+
/// to `"map"`.
126+
subplot: Option<String>,
127+
/// Determines if this trace's layer is inserted below the layer with the
128+
/// specified ID. By default, the layer is inserted above every existing
129+
/// layer.
130+
below: Option<String>,
131+
132+
/// Assigns extra meta information associated with this trace.
133+
meta: Option<NumOrString>,
134+
/// Assigns extra data to each datum.
135+
#[serde(rename = "customdata")]
136+
custom_data: Option<NumOrStringCollection>,
137+
/// Controls persistence of some user-driven changes to the trace.
138+
#[serde(rename = "uirevision")]
139+
ui_revision: Option<NumOrString>,
140+
}
141+
142+
impl<Lat, Lon, Z> DensityMap<Lat, Lon, Z>
143+
where
144+
Lat: Serialize + Clone + std::default::Default,
145+
Lon: Serialize + Clone + std::default::Default,
146+
Z: Serialize + Clone + std::default::Default,
147+
{
148+
pub fn new(lat: Vec<Lat>, lon: Vec<Lon>, z: Vec<Z>) -> Box<Self> {
149+
Box::new(Self {
150+
lat: Some(lat),
151+
lon: Some(lon),
152+
z: Some(z),
153+
..Default::default()
154+
})
155+
}
156+
}
157+
158+
impl<Lat, Lon, Z> Trace for DensityMap<Lat, Lon, Z>
159+
where
160+
Lat: Serialize + Clone,
161+
Lon: Serialize + Clone,
162+
Z: Serialize + Clone,
163+
{
164+
fn to_json(&self) -> String {
165+
serde_json::to_string(&self).unwrap()
166+
}
167+
}
168+
169+
#[cfg(test)]
170+
mod tests {
171+
use serde_json::{json, to_value};
172+
173+
use super::*;
174+
use crate::common::ColorScalePalette;
175+
176+
#[test]
177+
fn serialize_density_map() {
178+
let trace = DensityMap::new(vec![45.5017], vec![-73.5673], vec![1.0])
179+
.name("montreal")
180+
.radius(20)
181+
.opacity(0.5)
182+
.hover_text_array(vec!["Montreal"])
183+
.hover_info(HoverInfo::Text)
184+
.color_scale(ColorScale::Palette(ColorScalePalette::Viridis))
185+
.show_scale(true)
186+
.subplot("map")
187+
.below("");
188+
189+
let expected = json!({
190+
"type": "densitymap",
191+
"lat": [45.5017],
192+
"lon": [-73.5673],
193+
"z": [1.0],
194+
"name": "montreal",
195+
"radius": 20,
196+
"opacity": 0.5,
197+
"hovertext": ["Montreal"],
198+
"hoverinfo": "text",
199+
"colorscale": "Viridis",
200+
"showscale": true,
201+
"subplot": "map",
202+
"below": "",
203+
});
204+
205+
assert_eq!(to_value(trace).unwrap(), expected);
206+
}
207+
}

plotly/src/traces/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ mod candlestick;
66
pub mod choropleth;
77
pub mod choropleth_map;
88
pub mod contour;
9+
pub mod density_map;
910
mod density_mapbox;
1011
pub mod heat_map;
1112
pub mod histogram;
@@ -31,6 +32,7 @@ pub use candlestick::Candlestick;
3132
pub use choropleth::Choropleth;
3233
pub use choropleth_map::ChoroplethMap;
3334
pub use contour::Contour;
35+
pub use density_map::DensityMap;
3436
pub use density_mapbox::DensityMapbox;
3537
pub use heat_map::HeatMap;
3638
pub use histogram::Histogram;

0 commit comments

Comments
 (0)