plotlars_core/plots/density_mapbox.rs
1use bon::bon;
2
3use polars::frame::DataFrame;
4
5use crate::{
6 components::{Legend, Text},
7 ir::data::ColumnData,
8 ir::layout::{LayoutIR, MapboxIR},
9 ir::trace::{DensityMapboxIR, TraceIR},
10};
11
12/// A structure representing a density mapbox visualization.
13///
14/// The `DensityMapbox` struct enables the creation of geographic density visualizations on an interactive map.
15/// It displays density or intensity values at geographic locations using latitude and longitude coordinates,
16/// with a third dimension (z) representing the intensity at each point. This is useful for visualizing
17/// population density, heat maps of activity, or any geographic concentration of values.
18///
19/// # Backend Support
20///
21/// | Backend | Supported |
22/// |---------|-----------|
23/// | Plotly | Yes |
24/// | Plotters| -- |
25///
26/// # Arguments
27///
28/// * `data` - A reference to the `DataFrame` containing the data to be plotted.
29/// * `lat` - A string slice specifying the column name containing latitude values.
30/// * `lon` - A string slice specifying the column name containing longitude values.
31/// * `z` - A string slice specifying the column name containing intensity/density values.
32/// * `center` - An optional array `[f64; 2]` specifying the initial center point of the map ([latitude, longitude]).
33/// * `zoom` - An optional `u8` specifying the initial zoom level of the map.
34/// * `radius` - An optional `u8` specifying the radius of influence for each point.
35/// * `opacity` - An optional `f64` value between `0.0` and `1.0` specifying the opacity of the density layer.
36/// * `z_min` - An optional `f64` specifying the minimum value for the color scale.
37/// * `z_max` - An optional `f64` specifying the maximum value for the color scale.
38/// * `z_mid` - An optional `f64` specifying the midpoint value for the color scale.
39/// * `plot_title` - An optional `Text` struct specifying the title of the plot.
40/// * `legend_title` - An optional `Text` struct specifying the title of the legend.
41/// * `legend` - An optional reference to a `Legend` struct for customizing the legend.
42///
43/// # Example
44///
45/// ```rust
46/// use plotlars::{DensityMapbox, Plot, Text};
47/// use polars::prelude::*;
48///
49/// let data = LazyCsvReader::new(PlRefPath::new("data/us_city_density.csv"))
50/// .finish()
51/// .unwrap()
52/// .collect()
53/// .unwrap();
54///
55/// DensityMapbox::builder()
56/// .data(&data)
57/// .lat("city_lat")
58/// .lon("city_lon")
59/// .z("population_density")
60/// .center([39.8283, -98.5795])
61/// .zoom(3)
62/// .plot_title(
63/// Text::from("Density Mapbox")
64/// .font("Arial")
65/// .size(20)
66/// )
67/// .build()
68/// .plot();
69/// ```
70///
71/// 
72#[derive(Clone)]
73#[allow(dead_code)]
74pub struct DensityMapbox {
75 traces: Vec<TraceIR>,
76 layout: LayoutIR,
77}
78
79#[bon]
80impl DensityMapbox {
81 #[builder(on(String, into), on(Text, into))]
82 pub fn new(
83 data: &DataFrame,
84 lat: &str,
85 lon: &str,
86 z: &str,
87 center: Option<[f64; 2]>,
88 zoom: Option<u8>,
89 radius: Option<u8>,
90 opacity: Option<f64>,
91 z_min: Option<f64>,
92 z_max: Option<f64>,
93 z_mid: Option<f64>,
94 plot_title: Option<Text>,
95 legend_title: Option<Text>,
96 legend: Option<&Legend>,
97 ) -> Self {
98 let traces = vec![TraceIR::DensityMapbox(DensityMapboxIR {
99 lat: ColumnData::Numeric(crate::data::get_numeric_column(data, lat)),
100 lon: ColumnData::Numeric(crate::data::get_numeric_column(data, lon)),
101 z: ColumnData::Numeric(crate::data::get_numeric_column(data, z)),
102 radius,
103 opacity,
104 z_min,
105 z_max,
106 z_mid,
107 })];
108
109 let layout = LayoutIR {
110 title: plot_title,
111 x_title: None,
112 y_title: None,
113 y2_title: None,
114 z_title: None,
115 legend_title,
116 legend: legend.cloned(),
117 dimensions: None,
118 bar_mode: None,
119 box_mode: None,
120 box_gap: None,
121 margin_bottom: Some(0),
122 axes_2d: None,
123 scene_3d: None,
124 polar: None,
125 mapbox: Some(MapboxIR {
126 center: center.map(|c| (c[0], c[1])),
127 zoom: Some(zoom.map(|z| z as f64).unwrap_or(1.0)),
128 style: None,
129 }),
130 grid: None,
131 annotations: vec![],
132 };
133
134 Self { traces, layout }
135 }
136}
137
138#[bon]
139impl DensityMapbox {
140 #[builder(
141 start_fn = try_builder,
142 finish_fn = try_build,
143 builder_type = DensityMapboxTryBuilder,
144 on(String, into),
145 on(Text, into),
146 )]
147 pub fn try_new(
148 data: &DataFrame,
149 lat: &str,
150 lon: &str,
151 z: &str,
152 center: Option<[f64; 2]>,
153 zoom: Option<u8>,
154 radius: Option<u8>,
155 opacity: Option<f64>,
156 z_min: Option<f64>,
157 z_max: Option<f64>,
158 z_mid: Option<f64>,
159 plot_title: Option<Text>,
160 legend_title: Option<Text>,
161 legend: Option<&Legend>,
162 ) -> Result<Self, crate::io::PlotlarsError> {
163 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
164 Self::__orig_new(
165 data,
166 lat,
167 lon,
168 z,
169 center,
170 zoom,
171 radius,
172 opacity,
173 z_min,
174 z_max,
175 z_mid,
176 plot_title,
177 legend_title,
178 legend,
179 )
180 }))
181 .map_err(|panic| {
182 let msg = panic
183 .downcast_ref::<String>()
184 .cloned()
185 .or_else(|| panic.downcast_ref::<&str>().map(|s| s.to_string()))
186 .unwrap_or_else(|| "unknown error".to_string());
187 crate::io::PlotlarsError::PlotBuild { message: msg }
188 })
189 }
190}
191
192impl crate::Plot for DensityMapbox {
193 fn ir_traces(&self) -> &[TraceIR] {
194 &self.traces
195 }
196
197 fn ir_layout(&self) -> &LayoutIR {
198 &self.layout
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use crate::Plot;
206 use polars::prelude::*;
207
208 #[test]
209 fn test_basic_one_trace() {
210 let df = df![
211 "lat" => [40.7, 34.0, 41.8],
212 "lon" => [-74.0, -118.2, -87.6],
213 "density" => [100.0, 200.0, 150.0]
214 ]
215 .unwrap();
216 let plot = DensityMapbox::builder()
217 .data(&df)
218 .lat("lat")
219 .lon("lon")
220 .z("density")
221 .build();
222 assert_eq!(plot.ir_traces().len(), 1);
223 }
224
225 #[test]
226 fn test_trace_variant() {
227 let df = df![
228 "lat" => [40.7],
229 "lon" => [-74.0],
230 "density" => [100.0]
231 ]
232 .unwrap();
233 let plot = DensityMapbox::builder()
234 .data(&df)
235 .lat("lat")
236 .lon("lon")
237 .z("density")
238 .build();
239 assert!(matches!(plot.ir_traces()[0], TraceIR::DensityMapbox(_)));
240 }
241
242 #[test]
243 fn test_layout_has_mapbox() {
244 let df = df![
245 "lat" => [40.7],
246 "lon" => [-74.0],
247 "density" => [100.0]
248 ]
249 .unwrap();
250 let plot = DensityMapbox::builder()
251 .data(&df)
252 .lat("lat")
253 .lon("lon")
254 .z("density")
255 .build();
256 assert!(plot.ir_layout().mapbox.is_some());
257 }
258}