plotpy/
boxplot.rs

1use super::{generate_list, generate_nested_list, matrix_to_array, AsMatrix, GraphMaker};
2use num_traits::Num;
3use std::fmt::Write;
4
5/// Draw a box and whisker plot
6///
7/// [See Matplotlib's documentation](https://matplotlib.org/3.6.3/api/_as_gen/matplotlib.pyplot.boxplot.html)
8///
9/// # Examples
10///
11/// ## Data as a nested list
12///
13/// ```
14/// use plotpy::{Boxplot, Plot, StrError};
15///
16/// fn main() -> Result<(), StrError> {
17///     // data (as a nested list)
18///     let data = vec![
19///         vec![1, 2, 3, 4, 5],              // A
20///         vec![2, 3, 4, 5, 6, 7, 8, 9, 10], // B
21///         vec![3, 4, 5, 6],                 // C
22///         vec![4, 5, 6, 7, 8, 9, 10],       // D
23///         vec![5, 6, 7],                    // E
24///     ];
25///
26///     // x ticks and labels
27///     let n = data.len();
28///     let ticks: Vec<_> = (1..(n + 1)).into_iter().collect();
29///     let labels = ["A", "B", "C", "D", "E"];
30///
31///     // boxplot object and options
32///     let mut boxes = Boxplot::new();
33///     boxes.draw(&data);
34///
35///     // save figure
36///     let mut plot = Plot::new();
37///     plot.add(&boxes)
38///         .set_title("boxplot documentation test")
39///         .set_ticks_x_labels(&ticks, &labels)
40///         .save("/tmp/plotpy/doc_tests/doc_boxplot_2.svg")?;
41///     Ok(())
42/// }
43/// ```
44///
45/// ![doc_boxplot_2.svg](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/doc_boxplot_2.svg)
46///
47/// ## Data as a 2D array
48///
49/// ```
50/// use plotpy::{Boxplot, Plot, StrError};
51///
52/// fn main() -> Result<(), StrError> {
53///     // data (as a 2D array/matrix)
54///     let data = vec![
55///         //   A  B  C  D  E
56///         vec![1, 2, 3, 4, 5],
57///         vec![2, 3, 4, 5, 6],
58///         vec![3, 4, 5, 6, 7],
59///         vec![4, 5, 6, 7, 8],
60///         vec![5, 6, 7, 8, 9],
61///         vec![6, 7, 8, 9, 10],
62///         vec![14, 14, 14, 14, 14], // fliers
63///     ];
64///
65///     // x ticks and labels
66///     let ncol = data[0].len();
67///     let ticks: Vec<_> = (1..(ncol + 1)).into_iter().collect();
68///     let labels = ["A", "B", "C", "D", "E"];
69///
70///     // boxplot object and options
71///     let mut boxes = Boxplot::new();
72///     boxes.draw_mat(&data);
73///
74///     // save figure
75///     let mut plot = Plot::new();
76///     plot.add(&boxes)
77///         .set_title("boxplot documentation test")
78///         .set_ticks_x_labels(&ticks, &labels)
79///         .save("/tmp/plotpy/doc_tests/doc_boxplot_1.svg")?;
80///     Ok(())
81/// }
82/// ```
83///
84/// ![doc_boxplot_1.svg](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/doc_boxplot_1.svg)
85///
86/// ## More examples
87///
88/// See also integration test in the **tests** directory.
89pub struct Boxplot {
90    symbol: String,        // The default symbol for flier (outlier) points.
91    horizontal: bool,      // Horizontal boxplot (default is false)
92    whisker: Option<f64>,  // The position of the whiskers
93    positions: Vec<f64>,   // The positions of the boxes
94    width: Option<f64>,    // The width of the boxes
95    no_fliers: bool,       // Disables fliers
96    patch_artist: bool,    // Enables the use of Patch artist to draw boxes
97    median_props: String,  // The properties of the median
98    box_props: String,     // The properties of the box
99    whisker_props: String, // The properties of the whisker
100    extra: String,         // Extra commands (comma separated)
101    buffer: String,        // Buffer
102}
103
104impl Boxplot {
105    /// Creates a new Boxplot object
106    pub fn new() -> Self {
107        Boxplot {
108            symbol: String::new(),
109            horizontal: false,
110            whisker: None,
111            positions: Vec::new(),
112            width: None,
113            no_fliers: false,
114            patch_artist: false,
115            median_props: String::new(),
116            box_props: String::new(),
117            whisker_props: String::new(),
118            extra: String::new(),
119            buffer: String::new(),
120        }
121    }
122
123    /// Draws the box plot given a nested list
124    ///
125    /// # Input
126    ///
127    /// * `data` -- Is a sequence of 1D arrays such that a boxplot is drawn for each array in the sequence.
128    ///   [From Matplotlib](https://matplotlib.org/3.6.3/api/_as_gen/matplotlib.pyplot.boxplot.html)
129    pub fn draw<T>(&mut self, data: &Vec<Vec<T>>)
130    where
131        T: std::fmt::Display + Num,
132    {
133        generate_nested_list(&mut self.buffer, "x", data);
134        if self.positions.len() > 0 {
135            generate_list(&mut self.buffer, "positions", self.positions.as_slice());
136        }
137        let opt = self.options();
138        write!(&mut self.buffer, "p=plt.boxplot(x{})\n", &opt).unwrap();
139    }
140
141    /// Draws the box plot given a 2D array (matrix)
142    ///
143    /// # Input
144    ///
145    /// * `data` -- Is a 2D array (matrix) such that a boxplot is drawn for each column in the matrix.
146    ///   [From Matplotlib](https://matplotlib.org/3.6.3/api/_as_gen/matplotlib.pyplot.boxplot.html)
147    pub fn draw_mat<'a, T, U>(&mut self, data: &'a T)
148    where
149        T: AsMatrix<'a, U>,
150        U: 'a + std::fmt::Display + Num,
151    {
152        matrix_to_array(&mut self.buffer, "x", data);
153        if self.positions.len() > 0 {
154            generate_list(&mut self.buffer, "positions", self.positions.as_slice());
155        }
156        let opt = self.options();
157        write!(&mut self.buffer, "p=plt.boxplot(x{})\n", &opt).unwrap();
158    }
159
160    /// Sets the symbol for the fliers
161    pub fn set_symbol(&mut self, symbol: &str) -> &mut Self {
162        self.symbol = symbol.to_string();
163        self
164    }
165
166    /// Enables drawing horizontal boxes
167    pub fn set_horizontal(&mut self, flag: bool) -> &mut Self {
168        self.horizontal = flag;
169        self
170    }
171
172    /// Sets the position of the whiskers
173    ///
174    /// The default value of whisker = 1.5 corresponds to Tukey's original definition of boxplots.
175    ///
176    /// [See Matplotlib's documentation](https://matplotlib.org/3.6.3/api/_as_gen/matplotlib.pyplot.boxplot.html)
177    pub fn set_whisker(&mut self, whisker: f64) -> &mut Self {
178        self.whisker = Some(whisker);
179        self
180    }
181
182    /// Sets the positions of the boxes
183    pub fn set_positions(&mut self, positions: &[f64]) -> &mut Self {
184        self.positions = positions.to_vec();
185        self
186    }
187
188    /// Sets the width of the boxes
189    pub fn set_width(&mut self, width: f64) -> &mut Self {
190        self.width = Some(width);
191        self
192    }
193
194    /// Disables the fliers
195    pub fn set_no_fliers(&mut self, flag: bool) -> &mut Self {
196        self.no_fliers = flag;
197        self
198    }
199
200    /// Enables the use of Patch artist to draw boxes instead of Line2D artist
201    pub fn set_patch_artist(&mut self, flag: bool) -> &mut Self {
202        self.patch_artist = flag;
203        self
204    }
205
206    /// Set the median properties.
207    ///
208    /// [See Matplotlib's documentation](https://matplotlib.org/3.6.3/api/_as_gen/matplotlib.pyplot.boxplot.html)
209    pub fn set_medianprops(&mut self, props: &str) -> &mut Self {
210        self.median_props = props.to_string();
211        self
212    }
213
214    /// Set the properties of the box
215    pub fn set_boxprops(&mut self, props: &str) -> &mut Self {
216        self.box_props = props.to_string();
217        self
218    }
219
220    /// Set the properties of the whisker
221    pub fn set_whiskerprops(&mut self, props: &str) -> &mut Self {
222        self.whisker_props = props.to_string();
223        self
224    }
225
226    /// Sets extra matplotlib commands (comma separated)
227    ///
228    /// **Important:** The extra commands must be comma separated. For example:
229    ///
230    /// ```text
231    /// param1=123,param2='hello'
232    /// ```
233    ///
234    /// [See Matplotlib's documentation for extra parameters](https://matplotlib.org/3.6.3/api/_as_gen/matplotlib.pyplot.boxplot.html)
235    pub fn set_extra(&mut self, extra: &str) -> &mut Self {
236        self.extra = extra.to_string();
237        self
238    }
239
240    /// Returns options (optional parameters) for boxplot
241    fn options(&self) -> String {
242        let mut opt = String::new();
243        if self.symbol != "" {
244            write!(&mut opt, ",sym=r'{}'", self.symbol).unwrap();
245        }
246        if self.horizontal {
247            write!(&mut opt, ",vert=False").unwrap();
248        }
249        if self.whisker != None {
250            write!(&mut opt, ",whis={}", self.whisker.unwrap()).unwrap();
251        }
252        if self.positions.len() > 0 {
253            write!(&mut opt, ",positions=positions").unwrap();
254        }
255        if self.width != None {
256            write!(&mut opt, ",widths={}", self.width.unwrap()).unwrap();
257        }
258        if self.no_fliers {
259            write!(&mut opt, ",showfliers=False").unwrap();
260        }
261        if self.patch_artist {
262            write!(&mut opt, ",patch_artist=True").unwrap();
263        }
264        if self.median_props != "" {
265            write!(&mut opt, ",medianprops={}", self.median_props).unwrap();
266        }
267        if self.box_props != "" {
268            write!(&mut opt, ",boxprops={}", self.box_props).unwrap();
269        }
270        if self.whisker_props != "" {
271            write!(&mut opt, ",whiskerprops={}", self.whisker_props).unwrap();
272        }
273        if self.extra != "" {
274            write!(&mut opt, ",{}", self.extra).unwrap();
275        }
276        opt
277    }
278}
279
280impl GraphMaker for Boxplot {
281    fn get_buffer<'a>(&'a self) -> &'a String {
282        &self.buffer
283    }
284    fn clear_buffer(&mut self) {
285        self.buffer.clear();
286    }
287}
288
289/////////////////////////////////////////////////////////////////////////////
290
291#[cfg(test)]
292mod tests {
293    use super::Boxplot;
294    use crate::GraphMaker;
295
296    #[test]
297    fn new_works() {
298        let boxes = Boxplot::new();
299        assert_eq!(boxes.symbol.len(), 0);
300        assert_eq!(boxes.horizontal, false);
301        assert_eq!(boxes.whisker, None);
302        assert_eq!(boxes.positions.len(), 0);
303        assert_eq!(boxes.width, None);
304        assert_eq!(boxes.no_fliers, false);
305        assert_eq!(boxes.patch_artist, false);
306        assert_eq!(boxes.median_props.len(), 0);
307        assert_eq!(boxes.box_props.len(), 0);
308        assert_eq!(boxes.whisker_props.len(), 0);
309        assert_eq!(boxes.extra.len(), 0);
310        assert_eq!(boxes.buffer.len(), 0);
311    }
312
313    #[test]
314    fn draw_works_1() {
315        let x = vec![
316            vec![1, 2, 3],       // A
317            vec![2, 3, 4, 5, 6], // B
318            vec![6, 7],          // C
319        ];
320        let mut boxes = Boxplot::new();
321        boxes.draw(&x);
322        let b: &str = "x=[[1,2,3,],[2,3,4,5,6,],[6,7,],]\n\
323                       p=plt.boxplot(x)\n";
324        assert_eq!(boxes.buffer, b);
325        boxes.clear_buffer();
326        assert_eq!(boxes.buffer, "");
327    }
328
329    #[test]
330    fn draw_works_2() {
331        let x = vec![
332            vec![1, 2, 3],       // A
333            vec![2, 3, 4, 5, 6], // B
334            vec![6, 7],          // C
335        ];
336        let mut boxes = Boxplot::new();
337        boxes
338            .set_symbol("b+")
339            .set_no_fliers(true)
340            .set_horizontal(true)
341            .set_whisker(1.5)
342            .set_positions(&[1.0, 2.0, 3.0])
343            .set_width(0.5)
344            .set_patch_artist(true)
345            .set_boxprops("{'facecolor': 'C0', 'edgecolor': 'white','linewidth': 0.5}")
346            .draw(&x);
347        let b: &str = "x=[[1,2,3,],[2,3,4,5,6,],[6,7,],]\n\
348                       positions=[1,2,3,]\n\
349                       p=plt.boxplot(x,sym=r'b+',vert=False,whis=1.5,positions=positions,widths=0.5,showfliers=False,patch_artist=True,boxprops={'facecolor': 'C0', 'edgecolor': 'white','linewidth': 0.5})\n";
350        assert_eq!(boxes.buffer, b);
351        boxes.clear_buffer();
352        assert_eq!(boxes.buffer, "");
353    }
354
355    #[test]
356    fn draw_mat_works_1() {
357        let x = vec![
358            //   A  B  C  D  E
359            vec![1, 2, 3, 4, 5],
360            vec![2, 3, 4, 5, 6],
361            vec![3, 4, 5, 6, 7],
362            vec![4, 5, 6, 7, 8],
363            vec![5, 6, 7, 8, 9],
364            vec![6, 7, 8, 9, 10],
365        ];
366        let mut boxes = Boxplot::new();
367        boxes.draw_mat(&x);
368        let b: &str = "x=np.array([[1,2,3,4,5,],[2,3,4,5,6,],[3,4,5,6,7,],[4,5,6,7,8,],[5,6,7,8,9,],[6,7,8,9,10,],],dtype=float)\n\
369                       p=plt.boxplot(x)\n";
370        assert_eq!(boxes.buffer, b);
371        boxes.clear_buffer();
372        assert_eq!(boxes.buffer, "");
373    }
374
375    #[test]
376    fn draw_mat_works_2() {
377        let x = vec![
378            //   A  B  C  D  E
379            vec![1, 2, 3, 4, 5],
380            vec![2, 3, 4, 5, 6],
381            vec![3, 4, 5, 6, 7],
382            vec![4, 5, 6, 7, 8],
383            vec![5, 6, 7, 8, 9],
384            vec![6, 7, 8, 9, 10],
385        ];
386        let mut boxes = Boxplot::new();
387        boxes
388            .set_symbol("b+")
389            .set_no_fliers(true)
390            .set_horizontal(true)
391            .set_whisker(1.5)
392            .set_positions(&[1.0, 2.0, 3.0, 4.0, 5.0])
393            .set_width(0.5)
394            .draw_mat(&x);
395        let b: &str = "x=np.array([[1,2,3,4,5,],[2,3,4,5,6,],[3,4,5,6,7,],[4,5,6,7,8,],[5,6,7,8,9,],[6,7,8,9,10,],],dtype=float)\n\
396                       positions=[1,2,3,4,5,]\n\
397                       p=plt.boxplot(x,sym=r'b+',vert=False,whis=1.5,positions=positions,widths=0.5,showfliers=False)\n";
398        assert_eq!(boxes.buffer, b);
399        boxes.clear_buffer();
400        assert_eq!(boxes.buffer, "");
401    }
402}