Skip to main content

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/// ![Example](https://imgur.com/82eLyBm.png)
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}