| use std::marker::PhantomData; |
| |
| use crate::data::Quartiles; |
| use crate::element::{Drawable, PointCollection}; |
| use crate::style::{Color, ShapeStyle, BLACK}; |
| use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind}; |
| |
| /// The boxplot orientation trait |
| pub trait BoxplotOrient<K, V> { |
| type XType; |
| type YType; |
| |
| fn make_coord(key: K, val: V) -> (Self::XType, Self::YType); |
| fn with_offset(coord: BackendCoord, offset: f64) -> BackendCoord; |
| } |
| |
| /// The vertical boxplot phantom |
| pub struct BoxplotOrientV<K, V>(PhantomData<(K, V)>); |
| |
| /// The horizontal boxplot phantom |
| pub struct BoxplotOrientH<K, V>(PhantomData<(K, V)>); |
| |
| impl<K, V> BoxplotOrient<K, V> for BoxplotOrientV<K, V> { |
| type XType = K; |
| type YType = V; |
| |
| fn make_coord(key: K, val: V) -> (K, V) { |
| (key, val) |
| } |
| |
| fn with_offset(coord: BackendCoord, offset: f64) -> BackendCoord { |
| (coord.0 + offset as i32, coord.1) |
| } |
| } |
| |
| impl<K, V> BoxplotOrient<K, V> for BoxplotOrientH<K, V> { |
| type XType = V; |
| type YType = K; |
| |
| fn make_coord(key: K, val: V) -> (V, K) { |
| (val, key) |
| } |
| |
| fn with_offset(coord: BackendCoord, offset: f64) -> BackendCoord { |
| (coord.0, coord.1 + offset as i32) |
| } |
| } |
| |
| const DEFAULT_WIDTH: u32 = 10; |
| |
| /// The boxplot element |
| pub struct Boxplot<K, O: BoxplotOrient<K, f32>> { |
| style: ShapeStyle, |
| width: u32, |
| whisker_width: f64, |
| offset: f64, |
| key: K, |
| values: [f32; 5], |
| _p: PhantomData<O>, |
| } |
| |
| impl<K: Clone> Boxplot<K, BoxplotOrientV<K, f32>> { |
| /// 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, quartiles: &Quartiles) -> Self { |
| Self { |
| style: Into::<ShapeStyle>::into(&BLACK), |
| width: DEFAULT_WIDTH, |
| whisker_width: 1.0, |
| offset: 0.0, |
| key, |
| values: quartiles.values(), |
| _p: PhantomData, |
| } |
| } |
| } |
| |
| impl<K: Clone> Boxplot<K, BoxplotOrientH<K, f32>> { |
| /// 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, quartiles: &Quartiles) -> Self { |
| Self { |
| style: Into::<ShapeStyle>::into(&BLACK), |
| width: DEFAULT_WIDTH, |
| whisker_width: 1.0, |
| offset: 0.0, |
| key, |
| values: quartiles.values(), |
| _p: PhantomData, |
| } |
| } |
| } |
| |
| impl<K, O: BoxplotOrient<K, f32>> Boxplot<K, O> { |
| /// 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<S: Into<ShapeStyle>>(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<T: Into<f64> + Copy>(mut self, offset: T) -> Self { |
| self.offset = offset.into(); |
| self |
| } |
| } |
| |
| impl<'a, K: Clone, O: BoxplotOrient<K, f32>> PointCollection<'a, (O::XType, O::YType)> |
| for &'a Boxplot<K, O> |
| { |
| type Point = (O::XType, O::YType); |
| type IntoIter = Vec<Self::Point>; |
| fn point_iter(self) -> Self::IntoIter { |
| self.values |
| .iter() |
| .map(|v| O::make_coord(self.key.clone(), *v)) |
| .collect() |
| } |
| } |
| |
| impl<K, DB: DrawingBackend, O: BoxplotOrient<K, f32>> Drawable<DB> for Boxplot<K, O> { |
| fn draw<I: Iterator<Item = BackendCoord>>( |
| &self, |
| points: I, |
| backend: &mut DB, |
| _: (u32, u32), |
| ) -> Result<(), DrawingErrorKind<DB::ErrorType>> { |
| let points: Vec<_> = points.take(5).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, |
| )?; |
| } |
| 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()); |
| } |
| } |