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