Skip to main content

plotters_statistical/series/
violin_plot.rs

1//! Violin plot series — a mirrored KDE outline, optionally with an embedded box
2//! plot, built on [`crate::stats::kde`] and reusing [`BoxPlot`](crate::BoxPlot)'s geometry.
3
4use plotters::element::{Drawable, PointCollection};
5use plotters::style::{RGBColor, ShapeStyle};
6use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
7
8use crate::series::box_plot::BoxStyle;
9use crate::stats::{kde_curve, quartiles, StatsError};
10use crate::style::{stroke_style, translucent_fill};
11
12const DEFAULT_VIOLIN_FILL: RGBColor = RGBColor(0, 158, 115); // Okabe–Ito green
13const DEFAULT_VIOLIN_WIDTH: u32 = 60;
14const DEFAULT_KDE_POINTS: usize = 200;
15const EMBEDDED_BOX_WIDTH: u32 = 10;
16const N_BOX_POINTS: usize = 5;
17
18/// Styling for a violin plot.
19#[derive(Debug, Clone)]
20pub struct ViolinStyle {
21    /// Fill of the violin body.
22    pub fill: ShapeStyle,
23    /// Outline of the violin body.
24    pub outline: ShapeStyle,
25    /// Whether to draw the embedded box plot inside the violin.
26    pub show_box: bool,
27}
28
29impl Default for ViolinStyle {
30    fn default() -> Self {
31        Self {
32            fill: translucent_fill(&DEFAULT_VIOLIN_FILL, 0.4),
33            outline: stroke_style(&DEFAULT_VIOLIN_FILL, 2),
34            show_box: false,
35        }
36    }
37}
38
39/// A single violin at one coordinate on the category axis.
40///
41/// Like [`BoxPlot`](crate::BoxPlot), it is generic over the full chart coordinate `C`: a
42/// vertical violin lives on a `(X, f64)` chart, a horizontal one on `(f64, Y)`.
43/// The half-width scales with the kernel density, so the widest part of the
44/// violin corresponds to the mode of the sample.
45#[derive(Debug, Clone)]
46pub struct ViolinPlot<C> {
47    // Layout: [n_kde density points][5 box points]. The KDE points carry the
48    // outline; the 5 box points feed the optional embedded box.
49    points: Vec<C>,
50    density: Vec<f64>,
51    max_density: f64,
52    n_kde: usize,
53    width: u32,
54    horizontal: bool,
55    style: ViolinStyle,
56    box_style: BoxStyle,
57}
58
59impl<X: Clone> ViolinPlot<(X, f64)> {
60    /// A vertical violin at category position `x` from a raw `data` sample,
61    /// using a Silverman-rule bandwidth. Errors if the sample is empty or
62    /// degenerate (see [`crate::stats::kde`]).
63    pub fn vertical(x: X, data: &[f64]) -> Result<Self, StatsError> {
64        Self::vertical_with_bandwidth(x, data, None)
65    }
66
67    /// A vertical violin with an explicit KDE bandwidth override.
68    pub fn vertical_with_bandwidth(
69        x: X,
70        data: &[f64],
71        bandwidth: Option<f64>,
72    ) -> Result<Self, StatsError> {
73        let curve = kde_curve(data, bandwidth, DEFAULT_KDE_POINTS, 3.0)?;
74        let q = quartiles(data)?;
75        let mut points: Vec<(X, f64)> = curve.xs.iter().map(|&y| (x.clone(), y)).collect();
76        points.push((x.clone(), q.upper_whisker));
77        points.push((x.clone(), q.q3));
78        points.push((x.clone(), q.median));
79        points.push((x.clone(), q.q1));
80        points.push((x.clone(), q.lower_whisker));
81        Ok(Self {
82            n_kde: curve.xs.len(),
83            max_density: curve.max_density(),
84            density: curve.density,
85            points,
86            width: DEFAULT_VIOLIN_WIDTH,
87            horizontal: false,
88            style: ViolinStyle::default(),
89            box_style: BoxStyle::default(),
90        })
91    }
92}
93
94impl<Y: Clone> ViolinPlot<(f64, Y)> {
95    /// A horizontal violin at category position `y` from a raw `data` sample.
96    pub fn horizontal(y: Y, data: &[f64]) -> Result<Self, StatsError> {
97        Self::horizontal_with_bandwidth(y, data, None)
98    }
99
100    /// A horizontal violin with an explicit KDE bandwidth override.
101    pub fn horizontal_with_bandwidth(
102        y: Y,
103        data: &[f64],
104        bandwidth: Option<f64>,
105    ) -> Result<Self, StatsError> {
106        let curve = kde_curve(data, bandwidth, DEFAULT_KDE_POINTS, 3.0)?;
107        let q = quartiles(data)?;
108        let mut points: Vec<(f64, Y)> = curve.xs.iter().map(|&x| (x, y.clone())).collect();
109        points.push((q.upper_whisker, y.clone()));
110        points.push((q.q3, y.clone()));
111        points.push((q.median, y.clone()));
112        points.push((q.q1, y.clone()));
113        points.push((q.lower_whisker, y.clone()));
114        Ok(Self {
115            n_kde: curve.xs.len(),
116            max_density: curve.max_density(),
117            density: curve.density,
118            points,
119            width: DEFAULT_VIOLIN_WIDTH,
120            horizontal: true,
121            style: ViolinStyle::default(),
122            box_style: BoxStyle::default(),
123        })
124    }
125}
126
127impl<C> ViolinPlot<C> {
128    /// Set the full violin width in **pixels** (default 60).
129    pub fn width(mut self, width: u32) -> Self {
130        self.width = width;
131        self
132    }
133
134    /// Enable/disable the embedded box plot overlay.
135    pub fn show_box(mut self, show: bool) -> Self {
136        self.style.show_box = show;
137        self
138    }
139
140    /// Replace the violin style block.
141    pub fn style(mut self, style: ViolinStyle) -> Self {
142        self.style = style;
143        self
144    }
145
146    /// Replace the embedded-box style block.
147    pub fn box_style(mut self, style: BoxStyle) -> Self {
148        self.box_style = style;
149        self
150    }
151}
152
153impl<'a, C: 'a> PointCollection<'a, C> for &'a ViolinPlot<C> {
154    type Point = &'a C;
155    type IntoIter = &'a [C];
156    fn point_iter(self) -> &'a [C] {
157        &self.points
158    }
159}
160
161impl<C, DB: DrawingBackend> Drawable<DB> for ViolinPlot<C> {
162    fn draw<I: Iterator<Item = BackendCoord>>(
163        &self,
164        points: I,
165        backend: &mut DB,
166        _parent_dim: (u32, u32),
167    ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
168        let pix: Vec<BackendCoord> = points.collect();
169        if pix.len() < self.n_kde + N_BOX_POINTS || self.max_density <= 0.0 {
170            return Ok(());
171        }
172        let half = self.width as f64 / 2.0;
173        let kde = &pix[..self.n_kde];
174
175        // Build the mirrored outline. `offset(i)` is the half-width in pixels
176        // for the i-th density sample.
177        let offset =
178            |i: usize| -> i32 { ((self.density[i] / self.max_density) * half).round() as i32 };
179
180        let mut outline: Vec<BackendCoord> = Vec::with_capacity(self.n_kde * 2 + 1);
181        if self.horizontal {
182            let py = |i: usize| kde[i].1;
183            let px = |i: usize| kde[i].0;
184            for i in 0..self.n_kde {
185                outline.push((px(i), py(i) - offset(i)));
186            }
187            for i in (0..self.n_kde).rev() {
188                outline.push((px(i), py(i) + offset(i)));
189            }
190        } else {
191            let px = |i: usize| kde[i].0;
192            let py = |i: usize| kde[i].1;
193            for i in 0..self.n_kde {
194                outline.push((px(i) - offset(i), py(i)));
195            }
196            for i in (0..self.n_kde).rev() {
197                outline.push((px(i) + offset(i), py(i)));
198            }
199        }
200
201        backend.fill_polygon(outline.iter().copied(), &self.style.fill)?;
202        // Close the outline for the stroke.
203        if let Some(&first) = outline.first() {
204            outline.push(first);
205        }
206        backend.draw_path(outline, &self.style.outline)?;
207
208        // Optional embedded box plot, using the 5 trailing box points.
209        if self.style.show_box {
210            let b = &pix[self.n_kde..self.n_kde + N_BOX_POINTS];
211            draw_embedded_box(backend, b, self.horizontal, &self.box_style)?;
212        }
213        Ok(())
214    }
215}
216
217/// Draw a thin box (IQR box + median + whiskers) from the five box points, for
218/// the embedded-box overlay. Mirrors [`BoxPlot`]'s geometry at a fixed narrow
219/// width so it sits inside the violin.
220fn draw_embedded_box<DB: DrawingBackend>(
221    backend: &mut DB,
222    b: &[BackendCoord],
223    horizontal: bool,
224    style: &BoxStyle,
225) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
226    let (upper_w, q3, median, q1, lower_w) = (b[0], b[1], b[2], b[3], b[4]);
227    let half = (EMBEDDED_BOX_WIDTH / 2) as i32;
228    if horizontal {
229        let cy = q1.1;
230        let (bx1, bx2) = (q1.0.min(q3.0), q1.0.max(q3.0));
231        backend.draw_line((lower_w.0, cy), (upper_w.0, cy), &style.whisker)?;
232        backend.draw_rect((bx1, cy - half), (bx2, cy + half), &style.box_fill, true)?;
233        backend.draw_rect((bx1, cy - half), (bx2, cy + half), &style.box_border, false)?;
234        backend.draw_line((median.0, cy - half), (median.0, cy + half), &style.median)?;
235    } else {
236        let cx = q1.0;
237        let (by1, by2) = (q3.1.min(q1.1), q3.1.max(q1.1));
238        backend.draw_line((cx, lower_w.1), (cx, upper_w.1), &style.whisker)?;
239        backend.draw_rect((cx - half, by1), (cx + half, by2), &style.box_fill, true)?;
240        backend.draw_rect((cx - half, by1), (cx + half, by2), &style.box_border, false)?;
241        backend.draw_line((cx - half, median.1), (cx + half, median.1), &style.median)?;
242    }
243    Ok(())
244}
245
246/// A group of violins laid out side by side — mirrors [`BoxPlotSeries`](crate::BoxPlotSeries).
247#[derive(Debug, Clone)]
248pub struct ViolinPlotSeries<C> {
249    violins: Vec<ViolinPlot<C>>,
250}
251
252impl<X: Clone> ViolinPlotSeries<(X, f64)> {
253    /// Build a vertical multi-violin series from `(position, sample)` pairs.
254    pub fn from_samples<I, S>(groups: I) -> Result<Self, StatsError>
255    where
256        I: IntoIterator<Item = (X, S)>,
257        S: AsRef<[f64]>,
258    {
259        let violins = groups
260            .into_iter()
261            .map(|(x, s)| ViolinPlot::vertical(x, s.as_ref()))
262            .collect::<Result<Vec<_>, _>>()?;
263        Ok(Self { violins })
264    }
265}
266
267impl<C> ViolinPlotSeries<C> {
268    /// Apply a common width (pixels) to every violin.
269    pub fn width(mut self, width: u32) -> Self {
270        self.violins = self.violins.into_iter().map(|v| v.width(width)).collect();
271        self
272    }
273
274    /// Enable/disable the embedded box overlay on every violin.
275    pub fn show_box(mut self, show: bool) -> Self {
276        self.violins = self.violins.into_iter().map(|v| v.show_box(show)).collect();
277        self
278    }
279
280    /// Apply a common violin style to every violin.
281    pub fn style(mut self, style: ViolinStyle) -> Self {
282        self.violins = self
283            .violins
284            .into_iter()
285            .map(|v| v.style(style.clone()))
286            .collect();
287        self
288    }
289}
290
291impl<C> IntoIterator for ViolinPlotSeries<C> {
292    type Item = ViolinPlot<C>;
293    type IntoIter = std::vec::IntoIter<ViolinPlot<C>>;
294    fn into_iter(self) -> Self::IntoIter {
295        self.violins.into_iter()
296    }
297}