1use plotters::element::{Drawable, PointCollection};
8use plotters::style::{RGBColor, ShapeStyle};
9use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
10
11use crate::stats::{quartiles, Quartiles, StatsError};
12use crate::style::{stroke_style, translucent_fill};
13
14const DEFAULT_BOX_FILL: RGBColor = RGBColor(0, 114, 178); const NEAR_BLACK: RGBColor = RGBColor(30, 30, 30);
16const OUTLIER_GRAY: RGBColor = RGBColor(90, 90, 90);
17
18#[derive(Debug, Clone)]
21pub struct BoxStyle {
22 pub box_fill: ShapeStyle,
24 pub box_border: ShapeStyle,
26 pub whisker: ShapeStyle,
28 pub median: ShapeStyle,
30 pub outlier: ShapeStyle,
32 pub outlier_radius: u32,
34 pub cap_ratio: f64,
36}
37
38impl Default for BoxStyle {
39 fn default() -> Self {
40 Self {
41 box_fill: translucent_fill(&DEFAULT_BOX_FILL, 0.45),
42 box_border: stroke_style(&NEAR_BLACK, 1),
43 whisker: stroke_style(&NEAR_BLACK, 1),
44 median: stroke_style(&NEAR_BLACK, 2),
45 outlier: translucent_fill(&OUTLIER_GRAY, 0.8),
46 outlier_radius: 3,
47 cap_ratio: 0.6,
48 }
49 }
50}
51
52#[derive(Debug, Clone)]
73pub struct BoxPlot<C> {
74 points: Vec<C>,
77 width: u32,
78 horizontal: bool,
79 style: BoxStyle,
80}
81
82const N_BOX_POINTS: usize = 5;
83const DEFAULT_WIDTH: u32 = 24;
84
85impl<X: Clone> BoxPlot<(X, f64)> {
86 pub fn vertical(x: X, data: &[f64]) -> Result<Self, StatsError> {
89 Ok(Self::vertical_from_quartiles(x, &quartiles(data)?))
90 }
91
92 pub fn vertical_from_quartiles(x: X, q: &Quartiles) -> Self {
96 let mut points = vec![
97 (x.clone(), q.upper_whisker),
98 (x.clone(), q.q3),
99 (x.clone(), q.median),
100 (x.clone(), q.q1),
101 (x.clone(), q.lower_whisker),
102 ];
103 points.extend(q.outliers.iter().map(|&o| (x.clone(), o)));
104 Self {
105 points,
106 width: DEFAULT_WIDTH,
107 horizontal: false,
108 style: BoxStyle::default(),
109 }
110 }
111}
112
113impl<Y: Clone> BoxPlot<(f64, Y)> {
114 pub fn horizontal(y: Y, data: &[f64]) -> Result<Self, StatsError> {
117 Ok(Self::horizontal_from_quartiles(y, &quartiles(data)?))
118 }
119
120 pub fn horizontal_from_quartiles(y: Y, q: &Quartiles) -> Self {
122 let mut points = vec![
123 (q.upper_whisker, y.clone()),
124 (q.q3, y.clone()),
125 (q.median, y.clone()),
126 (q.q1, y.clone()),
127 (q.lower_whisker, y.clone()),
128 ];
129 points.extend(q.outliers.iter().map(|&o| (o, y.clone())));
130 Self {
131 points,
132 width: DEFAULT_WIDTH,
133 horizontal: true,
134 style: BoxStyle::default(),
135 }
136 }
137}
138
139impl<C> BoxPlot<C> {
140 pub fn width(mut self, width: u32) -> Self {
142 self.width = width;
143 self
144 }
145
146 pub fn style(mut self, style: BoxStyle) -> Self {
148 self.style = style;
149 self
150 }
151
152 pub fn with_style(mut self, f: impl FnOnce(&mut BoxStyle)) -> Self {
154 f(&mut self.style);
155 self
156 }
157}
158
159impl<'a, C: 'a> PointCollection<'a, C> for &'a BoxPlot<C> {
160 type Point = &'a C;
161 type IntoIter = &'a [C];
162 fn point_iter(self) -> &'a [C] {
163 &self.points
164 }
165}
166
167impl<C, DB: DrawingBackend> Drawable<DB> for BoxPlot<C> {
168 fn draw<I: Iterator<Item = BackendCoord>>(
169 &self,
170 points: I,
171 backend: &mut DB,
172 _parent_dim: (u32, u32),
173 ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
174 let pix: Vec<BackendCoord> = points.collect();
175 if pix.len() < N_BOX_POINTS {
176 return Ok(());
177 }
178 let (upper_w, q3, median, q1, lower_w) = (pix[0], pix[1], pix[2], pix[3], pix[4]);
179 let half = (self.width / 2) as i32;
180 let cap = ((self.width as f64 * self.style.cap_ratio) / 2.0).round() as i32;
181 let s = &self.style;
182
183 if self.horizontal {
184 let cy = q1.1; let (bx1, bx2) = (q1.0.min(q3.0), q1.0.max(q3.0));
186 backend.draw_line((lower_w.0, cy), (bx1, cy), &s.whisker)?;
188 backend.draw_line((bx2, cy), (upper_w.0, cy), &s.whisker)?;
189 backend.draw_line((lower_w.0, cy - cap), (lower_w.0, cy + cap), &s.whisker)?;
190 backend.draw_line((upper_w.0, cy - cap), (upper_w.0, cy + cap), &s.whisker)?;
191 backend.draw_rect((bx1, cy - half), (bx2, cy + half), &s.box_fill, true)?;
193 backend.draw_rect((bx1, cy - half), (bx2, cy + half), &s.box_border, false)?;
194 backend.draw_line((median.0, cy - half), (median.0, cy + half), &s.median)?;
196 } else {
197 let cx = q1.0; let (by1, by2) = (q3.1.min(q1.1), q3.1.max(q1.1));
199 backend.draw_line((cx, lower_w.1), (cx, by2), &s.whisker)?;
201 backend.draw_line((cx, by1), (cx, upper_w.1), &s.whisker)?;
202 backend.draw_line((cx - cap, lower_w.1), (cx + cap, lower_w.1), &s.whisker)?;
203 backend.draw_line((cx - cap, upper_w.1), (cx + cap, upper_w.1), &s.whisker)?;
204 backend.draw_rect((cx - half, by1), (cx + half, by2), &s.box_fill, true)?;
206 backend.draw_rect((cx - half, by1), (cx + half, by2), &s.box_border, false)?;
207 backend.draw_line((cx - half, median.1), (cx + half, median.1), &s.median)?;
209 }
210
211 for o in &pix[N_BOX_POINTS..] {
213 backend.draw_circle(*o, s.outlier_radius, &s.outlier, s.outlier.filled)?;
214 }
215 Ok(())
216 }
217}
218
219#[derive(Debug, Clone)]
225pub struct BoxPlotSeries<C> {
226 boxes: Vec<BoxPlot<C>>,
227}
228
229impl<X: Clone> BoxPlotSeries<(X, f64)> {
230 pub fn from_samples<I, S>(groups: I) -> Result<Self, StatsError>
233 where
234 I: IntoIterator<Item = (X, S)>,
235 S: AsRef<[f64]>,
236 {
237 let boxes = groups
238 .into_iter()
239 .map(|(x, s)| BoxPlot::vertical(x, s.as_ref()))
240 .collect::<Result<Vec<_>, _>>()?;
241 Ok(Self { boxes })
242 }
243}
244
245impl<Y: Clone> BoxPlotSeries<(f64, Y)> {
246 pub fn horizontal_from_samples<I, S>(groups: I) -> Result<Self, StatsError>
248 where
249 I: IntoIterator<Item = (Y, S)>,
250 S: AsRef<[f64]>,
251 {
252 let boxes = groups
253 .into_iter()
254 .map(|(y, s)| BoxPlot::horizontal(y, s.as_ref()))
255 .collect::<Result<Vec<_>, _>>()?;
256 Ok(Self { boxes })
257 }
258}
259
260impl<C> BoxPlotSeries<C> {
261 pub fn width(mut self, width: u32) -> Self {
263 self.boxes = self.boxes.into_iter().map(|b| b.width(width)).collect();
264 self
265 }
266
267 pub fn style(mut self, style: BoxStyle) -> Self {
269 self.boxes = self
270 .boxes
271 .into_iter()
272 .map(|b| b.style(style.clone()))
273 .collect();
274 self
275 }
276}
277
278impl<C> IntoIterator for BoxPlotSeries<C> {
279 type Item = BoxPlot<C>;
280 type IntoIter = std::vec::IntoIter<BoxPlot<C>>;
281 fn into_iter(self) -> Self::IntoIter {
282 self.boxes.into_iter()
283 }
284}