From 86056c6fa2e8a868b115bcd4111cf843283ed629 Mon Sep 17 00:00:00 2001 From: alceal Date: Sat, 26 Oct 2024 09:55:08 +0200 Subject: [PATCH] refactor: A deep refactoring --- Cargo.toml | 1 - README.md | 9 +- data/revenue_and_cost.csv | 13 + data/sales.csv | 65 ---- src/aesthetics/line.rs | 73 ---- src/aesthetics/mark.rs | 67 ---- src/aesthetics/mod.rs | 3 - src/aesthetics/orientation.rs | 22 -- src/colors/mod.rs | 15 - src/common/layout.rs | 212 +++++++++++ src/common/line.rs | 45 +++ src/common/mark.rs | 80 ++++ src/common/mod.rs | 11 + src/{traits => common}/plot.rs | 0 src/{traits => common}/polar.rs | 0 src/{axis/mod.rs => components/axis.rs} | 382 ++++++++++---------- src/components/color.rs | 55 +++ src/components/exponent.rs | 55 +++ src/{legend/mod.rs => components/legend.rs} | 78 ++-- src/components/line.rs | 60 +++ src/components/mod.rs | 17 + src/components/orientation.rs | 62 ++++ src/{shapes/mod.rs => components/shape.rs} | 71 +++- src/components/text.rs | 160 ++++++++ src/lib.rs | 44 +-- src/macros/marker.rs | 11 - src/macros/mod.rs | 2 - src/plots/barplot.rs | 309 ++++++++++++++++ src/plots/boxplot.rs | 346 ++++++++++++++++++ src/plots/histogram.rs | 233 ++++++++++++ src/plots/lineplot.rs | 290 +++++++++++++++ src/{traces => plots}/mod.rs | 0 src/plots/scatterplot.rs | 248 +++++++++++++ src/plots/timeseriesplot.rs | 270 ++++++++++++++ src/texts/mod.rs | 154 -------- src/traces/barplot.rs | 279 -------------- src/traces/boxplot.rs | 313 ---------------- src/traces/histogram.rs | 220 ----------- src/traces/lineplot.rs | 326 ----------------- src/traces/scatterplot.rs | 214 ----------- src/traces/timeseriesplot.rs | 322 ----------------- src/traits/layout.rs | 227 ------------ src/traits/mod.rs | 4 - src/traits/trace.rs | 120 ------ 44 files changed, 2777 insertions(+), 2711 deletions(-) create mode 100644 data/revenue_and_cost.csv delete mode 100644 data/sales.csv delete mode 100644 src/aesthetics/line.rs delete mode 100644 src/aesthetics/mark.rs delete mode 100644 src/aesthetics/mod.rs delete mode 100644 src/aesthetics/orientation.rs delete mode 100644 src/colors/mod.rs create mode 100644 src/common/layout.rs create mode 100644 src/common/line.rs create mode 100644 src/common/mark.rs create mode 100644 src/common/mod.rs rename src/{traits => common}/plot.rs (100%) rename src/{traits => common}/polar.rs (100%) rename src/{axis/mod.rs => components/axis.rs} (66%) create mode 100644 src/components/color.rs create mode 100644 src/components/exponent.rs rename src/{legend/mod.rs => components/legend.rs} (67%) create mode 100644 src/components/line.rs create mode 100644 src/components/mod.rs create mode 100644 src/components/orientation.rs rename src/{shapes/mod.rs => components/shape.rs} (86%) create mode 100644 src/components/text.rs delete mode 100644 src/macros/marker.rs delete mode 100644 src/macros/mod.rs create mode 100644 src/plots/barplot.rs create mode 100644 src/plots/boxplot.rs create mode 100644 src/plots/histogram.rs create mode 100644 src/plots/lineplot.rs rename src/{traces => plots}/mod.rs (100%) create mode 100644 src/plots/scatterplot.rs create mode 100644 src/plots/timeseriesplot.rs delete mode 100644 src/texts/mod.rs delete mode 100644 src/traces/barplot.rs delete mode 100644 src/traces/boxplot.rs delete mode 100644 src/traces/histogram.rs delete mode 100644 src/traces/lineplot.rs delete mode 100644 src/traces/scatterplot.rs delete mode 100644 src/traces/timeseriesplot.rs delete mode 100644 src/traits/layout.rs delete mode 100644 src/traits/mod.rs delete mode 100644 src/traits/trace.rs diff --git a/Cargo.toml b/Cargo.toml index 0042c96..09a5b46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,6 @@ categories = ["visualization"] bon = "2.3.0" plotly = "0.10.0" polars = { version = "0.43.1", features = [ - # "dtype-categorical", "lazy", "strings", ] } diff --git a/README.md b/README.md index 138e668..f22aa95 100644 --- a/README.md +++ b/README.md @@ -140,8 +140,13 @@ fn main() { .x("body_mass_g") .y("flipper_length_mm") .group("species") - .size(10) .opacity(0.5) + .size(12) + .colors(vec![ + Rgb(178, 34, 34), + Rgb(65, 105, 225), + Rgb(255, 140, 0), + ]) .plot_title("Penguin Flipper Length vs Body Mass") .x_title("Body Mass (g)") .y_title("Flipper Length (mm)") @@ -153,7 +158,7 @@ fn main() { This is the output: -![Plot example](https://imgur.com/PkQ9fsc.png) +![Plot example](https://imgur.com/QMkmhNh.png) With Plotlars, the same scatter plot is created with significantly less code. The library abstracts away the complexities of dealing with individual plot diff --git a/data/revenue_and_cost.csv b/data/revenue_and_cost.csv new file mode 100644 index 0000000..69a4da6 --- /dev/null +++ b/data/revenue_and_cost.csv @@ -0,0 +1,13 @@ +Date,Revenue,Cost +2023-01-31,10320.183877729267,8251.593217832866 +2023-02-28,23361.49949229147,15876.260386054937 +2023-03-31,33853.53981955318,17737.523093159074 +2023-04-30,47114.92739147375,34797.129368772985 +2023-05-31,61960.96581901887,48052.000872334065 +2023-06-30,73843.24767793017,49471.418315826566 +2023-07-31,85638.49495007776,48950.09739941198 +2023-08-31,93548.36563617105,64578.173115080426 +2023-09-30,104221.93731957133,77044.56755616776 +2023-10-31,117776.12613238876,65033.11155742126 +2023-11-30,128275.85434203708,69543.76388145327 +2023-12-31,140358.10583941432,103142.19972170686 diff --git a/data/sales.csv b/data/sales.csv deleted file mode 100644 index 4ac752b..0000000 --- a/data/sales.csv +++ /dev/null @@ -1,65 +0,0 @@ -Period,Revenue,Sales_quantity,Average_cost,The_average_annual_payroll_of_the_region -01.01.2015,16010072.1195,12729,1257.76354148008,30024676 -01.02.2015,15807587.449808,11636,1358.50699981162,30024676 -01.03.2015,22047146.023644,15922,1384.69702447205,30024676 -01.04.2015,18814583.29428,15227,1235.60670481907,30024676 -01.05.2015,14021479.611678,8620,1626.62176469582,30024676 -01.06.2015,16783928.522112,13160,1275.37450775927,30024676 -01.07.2015,19161892.194872,17254,1110.57680508126,30024676 -01.08.2015,15204984.296742,8642,1759.4288702548,30024676 -01.09.2015,20603939.9751,16144,1276.25990926041,30024676 -01.10.2015,20992874.780136,18135,1157.58890433615,30024676 -01.11.2015,14993369.65763,10841,1383.02459714325,30024676 -01.12.2015,27791807.639848,22113,1256.80855785502,30024676 -01.01.2016,28601586.496,15365,1861.4765047836,27828571 -01.02.2016,22367074.065584,13153,1700.53022622854,27828571 -01.03.2016,29738608.568,18339,1621.60469862043,27828571 -01.04.2016,28351007.9388,13909,2038.3210826659,27828571 -01.05.2016,15264603.734865,8553,1784.70755698176,27828571 -01.06.2016,24385658.077056,15101,1614.83730064605,27828571 -01.07.2016,29486517.069955,15695,1878.72042497324,27828571 -01.08.2016,15270117.2565,8314,1836.67515714457,27828571 -01.09.2016,36141027.562,17764,2034.50954526008,27828571 -01.10.2016,27915143.655,18969,1471.61914992883,27828571 -01.11.2016,21272049.3454,13433,1583.56654101094,27828571 -01.12.2016,42014159.88396,27029,1554.41044374413,27828571 -01.01.2017,36007380.67,16889,2132.00193439517,27406473 -01.02.2017,30396775.3784,15864,1916.08518522441,27406473 -01.03.2017,47678130.72603,22786,2092.43091047266,27406473 -01.04.2017,27013964.728324,17910,1508.31740526655,27406473 -01.05.2017,24948844.698,10777,2315.00832309548,27406473 -01.06.2017,31101345.543,18799,1654.4148913772,27406473 -01.07.2017,33848822.228544,17899,1891.10130334343,27406473 -01.08.2017,16454666.958,9649,1705.32355249249,27406473 -01.09.2017,31650092.652,20159,1570.02295014634,27406473 -01.10.2017,31572205.6224,19519,1617.51143103643,27406473 -01.11.2017,22446371.0268,15360,1461.35228039063,27406473 -01.12.2017,44966125.7696,30833,1458.37660200435,27406473 -01.01.2018,44067520.858,19812,2224.28431546538,28197847 -01.02.2018,36020287.1553,18424,1955.07420512918,28197847 -01.03.2018,46995990.4125,29004,1620.32790003103,28197847 -01.04.2018,35536487.6848,22033,1612.87558139155,28197847 -01.05.2018,29699599.176,14959,1985.40003850525,28197847 -01.06.2018,33261065.3886,23067,1441.93286463779,28197847 -01.07.2018,35826534.9072,18397,1947.41180122846,28197847 -01.08.2018,23268655.2112,12045,1931.81031226235,28197847 -01.09.2018,35423489.85,23358,1516.54635884922,28197847 -01.10.2018,39831565.6974,22644,1759.03399122946,28197847 -01.11.2018,32999145.2096,19765,1669.57476395649,28197847 -01.12.2018,47221828.2018,33207,1422.04439430843,28197847 -01.01.2019,36459960.091485,24096,1513.11255359749,29878525 -01.02.2019,36546498.663015,21624,1690.08965330258,29878525 -01.03.2019,54198706.7196,33379,1623.7366823332,29878525 -01.04.2019,32743989.6056,22265,1470.64853382439,29878525 -01.05.2019,32531657.5397,16967,1917.34882652797,29878525 -01.06.2019,47709701.6346,24958,1911.59955263242,29878525 -01.07.2019,45992141.57398,21917,2098.46884035133,29878525 -01.08.2019,36933665.022,14431,2559.32818390964,29878525 -01.09.2019,48526260.1344,23253,2086.88169846471,29878525 -01.10.2019,44160416.1824,26603,1659.9788062399,29878525 -01.11.2019,36374956.4944,21987,1654.38470434348,29878525 -01.12.2019,58756473.6608,38069,1543.42046444088,29878525 -01.01.2020,56288300.87,27184,2070.64085013243,29044998 -01.02.2020,40225243.264,23509,1711.05718082437,29044998 -01.03.2020,50022165.2325,32569,1535.88274839571,29044998 -01.04.2020,52320692.9428,26615,1965.83479026113,29044998 diff --git a/src/aesthetics/line.rs b/src/aesthetics/line.rs deleted file mode 100644 index 5165fff..0000000 --- a/src/aesthetics/line.rs +++ /dev/null @@ -1,73 +0,0 @@ -use plotly::common::{DashType, Line as LinePlotly}; - -/// An enum representing different styles of lines that can be used in plots. -/// -/// The `LineType` enum defines various styles of lines, such as solid, dashed, or dotted, -/// that can be applied to plot traces, such as lines in a line plot or borders in a bar plot. -/// -/// # Variants -/// -/// - `Solid`: A continuous, solid line with no breaks. -/// - `Dot`: A line composed of dots, spaced evenly. -/// - `Dash`: A dashed line with evenly spaced short dashes. -/// - `LongDash`: A dashed line with longer dashes than `Dash`. -/// - `DashDot`: A line that alternates between a dash and a dot. -/// - `LongDashDot`: A line that alternates between a long dash and a dot. -/// -/// # Example Usage -/// -/// ```rust -/// use crate::LineType; -/// -/// let solid_line = LineType::Solid; -/// let dashed_line = LineType::Dash; -/// let custom_line = LineType::LongDashDot; -/// ``` -#[derive(Clone)] -pub enum LineType { - Solid, - Dot, - Dash, - LongDash, - DashDot, - LongDashDot, -} - -pub(crate) trait Line { - fn create_line() -> LinePlotly { - LinePlotly::new() - } - - fn set_line_type( - line: &LinePlotly, - line_types: &Option>, - width: Option, - index: usize, - ) -> LinePlotly { - let mut updated_line = line.clone(); - - if let Some(line_type_list) = line_types { - if let Some(line_type) = line_type_list.get(index) { - let line_style = Self::convert_line_type(line_type); - updated_line = updated_line.dash(line_style); - } - } - - if let Some(width) = width { - updated_line = updated_line.width(width); - } - - updated_line - } - - fn convert_line_type(line_type: &LineType) -> DashType { - match line_type { - LineType::Solid => DashType::Solid, - LineType::Dot => DashType::Dot, - LineType::Dash => DashType::Dash, - LineType::LongDash => DashType::LongDash, - LineType::DashDot => DashType::DashDot, - LineType::LongDashDot => DashType::LongDashDot, - } - } -} diff --git a/src/aesthetics/mark.rs b/src/aesthetics/mark.rs deleted file mode 100644 index 7cba0e2..0000000 --- a/src/aesthetics/mark.rs +++ /dev/null @@ -1,67 +0,0 @@ -use plotly::common::Marker; - -use crate::{Rgb, Shape}; - -pub(crate) trait Mark { - fn create_marker(mut opacity: Option, mut size: Option) -> Marker { - if opacity.is_none() { - opacity = Some(1.0); - } - - if size.is_none() { - size = Some(10); - } - - marker!(opacity, size) - } - - fn set_color( - marker: &Marker, - color: &Option, - colors: &Option>, - index: usize, - ) -> Marker { - let mut updated_marker = marker.clone(); - - match color { - Some(rgb) => { - let group_color = plotly::color::Rgb::new(rgb.0, rgb.1, rgb.2); - updated_marker = updated_marker.color(group_color); - } - None => { - if let Some(colors) = colors { - if let Some(rgb) = colors.get(index) { - let group_color = plotly::color::Rgb::new(rgb.0, rgb.1, rgb.2); - updated_marker = updated_marker.color(group_color); - } - } - } - } - - updated_marker - } - - fn set_shape( - marker: &Marker, - shape: &Option, - shapes: &Option>, - index: usize, - ) -> Marker { - let mut updated_marker = marker.clone(); - - match shape { - Some(shape) => { - updated_marker = updated_marker.symbol(shape.get_shape()); - } - None => { - if let Some(shapes) = shapes { - if let Some(shape) = shapes.get(index) { - updated_marker = updated_marker.symbol(shape.get_shape()); - } - } - } - } - - updated_marker - } -} diff --git a/src/aesthetics/mod.rs b/src/aesthetics/mod.rs deleted file mode 100644 index cff4754..0000000 --- a/src/aesthetics/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub(crate) mod line; -pub(crate) mod mark; -pub(crate) mod orientation; diff --git a/src/aesthetics/orientation.rs b/src/aesthetics/orientation.rs deleted file mode 100644 index 9c36d81..0000000 --- a/src/aesthetics/orientation.rs +++ /dev/null @@ -1,22 +0,0 @@ -use plotly::common::Orientation as OrientationPlotly; - -/// Enumeration representing the orientation of the legend. -#[derive(Clone)] -pub enum Orientation { - Horizontal, - Vertical, -} - -impl Orientation { - /// Converts `Orientation` to the corresponding `OrientationPlotly` from the `plotly` crate. - /// - /// # Returns - /// - /// Returns the corresponding `OrientationPlotly`. - pub fn get_orientation(&self) -> OrientationPlotly { - match self { - Self::Horizontal => OrientationPlotly::Horizontal, - Self::Vertical => OrientationPlotly::Vertical, - } - } -} diff --git a/src/colors/mod.rs b/src/colors/mod.rs deleted file mode 100644 index e60f6e1..0000000 --- a/src/colors/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -use plotly::color::Color; -use serde::Serialize; - -/// A structure representing an RGB color with red, green, and blue components. -#[derive(Debug, Default, Serialize, Clone)] -pub struct Rgb( - /// Red component - pub u8, - /// Green component - pub u8, - /// Blue component - pub u8, -); - -impl Color for Rgb {} diff --git a/src/common/layout.rs b/src/common/layout.rs new file mode 100644 index 0000000..df9e84b --- /dev/null +++ b/src/common/layout.rs @@ -0,0 +1,212 @@ +use plotly::{ + color::Rgb as RgbPlotly, + common::{Font, Title}, + layout::{Axis as AxisPlotly, Legend as LegendPlotly}, + Layout as LayoutPlotly, +}; + +use crate::components::{Axis, Legend, Text}; + +#[allow(clippy::too_many_arguments)] +pub(crate) trait Layout { + fn create_layout( + plot_title: Option, + x_title: Option, + y_title: Option, + legend_title: Option, + x_axis: Option<&Axis>, + y_axis: Option<&Axis>, + legend: Option<&Legend>, + ) -> LayoutPlotly { + let mut layout = LayoutPlotly::new(); + + if let Some(title) = plot_title { + layout = layout.title(Self::set_title(title)); + } + + layout = layout.x_axis(Self::set_axis(x_title, x_axis)); + layout = layout.y_axis(Self::set_axis(y_title, y_axis)); + layout = layout.legend(Self::set_legend(legend_title, legend)); + layout + } + + fn set_title(title: Text) -> Title { + Title::with_text(title.content) + .font( + Font::new() + .family(title.font.as_str()) + .size(title.size) + .color(RgbPlotly::new(title.color.0, title.color.1, title.color.2)), + ) + .x(title.x) + .y(title.y) + } + + fn set_axis(title: Option, format: Option<&Axis>) -> AxisPlotly { + let mut axis = AxisPlotly::new(); + + if let Some(title) = title { + axis = axis.title(Self::set_title(title)); + } + + if let Some(format) = format { + axis = Self::set_axis_format(axis, format); + } + + axis + } + + fn set_legend(title: Option, format: Option<&Legend>) -> LegendPlotly { + let mut legend = LegendPlotly::new(); + + if let Some(title) = title { + legend = legend.title(Self::set_title(title)); + } + + if let Some(format) = format { + legend = Self::set_legend_format(legend, format); + } + + legend + } + + fn set_axis_format(mut axis: AxisPlotly, format: &Axis) -> AxisPlotly { + if let Some(visible) = format.show_axis { + axis = axis.visible(visible.to_owned()); + } + + if let Some(axis_position) = &format.axis_side { + axis = axis.side(axis_position.get_side()); + } + + if let Some(axis_type) = &format.axis_type { + axis = axis.type_(axis_type.get_type()); + } + + if let Some(color) = format.value_color { + axis = axis.color(RgbPlotly::new(color.0, color.1, color.2)); + } + + if let Some(range) = &format.value_range { + axis = axis.range(range.to_owned()); + } + + if let Some(thousands) = format.value_thousands { + axis = axis.separate_thousands(thousands.to_owned()); + } + + if let Some(exponent) = &format.value_exponent { + axis = axis.exponent_format(exponent.get_exponent()); + } + + if let Some(range_values) = &format.tick_values { + axis = axis.tick_values(range_values.to_owned()); + } + + if let Some(tick_text) = &format.tick_labels { + axis = axis.tick_text(tick_text.to_owned()); + } + + if let Some(tick_direction) = &format.tick_direction { + axis = axis.ticks(tick_direction.get_direction()); + } + + if let Some(tick_length) = format.tick_length { + axis = axis.tick_length(tick_length.to_owned()); + } + + if let Some(tick_width) = format.tick_width { + axis = axis.tick_width(tick_width.to_owned()); + } + + if let Some(tick_color) = format.tick_color { + axis = axis.tick_color(RgbPlotly::new(tick_color.0, tick_color.1, tick_color.2)); + } + + if let Some(tick_angle) = format.tick_angle { + axis = axis.tick_angle(tick_angle.to_owned()); + } + + if let Some(font) = &format.tick_font { + axis = axis.tick_font(Font::new().family(font.as_str())); + } + + if let Some(show_line) = format.show_line { + axis = axis.show_line(show_line.to_owned()); + } + + if let Some(line_color) = format.line_color { + axis = axis.line_color(RgbPlotly::new(line_color.0, line_color.1, line_color.2)); + } + + if let Some(line_width) = format.line_width { + axis = axis.line_width(line_width.to_owned()); + } + + if let Some(show_grid) = format.show_grid { + axis = axis.show_grid(show_grid.to_owned()); + } + + if let Some(grid_color) = format.grid_color { + axis = axis.grid_color(RgbPlotly::new(grid_color.0, grid_color.1, grid_color.2)); + } + + if let Some(grid_width) = format.grid_width { + axis = axis.grid_width(grid_width.to_owned()); + } + + if let Some(show_zero_line) = format.show_zero_line { + axis = axis.zero_line(show_zero_line.to_owned()); + } + + if let Some(zero_line_color) = format.zero_line_color { + axis = axis.zero_line_color(RgbPlotly::new( + zero_line_color.0, + zero_line_color.1, + zero_line_color.2, + )); + } + + if let Some(zero_line_width) = format.zero_line_width { + axis = axis.zero_line_width(zero_line_width.to_owned()); + } + + if let Some(axis_position) = format.axis_position { + axis = axis.position(axis_position.to_owned()); + } + + axis + } + + fn set_legend_format(mut legend: LegendPlotly, format: &Legend) -> LegendPlotly { + if let Some(color) = format.background_color { + legend = legend.background_color(RgbPlotly::new(color.0, color.1, color.2)); + } + + if let Some(color) = format.border_color { + legend = legend.border_color(RgbPlotly::new(color.0, color.1, color.2)); + } + + if let Some(width) = format.border_width { + legend = legend.border_width(width); + } + + if let Some(font) = &format.font { + legend = legend.font(Font::new().family(font.as_str())); + } + + if let Some(orientation) = &format.orientation { + legend = legend.orientation(orientation.get_orientation()); + } + + if let Some(x) = format.x { + legend = legend.x(x); + } + + if let Some(y) = format.y { + legend = legend.y(y); + } + + legend + } +} diff --git a/src/common/line.rs b/src/common/line.rs new file mode 100644 index 0000000..0c2e9e2 --- /dev/null +++ b/src/common/line.rs @@ -0,0 +1,45 @@ +use plotly::common::Line as LinePlotly; + +use crate::components::Line as LineStyle; + +pub(crate) trait Line { + fn create_line( + index: usize, + width: Option, + style: Option, + styles: Option>, + ) -> LinePlotly { + let mut line = LinePlotly::new(); + line = Self::set_width(line, width); + line = Self::set_style(line, style, styles, index); + line + } + + fn set_width(mut line: LinePlotly, width: Option) -> LinePlotly { + if let Some(width) = width { + line = line.width(width); + } + + line + } + + fn set_style( + mut line: LinePlotly, + style: Option, + styles: Option>, + index: usize, + ) -> LinePlotly { + if let Some(style) = style { + line = line.dash(style.get_line_type()); + return line; + } + + if let Some(styles) = styles { + if let Some(style) = styles.get(index) { + line = line.dash(style.get_line_type()); + } + } + + line + } +} diff --git a/src/common/mark.rs b/src/common/mark.rs new file mode 100644 index 0000000..fd43288 --- /dev/null +++ b/src/common/mark.rs @@ -0,0 +1,80 @@ +use plotly::common::Marker as MarkerPlotly; + +use crate::components::{Rgb, Shape}; + +pub(crate) trait Marker { + fn create_marker( + index: usize, + opacity: Option, + size: Option, + color: Option, + colors: Option>, + shape: Option, + shapes: Option>, + ) -> MarkerPlotly { + let mut marker = MarkerPlotly::new(); + marker = Self::set_opacity(marker, opacity); + marker = Self::set_size(marker, size); + marker = Self::set_color(marker, color, colors, index); + marker = Self::set_shape(marker, shape, shapes, index); + marker + } + + fn set_opacity(mut marker: MarkerPlotly, opacity: Option) -> MarkerPlotly { + if let Some(opacity) = opacity { + marker = marker.opacity(opacity); + } + + marker + } + + fn set_size(mut marker: MarkerPlotly, size: Option) -> MarkerPlotly { + if let Some(size) = size { + marker = marker.size(size); + } + + marker + } + + fn set_color( + mut marker: MarkerPlotly, + color: Option, + colors: Option>, + index: usize, + ) -> MarkerPlotly { + if let Some(rgb) = color { + let color = plotly::color::Rgb::new(rgb.0, rgb.1, rgb.2); + + marker = marker.color(color); + return marker; + } + + if let Some(colors) = colors { + if let Some(rgb) = colors.get(index) { + let group_color = plotly::color::Rgb::new(rgb.0, rgb.1, rgb.2); + marker = marker.color(group_color); + } + } + marker + } + + fn set_shape( + mut marker: MarkerPlotly, + shape: Option, + shapes: Option>, + index: usize, + ) -> MarkerPlotly { + if let Some(shape) = shape { + marker = marker.symbol(shape.get_shape()); + return marker; + } + + if let Some(shapes) = shapes { + if let Some(shape) = shapes.get(index) { + marker = marker.symbol(shape.get_shape()); + } + } + + marker + } +} diff --git a/src/common/mod.rs b/src/common/mod.rs new file mode 100644 index 0000000..1ba4423 --- /dev/null +++ b/src/common/mod.rs @@ -0,0 +1,11 @@ +pub(crate) mod layout; +pub(crate) mod line; +pub(crate) mod mark; +pub(crate) mod plot; +pub(crate) mod polar; + +pub(crate) use layout::Layout; +pub(crate) use line::Line; +pub(crate) use mark::Marker; +pub(crate) use plot::Plot; +pub(crate) use polar::Polar; diff --git a/src/traits/plot.rs b/src/common/plot.rs similarity index 100% rename from src/traits/plot.rs rename to src/common/plot.rs diff --git a/src/traits/polar.rs b/src/common/polar.rs similarity index 100% rename from src/traits/polar.rs rename to src/common/polar.rs diff --git a/src/axis/mod.rs b/src/components/axis.rs similarity index 66% rename from src/axis/mod.rs rename to src/components/axis.rs index 3a826bd..1048779 100644 --- a/src/axis/mod.rs +++ b/src/components/axis.rs @@ -1,16 +1,30 @@ use plotly::{ - common::{AxisSide as AxisSidePlotly, ExponentFormat}, + common::AxisSide as AxisSidePlotly, layout::{AxisType as AxisTypePlotly, TicksDirection}, }; -use crate::Rgb; +use crate::components::{Rgb, ValueExponent}; -/// A structure representing an axis with customizable properties such as position, type, color, ticks, and grid lines. +/// A structure representing a customizable axis. /// -/// **Example** +/// # Example /// -/// ``` -/// let axis_format = Axis::new() +/// ```rust +/// use plotlars::{Axis, Plot, Rgb, ScatterPlot, Text, TickDirection}; +/// +/// let dataset = LazyCsvReader::new("data/penguins.csv") +/// .finish() +/// .unwrap() +/// .select([ +/// col("species"), +/// col("sex").alias("gender"), +/// col("flipper_length_mm").cast(DataType::Int16), +/// col("body_mass_g").cast(DataType::Int16), +/// ]) +/// .collect() +/// .unwrap(); +/// +/// let axis = Axis::new() /// .show_line(true) /// .tick_direction(TickDirection::OutSide) /// .value_thousands(true) @@ -21,7 +35,11 @@ use crate::Rgb; /// .x("body_mass_g") /// .y("flipper_length_mm") /// .group("species") -/// .colors(vec![Rgb(255, 0, 0), Rgb(0, 255, 0), Rgb(0, 0, 255)]) +/// .colors(vec![ +/// Rgb(255, 0, 0), +/// Rgb(0, 255, 0), +/// Rgb(0, 0, 255), +/// ]) /// .opacity(0.5) /// .size(20) /// .plot_title( @@ -33,13 +51,13 @@ use crate::Rgb; /// .x_title("body mass (g)") /// .y_title("flipper length (mm)") /// .legend_title("species") -/// .x_axis(&axis_format) -/// .y_axis(&axis_format) +/// .x_axis(&axis) +/// .y_axis(&axis) /// .build() /// .plot(); /// ``` /// -/// ![example](https://imgur.com/9jfO8RU.png) +/// ![example](https://imgur.com/P24E1ND.png) #[derive(Default, Clone)] pub struct Axis { pub(crate) show_axis: Option, @@ -71,23 +89,15 @@ pub struct Axis { impl Axis { /// Creates a new `Axis` instance with default values. - /// - /// # Returns - /// - /// Returns a new `Axis` instance with all properties set to `None` or default values. pub fn new() -> Self { Self::default() } /// Sets the visibility of the axis. /// - /// # Arguments + /// # Argument /// /// * `bool` - A boolean value indicating whether the axis should be visible. - /// - /// # Returns - /// - /// Returns the `Axis` instance with the updated visibility. pub fn show_axis(mut self, bool: bool) -> Self { self.show_axis = Some(bool); self @@ -95,13 +105,9 @@ impl Axis { /// Sets the side of the axis. /// - /// # Arguments + /// # Argument /// /// * `side` - An `AxisSide` enum value representing the side of the axis. - /// - /// # Returns - /// - /// Returns the `Axis` instance with the updated side. pub fn axis_side(mut self, side: AxisSide) -> Self { self.axis_side = Some(side); self @@ -109,13 +115,9 @@ impl Axis { /// Sets the position of the axis. /// - /// # Arguments + /// # Argument /// /// * `position` - A `f64` value representing the position of the axis. - /// - /// # Returns - /// - /// Returns the `Axis` instance with the updated position. pub fn axis_position(mut self, position: f64) -> Self { self.axis_position = Some(position); self @@ -123,13 +125,9 @@ impl Axis { /// Sets the type of the axis. /// - /// # Arguments + /// # Argument /// /// * `axis_type` - An `AxisType` enum value representing the type of the axis. - /// - /// # Returns - /// - /// Returns the `Axis` instance with the updated type. pub fn axis_type(mut self, axis_type: AxisType) -> Self { self.axis_type = Some(axis_type); self @@ -137,13 +135,9 @@ impl Axis { /// Sets the color of the axis values. /// - /// # Arguments + /// # Argument /// /// * `color` - An `Rgb` struct representing the color of the axis values. - /// - /// # Returns - /// - /// Returns the `Axis` instance with the updated value color. pub fn value_color(mut self, color: Rgb) -> Self { self.value_color = Some(color); self @@ -151,13 +145,9 @@ impl Axis { /// Sets the range of values displayed on the axis. /// - /// # Arguments + /// # Argument /// /// * `range` - A vector of `f64` values representing the range of the axis. - /// - /// # Returns - /// - /// Returns the `Axis` instance with the updated value range. pub fn value_range(mut self, range: Vec) -> Self { self.value_range = Some(range); self @@ -165,13 +155,9 @@ impl Axis { /// Sets whether to use thousands separators for values. /// - /// # Arguments + /// # Argument /// /// * `bool` - A boolean value indicating whether to use thousands separators. - /// - /// # Returns - /// - /// Returns the `Axis` instance with the updated setting. pub fn value_thousands(mut self, bool: bool) -> Self { self.value_thousands = Some(bool); self @@ -179,13 +165,9 @@ impl Axis { /// Sets the exponent format for values on the axis. /// - /// # Arguments + /// # Argument /// /// * `exponent` - A `ValueExponent` enum value representing the exponent format. - /// - /// # Returns - /// - /// Returns the `Axis` instance with the updated exponent format. pub fn value_exponent(mut self, exponent: ValueExponent) -> Self { self.value_exponent = Some(exponent); self @@ -193,13 +175,9 @@ impl Axis { /// Sets the tick values for the axis. /// - /// # Arguments + /// # Argument /// /// * `tick_values` - A vector of `f64` values representing the tick values. - /// - /// # Returns - /// - /// Returns the `Axis` instance with the updated tick values. pub fn tick_values(mut self, tick_values: Vec) -> Self { self.tick_values = Some(tick_values); self @@ -207,13 +185,9 @@ impl Axis { /// Sets the tick labels for the axis. /// - /// # Arguments + /// # Argument /// /// * `tick_labels` - A vector of values that can be converted into `String`, representing the tick labels. - /// - /// # Returns - /// - /// Returns the `Axis` instance with the updated tick labels. pub fn tick_labels(mut self, tick_labels: Vec>) -> Self { self.tick_labels = Some(tick_labels.into_iter().map(|x| x.into()).collect()); self @@ -221,13 +195,9 @@ impl Axis { /// Sets the direction of the axis ticks. /// - /// # Arguments + /// # Argument /// /// * `tick_direction` - A `TickDirection` enum value representing the direction of the ticks. - /// - /// # Returns - /// - /// Returns the `Axis` instance with the updated tick direction. pub fn tick_direction(mut self, tick_direction: TickDirection) -> Self { self.tick_direction = Some(tick_direction); self @@ -235,13 +205,9 @@ impl Axis { /// Sets the length of the axis ticks. /// - /// # Arguments + /// # Argument /// /// * `tick_length` - A `usize` value representing the length of the ticks. - /// - /// # Returns - /// - /// Returns the `Axis` instance with the updated tick length. pub fn tick_length(mut self, tick_length: usize) -> Self { self.tick_length = Some(tick_length); self @@ -249,13 +215,9 @@ impl Axis { /// Sets the width of the axis ticks. /// - /// # Arguments + /// # Argument /// /// * `tick_width` - A `usize` value representing the width of the ticks. - /// - /// # Returns - /// - /// Returns the `Axis` instance with the updated tick width. pub fn tick_width(mut self, tick_width: usize) -> Self { self.tick_width = Some(tick_width); self @@ -263,13 +225,9 @@ impl Axis { /// Sets the color of the axis ticks. /// - /// # Arguments + /// # Argument /// /// * `tick_color` - An `Rgb` struct representing the color of the ticks. - /// - /// # Returns - /// - /// Returns the `Axis` instance with the updated tick color. pub fn tick_color(mut self, tick_color: Rgb) -> Self { self.tick_color = Some(tick_color); self @@ -277,13 +235,9 @@ impl Axis { /// Sets the angle of the axis ticks. /// - /// # Arguments + /// # Argument /// /// * `tick_angle` - A `f64` value representing the angle of the ticks in degrees. - /// - /// # Returns - /// - /// Returns the `Axis` instance with the updated tick angle. pub fn tick_angle(mut self, tick_angle: f64) -> Self { self.tick_angle = Some(tick_angle); self @@ -291,13 +245,9 @@ impl Axis { /// Sets the font of the axis tick labels. /// - /// # Arguments + /// # Argument /// /// * `tick_font` - A value that can be converted into a `String`, representing the font name for the tick labels. - /// - /// # Returns - /// - /// Returns the `Axis` instance with the updated tick font. pub fn tick_font(mut self, tick_font: impl Into) -> Self { self.tick_font = Some(tick_font.into()); self @@ -305,13 +255,9 @@ impl Axis { /// Sets whether to show the axis line. /// - /// # Arguments + /// # Argument /// /// * `bool` - A boolean value indicating whether the axis line should be visible. - /// - /// # Returns - /// - /// Returns the `Axis` instance with the updated axis line visibility. pub fn show_line(mut self, bool: bool) -> Self { self.show_line = Some(bool); self @@ -319,13 +265,9 @@ impl Axis { /// Sets the color of the axis line. /// - /// # Arguments + /// # Argument /// /// * `color` - An `Rgb` struct representing the color of the axis line. - /// - /// # Returns - /// - /// Returns the `Axis` instance with the updated axis line color. pub fn line_color(mut self, color: Rgb) -> Self { self.line_color = Some(color); self @@ -333,13 +275,9 @@ impl Axis { /// Sets the width of the axis line. /// - /// # Arguments + /// # Argument /// /// * `width` - A `usize` value representing the width of the axis line. - /// - /// # Returns - /// - /// Returns the `Axis` instance with the updated axis line width. pub fn line_width(mut self, width: usize) -> Self { self.line_width = Some(width); self @@ -347,13 +285,9 @@ impl Axis { /// Sets whether to show the grid lines on the axis. /// - /// # Arguments + /// # Argument /// /// * `bool` - A boolean value indicating whether the grid lines should be visible. - /// - /// # Returns - /// - /// Returns the `Axis` instance with the updated grid line visibility. pub fn show_grid(mut self, bool: bool) -> Self { self.show_grid = Some(bool); self @@ -361,13 +295,9 @@ impl Axis { /// Sets the color of the grid lines on the axis. /// - /// # Arguments + /// # Argument /// /// * `color` - An `Rgb` struct representing the color of the grid lines. - /// - /// # Returns - /// - /// Returns the `Axis` instance with the updated grid line color. pub fn grid_color(mut self, color: Rgb) -> Self { self.grid_color = Some(color); self @@ -375,13 +305,9 @@ impl Axis { /// Sets the width of the grid lines on the axis. /// - /// # Arguments + /// # Argument /// /// * `width` - A `usize` value representing the width of the grid lines. - /// - /// # Returns - /// - /// Returns the `Axis` instance with the updated grid line width. pub fn grid_width(mut self, width: usize) -> Self { self.grid_width = Some(width); self @@ -389,13 +315,9 @@ impl Axis { /// Sets whether to show the zero line on the axis. /// - /// # Arguments + /// # Argument /// /// * `bool` - A boolean value indicating whether the zero line should be visible. - /// - /// # Returns - /// - /// Returns the `Axis` instance with the updated zero line visibility. pub fn show_zero_line(mut self, bool: bool) -> Self { self.show_zero_line = Some(bool); self @@ -403,13 +325,9 @@ impl Axis { /// Sets the color of the zero line on the axis. /// - /// # Arguments + /// # Argument /// /// * `color` - An `Rgb` struct representing the color of the zero line. - /// - /// # Returns - /// - /// Returns the `Axis` instance with the updated zero line color. pub fn zero_line_color(mut self, color: Rgb) -> Self { self.zero_line_color = Some(color); self @@ -417,47 +335,62 @@ impl Axis { /// Sets the width of the zero line on the axis. /// - /// # Arguments + /// # Argument /// /// * `width` - A `usize` value representing the width of the zero line. - /// - /// # Returns - /// - /// Returns the `Axis` instance with the updated zero line width. pub fn zero_line_width(mut self, width: usize) -> Self { self.zero_line_width = Some(width); self } } -impl From<&Axis> for Axis { - fn from(value: &Axis) -> Self { - value.clone() - } -} - -/// Enumeration representing the direction of axis ticks. -#[derive(Clone)] -pub enum TickDirection { - OutSide, - InSide, -} - -impl TickDirection { - /// Converts `TickDirection` to the corresponding `TicksDirection` from the `plotly` crate. - /// - /// # Returns - /// - /// Returns the corresponding `TicksDirection`. - pub fn get_direction(&self) -> TicksDirection { - match self { - TickDirection::OutSide => TicksDirection::Outside, - TickDirection::InSide => TicksDirection::Inside, - } - } -} - /// Enumeration representing the position of the axis. +/// +/// # Example +/// +/// ```rust +/// use plotlars::{Axis, AxisSide, Legend, Line, Plot, Rgb, Shape, Text, TimeSeriesPlot}; +/// +/// let dataset = LazyCsvReader::new("data/revenue_and_cost.csv") +/// .finish() +/// .unwrap() +/// .select([ +/// col("Date").cast(DataType::String), +/// col("Revenue").cast(DataType::Int32), +/// col("Cost").cast(DataType::Int32), +/// ]) +/// .collect() +/// .unwrap(); +/// +/// TimeSeriesPlot::builder() +/// .data(&dataset) +/// .x("Date") +/// .y("Revenue") +/// .additional_series(vec!["Cost"]) +/// .size(8) +/// .colors(vec![Rgb(255, 0, 0), Rgb(0, 255, 0)]) +/// .lines(vec![Line::Dash, Line::Solid]) +/// .with_shape(true) +/// .shapes(vec![Shape::Circle, Shape::Square]) +/// .plot_title( +/// Text::from("Time Series Plot") +/// .font("Arial") +/// .size(18) +/// ) +/// .y_axis( +/// &Axis::new() +/// .axis_side(AxisSide::Right) +/// ) +/// .legend( +/// &Legend::new() +/// .x(0.05) +/// .y(0.9) +/// ) +/// .build() +/// .plot(); +/// ``` +/// +/// ![Example](https://imgur.com/Ok0c5R5.png) #[derive(Clone)] pub enum AxisSide { Top, @@ -467,12 +400,7 @@ pub enum AxisSide { } impl AxisSide { - /// Converts `AxisPosition` to the corresponding `AxisSide` from the `plotly` crate. - /// - /// # Returns - /// - /// Returns the corresponding `AxisSide`. - pub fn get_side(&self) -> AxisSidePlotly { + pub(crate) fn get_side(&self) -> AxisSidePlotly { match self { AxisSide::Top => AxisSidePlotly::Top, AxisSide::Bottom => AxisSidePlotly::Bottom, @@ -483,6 +411,49 @@ impl AxisSide { } /// Enumeration representing the type of the axis. +/// +/// # Example +/// +/// ```rust +/// use plotlars::{Axis, AxisType, LinePlot, Plot}; +/// +/// let linear_values = vec![ +/// 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, +/// 20, 30, 40, 50, 60, 70, 80, 90, 100, +/// 200, 300, 400, 500, 600, 700, 800, 900, 1000 +/// ]; +/// +/// let logarithms = vec![ +/// 0.0000, 0.3010, 0.4771, 0.6021, 0.6990, +/// 0.7782, 0.8451, 0.9031, 0.9542, 1.0000, +/// 1.3010, 1.4771, 1.6021, 1.6990, 1.7782, +/// 1.8451, 1.9031, 1.9542, 2.0000, +/// 2.3010, 2.4771, 2.6021, 2.6990, +/// 2.7782, 2.8451, 2.9031, 2.9542, 3.0000 +/// ]; +/// +/// let dataset = DataFrame::new(vec![ +/// Series::new("linear_values".into(), linear_values), +/// Series::new("logarithms".into(), logarithms), +/// ]).unwrap(); +/// +/// let axis = Axis::new() +/// .axis_type(AxisType::Log) +/// .show_line(true); +/// +/// LinePlot::builder() +/// .data(&dataset) +/// .x("linear_values") +/// .y("logarithms") +/// .y_title("log₁₀ x") +/// .x_title("x") +/// .y_axis(&axis) +/// .x_axis(&axis) +/// .build() +/// .plot(); +/// ``` +/// +/// ![Example](https://imgur.com/rjNNO5q.png) #[derive(Clone)] pub enum AxisType { Default, @@ -494,12 +465,7 @@ pub enum AxisType { } impl AxisType { - /// Converts `AxisType` to the corresponding `AxisTypePlotly` from the `plotly` crate. - /// - /// # Returns - /// - /// Returns the corresponding `AxisTypePlotly`. - pub fn get_type(&self) -> AxisTypePlotly { + pub(crate) fn get_type(&self) -> AxisTypePlotly { match self { AxisType::Default => AxisTypePlotly::Default, AxisType::Linear => AxisTypePlotly::Linear, @@ -511,31 +477,49 @@ impl AxisType { } } -/// Enumeration representing the format for value exponents on the axis. +/// Enumeration representing the direction of axis ticks. +/// +/// # Example +/// +/// ```rust +/// use plotlars::{Axis, Plot, ScatterPlot, TickDirection}; +/// +/// let x = vec![1]; +/// let y = vec![1]; +/// +/// let dataset = DataFrame::new(vec![ +/// Series::new("x".into(), x), +/// Series::new("y".into(), y), +/// ]).unwrap(); +/// +/// ScatterPlot::builder() +/// .data(&dataset) +/// .x("x") +/// .y("y") +/// .x_axis( +/// &Axis::new() +/// .tick_direction(TickDirection::OutSide) +/// ) +/// .y_axis( +/// &Axis::new() +/// .tick_direction(TickDirection::InSide) +/// ) +/// .build() +/// .plot(); +/// ``` +/// +/// ![Example](https://imgur.com/9DSwJnx.png) #[derive(Clone)] -pub enum ValueExponent { - None, - SmallE, - CapitalE, - Power, - SI, - B, +pub enum TickDirection { + OutSide, + InSide, } -impl ValueExponent { - /// Converts `ValueExponent` to the corresponding `ExponentFormat` from the `plotly` crate. - /// - /// # Returns - /// - /// Returns the corresponding `ExponentFormat`. - pub fn get_exponent(&self) -> ExponentFormat { +impl TickDirection { + pub(crate) fn get_direction(&self) -> TicksDirection { match self { - ValueExponent::None => ExponentFormat::None, - ValueExponent::SmallE => ExponentFormat::SmallE, - ValueExponent::CapitalE => ExponentFormat::CapitalE, - ValueExponent::Power => ExponentFormat::Power, - ValueExponent::SI => ExponentFormat::SI, - ValueExponent::B => ExponentFormat::B, + TickDirection::OutSide => TicksDirection::Outside, + TickDirection::InSide => TicksDirection::Inside, } } } diff --git a/src/components/color.rs b/src/components/color.rs new file mode 100644 index 0000000..a58eeee --- /dev/null +++ b/src/components/color.rs @@ -0,0 +1,55 @@ +use plotly::color::Color; +use serde::Serialize; + +/// A structure representing an RGB color with red, green, and blue components. +/// +/// # Example +/// +/// ```rust +/// use plotlars::{Axis, BarPlot, Legend, Orientation, Plot, Rgb}; +/// +/// let label = vec!["", "", ""]; +/// let color = vec!["red", "green", "blue"]; +/// let value = vec![1, 1, 1]; +/// +/// let df = DataFrame::new(vec![ +/// Series::new("label".into(), label), +/// Series::new("color".into(), color), +/// Series::new("value".into(), value), +/// ]).unwrap(); +/// +/// let axis = Axis::new() +/// .show_axis(false); +/// +/// let legend = Legend::new() +/// .orientation(Orientation::Horizontal) +/// .x(0.3); +/// +/// BarPlot::builder() +/// .data(&df) +/// .labels("label") +/// .values("value") +/// .group("color") +/// .colors(vec![ +/// Rgb(255, 0, 0), +/// Rgb(0, 255, 0), +/// Rgb(0, 0, 255), +/// ]) +/// .x_axis(&axis) +/// .y_axis(&axis) +/// .legend(&legend) +/// .build() +/// .plot(); +/// ``` +/// ![example](https://imgur.com/HPmtj9I.png) +#[derive(Debug, Default, Clone, Copy, Serialize)] +pub struct Rgb( + /// Red component + pub u8, + /// Green component + pub u8, + /// Blue component + pub u8, +); + +impl Color for Rgb {} diff --git a/src/components/exponent.rs b/src/components/exponent.rs new file mode 100644 index 0000000..b6ebdc5 --- /dev/null +++ b/src/components/exponent.rs @@ -0,0 +1,55 @@ +use plotly::common::ExponentFormat; + +/// An enumeration representing the format for value exponents on the axis. +/// +/// # Example +/// +/// ```rust +/// use plotlars::{Axis, Plot, TimeSeriesPlot, ValueExponent}; +/// +/// let dataset = LazyCsvReader::new("data/revenue_and_cost.csv") +/// .finish() +/// .unwrap() +/// .select([ +/// col("Date").cast(DataType::String), +/// col("Revenue").cast(DataType::Int32), +/// col("Cost").cast(DataType::Int32), +/// ]) +/// .collect() +/// .unwrap(); +/// +/// TimeSeriesPlot::builder() +/// .data(&dataset) +/// .x("Date") +/// .y("Revenue") +/// .y_axis( +/// &Axis::new() +/// .value_exponent(ValueExponent::SmallE) +/// ) +/// .build() +/// .plot(); +/// ``` +/// +/// ![Example](https://imgur.com/I6gYYkb.png) +#[derive(Clone)] +pub enum ValueExponent { + None, + SmallE, + CapitalE, + Power, + SI, + B, +} + +impl ValueExponent { + pub(crate) fn get_exponent(&self) -> ExponentFormat { + match self { + ValueExponent::None => ExponentFormat::None, + ValueExponent::SmallE => ExponentFormat::SmallE, + ValueExponent::CapitalE => ExponentFormat::CapitalE, + ValueExponent::Power => ExponentFormat::Power, + ValueExponent::SI => ExponentFormat::SI, + ValueExponent::B => ExponentFormat::B, + } + } +} diff --git a/src/legend/mod.rs b/src/components/legend.rs similarity index 67% rename from src/legend/mod.rs rename to src/components/legend.rs index 5201206..764cb31 100644 --- a/src/legend/mod.rs +++ b/src/components/legend.rs @@ -1,11 +1,25 @@ use crate::{Orientation, Rgb}; -/// A structure representing a customizable plot legend with properties such as background color, border, font, orientation, and position. +/// A structure representing a customizable plot legend. /// -/// **Example** +/// # Example /// -/// ``` -/// let legend_format = Legend::new() +/// ```rust +/// use plotlars::{Histogram, Legend, Orientation, Plot, Rgb}; +/// +/// let dataset = LazyCsvReader::new("data/penguins.csv") +/// .finish() +/// .unwrap() +/// .select([ +/// col("species"), +/// col("sex").alias("gender"), +/// col("flipper_length_mm").cast(DataType::Int16), +/// col("body_mass_g").cast(DataType::Int16), +/// ]) +/// .collect() +/// .unwrap(); +/// +/// let legend = Legend::new() /// .orientation(Orientation::Horizontal) /// .border_width(1) /// .x(0.78) @@ -15,17 +29,21 @@ use crate::{Orientation, Rgb}; /// .data(&dataset) /// .x("body_mass_g") /// .group("species") -/// .colors(vec![Rgb(255, 0, 0), Rgb(0, 255, 0), Rgb(0, 0, 255)]) +/// .colors(vec![ +/// Rgb(255, 0, 0), +/// Rgb(0, 255, 0), +/// Rgb(0, 0, 255), +/// ]) /// .opacity(0.5) /// .x_title("Body Mass (g)") /// .y_title("Frequency") /// .legend_title("Species") -/// .legend(&legend_format) +/// .legend(&legend) /// .build() /// .plot(); /// ``` /// -/// ![example](https://imgur.com/iWGEZs0.png) +/// ![example](https://imgur.com/GpUsgli.png) #[derive(Clone, Default)] pub struct Legend { pub(crate) background_color: Option, @@ -39,23 +57,15 @@ pub struct Legend { impl Legend { /// Creates a new `Legend` instance with default values. - /// - /// # Returns - /// - /// Returns a new `Legend` instance with all properties set to `None` or default values. pub fn new() -> Self { Self::default() } /// Sets the background color of the legend. /// - /// # Arguments + /// # Argument /// /// * `color` - An `Rgb` struct representing the background color. - /// - /// # Returns - /// - /// Returns the `Legend` instance with the updated background color. pub fn background_color(mut self, color: Rgb) -> Self { self.background_color = Some(color); self @@ -63,13 +73,9 @@ impl Legend { /// Sets the border color of the legend. /// - /// # Arguments + /// # Argument /// /// * `color` - An `Rgb` struct representing the border color. - /// - /// # Returns - /// - /// Returns the `Legend` instance with the updated border color. pub fn border_color(mut self, color: Rgb) -> Self { self.border_color = Some(color); self @@ -77,13 +83,9 @@ impl Legend { /// Sets the border width of the legend. /// - /// # Arguments + /// # Argument /// /// * `width` - A `usize` value representing the width of the border. - /// - /// # Returns - /// - /// Returns the `Legend` instance with the updated border width. pub fn border_width(mut self, width: usize) -> Self { self.border_width = Some(width); self @@ -91,13 +93,9 @@ impl Legend { /// Sets the font of the legend labels. /// - /// # Arguments + /// # Argument /// /// * `font` - A value that can be converted into a `String`, representing the font name for the labels. - /// - /// # Returns - /// - /// Returns the `Legend` instance with the updated font. pub fn font(mut self, font: impl Into) -> Self { self.font = Some(font.into()); self @@ -105,13 +103,9 @@ impl Legend { /// Sets the orientation of the legend. /// - /// # Arguments + /// # Argument /// /// * `orientation` - An `Orientation` enum value representing the layout direction of the legend. - /// - /// # Returns - /// - /// Returns the `Legend` instance with the updated orientation. pub fn orientation(mut self, orientation: Orientation) -> Self { self.orientation = Some(orientation); self @@ -119,13 +113,9 @@ impl Legend { /// Sets the horizontal position of the legend. /// - /// # Arguments + /// # Argument /// /// * `x` - A `f64` value representing the horizontal position of the legend. - /// - /// # Returns - /// - /// Returns the `Legend` instance with the updated x position. pub fn x(mut self, x: f64) -> Self { self.x = Some(x); self @@ -133,13 +123,9 @@ impl Legend { /// Sets the vertical position of the legend. /// - /// # Arguments + /// # Argument /// /// * `y` - A `f64` value representing the vertical position of the legend. - /// - /// # Returns - /// - /// Returns the `Legend` instance with the updated y position. pub fn y(mut self, y: f64) -> Self { self.y = Some(y); self diff --git a/src/components/line.rs b/src/components/line.rs new file mode 100644 index 0000000..a02d5ac --- /dev/null +++ b/src/components/line.rs @@ -0,0 +1,60 @@ +use plotly::common::DashType; + +/// An enumeration representing different styles of lines that can be used in plots. +/// +/// # Example +/// +/// ```rust +/// use plotlars::{Legend, Line, Plot, Rgb, TimeSeriesPlot}; +/// +/// let dataset = LazyCsvReader::new("data/revenue_and_cost.csv") +/// .finish() +/// .unwrap() +/// .select([ +/// col("Date").cast(DataType::String), +/// col("Revenue").cast(DataType::Int32), +/// col("Cost").cast(DataType::Int32), +/// ]) +/// .collect() +/// .unwrap(); +/// +/// TimeSeriesPlot::builder() +/// .data(&dataset) +/// .x("Date") +/// .y("Revenue") +/// .additional_series(vec!["Cost"]) +/// .size(8) +/// .colors(vec![Rgb(255, 0, 0), Rgb(0, 255, 0)]) +/// .lines(vec![Line::Dash, Line::Solid]) +/// .legend( +/// &Legend::new() +/// .x(0.05) +/// .y(0.9) +/// ) +/// .build() +/// .plot(); +/// ``` +/// +/// ![Example](https://imgur.com/y6ZyypZ.png) +#[derive(Clone, Copy)] +pub enum Line { + Solid, + Dot, + Dash, + LongDash, + DashDot, + LongDashDot, +} + +impl Line { + pub(crate) fn get_line_type(&self) -> DashType { + match self { + Line::Solid => DashType::Solid, + Line::Dot => DashType::Dot, + Line::Dash => DashType::Dash, + Line::LongDash => DashType::LongDash, + Line::DashDot => DashType::DashDot, + Line::LongDashDot => DashType::LongDashDot, + } + } +} diff --git a/src/components/mod.rs b/src/components/mod.rs new file mode 100644 index 0000000..3c3154b --- /dev/null +++ b/src/components/mod.rs @@ -0,0 +1,17 @@ +pub(crate) mod axis; +pub(crate) mod color; +pub(crate) mod exponent; +pub(crate) mod legend; +pub(crate) mod line; +pub(crate) mod orientation; +pub(crate) mod shape; +pub(crate) mod text; + +pub(crate) use axis::Axis; +pub(crate) use color::Rgb; +pub(crate) use exponent::ValueExponent; +pub(crate) use legend::Legend; +pub(crate) use line::Line; +pub(crate) use orientation::Orientation; +pub(crate) use shape::Shape; +pub(crate) use text::Text; diff --git a/src/components/orientation.rs b/src/components/orientation.rs new file mode 100644 index 0000000..c2c7854 --- /dev/null +++ b/src/components/orientation.rs @@ -0,0 +1,62 @@ +use plotly::common::Orientation as OrientationPlotly; + +/// An enumeration representing the orientation of the legend. +/// +/// # Example +/// +/// ```rust +/// use plotlars::{BarPlot, Legend, Orientation, Plot, Rgb}; +/// +/// let animal = vec![ +/// "giraffe", +/// "giraffe", +/// "orangutan", +/// "orangutan", +/// "monkey", +/// "monkey", +/// ]; +/// let gender = vec!["female", "male", "female", "male", "female", "male"]; +/// let value = vec![20.0f32, 25.0, 14.0, 18.0, 23.0, 31.0]; +/// let error = vec![1.0, 0.5, 1.5, 1.0, 0.5, 1.5]; +/// +/// let dataset = DataFrame::new(vec![ +/// Series::new("animal".into(), animal), +/// Series::new("gender".into(), gender), +/// Series::new("value".into(), value), +/// Series::new("error".into(), error), +/// ]) +/// .unwrap(); +/// +/// let legend = Legend::new() +/// .orientation(Orientation::Horizontal) +/// .y(1.1) +/// .x(0.3); +/// +/// BarPlot::builder() +/// .data(&dataset) +/// .labels("animal") +/// .values("value") +/// .orientation(Orientation::Horizontal) +/// .group("gender") +/// .error("error") +/// .colors(vec![Rgb(255, 127, 80), Rgb(64, 224, 208)]) +/// .legend(&legend) +/// .build() +/// .plot(); +/// ``` +/// +/// ![Example](https://imgur.com/6kspyX7.png) +#[derive(Clone)] +pub enum Orientation { + Horizontal, + Vertical, +} + +impl Orientation { + pub(crate) fn get_orientation(&self) -> OrientationPlotly { + match self { + Self::Horizontal => OrientationPlotly::Horizontal, + Self::Vertical => OrientationPlotly::Vertical, + } + } +} diff --git a/src/shapes/mod.rs b/src/components/shape.rs similarity index 86% rename from src/shapes/mod.rs rename to src/components/shape.rs index befbad9..65d7dad 100644 --- a/src/shapes/mod.rs +++ b/src/components/shape.rs @@ -1,5 +1,74 @@ use plotly::common::MarkerSymbol; +/// An enumeration of various marker shapes used in plots. +/// +/// # Example +/// +/// ```rust +/// use plotlars::{Axis, Legend, Plot, Rgb, ScatterPlot, Shape, Text, TickDirection}; +/// +/// let dataset = LazyCsvReader::new("data/penguins.csv") +/// .finish() +/// .unwrap() +/// .select([ +/// col("species"), +/// col("sex").alias("gender"), +/// col("flipper_length_mm").cast(DataType::Int16), +/// col("body_mass_g").cast(DataType::Int16), +/// ]) +/// .collect() +/// .unwrap(); +/// +/// let axis = Axis::new() +/// .show_line(true) +/// .tick_direction(TickDirection::OutSide) +/// .value_thousands(true); +/// +/// ScatterPlot::builder() +/// .data(&dataset) +/// .x("body_mass_g") +/// .y("flipper_length_mm") +/// .group("species") +/// .opacity(0.5) +/// .size(12) +/// .colors(vec![ +/// Rgb(178, 34, 34), +/// Rgb(65, 105, 225), +/// Rgb(255, 140, 0), +/// ]) +/// .shapes(vec![ +/// Shape::Circle, +/// Shape::Square, +/// Shape::Diamond, +/// ]) +/// .plot_title( +/// Text::from("Scatter Plot") +/// .font("Arial") +/// .size(20) +/// .x(0.065) +/// ) +/// .x_title("body mass (g)") +/// .y_title("flipper length (mm)") +/// .legend_title("species") +/// .x_axis( +/// &axis.clone() +/// .value_range(vec![2500.0, 6500.0]) +/// ) +/// .y_axis( +/// &axis.clone() +/// .value_range(vec![170.0, 240.0]) +/// ) +/// .legend( +/// &Legend::new() +/// .x(0.85) +/// .y(0.15) +/// ) +/// .build() +/// .plot(); +/// ``` +/// +/// ![Example](https://imgur.com/9jfO8RU.png) +#[derive(Clone, Copy)] pub enum Shape { Circle, CircleOpen, @@ -146,7 +215,7 @@ pub enum Shape { } impl Shape { - pub fn get_shape(&self) -> MarkerSymbol { + pub(crate) fn get_shape(&self) -> MarkerSymbol { match self { Shape::Circle => MarkerSymbol::Circle, Shape::CircleOpen => MarkerSymbol::CircleOpen, diff --git a/src/components/text.rs b/src/components/text.rs new file mode 100644 index 0000000..fe09944 --- /dev/null +++ b/src/components/text.rs @@ -0,0 +1,160 @@ +use crate::components::Rgb; + +/// A structure representing text with customizable content, font, size, and color. +/// +/// # Example +/// +/// ```rust +/// use plotlars::{Axis, BarPlot, Plot, Text, Rgb}; +/// +/// let label = vec![""]; +/// let value = vec![0]; +/// +/// let dataset = DataFrame::new(vec![ +/// Series::new("label".into(), label), +/// Series::new("value".into(), value), +/// ]).unwrap(); +/// +/// let axis = Axis::new() +/// .tick_values(vec![]); +/// +/// BarPlot::builder() +/// .data(&dataset) +/// .labels("label") +/// .values("value") +/// .plot_title( +/// Text::from("Title") +/// .x(0.1) +/// .color(Rgb(178, 34, 34)) +/// .size(30) +/// .font("Zapfino") +/// ) +/// .x_title( +/// Text::from("X") +/// .color(Rgb(65, 105, 225)) +/// .size(20) +/// .font("Marker Felt") +/// ) +/// .y_title( +/// Text::from("Y") +/// .color(Rgb(255, 140, 0)) +/// .size(20) +/// .font("Arial Black") +/// ) +/// .x_axis(&axis) +/// .y_axis(&axis) +/// .build() +/// .plot(); +/// ``` +/// ![Example](https://imgur.com/4outoUQ.png) +pub struct Text { + pub(crate) content: String, + pub(crate) font: String, + pub(crate) size: usize, + pub(crate) color: Rgb, + pub(crate) x: f64, + pub(crate) y: f64, +} + +impl Default for Text { + /// Provides default values for the `Text` struct. + /// + /// - `content`: An empty string. + /// - `font`: An empty string. + /// - `size`: `0`. + /// - `color`: Default `Rgb` value. + /// - `x`: `0.5`. + /// - `y`: `0.9`. + fn default() -> Self { + Text { + content: String::new(), + font: String::new(), + size: 0, + color: Rgb::default(), + x: 0.5, + y: 0.9, + } + } +} + +impl Text { + /// Creates a new `Text` instance from the given content. + /// + /// # Argument + /// + /// * `content` - A value that can be converted into a `String`, representing the textual content. + pub fn from(content: impl Into) -> Self { + Self { + content: content.into(), + ..Default::default() + } + } + + /// Sets the font of the text. + /// + /// # Argument + /// + /// * `font` - A value that can be converted into a `String`, representing the font name. + pub fn font(mut self, font: impl Into) -> Self { + self.font = font.into(); + self + } + + /// Sets the size of the text. + /// + /// # Argument + /// + /// * `size` - A `usize` value specifying the font size. + pub fn size(mut self, size: usize) -> Self { + self.size = size; + self + } + + /// Sets the color of the text. + /// + /// # Argument + /// + /// * `color` - An `Rgb` value specifying the color of the text. + pub fn color(mut self, color: Rgb) -> Self { + self.color = color; + self + } + + /// Sets the x-coordinate position of the text. + /// + /// # Argument + /// + /// * `x` - A `f64` value specifying the horizontal position. + pub fn x(mut self, x: f64) -> Self { + self.x = x; + self + } + + /// Sets the y-coordinate position of the text. + /// + /// # Argument + /// + /// * `y` - A `f64` value specifying the vertical position. + pub fn y(mut self, y: f64) -> Self { + self.y = y; + self + } +} + +impl From<&str> for Text { + fn from(content: &str) -> Self { + Self::from(content.to_string()) + } +} + +impl From for Text { + fn from(content: String) -> Self { + Self::from(content) + } +} + +impl From<&String> for Text { + fn from(content: &String) -> Self { + Self::from(content) + } +} diff --git a/src/lib.rs b/src/lib.rs index 0299a22..7f85a5a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,28 +1,22 @@ #![doc = include_str!("../README.md")] -#![allow(clippy::needless_doctest_main, deprecated)] +#![allow(clippy::needless_doctest_main)] -#[macro_use] -mod macros; +mod common; +mod components; +mod plots; -mod aesthetics; -mod axis; -mod colors; -mod legend; -mod shapes; -mod texts; -mod traces; -mod traits; - -pub use crate::aesthetics::{line::LineType, orientation::Orientation}; -pub use crate::axis::{Axis, AxisSide, AxisType, TickDirection, ValueExponent}; -pub use crate::colors::Rgb; -pub use crate::legend::Legend; -pub use crate::shapes::Shape; -pub use crate::texts::Text; -pub use crate::traces::barplot::BarPlot; -pub use crate::traces::boxplot::BoxPlot; -pub use crate::traces::histogram::Histogram; -pub use crate::traces::lineplot::LinePlot; -pub use crate::traces::scatterplot::ScatterPlot; -pub use crate::traces::timeseriesplot::TimeSeriesPlot; -pub use crate::traits::plot::Plot; +pub use crate::common::plot::Plot; +pub use crate::components::axis::{Axis, AxisSide, AxisType, TickDirection}; +pub use crate::components::color::Rgb; +pub use crate::components::exponent::ValueExponent; +pub use crate::components::legend::Legend; +pub use crate::components::line::Line; +pub use crate::components::orientation::Orientation; +pub use crate::components::shape::Shape; +pub use crate::components::text::Text; +pub use crate::plots::barplot::BarPlot; +pub use crate::plots::boxplot::BoxPlot; +pub use crate::plots::histogram::Histogram; +pub use crate::plots::lineplot::LinePlot; +pub use crate::plots::scatterplot::ScatterPlot; +pub use crate::plots::timeseriesplot::TimeSeriesPlot; diff --git a/src/macros/marker.rs b/src/macros/marker.rs deleted file mode 100644 index 9c33cb6..0000000 --- a/src/macros/marker.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[doc(hidden)] -#[macro_export] -macro_rules! marker { - ( $( $x:ident ),* ) => {{ - let mut marker = plotly::common::Marker::new(); - $( - marker = marker.$x($x.unwrap()); - )* - marker - }}; -} diff --git a/src/macros/mod.rs b/src/macros/mod.rs deleted file mode 100644 index 5f41205..0000000 --- a/src/macros/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -#[macro_use] -mod marker; diff --git a/src/plots/barplot.rs b/src/plots/barplot.rs new file mode 100644 index 0000000..d380b0c --- /dev/null +++ b/src/plots/barplot.rs @@ -0,0 +1,309 @@ +use bon::bon; + +use plotly::{ + common::{ErrorData, ErrorType, Marker as MarkerPlotly}, + layout::BarMode, + Bar, Layout as LayoutPlotly, Trace, +}; + +use polars::frame::DataFrame; + +use crate::{ + common::{Layout, Marker, Plot, Polar}, + components::{Axis, Legend, Orientation, Rgb, Text}, +}; + +/// A structure representing a bar plot. +/// +/// The `BarPlot` struct allows for the creation and customization of bar plots with various options +/// for data, layout, and aesthetics. It supports both vertical and horizontal orientations, grouping +/// of data, error bars, and customizable markers and colors. +/// +/// # Arguments +/// +/// * `data` - A reference to the `DataFrame` containing the data to be plotted. +/// * `labels` - A string slice specifying the column name to be used for the x-axis (independent variable). +/// * `values` - A string slice specifying the column name to be used for the y-axis (dependent variable). +/// * `orientation` - An optional `Orientation` enum specifying whether the plot should be horizontal or vertical. +/// * `group` - An optional string slice specifying the column name to be used for grouping data points. +/// * `error` - An optional string slice specifying the column name containing error values for the y-axis data. +/// * `color` - An optional `Rgb` value specifying the color of the markers to be used for the plot. This is used when `group` is not specified. +/// * `colors` - An optional vector of `Rgb` values specifying the colors to be used for the plot. This is used when `group` is specified to differentiate between groups. +/// * `plot_title` - An optional `Text` struct specifying the title of the plot. +/// * `x_title` - An optional `Text` struct specifying the title of the x-axis. +/// * `y_title` - An optional `Text` struct specifying the title of the y-axis. +/// * `legend_title` - An optional `Text` struct specifying the title of the legend. +/// * `x_axis` - An optional reference to an `Axis` struct for customizing the x-axis. +/// * `y_axis` - An optional reference to an `Axis` struct for customizing the y-axis. +/// * `legend` - An optional reference to a `Legend` struct for customizing the legend of the plot (e.g., positioning, font, etc.). +/// +/// # Example +/// +/// ```rust +/// use plotlars::{BarPlot, Legend, Orientation, Plot, Rgb, Text}; +/// +/// let animal = vec![ +/// "giraffe", +/// "giraffe", +/// "orangutan", +/// "orangutan", +/// "monkey", +/// "monkey", +/// ]; +/// +/// let gender = vec!["female", "male", "female", "male", "female", "male"]; +/// let value = vec![20.0f32, 25.0, 14.0, 18.0, 23.0, 31.0]; +/// let error = vec![1.0, 0.5, 1.5, 1.0, 0.5, 1.5]; +/// +/// let dataset = DataFrame::new(vec![ +/// Series::new("animal".into(), animal), +/// Series::new("gender".into(), gender), +/// Series::new("value".into(), value), +/// Series::new("error".into(), error), +/// ]) +/// .unwrap(); +/// +/// BarPlot::builder() +/// .data(&dataset) +/// .labels("animal") +/// .values("value") +/// .orientation(Orientation::Vertical) +/// .group("gender") +/// .error("error") +/// .colors(vec![ +/// Rgb(255, 127, 80), +/// Rgb(64, 224, 208), +/// ]) +/// .plot_title( +/// Text::from("Bar Plot") +/// .font("Arial") +/// .size(18) +/// ) +/// .x_title( +/// Text::from("animal") +/// .font("Arial") +/// .size(15) +/// ) +/// .y_title( +/// Text::from("value") +/// .font("Arial") +/// .size(15) +/// ) +/// .legend_title( +/// Text::from("gender") +/// .font("Arial") +/// .size(15) +/// ) +/// .legend( +/// &Legend::new() +/// .orientation(Orientation::Horizontal) +/// .y(1.0) +/// .x(0.4) +/// ) +/// .build() +/// .plot(); +/// ``` +/// +/// ![Example](https://imgur.com/2alZlO5.png) +pub struct BarPlot { + traces: Vec>, + layout: LayoutPlotly, +} + +#[bon] +impl BarPlot { + #[builder(on(String, into), on(Text, into))] + pub fn new( + data: &DataFrame, + labels: &str, + values: &str, + orientation: Option, + group: Option<&str>, + error: Option<&str>, + color: Option, + colors: Option>, + plot_title: Option, + x_title: Option, + y_title: Option, + legend_title: Option, + x_axis: Option<&Axis>, + y_axis: Option<&Axis>, + legend: Option<&Legend>, + ) -> Self { + let mut layout = Self::create_layout( + plot_title, + x_title, + y_title, + legend_title, + x_axis, + y_axis, + legend, + ); + + layout = layout.bar_mode(BarMode::Group); + + let traces = Self::create_traces( + data, + labels, + values, + orientation, + group, + error, + color, + colors, + ); + + Self { traces, layout } + } + + #[allow(clippy::too_many_arguments)] + fn create_traces( + data: &DataFrame, + labels: &str, + values: &str, + orientation: Option, + group: Option<&str>, + error: Option<&str>, + color: Option, + colors: Option>, + ) -> Vec> { + let mut traces: Vec> = Vec::new(); + + let opacity = None; + let size = None; + let shape = None; + let shapes = None; + + match group { + Some(group_col) => { + let groups = Self::get_unique_groups(data, group_col); + + let groups = groups.iter().map(|s| s.as_str()); + + for (i, group) in groups.enumerate() { + let marker = Self::create_marker( + i, + opacity, + size, + color, + colors.clone(), + shape, + shapes.clone(), + ); + + let subset = Self::filter_data_by_group(data, group_col, group); + + let trace = Self::create_trace( + &subset, + labels, + values, + orientation.clone(), + Some(group), + error, + marker, + ); + + traces.push(trace); + } + } + None => { + let group = None; + + let marker = Self::create_marker( + 0, + opacity, + size, + color, + colors.clone(), + shape, + shapes.clone(), + ); + + let trace = + Self::create_trace(data, labels, values, orientation, group, error, marker); + + traces.push(trace); + } + } + + traces + } + + fn create_trace( + data: &DataFrame, + labels: &str, + values: &str, + orientation: Option, + group: Option<&str>, + error: Option<&str>, + marker: MarkerPlotly, + ) -> Box { + let values = Self::get_numeric_column(data, values); + let labels = Self::get_string_column(data, labels); + + let orientation = orientation.unwrap_or(Orientation::Vertical); + + match orientation { + Orientation::Vertical => { + let mut trace = Bar::default() + .x(labels) + .y(values) + .orientation(orientation.get_orientation()); + + if let Some(error) = error { + let error = Self::get_numeric_column(data, error) + .iter() + .map(|x| x.unwrap() as f64) + .collect::>(); + + trace = trace.error_y(ErrorData::new(ErrorType::Data).array(error)) + } + + trace = trace.marker(marker); + + if let Some(group) = group { + trace = trace.name(group); + } + + trace + } + Orientation::Horizontal => { + let mut trace = Bar::default() + .x(values) + .y(labels) + .orientation(orientation.get_orientation()); + + if let Some(error) = error { + let error = Self::get_numeric_column(data, error) + .iter() + .map(|x| x.unwrap() as f64) + .collect::>(); + + trace = trace.error_x(ErrorData::new(ErrorType::Data).array(error)) + } + + trace = trace.marker(marker); + + if let Some(group) = group { + trace = trace.name(group); + } + + trace + } + } + } +} + +impl Layout for BarPlot {} +impl Marker for BarPlot {} +impl Polar for BarPlot {} + +impl Plot for BarPlot { + fn get_layout(&self) -> &LayoutPlotly { + &self.layout + } + + fn get_traces(&self) -> &Vec> { + &self.traces + } +} diff --git a/src/plots/boxplot.rs b/src/plots/boxplot.rs new file mode 100644 index 0000000..bdf98dc --- /dev/null +++ b/src/plots/boxplot.rs @@ -0,0 +1,346 @@ +use bon::bon; + +use plotly::{ + box_plot::BoxPoints, common::Marker as MarkerPlotly, layout::BoxMode, BoxPlot as BoxPlotly, + Layout as LayoutPlotly, Trace, +}; + +use polars::frame::DataFrame; + +use crate::{ + common::{Layout, Marker, Plot, Polar}, + components::{Axis, Legend, Orientation, Rgb, Text}, +}; + +/// A structure representing a box plot. +/// +/// The `BoxPlot` struct facilitates the creation and customization of box plots with various options +/// for data selection, layout configuration, and aesthetic adjustments. It supports both horizontal +/// and vertical orientations, grouping of data, display of individual data points with jitter and offset, +/// opacity settings, and customizable markers and colors. +/// +/// # Arguments +/// +/// * `data` - A reference to the `DataFrame` containing the data to be plotted. +/// * `labels` - A string slice specifying the column name to be used for the x-axis (independent variable). +/// * `values` - A string slice specifying the column name to be used for the y-axis (dependent variable). +/// * `orientation` - An optional `Orientation` enum specifying whether the plot should be horizontal or vertical. +/// * `group` - An optional string slice specifying the column name to be used for grouping data points. +/// * `box_points` - An optional boolean indicating whether individual data points should be plotted along with the box plot. +/// * `point_offset` - An optional `f64` value specifying the horizontal offset for individual data points when `box_points` is enabled. +/// * `jitter` - An optional `f64` value indicating the amount of jitter (random noise) to apply to individual data points for visibility. +/// * `opacity` - An optional `f64` value specifying the opacity of the plot markers (range: 0.0 to 1.0). +/// * `color` - An optional `Rgb` value specifying the color of the markers to be used for the plot. This is used when `group` is not specified. +/// * `colors` - An optional vector of `Rgb` values specifying the colors to be used for the plot. This is used when `group` is specified to differentiate between groups. +/// * `plot_title` - An optional `Text` struct specifying the title of the plot. +/// * `x_title` - An optional `Text` struct specifying the title of the x-axis. +/// * `y_title` - An optional `Text` struct specifying the title of the y-axis. +/// * `legend_title` - An optional `Text` struct specifying the title of the legend. +/// * `x_axis` - An optional reference to an `Axis` struct for customizing the x-axis. +/// * `y_axis` - An optional reference to an `Axis` struct for customizing the y-axis. +/// * `legend` - An optional reference to a `Legend` struct for customizing the legend of the plot (e.g., positioning, font, etc.). +/// +/// # Example +/// +/// ```rust +/// use plotlars::{Axis, BoxPlot, Legend, Orientation, Plot, Rgb, Text}; +/// +/// let dataset = LazyCsvReader::new("data/penguins.csv") +/// .finish() +/// .unwrap() +/// .select([ +/// col("species"), +/// col("sex").alias("gender"), +/// col("flipper_length_mm").cast(DataType::Int16), +/// col("body_mass_g").cast(DataType::Int16), +/// ]) +/// .collect() +/// .unwrap(); +/// +/// BoxPlot::builder() +/// .data(&dataset) +/// .labels("species") +/// .values("body_mass_g") +/// .orientation(Orientation::Vertical) +/// .group("gender") +/// .box_points(true) +/// .point_offset(-1.5) +/// .jitter(0.01) +/// .opacity(0.1) +/// .colors(vec![ +/// Rgb(0, 191, 255), +/// Rgb(57, 255, 20), +/// Rgb(255, 105, 180), +/// ]) +/// .plot_title( +/// Text::from("Box Plot") +/// .font("Arial") +/// .size(18) +/// ) +/// .x_title( +/// Text::from("species") +/// .font("Arial") +/// .size(15) +/// ) +/// .y_title( +/// Text::from("body mass (g)") +/// .font("Arial") +/// .size(15) +/// ) +/// .legend_title( +/// Text::from("gender") +/// .font("Arial") +/// .size(15) +/// ) +/// .y_axis( +/// &Axis::new() +/// .value_thousands(true) +/// ) +/// .legend( +/// &Legend::new() +/// .border_width(1) +/// .x(0.9) +/// ) +/// .build() +/// .plot(); +/// ``` +/// +/// ![Example](https://imgur.com/uj1LY90.png) +pub struct BoxPlot { + traces: Vec>, + layout: LayoutPlotly, +} + +#[bon] +impl BoxPlot { + #[builder(on(String, into), on(Text, into))] + pub fn new( + data: &DataFrame, + labels: &str, + values: &str, + orientation: Option, + group: Option<&str>, + box_points: Option, + point_offset: Option, + jitter: Option, + opacity: Option, + color: Option, + colors: Option>, + plot_title: Option, + x_title: Option, + y_title: Option, + legend_title: Option, + x_axis: Option<&Axis>, + y_axis: Option<&Axis>, + legend: Option<&Legend>, + ) -> Self { + let mut layout = Self::create_layout( + plot_title, + x_title, + y_title, + legend_title, + x_axis, + y_axis, + legend, + ); + + layout = layout.box_mode(BoxMode::Group); + + let traces = Self::create_traces( + data, + labels, + values, + orientation, + group, + box_points, + point_offset, + jitter, + opacity, + color, + colors, + ); + + Self { traces, layout } + } + + #[allow(clippy::too_many_arguments)] + fn create_traces( + data: &DataFrame, + labels: &str, + values: &str, + orientation: Option, + group: Option<&str>, + box_points: Option, + point_offset: Option, + jitter: Option, + opacity: Option, + color: Option, + colors: Option>, + ) -> Vec> { + let mut traces: Vec> = Vec::new(); + + let size = None; + let shape = None; + let shapes = None; + + match group { + Some(group_col) => { + let groups = Self::get_unique_groups(data, group_col); + + let groups = groups.iter().map(|s| s.as_str()); + + for (i, group) in groups.enumerate() { + let marker = Self::create_marker( + i, + opacity, + size, + color, + colors.clone(), + shape, + shapes.clone(), + ); + + let subset = Self::filter_data_by_group(data, group_col, group); + + let trace = Self::create_trace( + &subset, + labels, + values, + orientation.clone(), + Some(group), + box_points, + point_offset, + jitter, + marker, + ); + + traces.push(trace); + } + } + None => { + let group = None; + + let marker = Self::create_marker( + 0, + opacity, + size, + color, + colors.clone(), + shape, + shapes.clone(), + ); + + let trace = Self::create_trace( + data, + labels, + values, + orientation, + group, + box_points, + point_offset, + jitter, + marker, + ); + + traces.push(trace); + } + } + + traces + } + + #[allow(clippy::too_many_arguments)] + fn create_trace( + data: &DataFrame, + labels: &str, + values: &str, + orientation: Option, + group_name: Option<&str>, + box_points: Option, + point_offset: Option, + jitter: Option, + marker: MarkerPlotly, + ) -> Box { + let category_data = Self::get_string_column(data, labels); + let value_data = Self::get_numeric_column(data, values); + + let orientation = orientation.unwrap_or(Orientation::Vertical); + + match orientation { + Orientation::Vertical => { + let mut trace = BoxPlotly::default() + .x(category_data) + .y(value_data) + .orientation(orientation.get_orientation()); + + if let Some(all) = box_points { + if all { + trace = trace.box_points(BoxPoints::All); + } else { + trace = trace.box_points(BoxPoints::False); + } + } + + if let Some(point_position) = point_offset { + trace = trace.point_pos(point_position); + } + + if let Some(jitter) = jitter { + trace = trace.jitter(jitter); + } + + trace = trace.marker(marker); + + if let Some(name) = group_name { + trace = trace.name(name); + } + + trace + } + Orientation::Horizontal => { + let mut trace = BoxPlotly::default() + .x(value_data) + .y(category_data) + .orientation(orientation.get_orientation()); + + if let Some(all) = box_points { + if all { + trace = trace.box_points(BoxPoints::All); + } else { + trace = trace.box_points(BoxPoints::False); + } + } + + if let Some(point_position) = point_offset { + trace = trace.point_pos(point_position); + } + + if let Some(jitter) = jitter { + trace = trace.jitter(jitter); + } + + trace = trace.marker(marker); + + if let Some(name) = group_name { + trace = trace.name(name); + } + + trace + } + } + } +} + +impl Layout for BoxPlot {} +impl Marker for BoxPlot {} +impl Polar for BoxPlot {} + +impl Plot for BoxPlot { + fn get_layout(&self) -> &LayoutPlotly { + &self.layout + } + + fn get_traces(&self) -> &Vec> { + &self.traces + } +} diff --git a/src/plots/histogram.rs b/src/plots/histogram.rs new file mode 100644 index 0000000..c4e89f3 --- /dev/null +++ b/src/plots/histogram.rs @@ -0,0 +1,233 @@ +use bon::bon; + +use plotly::{ + common::Marker as MarkerPlotly, histogram::HistFunc, layout::BarMode, + Histogram as HistogramPlotly, Layout as LayoutPlotly, Trace, +}; + +use polars::frame::DataFrame; + +use crate::{ + common::{Layout, Marker, Plot, Polar}, + components::{Axis, Legend, Rgb, Text}, +}; + +/// A structure representing a histogram. +/// +/// The `Histogram` struct facilitates the creation and customization of histograms with various options +/// for data selection, layout configuration, and aesthetic adjustments. It supports grouping of data, +/// opacity settings, and customizable markers and colors. +/// +/// # Arguments +/// +/// * `data` - A reference to the `DataFrame` containing the data to be plotted. +/// * `x` - A string slice specifying the column name to be used for the x-axis (independent variable). +/// * `group` - An optional string slice specifying the column name to be used for grouping data points. +/// * `opacity` - An optional `f64` value specifying the opacity of the plot markers (range: 0.0 to 1.0). +/// * `color` - An optional `Rgb` value specifying the color of the markers to be used for the plot. This is used when `group` is not specified. +/// * `colors` - An optional vector of `Rgb` values specifying the colors to be used for the plot. This is used when `group` is specified to differentiate between groups. +/// * `plot_title` - An optional `Text` struct specifying the title of the plot. +/// * `x_title` - An optional `Text` struct specifying the title of the x-axis. +/// * `y_title` - An optional `Text` struct specifying the title of the y-axis. +/// * `legend_title` - An optional `Text` struct specifying the title of the legend. +/// * `x_axis` - An optional reference to an `Axis` struct for customizing the x-axis. +/// * `y_axis` - An optional reference to an `Axis` struct for customizing the y-axis. +/// * `legend` - An optional reference to a `Legend` struct for customizing the legend of the plot (e.g., positioning, font, etc.). +/// +/// # Example +/// +/// ```rust +/// use plotlars::{Axis, Histogram, Legend, Plot, Rgb, Text, TickDirection}; +/// +/// let dataset = LazyCsvReader::new("data/penguins.csv") +/// .finish() +/// .unwrap() +/// .select([ +/// col("species"), +/// col("sex").alias("gender"), +/// col("flipper_length_mm").cast(DataType::Int16), +/// col("body_mass_g").cast(DataType::Int16), +/// ]) +/// .collect() +/// .unwrap(); +/// +/// let axis = Axis::new() +/// .show_line(true) +/// .show_grid(true) +/// .value_thousands(true) +/// .tick_direction(TickDirection::OutSide); +/// +/// Histogram::builder() +/// .data(&dataset) +/// .x("body_mass_g") +/// .group("species") +/// .opacity(0.5) +/// .colors(vec![ +/// Rgb(255, 165, 0), +/// Rgb(147, 112, 219), +/// Rgb(46, 139, 87), +/// ]) +/// .plot_title( +/// Text::from("Histogram") +/// .font("Arial") +/// .size(18) +/// ) +/// .x_title( +/// Text::from("body mass (g)") +/// .font("Arial") +/// .size(15) +/// ) +/// .y_title( +/// Text::from("count") +/// .font("Arial") +/// .size(15) +/// ) +/// .legend_title( +/// Text::from("species") +/// .font("Arial") +/// .size(15) +/// ) +/// .x_axis(&axis) +/// .y_axis(&axis) +/// .legend( +/// &Legend::new() +/// .x(0.9) +/// ) +/// .build() +/// .plot(); +/// ``` +/// +/// ![Example](https://imgur.com/w2oiuIo.png) +pub struct Histogram { + traces: Vec>, + layout: LayoutPlotly, +} + +#[bon] +impl Histogram { + #[builder(on(String, into), on(Text, into))] + pub fn new( + data: &DataFrame, + x: &str, + group: Option<&str>, + opacity: Option, + color: Option, + colors: Option>, + plot_title: Option, + x_title: Option, + y_title: Option, + legend_title: Option, + x_axis: Option<&Axis>, + y_axis: Option<&Axis>, + legend: Option<&Legend>, + ) -> Self { + let mut layout = Self::create_layout( + plot_title, + x_title, + y_title, + legend_title, + x_axis, + y_axis, + legend, + ); + + layout = layout.bar_mode(BarMode::Overlay); + + let traces = Self::create_traces(data, x, group, opacity, color, colors); + + Self { traces, layout } + } + + fn create_traces( + data: &DataFrame, + x: &str, + group: Option<&str>, + opacity: Option, + color: Option, + colors: Option>, + ) -> Vec> { + let mut traces: Vec> = Vec::new(); + + let size = None; + let shape = None; + let shapes = None; + + match group { + Some(group_col) => { + let groups = Self::get_unique_groups(data, group_col); + + let groups = groups.iter().map(|s| s.as_str()); + + for (i, group) in groups.enumerate() { + let marker = Self::create_marker( + i, + opacity, + size, + color, + colors.clone(), + shape, + shapes.clone(), + ); + + let subset = Self::filter_data_by_group(data, group_col, group); + + let trace = Self::create_trace(&subset, x, Some(group), marker); + + traces.push(trace); + } + } + None => { + let group = None; + + let marker = Self::create_marker( + 0, + opacity, + size, + color, + colors.clone(), + shape, + shapes.clone(), + ); + + let trace = Self::create_trace(data, x, group, marker); + + traces.push(trace); + } + } + + traces + } + + fn create_trace( + data: &DataFrame, + x: &str, + group_name: Option<&str>, + marker: MarkerPlotly, + ) -> Box { + let x = Self::get_numeric_column(data, x); + + let mut trace = HistogramPlotly::default().x(x).hist_func(HistFunc::Count); + + trace = trace.marker(marker); + + if let Some(name) = group_name { + trace = trace.name(name); + } + + trace + } +} + +impl Layout for Histogram {} +impl Marker for Histogram {} +impl Polar for Histogram {} + +impl Plot for Histogram { + fn get_layout(&self) -> &LayoutPlotly { + &self.layout + } + + fn get_traces(&self) -> &Vec> { + &self.traces + } +} diff --git a/src/plots/lineplot.rs b/src/plots/lineplot.rs new file mode 100644 index 0000000..46471ee --- /dev/null +++ b/src/plots/lineplot.rs @@ -0,0 +1,290 @@ +use bon::bon; + +use plotly::{ + common::{Line as LinePlotly, Marker as MarkerPlotly, Mode}, + Layout as LayoutPlotly, Scatter, Trace, +}; + +use polars::{ + frame::DataFrame, + prelude::{col, IntoLazy}, +}; + +use crate::{ + common::{Layout, Line, Marker, Plot, Polar}, + components::{Axis, Legend, Line as LineStyle, Rgb, Shape, Text}, +}; + +/// A structure representing a line plot. +/// +/// The `LinePlot` struct facilitates the creation and customization of line plots with various options +/// for data selection, layout configuration, and aesthetic adjustments. It supports the addition of multiple +/// lines, customization of marker shapes, line styles, colors, opacity settings, and comprehensive layout +/// customization including titles, axes, and legends. +/// +/// # Arguments +/// +/// * `data` - A reference to the `DataFrame` containing the data to be plotted. +/// * `x` - A string slice specifying the column name to be used for the x-axis (independent variable). +/// * `y` - A string slice specifying the column name to be used for the y-axis (dependent variable). +/// * `additional_lines` - An optional vector of string slices specifying additional y-axis columns to be plotted as lines. +/// * `size` - An optional `usize` specifying the size of the markers or the thickness of the lines. +/// * `color` - An optional `Rgb` value specifying the color of the markers and lines. This is used when `additional_lines` is not specified. +/// * `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. +/// * `with_shape` - An optional `bool` indicating whether to display markers with shapes on the plot. +/// * `shape` - An optional `Shape` specifying the shape of the markers. +/// * `shapes` - An optional vector of `Shape` values specifying multiple shapes for the markers when plotting multiple lines. +/// * `width` - An optional `f64` specifying the width of the plotted lines. +/// * `line` - An optional `Line` specifying the type of the line (e.g., solid, dashed). This is used when `additional_lines` is not specified. +/// * `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. +/// * `plot_title` - An optional `Text` struct specifying the title of the plot. +/// * `x_title` - An optional `Text` struct specifying the title of the x-axis. +/// * `y_title` - An optional `Text` struct specifying the title of the y-axis. +/// * `legend_title` - An optional `Text` struct specifying the title of the legend. +/// * `x_axis` - An optional reference to an `Axis` struct for customizing the x-axis. +/// * `y_axis` - An optional reference to an `Axis` struct for customizing the y-axis. +/// * `legend` - An optional reference to a `Legend` struct for customizing the legend of the plot (e.g., positioning, font, etc.). +/// +/// # Example +/// +/// ```rust +/// use ndarray::Array; +/// +/// use plotlars::{Axis, Line, LinePlot, Plot, Rgb, Text, TickDirection}; +/// +/// let x_values: Array = Array::linspace(0.0, 2.0 * std::f64::consts::PI, 1000); +/// let sine_values = x_values.mapv(f64::sin).to_vec(); +/// let cosine_values = x_values.mapv(f64::cos).to_vec(); +/// let x_values = x_values.to_vec(); +/// +/// let dataset = DataFrame::new(vec![ +/// Series::new("x".into(), x_values), +/// Series::new("sine".into(), sine_values), +/// Series::new("cosine".into(), cosine_values), +/// ]) +/// .unwrap(); +/// +/// LinePlot::builder() +/// .data(&dataset) +/// .x("x") +/// .y("sine") +/// .additional_lines(vec!["cosine"]) +/// .colors(vec![ +/// Rgb(255, 0, 0), +/// Rgb(0, 255, 0), +/// ]) +/// .lines(vec![Line::Solid, Line::Dot]) +/// .width(3.0) +/// .with_shape(false) +/// .plot_title( +/// Text::from("Line Plot") +/// .font("Arial") +/// .size(18) +/// ) +/// .legend_title( +/// Text::from("series") +/// .font("Arial") +/// .size(15) +/// ) +/// .x_axis( +/// &Axis::new() +/// .tick_direction(TickDirection::OutSide) +/// .axis_position(0.5) +/// .tick_values(vec![ +/// 0.5 * std::f64::consts::PI, +/// std::f64::consts::PI, +/// 1.5 * std::f64::consts::PI, +/// 2.0 * std::f64::consts::PI, +/// ]) +/// .tick_labels(vec!["π/2", "π", "3π/2", "2π"]) +/// ) +/// .y_axis( +/// &Axis::new() +/// .tick_direction(TickDirection::OutSide) +/// .tick_values(vec![-1.0, 0.0, 1.0]) +/// .tick_labels(vec!["-1", "0", "1"]) +/// ) +/// .build() +/// .plot(); +/// ``` +/// +/// ![Example](https://imgur.com/PaXG300.png) +pub struct LinePlot { + traces: Vec>, + layout: LayoutPlotly, +} + +#[bon] +impl LinePlot { + #[builder(on(String, into), on(Text, into))] + pub fn new( + data: &DataFrame, + x: &str, + y: &str, + additional_lines: Option>, + size: Option, + color: Option, + colors: Option>, + with_shape: Option, + shape: Option, + shapes: Option>, + width: Option, + line: Option, + lines: Option>, + plot_title: Option, + x_title: Option, + y_title: Option, + legend_title: Option, + x_axis: Option<&Axis>, + y_axis: Option<&Axis>, + legend: Option<&Legend>, + ) -> Self { + let layout = Self::create_layout( + plot_title, + x_title, + y_title, + legend_title, + x_axis, + y_axis, + legend, + ); + + let traces = Self::create_traces( + data, + x, + y, + additional_lines, + size, + color, + colors, + with_shape, + shape, + shapes, + width, + line, + lines, + ); + + Self { traces, layout } + } + + #[allow(clippy::too_many_arguments)] + fn create_traces( + data: &DataFrame, + x_col: &str, + y_col: &str, + additional_lines: Option>, + size: Option, + color: Option, + colors: Option>, + with_shape: Option, + shape: Option, + shapes: Option>, + width: Option, + style: Option, + styles: Option>, + ) -> Vec> { + let mut traces: Vec> = Vec::new(); + + let opacity = None; + + let marker = Self::create_marker( + 0, + opacity, + size, + color, + colors.clone(), + shape, + shapes.clone(), + ); + + let line = Self::create_line(0, width, style, styles.clone()); + + let name = Some(y_col); + + let trace = Self::create_trace(data, x_col, y_col, name, with_shape, marker, line); + + traces.push(trace); + + if let Some(additional_lines) = additional_lines { + let additional_lines = additional_lines.into_iter(); + + for (i, series) in additional_lines.enumerate() { + let marker = Self::create_marker( + i + 1, + opacity, + size, + color, + colors.clone(), + shape, + shapes.clone(), + ); + + let line = Self::create_line(i + 1, width, style, styles.clone()); + + let subset = data + .clone() + .lazy() + .select([col(x_col), col(series)]) + .collect() + .unwrap(); + + let name = Some(series); + + let trace = + Self::create_trace(&subset, x_col, series, name, with_shape, marker, line); + + traces.push(trace); + } + } + + traces + } + + fn create_trace( + data: &DataFrame, + x_col: &str, + y_col: &str, + name: Option<&str>, + with_shape: Option, + marker: MarkerPlotly, + line: LinePlotly, + ) -> Box { + let x_data = Self::get_numeric_column(data, x_col); + let y_data = Self::get_numeric_column(data, y_col); + + let mut trace = Scatter::default().x(x_data).y(y_data); + + if let Some(with_shape) = with_shape { + if with_shape { + trace = trace.mode(Mode::LinesMarkers); + } else { + trace = trace.mode(Mode::Lines); + } + } + + trace = trace.marker(marker); + trace = trace.line(line); + + if let Some(name) = name { + trace = trace.name(name); + } + + trace + } +} + +impl Layout for LinePlot {} +impl Line for LinePlot {} +impl Marker for LinePlot {} +impl Polar for LinePlot {} + +impl Plot for LinePlot { + fn get_layout(&self) -> &LayoutPlotly { + &self.layout + } + + fn get_traces(&self) -> &Vec> { + &self.traces + } +} diff --git a/src/traces/mod.rs b/src/plots/mod.rs similarity index 100% rename from src/traces/mod.rs rename to src/plots/mod.rs diff --git a/src/plots/scatterplot.rs b/src/plots/scatterplot.rs new file mode 100644 index 0000000..0dc42fd --- /dev/null +++ b/src/plots/scatterplot.rs @@ -0,0 +1,248 @@ +use bon::bon; + +use plotly::{ + common::{Marker as MarkerPlotly, Mode}, + Layout as LayoutPlotly, Scatter, Trace, +}; + +use polars::frame::DataFrame; + +use crate::{ + common::{Layout, Line, Marker, Plot, Polar}, + components::{Axis, Legend, Rgb, Shape, Text}, +}; + +/// A structure representing a scatter plot. +/// +/// The `ScatterPlot` struct facilitates the creation and customization of scatter plots with various options +/// for data selection, grouping, layout configuration, and aesthetic adjustments. It supports grouping of data, +/// customization of marker shapes, colors, sizes, opacity settings, and comprehensive layout customization +/// including titles, axes, and legends. +/// +/// # Arguments +/// +/// * `data` - A reference to the `DataFrame` containing the data to be plotted. +/// * `x` - A string slice specifying the column name to be used for the x-axis (independent variable). +/// * `y` - A string slice specifying the column name to be used for the y-axis (dependent variable). +/// * `group` - An optional string slice specifying the column name to be used for grouping data points. +/// * `opacity` - An optional `f64` value specifying the opacity of the plot markers (range: 0.0 to 1.0). +/// * `size` - An optional `usize` specifying the size of the markers. +/// * `color` - An optional `Rgb` value specifying the color of the markers. This is used when `group` is not specified. +/// * `colors` - An optional vector of `Rgb` values specifying the colors for the markers. This is used when `group` is specified to differentiate between groups. +/// * `shape` - An optional `Shape` specifying the shape of the markers. This is used when `group` is not specified. +/// * `shapes` - An optional vector of `Shape` values specifying multiple shapes for the markers when plotting multiple groups. +/// * `plot_title` - An optional `Text` struct specifying the title of the plot. +/// * `x_title` - An optional `Text` struct specifying the title of the x-axis. +/// * `y_title` - An optional `Text` struct specifying the title of the y-axis. +/// * `legend_title` - An optional `Text` struct specifying the title of the legend. +/// * `x_axis` - An optional reference to an `Axis` struct for customizing the x-axis. +/// * `y_axis` - An optional reference to an `Axis` struct for customizing the y-axis. +/// * `legend` - An optional reference to a `Legend` struct for customizing the legend of the plot (e.g., positioning, font, etc.). +/// +/// # Example +/// +/// ```rust +/// use plotlars::{Axis, Legend, Plot, Rgb, ScatterPlot, Shape, Text, TickDirection}; +/// +/// let dataset = LazyCsvReader::new("data/penguins.csv") +/// .finish() +/// .unwrap() +/// .select([ +/// col("species"), +/// col("sex").alias("gender"), +/// col("flipper_length_mm").cast(DataType::Int16), +/// col("body_mass_g").cast(DataType::Int16), +/// ]) +/// .collect() +/// .unwrap(); +/// +/// let axis = Axis::new() +/// .show_line(true) +/// .tick_direction(TickDirection::OutSide) +/// .value_thousands(true); +/// +/// ScatterPlot::builder() +/// .data(&dataset) +/// .x("body_mass_g") +/// .y("flipper_length_mm") +/// .group("species") +/// .opacity(0.5) +/// .size(12) +/// .colors(vec![ +/// Rgb(178, 34, 34), +/// Rgb(65, 105, 225), +/// Rgb(255, 140, 0), +/// ]) +/// .shapes(vec![ +/// Shape::Circle, +/// Shape::Square, +/// Shape::Diamond, +/// ]) +/// .plot_title( +/// Text::from("Scatter Plot") +/// .font("Arial") +/// .size(20) +/// .x(0.065) +/// ) +/// .x_title("body mass (g)") +/// .y_title("flipper length (mm)") +/// .legend_title("species") +/// .x_axis( +/// &axis.clone() +/// .value_range(vec![2500.0, 6500.0]) +/// ) +/// .y_axis( +/// &axis.clone() +/// .value_range(vec![170.0, 240.0]) +/// ) +/// .legend( +/// &Legend::new() +/// .x(0.85) +/// .y(0.15) +/// ) +/// .build() +/// .plot(); +/// ``` +/// +/// ![Example](https://imgur.com/9jfO8RU.png) +pub struct ScatterPlot { + traces: Vec>, + layout: LayoutPlotly, +} + +#[bon] +impl ScatterPlot { + #[builder(on(String, into), on(Text, into))] + pub fn new( + data: &DataFrame, + x: &str, + y: &str, + group: Option<&str>, + opacity: Option, + size: Option, + color: Option, + colors: Option>, + shape: Option, + shapes: Option>, + plot_title: Option, + x_title: Option, + y_title: Option, + legend_title: Option, + x_axis: Option<&Axis>, + y_axis: Option<&Axis>, + legend: Option<&Legend>, + ) -> Self { + let layout = Self::create_layout( + plot_title, + x_title, + y_title, + legend_title, + x_axis, + y_axis, + legend, + ); + + let traces = Self::create_traces( + data, x, y, group, opacity, size, color, colors, shape, shapes, + ); + + Self { traces, layout } + } + + #[allow(clippy::too_many_arguments)] + fn create_traces( + data: &DataFrame, + x: &str, + y: &str, + group: Option<&str>, + opacity: Option, + size: Option, + color: Option, + colors: Option>, + shape: Option, + shapes: Option>, + ) -> Vec> { + let mut traces: Vec> = Vec::new(); + + match group { + Some(group_col) => { + let groups = Self::get_unique_groups(data, group_col); + + let groups = groups.iter().map(|s| s.as_str()); + + for (i, group) in groups.enumerate() { + let marker = Self::create_marker( + i, + opacity, + size, + color, + colors.clone(), + shape, + shapes.clone(), + ); + + let subset = Self::filter_data_by_group(data, group_col, group); + + let trace = Self::create_trace(&subset, x, y, Some(group), marker); + + traces.push(trace); + } + } + None => { + let group = None; + + let marker = Self::create_marker( + 0, + opacity, + size, + color, + colors.clone(), + shape, + shapes.clone(), + ); + + let trace = Self::create_trace(data, x, y, group, marker); + + traces.push(trace); + } + } + + traces + } + + fn create_trace( + data: &DataFrame, + x: &str, + y: &str, + group_name: Option<&str>, + marker: MarkerPlotly, + ) -> Box { + let x = Self::get_numeric_column(data, x); + let y = Self::get_numeric_column(data, y); + + let mut trace = Scatter::default().x(x).y(y).mode(Mode::Markers); + + trace = trace.marker(marker); + + if let Some(name) = group_name { + trace = trace.name(name); + } + + trace + } +} + +impl Layout for ScatterPlot {} +impl Line for ScatterPlot {} +impl Marker for ScatterPlot {} +impl Polar for ScatterPlot {} + +impl Plot for ScatterPlot { + fn get_layout(&self) -> &LayoutPlotly { + &self.layout + } + + fn get_traces(&self) -> &Vec> { + &self.traces + } +} diff --git a/src/plots/timeseriesplot.rs b/src/plots/timeseriesplot.rs new file mode 100644 index 0000000..5abe0f9 --- /dev/null +++ b/src/plots/timeseriesplot.rs @@ -0,0 +1,270 @@ +use bon::bon; + +use plotly::{ + common::{Line as LinePlotly, Marker as MarkerPlotly, Mode}, + Layout as LayoutPlotly, Scatter, Trace, +}; + +use polars::{ + frame::DataFrame, + prelude::{col, IntoLazy}, +}; + +use crate::{ + common::{Layout, Line, Marker, Plot, Polar}, + components::{Axis, Legend, Line as LineStyle, Rgb, Shape, Text}, +}; + +/// A structure representing a time series plot. +/// +/// The `TimeSeriesPlot` struct facilitates the creation and customization of time series plots with various options +/// for data selection, grouping, layout configuration, and aesthetic adjustments. It supports the addition of multiple +/// series, customization of marker shapes, colors, sizes, opacity settings, and comprehensive layout customization +/// including titles, axes, and legends. +/// +/// # Arguments +/// +/// * `data` - A reference to the `DataFrame` containing the data to be plotted. +/// * `x` - A string slice specifying the column name to be used for the x-axis, typically representing time or dates. +/// * `y` - A string slice specifying the column name to be used for the y-axis, typically representing the primary metric. +/// * `additional_series` - An optional vector of string slices specifying additional y-axis columns to be plotted as series. +/// * `size` - An optional `usize` specifying the size of the markers or line thickness. +/// * `color` - An optional `Rgb` value specifying the color of the markers. This is used when `group` is not specified. +/// * `colors` - An optional vector of `Rgb` values specifying the colors for the markers. This is used when `group` is specified to differentiate between groups. +/// * `with_shape` - An optional `bool` indicating whether to use shapes for markers in the plot. +/// * `shape` - An optional `Shape` specifying the shape of the markers. This is used when `group` is not specified. +/// * `shapes` - An optional vector of `Shape` values specifying multiple shapes for the markers when plotting multiple groups. +/// * `width` - An optional `f64` specifying the width of the plotted lines. +/// * `line` - An optional `LineStyle` specifying the style of the line. This is used when `additional_series` is not specified. +/// * `lines` - An optional vector of `LineStyle` enums specifying the styles of lines for each plotted series. This is used when `additional_series` is specified to differentiate between multiple series. +/// * `plot_title` - An optional `Text` struct specifying the title of the plot. +/// * `x_title` - An optional `Text` struct specifying the title of the x-axis. +/// * `y_title` - An optional `Text` struct specifying the title of the y-axis. +/// * `legend_title` - An optional `Text` struct specifying the title of the legend. +/// * `x_axis` - An optional reference to an `Axis` struct for customizing the x-axis. +/// * `y_axis` - An optional reference to an `Axis` struct for customizing the y-axis. +/// * `legend` - An optional reference to a `Legend` struct for customizing the legend of the plot (e.g., positioning, font, etc.). +/// +/// # Example +/// +/// ```rust +/// use plotlars::{Legend, Line, Plot, Rgb, Shape, Text, TimeSeriesPlot}; +/// +/// let dataset = LazyCsvReader::new("data/revenue_and_cost.csv") +/// .finish() +/// .unwrap() +/// .select([ +/// col("Date").cast(DataType::String), +/// col("Revenue").cast(DataType::Int32), +/// col("Cost").cast(DataType::Int32), +/// ]) +/// .collect() +/// .unwrap(); +/// +/// TimeSeriesPlot::builder() +/// .data(&dataset) +/// .x("Date") +/// .y("Revenue") +/// .additional_series(vec!["Cost"]) +/// .size(8) +/// .colors(vec![ +/// Rgb(255, 0, 0), +/// Rgb(0, 255, 0), +/// ]) +/// .lines(vec![Line::Dash, Line::Solid]) +/// .with_shape(true) +/// .shapes(vec![Shape::Circle, Shape::Square]) +/// .plot_title( +/// Text::from("Time Series Plot") +/// .font("Arial") +/// .size(18) +/// ) +/// .legend( +/// &Legend::new() +/// .x(0.05) +/// .y(0.9) +/// ) +/// .build() +/// .plot(); +/// ``` +/// +/// ![Example](https://imgur.com/1GaGFbk.png) +pub struct TimeSeriesPlot { + traces: Vec>, + layout: LayoutPlotly, +} + +#[bon] +impl TimeSeriesPlot { + #[builder(on(String, into), on(Text, into))] + pub fn new( + data: &DataFrame, + x: &str, + y: &str, + additional_series: Option>, + size: Option, + color: Option, + colors: Option>, + with_shape: Option, + shape: Option, + shapes: Option>, + width: Option, + line: Option, + lines: Option>, + plot_title: Option, + x_title: Option, + y_title: Option, + legend_title: Option, + x_axis: Option<&Axis>, + y_axis: Option<&Axis>, + legend: Option<&Legend>, + ) -> Self { + let layout = Self::create_layout( + plot_title, + x_title, + y_title, + legend_title, + x_axis, + y_axis, + legend, + ); + + let traces = Self::create_traces( + data, + x, + y, + additional_series, + size, + color, + colors, + with_shape, + shape, + shapes, + width, + line, + lines, + ); + + Self { traces, layout } + } + + #[allow(clippy::too_many_arguments)] + fn create_traces( + data: &DataFrame, + x_col: &str, + y_col: &str, + additional_series: Option>, + size: Option, + color: Option, + colors: Option>, + with_shape: Option, + shape: Option, + shapes: Option>, + width: Option, + style: Option, + styles: Option>, + ) -> Vec> { + let mut traces: Vec> = Vec::new(); + + let opacity = None; + + let marker = Self::create_marker( + 0, + opacity, + size, + color, + colors.clone(), + shape, + shapes.clone(), + ); + + let line = Self::create_line(0, width, style, styles.clone()); + + let name = Some(y_col); + + let trace = Self::create_trace(data, x_col, y_col, name, with_shape, marker, line); + + traces.push(trace); + + if let Some(additional_series) = additional_series { + let additional_series = additional_series.into_iter(); + + for (i, series) in additional_series.enumerate() { + let marker = Self::create_marker( + i + 1, + opacity, + size, + color, + colors.clone(), + shape, + shapes.clone(), + ); + + let line = Self::create_line(i + 1, width, style, styles.clone()); + + let subset = data + .clone() + .lazy() + .select([col(x_col), col(series)]) + .collect() + .unwrap(); + + let name = Some(series); + + let trace = + Self::create_trace(&subset, x_col, series, name, with_shape, marker, line); + + traces.push(trace); + } + } + + traces + } + + fn create_trace( + data: &DataFrame, + x_col: &str, + y_col: &str, + name: Option<&str>, + with_shape: Option, + marker: MarkerPlotly, + line: LinePlotly, + ) -> Box { + let x_data = Self::get_string_column(data, x_col); + let y_data = Self::get_numeric_column(data, y_col); + + let mut trace = Scatter::default().x(x_data).y(y_data); + + if let Some(with_shape) = with_shape { + if with_shape { + trace = trace.mode(Mode::LinesMarkers); + } else { + trace = trace.mode(Mode::Lines); + } + } + + trace = trace.marker(marker); + trace = trace.line(line); + + if let Some(name) = name { + trace = trace.name(name); + } + + trace + } +} + +impl Layout for TimeSeriesPlot {} +impl Line for TimeSeriesPlot {} +impl Marker for TimeSeriesPlot {} +impl Polar for TimeSeriesPlot {} + +impl Plot for TimeSeriesPlot { + fn get_layout(&self) -> &LayoutPlotly { + &self.layout + } + + fn get_traces(&self) -> &Vec> { + &self.traces + } +} diff --git a/src/texts/mod.rs b/src/texts/mod.rs deleted file mode 100644 index 75b59c0..0000000 --- a/src/texts/mod.rs +++ /dev/null @@ -1,154 +0,0 @@ -use crate::colors::Rgb; - -/// A structure representing text with customizable content, font, size, and color. -pub struct Text { - pub(crate) content: String, - pub(crate) font: String, - pub(crate) size: usize, - pub(crate) color: Rgb, - pub(crate) x: f64, - pub(crate) y: f64, -} - -impl Default for Text { - fn default() -> Self { - Text { - content: String::new(), - font: String::new(), - size: 0, - color: Rgb::default(), - x: 0.5, - y: 0.9, - } - } -} - -impl Text { - /// Creates a new `Text` instance from the given content. - /// - /// # Arguments - /// - /// * `content` - A value that can be converted into a `String`, representing the textual content. - /// - /// # Returns - /// - /// Returns a `Text` instance with the specified content and default font, size, and color. - /// - /// # Example - /// - /// ``` - /// let text = Text::from("Hello, World!"); - /// ``` - pub fn from(content: impl Into) -> Self { - Self { - content: content.into(), - ..Default::default() - } - } - - /// Sets the font of the `Text` instance. - /// - /// # Arguments - /// - /// * `font` - A value that can be converted into a `String`, representing the font name. - /// - /// # Returns - /// - /// Returns the `Text` instance with the updated font. - /// - /// # Example - /// - /// ``` - /// let text = Text::from("Hello, World!").font("Arial"); - /// ``` - pub fn font(mut self, font: impl Into) -> Self { - self.font = font.into(); - self - } - - /// Sets the size of the `Text` instance. - /// - /// # Arguments - /// - /// * `size` - A `usize` value representing the font size. - /// - /// # Returns - /// - /// Returns the `Text` instance with the updated size. - /// - /// # Example - /// - /// ``` - /// let text = Text::from("Hello, World!").size(24); - /// ``` - pub fn size(mut self, size: usize) -> Self { - self.size = size; - self - } - - /// Sets the color of the `Text` instance. - /// - /// # Arguments - /// - /// * `color` - An `Rgb` struct representing the color of the text. - /// - /// # Returns - /// - /// Returns the `Text` instance with the updated color. - /// - /// # Example - /// - /// ``` - /// let text = Text::from("Hello, World!").color(Rgb(255, 0, 0)); - /// ``` - pub fn color(mut self, color: Rgb) -> Self { - self.color = color; - self - } - - /// Sets the x-coordinate for the object. - /// - /// # Arguments - /// - /// * `x` - A `f64` value representing the x-coordinate. It only works in the plot title. - /// - /// # Returns - /// - /// Returns the modified object with the updated x-coordinate. - pub fn x(mut self, x: f64) -> Self { - self.x = x; - self - } - - /// Sets the y-coordinate for the object. - /// - /// # Arguments - /// - /// * `y` - A `f64` value representing the y-coordinate. It only works in the plot title. - /// - /// # Returns - /// - /// Returns the modified object with the updated y-coordinate. - pub fn y(mut self, y: f64) -> Self { - self.y = y; - self - } -} - -impl From<&str> for Text { - fn from(content: &str) -> Self { - Self::from(content.to_string()) - } -} - -impl From for Text { - fn from(content: String) -> Self { - Self::from(content) - } -} - -impl From<&String> for Text { - fn from(content: &String) -> Self { - Self::from(content) - } -} diff --git a/src/traces/barplot.rs b/src/traces/barplot.rs deleted file mode 100644 index 466d7a9..0000000 --- a/src/traces/barplot.rs +++ /dev/null @@ -1,279 +0,0 @@ -//! This module provides implementations for bar plots using the Plotly library. -//! -//! The `BarPlot` struct allow for the creation and customization of bar plots -//! with various options for data, layout, and aesthetics. - -use bon::bon; - -use plotly::{ - common::{ErrorData, ErrorType, Line as LinePlotly, Marker}, - layout::BarMode, - Bar, Layout, Trace as TracePlotly, -}; - -use polars::frame::DataFrame; - -use crate::{ - aesthetics::{line::Line, mark::Mark}, - colors::Rgb, - texts::Text, - traits::{layout::LayoutPlotly, plot::Plot, polar::Polar, trace::Trace}, - Axis, Legend, Orientation, -}; - -/// A structure representing a bar plot. -pub struct BarPlot { - traces: Vec>, - layout: Layout, -} - -#[bon] -impl BarPlot { - /// Creates a new `BarPlot`. - /// - /// # Arguments - /// - /// * `data` - A reference to the `DataFrame` containing the data to be plotted. - /// * `values` - A string specifying the column name to be used for the y-axis (the dependent variable). - /// * `labels` - A string specifying the column name to be used for the x-axis (the independent variable). - /// * `orientation` - An optional `Orientation` enum specifying whether the plot should be horizontal or vertical. - /// * `group` - An optional string specifying the column name to be used for grouping data points. - /// * `error` - An optional string specifying the column name containing error values for the y-axis data. - /// * `color` - An optional `Rgb` value specifying the color of the markers to be used for the plot. - /// * `colors` - An optional vector of `Rgb` values specifying the colors to be used for the plot. - /// * `plot_title` - An optional `Text` struct specifying the title of the plot. - /// * `x_title` - An optional `Text` struct specifying the title of the x-axis. - /// * `y_title` - An optional `Text` struct specifying the title of the y-axis. - /// * `x_axis` - An optional reference to an `Axis` struct for customizing the x-axis. - /// * `y_axis` - An optional reference to an `Axis` struct for customizing the y-axis. - /// * `legend_title` - An optional `Text` struct specifying the title of the legend. - /// * `legend` - An optional reference to a `Legend` struct for customizing the legend of the plot (e.g., positioning, font, etc.). - /// - /// # Returns - /// - /// Returns an instance of `BarPlot`. - /// - /// **Example** - /// - /// ``` - /// let legend = Legend::new() - /// .orientation(Orientation::Horizontal) - /// .y(1.0) - /// .x(0.4); - /// - /// BarPlot::builder() - /// .data(&barplot_dataset) - /// .labels("animals") - /// .values("values") - /// .orientation(Orientation::Vertical) - /// .group("gender") - /// .error("errors") - /// .colors(vec![Rgb(255, 127, 80), Rgb(64, 224, 208)]) - /// .plot_title( - /// Text::from("Vertical Bar Plot") - /// .font("Arial") - /// .size(18) - /// ) - /// .x_title( - /// Text::from("animal") - /// .font("Arial") - /// .size(15) - /// ) - /// .y_title( - /// Text::from("value") - /// .font("Arial") - /// .size(15) - /// ) - /// .legend_title( - /// Text::from("gender") - /// .font("Arial") - /// .size(15) - /// ) - /// .legend(&legend) - /// .build() - /// .plot(); - /// ``` - /// - /// ![Bar Plot](https://imgur.com/2alZlO5.png) - #[builder(on(String, into), on(Text, into))] - pub fn new( - data: &DataFrame, - values: String, - labels: String, - orientation: Option, - group: Option, - error: Option, - // Marker - color: Option, - colors: Option>, - // Layout - plot_title: Option, - x_title: Option, - y_title: Option, - legend_title: Option, - x_axis: Option<&Axis>, - y_axis: Option<&Axis>, - legend: Option<&Legend>, - ) -> Self { - let value_column = values.as_str(); - let label_column = labels.as_str(); - - // Layout - let bar_mode = Some(BarMode::Group); - - let layout = Self::create_layout( - bar_mode, - plot_title, - x_title, - x_axis, - y_title, - y_axis, - legend_title, - legend, - ); - - // Trace - let box_points = None; - let point_offset = None; - let jitter = None; - let additional_series = None; - - let opacity = None; - let size = None; - let with_shape = None; - let shape = None; - let shapes = None; - let line_types = None; - let line_width = None; - - let traces = Self::create_traces( - data, - value_column, - label_column, - orientation, - group, - error, - box_points, - point_offset, - jitter, - additional_series, - opacity, - size, - color, - colors, - with_shape, - shape, - shapes, - line_types, - line_width, - ); - - Self { traces, layout } - } -} - -impl LayoutPlotly for BarPlot {} -impl Polar for BarPlot {} -impl Mark for BarPlot {} -impl Line for BarPlot {} - -impl Trace for BarPlot { - fn create_trace( - data: &DataFrame, - x_col: &str, - y_col: &str, - orientation: Option, - group_name: Option<&str>, - error: Option, - #[allow(unused_variables)] box_points: Option, - #[allow(unused_variables)] point_offset: Option, - #[allow(unused_variables)] jitter: Option, - #[allow(unused_variables)] with_shape: Option, - marker: Marker, - #[allow(unused_variables)] line: LinePlotly, - ) -> Box { - let value_data = Self::get_numeric_column(data, x_col); - let category_data = Self::get_string_column(data, y_col); - - match orientation { - Some(orientation) => match orientation { - Orientation::Vertical => { - let mut trace = Bar::default() - .x(category_data) - .y(value_data) - .orientation(orientation.get_orientation()); - - if let Some(error) = error { - let error = Self::get_numeric_column(data, error.as_str()) - .iter() - .map(|x| x.unwrap() as f64) - .collect::>(); - - trace = trace.error_y(ErrorData::new(ErrorType::Data).array(error)) - } - - trace = trace.marker(marker); - - if let Some(name) = group_name { - trace = trace.name(name); - } - - trace - } - Orientation::Horizontal => { - let mut trace = Bar::default() - .x(value_data) - .y(category_data) - .orientation(orientation.get_orientation()); - - if let Some(error) = error { - let error = Self::get_numeric_column(data, error.as_str()) - .iter() - .map(|x| x.unwrap() as f64) - .collect::>(); - - trace = trace.error_x(ErrorData::new(ErrorType::Data).array(error)) - } - - trace = trace.marker(marker); - - if let Some(name) = group_name { - trace = trace.name(name); - } - - trace - } - }, - None => { - let mut trace = Bar::default().x(category_data).y(value_data); - - if let Some(error) = error { - let error = Self::get_numeric_column(data, error.as_str()) - .iter() - .map(|x| x.unwrap() as f64) - .collect::>(); - - trace = trace.error_y(ErrorData::new(ErrorType::Data).array(error)) - } - - trace = trace.marker(marker); - - if let Some(name) = group_name { - trace = trace.name(name); - } - - trace - } - } - } -} - -impl Plot for BarPlot { - fn get_layout(&self) -> &Layout { - &self.layout - } - - fn get_traces(&self) -> &Vec> { - &self.traces - } -} diff --git a/src/traces/boxplot.rs b/src/traces/boxplot.rs deleted file mode 100644 index 525ceda..0000000 --- a/src/traces/boxplot.rs +++ /dev/null @@ -1,313 +0,0 @@ -//! This module provides implementations for box plots using the Plotly library. -//! -//! The `BoxPlot` structs allow for the creation and customization of box plots -//! with various options for data, layout, and aesthetics. - -use bon::bon; - -use plotly::{ - box_plot::BoxPoints, - common::{Line as LinePlotly, Marker}, - BoxPlot as BoxPlotly, Layout, Trace as TracePlotly, -}; - -use polars::frame::DataFrame; - -use crate::{ - aesthetics::{line::Line, mark::Mark, orientation::Orientation}, - colors::Rgb, - texts::Text, - traits::{layout::LayoutPlotly, plot::Plot, polar::Polar, trace::Trace}, - Axis, Legend, -}; - -/// A structure representing a box plot. -pub struct BoxPlot { - traces: Vec>, - layout: Layout, -} - -#[bon] -impl BoxPlot { - /// Creates a new `BoxPlot`. - /// - /// # Arguments - /// - /// * `data` - A reference to the `DataFrame` containing the data to be plotted. - /// * `values` - A string specifying the column name to be used for the y-axis (the dependent variable). - /// * `labels` - A string specifying the column name to be used for the x-axis (the independent variable). - /// * `orientation` - An optional `Orientation` enum specifying whether the plot should be horizontal or vertical. - /// * `group` - An optional string specifying the column name to be used for grouping data points. - /// * `box_points` - An optional boolean indicating whether individual data points should be plotted along with the box plot. - /// * `point_offset` - An optional f64 value specifying the horizontal offset for individual data points when `box_points` is enabled. - /// * `jitter` - An optional f64 value indicating the amount of jitter (random noise) to apply to individual data points for visibility. - /// * `opacity` - An optional f64 value specifying the opacity of the plot markers (range: 0.0 to 1.0). - /// * `color` - An optional `Rgb` value specifying the color of the markers to be used for the plot. - /// * `colors` - An optional vector of `Rgb` values specifying the colors to be used for the plot. - /// * `plot_title` - An optional `Text` struct specifying the title of the plot. - /// * `x_title` - An optional `Text` struct specifying the title of the x-axis. - /// * `y_title` - An optional `Text` struct specifying the title of the y-axis. - /// * `x_axis` - An optional reference to an `Axis` struct for customizing the x-axis. - /// * `y_axis` - An optional reference to an `Axis` struct for customizing the y-axis. - /// * `legend_title` - An optional `Text` struct specifying the title of the legend. - /// * `legend` - An optional reference to a `Legend` struct for customizing the legend of the plot (e.g., positioning, font, etc.). - /// - /// # Returns - /// - /// Returns an instance of `BoxPlot`. - /// - /// **Example** - /// - /// ``` - /// let axis_format = Axis::new() - /// .value_thousands(true); - /// - /// let legend_format = Legend::new() - /// .border_width(1) - /// .x(0.9); - /// - /// BoxPlot::builder() - /// .data(&scatterplot_dataset) - /// .labels("species") - /// .values("body_mass_g") - /// .orientation(Orientation::Vertical) - /// .group("gender") - /// .box_points(true) - /// .point_offset(-1.5) - /// .jitter(0.01) - /// .opacity(0.1) - /// .colors(vec![ - /// Rgb(0, 191, 255), - /// Rgb(57, 255, 20), - /// Rgb(255, 105, 180), - /// ]) - /// .plot_title( - /// Text::from("Box Plot") - /// .font("Arial") - /// .size(18) - /// ) - /// .x_title( - /// Text::from("species") - /// .font("Arial") - /// .size(15) - /// ) - /// .y_title( - /// Text::from("body mass (g)") - /// .font("Arial") - /// .size(15) - /// ) - /// .y_axis(&axis_format) - /// .legend_title( - /// Text::from("gender") - /// .font("Arial") - /// .size(15) - /// ) - /// .legend(&legend_format) - /// .build() - /// .plot(); - /// ``` - /// - /// ![Box Plot](https://imgur.com/uj1LY90.png) - #[builder(on(String, into), on(Text, into))] - pub fn new( - data: &DataFrame, - values: String, - labels: String, - orientation: Option, - group: Option, - box_points: Option, - point_offset: Option, - jitter: Option, - // Marker - opacity: Option, - color: Option, - colors: Option>, - // Layout - plot_title: Option, - x_title: Option, - y_title: Option, - legend_title: Option, - x_axis: Option<&Axis>, - y_axis: Option<&Axis>, - legend: Option<&Legend>, - ) -> Self { - let value_column = values.as_str(); - let label_column = labels.as_str(); - - // Layout - let bar_mode = None; - - let layout = Self::create_layout( - bar_mode, - plot_title, - x_title, - x_axis, - y_title, - y_axis, - legend_title, - legend, - ); - - // Trace - let error = None; - let additional_series = None; - - let size = None; - let with_shape = None; - let shape = None; - let shapes = None; - let line_types = None; - let line_width = None; - - let traces = Self::create_traces( - data, - value_column, - label_column, - orientation, - group, - error, - box_points, - point_offset, - jitter, - additional_series, - opacity, - size, - color, - colors, - with_shape, - shape, - shapes, - line_types, - line_width, - ); - - Self { traces, layout } - } -} - -impl LayoutPlotly for BoxPlot {} -impl Polar for BoxPlot {} -impl Mark for BoxPlot {} -impl Line for BoxPlot {} - -impl Trace for BoxPlot { - fn create_trace( - data: &DataFrame, - x_col: &str, - y_col: &str, - orientation: Option, - group_name: Option<&str>, - #[allow(unused_variables)] error: Option, - box_points: Option, - point_offset: Option, - jitter: Option, - #[allow(unused_variables)] with_shape: Option, - marker: Marker, - #[allow(unused_variables)] line: LinePlotly, - ) -> Box { - let value_data = Self::get_numeric_column(data, x_col); - let category_data = Self::get_string_column(data, y_col); - - match orientation { - Some(orientation) => match orientation { - Orientation::Vertical => { - let mut trace = BoxPlotly::default() - .x(category_data) - .y(value_data) - .orientation(orientation.get_orientation()); - - if let Some(all) = box_points { - if all { - trace = trace.box_points(BoxPoints::All); - } else { - trace = trace.box_points(BoxPoints::False); - } - } - - if let Some(point_position) = point_offset { - trace = trace.point_pos(point_position); - } - - if let Some(jitter) = jitter { - trace = trace.jitter(jitter); - } - - trace = trace.marker(marker); - - if let Some(name) = group_name { - trace = trace.name(name); - } - - trace - } - Orientation::Horizontal => { - let mut trace = BoxPlotly::default() - .x(value_data) - .y(category_data) - .orientation(orientation.get_orientation()); - - if let Some(all) = box_points { - if all { - trace = trace.box_points(BoxPoints::All); - } else { - trace = trace.box_points(BoxPoints::False); - } - } - - if let Some(point_position) = point_offset { - trace = trace.point_pos(point_position); - } - - if let Some(jitter) = jitter { - trace = trace.jitter(jitter); - } - - trace = trace.marker(marker); - - if let Some(name) = group_name { - trace = trace.name(name); - } - - trace - } - }, - None => { - let mut trace = BoxPlotly::default().x(category_data).y(value_data); - - if let Some(all) = box_points { - if all { - trace = trace.box_points(BoxPoints::All); - } else { - trace = trace.box_points(BoxPoints::False); - } - } - - if let Some(point_position) = point_offset { - trace = trace.point_pos(point_position); - } - - if let Some(jitter) = jitter { - trace = trace.jitter(jitter); - } - - trace = trace.marker(marker); - - if let Some(name) = group_name { - trace = trace.name(name); - } - - trace - } - } - } -} - -impl Plot for BoxPlot { - fn get_layout(&self) -> &Layout { - &self.layout - } - - fn get_traces(&self) -> &Vec> { - &self.traces - } -} diff --git a/src/traces/histogram.rs b/src/traces/histogram.rs deleted file mode 100644 index c59bab2..0000000 --- a/src/traces/histogram.rs +++ /dev/null @@ -1,220 +0,0 @@ -use bon::bon; - -use plotly::{ - common::{Line as LinePlotly, Marker}, - histogram::HistFunc, - layout::BarMode, - Histogram as HistogramPlotly, Layout, Trace as TracePlotly, -}; - -use polars::frame::DataFrame; - -use crate::{ - aesthetics::{line::Line, mark::Mark}, - colors::Rgb, - texts::Text, - traits::{layout::LayoutPlotly, plot::Plot, polar::Polar, trace::Trace}, - Axis, Legend, Orientation, -}; - -/// A structure representing a histogram. -pub struct Histogram { - traces: Vec>, - layout: Layout, -} - -#[bon] -impl Histogram { - /// Creates a new `Histogram`. - /// - /// # Arguments - /// - /// * `data` - A reference to the `DataFrame` containing the data to be plotted. - /// * `x` - A string specifying the column name to be used for the x-axis. - /// * `group` - An optional string specifying the column name to be used for grouping data points. - /// * `opacity` - An optional f64 value specifying the opacity of the plot markers (range: 0.0 to 1.0). - /// * `color` - An optional `Rgb` value specifying the color of the markers to be used for the plot. - /// * `colors` - An optional vector of `Rgb` values specifying the colors to be used for the plot. - /// * `plot_title` - An optional `Text` struct specifying the title of the plot. - /// * `x_title` - An optional `Text` struct specifying the title of the x-axis. - /// * `y_title` - An optional `Text` struct specifying the title of the y-axis. - /// * `x_axis` - An optional reference to an `Axis` struct for customizing the x-axis. - /// * `y_axis` - An optional reference to an `Axis` struct for customizing the y-axis. - /// * `legend_title` - An optional `Text` struct specifying the title of the legend. - /// * `legend` - An optional reference to a `Legend` struct for customizing the legend of the plot (e.g., positioning, font, etc.). - /// - /// # Returns - /// - /// Returns an instance of `Histogram`. - /// - /// **Example** - /// - /// ``` - /// let axis_format = Axis::new() - /// .show_line(true) - /// .show_grid(true) - /// .value_thousands(true) - /// .tick_direction(TickDirection::OutSide); - /// - /// let legend_format = Legend::new() - /// .x(0.9); - /// - /// Histogram::builder() - /// .data(&scatterplot_dataset) - /// .x("body_mass_g") - /// .group("species") - /// .opacity(0.5) - /// .colors(vec![ - /// Rgb(255, 165, 0), - /// Rgb(147, 112, 219), - /// Rgb(46, 139, 87), - /// ]) - /// .plot_title( - /// Text::from("Histogram") - /// .font("Arial") - /// .size(18) - /// ) - /// .x_title( - /// Text::from("body mass (g)") - /// .font("Arial") - /// .size(15) - /// ) - /// .x_axis(&axis_format) - /// .y_title( - /// Text::from("count") - /// .font("Arial") - /// .size(15) - /// ) - /// .y_axis(&axis_format) - /// .legend_title( - /// Text::from("species") - /// .font("Arial") - /// .size(15) - /// ) - /// .legend(&legend_format) - /// .build() - /// .plot(); - /// ``` - /// - /// ![Histogram](https://imgur.com/w2oiuIo.png) - #[builder(on(String, into), on(Text, into))] - pub fn new( - data: &DataFrame, - x: String, - group: Option, - // Marker - opacity: Option, - color: Option, - colors: Option>, - // Layout - plot_title: Option, - x_title: Option, - y_title: Option, - legend_title: Option, - x_axis: Option<&Axis>, - y_axis: Option<&Axis>, - legend: Option<&Legend>, - ) -> Self { - let x_col = x.as_str(); - - // Layout - let bar_mode = Some(BarMode::Overlay); - - let layout = Self::create_layout( - bar_mode, - plot_title, - x_title, - x_axis, - y_title, - y_axis, - legend_title, - legend, - ); - - // Trace - let y_col = ""; - let orientation = None; - let error = None; - let box_points = None; - let point_offset = None; - let jitter = None; - let additional_series = None; - - let size = None; - let with_shape = None; - let shape = None; - let shapes = None; - let line_types = None; - let line_width = None; - - let traces = Self::create_traces( - data, - x_col, - y_col, - orientation, - group, - error, - box_points, - point_offset, - jitter, - additional_series, - opacity, - size, - color, - colors, - with_shape, - shape, - shapes, - line_types, - line_width, - ); - - Self { traces, layout } - } -} - -impl LayoutPlotly for Histogram {} -impl Polar for Histogram {} -impl Mark for Histogram {} -impl Line for Histogram {} - -impl Trace for Histogram { - fn create_trace( - data: &DataFrame, - x_col: &str, - #[allow(unused_variables)] y_col: &str, - #[allow(unused_variables)] orientation: Option, - group_name: Option<&str>, - #[allow(unused_variables)] error: Option, - #[allow(unused_variables)] box_points: Option, - #[allow(unused_variables)] point_offset: Option, - #[allow(unused_variables)] jitter: Option, - #[allow(unused_variables)] with_shape: Option, - marker: Marker, - #[allow(unused_variables)] line: LinePlotly, - ) -> Box { - let x_data = Self::get_numeric_column(data, x_col); - - let mut trace = HistogramPlotly::default() - .x(x_data) - .hist_func(HistFunc::Count); - - trace = trace.marker(marker); - - if let Some(name) = group_name { - trace = trace.name(name); - } - - trace - } -} - -impl Plot for Histogram { - fn get_layout(&self) -> &Layout { - &self.layout - } - - fn get_traces(&self) -> &Vec> { - &self.traces - } -} diff --git a/src/traces/lineplot.rs b/src/traces/lineplot.rs deleted file mode 100644 index da86e30..0000000 --- a/src/traces/lineplot.rs +++ /dev/null @@ -1,326 +0,0 @@ -use bon::bon; -use plotly::{ - common::{Line as LinePlotly, Marker, Mode}, - Layout, Scatter, Trace as TracePlotly, -}; - -use polars::{ - frame::DataFrame, - prelude::{col, IntoLazy}, -}; - -use crate::{ - aesthetics::{ - line::{Line, LineType}, - mark::Mark, - }, - colors::Rgb, - texts::Text, - traits::{layout::LayoutPlotly, plot::Plot, polar::Polar, trace::Trace}, - Axis, Legend, Orientation, Shape, -}; - -/// A structure representing a line plot. -pub struct LinePlot { - traces: Vec>, - layout: Layout, -} - -#[bon] -impl LinePlot { - /// Creates a new `LinePlot`. - /// - /// # Arguments - /// - /// * `data` - A reference to the `DataFrame` containing the data to be plotted. - /// * `x` - A string specifying the column name to be used for the x-axis. - /// * `y` - A string specifying the column name to be used for the y-axis. - /// * `additional_lines` - An optional vector of strings specifying additional y-axis columns to be plotted as lines. - /// * `size` - An optional `usize` specifying the size of the markers or line thickness. - /// * `color` - An optional `Rgb` value specifying the color of the marker to be used for the plot. - /// * `colors` - An optional vector of `Rgb` values specifying the color for the markers to be used for the plot. - /// * `with_shape` - An optional `bool` indicating whether to use shapes for markers in the plot. - /// * `shape` - An optional `Shape` specifying the shape of the markers. - /// * `shapes` - An optional `Vec` specifying multiple shapes for the markers. - /// * `line_types` - An optional vector of `LineType` specifying the types of lines (e.g., solid, dashed) for each plotted line. - /// * `line_width` - An optional `f64` specifying the width of the plotted lines. - /// * `plot_title` - An optional `Text` struct specifying the title of the plot. - /// * `x_title` - An optional `Text` struct specifying the title of the x-axis. - /// * `y_title` - An optional `Text` struct specifying the title of the y-axis. - /// * `x_axis` - An optional reference to an `Axis` struct for customizing the x-axis. - /// * `y_axis` - An optional reference to an `Axis` struct for customizing the y-axis. - /// * `legend_title` - An optional `Text` struct specifying the title of the legend. - /// * `legend` - An optional reference to a `Legend` struct for customizing the legend of the plot (e.g., positioning, font, etc.). - /// - /// # Returns - /// - /// Returns an instance of `LinePlot`. - /// - /// **Example** - /// - /// ``` - /// LinePlot::builder() - /// .data(&lineplot_dataset) - /// .x("x") - /// .y("sine") - /// .additional_lines(vec!["cosine"]) - /// .colors(vec![Rgb(255, 0, 0), Rgb(0, 255, 0)]) - /// .line_types(vec![LineType::Solid, LineType::Dot]) - /// .line_width(3.0) - /// .with_shape(false) - /// .plot_title( - /// Text::from("Line Plot") - /// .font("Arial") - /// .size(18) - /// ) - /// .x_axis( - /// &Axis::new() - /// .tick_direction(TickDirection::OutSide) - /// .axis_position(0.5) - /// .tick_values(vec![ - /// 0.5 * std::f64::consts::PI, - /// std::f64::consts::PI, - /// 1.5 * std::f64::consts::PI, - /// 2.0 * std::f64::consts::PI, - /// ]) - /// .tick_labels(vec!["π/2", "π", "3π/2", "2π"]) - /// ) - /// .y_axis( - /// &Axis::new() - /// .tick_direction(TickDirection::OutSide) - /// .clone() - /// .tick_values(vec![-1.0, 0.0, 1.0]) - /// .tick_labels(vec!["-1", "0", "1"]) - /// ) - /// .legend_title( - /// Text::from("series") - /// .font("Arial") - /// .size(15) - /// ) - /// .build() - /// .plot(); - /// ``` - /// - /// ![Line Plot](https://imgur.com/PaXG300.png) - #[builder(on(String, into), on(Text, into))] - pub fn new( - // Data - data: &DataFrame, - x: String, - y: String, - additional_lines: Option>, - // Marker - size: Option, - color: Option, - colors: Option>, - with_shape: Option, - shape: Option, - shapes: Option>, - line_types: Option>, - line_width: Option, - // Layout - plot_title: Option, - x_title: Option, - y_title: Option, - legend_title: Option, - x_axis: Option<&Axis>, - y_axis: Option<&Axis>, - legend: Option<&Legend>, - ) -> Self { - let x_col = x.as_str(); - let y_col = y.as_str(); - - // Layout - let bar_mode = None; - - let layout = Self::create_layout( - bar_mode, - plot_title, - x_title, - x_axis, - y_title, - y_axis, - legend_title, - legend, - ); - - // Trace - let orientation = None; - let group = None; - let error = None; - let box_points = None; - let point_offset = None; - let jitter = None; - let opacity = None; - - let traces = Self::create_traces( - data, - x_col, - y_col, - orientation, - group, - error, - box_points, - point_offset, - jitter, - additional_lines, - opacity, - size, - color, - colors, - with_shape, - shape, - shapes, - line_types, - line_width, - ); - - Self { traces, layout } - } -} - -impl LayoutPlotly for LinePlot {} -impl Polar for LinePlot {} -impl Mark for LinePlot {} -impl Line for LinePlot {} - -impl Trace for LinePlot { - fn create_trace( - data: &DataFrame, - x_col: &str, - y_col: &str, - #[allow(unused_variables)] orientation: Option, - group_name: Option<&str>, - #[allow(unused_variables)] error: Option, - #[allow(unused_variables)] box_points: Option, - #[allow(unused_variables)] point_offset: Option, - #[allow(unused_variables)] jitter: Option, - with_shape: Option, - marker: Marker, - line: LinePlotly, - ) -> Box { - let x_data = Self::get_numeric_column(data, x_col); - let y_data = Self::get_numeric_column(data, y_col); - - let mut trace = Scatter::default().x(x_data).y(y_data); - - if let Some(with_shape) = with_shape { - if with_shape { - trace = trace.mode(Mode::LinesMarkers); - } else { - trace = trace.mode(Mode::Lines); - } - } - - trace = trace.marker(marker); - trace = trace.line(line); - - if let Some(name) = group_name { - trace = trace.name(name); - } - - trace - } - - fn create_traces( - data: &DataFrame, - x_col: &str, - y_col: &str, - orientation: Option, - #[allow(unused_variables)] group: Option, - error: Option, - box_points: Option, - point_offset: Option, - jitter: Option, - additional_series: Option>, - opacity: Option, - size: Option, - color: Option, - colors: Option>, - with_shape: Option, - shape: Option, - shapes: Option>, - line_type: Option>, - line_width: Option, - ) -> Vec> { - let mut traces: Vec> = Vec::new(); - - let mark = Self::create_marker(opacity, size); - let line = Self::create_line(); - - let series_mark = Self::set_color(&mark, &color, &colors, 0); - - let series_mark = Self::set_shape(&series_mark, &shape, &shapes, 0); - - let series_line = Self::set_line_type(&line, &line_type, line_width, 0); - - let group_name = Some(y_col); - - let trace = Self::create_trace( - data, - x_col, - y_col, - orientation.clone(), - group_name, - error.clone(), - box_points, - point_offset, - jitter, - with_shape, - series_mark, - series_line, - ); - - traces.push(trace); - - if let Some(additional_series) = additional_series { - let additional_series = additional_series.into_iter(); - - for (i, series) in additional_series.enumerate() { - let series_mark = Self::set_color(&mark, &color, &colors, i + 1); - - let series_mark = Self::set_shape(&series_mark, &shape, &shapes, i + 1); - - let series_line = Self::set_line_type(&line, &line_type, line_width, i + 1); - - let subset = data - .clone() - .lazy() - .select([col(x_col), col(series)]) - .collect() - .unwrap(); - - let group_name = Some(series); - - let trace = Self::create_trace( - &subset, - x_col, - series, - orientation.clone(), - group_name, - error.clone(), - box_points, - point_offset, - jitter, - with_shape, - series_mark, - series_line, - ); - - traces.push(trace); - } - } - - traces - } -} - -impl Plot for LinePlot { - fn get_layout(&self) -> &Layout { - &self.layout - } - - fn get_traces(&self) -> &Vec> { - &self.traces - } -} diff --git a/src/traces/scatterplot.rs b/src/traces/scatterplot.rs deleted file mode 100644 index eaba76b..0000000 --- a/src/traces/scatterplot.rs +++ /dev/null @@ -1,214 +0,0 @@ -use bon::bon; - -use plotly::{ - common::{Line as LinePlotly, Marker, Mode}, - Layout, Scatter, Trace as TracePlotly, -}; - -use polars::frame::DataFrame; - -use crate::{ - aesthetics::{line::Line, mark::Mark}, - colors::Rgb, - texts::Text, - traits::{layout::LayoutPlotly, plot::Plot, polar::Polar, trace::Trace}, - Axis, Legend, Orientation, Shape, -}; - -/// A structure representing a scatter plot. -pub struct ScatterPlot { - traces: Vec>, - layout: Layout, -} - -#[bon] -impl ScatterPlot { - /// Creates a new `ScatterPlot`. - /// - /// # Arguments - /// - /// * `data` - A reference to the `DataFrame` containing the data to be plotted. - /// * `x` - A string specifying the column name to be used for the x-axis. - /// * `y` - A string specifying the column name to be used for the y-axis. - /// * `group` - An optional string specifying the column name to be used for grouping data points. - /// * `opacity` - An optional f64 value specifying the opacity of the plot markers (range: 0.0 to 1.0). - /// * `size` - An optional `usize` specifying the size of the markers. - /// * `color` - An optional `Rgb` value specifying the color of the marker to be used for the plot. - /// * `colors` - An optional vector of `Rgb` values specifying the color for the markers to be used for the plot. - /// * `shape` - An optional `Shape` specifying the shape of the markers. - /// * `shapes` - An optional `Vec` specifying multiple shapes for the markers. - /// * `plot_title` - An optional `Text` struct specifying the title of the plot. - /// * `x_title` - An optional `Text` struct specifying the title of the x-axis. - /// * `y_title` - An optional `Text` struct specifying the title of the y-axis. - /// * `x_axis` - An optional reference to an `Axis` struct for customizing the x-axis. - /// * `y_axis` - An optional reference to an `Axis` struct for customizing the y-axis. - /// * `legend_title` - An optional `Text` struct specifying the title of the legend. - /// * `legend` - An optional reference to a `Legend` struct for customizing the legend of the plot (e.g., positioning, font, etc.). - /// - /// # Returns - /// - /// Returns an instance of `ScatterPlot`. - /// - /// **Example** - /// - /// ``` - /// let axis_format = Axis::new() - /// .show_line(true) - /// .tick_direction(TickDirection::OutSide) - /// .value_thousands(true); - /// - /// ScatterPlot::builder() - /// .data(&scatterplot_dataset) - /// .x("body_mass_g") - /// .y("flipper_length_mm") - /// .group("species") - /// .opacity(0.5) - /// .size(12) - /// .colors(vec![ - /// Rgb(178, 34, 34), - /// Rgb(65, 105, 225), - /// Rgb(255, 140, 0), - /// ]) - /// .shapes(vec![Shape::Circle, Shape::Square, Shape::Diamond]) - /// .plot_title( - /// Text::from("Scatter Plot") - /// .font("Arial") - /// .size(20) - /// .x(0.065) - /// ) - /// .x_title("body mass (g)") - /// .y_title("flipper length (mm)") - /// .legend_title("species") - /// .x_axis(&axis_format.clone().value_range(vec![2500.0, 6500.0])) - /// .y_axis(&axis_format.clone().value_range(vec![170.0, 240.0])) - /// .legend( - /// &Legend::new() - /// .x(0.85) - /// .y(0.15) - /// ) - /// .build() - /// .plot(); - /// ``` - /// - /// ![Scatter Plot](https://imgur.com/9jfO8RU.png) - #[builder(on(String, into), on(Text, into))] - pub fn new( - // Data - data: &DataFrame, - x: String, - y: String, - group: Option, - // Marker - opacity: Option, - size: Option, - color: Option, - colors: Option>, - shape: Option, - shapes: Option>, - // Layout - plot_title: Option, - x_title: Option, - y_title: Option, - legend_title: Option, - x_axis: Option<&Axis>, - y_axis: Option<&Axis>, - legend: Option<&Legend>, - ) -> Self { - let x_col = x.as_str(); - let y_col = y.as_str(); - - // Layout - let bar_mode = None; - - let layout = Self::create_layout( - bar_mode, - plot_title, - x_title, - x_axis, - y_title, - y_axis, - legend_title, - legend, - ); - - // Trace - let orientation = None; - let error = None; - let box_points = None; - let point_offset = None; - let jitter = None; - let additional_series = None; - let line_types = None; - let with_shape = None; - let line_width = None; - - let traces = Self::create_traces( - data, - x_col, - y_col, - orientation, - group, - error, - box_points, - point_offset, - jitter, - additional_series, - opacity, - size, - color, - colors, - with_shape, - shape, - shapes, - line_types, - line_width, - ); - - Self { traces, layout } - } -} - -impl LayoutPlotly for ScatterPlot {} -impl Polar for ScatterPlot {} -impl Mark for ScatterPlot {} -impl Line for ScatterPlot {} - -impl Trace for ScatterPlot { - fn create_trace( - data: &DataFrame, - x_col: &str, - y_col: &str, - #[allow(unused_variables)] orientation: Option, - group_name: Option<&str>, - #[allow(unused_variables)] error: Option, - #[allow(unused_variables)] box_points: Option, - #[allow(unused_variables)] point_offset: Option, - #[allow(unused_variables)] jitter: Option, - #[allow(unused_variables)] with_shape: Option, - marker: Marker, - #[allow(unused_variables)] line: LinePlotly, - ) -> Box { - let x_data = Self::get_numeric_column(data, x_col); - let y_data = Self::get_numeric_column(data, y_col); - - let mut trace = Scatter::default().x(x_data).y(y_data).mode(Mode::Markers); - - trace = trace.marker(marker); - - if let Some(name) = group_name { - trace = trace.name(name); - } - - trace - } -} - -impl Plot for ScatterPlot { - fn get_layout(&self) -> &Layout { - &self.layout - } - - fn get_traces(&self) -> &Vec> { - &self.traces - } -} diff --git a/src/traces/timeseriesplot.rs b/src/traces/timeseriesplot.rs deleted file mode 100644 index e17815e..0000000 --- a/src/traces/timeseriesplot.rs +++ /dev/null @@ -1,322 +0,0 @@ -use bon::bon; -use plotly::{ - common::{Line as LinePlotly, Marker, Mode}, - Layout, Scatter, Trace as TracePlotly, -}; - -use polars::{ - frame::DataFrame, - prelude::{col, IntoLazy}, -}; - -use crate::{ - aesthetics::{ - line::{Line, LineType}, - mark::Mark, - }, - colors::Rgb, - texts::Text, - traits::{layout::LayoutPlotly, plot::Plot, polar::Polar, trace::Trace}, - Axis, Legend, Orientation, Shape, -}; - -/// A structure representing a time series plot. -pub struct TimeSeriesPlot { - traces: Vec>, - layout: Layout, -} - -#[bon] -impl TimeSeriesPlot { - /// Creates a new `TimeSeriesPlot`. - /// - /// # Arguments - /// - /// * `data` - A reference to the `DataFrame` containing the data to be plotted. - /// * `x` - A string specifying the column name to be used for the x-axis, typically representing time or dates. - /// * `y` - A string specifying the column name to be used for the y-axis, typically representing the primary metric. - /// * `additional_series` - An optional vector of strings specifying additional y-axis columns to be plotted as series. - /// * `size` - An optional `usize` specifying the size of the markers or line thickness. - /// * `color` - An optional `Rgb` value specifying the color of the marker to be used for the plot. - /// * `colors` - An optional vector of `Rgb` values specifying the color for the markers to be used for the plot. - /// * `with_shape` - An optional `bool` indicating whether to use shapes for markers in the plot. - /// * `shape` - An optional `Shape` specifying the shape of the markers. - /// * `shapes` - An optional `Vec` specifying multiple shapes for the markers. - /// * `line_types` - An optional vector of `LineType` specifying the types of lines (e.g., solid, dashed) for each plotted series. - /// * `line_width` - An optional `f64` specifying the width of the plotted lines. - /// * `plot_title` - An optional `Text` struct specifying the title of the plot. - /// * `x_title` - An optional `Text` struct specifying the title of the x-axis. - /// * `y_title` - An optional `Text` struct specifying the title of the y-axis. - /// * `x_axis` - An optional reference to an `Axis` struct for customizing the x-axis. - /// * `y_axis` - An optional reference to an `Axis` struct for customizing the y-axis. - /// * `legend_title` - An optional `Text` struct specifying the title of the legend. - /// * `legend` - An optional reference to a `Legend` struct for customizing the legend of the plot (e.g., positioning, font, etc.). - /// - /// # Returns - /// - /// Returns an instance of `TimeSeriesPlot`. - /// - /// **Example** - /// - /// ``` - /// TimeSeriesPlot::builder() - /// .data(×eries_dataset) - /// .x("date") - /// .y("series_1") - /// .additional_series(vec!["series_2"]) - /// .size(5) - /// .colors(vec![Rgb(255, 0, 0), Rgb(0, 255, 0)]) - /// .line_types(vec![LineType::Dash, LineType::Solid]) - /// .shapes(vec![Shape::Circle, Shape::Square]) - /// .plot_title( - /// Text::from("Time Series Plot") - /// .font("Arial") - /// .size(18) - /// ) - /// .x_title( - /// Text::from("date") - /// .font("Arial") - /// .size(15) - /// ) - /// .y_title( - /// Text::from("sales") - /// .font("Arial") - /// .size(15) - /// ) - /// .legend_title( - /// Text::from("series") - /// .font("Arial") - /// .size(15) - /// ) - /// .legend( - /// &Legend::new() - /// .x(0.05) - /// .y(0.9) - /// ) - /// .build() - /// .plot(); - /// ``` - /// - /// ![Time Series Plot](https://imgur.com/k0FShJp.png) - #[builder(on(String, into), on(Text, into))] - pub fn new( - // Data - data: &DataFrame, - x: String, - y: String, - additional_series: Option>, - // Marker - size: Option, - color: Option, - colors: Option>, - with_shape: Option, - shape: Option, - shapes: Option>, - line_types: Option>, - line_width: Option, - // Layout - plot_title: Option, - x_title: Option, - y_title: Option, - legend_title: Option, - x_axis: Option<&Axis>, - y_axis: Option<&Axis>, - legend: Option<&Legend>, - ) -> Self { - let x_col = x.as_str(); - let y_col = y.as_str(); - - // Layout - let bar_mode = None; - - let layout = Self::create_layout( - bar_mode, - plot_title, - x_title, - x_axis, - y_title, - y_axis, - legend_title, - legend, - ); - - // Trace - let orientation = None; - let group = None; - let error = None; - let box_points = None; - let point_offset = None; - let jitter = None; - let opacity = None; - - let traces = Self::create_traces( - data, - x_col, - y_col, - orientation, - group, - error, - box_points, - point_offset, - jitter, - additional_series, - opacity, - size, - color, - colors, - with_shape, - shape, - shapes, - line_types, - line_width, - ); - - Self { traces, layout } - } -} - -impl LayoutPlotly for TimeSeriesPlot {} -impl Polar for TimeSeriesPlot {} -impl Mark for TimeSeriesPlot {} -impl Line for TimeSeriesPlot {} - -impl Trace for TimeSeriesPlot { - fn create_trace( - data: &DataFrame, - x_col: &str, - y_col: &str, - #[allow(unused_variables)] orientation: Option, - group_name: Option<&str>, - #[allow(unused_variables)] error: Option, - #[allow(unused_variables)] box_points: Option, - #[allow(unused_variables)] point_offset: Option, - #[allow(unused_variables)] jitter: Option, - with_shape: Option, - marker: Marker, - line: LinePlotly, - ) -> Box { - let x_data = Self::get_string_column(data, x_col); - let y_data = Self::get_numeric_column(data, y_col); - - let mut trace = Scatter::default().x(x_data).y(y_data); - - if let Some(with_shape) = with_shape { - if with_shape { - trace = trace.mode(Mode::LinesMarkers); - } else { - trace = trace.mode(Mode::Lines); - } - } - - trace = trace.marker(marker); - trace = trace.line(line); - - if let Some(name) = group_name { - trace = trace.name(name); - } - - trace - } - - fn create_traces( - data: &DataFrame, - x_col: &str, - y_col: &str, - orientation: Option, - #[allow(unused_variables)] group: Option, - error: Option, - box_points: Option, - point_offset: Option, - jitter: Option, - additional_series: Option>, - opacity: Option, - size: Option, - color: Option, - colors: Option>, - with_shape: Option, - shape: Option, - shapes: Option>, - line_type: Option>, - line_width: Option, - ) -> Vec> { - let mut traces: Vec> = Vec::new(); - - let mark = Self::create_marker(opacity, size); - let line = Self::create_line(); - - let series_mark = Self::set_color(&mark, &color, &colors, 0); - - let series_mark = Self::set_shape(&series_mark, &shape, &shapes, 0); - - let series_line = Self::set_line_type(&line, &line_type, line_width, 0); - - let group_name = Some(y_col); - - let trace = Self::create_trace( - data, - x_col, - y_col, - orientation.clone(), - group_name, - error.clone(), - box_points, - point_offset, - jitter, - with_shape, - series_mark, - series_line, - ); - - traces.push(trace); - - if let Some(additional_series) = additional_series { - let additional_series = additional_series.into_iter(); - - for (i, series) in additional_series.enumerate() { - let series_mark = Self::set_color(&mark, &color, &colors, i + 1); - - let series_mark = Self::set_shape(&series_mark, &shape, &shapes, i + 1); - - let series_line = Self::set_line_type(&line, &line_type, line_width, i + 1); - - let subset = data - .clone() - .lazy() - .select([col(x_col), col(series)]) - .collect() - .unwrap(); - - let group_name = Some(series); - - let trace = Self::create_trace( - &subset, - x_col, - series, - orientation.clone(), - group_name, - error.clone(), - box_points, - point_offset, - jitter, - with_shape, - series_mark, - series_line, - ); - - traces.push(trace); - } - } - - traces - } -} - -impl Plot for TimeSeriesPlot { - fn get_layout(&self) -> &Layout { - &self.layout - } - - fn get_traces(&self) -> &Vec> { - &self.traces - } -} diff --git a/src/traits/layout.rs b/src/traits/layout.rs deleted file mode 100644 index 10dfdae..0000000 --- a/src/traits/layout.rs +++ /dev/null @@ -1,227 +0,0 @@ -use plotly::{ - color::Rgb as RgbPlotly, - common::{Font, Title}, - layout::{Axis as AxisPlotly, BarMode, BoxMode, Legend as LegendPlotly}, - Layout, -}; - -use crate::{Axis, Legend, Text}; - -#[allow(clippy::too_many_arguments)] -pub(crate) trait LayoutPlotly { - fn create_layout( - bar_mode: Option, - plot_title: Option, - x_title: Option, - x_axis: Option<&Axis>, - y_title: Option, - y_axis: Option<&Axis>, - legend_title: Option, - legend: Option<&Legend>, - ) -> Layout { - let mut layout = Layout::new().box_mode(BoxMode::Group); - let mut x_axis_format = AxisPlotly::new(); - let mut y_axis_format = AxisPlotly::new(); - let mut legend_format = LegendPlotly::new(); - - if let Some(mode) = bar_mode { - layout = layout.bar_mode(mode); - } - - if let Some(title) = plot_title { - layout = layout.title(Self::set_title(title)); - } - - if let Some(title) = x_title { - x_axis_format = x_axis_format.title(Self::set_title(title)); - } - - if let Some(axis_details) = x_axis { - x_axis_format = Self::set_axis_format(x_axis_format, axis_details.clone()); - } - - layout = layout.x_axis(x_axis_format); - - if let Some(title) = y_title { - y_axis_format = y_axis_format.title(Self::set_title(title)); - } - - if let Some(axis_details) = y_axis { - y_axis_format = Self::set_axis_format(y_axis_format, axis_details.clone()); - } - - layout = layout.y_axis(y_axis_format); - - if let Some(title) = legend_title { - legend_format = legend_format.title(Self::set_title(title)); - } - - if let Some(legend_details) = legend { - legend_format = Self::set_legend_format(legend_format, legend_details.clone()); - } - - layout = layout.legend(legend_format); - - layout - } - - fn set_legend_format(mut legend_format: LegendPlotly, legend_details: Legend) -> LegendPlotly { - if let Some(color) = legend_details.background_color { - legend_format = - legend_format.background_color(RgbPlotly::new(color.0, color.1, color.2)); - } - - if let Some(color) = legend_details.border_color { - legend_format = legend_format.border_color(RgbPlotly::new(color.0, color.1, color.2)); - } - - if let Some(width) = legend_details.border_width { - legend_format = legend_format.border_width(width); - } - - if let Some(font) = legend_details.font { - legend_format = legend_format.font(Font::new().family(font.as_str())); - } - - if let Some(orientation) = legend_details.orientation { - legend_format = legend_format.orientation(orientation.get_orientation()); - } - - if let Some(x) = legend_details.x { - legend_format = legend_format.x(x); - } - - if let Some(y) = legend_details.y { - legend_format = legend_format.y(y); - } - - legend_format - } - - fn set_axis_format(mut x_axis_format: AxisPlotly, axis_details: Axis) -> AxisPlotly { - if let Some(visible) = axis_details.show_axis { - x_axis_format = x_axis_format.visible(visible); - } - - if let Some(axis_position) = axis_details.axis_side { - x_axis_format = x_axis_format.side(axis_position.get_side()); - } - - if let Some(axis_type) = axis_details.axis_type { - x_axis_format = x_axis_format.type_(axis_type.get_type()); - } - - if let Some(color) = axis_details.value_color { - x_axis_format = x_axis_format.color(RgbPlotly::new(color.0, color.1, color.2)); - } - - if let Some(range) = axis_details.value_range { - x_axis_format = x_axis_format.range(range); - } - - if let Some(thousands) = axis_details.value_thousands { - x_axis_format = x_axis_format.separate_thousands(thousands); - } - - if let Some(exponent) = axis_details.value_exponent { - x_axis_format = x_axis_format.exponent_format(exponent.get_exponent()); - } - - if let Some(range_values) = axis_details.tick_values { - x_axis_format = x_axis_format.tick_values(range_values); - } - - if let Some(tick_text) = axis_details.tick_labels { - x_axis_format = x_axis_format.tick_text(tick_text); - } - - if let Some(tick_direction) = axis_details.tick_direction { - x_axis_format = x_axis_format.ticks(tick_direction.get_direction()); - } - - if let Some(tick_length) = axis_details.tick_length { - x_axis_format = x_axis_format.tick_length(tick_length); - } - - if let Some(tick_width) = axis_details.tick_width { - x_axis_format = x_axis_format.tick_width(tick_width); - } - - if let Some(tick_color) = axis_details.tick_color { - x_axis_format = - x_axis_format.tick_color(RgbPlotly::new(tick_color.0, tick_color.1, tick_color.2)); - } - - if let Some(tick_angle) = axis_details.tick_angle { - x_axis_format = x_axis_format.tick_angle(tick_angle); - } - - if let Some(font) = axis_details.tick_font { - x_axis_format = x_axis_format.tick_font(Font::new().family(font.as_str())); - } - - if let Some(show_line) = axis_details.show_line { - x_axis_format = x_axis_format.show_line(show_line); - } - - if let Some(line_color) = axis_details.line_color { - x_axis_format = - x_axis_format.line_color(RgbPlotly::new(line_color.0, line_color.1, line_color.2)); - } - - if let Some(line_width) = axis_details.line_width { - x_axis_format = x_axis_format.line_width(line_width); - } - - if let Some(show_grid) = axis_details.show_grid { - x_axis_format = x_axis_format.show_grid(show_grid); - } - - if let Some(grid_color) = axis_details.grid_color { - x_axis_format = - x_axis_format.grid_color(RgbPlotly::new(grid_color.0, grid_color.1, grid_color.2)); - } - - if let Some(grid_width) = axis_details.grid_width { - x_axis_format = x_axis_format.grid_width(grid_width); - } - - if let Some(show_zero_line) = axis_details.show_zero_line { - x_axis_format = x_axis_format.zero_line(show_zero_line); - } - - if let Some(zero_line_color) = axis_details.zero_line_color { - x_axis_format = x_axis_format.zero_line_color(RgbPlotly::new( - zero_line_color.0, - zero_line_color.1, - zero_line_color.2, - )); - } - - if let Some(zero_line_width) = axis_details.zero_line_width { - x_axis_format = x_axis_format.zero_line_width(zero_line_width); - } - - if let Some(axis_position) = axis_details.axis_position { - x_axis_format = x_axis_format.position(axis_position); - } - - x_axis_format - } - - fn set_title(title_details: Text) -> Title { - Title::with_text(title_details.content) - .font( - Font::new() - .family(title_details.font.as_str()) - .size(title_details.size) - .color(RgbPlotly::new( - title_details.color.0, - title_details.color.1, - title_details.color.2, - )), - ) - .x(title_details.x) - .y(title_details.y) - } -} diff --git a/src/traits/mod.rs b/src/traits/mod.rs deleted file mode 100644 index 011b5d7..0000000 --- a/src/traits/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub(crate) mod layout; -pub(crate) mod plot; -pub(crate) mod polar; -pub(crate) mod trace; diff --git a/src/traits/trace.rs b/src/traits/trace.rs deleted file mode 100644 index ace1294..0000000 --- a/src/traits/trace.rs +++ /dev/null @@ -1,120 +0,0 @@ -use plotly::{ - common::{Line as LinePlotly, Marker}, - Trace as TracePlotly, -}; - -use polars::frame::DataFrame; - -use crate::{ - aesthetics::{line::Line, mark::Mark}, - LineType, Orientation, Rgb, Shape, -}; - -use crate::traits::polar::Polar; - -pub(crate) trait Trace: Polar + Mark + Line { - #[allow(clippy::too_many_arguments)] - fn create_trace( - data: &DataFrame, - x_col: &str, - y_col: &str, - orientation: Option, - group_name: Option<&str>, - error: Option, - box_points: Option, - point_offset: Option, - jitter: Option, - with_shape: Option, - marker: Marker, - line: LinePlotly, - ) -> Box; - - #[allow(clippy::too_many_arguments)] - fn create_traces( - data: &DataFrame, - x_col: &str, - y_col: &str, - orientation: Option, - group: Option, - error: Option, - box_points: Option, - point_offset: Option, - jitter: Option, - #[allow(unused_variables)] additional_series: Option>, - opacity: Option, - size: Option, - color: Option, - colors: Option>, - with_shape: Option, - shape: Option, - shapes: Option>, - line_types: Option>, - line_width: Option, - ) -> Vec> { - let mark = Self::create_marker(opacity, size); - let mut line = Self::create_line(); - - let mut traces: Vec> = Vec::new(); - - match group { - Some(group) => { - let group_col = group.as_str(); - - let unique_groups = Self::get_unique_groups(data, group_col); - - let groups = unique_groups.iter().map(|s| s.as_str()); - - for (i, group_name) in groups.enumerate() { - let group_mark = Self::set_color(&mark, &color, &colors, i); - let group_mark = Self::set_shape(&group_mark, &shape, &shapes, i); - - line = Self::set_line_type(&line, &line_types, line_width, i); - - let subset = Self::filter_data_by_group(data, group_col, group_name); - - let trace = Self::create_trace( - &subset, - x_col, - y_col, - orientation.clone(), - Some(group_name), - error.clone(), - box_points, - point_offset, - jitter, - with_shape, - group_mark, - line.clone(), - ); - traces.push(trace); - } - } - None => { - let group_name = None; - let mut mark = mark.clone(); - - mark = Self::set_color(&mark, &color, &colors, 0); - mark = Self::set_shape(&mark, &shape, &shapes, 0); - - let trace = Self::create_trace( - data, - x_col, - y_col, - orientation, - group_name, - error, - box_points, - point_offset, - jitter, - with_shape, - mark, - line, - ); - - traces.push(trace); - } - } - - traces - } -}