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