plotpy/
barplot.rs

1use super::{generate_list_quoted, vector_to_array, AsVector, GraphMaker};
2use num_traits::Num;
3use std::fmt::Write;
4
5/// Generates a Barplot plot
6///
7/// [See Matplotlib's documentation](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.bar.html)
8///
9/// # Examples
10///
11/// ## Basic bar plot
12///
13/// ```
14/// use plotpy::{Barplot, Plot, StrError};
15/// use std::collections::HashMap;
16///
17/// fn main() -> Result<(), StrError> {
18///     // data
19///     let x = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
20///     let y = [5, 4, 3, 2, 1, 0, 1, 2, 3, 4];
21///
22///     // barplot object and options
23///     let mut bar = Barplot::new();
24///     bar.draw(&x, &y);
25///
26///     // save figure
27///     let mut plot = Plot::new();
28///     plot.add(&bar)
29///         .save("/tmp/plotpy/doc_tests/doc_barplot_1.svg")?;
30///     Ok(())
31/// }
32/// ```
33///
34/// ![doc_barplot_1.svg](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/doc_barplot_1.svg)
35///
36/// ## Using string as labels
37///
38/// The code below implements the [Bar Label Demo from Matplotlib documentation](https://matplotlib.org/stable/gallery/lines_bars_and_markers/bar_label_demo.html#sphx-glr-gallery-lines-bars-and-markers-bar-label-demo-py)
39///
40/// ```
41/// use plotpy::{Barplot, Plot, StrError};
42/// use std::collections::HashMap;
43///
44/// fn main() -> Result<(), StrError> {
45///     // data
46///     let species = ["Adelie", "Chinstrap", "Gentoo"];
47///     let sex_counts = HashMap::from([
48///         ("Male", [73.0, 34.0, 61.0]), //
49///         ("Female", [73.0, 34.0, 58.0]),
50///     ]);
51///
52///    // barplot object and options
53///    let mut bar = Barplot::new();
54///    bar.set_with_text("center");
55///
56///    // draw bars
57///    let mut bottom = [0.0, 0.0, 0.0];
58///    for (sex, sex_count) in &sex_counts {
59///        bar.set_label(sex)
60///            .set_bottom(&bottom)
61///            .draw_with_str(&species, sex_count);
62///        for i in 0..sex_count.len() {
63///            bottom[i] += sex_count[i];
64///        }
65///    }
66///
67///     // add barplot to plot and save figure
68///     let mut plot = Plot::new();
69///     plot.add(&bar)
70///         .set_title("Number of penguins by sex")
71///         .legend()
72///         .save("/tmp/plotpy/doc_tests/doc_barplot_2.svg")?;
73///     Ok(())
74/// }
75/// ```
76///
77/// ![doc_barplot_2.svg](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/doc_barplot_2.svg)
78///
79/// ## Horizontal bars
80///
81/// ```
82/// use plotpy::{Barplot, Plot, StrError};
83///
84/// fn main() -> Result<(), StrError> {
85///     // data
86///     let fruits = ["Apple", "Banana", "Orange"];
87///     let prices = [10.0, 20.0, 30.0];
88///     let errors = [3.0, 2.0, 1.0];
89///
90///     // barplot object and options
91///     let mut bar = Barplot::new();
92///     bar.set_errors(&errors)
93///         .set_horizontal(true)
94///         .set_with_text("edge")
95///         .draw_with_str(&fruits, &prices);
96///
97///     // save figure
98///     let mut plot = Plot::new();
99///     plot.set_inv_y()
100///         .add(&bar)
101///         .set_title("Fruits")
102///         .set_label_x("price")
103///         .save("/tmp/plotpy/doc_tests/doc_barplot_3.svg")?;
104///     Ok(())
105/// }
106/// ```
107///
108/// ![doc_barplot_3.svg](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/doc_barplot_3.svg)
109///
110/// ## More examples
111///
112/// See also integration test in the **tests** directory.
113pub struct Barplot {
114    label: String,             // Name of this bar in the legend
115    colors: Vec<String>,       // Colors for each bar
116    width: f64,                // Width of the bars
117    bottom: Vec<f64>,          // bottom coordinates to stack bars
118    with_text: Option<String>, // Text to be added to each bar (aka, bar_label)
119    horizontal: bool,          // Horizontal barplot
120    errors: Vec<f64>,          // Shows error icons on bars
121    extra: String,             // Extra commands (comma separated)
122    buffer: String,            // buffer
123}
124
125impl Barplot {
126    /// Creates a new Barplot object
127    pub fn new() -> Self {
128        Barplot {
129            label: String::new(),
130            colors: Vec::new(),
131            width: 0.0,
132            bottom: Vec::new(),
133            with_text: None,
134            horizontal: false,
135            errors: Vec::new(),
136            extra: String::new(),
137            buffer: String::new(),
138        }
139    }
140
141    /// Draws the bar plot
142    pub fn draw<'a, T, U>(&mut self, x: &'a T, y: &'a T)
143    where
144        T: AsVector<'a, U>,
145        U: 'a + std::fmt::Display + Num,
146    {
147        vector_to_array(&mut self.buffer, "x", x);
148        vector_to_array(&mut self.buffer, "y", y);
149        let opt = self.options();
150        if self.colors.len() > 0 {
151            generate_list_quoted(&mut self.buffer, "colors", self.colors.as_slice());
152        }
153        if self.bottom.len() > 0 {
154            vector_to_array(&mut self.buffer, "bottom", &self.bottom);
155        }
156        if self.errors.len() > 0 {
157            vector_to_array(&mut self.buffer, "err", &self.errors);
158        }
159        if self.horizontal {
160            write!(&mut self.buffer, "p=plt.barh(x,y{})\n", &opt).unwrap();
161        } else {
162            write!(&mut self.buffer, "p=plt.bar(x,y{})\n", &opt).unwrap();
163        }
164        if let Some(t) = &self.with_text {
165            write!(&mut self.buffer, "plt.gca().bar_label(p,label_type='{}')\n", t).unwrap();
166        }
167    }
168
169    /// Draws the bar plot with strings
170    pub fn draw_with_str<'a, T, U>(&mut self, x: &[&str], y: &'a T)
171    where
172        T: AsVector<'a, U>,
173        U: 'a + std::fmt::Display + Num,
174    {
175        generate_list_quoted(&mut self.buffer, "x", x);
176        vector_to_array(&mut self.buffer, "y", y);
177        let opt = self.options();
178        if self.colors.len() > 0 {
179            generate_list_quoted(&mut self.buffer, "colors", self.colors.as_slice());
180        }
181        if self.bottom.len() > 0 {
182            vector_to_array(&mut self.buffer, "bottom", &self.bottom);
183        }
184        if self.errors.len() > 0 {
185            vector_to_array(&mut self.buffer, "err", &self.errors);
186        }
187        if self.horizontal {
188            write!(&mut self.buffer, "p=plt.barh(x,y{})\n", &opt).unwrap();
189        } else {
190            write!(&mut self.buffer, "p=plt.bar(x,y{})\n", &opt).unwrap();
191        }
192        if let Some(t) = &self.with_text {
193            write!(&mut self.buffer, "plt.gca().bar_label(p,label_type='{}')\n", t).unwrap();
194        }
195    }
196
197    /// Sets the name of this bar in the legend
198    pub fn set_label(&mut self, label: &str) -> &mut Self {
199        self.label = String::from(label);
200        self
201    }
202
203    /// Sets the colors for each bar
204    pub fn set_colors(&mut self, colors: &[&str]) -> &mut Self {
205        self.colors = colors.iter().map(|color| color.to_string()).collect();
206        self
207    }
208
209    /// Sets the width of the bars
210    pub fn set_width(&mut self, width: f64) -> &mut Self {
211        self.width = width;
212        self
213    }
214
215    /// Sets the vertical offset to stack bars
216    pub fn set_bottom(&mut self, bottom: &[f64]) -> &mut Self {
217        self.bottom = Vec::from(bottom);
218        self
219    }
220
221    /// Sets an option to show the text (labels) of each bar
222    ///
223    /// # Input
224    ///
225    /// `position` -- "edge" or "center"; Use "" to remove the label
226    ///
227    /// See [Matplotlib documentation](https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.bar_label.html#matplotlib.axes.Axes.bar_label)
228    pub fn set_with_text(&mut self, position: &str) -> &mut Self {
229        if position == "" {
230            self.with_text = None
231        } else {
232            self.with_text = Some(position.to_string());
233        }
234        self
235    }
236
237    /// Enables drawing horizontal bars
238    pub fn set_horizontal(&mut self, flag: bool) -> &mut Self {
239        self.horizontal = flag;
240        self
241    }
242
243    /// Enables error indicators
244    pub fn set_errors(&mut self, errors: &[f64]) -> &mut Self {
245        self.errors = errors.to_vec();
246        self
247    }
248
249    /// Sets extra matplotlib commands (comma separated)
250    ///
251    /// **Important:** The extra commands must be comma separated. For example:
252    ///
253    /// ```text
254    /// param1=123,param2='hello'
255    /// ```
256    ///
257    /// [See Matplotlib's documentation for extra parameters](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.bar.html)
258    pub fn set_extra(&mut self, extra: &str) -> &mut Self {
259        self.extra = extra.to_string();
260        self
261    }
262
263    /// Returns options for barplot
264    fn options(&self) -> String {
265        let mut opt = String::new();
266        if self.label != "" {
267            write!(&mut opt, ",label=r'{}'", self.label).unwrap();
268        }
269        if self.colors.len() > 0 {
270            write!(&mut opt, ",color=colors").unwrap();
271        }
272        if self.width > 0.0 {
273            write!(&mut opt, ",width={}", self.width).unwrap();
274        }
275        if self.bottom.len() > 0 {
276            write!(&mut opt, ",bottom=bottom").unwrap();
277        }
278        if self.errors.len() > 0 {
279            if self.horizontal {
280                write!(&mut opt, ",xerr=err").unwrap();
281            } else {
282                write!(&mut opt, ",yerr=err").unwrap();
283            }
284        }
285        if self.extra != "" {
286            write!(&mut opt, ",{}", self.extra).unwrap();
287        }
288        opt
289    }
290}
291
292impl GraphMaker for Barplot {
293    fn get_buffer<'a>(&'a self) -> &'a String {
294        &self.buffer
295    }
296    fn clear_buffer(&mut self) {
297        self.buffer.clear();
298    }
299}
300
301////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
302
303#[cfg(test)]
304mod tests {
305    use super::Barplot;
306    use crate::GraphMaker;
307
308    #[test]
309    fn new_works() {
310        let barplot = Barplot::new();
311        assert_eq!(barplot.label.len(), 0);
312        assert_eq!(barplot.colors.len(), 0);
313        assert_eq!(barplot.width, 0.0);
314        assert_eq!(barplot.bottom.len(), 0);
315        assert_eq!(barplot.with_text, None);
316        assert_eq!(barplot.buffer.len(), 0);
317    }
318
319    #[test]
320    fn draw_works_1() {
321        let xx = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
322        let yy = [5, 4, 3, 2, 1, 0, 1, 2, 3, 4];
323        let mut bar = Barplot::new();
324        bar.draw(&xx, &yy);
325        let b: &str = "x=np.array([0,1,2,3,4,5,6,7,8,9,],dtype=float)\n\
326                       y=np.array([5,4,3,2,1,0,1,2,3,4,],dtype=float)\n\
327                       p=plt.bar(x,y)\n";
328        assert_eq!(bar.buffer, b);
329        bar.clear_buffer();
330        assert_eq!(bar.buffer, "");
331    }
332
333    #[test]
334    fn draw_works_2() {
335        let xx = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
336        let yy = [5, 4, 3, 2, 1, 0, 1, 2, 3, 4];
337        let mut bar = Barplot::new();
338        bar.set_label("LABEL")
339            .set_colors(&vec!["red", "green"])
340            .set_width(10.0)
341            .set_bottom(&[1.0, 2.0, 3.0])
342            .set_with_text("center")
343            .set_extra("edgecolor='black'")
344            .draw(&xx, &yy);
345        let b: &str = "x=np.array([0,1,2,3,4,5,6,7,8,9,],dtype=float)\n\
346                       y=np.array([5,4,3,2,1,0,1,2,3,4,],dtype=float)\n\
347                       colors=['red','green',]\n\
348                       bottom=np.array([1,2,3,],dtype=float)\n\
349                       p=plt.bar(x,y\
350                       ,label=r'LABEL'\
351                       ,color=colors\
352                       ,width=10\
353                       ,bottom=bottom\
354                       ,edgecolor='black')\n\
355                       plt.gca().bar_label(p,label_type='center')\n";
356        assert_eq!(bar.buffer, b);
357        bar.clear_buffer();
358        bar.set_with_text("");
359        assert_eq!(bar.buffer, "");
360    }
361
362    #[test]
363    fn draw_with_str_works_1() {
364        let xx = ["one", "two", "three"];
365        let yy = [1, 2, 3];
366        let mut bar = Barplot::new();
367        bar.draw_with_str(&xx, &yy);
368        let b: &str = "x=['one','two','three',]\n\
369                       y=np.array([1,2,3,],dtype=float)\n\
370                       p=plt.bar(x,y)\n";
371        assert_eq!(bar.buffer, b);
372    }
373
374    #[test]
375    fn draw_with_str_works_2() {
376        let xx = ["one", "two", "three"];
377        let yy = [1, 2, 3];
378        let mut bar = Barplot::new();
379        bar.set_label("LABEL")
380            .set_colors(&vec!["red", "green"])
381            .set_width(10.0)
382            .set_bottom(&[1.0, 2.0, 3.0])
383            .set_with_text("center")
384            .set_extra("edgecolor='black'")
385            .draw_with_str(&xx, &yy);
386        let b: &str = "x=['one','two','three',]\n\
387                       y=np.array([1,2,3,],dtype=float)\n\
388                       colors=['red','green',]\n\
389                       bottom=np.array([1,2,3,],dtype=float)\n\
390                       p=plt.bar(x,y\
391                       ,label=r'LABEL'\
392                       ,color=colors\
393                       ,width=10\
394                       ,bottom=bottom\
395                       ,edgecolor='black')\n\
396                       plt.gca().bar_label(p,label_type='center')\n";
397        assert_eq!(bar.buffer, b);
398    }
399}