plotpy/
legend.rs

1use super::{generate_list, GraphMaker};
2use std::fmt::Write;
3
4/// Generates a Legend
5///
6/// [See Matplotlib's documentation](https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.legend.html)
7///
8/// # Example
9///
10/// ```
11/// use plotpy::{linspace, Curve, Legend, Plot, StrError};
12///
13/// fn main() -> Result<(), StrError> {
14///     // generate (x,y) points
15///     let x  = linspace(0.0, 5.0, 6);
16///     let y1: Vec<_> = x.iter().map(|v| 0.5 * *v).collect();
17///     let y2: Vec<_> = x.iter().map(|v| 1.0 * *v).collect();
18///     let y3: Vec<_> = x.iter().map(|v| 1.5 * *v).collect();
19///     let y4: Vec<_> = x.iter().map(|v| 2.0 * *v).collect();
20///
21///     // configure and draw curves
22///     let mut curve1 = Curve::new();
23///     let mut curve2 = Curve::new();
24///     let mut curve3 = Curve::new();
25///     let mut curve4 = Curve::new();
26///     curve1.set_label("y = 0.5 x");
27///     curve2.set_label("y = 1.0 x");
28///     curve3.set_label("y = 1.5 x");
29///     curve4.set_label("y = 2.0 x");
30///     curve1.draw(&x, &y1);
31///     curve2.draw(&x, &y2);
32///     curve3.draw(&x, &y3);
33///     curve4.draw(&x, &y4);
34///
35///     // configure and draw legends
36///     let mut legend1 = Legend::new();
37///     legend1.set_fontsize(14.0)
38///         .set_handle_len(6.0)
39///         .set_num_col(2)
40///         .set_outside(true)
41///         .set_show_frame(false);
42///     legend1.draw();
43///     let mut legend2 = Legend::new();
44///     legend2.draw();
45///
46///     // add curves and legends to subplots
47///     let mut plot = Plot::new();
48///     plot.set_subplot(2, 1, 1)
49///         .add(&curve1)
50///         .add(&curve2)
51///         .add(&curve3)
52///         .add(&curve4)
53///         .add(&legend1);
54///     plot.set_subplot(2, 1, 2)
55///         .add(&curve1)
56///         .add(&curve2)
57///         .add(&curve3)
58///         .add(&curve4)
59///         .add(&legend2);
60///
61///     // save figure
62///     plot.save("/tmp/plotpy/doc_tests/doc_legend.svg")?;
63///     Ok(())
64/// }
65/// ```
66///
67/// ![doc_legend.svg](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/doc_legend.svg)
68///
69/// See also integration tests in the [tests directory](https://github.com/cpmech/plotpy/tree/main/tests)
70pub struct Legend {
71    fontsize: f64,      // Fontsize
72    handle_len: f64,    // Length of legend's indicator line
73    num_col: usize,     // Number of columns
74    location: String,   // Location, e.g., "best", "right", "center left"
75    outside: bool,      // Put legend outside plot area
76    show_frame: bool,   // Show frame around legend
77    x_coords: Vec<f64>, // Normalized coordinates to put legend outside
78    extra: String,      // Extra commands (comma separated)
79    buffer: String,     // buffer
80}
81
82impl Legend {
83    /// Creates a new Legend object
84    pub fn new() -> Self {
85        Legend {
86            fontsize: 0.0,
87            handle_len: 3.0,
88            num_col: 1,
89            location: "best".to_string(),
90            outside: false,
91            show_frame: true,
92            x_coords: vec![0.0, 1.02, 1.0, 0.102],
93            extra: String::new(),
94            buffer: String::new(),
95        }
96    }
97
98    /// Draws legend
99    pub fn draw(&mut self) {
100        let opt = self.options();
101        if self.outside {
102            generate_list(&mut self.buffer, "coo", self.x_coords.as_slice());
103        }
104        write!(&mut self.buffer, "h,l=plt.gca().get_legend_handles_labels()\n").unwrap();
105        write!(&mut self.buffer, "if len(h)>0 and len(l)>0:\n").unwrap();
106        write!(&mut self.buffer, "    leg=plt.legend({})\n", &opt).unwrap();
107        write!(&mut self.buffer, "    add_to_ea(leg)\n").unwrap();
108        if !self.show_frame {
109            write!(&mut self.buffer, "    leg.get_frame().set_linewidth(0.0)\n").unwrap();
110        }
111    }
112
113    /// Sets the fontsize
114    pub fn set_fontsize(&mut self, fontsize: f64) -> &mut Self {
115        self.fontsize = fontsize;
116        self
117    }
118
119    /// Sets the length of legend's indicator line
120    pub fn set_handle_len(&mut self, length: f64) -> &mut Self {
121        self.handle_len = length;
122        self
123    }
124
125    /// Sets the number of columns
126    pub fn set_num_col(&mut self, num_columns: usize) -> &mut Self {
127        self.num_col = num_columns;
128        self
129    }
130
131    /// Sets the location
132    ///
133    /// Options:
134    ///
135    /// * "best", "right", "center left"
136    /// * Note: Only used if outside == false
137    pub fn set_location(&mut self, location: &str) -> &mut Self {
138        self.location = String::from(location);
139        self
140    }
141
142    /// Sets option to put legend outside of plot area
143    pub fn set_outside(&mut self, flag: bool) -> &mut Self {
144        self.outside = flag;
145        self
146    }
147
148    /// Sets option to show frame around legend
149    pub fn set_show_frame(&mut self, flag: bool) -> &mut Self {
150        self.show_frame = flag;
151        self
152    }
153
154    /// Sets the normalized coordinates when drawing an outside legend
155    ///
156    /// Example: `[0.0, 1.02, 1.0, 0.102]`
157    pub fn set_x_coords(&mut self, coords: &[f64]) -> &mut Self {
158        self.x_coords = coords.to_vec();
159        self
160    }
161
162    /// Sets extra matplotlib commands (comma separated)
163    ///
164    /// **Important:** The extra commands must be comma separated. For example:
165    ///
166    /// ```text
167    /// param1=123,param2='hello'
168    /// ```
169    ///
170    /// [See Matplotlib's documentation for extra parameters](https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.legend.html)
171    pub fn set_extra(&mut self, extra: &str) -> &mut Self {
172        self.extra = extra.to_string();
173        self
174    }
175
176    /// Returns options for legend
177    fn options(&self) -> String {
178        let mut opt = String::new();
179        let mut comma = "";
180        if self.handle_len > 0.0 {
181            write!(&mut opt, "handlelength={}", self.handle_len).unwrap();
182            comma = ",";
183        }
184        if self.fontsize > 0.0 {
185            write!(&mut opt, "{}prop={{'size':{}}}", comma, self.fontsize).unwrap();
186            comma = ",";
187        }
188        if self.num_col > 0 {
189            write!(&mut opt, "{}ncol={}", comma, self.num_col).unwrap();
190            comma = ",";
191        }
192        if self.outside {
193            write!(
194                &mut opt,
195                "{}loc=3,bbox_to_anchor=coo,mode='expand',borderaxespad=0.0,columnspacing=1,handletextpad=0.05",
196                comma
197            )
198            .unwrap();
199        } else {
200            if self.location != "" {
201                write!(&mut opt, "{}loc='{}'", comma, self.location).unwrap();
202            }
203        }
204        if self.extra != "" {
205            write!(&mut opt, ",{}", self.extra).unwrap();
206        }
207        opt
208    }
209}
210
211impl GraphMaker for Legend {
212    fn get_buffer<'a>(&'a self) -> &'a String {
213        &self.buffer
214    }
215    fn clear_buffer(&mut self) {
216        self.buffer.clear();
217    }
218}
219
220////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
221
222#[cfg(test)]
223mod tests {
224    use super::Legend;
225    use crate::GraphMaker;
226
227    #[test]
228    fn new_works() {
229        let legend = Legend::new();
230        assert_eq!(legend.fontsize, 0.0);
231        assert_eq!(legend.handle_len, 3.0);
232        assert_eq!(legend.num_col, 1);
233        assert_eq!(legend.location, "best".to_string());
234        assert_eq!(legend.outside, false);
235        assert_eq!(legend.show_frame, true);
236        assert_eq!(legend.x_coords, vec![0.0, 1.02, 1.0, 0.102]);
237        assert_eq!(legend.buffer.len(), 0);
238    }
239
240    #[test]
241    fn options_works() {
242        let mut legend = Legend::new();
243        legend.set_handle_len(6.0);
244        let opt = legend.options();
245        assert_eq!(opt, "handlelength=6,ncol=1,loc='best'");
246    }
247
248    #[test]
249    fn draw_works() {
250        let mut legend = Legend::new();
251        legend.draw();
252        let b: &str = "h,l=plt.gca().get_legend_handles_labels()\n\
253                       if len(h)>0 and len(l)>0:\n\
254                       \x20\x20\x20\x20leg=plt.legend(handlelength=3,ncol=1,loc='best')\n\
255                       \x20\x20\x20\x20add_to_ea(leg)\n";
256        assert_eq!(legend.buffer, b);
257        legend.clear_buffer();
258        assert_eq!(legend.buffer, "");
259    }
260}