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/// 
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/// 
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/// 
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/// 
158///
159/// 
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}