diff --git a/figures/doc_boxplot_3.svg b/figures/doc_boxplot_3.svg new file mode 100644 index 0000000..3698e06 --- /dev/null +++ b/figures/doc_boxplot_3.svg @@ -0,0 +1,1198 @@ + + + + + + + + 2024-09-23T11:51:59.179497 + image/svg+xml + + + Matplotlib v3.9.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/boxplot.rs b/src/boxplot.rs index d6869bd..78b6623 100644 --- a/src/boxplot.rs +++ b/src/boxplot.rs @@ -82,6 +82,66 @@ use std::fmt::Write; /// /// ![doc_boxplot_1.svg](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/doc_boxplot_1.svg) /// +/// ## Grouped boxplot (Data as a nested list for each group) +/// +/// ``` +/// use plotpy::{Boxplot, Plot, StrError}; +/// +/// fn main() -> Result<(), StrError> { +/// let data1 = vec![ +/// vec![1, 2, 3, 4, 5], +/// vec![2, 3, 4, 5, 6], +/// vec![3, 4, 5, 6, 7], +/// vec![4, 5, 6, 7, 8], +/// vec![5, 6, 7, 8, 9],]; +/// let data2 = vec![ +/// vec![2, 3, 4, 5, 6], +/// vec![3, 4, 5, 6, 7], +/// vec![3, 2, 4, 7, 5], +/// vec![5, 6, 7, 8, 9], +/// vec![6, 7, 8, 9, 10],]; +/// let datasets = vec![&data1, &data2]; +/// +/// // Adjust the positions and width for each group +/// let (positions, width) = Boxplot::adjust_positions_and_width(&datasets, 0.1, 0.6); +/// +/// // x ticks and labels +/// let ticks: Vec<_> = (1..(datasets[0].len() + 1)).into_iter().collect(); +/// let labels = ["A", "B", "C", "D", "E"]; +/// +/// // boxplot objects and options +/// let mut boxes = Boxplot::new(); +/// boxes +/// .set_width(width) +/// .set_positions(&positions[0]) +/// .set_patch_artist(true) +/// .set_medianprops("{'color': 'black'}") +/// .set_boxprops("{'facecolor': 'C0'}") +/// .set_extra("label='group1'") // Legend label +/// .draw(&data1); +/// boxes +/// .set_width(width) +/// .set_positions(&positions[1]) +/// .set_patch_artist(true) +/// .set_medianprops("{'color': 'black'}") +/// .set_boxprops("{'facecolor': 'C1'}") +/// .set_extra("label='group2'") // Legend label +/// .draw(&data2); +/// +/// // Save figure +/// let mut plot = Plot::new(); +/// plot +/// .add(&boxes) +/// .legend() +/// .set_ticks_x_labels(&ticks, &labels) +/// .set_label_x("Time/s") +/// .set_label_y("Volumn/mL") +/// .save("/tmp/plotpy/doc_tests/doc_boxplot_3.svg")?; +/// Ok(()) +/// } +/// ``` +/// ![doc_boxplot_3.svg](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/doc_boxplot_3.svg) +/// /// ## More examples /// /// See also integration test in the **tests** directory. @@ -282,6 +342,85 @@ impl Boxplot { } opt } + + /// A helper function to adjust the boxes positions and width to beautify the layout when plotting grouped boxplot + /// + /// # Input + /// + /// * `datasets` is a sequence of data ( a sequence of 1D arrays) used by `draw`. + /// * `gap`: Shrink on the orient axis by this factor to add a gap between dodged elements. 0.0-0.5 usually gives a beautiful layout. + /// * `span`: The total width of boxes and gaps in a position. 0.5-1.0 usually gives a beautiful layout. + /// + /// # Notes + /// + /// * The type `T` must be a number. + pub fn adjust_positions_and_width(datasets: &Vec<&Vec>>, gap: f64, span: f64) -> (Vec>, f64) + where + T: std::fmt::Display, + { + let groups = datasets.len(); // The number of groups + let gap = gap; + let span = span; + + // Generate the adjusted width of a box + let mut width: f64 = 0.5; + width = width.min(span/(groups as f64 + (groups-1) as f64*gap)); + + // Generate the position offset for each box by an empirical formula. seaborn and plotnine all have their own algorithms. + let offsets: Vec = ((1 - groups as i64)..=(groups as i64 - 1)).step_by(2).map(|x| x as f64 * width * (1.0+gap)/2.0).collect(); + + let mut positions = Vec::new(); + for i in 0..groups { + let mut position = Vec::new(); + for j in 0..datasets[i].len() { + position.push((j+1) as f64 + offsets[i]); + } + positions.push(position); + } + + // Return the adjusted positions and width for each group + (positions, width) + } + + /// A helper function to adjust the boxes positions and width to beautify the layout for `draw_mat` when plotting grouped boxplot + /// + /// # Input + /// + /// * `datasets`: A sequence of data (2D array) used by `draw_mat`. + /// * `gap`: Shrink on the orient axis by this factor to add a gap between dodged elements. 0.0-0.5 usually gives a beautiful layout. + /// * `span`: The total width of boxes and gaps in a position. 0.0-1.0 usually gives a beautiful layout. + /// + /// # Notes + /// + /// * The type `U` must be a number. + pub fn adjust_positions_and_width_mat<'a, T, U>(datasets: &Vec<&'a T>, gap: f64, span: f64) -> (Vec>, f64) + where + T: AsMatrix<'a, U>, + U: 'a + std::fmt::Display, + { + let groups = datasets.len(); // The number of groups + let gap = gap; + let span = span; + + // Generate the adjusted width of a box + let mut width: f64 = 0.5; + width = width.min(span/(groups as f64 + (groups-1) as f64*gap)); + + // Generate the position offset for each box by an empirical formula. seaborn and plotnine all have their own algorithms. + let offsets: Vec = ((1 - groups as i64)..=(groups as i64 - 1)).step_by(2).map(|x| x as f64 * width * (1.0+gap)/2.0).collect(); + + let mut positions = Vec::new(); + for i in 0..groups { + let mut position = Vec::new(); + for j in 0..datasets[i].size().1 { + position.push((j+1) as f64 + offsets[i]); + } + positions.push(position); + } + + // Return the adjusted positions and width for each group + (positions, width) + } } impl GraphMaker for Boxplot { @@ -406,4 +545,46 @@ mod tests { boxes.clear_buffer(); assert_eq!(boxes.buffer, ""); } + + #[test] + fn adjust_positions_and_width_works() { + let data1 = vec![ + vec![1, 2, 3, 4, 5], + vec![2, 3, 4, 5, 6], + vec![3, 4, 5, 6, 7], + vec![4, 5, 6, 7, 8], + vec![5, 6, 7, 8, 9],]; + let data2 = vec![ + vec![2, 3, 4, 5, 6], + vec![3, 4, 5, 6, 7], + vec![3, 2, 4, 7, 5], + vec![5, 6, 7, 8, 9], + vec![6, 7, 8, 9, 10],]; + let datasets = vec![&data1, &data2]; + let (positions, width) = Boxplot::adjust_positions_and_width(&datasets, 0.1, 0.6); + assert_eq!(positions, vec![vec![0.8428571428571429, 1.842857142857143, 2.842857142857143, 3.842857142857143, 4.8428571428571425], + vec![1.157142857142857, 2.157142857142857, 3.157142857142857, 4.1571428571428575, 5.1571428571428575]]); + assert_eq!(width, 0.2857142857142857); + } + + #[test] + fn adjust_positions_and_width_mat_works() { + let data1 = vec![ + vec![1, 2, 3, 4, 5], + vec![2, 3, 4, 5, 6], + vec![3, 4, 5, 6, 7], + vec![4, 5, 6, 7, 8], + vec![5, 6, 7, 8, 9],]; + let data2 = vec![ + vec![2, 3, 4, 5, 6], + vec![3, 4, 5, 6, 7], + vec![3, 2, 4, 7, 5], + vec![5, 6, 7, 8, 9], + vec![6, 7, 8, 9, 10],]; + let datasets = vec![&data1, &data2]; + let (positions, width) = Boxplot::adjust_positions_and_width_mat(&datasets, 0.1, 0.6); + assert_eq!(positions, vec![vec![0.8428571428571429, 1.842857142857143, 2.842857142857143, 3.842857142857143, 4.8428571428571425], + vec![1.157142857142857, 2.157142857142857, 3.157142857142857, 4.1571428571428575, 5.1571428571428575]]); + assert_eq!(width, 0.2857142857142857); + } } diff --git a/tests/test_boxplot.rs b/tests/test_boxplot.rs index 3eb6163..e0224b7 100644 --- a/tests/test_boxplot.rs +++ b/tests/test_boxplot.rs @@ -215,3 +215,67 @@ fn test_boxplot_5() -> Result<(), StrError> { assert!(c > 460 && c < 500); Ok(()) } + +#[test] +fn test_boxplot_6() -> Result<(), StrError> { + let data1 = vec![ + vec![1, 2, 3, 4, 5], + vec![2, 3, 4, 5, 6], + vec![3, 4, 5, 6, 7], + vec![4, 5, 6, 7, 8], + vec![5, 6, 7, 8, 9],]; + let data2 = vec![ + vec![2, 3, 4, 5, 6], + vec![3, 4, 5, 6, 7], + vec![3, 2, 4, 7, 5], + vec![5, 6, 7, 8, 9], + vec![6, 7, 8, 9, 10],]; + let datasets = vec![&data1, &data2]; + + // Adjust the positions and width for each group + let (positions, width) = Boxplot::adjust_positions_and_width(&datasets, 0.1, 0.6); + + // x ticks and labels + let ticks: Vec<_> = (1..(datasets[0].len() + 1)).into_iter().collect(); + let labels = ["A", "B", "C", "D", "E"]; + + // boxplot objects and options + let mut boxes = Boxplot::new(); + boxes + .set_width(width) + .set_positions(&positions[0]) + .set_patch_artist(true) + .set_medianprops("{'color': 'black'}") + .set_boxprops("{'facecolor': 'C0'}") + .set_extra("label='group1'") + .draw(&data1); + boxes + .set_width(width) + .set_positions(&positions[1]) + .set_patch_artist(true) + .set_medianprops("{'color': 'black'}") + .set_boxprops("{'facecolor': 'C1'}") + .set_extra("label='group2'") + .draw(&data2); + + // Save figure + let mut plot = Plot::new(); + plot + .add(&boxes) + .legend() + .set_ticks_x_labels(&ticks, &labels) + .set_label_x("Time/s") + .set_label_y("Volumn/mL"); + + // save figure + let path = Path::new(OUT_DIR).join("integ_boxplot_6.svg"); + plot.save(&path)?; + + // check number of lines + let file = File::open(path).map_err(|_| "cannot open file")?; + let buffered = BufReader::new(file); + let lines_iter = buffered.lines(); + let c = lines_iter.count(); + assert!(c > 1150 && c < 1250); + Ok(()) +}