plotlars/plots/lineplot.rs
1use bon::bon;
2
3use plotly::{
4 common::{Line as LinePlotly, Marker as MarkerPlotly, Mode},
5 Layout as LayoutPlotly, Scatter, Trace,
6};
7
8use polars::{
9 frame::DataFrame,
10 prelude::{col, IntoLazy},
11};
12use serde::Serialize;
13
14use crate::{
15 common::{Layout, Line, Marker, PlotHelper, Polar},
16 components::{Axis, Legend, Line as LineStyle, Rgb, Shape, Text},
17};
18
19/// A structure representing a line plot.
20///
21/// The `LinePlot` struct facilitates the creation and customization of line plots with various options
22/// for data selection, layout configuration, and aesthetic adjustments. It supports the addition of multiple
23/// lines, customization of marker shapes, line styles, colors, opacity settings, and comprehensive layout
24/// customization including titles, axes, and legends.
25///
26/// # Arguments
27///
28/// * `data` - A reference to the `DataFrame` containing the data to be plotted.
29/// * `x` - A string slice specifying the column name to be used for the x-axis (independent variable).
30/// * `y` - A string slice specifying the column name to be used for the y-axis (dependent variable).
31/// * `additional_lines` - An optional vector of string slices specifying additional y-axis columns to be plotted as lines.
32/// * `size` - An optional `usize` specifying the size of the markers or the thickness of the lines.
33/// * `color` - An optional `Rgb` value specifying the color of the markers and lines. This is used when `additional_lines` is not specified.
34/// * `colors` - An optional vector of `Rgb` values specifying the colors for the markers and lines. This is used when `additional_lines` is specified to differentiate between multiple lines.
35/// * `with_shape` - An optional `bool` indicating whether to display markers with shapes on the plot.
36/// * `shape` - An optional `Shape` specifying the shape of the markers.
37/// * `shapes` - An optional vector of `Shape` values specifying multiple shapes for the markers when plotting multiple lines.
38/// * `width` - An optional `f64` specifying the width of the plotted lines.
39/// * `line` - An optional `Line` specifying the type of the line (e.g., solid, dashed). This is used when `additional_lines` is not specified.
40/// * `lines` - An optional vector of `Line` enums specifying the types of lines (e.g., solid, dashed) for each plotted line. This is used when `additional_lines` is specified to differentiate between multiple lines.
41/// * `plot_title` - An optional `Text` struct specifying the title of the plot.
42/// * `x_title` - An optional `Text` struct specifying the title of the x-axis.
43/// * `y_title` - An optional `Text` struct specifying the title of the y-axis.
44/// * `legend_title` - An optional `Text` struct specifying the title of the legend.
45/// * `x_axis` - An optional reference to an `Axis` struct for customizing the x-axis.
46/// * `y_axis` - An optional reference to an `Axis` struct for customizing the y-axis.
47/// * `legend` - An optional reference to a `Legend` struct for customizing the legend of the plot (e.g., positioning, font, etc.).
48///
49/// # Example
50///
51/// ```rust
52/// use ndarray::Array;
53///
54/// use polars::prelude::*;
55/// use plotlars::{Axis, Line, LinePlot, Plot, Rgb, Text, TickDirection};
56///
57/// let x_values: Array<f64, _> = Array::linspace(0.0, 2.0 * std::f64::consts::PI, 1000);
58/// let sine_values = x_values.mapv(f64::sin).to_vec();
59/// let cosine_values = x_values.mapv(f64::cos).to_vec();
60/// let x_values = x_values.to_vec();
61///
62/// let dataset = DataFrame::new(vec![
63/// Column::new("x".into(), x_values),
64/// Column::new("sine".into(), sine_values),
65/// Column::new("cosine".into(), cosine_values),
66/// ])
67/// .unwrap();
68///
69/// LinePlot::builder()
70/// .data(&dataset)
71/// .x("x")
72/// .y("sine")
73/// .additional_lines(vec!["cosine"])
74/// .colors(vec![
75/// Rgb(255, 0, 0),
76/// Rgb(0, 255, 0),
77/// ])
78/// .lines(vec![Line::Solid, Line::Dot])
79/// .width(3.0)
80/// .with_shape(false)
81/// .plot_title(
82/// Text::from("Line Plot")
83/// .font("Arial")
84/// .size(18)
85/// )
86/// .legend_title(
87/// Text::from("series")
88/// .font("Arial")
89/// .size(15)
90/// )
91/// .x_axis(
92/// &Axis::new()
93/// .tick_direction(TickDirection::OutSide)
94/// .axis_position(0.5)
95/// .tick_values(vec![
96/// 0.5 * std::f64::consts::PI,
97/// std::f64::consts::PI,
98/// 1.5 * std::f64::consts::PI,
99/// 2.0 * std::f64::consts::PI,
100/// ])
101/// .tick_labels(vec!["π/2", "π", "3π/2", "2π"])
102/// )
103/// .y_axis(
104/// &Axis::new()
105/// .tick_direction(TickDirection::OutSide)
106/// .tick_values(vec![-1.0, 0.0, 1.0])
107/// .tick_labels(vec!["-1", "0", "1"])
108/// )
109/// .build()
110/// .plot();
111/// ```
112///
113/// 
114#[derive(Clone, Serialize)]
115pub struct LinePlot {
116 traces: Vec<Box<dyn Trace + 'static>>,
117 layout: LayoutPlotly,
118}
119
120#[bon]
121impl LinePlot {
122 #[builder(on(String, into), on(Text, into))]
123 pub fn new(
124 data: &DataFrame,
125 x: &str,
126 y: &str,
127 additional_lines: Option<Vec<&str>>,
128 size: Option<usize>,
129 color: Option<Rgb>,
130 colors: Option<Vec<Rgb>>,
131 with_shape: Option<bool>,
132 shape: Option<Shape>,
133 shapes: Option<Vec<Shape>>,
134 width: Option<f64>,
135 line: Option<LineStyle>,
136 lines: Option<Vec<LineStyle>>,
137 plot_title: Option<Text>,
138 x_title: Option<Text>,
139 y_title: Option<Text>,
140 y2_title: Option<Text>,
141 legend_title: Option<Text>,
142 x_axis: Option<&Axis>,
143 y_axis: Option<&Axis>,
144 y2_axis: Option<&Axis>,
145 legend: Option<&Legend>,
146 ) -> Self {
147 let z_title = None;
148 let z_axis = None;
149
150 let layout = Self::create_layout(
151 plot_title,
152 x_title,
153 y_title,
154 y2_title,
155 z_title,
156 legend_title,
157 x_axis,
158 y_axis,
159 y2_axis,
160 z_axis,
161 legend,
162 );
163
164 let traces = Self::create_traces(
165 data,
166 x,
167 y,
168 additional_lines,
169 size,
170 color,
171 colors,
172 with_shape,
173 shape,
174 shapes,
175 width,
176 line,
177 lines,
178 );
179
180 Self { traces, layout }
181 }
182
183 #[allow(clippy::too_many_arguments)]
184 fn create_traces(
185 data: &DataFrame,
186 x_col: &str,
187 y_col: &str,
188 additional_lines: Option<Vec<&str>>,
189 size: Option<usize>,
190 color: Option<Rgb>,
191 colors: Option<Vec<Rgb>>,
192 with_shape: Option<bool>,
193 shape: Option<Shape>,
194 shapes: Option<Vec<Shape>>,
195 width: Option<f64>,
196 style: Option<LineStyle>,
197 styles: Option<Vec<LineStyle>>,
198 ) -> Vec<Box<dyn Trace + 'static>> {
199 let mut traces: Vec<Box<dyn Trace + 'static>> = Vec::new();
200
201 let opacity = None;
202
203 let marker = Self::create_marker(
204 0,
205 opacity,
206 size,
207 color,
208 colors.clone(),
209 shape,
210 shapes.clone(),
211 );
212
213 let line = Self::create_line(0, width, style, styles.clone());
214
215 let name = Some(y_col);
216
217 let trace = Self::create_trace(data, x_col, y_col, name, with_shape, marker, line);
218
219 traces.push(trace);
220
221 if let Some(additional_lines) = additional_lines {
222 let additional_lines = additional_lines.into_iter();
223
224 for (i, series) in additional_lines.enumerate() {
225 let marker = Self::create_marker(
226 i + 1,
227 opacity,
228 size,
229 color,
230 colors.clone(),
231 shape,
232 shapes.clone(),
233 );
234
235 let line = Self::create_line(i + 1, width, style, styles.clone());
236
237 let subset = data
238 .clone()
239 .lazy()
240 .select([col(x_col), col(series)])
241 .collect()
242 .unwrap();
243
244 let name = Some(series);
245
246 let trace =
247 Self::create_trace(&subset, x_col, series, name, with_shape, marker, line);
248
249 traces.push(trace);
250 }
251 }
252
253 traces
254 }
255
256 fn create_trace(
257 data: &DataFrame,
258 x_col: &str,
259 y_col: &str,
260 name: Option<&str>,
261 with_shape: Option<bool>,
262 marker: MarkerPlotly,
263 line: LinePlotly,
264 ) -> Box<dyn Trace + 'static> {
265 let x_data = Self::get_numeric_column(data, x_col);
266 let y_data = Self::get_numeric_column(data, y_col);
267
268 let mut trace = Scatter::default().x(x_data).y(y_data);
269
270 if let Some(with_shape) = with_shape {
271 if with_shape {
272 trace = trace.mode(Mode::LinesMarkers);
273 } else {
274 trace = trace.mode(Mode::Lines);
275 }
276 }
277
278 trace = trace.marker(marker);
279 trace = trace.line(line);
280
281 if let Some(name) = name {
282 trace = trace.name(name);
283 }
284
285 trace
286 }
287}
288
289impl Layout for LinePlot {}
290impl Line for LinePlot {}
291impl Marker for LinePlot {}
292impl Polar for LinePlot {}
293
294impl PlotHelper for LinePlot {
295 fn get_layout(&self) -> &LayoutPlotly {
296 &self.layout
297 }
298
299 fn get_traces(&self) -> &Vec<Box<dyn Trace + 'static>> {
300 &self.traces
301 }
302}