1 //! [Criterion]'s plotting library.
2 //!
3 //! [Criterion]: https://github.com/bheisler/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 //! ![Plot](curve.svg)
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 //! ![Plot](error_bar.svg)
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 //! ![Plot](candlesticks.svg)
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 //! ![Plot](multiaxis.svg)
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 //! ![Plot](filled_curve.svg)
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(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 #![cfg_attr(feature = "cargo-clippy", allow(clippy::new_without_default))]
370 // False positives with images
371 #![cfg_attr(feature = "cargo-clippy", allow(clippy::doc_markdown))]
372 #![cfg_attr(feature = "cargo-clippy", allow(clippy::many_single_char_names))]
373 
374 extern crate cast;
375 #[macro_use]
376 extern crate itertools;
377 
378 use std::borrow::Cow;
379 use std::fmt;
380 use std::fs::File;
381 use std::io;
382 use std::num::ParseIntError;
383 use std::path::Path;
384 use std::process::{Child, Command};
385 use std::str;
386 
387 use crate::data::Matrix;
388 use crate::traits::{Configure, Set};
389 
390 mod data;
391 mod display;
392 mod map;
393 
394 pub mod axis;
395 pub mod candlestick;
396 pub mod curve;
397 pub mod errorbar;
398 pub mod filledcurve;
399 pub mod grid;
400 pub mod key;
401 pub mod prelude;
402 pub mod proxy;
403 pub mod traits;
404 
405 /// Plot container
406 #[derive(Clone)]
407 pub struct Figure {
408     alpha: Option<f64>,
409     axes: map::axis::Map<axis::Properties>,
410     box_width: Option<f64>,
411     font: Option<Cow<'static, str>>,
412     font_size: Option<f64>,
413     key: Option<key::Properties>,
414     output: Cow<'static, Path>,
415     plots: Vec<Plot>,
416     size: Option<(usize, usize)>,
417     terminal: Terminal,
418     tics: map::axis::Map<String>,
419     title: Option<Cow<'static, str>>,
420 }
421 
422 impl Figure {
423     /// Creates an empty figure
new() -> Figure424     pub fn new() -> Figure {
425         Figure {
426             alpha: None,
427             axes: map::axis::Map::new(),
428             box_width: None,
429             font: None,
430             font_size: None,
431             key: None,
432             output: Cow::Borrowed(Path::new("output.plot")),
433             plots: Vec::new(),
434             size: None,
435             terminal: Terminal::Svg,
436             tics: map::axis::Map::new(),
437             title: None,
438         }
439     }
440 
441     // Allow clippy::format_push_string even with older versions of rust (<1.62) which
442     // don't have it defined.
443     #[allow(clippy::all)]
script(&self) -> Vec<u8>444     fn script(&self) -> Vec<u8> {
445         let mut s = String::new();
446 
447         s.push_str(&format!(
448             "set output '{}'\n",
449             self.output.display().to_string().replace('\'', "''")
450         ));
451 
452         if let Some(width) = self.box_width {
453             s.push_str(&format!("set boxwidth {}\n", width))
454         }
455 
456         if let Some(ref title) = self.title {
457             s.push_str(&format!("set title '{}'\n", title))
458         }
459 
460         for axis in self.axes.iter() {
461             s.push_str(&axis.script());
462         }
463 
464         for (_, script) in self.tics.iter() {
465             s.push_str(script);
466         }
467 
468         if let Some(ref key) = self.key {
469             s.push_str(&key.script())
470         }
471 
472         if let Some(alpha) = self.alpha {
473             s.push_str(&format!("set style fill transparent solid {}\n", alpha))
474         }
475 
476         s.push_str(&format!("set terminal {} dashed", self.terminal.display()));
477 
478         if let Some((width, height)) = self.size {
479             s.push_str(&format!(" size {}, {}", width, height))
480         }
481 
482         if let Some(ref name) = self.font {
483             if let Some(size) = self.font_size {
484                 s.push_str(&format!(" font '{},{}'", name, size))
485             } else {
486                 s.push_str(&format!(" font '{}'", name))
487             }
488         }
489 
490         // TODO This removes the crossbars from the ends of error bars, but should be configurable
491         s.push_str("\nunset bars\n");
492 
493         let mut is_first_plot = true;
494         for plot in &self.plots {
495             let data = plot.data();
496 
497             if data.bytes().is_empty() {
498                 continue;
499             }
500 
501             if is_first_plot {
502                 s.push_str("plot ");
503                 is_first_plot = false;
504             } else {
505                 s.push_str(", ");
506             }
507 
508             s.push_str(&format!(
509                 "'-' binary endian=little record={} format='%float64' using ",
510                 data.nrows()
511             ));
512 
513             let mut is_first_col = true;
514             for col in 0..data.ncols() {
515                 if is_first_col {
516                     is_first_col = false;
517                 } else {
518                     s.push(':');
519                 }
520                 s.push_str(&(col + 1).to_string());
521             }
522             s.push(' ');
523 
524             s.push_str(plot.script());
525         }
526 
527         let mut buffer = s.into_bytes();
528         let mut is_first = true;
529         for plot in &self.plots {
530             if is_first {
531                 is_first = false;
532                 buffer.push(b'\n');
533             }
534             buffer.extend_from_slice(plot.data().bytes());
535         }
536 
537         buffer
538     }
539 
540     /// Spawns a drawing child process
541     ///
542     /// NOTE: stderr, stdin, and stdout are piped
draw(&mut self) -> io::Result<Child>543     pub fn draw(&mut self) -> io::Result<Child> {
544         use std::process::Stdio;
545 
546         let mut gnuplot = Command::new("gnuplot")
547             .stderr(Stdio::piped())
548             .stdin(Stdio::piped())
549             .stdout(Stdio::piped())
550             .spawn()?;
551         self.dump(gnuplot.stdin.as_mut().unwrap())?;
552         Ok(gnuplot)
553     }
554 
555     /// Dumps the script required to produce the figure into `sink`
dump<W>(&mut self, sink: &mut W) -> io::Result<&mut Figure> where W: io::Write,556     pub fn dump<W>(&mut self, sink: &mut W) -> io::Result<&mut Figure>
557     where
558         W: io::Write,
559     {
560         sink.write_all(&self.script())?;
561         Ok(self)
562     }
563 
564     /// Saves the script required to produce the figure to `path`
save(&self, path: &Path) -> io::Result<&Figure>565     pub fn save(&self, path: &Path) -> io::Result<&Figure> {
566         use std::io::Write;
567 
568         File::create(path)?.write_all(&self.script())?;
569         Ok(self)
570     }
571 }
572 
573 impl Configure<Axis> for Figure {
574     type Properties = axis::Properties;
575 
576     /// Configures an axis
configure<F>(&mut self, axis: Axis, configure: F) -> &mut Figure where F: FnOnce(&mut axis::Properties) -> &mut axis::Properties,577     fn configure<F>(&mut self, axis: Axis, configure: F) -> &mut Figure
578     where
579         F: FnOnce(&mut axis::Properties) -> &mut axis::Properties,
580     {
581         if self.axes.contains_key(axis) {
582             configure(self.axes.get_mut(axis).unwrap());
583         } else {
584             let mut properties = Default::default();
585             configure(&mut properties);
586             self.axes.insert(axis, properties);
587         }
588         self
589     }
590 }
591 
592 impl Configure<Key> for Figure {
593     type Properties = key::Properties;
594 
595     /// Configures the key (legend)
configure<F>(&mut self, _: Key, configure: F) -> &mut Figure where F: FnOnce(&mut key::Properties) -> &mut key::Properties,596     fn configure<F>(&mut self, _: Key, configure: F) -> &mut Figure
597     where
598         F: FnOnce(&mut key::Properties) -> &mut key::Properties,
599     {
600         if self.key.is_some() {
601             configure(self.key.as_mut().unwrap());
602         } else {
603             let mut key = Default::default();
604             configure(&mut key);
605             self.key = Some(key);
606         }
607         self
608     }
609 }
610 
611 impl Set<BoxWidth> for Figure {
612     /// Changes the box width of all the box related plots (bars, candlesticks, etc)
613     ///
614     /// **Note** The default value is 0
615     ///
616     /// # Panics
617     ///
618     /// Panics if `width` is a negative value
set(&mut self, width: BoxWidth) -> &mut Figure619     fn set(&mut self, width: BoxWidth) -> &mut Figure {
620         let width = width.0;
621 
622         assert!(width >= 0.);
623 
624         self.box_width = Some(width);
625         self
626     }
627 }
628 
629 impl Set<Font> for Figure {
630     /// Changes the font
set(&mut self, font: Font) -> &mut Figure631     fn set(&mut self, font: Font) -> &mut Figure {
632         self.font = Some(font.0);
633         self
634     }
635 }
636 
637 impl Set<FontSize> for Figure {
638     /// Changes the size of the font
639     ///
640     /// # Panics
641     ///
642     /// Panics if `size` is a non-positive value
set(&mut self, size: FontSize) -> &mut Figure643     fn set(&mut self, size: FontSize) -> &mut Figure {
644         let size = size.0;
645 
646         assert!(size >= 0.);
647 
648         self.font_size = Some(size);
649         self
650     }
651 }
652 
653 impl Set<Output> for Figure {
654     /// Changes the output file
655     ///
656     /// **Note** The default output file is `output.plot`
set(&mut self, output: Output) -> &mut Figure657     fn set(&mut self, output: Output) -> &mut Figure {
658         self.output = output.0;
659         self
660     }
661 }
662 
663 impl Set<Size> for Figure {
664     /// Changes the figure size
set(&mut self, size: Size) -> &mut Figure665     fn set(&mut self, size: Size) -> &mut Figure {
666         self.size = Some((size.0, size.1));
667         self
668     }
669 }
670 
671 impl Set<Terminal> for Figure {
672     /// Changes the output terminal
673     ///
674     /// **Note** By default, the terminal is set to `Svg`
set(&mut self, terminal: Terminal) -> &mut Figure675     fn set(&mut self, terminal: Terminal) -> &mut Figure {
676         self.terminal = terminal;
677         self
678     }
679 }
680 
681 impl Set<Title> for Figure {
682     /// Sets the title
set(&mut self, title: Title) -> &mut Figure683     fn set(&mut self, title: Title) -> &mut Figure {
684         self.title = Some(title.0);
685         self
686     }
687 }
688 
689 impl Default for Figure {
default() -> Self690     fn default() -> Self {
691         Self::new()
692     }
693 }
694 
695 /// Box width for box-related plots: bars, candlesticks, etc
696 #[derive(Clone, Copy)]
697 pub struct BoxWidth(pub f64);
698 
699 /// A font name
700 pub struct Font(Cow<'static, str>);
701 
702 /// The size of a font
703 #[derive(Clone, Copy)]
704 pub struct FontSize(pub f64);
705 
706 /// The key or legend
707 #[derive(Clone, Copy)]
708 pub struct Key;
709 
710 /// Plot label
711 pub struct Label(Cow<'static, str>);
712 
713 /// Width of the lines
714 #[derive(Clone, Copy)]
715 pub struct LineWidth(pub f64);
716 
717 /// Fill color opacity
718 #[derive(Clone, Copy)]
719 pub struct Opacity(pub f64);
720 
721 /// Output file path
722 pub struct Output(Cow<'static, Path>);
723 
724 /// Size of the points
725 #[derive(Clone, Copy)]
726 pub struct PointSize(pub f64);
727 
728 /// Axis range
729 #[derive(Clone, Copy)]
730 pub enum Range {
731     /// Autoscale the axis
732     Auto,
733     /// Set the limits of the axis
734     Limits(f64, f64),
735 }
736 
737 /// Figure size
738 #[derive(Clone, Copy)]
739 pub struct Size(pub usize, pub usize);
740 
741 /// Labels attached to the tics of an axis
742 pub struct TicLabels<P, L> {
743     /// Labels to attach to the tics
744     pub labels: L,
745     /// Position of the tics on the axis
746     pub positions: P,
747 }
748 
749 /// Figure title
750 pub struct Title(Cow<'static, str>);
751 
752 /// A pair of axes that define a coordinate system
753 #[allow(missing_docs)]
754 #[derive(Clone, Copy)]
755 pub enum Axes {
756     BottomXLeftY,
757     BottomXRightY,
758     TopXLeftY,
759     TopXRightY,
760 }
761 
762 /// A coordinate axis
763 #[derive(Clone, Copy)]
764 pub enum Axis {
765     /// X axis on the bottom side of the figure
766     BottomX,
767     /// Y axis on the left side of the figure
768     LeftY,
769     /// Y axis on the right side of the figure
770     RightY,
771     /// X axis on the top side of the figure
772     TopX,
773 }
774 
775 impl Axis {
next(self) -> Option<Axis>776     fn next(self) -> Option<Axis> {
777         use crate::Axis::*;
778 
779         match self {
780             BottomX => Some(LeftY),
781             LeftY => Some(RightY),
782             RightY => Some(TopX),
783             TopX => None,
784         }
785     }
786 }
787 
788 /// Color
789 #[allow(missing_docs)]
790 #[derive(Clone, Copy)]
791 pub enum Color {
792     Black,
793     Blue,
794     Cyan,
795     DarkViolet,
796     ForestGreen,
797     Gold,
798     Gray,
799     Green,
800     Magenta,
801     Red,
802     /// Custom RGB color
803     Rgb(u8, u8, u8),
804     White,
805     Yellow,
806 }
807 
808 /// Grid line
809 #[derive(Clone, Copy)]
810 pub enum Grid {
811     /// Major gridlines
812     Major,
813     /// Minor gridlines
814     Minor,
815 }
816 
817 impl Grid {
next(self) -> Option<Grid>818     fn next(self) -> Option<Grid> {
819         use crate::Grid::*;
820 
821         match self {
822             Major => Some(Minor),
823             Minor => None,
824         }
825     }
826 }
827 
828 /// Line type
829 #[allow(missing_docs)]
830 #[derive(Clone, Copy)]
831 pub enum LineType {
832     Dash,
833     Dot,
834     DotDash,
835     DotDotDash,
836     /// Line made of minimally sized dots
837     SmallDot,
838     Solid,
839 }
840 
841 /// Point type
842 #[allow(missing_docs)]
843 #[derive(Clone, Copy)]
844 pub enum PointType {
845     Circle,
846     FilledCircle,
847     FilledSquare,
848     FilledTriangle,
849     Plus,
850     Square,
851     Star,
852     Triangle,
853     X,
854 }
855 
856 /// Axis scale
857 #[allow(missing_docs)]
858 #[derive(Clone, Copy)]
859 pub enum Scale {
860     Linear,
861     Logarithmic,
862 }
863 
864 /// Axis scale factor
865 #[allow(missing_docs)]
866 #[derive(Clone, Copy)]
867 pub struct ScaleFactor(pub f64);
868 
869 /// Output terminal
870 #[allow(missing_docs)]
871 #[derive(Clone, Copy)]
872 pub enum Terminal {
873     Svg,
874 }
875 
876 /// Not public version of `std::default::Default`, used to not leak default constructors into the
877 /// public API
878 trait Default {
879     /// Creates `Properties` with default configuration
default() -> Self880     fn default() -> Self;
881 }
882 
883 /// Enums that can produce gnuplot code
884 trait Display<S> {
885     /// Translates the enum in gnuplot code
display(&self) -> S886     fn display(&self) -> S;
887 }
888 
889 /// Curve variant of Default
890 trait CurveDefault<S> {
891     /// Creates `curve::Properties` with default configuration
default(s: S) -> Self892     fn default(s: S) -> Self;
893 }
894 
895 /// Error bar variant of Default
896 trait ErrorBarDefault<S> {
897     /// Creates `errorbar::Properties` with default configuration
default(s: S) -> Self898     fn default(s: S) -> Self;
899 }
900 
901 /// Structs that can produce gnuplot code
902 trait Script {
903     /// Translates some configuration struct into gnuplot code
script(&self) -> String904     fn script(&self) -> String;
905 }
906 
907 #[derive(Clone)]
908 struct Plot {
909     data: Matrix,
910     script: String,
911 }
912 
913 impl Plot {
new<S>(data: Matrix, script: &S) -> Plot where S: Script,914     fn new<S>(data: Matrix, script: &S) -> Plot
915     where
916         S: Script,
917     {
918         Plot {
919             data,
920             script: script.script(),
921         }
922     }
923 
data(&self) -> &Matrix924     fn data(&self) -> &Matrix {
925         &self.data
926     }
927 
script(&self) -> &str928     fn script(&self) -> &str {
929         &self.script
930     }
931 }
932 
933 /// Possible errors when parsing gnuplot's version string
934 #[derive(Debug)]
935 pub enum VersionError {
936     /// The `gnuplot` command couldn't be executed
937     Exec(io::Error),
938     /// The `gnuplot` command returned an error message
939     Error(String),
940     /// The `gnuplot` command returned invalid utf-8
941     OutputError,
942     /// The `gnuplot` command returned an unparseable string
943     ParseError(String),
944 }
945 impl fmt::Display for VersionError {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result946     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
947         match self {
948             VersionError::Exec(err) => write!(f, "`gnuplot --version` failed: {}", err),
949             VersionError::Error(msg) => {
950                 write!(f, "`gnuplot --version` failed with error message:\n{}", msg)
951             }
952             VersionError::OutputError => write!(f, "`gnuplot --version` returned invalid utf-8"),
953             VersionError::ParseError(msg) => write!(
954                 f,
955                 "`gnuplot --version` returned an unparseable version string: {}",
956                 msg
957             ),
958         }
959     }
960 }
961 impl ::std::error::Error for VersionError {
description(&self) -> &str962     fn description(&self) -> &str {
963         match self {
964             VersionError::Exec(_) => "Execution Error",
965             VersionError::Error(_) => "Other Error",
966             VersionError::OutputError => "Output Error",
967             VersionError::ParseError(_) => "Parse Error",
968         }
969     }
970 
cause(&self) -> Option<&dyn ::std::error::Error>971     fn cause(&self) -> Option<&dyn ::std::error::Error> {
972         match self {
973             VersionError::Exec(err) => Some(err),
974             _ => None,
975         }
976     }
977 }
978 
979 /// Structure representing a gnuplot version number.
980 pub struct Version {
981     /// The major version number
982     pub major: usize,
983     /// The minor version number
984     pub minor: usize,
985     /// The patch level
986     pub patch: String,
987 }
988 
989 /// Returns `gnuplot` version
version() -> Result<Version, VersionError>990 pub fn version() -> Result<Version, VersionError> {
991     let command_output = Command::new("gnuplot")
992         .arg("--version")
993         .output()
994         .map_err(VersionError::Exec)?;
995     if !command_output.status.success() {
996         let error =
997             String::from_utf8(command_output.stderr).map_err(|_| VersionError::OutputError)?;
998         return Err(VersionError::Error(error));
999     }
1000 
1001     let output = String::from_utf8(command_output.stdout).map_err(|_| VersionError::OutputError)?;
1002 
1003     parse_version(&output).map_err(|_| VersionError::ParseError(output.clone()))
1004 }
1005 
parse_version(version_str: &str) -> Result<Version, Option<ParseIntError>>1006 fn parse_version(version_str: &str) -> Result<Version, Option<ParseIntError>> {
1007     let mut words = version_str.split_whitespace().skip(1);
1008     let mut version = words.next().ok_or(None)?.split('.');
1009     let major = version.next().ok_or(None)?.parse()?;
1010     let minor = version.next().ok_or(None)?.parse()?;
1011     let patchlevel = words.nth(1).ok_or(None)?.to_owned();
1012 
1013     Ok(Version {
1014         major,
1015         minor,
1016         patch: patchlevel,
1017     })
1018 }
1019 
scale_factor(map: &map::axis::Map<axis::Properties>, axes: Axes) -> (f64, f64)1020 fn scale_factor(map: &map::axis::Map<axis::Properties>, axes: Axes) -> (f64, f64) {
1021     use crate::Axes::*;
1022     use crate::Axis::*;
1023 
1024     match axes {
1025         BottomXLeftY => (
1026             map.get(BottomX).map_or(1., ScaleFactorTrait::scale_factor),
1027             map.get(LeftY).map_or(1., ScaleFactorTrait::scale_factor),
1028         ),
1029         BottomXRightY => (
1030             map.get(BottomX).map_or(1., ScaleFactorTrait::scale_factor),
1031             map.get(RightY).map_or(1., ScaleFactorTrait::scale_factor),
1032         ),
1033         TopXLeftY => (
1034             map.get(TopX).map_or(1., ScaleFactorTrait::scale_factor),
1035             map.get(LeftY).map_or(1., ScaleFactorTrait::scale_factor),
1036         ),
1037         TopXRightY => (
1038             map.get(TopX).map_or(1., ScaleFactorTrait::scale_factor),
1039             map.get(RightY).map_or(1., ScaleFactorTrait::scale_factor),
1040         ),
1041     }
1042 }
1043 
1044 // XXX :-1: to intra-crate privacy rules
1045 /// Private
1046 trait ScaleFactorTrait {
1047     /// Private
scale_factor(&self) -> f641048     fn scale_factor(&self) -> f64;
1049 }
1050 
1051 #[cfg(test)]
1052 mod test {
1053     #[test]
version()1054     fn version() {
1055         if let Ok(version) = super::version() {
1056             assert!(version.major >= 4);
1057         } else {
1058             println!("Gnuplot not installed.");
1059         }
1060     }
1061 
1062     #[test]
test_parse_version_on_valid_string()1063     fn test_parse_version_on_valid_string() {
1064         let string = "gnuplot 5.0 patchlevel 7";
1065         let version = super::parse_version(&string).unwrap();
1066         assert_eq!(5, version.major);
1067         assert_eq!(0, version.minor);
1068         assert_eq!("7", &version.patch);
1069     }
1070 
1071     #[test]
test_parse_gentoo_version()1072     fn test_parse_gentoo_version() {
1073         let string = "gnuplot 5.2 patchlevel 5a (Gentoo revision r0)";
1074         let version = super::parse_version(&string).unwrap();
1075         assert_eq!(5, version.major);
1076         assert_eq!(2, version.minor);
1077         assert_eq!("5a", &version.patch);
1078     }
1079 
1080     #[test]
test_parse_version_returns_error_on_invalid_strings()1081     fn test_parse_version_returns_error_on_invalid_strings() {
1082         let strings = [
1083             "",
1084             "foobar",
1085             "gnuplot 50 patchlevel 7",
1086             "gnuplot 5.0 patchlevel",
1087             "gnuplot foo.bar patchlevel 7",
1088         ];
1089         for string in &strings {
1090             assert!(super::parse_version(string).is_err());
1091         }
1092     }
1093 }
1094