use plotters::element::{Drawable, PointCollection};
use plotters::style::{RGBColor, ShapeStyle};
use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
use crate::stats::{quartiles, Quartiles, StatsError};
use crate::style::{stroke_style, translucent_fill};
const DEFAULT_BOX_FILL: RGBColor = RGBColor(0, 114, 178); const NEAR_BLACK: RGBColor = RGBColor(30, 30, 30);
const OUTLIER_GRAY: RGBColor = RGBColor(90, 90, 90);
#[derive(Debug, Clone)]
pub struct BoxStyle {
pub box_fill: ShapeStyle,
pub box_border: ShapeStyle,
pub whisker: ShapeStyle,
pub median: ShapeStyle,
pub outlier: ShapeStyle,
pub outlier_radius: u32,
pub cap_ratio: f64,
}
impl Default for BoxStyle {
fn default() -> Self {
Self {
box_fill: translucent_fill(&DEFAULT_BOX_FILL, 0.45),
box_border: stroke_style(&NEAR_BLACK, 1),
whisker: stroke_style(&NEAR_BLACK, 1),
median: stroke_style(&NEAR_BLACK, 2),
outlier: translucent_fill(&OUTLIER_GRAY, 0.8),
outlier_radius: 3,
cap_ratio: 0.6,
}
}
}
#[derive(Debug, Clone)]
pub struct BoxPlot<C> {
points: Vec<C>,
width: u32,
horizontal: bool,
style: BoxStyle,
}
const N_BOX_POINTS: usize = 5;
const DEFAULT_WIDTH: u32 = 24;
impl<X: Clone> BoxPlot<(X, f64)> {
pub fn vertical(x: X, data: &[f64]) -> Result<Self, StatsError> {
Ok(Self::vertical_from_quartiles(x, &quartiles(data)?))
}
pub fn vertical_from_quartiles(x: X, q: &Quartiles) -> Self {
let mut points = vec![
(x.clone(), q.upper_whisker),
(x.clone(), q.q3),
(x.clone(), q.median),
(x.clone(), q.q1),
(x.clone(), q.lower_whisker),
];
points.extend(q.outliers.iter().map(|&o| (x.clone(), o)));
Self {
points,
width: DEFAULT_WIDTH,
horizontal: false,
style: BoxStyle::default(),
}
}
}
impl<Y: Clone> BoxPlot<(f64, Y)> {
pub fn horizontal(y: Y, data: &[f64]) -> Result<Self, StatsError> {
Ok(Self::horizontal_from_quartiles(y, &quartiles(data)?))
}
pub fn horizontal_from_quartiles(y: Y, q: &Quartiles) -> Self {
let mut points = vec![
(q.upper_whisker, y.clone()),
(q.q3, y.clone()),
(q.median, y.clone()),
(q.q1, y.clone()),
(q.lower_whisker, y.clone()),
];
points.extend(q.outliers.iter().map(|&o| (o, y.clone())));
Self {
points,
width: DEFAULT_WIDTH,
horizontal: true,
style: BoxStyle::default(),
}
}
}
impl<C> BoxPlot<C> {
pub fn width(mut self, width: u32) -> Self {
self.width = width;
self
}
pub fn style(mut self, style: BoxStyle) -> Self {
self.style = style;
self
}
pub fn with_style(mut self, f: impl FnOnce(&mut BoxStyle)) -> Self {
f(&mut self.style);
self
}
}
impl<'a, C: 'a> PointCollection<'a, C> for &'a BoxPlot<C> {
type Point = &'a C;
type IntoIter = &'a [C];
fn point_iter(self) -> &'a [C] {
&self.points
}
}
impl<C, DB: DrawingBackend> Drawable<DB> for BoxPlot<C> {
fn draw<I: Iterator<Item = BackendCoord>>(
&self,
points: I,
backend: &mut DB,
_parent_dim: (u32, u32),
) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
let pix: Vec<BackendCoord> = points.collect();
if pix.len() < N_BOX_POINTS {
return Ok(());
}
let (upper_w, q3, median, q1, lower_w) = (pix[0], pix[1], pix[2], pix[3], pix[4]);
let half = (self.width / 2) as i32;
let cap = ((self.width as f64 * self.style.cap_ratio) / 2.0).round() as i32;
let s = &self.style;
if self.horizontal {
let cy = q1.1; let (bx1, bx2) = (q1.0.min(q3.0), q1.0.max(q3.0));
backend.draw_line((lower_w.0, cy), (bx1, cy), &s.whisker)?;
backend.draw_line((bx2, cy), (upper_w.0, cy), &s.whisker)?;
backend.draw_line((lower_w.0, cy - cap), (lower_w.0, cy + cap), &s.whisker)?;
backend.draw_line((upper_w.0, cy - cap), (upper_w.0, cy + cap), &s.whisker)?;
backend.draw_rect((bx1, cy - half), (bx2, cy + half), &s.box_fill, true)?;
backend.draw_rect((bx1, cy - half), (bx2, cy + half), &s.box_border, false)?;
backend.draw_line((median.0, cy - half), (median.0, cy + half), &s.median)?;
} else {
let cx = q1.0; let (by1, by2) = (q3.1.min(q1.1), q3.1.max(q1.1));
backend.draw_line((cx, lower_w.1), (cx, by2), &s.whisker)?;
backend.draw_line((cx, by1), (cx, upper_w.1), &s.whisker)?;
backend.draw_line((cx - cap, lower_w.1), (cx + cap, lower_w.1), &s.whisker)?;
backend.draw_line((cx - cap, upper_w.1), (cx + cap, upper_w.1), &s.whisker)?;
backend.draw_rect((cx - half, by1), (cx + half, by2), &s.box_fill, true)?;
backend.draw_rect((cx - half, by1), (cx + half, by2), &s.box_border, false)?;
backend.draw_line((cx - half, median.1), (cx + half, median.1), &s.median)?;
}
for o in &pix[N_BOX_POINTS..] {
backend.draw_circle(*o, s.outlier_radius, &s.outlier, s.outlier.filled)?;
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct BoxPlotSeries<C> {
boxes: Vec<BoxPlot<C>>,
}
impl<X: Clone> BoxPlotSeries<(X, f64)> {
pub fn from_samples<I, S>(groups: I) -> Result<Self, StatsError>
where
I: IntoIterator<Item = (X, S)>,
S: AsRef<[f64]>,
{
let boxes = groups
.into_iter()
.map(|(x, s)| BoxPlot::vertical(x, s.as_ref()))
.collect::<Result<Vec<_>, _>>()?;
Ok(Self { boxes })
}
}
impl<Y: Clone> BoxPlotSeries<(f64, Y)> {
pub fn horizontal_from_samples<I, S>(groups: I) -> Result<Self, StatsError>
where
I: IntoIterator<Item = (Y, S)>,
S: AsRef<[f64]>,
{
let boxes = groups
.into_iter()
.map(|(y, s)| BoxPlot::horizontal(y, s.as_ref()))
.collect::<Result<Vec<_>, _>>()?;
Ok(Self { boxes })
}
}
impl<C> BoxPlotSeries<C> {
pub fn width(mut self, width: u32) -> Self {
self.boxes = self.boxes.into_iter().map(|b| b.width(width)).collect();
self
}
pub fn style(mut self, style: BoxStyle) -> Self {
self.boxes = self
.boxes
.into_iter()
.map(|b| b.style(style.clone()))
.collect();
self
}
}
impl<C> IntoIterator for BoxPlotSeries<C> {
type Item = BoxPlot<C>;
type IntoIter = std::vec::IntoIter<BoxPlot<C>>;
fn into_iter(self) -> Self::IntoIter {
self.boxes.into_iter()
}
}