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()); } }