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 @@
+
+
+
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(())
+}