It's tricky to implement grouped boxplot as we need to adjust the postions and width of the boxes to optimize the visualization layout without the prior knowledge on the group numbers and boxes numbers in each group. Here two helper function adjust_positions_and_width and adjust_positions_and_width_mat are defined to help users adjust the layout automatically.
We will generate an grouped boxplot as an example, where the data is a nested list for each group. For data as 2D array, please use adjust_positions_and_width_mat instead.
use plotpy::{Boxplot, AsMatrix, 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(())
}
// 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.
fn adjust_positions_and_width<T>(datasets: &Vec<&Vec<Vec<T>>>, gap: f64, span: f64) -> (Vec<Vec<f64>>, 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<f64> = ((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.
fn adjust_positions_and_width_mat<'a, T, U>(datasets: &Vec<&'a T>, gap: f64, span: f64) -> (Vec<Vec<f64>>, 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<f64> = ((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)
}
The following is the grouped boxplot generated by the above code:
It's tricky to implement grouped boxplot as we need to adjust the postions and width of the boxes to optimize the visualization layout without the prior knowledge on the group numbers and boxes numbers in each group. Here two helper function
adjust_positions_and_width
andadjust_positions_and_width_mat
are defined to help users adjust the layout automatically.We will generate an grouped boxplot as an example, where the data is a nested list for each group. For data as 2D array, please use
adjust_positions_and_width_mat
instead.The following is the grouped boxplot generated by the above code: