box-plotters 0.1.0

An alternative to the Boxplot and Quartiles types found in plotters
Documentation
use itertools::Itertools;

/// Stores the whisker positions, quartiles and median.
#[derive(Clone, Copy, Debug)]
pub struct Quartiles {
    pub lower_whisker: f64,
    pub lower_quartile: f64,
    pub median: f64,
    pub upper_quartile: f64,
    pub upper_whisker: f64,
}

impl Quartiles {
    /// Constructs a new [Quartiles] instance.
    pub fn new(
        lower_whisker: f64,
        lower_quartile: f64,
        median: f64,
        upper_quartile: f64,
        upper_whisker: f64,
    ) -> Self {
        Self { 
            lower_whisker, 
            lower_quartile, 
            median, 
            upper_quartile, 
            upper_whisker 
        }
    }

    /// Constructs a new [Quartiles] instance 
    /// where whiskers are calculated as the minimum and maximum value.
    /// Use [Quartiles::new_min_max_from_sorted] if the data is already sorted.
    pub fn new_min_max(values: &[f64]) -> Self {
        let sorted_values = values
            .iter()
            .cloned()
            .sorted_by(f64::total_cmp)
            .collect::<Vec<_>>();
        Self::new_min_max_from_sorted(&sorted_values)
    }

    /// Constructs a new [Quartiles] instance
    /// where whiskers are calculated as the minimum and maximum value.
    pub fn new_min_max_from_sorted(values: &[f64]) -> Self {
        let lower_whisker = quartile_from_sorted(values, 0.0);
        let lower_quartile = quartile_from_sorted(values, 1.0 / 4.0);
        let median = quartile_from_sorted(values, 1.0 / 2.0);
        let upper_quartile = quartile_from_sorted(values, 3.0 / 4.0);
        let upper_whisker = quartile_from_sorted(values, 1.0);
        Self::new(lower_whisker, lower_quartile, median, upper_quartile, upper_whisker)
    }

    /// Constructs a new [Quartiles] instance
    /// where whiskers are calculated using interquartile range
    /// (https://en.wikipedia.org/wiki/Interquartile_range).
    /// Use [Quartiles::new_iqr_from_sorted] if the data is already sorted.
    pub fn new_iqr(values: &[f64]) -> Self {
        let sorted_values = values
            .iter()
            .cloned()
            .sorted_by(f64::total_cmp)
            .collect::<Vec<_>>();
        Self::new_iqr_from_sorted(&sorted_values)
    }

    /// Constructs a new [Quartiles] instance from sorted data.
    /// where whiskers are calculated using interquartile range
    /// (https://en.wikipedia.org/wiki/Interquartile_range).
    pub fn new_iqr_from_sorted(values: &[f64]) -> Self {
        let min = quartile_from_sorted(values, 0.0);
        let lower_quartile = quartile_from_sorted(values, 1.0 / 4.0);
        let median = quartile_from_sorted(values, 1.0 / 2.0);
        let upper_quartile = quartile_from_sorted(values, 3.0 / 4.0);
        let max = quartile_from_sorted(values, 1.0);
        let iqr = upper_quartile - lower_quartile;
        let lower_whisker = (lower_quartile - 1.5 * iqr).max(min);
        let upper_whisker = (upper_quartile + 1.5 * iqr).min(max);
        Self::new(lower_whisker, lower_quartile, median, upper_quartile, upper_whisker)
    }

    /// Returns the fields as an array.
    pub fn values(&self) -> [f64; 5] {
        [
            self.lower_whisker,
            self.lower_quartile,
            self.median,
            self.upper_quartile,
            self.upper_whisker
        ]
    }
}

fn quartile_from_sorted(values: &[f64], t: f64) -> f64 {
    let p = t * (values.len() - 1) as f64;
    let a = values[p.floor() as usize];
    let b = values[p.ceil() as usize];
    lerp(a, b, p - p.floor())
}

fn lerp(a: f64, b: f64, t: f64) -> f64 {
    a + (b - a) * t
}

#[cfg(test)]
mod tests {
    use approx::assert_abs_diff_eq;

    use super::*;

    #[test]
    #[should_panic]
    fn test_min_max_panic() {
        Quartiles::new_min_max(&[]);
    }

    #[test]
    #[should_panic]
    fn test_iqr_panic() {
        Quartiles::new_iqr(&[]);
    }

    #[test]
    fn test_0_and_1_min_max() {
        let quartiles = Quartiles::new_min_max_from_sorted(&[0.0, 1.0]);
        assert_abs_diff_eq!(quartiles.lower_whisker, 0.0);
        assert_abs_diff_eq!(quartiles.lower_quartile, 0.25);
        assert_abs_diff_eq!(quartiles.median, 0.5);
        assert_abs_diff_eq!(quartiles.upper_quartile, 0.75);
        assert_abs_diff_eq!(quartiles.upper_whisker, 1.0);
    }

    #[test]
    fn test_0_and_1_iqr() {
        let quartiles = Quartiles::new_iqr_from_sorted(&[0.0, 1.0]);
        assert_abs_diff_eq!(quartiles.lower_whisker, 0.0);
        assert_abs_diff_eq!(quartiles.lower_quartile, 0.25);
        assert_abs_diff_eq!(quartiles.median, 0.5);
        assert_abs_diff_eq!(quartiles.upper_quartile, 0.75);
        assert_abs_diff_eq!(quartiles.upper_whisker, 1.0);
    }

    #[test]
    fn test_iqr_5_points() {
        let quartiles = Quartiles::new_iqr_from_sorted(&[0.0, 0.4, 0.5, 0.6, 1.0]);
        assert_abs_diff_eq!(quartiles.lower_whisker, 0.1);
        assert_abs_diff_eq!(quartiles.lower_quartile, 0.4);
        assert_abs_diff_eq!(quartiles.median, 0.5);
        assert_abs_diff_eq!(quartiles.upper_quartile, 0.6);
        assert_abs_diff_eq!(quartiles.upper_whisker, 0.9);
    }
}