Skip to main content

plotters_statistical/series/
ecdf.rs

1//! Empirical CDF series — a right-continuous step curve, optionally with a DKW
2//! confidence band and step markers.
3
4use plotters::element::{Drawable, PointCollection};
5use plotters::style::RGBColor;
6use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
7
8use crate::stats::{dkw_epsilon, ecdf, StatsError};
9use crate::style::{stroke_style, translucent_fill};
10
11const DEFAULT_COLOR: RGBColor = RGBColor(0, 114, 178); // Okabe–Ito blue
12
13/// An empirical cumulative distribution function as a drawable step series
14/// (coordinate space `(f64, f64)` = `(value, cumulative proportion)`).
15#[derive(Debug, Clone)]
16pub struct Ecdf {
17    ex: Vec<f64>,
18    ep: Vec<f64>,
19    n: usize,
20    complementary: bool,
21    ci_alpha: Option<f64>,
22    // Rendered geometry: [step vertices (n_step)] then, if a band, [band polygon
23    // (2 * n_step)].
24    points: Vec<(f64, f64)>,
25    n_step: usize,
26    has_band: bool,
27    color: RGBColor,
28    stroke_width: u32,
29    band_opacity: f64,
30    marker_radius: u32,
31    show_markers: bool,
32}
33
34impl Ecdf {
35    /// Build from a raw `data` sample. Errors if no finite values remain.
36    pub fn from_data(data: &[f64]) -> Result<Self, StatsError> {
37        let e = ecdf(data)?;
38        let mut this = Self {
39            ex: e.x,
40            ep: e.p,
41            n: e.n,
42            complementary: false,
43            ci_alpha: None,
44            points: Vec::new(),
45            n_step: 0,
46            has_band: false,
47            color: DEFAULT_COLOR,
48            stroke_width: 2,
49            band_opacity: 0.15,
50            marker_radius: 3,
51            show_markers: false,
52        };
53        this.rebuild();
54        Ok(this)
55    }
56
57    /// Plot the complementary ECDF (survival function, `1 - F`) instead.
58    pub fn complementary(mut self, yes: bool) -> Self {
59        self.complementary = yes;
60        self.rebuild();
61        self
62    }
63
64    /// Add a Dvoretzky–Kiefer–Wolfowitz confidence band at level `1 - alpha`
65    /// (e.g. `alpha = 0.05` for 95%).
66    pub fn confidence_band(mut self, alpha: f64) -> Self {
67        self.ci_alpha = Some(alpha);
68        self.rebuild();
69        self
70    }
71
72    /// Draw a marker at each observed step.
73    pub fn markers(mut self, show: bool) -> Self {
74        self.show_markers = show;
75        self
76    }
77
78    /// Set the line color.
79    pub fn color(mut self, color: RGBColor) -> Self {
80        self.color = color;
81        self
82    }
83
84    /// Set the line stroke width in pixels.
85    pub fn stroke_width(mut self, width: u32) -> Self {
86        self.stroke_width = width;
87        self
88    }
89
90    fn py(&self, i: usize) -> f64 {
91        if self.complementary {
92            1.0 - self.ep[i]
93        } else {
94            self.ep[i]
95        }
96    }
97
98    fn rebuild(&mut self) {
99        let m = self.ex.len();
100        let base = if self.complementary { 1.0 } else { 0.0 };
101        let mut verts: Vec<(f64, f64)> = Vec::with_capacity(2 * m);
102        verts.push((self.ex[0], base));
103        verts.push((self.ex[0], self.py(0)));
104        for i in 1..m {
105            verts.push((self.ex[i], self.py(i - 1)));
106            verts.push((self.ex[i], self.py(i)));
107        }
108        self.n_step = verts.len();
109
110        self.points = verts.clone();
111        self.has_band = false;
112        if let Some(alpha) = self.ci_alpha {
113            let eps = dkw_epsilon(self.n, alpha);
114            let mut band: Vec<(f64, f64)> = Vec::with_capacity(2 * self.n_step);
115            for &(x, y) in &verts {
116                band.push((x, (y + eps).clamp(0.0, 1.0)));
117            }
118            for &(x, y) in verts.iter().rev() {
119                band.push((x, (y - eps).clamp(0.0, 1.0)));
120            }
121            self.points.extend(band);
122            self.has_band = true;
123        }
124    }
125}
126
127impl<'a> PointCollection<'a, (f64, f64)> for &'a Ecdf {
128    type Point = &'a (f64, f64);
129    type IntoIter = &'a [(f64, f64)];
130    fn point_iter(self) -> &'a [(f64, f64)] {
131        &self.points
132    }
133}
134
135impl<DB: DrawingBackend> Drawable<DB> for Ecdf {
136    fn draw<I: Iterator<Item = BackendCoord>>(
137        &self,
138        points: I,
139        backend: &mut DB,
140        _parent_dim: (u32, u32),
141    ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
142        let pix: Vec<BackendCoord> = points.collect();
143        if pix.len() < self.n_step {
144            return Ok(());
145        }
146        if self.has_band && pix.len() >= self.n_step * 3 {
147            let band = &pix[self.n_step..self.n_step * 3];
148            backend.fill_polygon(
149                band.iter().copied(),
150                &translucent_fill(&self.color, self.band_opacity),
151            )?;
152        }
153        let step = &pix[..self.n_step];
154        backend.draw_path(
155            step.iter().copied(),
156            &stroke_style(&self.color, self.stroke_width),
157        )?;
158        if self.show_markers {
159            let fill = translucent_fill(&self.color, 0.9);
160            for k in (1..self.n_step).step_by(2) {
161                backend.draw_circle(step[k], self.marker_radius, &fill, true)?;
162            }
163        }
164        Ok(())
165    }
166}