1 | use std::marker::PhantomData; |
2 | |
3 | use crate::data::Quartiles; |
4 | use crate::element::{Drawable, PointCollection}; |
5 | use crate::style::{Color, ShapeStyle, BLACK}; |
6 | use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind}; |
7 | |
8 | /// The boxplot orientation trait |
9 | pub trait BoxplotOrient<K, V> { |
10 | type XType; |
11 | type YType; |
12 | |
13 | fn make_coord(key: K, val: V) -> (Self::XType, Self::YType); |
14 | fn with_offset(coord: BackendCoord, offset: f64) -> BackendCoord; |
15 | } |
16 | |
17 | /// The vertical boxplot phantom |
18 | pub struct BoxplotOrientV<K, V>(PhantomData<(K, V)>); |
19 | |
20 | /// The horizontal boxplot phantom |
21 | pub struct BoxplotOrientH<K, V>(PhantomData<(K, V)>); |
22 | |
23 | impl<K, V> BoxplotOrient<K, V> for BoxplotOrientV<K, V> { |
24 | type XType = K; |
25 | type YType = V; |
26 | |
27 | fn make_coord(key: K, val: V) -> (K, V) { |
28 | (key, val) |
29 | } |
30 | |
31 | fn with_offset(coord: BackendCoord, offset: f64) -> BackendCoord { |
32 | (coord.0 + offset as i32, coord.1) |
33 | } |
34 | } |
35 | |
36 | impl<K, V> BoxplotOrient<K, V> for BoxplotOrientH<K, V> { |
37 | type XType = V; |
38 | type YType = K; |
39 | |
40 | fn make_coord(key: K, val: V) -> (V, K) { |
41 | (val, key) |
42 | } |
43 | |
44 | fn with_offset(coord: BackendCoord, offset: f64) -> BackendCoord { |
45 | (coord.0, coord.1 + offset as i32) |
46 | } |
47 | } |
48 | |
49 | const DEFAULT_WIDTH: u32 = 10; |
50 | |
51 | /// The boxplot element |
52 | pub struct Boxplot<K, O: BoxplotOrient<K, f32>> { |
53 | style: ShapeStyle, |
54 | width: u32, |
55 | whisker_width: f64, |
56 | offset: f64, |
57 | key: K, |
58 | values: [f32; 5], |
59 | _p: PhantomData<O>, |
60 | } |
61 | |
62 | impl<K: Clone> Boxplot<K, BoxplotOrientV<K, f32>> { |
63 | /// Create a new vertical boxplot element. |
64 | /// |
65 | /// - `key`: The key (the X axis value) |
66 | /// - `quartiles`: The quartiles values for the Y axis |
67 | /// - **returns** The newly created boxplot element |
68 | /// |
69 | /// ```rust |
70 | /// use plotters::prelude::*; |
71 | /// |
72 | /// let quartiles = Quartiles::new(&[7, 15, 36, 39, 40, 41]); |
73 | /// let plot = Boxplot::new_vertical("group" , &quartiles); |
74 | /// ``` |
75 | pub fn new_vertical(key: K, quartiles: &Quartiles) -> Self { |
76 | Self { |
77 | style: Into::<ShapeStyle>::into(&BLACK), |
78 | width: DEFAULT_WIDTH, |
79 | whisker_width: 1.0, |
80 | offset: 0.0, |
81 | key, |
82 | values: quartiles.values(), |
83 | _p: PhantomData, |
84 | } |
85 | } |
86 | } |
87 | |
88 | impl<K: Clone> Boxplot<K, BoxplotOrientH<K, f32>> { |
89 | /// Create a new horizontal boxplot element. |
90 | /// |
91 | /// - `key`: The key (the Y axis value) |
92 | /// - `quartiles`: The quartiles values for the X axis |
93 | /// - **returns** The newly created boxplot element |
94 | /// |
95 | /// ```rust |
96 | /// use plotters::prelude::*; |
97 | /// |
98 | /// let quartiles = Quartiles::new(&[7, 15, 36, 39, 40, 41]); |
99 | /// let plot = Boxplot::new_horizontal("group" , &quartiles); |
100 | /// ``` |
101 | pub fn new_horizontal(key: K, quartiles: &Quartiles) -> Self { |
102 | Self { |
103 | style: Into::<ShapeStyle>::into(&BLACK), |
104 | width: DEFAULT_WIDTH, |
105 | whisker_width: 1.0, |
106 | offset: 0.0, |
107 | key, |
108 | values: quartiles.values(), |
109 | _p: PhantomData, |
110 | } |
111 | } |
112 | } |
113 | |
114 | impl<K, O: BoxplotOrient<K, f32>> Boxplot<K, O> { |
115 | /// Set the style of the boxplot. |
116 | /// |
117 | /// - `S`: The required style |
118 | /// - **returns** The up-to-dated boxplot element |
119 | /// |
120 | /// ```rust |
121 | /// use plotters::prelude::*; |
122 | /// |
123 | /// let quartiles = Quartiles::new(&[7, 15, 36, 39, 40, 41]); |
124 | /// let plot = Boxplot::new_horizontal("group" , &quartiles).style(&BLUE); |
125 | /// ``` |
126 | pub fn style<S: Into<ShapeStyle>>(mut self, style: S) -> Self { |
127 | self.style = style.into(); |
128 | self |
129 | } |
130 | |
131 | /// Set the bar width. |
132 | /// |
133 | /// - `width`: The required width |
134 | /// - **returns** The up-to-dated boxplot element |
135 | /// |
136 | /// ```rust |
137 | /// use plotters::prelude::*; |
138 | /// |
139 | /// let quartiles = Quartiles::new(&[7, 15, 36, 39, 40, 41]); |
140 | /// let plot = Boxplot::new_horizontal("group" , &quartiles).width(10); |
141 | /// ``` |
142 | pub fn width(mut self, width: u32) -> Self { |
143 | self.width = width; |
144 | self |
145 | } |
146 | |
147 | /// Set the width of the whiskers as a fraction of the bar width. |
148 | /// |
149 | /// - `whisker_width`: The required fraction |
150 | /// - **returns** The up-to-dated boxplot element |
151 | /// |
152 | /// ```rust |
153 | /// use plotters::prelude::*; |
154 | /// |
155 | /// let quartiles = Quartiles::new(&[7, 15, 36, 39, 40, 41]); |
156 | /// let plot = Boxplot::new_horizontal("group" , &quartiles).whisker_width(0.5); |
157 | /// ``` |
158 | pub fn whisker_width(mut self, whisker_width: f64) -> Self { |
159 | self.whisker_width = whisker_width; |
160 | self |
161 | } |
162 | |
163 | /// Set the element offset on the key axis. |
164 | /// |
165 | /// - `offset`: The required offset (on the X axis for vertical, on the Y axis for horizontal) |
166 | /// - **returns** The up-to-dated boxplot element |
167 | /// |
168 | /// ```rust |
169 | /// use plotters::prelude::*; |
170 | /// |
171 | /// let quartiles = Quartiles::new(&[7, 15, 36, 39, 40, 41]); |
172 | /// let plot = Boxplot::new_horizontal("group" , &quartiles).offset(-5); |
173 | /// ``` |
174 | pub fn offset<T: Into<f64> + Copy>(mut self, offset: T) -> Self { |
175 | self.offset = offset.into(); |
176 | self |
177 | } |
178 | } |
179 | |
180 | impl<'a, K: Clone, O: BoxplotOrient<K, f32>> PointCollection<'a, (O::XType, O::YType)> |
181 | for &'a Boxplot<K, O> |
182 | { |
183 | type Point = (O::XType, O::YType); |
184 | type IntoIter = Vec<Self::Point>; |
185 | fn point_iter(self) -> Self::IntoIter { |
186 | self.values |
187 | .iter() |
188 | .map(|v| O::make_coord(self.key.clone(), *v)) |
189 | .collect() |
190 | } |
191 | } |
192 | |
193 | impl<K, DB: DrawingBackend, O: BoxplotOrient<K, f32>> Drawable<DB> for Boxplot<K, O> { |
194 | fn draw<I: Iterator<Item = BackendCoord>>( |
195 | &self, |
196 | points: I, |
197 | backend: &mut DB, |
198 | _: (u32, u32), |
199 | ) -> Result<(), DrawingErrorKind<DB::ErrorType>> { |
200 | let points: Vec<_> = points.take(5).collect(); |
201 | if points.len() == 5 { |
202 | let width = f64::from(self.width); |
203 | let moved = |coord| O::with_offset(coord, self.offset); |
204 | let start_bar = |coord| O::with_offset(moved(coord), -width / 2.0); |
205 | let end_bar = |coord| O::with_offset(moved(coord), width / 2.0); |
206 | let start_whisker = |
207 | |coord| O::with_offset(moved(coord), -width * self.whisker_width / 2.0); |
208 | let end_whisker = |
209 | |coord| O::with_offset(moved(coord), width * self.whisker_width / 2.0); |
210 | |
211 | // |---[ | ]----| |
212 | // ^________________ |
213 | backend.draw_line( |
214 | start_whisker(points[0]), |
215 | end_whisker(points[0]), |
216 | &self.style, |
217 | )?; |
218 | |
219 | // |---[ | ]----| |
220 | // _^^^_____________ |
221 | |
222 | backend.draw_line( |
223 | moved(points[0]), |
224 | moved(points[1]), |
225 | &self.style.color.to_backend_color(), |
226 | )?; |
227 | |
228 | // |---[ | ]----| |
229 | // ____^______^_____ |
230 | let corner1 = start_bar(points[3]); |
231 | let corner2 = end_bar(points[1]); |
232 | let upper_left = (corner1.0.min(corner2.0), corner1.1.min(corner2.1)); |
233 | let bottom_right = (corner1.0.max(corner2.0), corner1.1.max(corner2.1)); |
234 | backend.draw_rect(upper_left, bottom_right, &self.style, false)?; |
235 | |
236 | // |---[ | ]----| |
237 | // ________^________ |
238 | backend.draw_line(start_bar(points[2]), end_bar(points[2]), &self.style)?; |
239 | |
240 | // |---[ | ]----| |
241 | // ____________^^^^_ |
242 | backend.draw_line(moved(points[3]), moved(points[4]), &self.style)?; |
243 | |
244 | // |---[ | ]----| |
245 | // ________________^ |
246 | backend.draw_line( |
247 | start_whisker(points[4]), |
248 | end_whisker(points[4]), |
249 | &self.style, |
250 | )?; |
251 | } |
252 | Ok(()) |
253 | } |
254 | } |
255 | |
256 | #[cfg (test)] |
257 | mod test { |
258 | use super::*; |
259 | use crate::prelude::*; |
260 | |
261 | #[test] |
262 | fn test_draw_v() { |
263 | let root = MockedBackend::new(1024, 768).into_drawing_area(); |
264 | let chart = ChartBuilder::on(&root) |
265 | .build_cartesian_2d(0..2, 0f32..100f32) |
266 | .unwrap(); |
267 | |
268 | let values = Quartiles::new(&[6]); |
269 | assert!(chart |
270 | .plotting_area() |
271 | .draw(&Boxplot::new_vertical(1, &values)) |
272 | .is_ok()); |
273 | } |
274 | |
275 | #[test] |
276 | fn test_draw_h() { |
277 | let root = MockedBackend::new(1024, 768).into_drawing_area(); |
278 | let chart = ChartBuilder::on(&root) |
279 | .build_cartesian_2d(0f32..100f32, 0..2) |
280 | .unwrap(); |
281 | |
282 | let values = Quartiles::new(&[6]); |
283 | assert!(chart |
284 | .plotting_area() |
285 | .draw(&Boxplot::new_horizontal(1, &values)) |
286 | .is_ok()); |
287 | } |
288 | } |
289 | |