plotpy/
curve.rs

1use super::{vector_to_array, AsVector, GraphMaker};
2use crate::quote_marker;
3use num_traits::Num;
4use std::fmt::Write;
5
6/// Holds either the second point coordinates of a ray or the slope of the ray
7#[derive(Clone, Debug)]
8pub enum RayEndpoint {
9    /// Coordinates of the second point
10    Coords(f64, f64),
11
12    /// Slope of the ray
13    Slope(f64),
14
15    /// Indicates a horizontal ray
16    Horizontal,
17
18    /// Indicates a vertical ray
19    Vertical,
20}
21
22/// Generates a curve (aka line-plot) given two arrays (x,y)
23///
24/// [See Matplotlib's documentation](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html)
25///
26/// # Notes
27///
28/// * This struct corresponds to the **plot** function of Matplotlib.
29/// * You may plot a Scatter plot by setting line_style = "None"
30///
31/// # Examples
32///
33/// ## Using methods to set the points
34///
35/// ```
36/// use plotpy::{Curve, Plot, StrError};
37/// use std::f64::consts::PI;
38///
39/// fn main() -> Result<(), StrError> {
40///     // configure curve
41///     let mut curve = Curve::new();
42///     curve.set_line_width(2.0);
43///
44///     // add points
45///     const N: usize = 30;
46///     curve.points_begin();
47///     for i in 0..N {
48///         let x = (i as f64) * 2.0 * PI / ((N - 1) as f64);
49///         let y = f64::sin(x);
50///         curve.points_add(x, y);
51///     }
52///     curve.points_end();
53///
54///     // add curve to plot
55///     let mut plot = Plot::new();
56///     plot.add(&curve).grid_and_labels("x", "y");
57///
58///     // configure multiple-of-pi formatter
59///     let minor_every = PI / 12.0;
60///     plot.set_ticks_x_multiple_of_pi(minor_every);
61///
62///     // save figure
63///     plot.save("/tmp/plotpy/doc_tests/doc_curve_methods.svg")?;
64///     Ok(())
65/// }
66/// ```
67///
68/// ![doc_curve_methods.svg](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/doc_curve_methods.svg)
69///
70/// ## Using Vector with point data
71///
72/// ```
73/// use plotpy::{linspace, Curve, Plot, StrError};
74///
75/// fn main() -> Result<(), StrError> {
76///     // generate (x,y) points
77///     let x = linspace(-1.0, 1.0, 21);
78///     let y: Vec<_> = x.iter().map(|v| 1.0 / (1.0 + f64::exp(-5.0 * *v))).collect();
79///
80///     // configure curve
81///     let mut curve = Curve::new();
82///     curve
83///         .set_label("logistic function")
84///         .set_line_alpha(0.8)
85///         .set_line_color("#5f9cd8")
86///         .set_line_style("-")
87///         .set_line_width(5.0)
88///         .set_marker_color("#eeea83")
89///         .set_marker_every(5)
90///         .set_marker_line_color("#da98d1")
91///         .set_marker_line_width(2.5)
92///         .set_marker_size(20.0)
93///         .set_marker_style("*");
94///
95///     // draw curve
96///     curve.draw(&x, &y);
97///
98///     // add curve to plot
99///     let mut plot = Plot::new();
100///     plot.add(&curve).set_num_ticks_y(11).grid_labels_legend("x", "y");
101///
102///     // save figure
103///     plot.save("/tmp/plotpy/doc_tests/doc_curve.svg")?;
104///     Ok(())
105/// }
106/// ```
107///
108/// ![doc_curve_vector.svg](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/doc_curve_vector.svg)
109///
110/// ## (twinx) Plot two vertical axes with different scales
111///
112/// ```
113/// use plotpy::{linspace, Curve, Plot, StrError};
114/// use std::f64::consts::PI;
115///
116/// fn main() -> Result<(), StrError> {
117///     // data
118///     let np = 201;
119///     let mut x = vec![0.0; np];
120///     let mut y1 = vec![0.0; np];
121///     let mut y2 = vec![0.0; np];
122///     let dx = 4.0 / (np as f64);
123///     for i in 0..np {
124///         x[i] = (i as f64) * dx;
125///         y1[i] = f64::exp(x[i]);
126///         y2[i] = f64::sin(2.0 * PI * x[i]);
127///     }
128///
129///     // curve
130///     let mut curve = Curve::new();
131///     curve.set_line_color("red").draw(&x, &y1);
132///     curve.set_line_color("blue").draw_with_twin_x(&y2);
133///
134///     // add curve to plot
135///     let mut plot = Plot::new();
136///     plot.add(&curve) // must occur before set twinx options
137///         .grid_and_labels("time (s)", "exp function")
138///         .set_label_x_color("green")
139///         .set_label_y_color("red")
140///         .set_label_y_twinx("sin function")
141///         .set_label_y_twinx_color("blue");
142///
143///     // save figure
144///     plot.save("/tmp/plotpy/doc_tests/doc_curve_twinx.svg")?;
145///     Ok(())
146/// }
147/// ```
148///
149/// ![doc_curve_twinx.svg](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/doc_curve_twinx.svg)
150///
151/// ## More examples
152///
153/// See also integration tests in the [tests directory](https://github.com/cpmech/plotpy/tree/main/tests)
154///
155/// Output from some integration tests:
156///
157/// ![integ_curve.svg](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/integ_curve.svg)
158///
159/// ![integ_curve_3d.svg](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/integ_curve_3d.svg)
160pub struct Curve {
161    label: String,             // Name of this curve in the legend
162    line_alpha: f64,           // Opacity of lines (0, 1]. A<1e-14 => A=1.0
163    line_color: String,        // Color of lines
164    line_style: String,        // Style of lines
165    line_width: f64,           // Width of lines
166    marker_color: String,      // Color of markers
167    marker_every: usize,       // Increment of data points to use when drawing markers
168    marker_void: bool,         // Draws a void marker (edge only)
169    marker_line_color: String, // Edge color of markers
170    marker_line_width: f64,    // Edge width of markers
171    marker_size: f64,          // Size of markers
172    marker_style: String,      // Style of markers, e.g., "`o`", "`+`"
173    stop_clip: bool,           // Stop clipping features within margins
174    extra: String,             // Extra commands (comma separated)
175    buffer: String,            // buffer
176}
177
178impl Curve {
179    /// Creates new Curve object
180    pub fn new() -> Self {
181        Curve {
182            label: String::new(),
183            line_alpha: 0.0,
184            line_color: String::new(),
185            line_style: String::new(),
186            line_width: 0.0,
187            marker_color: String::new(),
188            marker_every: 0,
189            marker_void: false,
190            marker_line_color: String::new(),
191            marker_line_width: 0.0,
192            marker_size: 0.0,
193            marker_style: String::new(),
194            stop_clip: false,
195            extra: String::new(),
196            buffer: String::new(),
197        }
198    }
199
200    /// Begins adding points to the curve (2D only)
201    ///
202    /// # Warning
203    ///
204    /// This function must be followed by [Curve::points_add] and [Curve::points_end],
205    /// otherwise Python/Matplotlib will fail.
206    pub fn points_begin(&mut self) -> &mut Self {
207        write!(&mut self.buffer, "xy=np.array([").unwrap();
208        self
209    }
210
211    /// Adds point to the curve (2D only)
212    ///
213    /// # Warning
214    ///
215    /// This function must be called after [Curve::points_begin] and must be followed by [Curve::points_end],
216    /// otherwise Python/Matplotlib will fail.
217    pub fn points_add<T>(&mut self, x: T, y: T) -> &mut Self
218    where
219        T: std::fmt::Display + Num,
220    {
221        write!(&mut self.buffer, "[{},{}],", x, y).unwrap();
222        self
223    }
224
225    /// Ends adding points to the curve (2D only)
226    ///
227    /// # Warning
228    ///
229    /// This function must be called after [Curve::points_begin] and [Curve::points_add],
230    /// otherwise Python/Matplotlib will fail.
231    pub fn points_end(&mut self) -> &mut Self {
232        let opt = self.options();
233        write!(&mut self.buffer, "])\nplt.plot(xy[:,0],xy[:,1]{})\n", &opt).unwrap();
234        self
235    }
236
237    /// Begins adding 3D points to the curve
238    ///
239    /// # Warning
240    ///
241    /// This function must be followed by [Curve::points_3d_add] and [Curve::points_3d_end],
242    /// otherwise Python/Matplotlib will fail
243    pub fn points_3d_begin(&mut self) -> &mut Self {
244        write!(&mut self.buffer, "xyz=np.array([").unwrap();
245        self
246    }
247
248    /// Adds 3D point to the curve
249    ///
250    /// # Warning
251    ///
252    /// This function must be called after [Curve::points_3d_begin] and must be followed by [Curve::points_3d_end],
253    /// otherwise Python/Matplotlib will fail.
254    pub fn points_3d_add<T>(&mut self, x: T, y: T, z: T) -> &mut Self
255    where
256        T: std::fmt::Display + Num,
257    {
258        write!(&mut self.buffer, "[{},{},{}],", x, y, z).unwrap();
259        self
260    }
261
262    /// Ends adding 3D points to the curve
263    ///
264    /// # Warning
265    ///
266    /// This function must be called after [Curve::points_3d_begin] and [Curve::points_3d_add],
267    /// otherwise Python/Matplotlib will fail.
268    pub fn points_3d_end(&mut self) -> &mut Self {
269        let opt = self.options();
270        write!(
271            &mut self.buffer,
272            "])\nax3d().plot(xyz[:,0],xyz[:,1],xyz[:,2]{})\n",
273            &opt
274        )
275        .unwrap();
276        self
277    }
278
279    /// Draws curve
280    ///
281    /// # Input
282    ///
283    /// * `x` - abscissa values
284    /// * `y` - ordinate values
285    pub fn draw<'a, T, U>(&mut self, x: &'a T, y: &'a T)
286    where
287        T: AsVector<'a, U>,
288        U: 'a + std::fmt::Display + Num,
289    {
290        vector_to_array(&mut self.buffer, "x", x);
291        vector_to_array(&mut self.buffer, "y", y);
292        let opt = self.options();
293        write!(&mut self.buffer, "plt.plot(x,y{})\n", &opt).unwrap();
294    }
295
296    /// Draws curve on a previously drawn figure with the same x
297    ///
298    /// * `y` - ordinate values on the right-hand side
299    pub fn draw_with_twin_x<'a, T, U>(&mut self, y: &'a T)
300    where
301        T: AsVector<'a, U>,
302        U: 'a + std::fmt::Display + Num,
303    {
304        vector_to_array(&mut self.buffer, "y2", y);
305        let opt = self.options();
306        write!(
307            &mut self.buffer,
308            "ax=plt.gca()\n\
309             ax_twinx=ax.twinx()\n\
310             ax_twinx.plot(x,y2{})\n\
311             plt.sca(ax)\n",
312            &opt
313        )
314        .unwrap();
315    }
316
317    /// Draws curve in 3D plot
318    ///
319    /// # Input
320    ///
321    /// * `x` - x values
322    /// * `y` - y values
323    /// * `z` - z values
324    pub fn draw_3d<'a, T, U>(&mut self, x: &'a T, y: &'a T, z: &'a T)
325    where
326        T: AsVector<'a, U>,
327        U: 'a + std::fmt::Display + Num,
328    {
329        vector_to_array(&mut self.buffer, "x", x);
330        vector_to_array(&mut self.buffer, "y", y);
331        vector_to_array(&mut self.buffer, "z", z);
332        let opt = self.options();
333        write!(&mut self.buffer, "ax3d().plot(x,y,z{})\n", &opt).unwrap();
334    }
335
336    /// Sets the name of this curve in the legend
337    pub fn set_label(&mut self, label: &str) -> &mut Self {
338        self.label = String::from(label);
339        self
340    }
341
342    /// Sets the opacity of lines (0, 1]. A<1e-14 => A=1.0
343    pub fn set_line_alpha(&mut self, alpha: f64) -> &mut Self {
344        self.line_alpha = alpha;
345        self
346    }
347
348    /// Sets the color of lines
349    pub fn set_line_color(&mut self, color: &str) -> &mut Self {
350        self.line_color = String::from(color);
351        self
352    }
353
354    /// Draws a ray (an infinite line)
355    ///
356    /// * For horizontal rays, only `ya` is used
357    /// * For vertical rays, only `xa` is used
358    pub fn draw_ray(&mut self, xa: f64, ya: f64, endpoint: RayEndpoint) {
359        let opt = self.options();
360        match endpoint {
361            RayEndpoint::Coords(xb, yb) => write!(
362                &mut self.buffer,
363                "plt.axline(({},{}),({},{}){})\n",
364                xa, ya, xb, yb, &opt
365            )
366            .unwrap(),
367            RayEndpoint::Slope(m) => write!(
368                &mut self.buffer,
369                "plt.axline(({},{}),None,slope={}{})\n",
370                xa, ya, m, &opt
371            )
372            .unwrap(),
373            RayEndpoint::Horizontal => write!(&mut self.buffer, "plt.axhline({}{})\n", ya, &opt).unwrap(),
374            RayEndpoint::Vertical => write!(&mut self.buffer, "plt.axvline({}{})\n", xa, &opt).unwrap(),
375        }
376    }
377
378    /// Sets the style of lines
379    ///
380    /// Options:
381    ///
382    /// * "`-`", `:`", "`--`", "`-.`", or "`None`"
383    /// * As defined in <https://matplotlib.org/stable/gallery/lines_bars_and_markers/linestyles.html>
384    pub fn set_line_style(&mut self, style: &str) -> &mut Self {
385        self.line_style = String::from(style);
386        self
387    }
388
389    /// Sets the width of lines
390    pub fn set_line_width(&mut self, width: f64) -> &mut Self {
391        self.line_width = width;
392        self
393    }
394
395    /// Sets the color of markers
396    pub fn set_marker_color(&mut self, color: &str) -> &mut Self {
397        self.marker_color = String::from(color);
398        self
399    }
400
401    /// Sets the increment of data points to use when drawing markers
402    pub fn set_marker_every(&mut self, every: usize) -> &mut Self {
403        self.marker_every = every;
404        self
405    }
406
407    /// Sets the option to draw a void marker (draw edge only)
408    pub fn set_marker_void(&mut self, flag: bool) -> &mut Self {
409        self.marker_void = flag;
410        self
411    }
412
413    /// Sets the edge color of markers
414    pub fn set_marker_line_color(&mut self, color: &str) -> &mut Self {
415        self.marker_line_color = String::from(color);
416        self
417    }
418
419    /// Sets the edge width of markers
420    pub fn set_marker_line_width(&mut self, width: f64) -> &mut Self {
421        self.marker_line_width = width;
422        self
423    }
424
425    /// Sets the size of markers
426    pub fn set_marker_size(&mut self, size: f64) -> &mut Self {
427        self.marker_size = size;
428        self
429    }
430
431    /// Sets the style of markers
432    ///
433    /// Examples:
434    ///
435    /// * "`o`", "`+`"
436    /// * As defined in <https://matplotlib.org/stable/api/markers_api.html>
437    pub fn set_marker_style(&mut self, style: &str) -> &mut Self {
438        self.marker_style = String::from(style);
439        self
440    }
441
442    /// Sets the flag to stop clipping features within margins
443    pub fn set_stop_clip(&mut self, flag: bool) -> &mut Self {
444        self.stop_clip = flag;
445        self
446    }
447
448    /// Sets extra matplotlib commands (comma separated)
449    ///
450    /// **Important:** The extra commands must be comma separated. For example:
451    ///
452    /// ```text
453    /// param1=123,param2='hello'
454    /// ```
455    ///
456    /// [See Matplotlib's documentation for extra parameters](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html)
457    pub fn set_extra(&mut self, extra: &str) -> &mut Self {
458        self.extra = extra.to_string();
459        self
460    }
461
462    /// Returns options for curve
463    fn options(&self) -> String {
464        // output
465        let mut opt = String::new();
466
467        // label
468        if self.label != "" {
469            write!(&mut opt, ",label=r'{}'", self.label).unwrap();
470        }
471
472        // lines
473        if self.line_alpha > 0.0 {
474            write!(&mut opt, ",alpha={}", self.line_alpha).unwrap();
475        }
476        if self.line_color != "" {
477            write!(&mut opt, ",color='{}'", self.line_color).unwrap();
478        }
479        if self.line_style != "" {
480            write!(&mut opt, ",linestyle='{}'", self.line_style).unwrap();
481        }
482        if self.line_width > 0.0 {
483            write!(&mut opt, ",linewidth={}", self.line_width).unwrap();
484        }
485
486        // markers
487        if !self.marker_void && self.marker_color != "" {
488            write!(&mut opt, ",markerfacecolor='{}'", self.marker_color).unwrap();
489        }
490        if self.marker_every > 0 {
491            write!(&mut opt, ",markevery={}", self.marker_every).unwrap();
492        }
493        if self.marker_void {
494            write!(&mut opt, ",markerfacecolor='none'").unwrap();
495        }
496        if self.marker_line_color != "" {
497            write!(&mut opt, ",markeredgecolor='{}'", self.marker_line_color).unwrap();
498        }
499        if self.marker_line_width > 0.0 {
500            write!(&mut opt, ",markeredgewidth={}", self.marker_line_width).unwrap();
501        }
502        if self.marker_size > 0.0 {
503            write!(&mut opt, ",markersize={}", self.marker_size).unwrap();
504        }
505        if self.marker_style != "" {
506            write!(&mut opt, ",marker={}", quote_marker(&self.marker_style)).unwrap();
507        }
508
509        // clipping
510        if self.stop_clip {
511            write!(&mut opt, ",clip_on=False").unwrap();
512        }
513
514        // extra
515        if self.extra != "" {
516            write!(&mut opt, ",{}", self.extra).unwrap();
517        }
518        opt
519    }
520}
521
522impl GraphMaker for Curve {
523    fn get_buffer<'a>(&'a self) -> &'a String {
524        &self.buffer
525    }
526    fn clear_buffer(&mut self) {
527        self.buffer.clear();
528    }
529}
530
531////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
532
533#[cfg(test)]
534mod tests {
535    use super::{Curve, RayEndpoint};
536    use crate::GraphMaker;
537
538    #[test]
539    fn new_works() {
540        let curve = Curve::new();
541        assert_eq!(curve.label.len(), 0);
542        assert_eq!(curve.line_alpha, 0.0);
543        assert_eq!(curve.line_color.len(), 0);
544        assert_eq!(curve.line_style.len(), 0);
545        assert_eq!(curve.line_width, 0.0);
546        assert_eq!(curve.marker_color.len(), 0);
547        assert_eq!(curve.marker_every, 0);
548        assert_eq!(curve.marker_void, false);
549        assert_eq!(curve.marker_line_color.len(), 0);
550        assert_eq!(curve.marker_line_width, 0.0);
551        assert_eq!(curve.marker_size, 0.0);
552        assert_eq!(curve.marker_style.len(), 0);
553        assert_eq!(curve.buffer.len(), 0);
554    }
555
556    #[test]
557    fn options_works() {
558        let mut curve = Curve::new();
559        curve
560            .set_label("my-curve")
561            .set_line_alpha(0.7)
562            .set_line_color("#b33434")
563            .set_line_style("-")
564            .set_line_width(3.0)
565            .set_marker_color("#4c4deb")
566            .set_marker_every(2)
567            .set_marker_void(false)
568            .set_marker_line_color("blue")
569            .set_marker_line_width(1.5)
570            .set_marker_size(8.0)
571            .set_marker_style("o")
572            .set_stop_clip(true);
573        let options = curve.options();
574        assert_eq!(
575            options,
576            ",label=r'my-curve'\
577             ,alpha=0.7\
578             ,color='#b33434'\
579             ,linestyle='-'\
580             ,linewidth=3\
581             ,markerfacecolor='#4c4deb'\
582             ,markevery=2\
583             ,markeredgecolor='blue'\
584             ,markeredgewidth=1.5\
585             ,markersize=8\
586             ,marker='o'\
587             ,clip_on=False"
588        );
589        let mut curve = Curve::new();
590        for i in 5..12 {
591            curve.set_marker_style(&format!("{}", i));
592            let options = curve.options();
593            assert_eq!(options, format!(",marker={}", i));
594        }
595    }
596
597    #[test]
598    fn points_methods_work() {
599        let mut curve = Curve::new();
600        curve.points_begin().points_add(1, 2).points_add(3, 4).points_end();
601        let b: &str = "xy=np.array([[1,2],[3,4],])\n\
602                       plt.plot(xy[:,0],xy[:,1])\n";
603        assert_eq!(curve.buffer, b);
604    }
605
606    #[test]
607    fn points_3d_methods_work() {
608        let mut curve = Curve::new();
609        curve
610            .points_3d_begin()
611            .points_3d_add(1, 2, 3)
612            .points_3d_add(4, 5, 6)
613            .points_3d_end();
614        let b: &str = "\
615                       xyz=np.array([[1,2,3],[4,5,6],])\n\
616                       ax3d().plot(xyz[:,0],xyz[:,1],xyz[:,2])\n";
617        assert_eq!(curve.buffer, b);
618    }
619
620    #[test]
621    fn draw_works() {
622        let x = &[1.0, 2.0, 3.0, 4.0, 5.0];
623        let y = &[1.0, 4.0, 9.0, 16.0, 25.0];
624        let mut curve = Curve::new();
625        curve.set_label("the-curve");
626        curve.draw(x, y);
627        let b: &str = "x=np.array([1,2,3,4,5,])\n\
628                       y=np.array([1,4,9,16,25,])\n\
629                       plt.plot(x,y,label=r'the-curve')\n";
630        assert_eq!(curve.buffer, b);
631        curve.clear_buffer();
632        assert_eq!(curve.buffer, "");
633    }
634
635    #[test]
636    fn draw_3d_works() {
637        let x = &[1.0, 2.0, 3.0, 4.0, 5.0];
638        let y = &[1.0, 4.0, 9.0, 16.0, 25.0];
639        let z = &[0.0, 0.0, 0.0, 1.0, 1.0];
640        let mut curve = Curve::new();
641        curve.set_label("the-curve");
642        curve.draw_3d(x, y, z);
643        let b: &str = "x=np.array([1,2,3,4,5,])\n\
644                       y=np.array([1,4,9,16,25,])\n\
645                       z=np.array([0,0,0,1,1,])\n\
646                       ax3d().plot(x,y,z,label=r'the-curve')\n";
647        assert_eq!(curve.buffer, b);
648    }
649
650    #[test]
651    fn derive_works() {
652        let endpoint = RayEndpoint::Coords(8.0, 0.5);
653        let cloned = endpoint.clone();
654        assert_eq!(format!("{:?}", endpoint), "Coords(8.0, 0.5)");
655        assert_eq!(format!("{:?}", cloned), "Coords(8.0, 0.5)");
656    }
657
658    #[test]
659    fn draw_ray_works() {
660        let mut ray = Curve::new();
661        ray.draw_ray(2.0, 0.0, RayEndpoint::Coords(8.0, 0.5));
662        ray.draw_ray(2.0, 0.0, RayEndpoint::Slope(0.2));
663        ray.draw_ray(2.0, 0.0, RayEndpoint::Horizontal);
664        ray.draw_ray(2.0, 0.0, RayEndpoint::Vertical);
665        let b: &str = "plt.axline((2,0),(8,0.5))\n\
666                       plt.axline((2,0),None,slope=0.2)\n\
667                       plt.axhline(0)\n\
668                       plt.axvline(2)\n";
669        assert_eq!(ray.buffer, b);
670    }
671}