use plotters::element::{Drawable, PointCollection};
use plotters::style::RGBColor;
use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
use crate::stats::{norm_ppf, quartiles, sorted_finite, StatsError};
use crate::style::{stroke_style, translucent_fill};
const DEFAULT_COLOR: RGBColor = RGBColor(0, 114, 178); const LINE_GRAY: RGBColor = RGBColor(120, 120, 120);
#[derive(Debug, Clone)]
pub struct QqPlot {
points: Vec<(f64, f64)>,
n: usize,
marker_radius: u32,
color: RGBColor,
line_width: u32,
show_line: bool,
}
impl QqPlot {
pub fn from_data(data: &[f64]) -> Result<Self, StatsError> {
let sorted = sorted_finite(data);
if sorted.is_empty() {
return Err(StatsError::EmptyInput);
}
let n = sorted.len();
let nf = n as f64;
let mut pts: Vec<(f64, f64)> = Vec::with_capacity(n);
for (i, &s) in sorted.iter().enumerate() {
let p = ((i as f64 + 1.0) - 0.375) / (nf + 0.25);
pts.push((norm_ppf(p), s));
}
let q = quartiles(&sorted)?;
let tx1 = norm_ppf(0.25);
let tx3 = norm_ppf(0.75);
let (slope, intercept) = if tx3 > tx1 {
let m = (q.q3 - q.q1) / (tx3 - tx1);
(m, q.q1 - m * tx1)
} else {
(1.0, 0.0)
};
let x_lo = pts.first().map(|p| p.0).unwrap_or(-3.0);
let x_hi = pts.last().map(|p| p.0).unwrap_or(3.0);
pts.push((x_lo, slope * x_lo + intercept));
pts.push((x_hi, slope * x_hi + intercept));
Ok(Self {
points: pts,
n,
marker_radius: 3,
color: DEFAULT_COLOR,
line_width: 1,
show_line: true,
})
}
pub fn reference_line(mut self, show: bool) -> Self {
self.show_line = show;
self
}
pub fn color(mut self, color: RGBColor) -> Self {
self.color = color;
self
}
pub fn marker_radius(mut self, radius: u32) -> Self {
self.marker_radius = radius;
self
}
pub fn line_width(mut self, width: u32) -> Self {
self.line_width = width;
self
}
}
impl<'a> PointCollection<'a, (f64, f64)> for &'a QqPlot {
type Point = &'a (f64, f64);
type IntoIter = &'a [(f64, f64)];
fn point_iter(self) -> &'a [(f64, f64)] {
&self.points
}
}
impl<DB: DrawingBackend> Drawable<DB> for QqPlot {
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() < self.n + 2 {
return Ok(());
}
if self.show_line {
backend.draw_line(
pix[self.n],
pix[self.n + 1],
&stroke_style(&LINE_GRAY, self.line_width),
)?;
}
let fill = translucent_fill(&self.color, 0.7);
for p in &pix[..self.n] {
backend.draw_circle(*p, self.marker_radius, &fill, true)?;
}
Ok(())
}
}