plotlars/plots/
density_mapbox.rs

1use bon::bon;
2
3use plotly::{
4    layout::{Center, Layout as LayoutPlotly, Mapbox, MapboxStyle, Margin},
5    DensityMapbox as DensityMapboxPlotly, Trace,
6};
7
8use polars::frame::DataFrame;
9use serde::Serialize;
10
11use crate::{
12    common::{Layout, PlotHelper, Polar},
13    components::{Legend, Text},
14};
15
16/// A structure representing a density mapbox visualization.
17///
18/// The `DensityMapbox` struct enables the creation of geographic density visualizations on an interactive map.
19/// It displays density or intensity values at geographic locations using latitude and longitude coordinates,
20/// with a third dimension (z) representing the intensity at each point. This is useful for visualizing
21/// population density, heat maps of activity, or any geographic concentration of values.
22///
23/// # Arguments
24///
25/// * `data` - A reference to the `DataFrame` containing the data to be plotted.
26/// * `lat` - A string slice specifying the column name containing latitude values.
27/// * `lon` - A string slice specifying the column name containing longitude values.
28/// * `z` - A string slice specifying the column name containing intensity/density values.
29/// * `center` - An optional array `[f64; 2]` specifying the initial center point of the map ([latitude, longitude]).
30/// * `zoom` - An optional `u8` specifying the initial zoom level of the map.
31/// * `radius` - An optional `u8` specifying the radius of influence for each point.
32/// * `opacity` - An optional `f64` value between `0.0` and `1.0` specifying the opacity of the density layer.
33/// * `z_min` - An optional `f64` specifying the minimum value for the color scale.
34/// * `z_max` - An optional `f64` specifying the maximum value for the color scale.
35/// * `z_mid` - An optional `f64` specifying the midpoint value for the color scale.
36/// * `plot_title` - An optional `Text` struct specifying the title of the plot.
37/// * `legend_title` - An optional `Text` struct specifying the title of the legend.
38/// * `legend` - An optional reference to a `Legend` struct for customizing the legend.
39///
40/// # Example
41///
42/// ```rust
43/// use plotlars::{DensityMapbox, Plot, Text};
44/// use polars::prelude::*;
45///
46/// let data = LazyCsvReader::new(PlPath::new("data/us_city_density.csv"))
47///     .finish()
48///     .unwrap()
49///     .collect()
50///     .unwrap();
51///
52/// DensityMapbox::builder()
53///     .data(&data)
54///     .lat("city_lat")
55///     .lon("city_lon")
56///     .z("population_density")
57///     .center([39.8283, -98.5795])
58///     .zoom(3)
59///     .plot_title(
60///         Text::from("Density Mapbox")
61///             .font("Arial")
62///             .size(20)
63///     )
64///     .build()
65///     .plot();
66/// ```
67///
68/// ![Example](https://imgur.com/82eLyBm.png)
69#[derive(Clone, Serialize)]
70pub struct DensityMapbox {
71    traces: Vec<Box<dyn Trace + 'static>>,
72    layout: LayoutPlotly,
73}
74
75#[bon]
76impl DensityMapbox {
77    #[builder(on(String, into), on(Text, into))]
78    pub fn new(
79        data: &DataFrame,
80        lat: &str,
81        lon: &str,
82        z: &str,
83        center: Option<[f64; 2]>,
84        zoom: Option<u8>,
85        radius: Option<u8>,
86        opacity: Option<f64>,
87        z_min: Option<f64>,
88        z_max: Option<f64>,
89        z_mid: Option<f64>,
90        plot_title: Option<Text>,
91        legend_title: Option<Text>,
92        legend: Option<&Legend>,
93    ) -> Self {
94        let x_title = None;
95        let y_title = None;
96        let z_title = None;
97        let x_axis = None;
98        let y_axis = None;
99        let z_axis = None;
100        let y2_title = None;
101        let y2_axis = None;
102
103        let mut layout = Self::create_layout(
104            plot_title,
105            x_title,
106            y_title,
107            y2_title,
108            z_title,
109            legend_title,
110            x_axis,
111            y_axis,
112            y2_axis,
113            z_axis,
114            legend,
115            None,
116        )
117        .margin(Margin::new().bottom(0));
118
119        let mut map_box = Mapbox::new().style(MapboxStyle::OpenStreetMap);
120
121        if let Some(center) = center {
122            map_box = map_box.center(Center::new(center[0], center[1]));
123        }
124
125        if let Some(zoom) = zoom {
126            map_box = map_box.zoom(zoom);
127        } else {
128            map_box = map_box.zoom(1);
129        }
130
131        layout = layout.mapbox(map_box);
132
133        let traces = Self::create_traces(data, lat, lon, z, radius, opacity, z_min, z_max, z_mid);
134
135        Self { traces, layout }
136    }
137
138    #[allow(clippy::too_many_arguments)]
139    fn create_traces(
140        data: &DataFrame,
141        lat: &str,
142        lon: &str,
143        z: &str,
144        radius: Option<u8>,
145        opacity: Option<f64>,
146        z_min: Option<f64>,
147        z_max: Option<f64>,
148        z_mid: Option<f64>,
149    ) -> Vec<Box<dyn Trace + 'static>> {
150        let mut traces: Vec<Box<dyn Trace + 'static>> = Vec::new();
151
152        let trace = Self::create_trace(data, lat, lon, z, radius, opacity, z_min, z_max, z_mid);
153
154        traces.push(trace);
155        traces
156    }
157
158    #[allow(clippy::too_many_arguments)]
159    fn create_trace(
160        data: &DataFrame,
161        lat: &str,
162        lon: &str,
163        z: &str,
164        radius: Option<u8>,
165        opacity: Option<f64>,
166        z_min: Option<f64>,
167        z_max: Option<f64>,
168        z_mid: Option<f64>,
169    ) -> Box<dyn Trace + 'static> {
170        let lat_data = Self::get_numeric_column(data, lat);
171        let lon_data = Self::get_numeric_column(data, lon);
172        let z_data = Self::get_numeric_column(data, z);
173
174        let mut trace = DensityMapboxPlotly::new(lat_data, lon_data, z_data);
175
176        if let Some(radius) = radius {
177            trace = trace.radius(radius);
178        }
179
180        if let Some(opacity) = opacity {
181            trace = trace.opacity(opacity);
182        }
183
184        if let Some(z_min) = z_min {
185            trace = trace.zmin(Some(z_min as f32));
186        }
187
188        if let Some(z_max) = z_max {
189            trace = trace.zmax(Some(z_max as f32));
190        }
191
192        if let Some(z_mid) = z_mid {
193            trace = trace.zmid(Some(z_mid as f32));
194        }
195
196        trace
197    }
198}
199
200impl Layout for DensityMapbox {}
201impl Polar for DensityMapbox {}
202
203impl PlotHelper for DensityMapbox {
204    fn get_layout(&self) -> &LayoutPlotly {
205        &self.layout
206    }
207
208    fn get_traces(&self) -> &Vec<Box<dyn Trace + 'static>> {
209        &self.traces
210    }
211}