criterion_plot/lib.rs
1//! [Criterion]'s plotting library.
2//!
3//! [Criterion]: https://github.com/criterion-rs/criterion.rs
4//!
5//! **WARNING** This library is criterion's implementation detail and there no plans to stabilize
6//! it. In other words, the API may break at any time without notice.
7//!
8//! # Examples
9//!
10//! - Simple "curves" (based on [`simple.dem`](http://gnuplot.sourceforge.net/demo/simple.html))
11//!
12//! 
13//!
14//! ```
15//! # use std::fs;
16//! # use std::path::Path;
17//! use itertools_num::linspace;
18//! use criterion_plot::prelude::*;
19//!
20//! # if let Err(_) = criterion_plot::version() {
21//! # return;
22//! # }
23//! let ref xs = linspace::<f64>(-10., 10., 51).collect::<Vec<_>>();
24//!
25//! # fs::create_dir_all(Path::new("target/doc/criterion_plot")).unwrap();
26//! # assert_eq!(Some(String::new()),
27//! Figure::new()
28//! # .set(Font("Helvetica"))
29//! # .set(FontSize(12.))
30//! # .set(Output(Path::new("target/doc/criterion_plot/curve.svg")))
31//! # .set(Size(1280, 720))
32//! .configure(Key, |k| {
33//! k.set(Boxed::Yes)
34//! .set(Position::Inside(Vertical::Top, Horizontal::Left))
35//! })
36//! .plot(LinesPoints {
37//! x: xs,
38//! y: xs.iter().map(|x| x.sin()),
39//! },
40//! |lp| {
41//! lp.set(Color::DarkViolet)
42//! .set(Label("sin(x)"))
43//! .set(LineType::Dash)
44//! .set(PointSize(1.5))
45//! .set(PointType::Circle)
46//! })
47//! .plot(Steps {
48//! x: xs,
49//! y: xs.iter().map(|x| x.atan()),
50//! },
51//! |s| {
52//! s.set(Color::Rgb(0, 158, 115))
53//! .set(Label("atan(x)"))
54//! .set(LineWidth(2.))
55//! })
56//! .plot(Impulses {
57//! x: xs,
58//! y: xs.iter().map(|x| x.atan().cos()),
59//! },
60//! |i| {
61//! i.set(Color::Rgb(86, 180, 233))
62//! .set(Label("cos(atan(x))"))
63//! })
64//! .draw() // (rest of the chain has been omitted)
65//! # .ok()
66//! # .and_then(|gnuplot| {
67//! # gnuplot.wait_with_output().ok().and_then(|p| String::from_utf8(p.stderr).ok())
68//! # }));
69//! ```
70//!
71//! - error bars (based on
72//! [Julia plotting tutorial](https://plot.ly/julia/error-bars/#Colored-and-Styled-Error-Bars))
73//!
74//! 
75//!
76//! ```
77//! # use std::fs;
78//! # use std::path::Path;
79//! use std::f64::consts::PI;
80//!
81//! use itertools_num::linspace;
82//! use rand::Rng;
83//! use criterion_plot::prelude::*;
84//!
85//! fn sinc(mut x: f64) -> f64 {
86//! if x == 0. {
87//! 1.
88//! } else {
89//! x *= PI;
90//! x.sin() / x
91//! }
92//! }
93//!
94//! # if let Err(_) = criterion_plot::version() {
95//! # return;
96//! # }
97//! let ref xs_ = linspace::<f64>(-4., 4., 101).collect::<Vec<_>>();
98//!
99//! // Fake some data
100//! let ref mut rng = rand::thread_rng();
101//! let xs = linspace::<f64>(-4., 4., 13).skip(1).take(11);
102//! let ys = xs.map(|x| sinc(x) + 0.05 * rng.gen::<f64>() - 0.025).collect::<Vec<_>>();
103//! let y_low = ys.iter().map(|&y| y - 0.025 - 0.075 * rng.gen::<f64>()).collect::<Vec<_>>();
104//! let y_high = ys.iter().map(|&y| y + 0.025 + 0.075 * rng.gen::<f64>()).collect::<Vec<_>>();
105//! let xs = linspace::<f64>(-4., 4., 13).skip(1).take(11);
106//! let xs = xs.map(|x| x + 0.2 * rng.gen::<f64>() - 0.1);
107//!
108//! # fs::create_dir_all(Path::new("target/doc/criterion_plot")).unwrap();
109//! # assert_eq!(Some(String::new()),
110//! Figure::new()
111//! # .set(Font("Helvetica"))
112//! # .set(FontSize(12.))
113//! # .set(Output(Path::new("target/doc/criterion_plot/error_bar.svg")))
114//! # .set(Size(1280, 720))
115//! .configure(Axis::BottomX, |a| {
116//! a.set(TicLabels {
117//! labels: &["-π", "0", "π"],
118//! positions: &[-PI, 0., PI],
119//! })
120//! })
121//! .configure(Key,
122//! |k| k.set(Position::Outside(Vertical::Top, Horizontal::Right)))
123//! .plot(Lines {
124//! x: xs_,
125//! y: xs_.iter().cloned().map(sinc),
126//! },
127//! |l| {
128//! l.set(Color::Rgb(0, 158, 115))
129//! .set(Label("sinc(x)"))
130//! .set(LineWidth(2.))
131//! })
132//! .plot(YErrorBars {
133//! x: xs,
134//! y: &ys,
135//! y_low: &y_low,
136//! y_high: &y_high,
137//! },
138//! |eb| {
139//! eb.set(Color::DarkViolet)
140//! .set(LineWidth(2.))
141//! .set(PointType::FilledCircle)
142//! .set(Label("measured"))
143//! })
144//! .draw() // (rest of the chain has been omitted)
145//! # .ok()
146//! # .and_then(|gnuplot| {
147//! # gnuplot.wait_with_output().ok().and_then(|p| String::from_utf8(p.stderr).ok())
148//! # }));
149//! ```
150//!
151//! - Candlesticks (based on
152//! [`candlesticks.dem`](http://gnuplot.sourceforge.net/demo/candlesticks.html))
153//!
154//! 
155//!
156//! ```
157//! # use std::fs;
158//! # use std::path::Path;
159//! use criterion_plot::prelude::*;
160//! use rand::Rng;
161//!
162//! # if let Err(_) = criterion_plot::version() {
163//! # return;
164//! # }
165//! let xs = 1..11;
166//!
167//! // Fake some data
168//! let mut rng = rand::thread_rng();
169//! let bh = xs.clone().map(|_| 5f64 + 2.5 * rng.gen::<f64>()).collect::<Vec<_>>();
170//! let bm = xs.clone().map(|_| 2.5f64 + 2.5 * rng.gen::<f64>()).collect::<Vec<_>>();
171//! let wh = bh.iter().map(|&y| y + (10. - y) * rng.gen::<f64>()).collect::<Vec<_>>();
172//! let wm = bm.iter().map(|&y| y * rng.gen::<f64>()).collect::<Vec<_>>();
173//! let m = bm.iter().zip(bh.iter()).map(|(&l, &h)| (h - l) * rng.gen::<f64>() + l)
174//! .collect::<Vec<_>>();
175//!
176//! # fs::create_dir_all(Path::new("target/doc/criterion_plot")).unwrap();
177//! # assert_eq!(Some(String::new()),
178//! Figure::new()
179//! # .set(Font("Helvetica"))
180//! # .set(FontSize(12.))
181//! # .set(Output(Path::new("target/doc/criterion_plot/candlesticks.svg")))
182//! # .set(Size(1280, 720))
183//! .set(BoxWidth(0.2))
184//! .configure(Axis::BottomX, |a| a.set(Range::Limits(0., 11.)))
185//! .plot(Candlesticks {
186//! x: xs.clone(),
187//! whisker_min: &wm,
188//! box_min: &bm,
189//! box_high: &bh,
190//! whisker_high: &wh,
191//! },
192//! |cs| {
193//! cs.set(Color::Rgb(86, 180, 233))
194//! .set(Label("Quartiles"))
195//! .set(LineWidth(2.))
196//! })
197//! // trick to plot the median
198//! .plot(Candlesticks {
199//! x: xs,
200//! whisker_min: &m,
201//! box_min: &m,
202//! box_high: &m,
203//! whisker_high: &m,
204//! },
205//! |cs| {
206//! cs.set(Color::Black)
207//! .set(LineWidth(2.))
208//! })
209//! .draw() // (rest of the chain has been omitted)
210//! # .ok()
211//! # .and_then(|gnuplot| {
212//! # gnuplot.wait_with_output().ok().and_then(|p| String::from_utf8(p.stderr).ok())
213//! # }));
214//! ```
215//!
216//! - Multiaxis (based on [`multiaxis.dem`](http://gnuplot.sourceforge.net/demo/multiaxis.html))
217//!
218//! 
219//!
220//! ```
221//! # use std::fs;
222//! # use std::path::Path;
223//! use std::f64::consts::PI;
224//!
225//! use itertools_num::linspace;
226//! use num_complex::Complex;
227//! use criterion_plot::prelude::*;
228//!
229//! fn tf(x: f64) -> Complex<f64> {
230//! Complex::new(0., x) / Complex::new(10., x) / Complex::new(1., x / 10_000.)
231//! }
232//!
233//! # if let Err(_) = criterion_plot::version() {
234//! # return;
235//! # }
236//! let (start, end): (f64, f64) = (1.1, 90_000.);
237//! let ref xs = linspace(start.ln(), end.ln(), 101).map(|x| x.exp()).collect::<Vec<_>>();
238//! let phase = xs.iter().map(|&x| tf(x).arg() * 180. / PI);
239//! let magnitude = xs.iter().map(|&x| tf(x).norm());
240//!
241//! # fs::create_dir_all(Path::new("target/doc/criterion_plot")).unwrap();
242//! # assert_eq!(Some(String::new()),
243//! Figure::new().
244//! # set(Font("Helvetica")).
245//! # set(FontSize(12.)).
246//! # set(Output(Path::new("target/doc/criterion_plot/multiaxis.svg"))).
247//! # set(Size(1280, 720)).
248//! set(Title("Frequency response")).
249//! configure(Axis::BottomX, |a| a.
250//! configure(Grid::Major, |g| g.
251//! show()).
252//! set(Label("Angular frequency (rad/s)")).
253//! set(Range::Limits(start, end)).
254//! set(Scale::Logarithmic)).
255//! configure(Axis::LeftY, |a| a.
256//! set(Label("Gain")).
257//! set(Scale::Logarithmic)).
258//! configure(Axis::RightY, |a| a.
259//! configure(Grid::Major, |g| g.
260//! show()).
261//! set(Label("Phase shift (°)"))).
262//! configure(Key, |k| k.
263//! set(Position::Inside(Vertical::Top, Horizontal::Center)).
264//! set(Title(" "))).
265//! plot(Lines {
266//! x: xs,
267//! y: magnitude,
268//! }, |l| l.
269//! set(Color::DarkViolet).
270//! set(Label("Magnitude")).
271//! set(LineWidth(2.))).
272//! plot(Lines {
273//! x: xs,
274//! y: phase,
275//! }, |l| l.
276//! set(Axes::BottomXRightY).
277//! set(Color::Rgb(0, 158, 115)).
278//! set(Label("Phase")).
279//! set(LineWidth(2.))).
280//! draw(). // (rest of the chain has been omitted)
281//! # ok().and_then(|gnuplot| {
282//! # gnuplot.wait_with_output().ok().and_then(|p| {
283//! # String::from_utf8(p.stderr).ok()
284//! # })
285//! # }));
286//! ```
287//! - Filled curves (based on
288//! [`transparent.dem`](http://gnuplot.sourceforge.net/demo/transparent.html))
289//!
290//! 
291//!
292//! ```
293//! # use std::fs;
294//! # use std::path::Path;
295//! use std::f64::consts::PI;
296//! use std::iter;
297//!
298//! use itertools_num::linspace;
299//! use criterion_plot::prelude::*;
300//!
301//! # if let Err(_) = criterion_plot::version() {
302//! # return;
303//! # }
304//! let (start, end) = (-5., 5.);
305//! let ref xs = linspace(start, end, 101).collect::<Vec<_>>();
306//! let zeros = iter::repeat(0);
307//!
308//! fn gaussian(x: f64, mu: f64, sigma: f64) -> f64 {
309//! 1. / (((x - mu).powi(2) / 2. / sigma.powi(2)).exp() * sigma * (2. * PI).sqrt())
310//! }
311//!
312//! # fs::create_dir_all(Path::new("target/doc/criterion_plot")).unwrap();
313//! # assert_eq!(Some(String::new()),
314//! Figure::new()
315//! # .set(Font("Helvetica"))
316//! # .set(FontSize(12.))
317//! # .set(Output(Path::new("target/doc/criterion_plot/filled_curve.svg")))
318//! # .set(Size(1280, 720))
319//! .set(Title("Transparent filled curve"))
320//! .configure(Axis::BottomX, |a| a.set(Range::Limits(start, end)))
321//! .configure(Axis::LeftY, |a| a.set(Range::Limits(0., 1.)))
322//! .configure(Key, |k| {
323//! k.set(Justification::Left)
324//! .set(Order::SampleText)
325//! .set(Position::Inside(Vertical::Top, Horizontal::Left))
326//! .set(Title("Gaussian Distribution"))
327//! })
328//! .plot(FilledCurve {
329//! x: xs,
330//! y1: xs.iter().map(|&x| gaussian(x, 0.5, 0.5)),
331//! y2: zeros.clone(),
332//! },
333//! |fc| {
334//! fc.set(Color::ForestGreen)
335//! .set(Label("μ = 0.5 σ = 0.5"))
336//! })
337//! .plot(FilledCurve {
338//! x: xs,
339//! y1: xs.iter().map(|&x| gaussian(x, 2.0, 1.0)),
340//! y2: zeros.clone(),
341//! },
342//! |fc| {
343//! fc.set(Color::Gold)
344//! .set(Label("μ = 2.0 σ = 1.0"))
345//! .set(Opacity(0.5))
346//! })
347//! .plot(FilledCurve {
348//! x: xs,
349//! y1: xs.iter().map(|&x| gaussian(x, -1.0, 2.0)),
350//! y2: zeros,
351//! },
352//! |fc| {
353//! fc.set(Color::Red)
354//! .set(Label("μ = -1.0 σ = 2.0"))
355//! .set(Opacity(0.5))
356//! })
357//! .draw()
358//! .ok()
359//! .and_then(|gnuplot| {
360//! gnuplot.wait_with_output().ok().and_then(|p| String::from_utf8(p.stderr).ok())
361//! }));
362//! ```
363
364#![deny(clippy::doc_markdown, missing_docs)]
365#![deny(warnings)]
366#![deny(bare_trait_objects)]
367// This lint has lots of false positives ATM, see
368// https://github.com/Manishearth/rust-clippy/issues/761
369#![allow(clippy::new_without_default)]
370#![allow(clippy::many_single_char_names)]
371
372use std::{
373 borrow::Cow,
374 fmt,
375 fs::File,
376 io,
377 num::ParseIntError,
378 path::Path,
379 process::{Child, Command},
380 str,
381};
382
383use crate::{
384 data::Matrix,
385 traits::{Configure, Set},
386};
387
388mod data;
389mod display;
390mod map;
391
392pub mod axis;
393pub mod candlestick;
394pub mod curve;
395pub mod errorbar;
396pub mod filledcurve;
397pub mod grid;
398pub mod key;
399pub mod prelude;
400pub mod proxy;
401pub mod traits;
402
403/// Plot container
404#[derive(Clone)]
405pub struct Figure {
406 alpha: Option<f64>,
407 axes: map::axis::Map<axis::Properties>,
408 box_width: Option<f64>,
409 font: Option<Cow<'static, str>>,
410 font_size: Option<f64>,
411 key: Option<key::Properties>,
412 output: Cow<'static, Path>,
413 plots: Vec<Plot>,
414 size: Option<(usize, usize)>,
415 terminal: Terminal,
416 tics: map::axis::Map<String>,
417 title: Option<Cow<'static, str>>,
418}
419
420impl Figure {
421 /// Creates an empty figure
422 pub fn new() -> Figure {
423 Figure {
424 alpha: None,
425 axes: map::axis::Map::new(),
426 box_width: None,
427 font: None,
428 font_size: None,
429 key: None,
430 output: Cow::Borrowed(Path::new("output.plot")),
431 plots: Vec::new(),
432 size: None,
433 terminal: Terminal::Svg,
434 tics: map::axis::Map::new(),
435 title: None,
436 }
437 }
438
439 fn script(&self) -> Vec<u8> {
440 let mut s = String::new();
441
442 s.push_str(&format!(
443 "set output '{}'\n",
444 self.output.display().to_string().replace('\'', "''")
445 ));
446
447 if let Some(width) = self.box_width {
448 s.push_str(&format!("set boxwidth {}\n", width))
449 }
450
451 if let Some(ref title) = self.title {
452 s.push_str(&format!("set title '{}'\n", title))
453 }
454
455 for axis in self.axes.iter() {
456 s.push_str(&axis.script());
457 }
458
459 for (_, script) in self.tics.iter() {
460 s.push_str(script);
461 }
462
463 if let Some(ref key) = self.key {
464 s.push_str(&key.script())
465 }
466
467 if let Some(alpha) = self.alpha {
468 s.push_str(&format!("set style fill transparent solid {}\n", alpha))
469 }
470
471 s.push_str(&format!("set terminal {} dashed", self.terminal.display()));
472
473 if let Some((width, height)) = self.size {
474 s.push_str(&format!(" size {}, {}", width, height))
475 }
476
477 if let Some(ref name) = self.font {
478 if let Some(size) = self.font_size {
479 s.push_str(&format!(" font '{},{}'", name, size))
480 } else {
481 s.push_str(&format!(" font '{}'", name))
482 }
483 }
484
485 // TODO This removes the crossbars from the ends of error bars, but should be configurable
486 s.push_str("\nunset bars\n");
487
488 let mut is_first_plot = true;
489 for plot in &self.plots {
490 let data = plot.data();
491
492 if data.bytes().is_empty() {
493 continue;
494 }
495
496 if is_first_plot {
497 s.push_str("plot ");
498 is_first_plot = false;
499 } else {
500 s.push_str(", ");
501 }
502
503 s.push_str(&format!(
504 "'-' binary endian=little record={} format='%float64' using ",
505 data.nrows()
506 ));
507
508 let mut is_first_col = true;
509 for col in 0..data.ncols() {
510 if is_first_col {
511 is_first_col = false;
512 } else {
513 s.push(':');
514 }
515 s.push_str(&(col + 1).to_string());
516 }
517 s.push(' ');
518
519 s.push_str(plot.script());
520 }
521
522 let mut buffer = s.into_bytes();
523 let mut is_first = true;
524 for plot in &self.plots {
525 if is_first {
526 is_first = false;
527 buffer.push(b'\n');
528 }
529 buffer.extend_from_slice(plot.data().bytes());
530 }
531
532 buffer
533 }
534
535 /// Spawns a drawing child process
536 ///
537 /// NOTE: stderr, stdin, and stdout are piped
538 pub fn draw(&mut self) -> io::Result<Child> {
539 use std::process::Stdio;
540
541 let mut gnuplot = Command::new("gnuplot")
542 .stderr(Stdio::piped())
543 .stdin(Stdio::piped())
544 .stdout(Stdio::piped())
545 .spawn()?;
546 self.dump(gnuplot.stdin.as_mut().unwrap())?;
547 Ok(gnuplot)
548 }
549
550 /// Dumps the script required to produce the figure into `sink`
551 pub fn dump<W>(&mut self, sink: &mut W) -> io::Result<&mut Figure>
552 where
553 W: io::Write,
554 {
555 sink.write_all(&self.script())?;
556 Ok(self)
557 }
558
559 /// Saves the script required to produce the figure to `path`
560 pub fn save(&self, path: &Path) -> io::Result<&Figure> {
561 use std::io::Write;
562
563 File::create(path)?.write_all(&self.script())?;
564 Ok(self)
565 }
566}
567
568impl Configure<Axis> for Figure {
569 type Properties = axis::Properties;
570
571 /// Configures an axis
572 fn configure<F>(&mut self, axis: Axis, configure: F) -> &mut Figure
573 where
574 F: FnOnce(&mut axis::Properties) -> &mut axis::Properties,
575 {
576 if self.axes.contains_key(axis) {
577 configure(self.axes.get_mut(axis).unwrap());
578 } else {
579 let mut properties = Default::default();
580 configure(&mut properties);
581 self.axes.insert(axis, properties);
582 }
583 self
584 }
585}
586
587impl Configure<Key> for Figure {
588 type Properties = key::Properties;
589
590 /// Configures the key (legend)
591 fn configure<F>(&mut self, _: Key, configure: F) -> &mut Figure
592 where
593 F: FnOnce(&mut key::Properties) -> &mut key::Properties,
594 {
595 if self.key.is_some() {
596 configure(self.key.as_mut().unwrap());
597 } else {
598 let mut key = Default::default();
599 configure(&mut key);
600 self.key = Some(key);
601 }
602 self
603 }
604}
605
606impl Set<BoxWidth> for Figure {
607 /// Changes the box width of all the box related plots (bars, candlesticks, etc)
608 ///
609 /// **Note** The default value is 0
610 ///
611 /// # Panics
612 ///
613 /// Panics if `width` is a negative value
614 fn set(&mut self, width: BoxWidth) -> &mut Figure {
615 let width = width.0;
616
617 assert!(width >= 0.);
618
619 self.box_width = Some(width);
620 self
621 }
622}
623
624impl Set<Font> for Figure {
625 /// Changes the font
626 fn set(&mut self, font: Font) -> &mut Figure {
627 self.font = Some(font.0);
628 self
629 }
630}
631
632impl Set<FontSize> for Figure {
633 /// Changes the size of the font
634 ///
635 /// # Panics
636 ///
637 /// Panics if `size` is a non-positive value
638 fn set(&mut self, size: FontSize) -> &mut Figure {
639 let size = size.0;
640
641 assert!(size >= 0.);
642
643 self.font_size = Some(size);
644 self
645 }
646}
647
648impl Set<Output> for Figure {
649 /// Changes the output file
650 ///
651 /// **Note** The default output file is `output.plot`
652 fn set(&mut self, output: Output) -> &mut Figure {
653 self.output = output.0;
654 self
655 }
656}
657
658impl Set<Size> for Figure {
659 /// Changes the figure size
660 fn set(&mut self, size: Size) -> &mut Figure {
661 self.size = Some((size.0, size.1));
662 self
663 }
664}
665
666impl Set<Terminal> for Figure {
667 /// Changes the output terminal
668 ///
669 /// **Note** By default, the terminal is set to `Svg`
670 fn set(&mut self, terminal: Terminal) -> &mut Figure {
671 self.terminal = terminal;
672 self
673 }
674}
675
676impl Set<Title> for Figure {
677 /// Sets the title
678 fn set(&mut self, title: Title) -> &mut Figure {
679 self.title = Some(title.0);
680 self
681 }
682}
683
684impl Default for Figure {
685 fn default() -> Self {
686 Self::new()
687 }
688}
689
690/// Box width for box-related plots: bars, candlesticks, etc
691#[derive(Clone, Copy)]
692pub struct BoxWidth(pub f64);
693
694/// A font name
695pub struct Font(Cow<'static, str>);
696
697/// The size of a font
698#[derive(Clone, Copy)]
699pub struct FontSize(pub f64);
700
701/// The key or legend
702#[derive(Clone, Copy)]
703pub struct Key;
704
705/// Plot label
706pub struct Label(Cow<'static, str>);
707
708/// Width of the lines
709#[derive(Clone, Copy)]
710pub struct LineWidth(pub f64);
711
712/// Fill color opacity
713#[derive(Clone, Copy)]
714pub struct Opacity(pub f64);
715
716/// Output file path
717pub struct Output(Cow<'static, Path>);
718
719/// Size of the points
720#[derive(Clone, Copy)]
721pub struct PointSize(pub f64);
722
723/// Axis range
724#[derive(Clone, Copy)]
725pub enum Range {
726 /// Autoscale the axis
727 Auto,
728 /// Set the limits of the axis
729 Limits(f64, f64),
730}
731
732/// Figure size
733#[derive(Clone, Copy)]
734pub struct Size(pub usize, pub usize);
735
736/// Labels attached to the tics of an axis
737pub struct TicLabels<P, L> {
738 /// Labels to attach to the tics
739 pub labels: L,
740 /// Position of the tics on the axis
741 pub positions: P,
742}
743
744/// Figure title
745pub struct Title(Cow<'static, str>);
746
747/// A pair of axes that define a coordinate system
748#[allow(missing_docs)]
749#[derive(Clone, Copy)]
750pub enum Axes {
751 BottomXLeftY,
752 BottomXRightY,
753 TopXLeftY,
754 TopXRightY,
755}
756
757/// A coordinate axis
758#[derive(Clone, Copy)]
759pub enum Axis {
760 /// X axis on the bottom side of the figure
761 BottomX,
762 /// Y axis on the left side of the figure
763 LeftY,
764 /// Y axis on the right side of the figure
765 RightY,
766 /// X axis on the top side of the figure
767 TopX,
768}
769
770impl Axis {
771 fn next(self) -> Option<Axis> {
772 use crate::Axis::*;
773
774 match self {
775 BottomX => Some(LeftY),
776 LeftY => Some(RightY),
777 RightY => Some(TopX),
778 TopX => None,
779 }
780 }
781}
782
783/// Color
784#[allow(missing_docs)]
785#[derive(Clone, Copy)]
786pub enum Color {
787 Black,
788 Blue,
789 Cyan,
790 DarkViolet,
791 ForestGreen,
792 Gold,
793 Gray,
794 Green,
795 Magenta,
796 Red,
797 /// Custom RGB color
798 Rgb(u8, u8, u8),
799 White,
800 Yellow,
801}
802
803/// Grid line
804#[derive(Clone, Copy)]
805pub enum Grid {
806 /// Major gridlines
807 Major,
808 /// Minor gridlines
809 Minor,
810}
811
812impl Grid {
813 fn next(self) -> Option<Grid> {
814 use crate::Grid::*;
815
816 match self {
817 Major => Some(Minor),
818 Minor => None,
819 }
820 }
821}
822
823/// Line type
824#[allow(missing_docs)]
825#[derive(Clone, Copy)]
826pub enum LineType {
827 Dash,
828 Dot,
829 DotDash,
830 DotDotDash,
831 /// Line made of minimally sized dots
832 SmallDot,
833 Solid,
834}
835
836/// Point type
837#[allow(missing_docs)]
838#[derive(Clone, Copy)]
839pub enum PointType {
840 Circle,
841 FilledCircle,
842 FilledSquare,
843 FilledTriangle,
844 Plus,
845 Square,
846 Star,
847 Triangle,
848 X,
849}
850
851/// Axis scale
852#[allow(missing_docs)]
853#[derive(Clone, Copy)]
854pub enum Scale {
855 Linear,
856 Logarithmic,
857}
858
859/// Axis scale factor
860#[allow(missing_docs)]
861#[derive(Clone, Copy)]
862pub struct ScaleFactor(pub f64);
863
864/// Output terminal
865#[allow(missing_docs)]
866#[derive(Clone, Copy)]
867pub enum Terminal {
868 Svg,
869}
870
871/// Not public version of `std::default::Default`, used to not leak default constructors into the
872/// public API
873trait Default {
874 /// Creates `Properties` with default configuration
875 fn default() -> Self;
876}
877
878/// Enums that can produce gnuplot code
879trait Display<S> {
880 /// Translates the enum in gnuplot code
881 fn display(&self) -> S;
882}
883
884/// Curve variant of Default
885trait CurveDefault<S> {
886 /// Creates `curve::Properties` with default configuration
887 fn default(s: S) -> Self;
888}
889
890/// Error bar variant of Default
891trait ErrorBarDefault<S> {
892 /// Creates `errorbar::Properties` with default configuration
893 fn default(s: S) -> Self;
894}
895
896/// Structs that can produce gnuplot code
897trait Script {
898 /// Translates some configuration struct into gnuplot code
899 fn script(&self) -> String;
900}
901
902#[derive(Clone)]
903struct Plot {
904 data: Matrix,
905 script: String,
906}
907
908impl Plot {
909 fn new<S>(data: Matrix, script: &S) -> Plot
910 where
911 S: Script,
912 {
913 Plot {
914 data,
915 script: script.script(),
916 }
917 }
918
919 fn data(&self) -> &Matrix {
920 &self.data
921 }
922
923 fn script(&self) -> &str {
924 &self.script
925 }
926}
927
928/// Possible errors when parsing gnuplot's version string
929#[derive(Debug)]
930pub enum VersionError {
931 /// The `gnuplot` command couldn't be executed
932 Exec(io::Error),
933 /// The `gnuplot` command returned an error message
934 Error(String),
935 /// The `gnuplot` command returned invalid utf-8
936 OutputError,
937 /// The `gnuplot` command returned an unparsable string
938 ParseError(String),
939}
940impl fmt::Display for VersionError {
941 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
942 match self {
943 VersionError::Exec(err) => write!(f, "`gnuplot --version` failed: {}", err),
944 VersionError::Error(msg) => {
945 write!(f, "`gnuplot --version` failed with error message:\n{}", msg)
946 }
947 VersionError::OutputError => write!(f, "`gnuplot --version` returned invalid utf-8"),
948 VersionError::ParseError(msg) => write!(
949 f,
950 "`gnuplot --version` returned an unparsable version string: {}",
951 msg
952 ),
953 }
954 }
955}
956impl ::std::error::Error for VersionError {
957 fn description(&self) -> &str {
958 match self {
959 VersionError::Exec(_) => "Execution Error",
960 VersionError::Error(_) => "Other Error",
961 VersionError::OutputError => "Output Error",
962 VersionError::ParseError(_) => "Parse Error",
963 }
964 }
965
966 fn cause(&self) -> Option<&dyn ::std::error::Error> {
967 match self {
968 VersionError::Exec(err) => Some(err),
969 _ => None,
970 }
971 }
972}
973
974/// Structure representing a gnuplot version number.
975pub struct Version {
976 /// The major version number
977 pub major: usize,
978 /// The minor version number
979 pub minor: usize,
980 /// The patch level
981 pub patch: String,
982}
983
984/// Returns `gnuplot` version
985pub fn version() -> Result<Version, VersionError> {
986 let command_output = Command::new("gnuplot")
987 .arg("--version")
988 .output()
989 .map_err(VersionError::Exec)?;
990 if !command_output.status.success() {
991 let error =
992 String::from_utf8(command_output.stderr).map_err(|_| VersionError::OutputError)?;
993 return Err(VersionError::Error(error));
994 }
995
996 parse_version_utf8(&command_output.stdout).or_else(|utf8_err| {
997 // gnuplot can emit UTF-16 on some systems/configurations (e.g. some Windows machines).
998 // If we failed to parse as UTF-8, try again as UTF-16 to account for this.
999 // If UTF-16 parsing also fails, return the original error we got for UTF-8 to avoid confusing matters more.
1000 parse_version_utf16(&command_output.stdout).map_err(|_| utf8_err)
1001 })
1002}
1003
1004fn parse_version_utf8(output_bytes: &[u8]) -> Result<Version, VersionError> {
1005 let output = str::from_utf8(output_bytes).map_err(|_| VersionError::OutputError)?;
1006 parse_version(output).map_err(|_| VersionError::ParseError(output.to_owned()))
1007}
1008
1009fn parse_version_utf16(output_bytes: &[u8]) -> Result<Version, VersionError> {
1010 if output_bytes.len() % 2 != 0 {
1011 // Not an even number of bytes, so cannot be UTF-16.
1012 return Err(VersionError::OutputError);
1013 }
1014
1015 let output_as_u16: Vec<u16> = output_bytes
1016 .chunks_exact(2)
1017 .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
1018 .collect();
1019
1020 let output = String::from_utf16(&output_as_u16).map_err(|_| VersionError::OutputError)?;
1021 parse_version(&output).map_err(|_| VersionError::ParseError(output.to_owned()))
1022}
1023
1024fn parse_version(version_str: &str) -> Result<Version, Option<ParseIntError>> {
1025 let mut words = version_str.split_whitespace().skip(1);
1026 let mut version = words.next().ok_or(None)?.split('.');
1027 let major = version.next().ok_or(None)?.parse()?;
1028 let minor = version.next().ok_or(None)?.parse()?;
1029 let patchlevel = words.nth(1).ok_or(None)?.to_owned();
1030
1031 Ok(Version {
1032 major,
1033 minor,
1034 patch: patchlevel,
1035 })
1036}
1037
1038fn scale_factor(map: &map::axis::Map<axis::Properties>, axes: Axes) -> (f64, f64) {
1039 use crate::{Axes::*, Axis::*};
1040
1041 match axes {
1042 BottomXLeftY => (
1043 map.get(BottomX).map_or(1., ScaleFactorTrait::scale_factor),
1044 map.get(LeftY).map_or(1., ScaleFactorTrait::scale_factor),
1045 ),
1046 BottomXRightY => (
1047 map.get(BottomX).map_or(1., ScaleFactorTrait::scale_factor),
1048 map.get(RightY).map_or(1., ScaleFactorTrait::scale_factor),
1049 ),
1050 TopXLeftY => (
1051 map.get(TopX).map_or(1., ScaleFactorTrait::scale_factor),
1052 map.get(LeftY).map_or(1., ScaleFactorTrait::scale_factor),
1053 ),
1054 TopXRightY => (
1055 map.get(TopX).map_or(1., ScaleFactorTrait::scale_factor),
1056 map.get(RightY).map_or(1., ScaleFactorTrait::scale_factor),
1057 ),
1058 }
1059}
1060
1061// XXX :-1: to intra-crate privacy rules
1062/// Private
1063trait ScaleFactorTrait {
1064 /// Private
1065 fn scale_factor(&self) -> f64;
1066}
1067
1068#[cfg(test)]
1069mod test {
1070 #[test]
1071 fn version() {
1072 if let Ok(version) = super::version() {
1073 assert!(version.major >= 4);
1074 } else {
1075 println!("Gnuplot not installed.");
1076 }
1077 }
1078
1079 #[test]
1080 fn test_parse_version_on_valid_string() {
1081 let string = "gnuplot 5.0 patchlevel 7";
1082 let version = super::parse_version(string).unwrap();
1083 assert_eq!(5, version.major);
1084 assert_eq!(0, version.minor);
1085 assert_eq!("7", &version.patch);
1086 }
1087
1088 #[test]
1089 fn test_parse_gentoo_version() {
1090 let string = "gnuplot 5.2 patchlevel 5a (Gentoo revision r0)";
1091 let version = super::parse_version(string).unwrap();
1092 assert_eq!(5, version.major);
1093 assert_eq!(2, version.minor);
1094 assert_eq!("5a", &version.patch);
1095 }
1096
1097 #[test]
1098 fn test_parse_version_returns_error_on_invalid_strings() {
1099 let strings = [
1100 "",
1101 "foobar",
1102 "gnuplot 50 patchlevel 7",
1103 "gnuplot 5.0 patchlevel",
1104 "gnuplot foo.bar patchlevel 7",
1105 ];
1106 for string in &strings {
1107 assert!(super::parse_version(string).is_err());
1108 }
1109 }
1110}