From 31bee4c35cd8cde9f0e899879ea0e33c90a2b38c Mon Sep 17 00:00:00 2001 From: Tobias van Driessel Date: Thu, 11 Mar 2021 14:41:17 +0100 Subject: [PATCH 1/5] added boxplot with outliers --- Cargo.toml | 3 +- src/element/boxplot_outliers.rs | 343 ++++++++++++++++++++++++++++++++ src/element/mod.rs | 5 + src/lib.rs | 2 + 4 files changed, 352 insertions(+), 1 deletion(-) create mode 100644 src/element/boxplot_outliers.rs diff --git a/Cargo.toml b/Cargo.toml index f675967a..2529cb7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,7 +61,7 @@ default = [ "deprecated_items", "all_series", "all_elements" ] all_series = ["area_series", "line_series", "point_series", "surface_series"] -all_elements = ["errorbar", "candlestick", "boxplot", "histogram"] +all_elements = ["errorbar", "candlestick", "boxplot", "boxplot_outliers", "histogram"] # Tier 1 Backends bitmap_backend = ["plotters-bitmap", "ttf"] @@ -73,6 +73,7 @@ svg_backend = ["plotters-svg"] errorbar = [] candlestick = [] boxplot = [] +boxplot_outliers = [] # Series histogram = [] diff --git a/src/element/boxplot_outliers.rs b/src/element/boxplot_outliers.rs new file mode 100644 index 00000000..4d94c07e --- /dev/null +++ b/src/element/boxplot_outliers.rs @@ -0,0 +1,343 @@ +use std::{cmp::max, marker::PhantomData}; + +use super::boxplot::{BoxplotOrient, BoxplotOrientH, BoxplotOrientV}; +use crate::element::{Drawable, PointCollection}; +use crate::style::{Color, ShapeStyle, BLACK}; +use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind}; + +const DEFAULT_WIDTH: u32 = 10; + +pub struct BoxplotData { + minimum: f64, + lower_quartile: f64, + median: f64, + upper_quartile: f64, + maximum: f64, + outliers: Vec, +} + +impl BoxplotData { + // Extract a value representing the `pct` percentile of a + // sorted `s`, using linear interpolation. + fn percentile_of_sorted + Copy>(s: &[T], pct: f64) -> f64 { + assert!(!s.is_empty()); + if s.len() == 1 { + return s[0].into(); + } + assert!(0_f64 <= pct); + let hundred = 100_f64; + assert!(pct <= hundred); + if (pct - hundred).abs() < std::f64::EPSILON { + return s[s.len() - 1].into(); + } + let length = (s.len() - 1) as f64; + let rank = (pct / hundred) * length; + let lower_rank = rank.floor(); + let d = rank - lower_rank; + let n = lower_rank as usize; + let lo = s[n].into(); + let hi = s[n + 1].into(); + lo + (hi - lo) * d + } + + pub fn values(&self) -> [f32; 5] { + [ + self.minimum as f32, + self.lower_quartile as f32, + self.median as f32, + self.upper_quartile as f32, + self.maximum as f32, + ] + } + + pub fn new + Copy + PartialOrd>(values: &[T]) -> Self { + let mut values = values.to_owned(); + values.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap()); + + let lower = BoxplotData::percentile_of_sorted(&values, 25_f64); + let median = BoxplotData::percentile_of_sorted(&values, 50_f64); + let upper = BoxplotData::percentile_of_sorted(&values, 75_f64); + let iqr = upper - lower; + let lower_fence = lower - 1.5 * iqr; + let upper_fence = upper + 1.5 * iqr; + + let mut outliers = Vec::with_capacity(values.len() / 2); + + let mut minimum = None; + let mut maximum = None; + + for v in values { + if v.into() < lower_fence || v.into() > upper_fence { + outliers.push(v.into()); + } else { + if minimum.is_none() { + minimum = Some(v.into()); + } + maximum = Some(v.into()); + } + } + + assert!(minimum.is_some()); + assert!(maximum.is_some()); + + Self { + minimum: minimum.unwrap(), + lower_quartile: lower, + median, + upper_quartile: upper, + maximum: maximum.unwrap(), + outliers + } + } +} +/// The boxplot element +pub struct BoxplotOutliers> { + style: ShapeStyle, + width: u32, + whisker_width: f64, + offset: f64, + key: K, + values: [f32; 5], + outliers: Vec, + _p: PhantomData, +} + +impl BoxplotOutliers> { + /// Create a new vertical boxplot element. + /// + /// - `key`: The key (the X axis value) + /// - `quartiles`: The quartiles values for the Y axis + /// - **returns** The newly created boxplot element + /// + /// ```rust + /// use plotters::prelude::*; + /// + /// let quartiles = Quartiles::new(&[7, 15, 36, 39, 40, 41]); + /// let plot = Boxplot::new_vertical("group", &quartiles); + /// ``` + pub fn new_vertical(key: K, boxplot_data: &BoxplotData) -> Self { + let outliers = boxplot_data.outliers.iter().map(|o| *o as f32).collect(); + Self { + style: Into::::into(&BLACK), + width: DEFAULT_WIDTH, + whisker_width: 1.0, + offset: 0.0, + key, + values: boxplot_data.values(), + outliers, + _p: PhantomData, + } + } +} + +impl BoxplotOutliers> { + /// Create a new horizontal boxplot element. + /// + /// - `key`: The key (the Y axis value) + /// - `quartiles`: The quartiles values for the X axis + /// - **returns** The newly created boxplot element + /// + /// ```rust + /// use plotters::prelude::*; + /// + /// let quartiles = Quartiles::new(&[7, 15, 36, 39, 40, 41]); + /// let plot = Boxplot::new_horizontal("group", &quartiles); + /// ``` + pub fn new_horizontal(key: K, boxplot_data: &BoxplotData) -> Self { + let outliers = boxplot_data.outliers.iter().map(|o| *o as f32).collect(); + Self { + style: Into::::into(&BLACK), + width: DEFAULT_WIDTH, + whisker_width: 1.0, + offset: 0.0, + key, + values: boxplot_data.values(), + outliers, + _p: PhantomData, + } + } +} + +impl> BoxplotOutliers { + /// Set the style of the boxplot. + /// + /// - `S`: The required style + /// - **returns** The up-to-dated boxplot element + /// + /// ```rust + /// use plotters::prelude::*; + /// + /// let quartiles = Quartiles::new(&[7, 15, 36, 39, 40, 41]); + /// let plot = Boxplot::new_horizontal("group", &quartiles).style(&BLUE); + /// ``` + pub fn style>(mut self, style: S) -> Self { + self.style = style.into(); + self + } + + /// Set the bar width. + /// + /// - `width`: The required width + /// - **returns** The up-to-dated boxplot element + /// + /// ```rust + /// use plotters::prelude::*; + /// + /// let quartiles = Quartiles::new(&[7, 15, 36, 39, 40, 41]); + /// let plot = Boxplot::new_horizontal("group", &quartiles).width(10); + /// ``` + pub fn width(mut self, width: u32) -> Self { + self.width = width; + self + } + + /// Set the width of the whiskers as a fraction of the bar width. + /// + /// - `whisker_width`: The required fraction + /// - **returns** The up-to-dated boxplot element + /// + /// ```rust + /// use plotters::prelude::*; + /// + /// let quartiles = Quartiles::new(&[7, 15, 36, 39, 40, 41]); + /// let plot = Boxplot::new_horizontal("group", &quartiles).whisker_width(0.5); + /// ``` + pub fn whisker_width(mut self, whisker_width: f64) -> Self { + self.whisker_width = whisker_width; + self + } + + /// Set the element offset on the key axis. + /// + /// - `offset`: The required offset (on the X axis for vertical, on the Y axis for horizontal) + /// - **returns** The up-to-dated boxplot element + /// + /// ```rust + /// use plotters::prelude::*; + /// + /// let quartiles = Quartiles::new(&[7, 15, 36, 39, 40, 41]); + /// let plot = Boxplot::new_horizontal("group", &quartiles).offset(-5); + /// ``` + pub fn offset + Copy>(mut self, offset: T) -> Self { + self.offset = offset.into(); + self + } +} + +impl<'a, K: Clone, O: BoxplotOrient> PointCollection<'a, (O::XType, O::YType)> + for &'a BoxplotOutliers +{ + type Point = (O::XType, O::YType); + type IntoIter = Vec; + fn point_iter(self) -> Self::IntoIter { + let mut points: Vec = self.values + .iter() + .map(|v| O::make_coord(self.key.clone(), *v)) + .collect(); + for i in 0..self.outliers.len() { + points.push(O::make_coord(self.key.clone(), self.outliers[i])); + } + points + } +} + +impl> Drawable for BoxplotOutliers { + fn draw>( + &self, + points: I, + backend: &mut DB, + _: (u32, u32), + ) -> Result<(), DrawingErrorKind> { + let points: Vec<_> = points.collect(); + if points.len() >= 5 { + let width = f64::from(self.width); + let moved = |coord| O::with_offset(coord, self.offset); + let start_bar = |coord| O::with_offset(moved(coord), -width / 2.0); + let end_bar = |coord| O::with_offset(moved(coord), width / 2.0); + let start_whisker = + |coord| O::with_offset(moved(coord), -width * self.whisker_width / 2.0); + let end_whisker = + |coord| O::with_offset(moved(coord), width * self.whisker_width / 2.0); + + // |---[ | ]----| + // ^________________ + backend.draw_line( + start_whisker(points[0]), + end_whisker(points[0]), + &self.style, + )?; + + // |---[ | ]----| + // _^^^_____________ + + backend.draw_line( + moved(points[0]), + moved(points[1]), + &self.style.color.to_backend_color(), + )?; + + // |---[ | ]----| + // ____^______^_____ + let corner1 = start_bar(points[3]); + let corner2 = end_bar(points[1]); + let upper_left = (corner1.0.min(corner2.0), corner1.1.min(corner2.1)); + let bottom_right = (corner1.0.max(corner2.0), corner1.1.max(corner2.1)); + backend.draw_rect(upper_left, bottom_right, &self.style, false)?; + + // |---[ | ]----| + // ________^________ + backend.draw_line(start_bar(points[2]), end_bar(points[2]), &self.style)?; + + // |---[ | ]----| + // ____________^^^^_ + backend.draw_line(moved(points[3]), moved(points[4]), &self.style)?; + + // |---[ | ]----| + // ________________^ + backend.draw_line( + start_whisker(points[4]), + end_whisker(points[4]), + &self.style, + )?; + + for i in 5..points.len() { + backend.draw_circle(moved(points[i]), (width / 2.0) as u32, &self.style, false)?; + } + } + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::prelude::*; + + #[test] + fn test_draw_v() { + let root = MockedBackend::new(1024, 768).into_drawing_area(); + let chart = ChartBuilder::on(&root) + .build_cartesian_2d(0..2, 0f32..100f32) + .unwrap(); + + let values = Quartiles::new(&[6]); + assert!(chart + .plotting_area() + .draw(&Boxplot::new_vertical(1, &values)) + .is_ok()); + } + + #[test] + fn test_draw_h() { + let root = MockedBackend::new(1024, 768).into_drawing_area(); + let chart = ChartBuilder::on(&root) + .build_cartesian_2d(0f32..100f32, 0..2) + .unwrap(); + + let values = Quartiles::new(&[6]); + assert!(chart + .plotting_area() + .draw(&Boxplot::new_horizontal(1, &values)) + .is_ok()); + } +} diff --git a/src/element/mod.rs b/src/element/mod.rs index 41f95fdb..267aed2f 100644 --- a/src/element/mod.rs +++ b/src/element/mod.rs @@ -187,6 +187,11 @@ mod boxplot; #[cfg(feature = "boxplot")] pub use boxplot::Boxplot; +#[cfg(feature = "boxplot_outliers")] +mod boxplot_outliers; +#[cfg(feature = "boxplot_outliers")] +pub use boxplot_outliers::BoxplotOutliers; + #[cfg(feature = "bitmap_backend")] mod image; #[cfg(feature = "bitmap_backend")] diff --git a/src/lib.rs b/src/lib.rs index 81a297db..359be41d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -799,6 +799,8 @@ pub mod prelude { #[cfg(feature = "boxplot")] pub use crate::element::Boxplot; + #[cfg(feature = "boxplot_outliers")] + pub use crate::element::BoxplotOutliers; #[cfg(feature = "candlestick")] pub use crate::element::CandleStick; #[cfg(feature = "errorbar")] From be0f97371493ce4e9c9e713b5f7da89acd6ecfd5 Mon Sep 17 00:00:00 2001 From: Tobias van Driessel Date: Thu, 11 Mar 2021 14:55:53 +0100 Subject: [PATCH 2/5] public BoxplotData --- Cargo.toml | 2 +- src/element/boxplot_outliers.rs | 1 + src/element/mod.rs | 2 ++ src/lib.rs | 2 ++ 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 2529cb7e..60632dc9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "plotters" -version = "0.3.0" +version = "0.3.1" authors = ["Hao Hou "] edition = "2018" license = "MIT" diff --git a/src/element/boxplot_outliers.rs b/src/element/boxplot_outliers.rs index 4d94c07e..5c9726d9 100644 --- a/src/element/boxplot_outliers.rs +++ b/src/element/boxplot_outliers.rs @@ -7,6 +7,7 @@ use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind}; const DEFAULT_WIDTH: u32 = 10; +#[derive(Clone, Debug)] pub struct BoxplotData { minimum: f64, lower_quartile: f64, diff --git a/src/element/mod.rs b/src/element/mod.rs index 267aed2f..5df61979 100644 --- a/src/element/mod.rs +++ b/src/element/mod.rs @@ -191,6 +191,8 @@ pub use boxplot::Boxplot; mod boxplot_outliers; #[cfg(feature = "boxplot_outliers")] pub use boxplot_outliers::BoxplotOutliers; +#[cfg(feature = "boxplot_outliers")] +pub use boxplot_outliers::BoxplotData; #[cfg(feature = "bitmap_backend")] mod image; diff --git a/src/lib.rs b/src/lib.rs index 359be41d..1f436609 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -801,6 +801,8 @@ pub mod prelude { pub use crate::element::Boxplot; #[cfg(feature = "boxplot_outliers")] pub use crate::element::BoxplotOutliers; + #[cfg(feature = "boxplot_outliers")] + pub use crate::element::BoxplotData; #[cfg(feature = "candlestick")] pub use crate::element::CandleStick; #[cfg(feature = "errorbar")] From 46fcbace29c858bb7b691e70eaf91b4274e271c8 Mon Sep 17 00:00:00 2001 From: Tobias van Driessel Date: Thu, 11 Mar 2021 17:13:35 +0100 Subject: [PATCH 3/5] set version back to correct version, updated doc comments and tests --- Cargo.toml | 2 +- src/element/boxplot_outliers.rs | 132 +++++++++++++++++++++----------- 2 files changed, 87 insertions(+), 47 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 60632dc9..2529cb7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "plotters" -version = "0.3.1" +version = "0.3.0" authors = ["Hao Hou "] edition = "2018" license = "MIT" diff --git a/src/element/boxplot_outliers.rs b/src/element/boxplot_outliers.rs index 5c9726d9..875583a9 100644 --- a/src/element/boxplot_outliers.rs +++ b/src/element/boxplot_outliers.rs @@ -1,4 +1,4 @@ -use std::{cmp::max, marker::PhantomData}; +use std::{marker::PhantomData}; use super::boxplot::{BoxplotOrient, BoxplotOrientH, BoxplotOrientV}; use crate::element::{Drawable, PointCollection}; @@ -7,6 +7,7 @@ use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind}; const DEFAULT_WIDTH: u32 = 10; +///Structure to contain the boxplot data with outliers #[derive(Clone, Debug)] pub struct BoxplotData { minimum: f64, @@ -19,7 +20,8 @@ pub struct BoxplotData { impl BoxplotData { // Extract a value representing the `pct` percentile of a - // sorted `s`, using linear interpolation. + // sorted `s`, using linear interpolation. + // Copied from Quartiles. fn percentile_of_sorted + Copy>(s: &[T], pct: f64) -> f64 { assert!(!s.is_empty()); if s.len() == 1 { @@ -41,33 +43,34 @@ impl BoxplotData { lo + (hi - lo) * d } - pub fn values(&self) -> [f32; 5] { - [ - self.minimum as f32, - self.lower_quartile as f32, - self.median as f32, - self.upper_quartile as f32, - self.maximum as f32, - ] - } - - pub fn new + Copy + PartialOrd>(values: &[T]) -> Self { - let mut values = values.to_owned(); - values.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap()); + /// Create a new BoxplotData struct with the values calculated from the argument. + /// + /// - `s`: The array of the original values + /// - **returns** The newly created BoxplotData struct + /// + /// ```rust + /// use plotters::prelude::*; + /// + /// let boxplot_data = BoxplotData::new(&[7, 15, 36, 39, 40, 41]); + /// assert_eq!(boxplot_data.median(), 37.5); + /// ``` + pub fn new + Copy + PartialOrd>(s: &[T]) -> Self { + let mut s = s.to_owned(); + s.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap()); - let lower = BoxplotData::percentile_of_sorted(&values, 25_f64); - let median = BoxplotData::percentile_of_sorted(&values, 50_f64); - let upper = BoxplotData::percentile_of_sorted(&values, 75_f64); + let lower = BoxplotData::percentile_of_sorted(&s, 25_f64); + let median = BoxplotData::percentile_of_sorted(&s, 50_f64); + let upper = BoxplotData::percentile_of_sorted(&s, 75_f64); let iqr = upper - lower; let lower_fence = lower - 1.5 * iqr; let upper_fence = upper + 1.5 * iqr; - let mut outliers = Vec::with_capacity(values.len() / 2); + let mut outliers = Vec::with_capacity(s.len() / 2); let mut minimum = None; let mut maximum = None; - for v in values { + for v in s { if v.into() < lower_fence || v.into() > upper_fence { outliers.push(v.into()); } else { @@ -90,8 +93,43 @@ impl BoxplotData { outliers } } + + /// Get the Boxplot values (without outliers). + /// + /// - **returns** The array [minimum, lower quartile, median, upper quartile, maximum] + /// + /// ```rust + /// use plotters::prelude::*; + /// + /// let boxplot_data = BoxplotData::new(&[7, 15, 36, 39, 40, 41]); + /// let values = boxplot_data.values(); + /// assert_eq!(values, [7.0, 20.25, 37.5, 39.75, 41.0]); + /// ``` + pub fn values(&self) -> [f32; 5] { + [ + self.minimum as f32, + self.lower_quartile as f32, + self.median as f32, + self.upper_quartile as f32, + self.maximum as f32, + ] + } + + /// Get the Boxplot data median. + /// + /// - **returns** The median + /// + /// ```rust + /// use plotters::prelude::*; + /// + /// let boxplot_data = BoxplotData::new(&[7, 15, 36, 39, 40, 41]); + /// assert_eq!(boxplot_data.median(), 37.5); + /// ``` + pub fn median(&self) -> f64 { + self.median + } } -/// The boxplot element +/// The BoxplotOutliers element pub struct BoxplotOutliers> { style: ShapeStyle, width: u32, @@ -104,17 +142,17 @@ pub struct BoxplotOutliers> { } impl BoxplotOutliers> { - /// Create a new vertical boxplot element. + /// Create a new vertical BoxplotOutliers element. /// /// - `key`: The key (the X axis value) - /// - `quartiles`: The quartiles values for the Y axis - /// - **returns** The newly created boxplot element + /// - `boxplot_data`: The boxplot_data for the Y axis + /// - **returns** The newly created BoxplotOutliers element /// /// ```rust /// use plotters::prelude::*; /// - /// let quartiles = Quartiles::new(&[7, 15, 36, 39, 40, 41]); - /// let plot = Boxplot::new_vertical("group", &quartiles); + /// let boxplot_data = BoxplotData::new(&[7, 15, 36, 39, 40, 41]); + /// let plot = BoxplotOutliers::new_vertical("group", &boxplot_data); /// ``` pub fn new_vertical(key: K, boxplot_data: &BoxplotData) -> Self { let outliers = boxplot_data.outliers.iter().map(|o| *o as f32).collect(); @@ -132,17 +170,17 @@ impl BoxplotOutliers> { } impl BoxplotOutliers> { - /// Create a new horizontal boxplot element. + /// Create a new horizontal BoxplotOutliers element. /// /// - `key`: The key (the Y axis value) - /// - `quartiles`: The quartiles values for the X axis - /// - **returns** The newly created boxplot element + /// - `boxplot_data`: The boxplot_data for the X axis + /// - **returns** The newly created BoxplotOutliers element /// /// ```rust /// use plotters::prelude::*; /// - /// let quartiles = Quartiles::new(&[7, 15, 36, 39, 40, 41]); - /// let plot = Boxplot::new_horizontal("group", &quartiles); + /// let boxplot_data = BoxplotData::new(&[7, 15, 36, 39, 40, 41]); + /// let plot = BoxplotOutliers::new_vertical("group", &boxplot_data); /// ``` pub fn new_horizontal(key: K, boxplot_data: &BoxplotData) -> Self { let outliers = boxplot_data.outliers.iter().map(|o| *o as f32).collect(); @@ -160,16 +198,16 @@ impl BoxplotOutliers> { } impl> BoxplotOutliers { - /// Set the style of the boxplot. + /// Set the style of the BoxplotOutliers. /// /// - `S`: The required style - /// - **returns** The up-to-dated boxplot element + /// - **returns** The up-to-dated BoxplotOutliers element /// /// ```rust /// use plotters::prelude::*; /// - /// let quartiles = Quartiles::new(&[7, 15, 36, 39, 40, 41]); - /// let plot = Boxplot::new_horizontal("group", &quartiles).style(&BLUE); + /// let boxplot_data = BoxplotData::new(&[7, 15, 36, 39, 40, 41]); + /// let plot = BoxplotOutliers::new_horizontal("group", &boxplot_data).style(&BLUE); /// ``` pub fn style>(mut self, style: S) -> Self { self.style = style.into(); @@ -179,13 +217,13 @@ impl> BoxplotOutliers { /// Set the bar width. /// /// - `width`: The required width - /// - **returns** The up-to-dated boxplot element + /// - **returns** The up-to-dated BoxplotOutliers element /// /// ```rust /// use plotters::prelude::*; /// - /// let quartiles = Quartiles::new(&[7, 15, 36, 39, 40, 41]); - /// let plot = Boxplot::new_horizontal("group", &quartiles).width(10); + /// let boxplot_data = BoxplotData::new(&[7, 15, 36, 39, 40, 41]); + /// let plot = BoxplotOutliers::new_horizontal("group", &boxplot_data).width(10); /// ``` pub fn width(mut self, width: u32) -> Self { self.width = width; @@ -200,8 +238,8 @@ impl> BoxplotOutliers { /// ```rust /// use plotters::prelude::*; /// - /// let quartiles = Quartiles::new(&[7, 15, 36, 39, 40, 41]); - /// let plot = Boxplot::new_horizontal("group", &quartiles).whisker_width(0.5); + /// let boxplot_data = BoxplotData::new(&[7, 15, 36, 39, 40, 41]); + /// let plot = BoxplotOutliers::new_horizontal("group", &boxplot_data).whisker_width(0.5); /// ``` pub fn whisker_width(mut self, whisker_width: f64) -> Self { self.whisker_width = whisker_width; @@ -216,8 +254,8 @@ impl> BoxplotOutliers { /// ```rust /// use plotters::prelude::*; /// - /// let quartiles = Quartiles::new(&[7, 15, 36, 39, 40, 41]); - /// let plot = Boxplot::new_horizontal("group", &quartiles).offset(-5); + /// let boxplot_data = BoxplotData::new(&[7, 15, 36, 39, 40, 41]); + /// let plot = BoxplotOutliers::new_horizontal("group", &boxplot_data).offset(-5); /// ``` pub fn offset + Copy>(mut self, offset: T) -> Self { self.offset = offset.into(); @@ -301,6 +339,8 @@ impl> Drawable for BoxplotOu &self.style, )?; + // o o o |---[ | ]----| oo o + // ^__^_^_____________________^^_^ for i in 5..points.len() { backend.draw_circle(moved(points[i]), (width / 2.0) as u32, &self.style, false)?; } @@ -321,10 +361,10 @@ mod test { .build_cartesian_2d(0..2, 0f32..100f32) .unwrap(); - let values = Quartiles::new(&[6]); + let values = BoxplotData::new(&[6]); assert!(chart .plotting_area() - .draw(&Boxplot::new_vertical(1, &values)) + .draw(&BoxplotOutliers::new_vertical(1, &values)) .is_ok()); } @@ -335,10 +375,10 @@ mod test { .build_cartesian_2d(0f32..100f32, 0..2) .unwrap(); - let values = Quartiles::new(&[6]); + let values = BoxplotData::new(&[6]); assert!(chart .plotting_area() - .draw(&Boxplot::new_horizontal(1, &values)) + .draw(&BoxplotOutliers::new_horizontal(1, &values)) .is_ok()); } } From 7ebebb8425f9133749f85c4625e833a1df8fea94 Mon Sep 17 00:00:00 2001 From: Tobias van Driessel Date: Thu, 11 Mar 2021 17:38:05 +0100 Subject: [PATCH 4/5] fix overlap when max lower than upper or min higher than lower --- src/element/boxplot_outliers.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/element/boxplot_outliers.rs b/src/element/boxplot_outliers.rs index 875583a9..9a5d11a2 100644 --- a/src/element/boxplot_outliers.rs +++ b/src/element/boxplot_outliers.rs @@ -84,12 +84,23 @@ impl BoxplotData { assert!(minimum.is_some()); assert!(maximum.is_some()); + let mut minimum = minimum.unwrap(); + let mut maximum = maximum.unwrap(); + + //Make sure whiskers don't overlap with body of the upper-median-lower + if minimum > lower { + minimum = lower; + } + if maximum < upper { + maximum = upper; + } + Self { - minimum: minimum.unwrap(), + minimum, lower_quartile: lower, median, upper_quartile: upper, - maximum: maximum.unwrap(), + maximum, outliers } } From 32a577eaf6036173af705b300fdeac3fe18d5312 Mon Sep 17 00:00:00 2001 From: Tobias van Driessel Date: Thu, 11 Mar 2021 22:29:42 +0100 Subject: [PATCH 5/5] higher test coverage --- src/element/boxplot_outliers.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/element/boxplot_outliers.rs b/src/element/boxplot_outliers.rs index 9a5d11a2..d8284892 100644 --- a/src/element/boxplot_outliers.rs +++ b/src/element/boxplot_outliers.rs @@ -392,4 +392,32 @@ mod test { .draw(&BoxplotOutliers::new_horizontal(1, &values)) .is_ok()); } + + #[test] + fn test_draw_with_outliers() { + let root = MockedBackend::new(1024, 768).into_drawing_area(); + let chart = ChartBuilder::on(&root) + .build_cartesian_2d(0..2, 0f32..100f32) + .unwrap(); + + let values = BoxplotData::new(&[1,50,50,50,50,50,50,50,50,50,50,50,50,50]); + assert!(chart + .plotting_area() + .draw(&BoxplotOutliers::new_vertical(1, &values)) + .is_ok()); + } + + #[test] + fn test_draw_with_outliers_two_sides() { + let root = MockedBackend::new(1024, 768).into_drawing_area(); + let chart = ChartBuilder::on(&root) + .build_cartesian_2d(0..2, 0f32..100f32) + .unwrap(); + + let values = BoxplotData::new(&[1,50,50,50,50,50,100]); + assert!(chart + .plotting_area() + .draw(&BoxplotOutliers::new_vertical(1, &values)) + .is_ok()); + } }