plotlars/plots/
lineplot.rs

1use bon::bon;
2
3use plotly::{
4    common::{Line as LinePlotly, Marker as MarkerPlotly, Mode},
5    layout::{GridPattern, LayoutGrid},
6    Layout as LayoutPlotly, Scatter, Trace,
7};
8
9use polars::{
10    frame::DataFrame,
11    prelude::{col, IntoLazy},
12};
13use serde::Serialize;
14
15use crate::{
16    common::{Layout, Line, Marker, PlotHelper, Polar},
17    components::{
18        Axis, FacetConfig, FacetScales, Legend, Line as LineStyle, Rgb, Shape, Text,
19        DEFAULT_PLOTLY_COLORS,
20    },
21};
22
23/// A structure representing a line plot.
24///
25/// The `LinePlot` struct facilitates the creation and customization of line plots with various options
26/// for data selection, layout configuration, and aesthetic adjustments. It supports the addition of multiple
27/// lines, customization of marker shapes, line styles, colors, opacity settings, and comprehensive layout
28/// customization including titles, axes, and legends.
29///
30/// # Arguments
31///
32/// * `data` - A reference to the `DataFrame` containing the data to be plotted.
33/// * `x` - A string slice specifying the column name to be used for the x-axis (independent variable).
34/// * `y` - A string slice specifying the column name to be used for the y-axis (dependent variable).
35/// * `additional_lines` - An optional vector of string slices specifying additional y-axis columns to be plotted as lines.
36/// * `facet` - An optional string slice specifying the column name to be used for faceting (creating multiple subplots).
37/// * `facet_config` - An optional reference to a `FacetConfig` struct for customizing facet behavior (grid dimensions, scales, gaps, etc.).
38/// * `size` - An optional `usize` specifying the size of the markers or the thickness of the lines.
39/// * `color` - An optional `Rgb` value specifying the color of the markers and lines. This is used when `additional_lines` is not specified.
40/// * `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.
41/// * `with_shape` - An optional `bool` indicating whether to display markers with shapes on the plot.
42/// * `shape` - An optional `Shape` specifying the shape of the markers.
43/// * `shapes` - An optional vector of `Shape` values specifying multiple shapes for the markers when plotting multiple lines.
44/// * `width` - An optional `f64` specifying the width of the plotted lines.
45/// * `line` - An optional `Line` specifying the type of the line (e.g., solid, dashed). This is used when `additional_lines` is not specified.
46/// * `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.
47/// * `plot_title` - An optional `Text` struct specifying the title of the plot.
48/// * `x_title` - An optional `Text` struct specifying the title of the x-axis.
49/// * `y_title` - An optional `Text` struct specifying the title of the y-axis.
50/// * `y2_title` - An optional `Text` struct specifying the title of the secondary y-axis.
51/// * `legend_title` - An optional `Text` struct specifying the title of the legend.
52/// * `x_axis` - An optional reference to an `Axis` struct for customizing the x-axis.
53/// * `y_axis` - An optional reference to an `Axis` struct for customizing the y-axis.
54/// * `y2_axis` - An optional reference to an `Axis` struct for customizing the secondary y-axis.
55/// * `legend` - An optional reference to a `Legend` struct for customizing the legend of the plot (e.g., positioning, font, etc.).
56///
57/// # Example
58///
59/// ```rust
60/// use ndarray::Array;
61/// use plotlars::{Axis, Line, LinePlot, Plot, Rgb, Text, TickDirection};
62/// use polars::prelude::*;
63///
64///
65/// let x_values = Array::linspace(0.0, 2.0 * std::f64::consts::PI, 1000).to_vec();
66/// let sine_values = x_values.iter().map(|arg0: &f64| f64::sin(*arg0)).collect::<Vec<_>>();
67/// let cosine_values = x_values.iter().map(|arg0: &f64| f64::cos(*arg0)).collect::<Vec<_>>();
68///
69/// let dataset = df![
70///         "x" => &x_values,
71///         "sine" => &sine_values,
72///         "cosine" => &cosine_values,
73///     ]
74///     .unwrap();
75///
76/// LinePlot::builder()
77///     .data(&dataset)
78///     .x("x")
79///     .y("sine")
80///     .additional_lines(vec!["cosine"])
81///     .colors(vec![
82///         Rgb(255, 0, 0),
83///         Rgb(0, 255, 0),
84///     ])
85///     .lines(vec![
86///         Line::Solid,
87///         Line::Dot,
88///     ])
89///     .width(3.0)
90///     .with_shape(false)
91///     .plot_title(
92///         Text::from("Line Plot")
93///             .font("Arial")
94///             .size(18)
95///     )
96///     .legend_title(
97///         Text::from("series")
98///             .font("Arial")
99///             .size(15)
100///     )
101///     .x_axis(
102///        &Axis::new()
103///            .tick_direction(TickDirection::OutSide)
104///            .axis_position(0.5)
105///            .tick_values(vec![
106///                0.5 * std::f64::consts::PI,
107///                std::f64::consts::PI,
108///                1.5 * std::f64::consts::PI,
109///                2.0 * std::f64::consts::PI,
110///            ])
111///            .tick_labels(vec!["π/2", "π", "3π/2", "2π"])
112///     )
113///     .y_axis(
114///        &Axis::new()
115///            .tick_direction(TickDirection::OutSide)
116///            .tick_values(vec![-1.0, 0.0, 1.0])
117///            .tick_labels(vec!["-1", "0", "1"])
118///     )
119///     .build()
120///     .plot();
121/// ```
122///
123/// ![Example](https://imgur.com/PaXG300.png)
124#[derive(Clone, Serialize)]
125pub struct LinePlot {
126    traces: Vec<Box<dyn Trace + 'static>>,
127    layout: LayoutPlotly,
128}
129
130#[bon]
131impl LinePlot {
132    #[builder(on(String, into), on(Text, into))]
133    pub fn new(
134        data: &DataFrame,
135        x: &str,
136        y: &str,
137        additional_lines: Option<Vec<&str>>,
138        facet: Option<&str>,
139        facet_config: Option<&FacetConfig>,
140        size: Option<usize>,
141        color: Option<Rgb>,
142        colors: Option<Vec<Rgb>>,
143        with_shape: Option<bool>,
144        shape: Option<Shape>,
145        shapes: Option<Vec<Shape>>,
146        width: Option<f64>,
147        line: Option<LineStyle>,
148        lines: Option<Vec<LineStyle>>,
149        plot_title: Option<Text>,
150        x_title: Option<Text>,
151        y_title: Option<Text>,
152        y2_title: Option<Text>,
153        legend_title: Option<Text>,
154        x_axis: Option<&Axis>,
155        y_axis: Option<&Axis>,
156        y2_axis: Option<&Axis>,
157        legend: Option<&Legend>,
158    ) -> Self {
159        let z_title = None;
160        let z_axis = None;
161
162        let (layout, traces) = match facet {
163            Some(facet_column) => {
164                let config = facet_config.cloned().unwrap_or_default();
165
166                let layout = Self::create_faceted_layout(
167                    data,
168                    facet_column,
169                    &config,
170                    plot_title,
171                    x_title,
172                    y_title,
173                    legend_title,
174                    x_axis,
175                    y_axis,
176                    legend,
177                );
178
179                let traces = Self::create_faceted_traces(
180                    data,
181                    x,
182                    y,
183                    additional_lines,
184                    facet_column,
185                    &config,
186                    size,
187                    color,
188                    colors,
189                    with_shape,
190                    shape,
191                    shapes,
192                    width,
193                    line,
194                    lines,
195                );
196
197                (layout, traces)
198            }
199            None => {
200                let layout = Self::create_layout(
201                    plot_title,
202                    x_title,
203                    y_title,
204                    y2_title,
205                    z_title,
206                    legend_title,
207                    x_axis,
208                    y_axis,
209                    y2_axis,
210                    z_axis,
211                    legend,
212                    None,
213                );
214
215                let traces = Self::create_traces(
216                    data,
217                    x,
218                    y,
219                    additional_lines,
220                    size,
221                    color,
222                    colors,
223                    with_shape,
224                    shape,
225                    shapes,
226                    width,
227                    line,
228                    lines,
229                );
230
231                (layout, traces)
232            }
233        };
234
235        Self { traces, layout }
236    }
237
238    #[allow(clippy::too_many_arguments)]
239    fn create_traces(
240        data: &DataFrame,
241        x_col: &str,
242        y_col: &str,
243        additional_lines: Option<Vec<&str>>,
244        size: Option<usize>,
245        color: Option<Rgb>,
246        colors: Option<Vec<Rgb>>,
247        with_shape: Option<bool>,
248        shape: Option<Shape>,
249        shapes: Option<Vec<Shape>>,
250        width: Option<f64>,
251        style: Option<LineStyle>,
252        styles: Option<Vec<LineStyle>>,
253    ) -> Vec<Box<dyn Trace + 'static>> {
254        let mut traces: Vec<Box<dyn Trace + 'static>> = Vec::new();
255
256        let opacity = None;
257
258        let marker = Self::create_marker(
259            0,
260            opacity,
261            size,
262            color,
263            colors.clone(),
264            shape,
265            shapes.clone(),
266        );
267
268        let line = Self::create_line(0, width, style, styles.clone());
269
270        let name = Some(y_col);
271
272        let trace = Self::create_trace(data, x_col, y_col, name, with_shape, marker, line);
273
274        traces.push(trace);
275
276        if let Some(additional_lines) = additional_lines {
277            let additional_lines = additional_lines.into_iter();
278
279            for (i, series) in additional_lines.enumerate() {
280                let marker = Self::create_marker(
281                    i + 1,
282                    opacity,
283                    size,
284                    color,
285                    colors.clone(),
286                    shape,
287                    shapes.clone(),
288                );
289
290                let line = Self::create_line(i + 1, width, style, styles.clone());
291
292                let subset = data
293                    .clone()
294                    .lazy()
295                    .select([col(x_col), col(series)])
296                    .collect()
297                    .unwrap();
298
299                let name = Some(series);
300
301                let trace =
302                    Self::create_trace(&subset, x_col, series, name, with_shape, marker, line);
303
304                traces.push(trace);
305            }
306        }
307
308        traces
309    }
310
311    fn create_trace(
312        data: &DataFrame,
313        x_col: &str,
314        y_col: &str,
315        name: Option<&str>,
316        with_shape: Option<bool>,
317        marker: MarkerPlotly,
318        line: LinePlotly,
319    ) -> Box<dyn Trace + 'static> {
320        let x_data = Self::get_numeric_column(data, x_col);
321        let y_data = Self::get_numeric_column(data, y_col);
322
323        let mut trace = Scatter::default().x(x_data).y(y_data);
324
325        if let Some(with_shape) = with_shape {
326            if with_shape {
327                trace = trace.mode(Mode::LinesMarkers);
328            } else {
329                trace = trace.mode(Mode::Lines);
330            }
331        }
332
333        trace = trace.marker(marker);
334        trace = trace.line(line);
335
336        if let Some(name) = name {
337            trace = trace.name(name);
338        }
339
340        trace
341    }
342
343    #[allow(clippy::too_many_arguments)]
344    fn build_line_trace_with_axes(
345        data: &DataFrame,
346        x_col: &str,
347        y_col: &str,
348        name: Option<&str>,
349        with_shape: Option<bool>,
350        marker: MarkerPlotly,
351        line: LinePlotly,
352        x_axis: Option<&str>,
353        y_axis: Option<&str>,
354        show_legend: bool,
355        legend_group: Option<&str>,
356    ) -> Box<dyn Trace + 'static> {
357        let x_data = Self::get_numeric_column(data, x_col);
358        let y_data = Self::get_numeric_column(data, y_col);
359
360        let mut trace = Scatter::default().x(x_data).y(y_data);
361
362        if let Some(with_shape) = with_shape {
363            if with_shape {
364                trace = trace.mode(Mode::LinesMarkers);
365            } else {
366                trace = trace.mode(Mode::Lines);
367            }
368        } else {
369            trace = trace.mode(Mode::Lines);
370        }
371
372        trace = trace.marker(marker);
373        trace = trace.line(line);
374
375        if let Some(name) = name {
376            trace = trace.name(name);
377        }
378
379        if let Some(axis) = x_axis {
380            trace = trace.x_axis(axis);
381        }
382
383        if let Some(axis) = y_axis {
384            trace = trace.y_axis(axis);
385        }
386
387        if let Some(group) = legend_group {
388            trace = trace.legend_group(group);
389        }
390
391        if !show_legend {
392            trace.show_legend(false)
393        } else {
394            trace
395        }
396    }
397
398    #[allow(clippy::too_many_arguments)]
399    fn create_faceted_traces(
400        data: &DataFrame,
401        x: &str,
402        y: &str,
403        additional_lines: Option<Vec<&str>>,
404        facet_column: &str,
405        config: &FacetConfig,
406        size: Option<usize>,
407        color: Option<Rgb>,
408        colors: Option<Vec<Rgb>>,
409        with_shape: Option<bool>,
410        shape: Option<Shape>,
411        shapes: Option<Vec<Shape>>,
412        width: Option<f64>,
413        style: Option<LineStyle>,
414        styles: Option<Vec<LineStyle>>,
415    ) -> Vec<Box<dyn Trace + 'static>> {
416        const MAX_FACETS: usize = 8;
417
418        let facet_categories = Self::get_unique_groups(data, facet_column, config.sorter);
419
420        if facet_categories.len() > MAX_FACETS {
421            panic!(
422                "Facet column '{}' has {} unique values, but plotly.rs supports maximum {} subplots",
423                facet_column,
424                facet_categories.len(),
425                MAX_FACETS
426            );
427        }
428
429        let all_y_cols = if let Some(ref add_lines) = additional_lines {
430            let mut cols = vec![y];
431            cols.extend(add_lines.iter().copied());
432            cols
433        } else {
434            vec![y]
435        };
436
437        if let Some(ref color_vec) = colors {
438            if additional_lines.is_none() {
439                let color_count = color_vec.len();
440                let facet_count = facet_categories.len();
441
442                if color_count != facet_count {
443                    panic!(
444                        "When using colors with facet (without additional_lines), colors.len() must equal number of facets. \
445                         Expected {} colors for {} facets, but got {} colors. \
446                         Each facet must be assigned exactly one color.",
447                        facet_count, facet_count, color_count
448                    );
449                }
450            } else {
451                let color_count = color_vec.len();
452                let line_count = all_y_cols.len();
453
454                if color_count < line_count {
455                    panic!(
456                        "When using colors with additional_lines, colors.len() must be >= number of lines. \
457                         Need at least {} colors for {} lines, but got {} colors",
458                        line_count, line_count, color_count
459                    );
460                }
461            }
462        }
463
464        let colors = if additional_lines.is_some() && colors.is_none() {
465            Some(DEFAULT_PLOTLY_COLORS.to_vec())
466        } else {
467            colors
468        };
469
470        let mut all_traces = Vec::new();
471        let opacity = None;
472
473        if config.highlight_facet {
474            for (facet_idx, facet_value) in facet_categories.iter().enumerate() {
475                let x_axis = Self::get_axis_reference(facet_idx, "x");
476                let y_axis = Self::get_axis_reference(facet_idx, "y");
477
478                for other_facet_value in facet_categories.iter() {
479                    if other_facet_value != facet_value {
480                        let other_data =
481                            Self::filter_data_by_group(data, facet_column, other_facet_value);
482
483                        let grey_color = config.unhighlighted_color.unwrap_or(Rgb(200, 200, 200));
484
485                        for (line_idx, y_col) in all_y_cols.iter().enumerate() {
486                            let grey_marker = Self::create_marker(
487                                line_idx,
488                                opacity,
489                                size,
490                                Some(grey_color),
491                                None,
492                                shape,
493                                None,
494                            );
495
496                            let grey_line =
497                                Self::create_line(line_idx, width, style, styles.clone());
498
499                            let trace = Self::build_line_trace_with_axes(
500                                &other_data,
501                                x,
502                                y_col,
503                                None,
504                                with_shape,
505                                grey_marker,
506                                grey_line,
507                                Some(&x_axis),
508                                Some(&y_axis),
509                                false,
510                                Some(y_col),
511                            );
512
513                            all_traces.push(trace);
514                        }
515                    }
516                }
517
518                let facet_data = Self::filter_data_by_group(data, facet_column, facet_value);
519
520                for (line_idx, y_col) in all_y_cols.iter().enumerate() {
521                    let color_index = if additional_lines.is_none() {
522                        facet_idx
523                    } else {
524                        line_idx
525                    };
526
527                    let marker = Self::create_marker(
528                        color_index,
529                        opacity,
530                        size,
531                        color,
532                        colors.clone(),
533                        shape,
534                        shapes.clone(),
535                    );
536
537                    let line = Self::create_line(line_idx, width, style, styles.clone());
538
539                    let show_legend = facet_idx == 0;
540                    let name = if show_legend { Some(*y_col) } else { None };
541
542                    let trace = Self::build_line_trace_with_axes(
543                        &facet_data,
544                        x,
545                        y_col,
546                        name,
547                        with_shape,
548                        marker,
549                        line,
550                        Some(&x_axis),
551                        Some(&y_axis),
552                        show_legend,
553                        Some(y_col),
554                    );
555
556                    all_traces.push(trace);
557                }
558            }
559        } else {
560            for (facet_idx, facet_value) in facet_categories.iter().enumerate() {
561                let facet_data = Self::filter_data_by_group(data, facet_column, facet_value);
562
563                let x_axis = Self::get_axis_reference(facet_idx, "x");
564                let y_axis = Self::get_axis_reference(facet_idx, "y");
565
566                for (line_idx, y_col) in all_y_cols.iter().enumerate() {
567                    let color_index = if additional_lines.is_none() {
568                        facet_idx
569                    } else {
570                        line_idx
571                    };
572
573                    let marker = Self::create_marker(
574                        color_index,
575                        opacity,
576                        size,
577                        color,
578                        colors.clone(),
579                        shape,
580                        shapes.clone(),
581                    );
582
583                    let line = Self::create_line(line_idx, width, style, styles.clone());
584
585                    let show_legend = facet_idx == 0;
586                    let name = if show_legend { Some(*y_col) } else { None };
587
588                    let trace = Self::build_line_trace_with_axes(
589                        &facet_data,
590                        x,
591                        y_col,
592                        name,
593                        with_shape,
594                        marker,
595                        line,
596                        Some(&x_axis),
597                        Some(&y_axis),
598                        show_legend,
599                        Some(y_col),
600                    );
601
602                    all_traces.push(trace);
603                }
604            }
605        }
606
607        all_traces
608    }
609
610    #[allow(clippy::too_many_arguments)]
611    fn create_faceted_layout(
612        data: &DataFrame,
613        facet_column: &str,
614        config: &FacetConfig,
615        plot_title: Option<Text>,
616        x_title: Option<Text>,
617        y_title: Option<Text>,
618        legend_title: Option<Text>,
619        x_axis: Option<&Axis>,
620        y_axis: Option<&Axis>,
621        legend: Option<&Legend>,
622    ) -> LayoutPlotly {
623        let facet_categories = Self::get_unique_groups(data, facet_column, config.sorter);
624        let n_facets = facet_categories.len();
625
626        let (ncols, nrows) = Self::calculate_grid_dimensions(n_facets, config.cols, config.rows);
627
628        let mut grid = LayoutGrid::new()
629            .rows(nrows)
630            .columns(ncols)
631            .pattern(GridPattern::Independent);
632
633        if let Some(x_gap) = config.h_gap {
634            grid = grid.x_gap(x_gap);
635        }
636        if let Some(y_gap) = config.v_gap {
637            grid = grid.y_gap(y_gap);
638        }
639
640        let mut layout = LayoutPlotly::new().grid(grid);
641
642        if let Some(title) = plot_title {
643            layout = layout.title(title.to_plotly());
644        }
645
646        layout = Self::apply_axis_matching(layout, n_facets, &config.scales);
647
648        layout = Self::apply_facet_axis_titles(
649            layout, n_facets, ncols, nrows, x_title, y_title, x_axis, y_axis,
650        );
651
652        let annotations =
653            Self::create_facet_annotations(&facet_categories, config.title_style.as_ref());
654        layout = layout.annotations(annotations);
655
656        layout = layout.legend(Legend::set_legend(legend_title, legend));
657
658        layout
659    }
660
661    fn apply_axis_matching(
662        mut layout: LayoutPlotly,
663        n_facets: usize,
664        scales: &FacetScales,
665    ) -> LayoutPlotly {
666        use plotly::layout::Axis as AxisPlotly;
667
668        match scales {
669            FacetScales::Fixed => {
670                for i in 1..n_facets {
671                    let x_axis = AxisPlotly::new().matches("x");
672                    let y_axis = AxisPlotly::new().matches("y");
673                    layout = match i {
674                        1 => layout.x_axis2(x_axis).y_axis2(y_axis),
675                        2 => layout.x_axis3(x_axis).y_axis3(y_axis),
676                        3 => layout.x_axis4(x_axis).y_axis4(y_axis),
677                        4 => layout.x_axis5(x_axis).y_axis5(y_axis),
678                        5 => layout.x_axis6(x_axis).y_axis6(y_axis),
679                        6 => layout.x_axis7(x_axis).y_axis7(y_axis),
680                        7 => layout.x_axis8(x_axis).y_axis8(y_axis),
681                        _ => layout,
682                    };
683                }
684            }
685            FacetScales::FreeX => {
686                for i in 1..n_facets {
687                    let axis = AxisPlotly::new().matches("y");
688                    layout = match i {
689                        1 => layout.y_axis2(axis),
690                        2 => layout.y_axis3(axis),
691                        3 => layout.y_axis4(axis),
692                        4 => layout.y_axis5(axis),
693                        5 => layout.y_axis6(axis),
694                        6 => layout.y_axis7(axis),
695                        7 => layout.y_axis8(axis),
696                        _ => layout,
697                    };
698                }
699            }
700            FacetScales::FreeY => {
701                for i in 1..n_facets {
702                    let axis = AxisPlotly::new().matches("x");
703                    layout = match i {
704                        1 => layout.x_axis2(axis),
705                        2 => layout.x_axis3(axis),
706                        3 => layout.x_axis4(axis),
707                        4 => layout.x_axis5(axis),
708                        5 => layout.x_axis6(axis),
709                        6 => layout.x_axis7(axis),
710                        7 => layout.x_axis8(axis),
711                        _ => layout,
712                    };
713                }
714            }
715            FacetScales::Free => {}
716        }
717
718        layout
719    }
720
721    #[allow(clippy::too_many_arguments)]
722    fn apply_facet_axis_titles(
723        mut layout: LayoutPlotly,
724        n_facets: usize,
725        ncols: usize,
726        nrows: usize,
727        x_title: Option<Text>,
728        y_title: Option<Text>,
729        x_axis_config: Option<&Axis>,
730        y_axis_config: Option<&Axis>,
731    ) -> LayoutPlotly {
732        for i in 0..n_facets {
733            let is_bottom = Self::is_bottom_row(i, ncols, nrows, n_facets);
734            let is_left = Self::is_left_column(i, ncols);
735
736            let x_title_for_subplot = if is_bottom { x_title.clone() } else { None };
737            let y_title_for_subplot = if is_left { y_title.clone() } else { None };
738
739            if x_title_for_subplot.is_some() || x_axis_config.is_some() {
740                let axis = match x_axis_config {
741                    Some(config) => Axis::set_axis(x_title_for_subplot, config, None),
742                    None => {
743                        if let Some(title) = x_title_for_subplot {
744                            Axis::set_axis(Some(title), &Axis::default(), None)
745                        } else {
746                            continue;
747                        }
748                    }
749                };
750
751                layout = match i {
752                    0 => layout.x_axis(axis),
753                    1 => layout.x_axis2(axis),
754                    2 => layout.x_axis3(axis),
755                    3 => layout.x_axis4(axis),
756                    4 => layout.x_axis5(axis),
757                    5 => layout.x_axis6(axis),
758                    6 => layout.x_axis7(axis),
759                    7 => layout.x_axis8(axis),
760                    _ => layout,
761                };
762            }
763
764            if y_title_for_subplot.is_some() || y_axis_config.is_some() {
765                let axis = match y_axis_config {
766                    Some(config) => Axis::set_axis(y_title_for_subplot, config, None),
767                    None => {
768                        if let Some(title) = y_title_for_subplot {
769                            Axis::set_axis(Some(title), &Axis::default(), None)
770                        } else {
771                            continue;
772                        }
773                    }
774                };
775
776                layout = match i {
777                    0 => layout.y_axis(axis),
778                    1 => layout.y_axis2(axis),
779                    2 => layout.y_axis3(axis),
780                    3 => layout.y_axis4(axis),
781                    4 => layout.y_axis5(axis),
782                    5 => layout.y_axis6(axis),
783                    6 => layout.y_axis7(axis),
784                    7 => layout.y_axis8(axis),
785                    _ => layout,
786                };
787            }
788        }
789
790        layout
791    }
792}
793
794impl Layout for LinePlot {}
795impl Line for LinePlot {}
796impl Marker for LinePlot {}
797impl Polar for LinePlot {}
798
799impl PlotHelper for LinePlot {
800    fn get_layout(&self) -> &LayoutPlotly {
801        &self.layout
802    }
803
804    fn get_traces(&self) -> &Vec<Box<dyn Trace + 'static>> {
805        &self.traces
806    }
807}