| //! [Criterion]'s plotting library. |
| //! |
| //! [Criterion]: https://github.com/bheisler/criterion.rs |
| //! |
| //! **WARNING** This library is criterion's implementation detail and there no plans to stabilize |
| //! it. In other words, the API may break at any time without notice. |
| //! |
| //! # Examples |
| //! |
| //! - Simple "curves" (based on [`simple.dem`](http://gnuplot.sourceforge.net/demo/simple.html)) |
| //! |
| //! ![Plot](curve.svg) |
| //! |
| //! ``` |
| //! # use std::fs; |
| //! # use std::path::Path; |
| //! use itertools_num::linspace; |
| //! use criterion_plot::prelude::*; |
| //! |
| //! # if let Err(_) = criterion_plot::version() { |
| //! # return; |
| //! # } |
| //! let ref xs = linspace::<f64>(-10., 10., 51).collect::<Vec<_>>(); |
| //! |
| //! # fs::create_dir_all(Path::new("target/doc/criterion_plot")).unwrap(); |
| //! # assert_eq!(Some(String::new()), |
| //! Figure::new() |
| //! # .set(Font("Helvetica")) |
| //! # .set(FontSize(12.)) |
| //! # .set(Output(Path::new("target/doc/criterion_plot/curve.svg"))) |
| //! # .set(Size(1280, 720)) |
| //! .configure(Key, |k| { |
| //! k.set(Boxed::Yes) |
| //! .set(Position::Inside(Vertical::Top, Horizontal::Left)) |
| //! }) |
| //! .plot(LinesPoints { |
| //! x: xs, |
| //! y: xs.iter().map(|x| x.sin()), |
| //! }, |
| //! |lp| { |
| //! lp.set(Color::DarkViolet) |
| //! .set(Label("sin(x)")) |
| //! .set(LineType::Dash) |
| //! .set(PointSize(1.5)) |
| //! .set(PointType::Circle) |
| //! }) |
| //! .plot(Steps { |
| //! x: xs, |
| //! y: xs.iter().map(|x| x.atan()), |
| //! }, |
| //! |s| { |
| //! s.set(Color::Rgb(0, 158, 115)) |
| //! .set(Label("atan(x)")) |
| //! .set(LineWidth(2.)) |
| //! }) |
| //! .plot(Impulses { |
| //! x: xs, |
| //! y: xs.iter().map(|x| x.atan().cos()), |
| //! }, |
| //! |i| { |
| //! i.set(Color::Rgb(86, 180, 233)) |
| //! .set(Label("cos(atan(x))")) |
| //! }) |
| //! .draw() // (rest of the chain has been omitted) |
| //! # .ok() |
| //! # .and_then(|gnuplot| { |
| //! # gnuplot.wait_with_output().ok().and_then(|p| String::from_utf8(p.stderr).ok()) |
| //! # })); |
| //! ``` |
| //! |
| //! - error bars (based on |
| //! [Julia plotting tutorial](https://plot.ly/julia/error-bars/#Colored-and-Styled-Error-Bars)) |
| //! |
| //! ![Plot](error_bar.svg) |
| //! |
| //! ``` |
| //! # use std::fs; |
| //! # use std::path::Path; |
| //! use std::f64::consts::PI; |
| //! |
| //! use itertools_num::linspace; |
| //! use rand::{Rng, XorShiftRng}; |
| //! use criterion_plot::prelude::*; |
| //! |
| //! fn sinc(mut x: f64) -> f64 { |
| //! if x == 0. { |
| //! 1. |
| //! } else { |
| //! x *= PI; |
| //! x.sin() / x |
| //! } |
| //! } |
| //! |
| //! # if let Err(_) = criterion_plot::version() { |
| //! # return; |
| //! # } |
| //! let ref xs_ = linspace::<f64>(-4., 4., 101).collect::<Vec<_>>(); |
| //! |
| //! // Fake some data |
| //! let ref mut rng: XorShiftRng = rand::thread_rng().gen(); |
| //! let xs = linspace::<f64>(-4., 4., 13).skip(1).take(11); |
| //! let ys = xs.map(|x| sinc(x) + 0.05 * rng.gen::<f64>() - 0.025).collect::<Vec<_>>(); |
| //! let y_low = ys.iter().map(|&y| y - 0.025 - 0.075 * rng.gen::<f64>()).collect::<Vec<_>>(); |
| //! let y_high = ys.iter().map(|&y| y + 0.025 + 0.075 * rng.gen::<f64>()).collect::<Vec<_>>(); |
| //! let xs = linspace::<f64>(-4., 4., 13).skip(1).take(11); |
| //! let xs = xs.map(|x| x + 0.2 * rng.gen::<f64>() - 0.1); |
| //! |
| //! # fs::create_dir_all(Path::new("target/doc/criterion_plot")).unwrap(); |
| //! # assert_eq!(Some(String::new()), |
| //! Figure::new() |
| //! # .set(Font("Helvetica")) |
| //! # .set(FontSize(12.)) |
| //! # .set(Output(Path::new("target/doc/criterion_plot/error_bar.svg"))) |
| //! # .set(Size(1280, 720)) |
| //! .configure(Axis::BottomX, |a| { |
| //! a.set(TicLabels { |
| //! labels: &["-π", "0", "π"], |
| //! positions: &[-PI, 0., PI], |
| //! }) |
| //! }) |
| //! .configure(Key, |
| //! |k| k.set(Position::Outside(Vertical::Top, Horizontal::Right))) |
| //! .plot(Lines { |
| //! x: xs_, |
| //! y: xs_.iter().cloned().map(sinc), |
| //! }, |
| //! |l| { |
| //! l.set(Color::Rgb(0, 158, 115)) |
| //! .set(Label("sinc(x)")) |
| //! .set(LineWidth(2.)) |
| //! }) |
| //! .plot(YErrorBars { |
| //! x: xs, |
| //! y: &ys, |
| //! y_low: &y_low, |
| //! y_high: &y_high, |
| //! }, |
| //! |eb| { |
| //! eb.set(Color::DarkViolet) |
| //! .set(LineWidth(2.)) |
| //! .set(PointType::FilledCircle) |
| //! .set(Label("measured")) |
| //! }) |
| //! .draw() // (rest of the chain has been omitted) |
| //! # .ok() |
| //! # .and_then(|gnuplot| { |
| //! # gnuplot.wait_with_output().ok().and_then(|p| String::from_utf8(p.stderr).ok()) |
| //! # })); |
| //! ``` |
| //! |
| //! - Candlesticks (based on |
| //! [`candlesticks.dem`](http://gnuplot.sourceforge.net/demo/candlesticks.html)) |
| //! |
| //! ![Plot](candlesticks.svg) |
| //! |
| //! ``` |
| //! # use std::fs; |
| //! # use std::path::Path; |
| //! use criterion_plot::prelude::*; |
| //! use rand::Rng; |
| //! |
| //! # if let Err(_) = criterion_plot::version() { |
| //! # return; |
| //! # } |
| //! let xs = 1..11; |
| //! |
| //! // Fake some data |
| //! let mut rng = rand::thread_rng(); |
| //! let bh = xs.clone().map(|_| 5f64 + 2.5 * rng.gen::<f64>()).collect::<Vec<_>>(); |
| //! let bm = xs.clone().map(|_| 2.5f64 + 2.5 * rng.gen::<f64>()).collect::<Vec<_>>(); |
| //! let wh = bh.iter().map(|&y| y + (10. - y) * rng.gen::<f64>()).collect::<Vec<_>>(); |
| //! let wm = bm.iter().map(|&y| y * rng.gen::<f64>()).collect::<Vec<_>>(); |
| //! let m = bm.iter().zip(bh.iter()).map(|(&l, &h)| (h - l) * rng.gen::<f64>() + l) |
| //! .collect::<Vec<_>>(); |
| //! |
| //! # fs::create_dir_all(Path::new("target/doc/criterion_plot")).unwrap(); |
| //! # assert_eq!(Some(String::new()), |
| //! Figure::new() |
| //! # .set(Font("Helvetica")) |
| //! # .set(FontSize(12.)) |
| //! # .set(Output(Path::new("target/doc/criterion_plot/candlesticks.svg"))) |
| //! # .set(Size(1280, 720)) |
| //! .set(BoxWidth(0.2)) |
| //! .configure(Axis::BottomX, |a| a.set(Range::Limits(0., 11.))) |
| //! .plot(Candlesticks { |
| //! x: xs.clone(), |
| //! whisker_min: &wm, |
| //! box_min: &bm, |
| //! box_high: &bh, |
| //! whisker_high: &wh, |
| //! }, |
| //! |cs| { |
| //! cs.set(Color::Rgb(86, 180, 233)) |
| //! .set(Label("Quartiles")) |
| //! .set(LineWidth(2.)) |
| //! }) |
| //! // trick to plot the median |
| //! .plot(Candlesticks { |
| //! x: xs, |
| //! whisker_min: &m, |
| //! box_min: &m, |
| //! box_high: &m, |
| //! whisker_high: &m, |
| //! }, |
| //! |cs| { |
| //! cs.set(Color::Black) |
| //! .set(LineWidth(2.)) |
| //! }) |
| //! .draw() // (rest of the chain has been omitted) |
| //! # .ok() |
| //! # .and_then(|gnuplot| { |
| //! # gnuplot.wait_with_output().ok().and_then(|p| String::from_utf8(p.stderr).ok()) |
| //! # })); |
| //! ``` |
| //! |
| //! - Multiaxis (based on [`multiaxis.dem`](http://gnuplot.sourceforge.net/demo/multiaxis.html)) |
| //! |
| //! ![Plot](multiaxis.svg) |
| //! |
| //! ``` |
| //! # use std::fs; |
| //! # use std::path::Path; |
| //! use std::f64::consts::PI; |
| //! |
| //! use itertools_num::linspace; |
| //! use num_complex::Complex; |
| //! use criterion_plot::prelude::*; |
| //! |
| //! fn tf(x: f64) -> Complex<f64> { |
| //! Complex::new(0., x) / Complex::new(10., x) / Complex::new(1., x / 10_000.) |
| //! } |
| //! |
| //! # if let Err(_) = criterion_plot::version() { |
| //! # return; |
| //! # } |
| //! let (start, end): (f64, f64) = (1.1, 90_000.); |
| //! let ref xs = linspace(start.ln(), end.ln(), 101).map(|x| x.exp()).collect::<Vec<_>>(); |
| //! let phase = xs.iter().map(|&x| tf(x).arg() * 180. / PI); |
| //! let magnitude = xs.iter().map(|&x| tf(x).norm()); |
| //! |
| //! # fs::create_dir_all(Path::new("target/doc/criterion_plot")).unwrap(); |
| //! # assert_eq!(Some(String::new()), |
| //! Figure::new(). |
| //! # set(Font("Helvetica")). |
| //! # set(FontSize(12.)). |
| //! # set(Output(Path::new("target/doc/criterion_plot/multiaxis.svg"))). |
| //! # set(Size(1280, 720)). |
| //! set(Title("Frequency response")). |
| //! configure(Axis::BottomX, |a| a. |
| //! configure(Grid::Major, |g| g. |
| //! show()). |
| //! set(Label("Angular frequency (rad/s)")). |
| //! set(Range::Limits(start, end)). |
| //! set(Scale::Logarithmic)). |
| //! configure(Axis::LeftY, |a| a. |
| //! set(Label("Gain")). |
| //! set(Scale::Logarithmic)). |
| //! configure(Axis::RightY, |a| a. |
| //! configure(Grid::Major, |g| g. |
| //! show()). |
| //! set(Label("Phase shift (°)"))). |
| //! configure(Key, |k| k. |
| //! set(Position::Inside(Vertical::Top, Horizontal::Center)). |
| //! set(Title(" "))). |
| //! plot(Lines { |
| //! x: xs, |
| //! y: magnitude, |
| //! }, |l| l. |
| //! set(Color::DarkViolet). |
| //! set(Label("Magnitude")). |
| //! set(LineWidth(2.))). |
| //! plot(Lines { |
| //! x: xs, |
| //! y: phase, |
| //! }, |l| l. |
| //! set(Axes::BottomXRightY). |
| //! set(Color::Rgb(0, 158, 115)). |
| //! set(Label("Phase")). |
| //! set(LineWidth(2.))). |
| //! draw(). // (rest of the chain has been omitted) |
| //! # ok().and_then(|gnuplot| { |
| //! # gnuplot.wait_with_output().ok().and_then(|p| { |
| //! # String::from_utf8(p.stderr).ok() |
| //! # }) |
| //! # })); |
| //! ``` |
| //! - Filled curves (based on |
| //! [`transparent.dem`](http://gnuplot.sourceforge.net/demo/transparent.html)) |
| //! |
| //! ![Plot](filled_curve.svg) |
| //! |
| //! ``` |
| //! # use std::fs; |
| //! # use std::path::Path; |
| //! use std::f64::consts::PI; |
| //! use std::iter; |
| //! |
| //! use itertools_num::linspace; |
| //! use criterion_plot::prelude::*; |
| //! |
| //! # if let Err(_) = criterion_plot::version() { |
| //! # return; |
| //! # } |
| //! let (start, end) = (-5., 5.); |
| //! let ref xs = linspace(start, end, 101).collect::<Vec<_>>(); |
| //! let zeros = iter::repeat(0); |
| //! |
| //! fn gaussian(x: f64, mu: f64, sigma: f64) -> f64 { |
| //! 1. / (((x - mu).powi(2) / 2. / sigma.powi(2)).exp() * sigma * (2. * PI).sqrt()) |
| //! } |
| //! |
| //! # fs::create_dir_all(Path::new("target/doc/criterion_plot")).unwrap(); |
| //! # assert_eq!(Some(String::new()), |
| //! Figure::new() |
| //! # .set(Font("Helvetica")) |
| //! # .set(FontSize(12.)) |
| //! # .set(Output(Path::new("target/doc/criterion_plot/filled_curve.svg"))) |
| //! # .set(Size(1280, 720)) |
| //! .set(Title("Transparent filled curve")) |
| //! .configure(Axis::BottomX, |a| a.set(Range::Limits(start, end))) |
| //! .configure(Axis::LeftY, |a| a.set(Range::Limits(0., 1.))) |
| //! .configure(Key, |k| { |
| //! k.set(Justification::Left) |
| //! .set(Order::SampleText) |
| //! .set(Position::Inside(Vertical::Top, Horizontal::Left)) |
| //! .set(Title("Gaussian Distribution")) |
| //! }) |
| //! .plot(FilledCurve { |
| //! x: xs, |
| //! y1: xs.iter().map(|&x| gaussian(x, 0.5, 0.5)), |
| //! y2: zeros.clone(), |
| //! }, |
| //! |fc| { |
| //! fc.set(Color::ForestGreen) |
| //! .set(Label("μ = 0.5 σ = 0.5")) |
| //! }) |
| //! .plot(FilledCurve { |
| //! x: xs, |
| //! y1: xs.iter().map(|&x| gaussian(x, 2.0, 1.0)), |
| //! y2: zeros.clone(), |
| //! }, |
| //! |fc| { |
| //! fc.set(Color::Gold) |
| //! .set(Label("μ = 2.0 σ = 1.0")) |
| //! .set(Opacity(0.5)) |
| //! }) |
| //! .plot(FilledCurve { |
| //! x: xs, |
| //! y1: xs.iter().map(|&x| gaussian(x, -1.0, 2.0)), |
| //! y2: zeros, |
| //! }, |
| //! |fc| { |
| //! fc.set(Color::Red) |
| //! .set(Label("μ = -1.0 σ = 2.0")) |
| //! .set(Opacity(0.5)) |
| //! }) |
| //! .draw() |
| //! .ok() |
| //! .and_then(|gnuplot| { |
| //! gnuplot.wait_with_output().ok().and_then(|p| String::from_utf8(p.stderr).ok()) |
| //! })); |
| //! ``` |
| |
| #![deny(missing_docs)] |
| #![deny(warnings)] |
| #![deny(bare_trait_objects)] |
| // This lint has lots of false positives ATM, see |
| // https://github.com/Manishearth/rust-clippy/issues/761 |
| #![cfg_attr(feature = "cargo-clippy", allow(clippy::new_without_default))] |
| // False positives with images |
| #![cfg_attr(feature = "cargo-clippy", allow(clippy::doc_markdown))] |
| #![cfg_attr(feature = "cargo-clippy", allow(clippy::many_single_char_names))] |
| |
| extern crate cast; |
| #[macro_use] |
| extern crate itertools; |
| |
| use std::borrow::Cow; |
| use std::fmt; |
| use std::fs::File; |
| use std::io; |
| use std::num::ParseIntError; |
| use std::path::Path; |
| use std::process::{Child, Command}; |
| use std::str; |
| |
| use crate::data::Matrix; |
| use crate::traits::{Configure, Set}; |
| |
| mod data; |
| mod display; |
| mod map; |
| |
| pub mod axis; |
| pub mod candlestick; |
| pub mod curve; |
| pub mod errorbar; |
| pub mod filledcurve; |
| pub mod grid; |
| pub mod key; |
| pub mod prelude; |
| pub mod proxy; |
| pub mod traits; |
| |
| /// Plot container |
| #[derive(Clone)] |
| pub struct Figure { |
| alpha: Option<f64>, |
| axes: map::axis::Map<axis::Properties>, |
| box_width: Option<f64>, |
| font: Option<Cow<'static, str>>, |
| font_size: Option<f64>, |
| key: Option<key::Properties>, |
| output: Cow<'static, Path>, |
| plots: Vec<Plot>, |
| size: Option<(usize, usize)>, |
| terminal: Terminal, |
| tics: map::axis::Map<String>, |
| title: Option<Cow<'static, str>>, |
| } |
| |
| impl Figure { |
| /// Creates an empty figure |
| pub fn new() -> Figure { |
| Figure { |
| alpha: None, |
| axes: map::axis::Map::new(), |
| box_width: None, |
| font: None, |
| font_size: None, |
| key: None, |
| output: Cow::Borrowed(Path::new("output.plot")), |
| plots: Vec::new(), |
| size: None, |
| terminal: Terminal::Svg, |
| tics: map::axis::Map::new(), |
| title: None, |
| } |
| } |
| |
| fn script(&self) -> Vec<u8> { |
| let mut s = String::new(); |
| |
| s.push_str(&format!( |
| "set output '{}'\n", |
| self.output.display().to_string().replace("'", "''") |
| )); |
| |
| if let Some(width) = self.box_width { |
| s.push_str(&format!("set boxwidth {}\n", width)) |
| } |
| |
| if let Some(ref title) = self.title { |
| s.push_str(&format!("set title '{}'\n", title)) |
| } |
| |
| for axis in self.axes.iter() { |
| s.push_str(&axis.script()); |
| } |
| |
| for (_, script) in self.tics.iter() { |
| s.push_str(script); |
| } |
| |
| if let Some(ref key) = self.key { |
| s.push_str(&key.script()) |
| } |
| |
| if let Some(alpha) = self.alpha { |
| s.push_str(&format!("set style fill transparent solid {}\n", alpha)) |
| } |
| |
| s.push_str(&format!("set terminal {} dashed", self.terminal.display())); |
| |
| if let Some((width, height)) = self.size { |
| s.push_str(&format!(" size {}, {}", width, height)) |
| } |
| |
| if let Some(ref name) = self.font { |
| if let Some(size) = self.font_size { |
| s.push_str(&format!(" font '{},{}'", name, size)) |
| } else { |
| s.push_str(&format!(" font '{}'", name)) |
| } |
| } |
| |
| // TODO This removes the crossbars from the ends of error bars, but should be configurable |
| s.push_str("\nunset bars\n"); |
| |
| let mut is_first_plot = true; |
| for plot in &self.plots { |
| let data = plot.data(); |
| |
| if data.bytes().is_empty() { |
| continue; |
| } |
| |
| if is_first_plot { |
| s.push_str("plot "); |
| is_first_plot = false; |
| } else { |
| s.push_str(", "); |
| } |
| |
| s.push_str(&format!( |
| "'-' binary endian=little record={} format='%float64' using ", |
| data.nrows() |
| )); |
| |
| let mut is_first_col = true; |
| for col in 0..data.ncols() { |
| if is_first_col { |
| is_first_col = false; |
| } else { |
| s.push(':'); |
| } |
| s.push_str(&(col + 1).to_string()); |
| } |
| s.push(' '); |
| |
| s.push_str(plot.script()); |
| } |
| |
| let mut buffer = s.into_bytes(); |
| let mut is_first = true; |
| for plot in &self.plots { |
| if is_first { |
| is_first = false; |
| buffer.push(b'\n'); |
| } |
| buffer.extend_from_slice(plot.data().bytes()); |
| } |
| |
| buffer |
| } |
| |
| /// Spawns a drawing child process |
| /// |
| /// NOTE: stderr, stdin, and stdout are piped |
| pub fn draw(&mut self) -> io::Result<Child> { |
| use std::process::Stdio; |
| |
| let mut gnuplot = Command::new("gnuplot") |
| .stderr(Stdio::piped()) |
| .stdin(Stdio::piped()) |
| .stdout(Stdio::piped()) |
| .spawn()?; |
| self.dump(gnuplot.stdin.as_mut().unwrap())?; |
| Ok(gnuplot) |
| } |
| |
| /// Dumps the script required to produce the figure into `sink` |
| pub fn dump<W>(&mut self, sink: &mut W) -> io::Result<&mut Figure> |
| where |
| W: io::Write, |
| { |
| sink.write_all(&self.script())?; |
| Ok(self) |
| } |
| |
| /// Saves the script required to produce the figure to `path` |
| pub fn save(&self, path: &Path) -> io::Result<&Figure> { |
| use std::io::Write; |
| |
| File::create(path)?.write_all(&self.script())?; |
| Ok(self) |
| } |
| } |
| |
| impl Configure<Axis> for Figure { |
| type Properties = axis::Properties; |
| |
| /// Configures an axis |
| fn configure<F>(&mut self, axis: Axis, configure: F) -> &mut Figure |
| where |
| F: FnOnce(&mut axis::Properties) -> &mut axis::Properties, |
| { |
| if self.axes.contains_key(axis) { |
| configure(self.axes.get_mut(axis).unwrap()); |
| } else { |
| let mut properties = Default::default(); |
| configure(&mut properties); |
| self.axes.insert(axis, properties); |
| } |
| self |
| } |
| } |
| |
| impl Configure<Key> for Figure { |
| type Properties = key::Properties; |
| |
| /// Configures the key (legend) |
| fn configure<F>(&mut self, _: Key, configure: F) -> &mut Figure |
| where |
| F: FnOnce(&mut key::Properties) -> &mut key::Properties, |
| { |
| if self.key.is_some() { |
| configure(self.key.as_mut().unwrap()); |
| } else { |
| let mut key = Default::default(); |
| configure(&mut key); |
| self.key = Some(key); |
| } |
| self |
| } |
| } |
| |
| impl Set<BoxWidth> for Figure { |
| /// Changes the box width of all the box related plots (bars, candlesticks, etc) |
| /// |
| /// **Note** The default value is 0 |
| /// |
| /// # Panics |
| /// |
| /// Panics if `width` is a negative value |
| fn set(&mut self, width: BoxWidth) -> &mut Figure { |
| let width = width.0; |
| |
| assert!(width >= 0.); |
| |
| self.box_width = Some(width); |
| self |
| } |
| } |
| |
| impl Set<Font> for Figure { |
| /// Changes the font |
| fn set(&mut self, font: Font) -> &mut Figure { |
| self.font = Some(font.0); |
| self |
| } |
| } |
| |
| impl Set<FontSize> for Figure { |
| /// Changes the size of the font |
| /// |
| /// # Panics |
| /// |
| /// Panics if `size` is a non-positive value |
| fn set(&mut self, size: FontSize) -> &mut Figure { |
| let size = size.0; |
| |
| assert!(size >= 0.); |
| |
| self.font_size = Some(size); |
| self |
| } |
| } |
| |
| impl Set<Output> for Figure { |
| /// Changes the output file |
| /// |
| /// **Note** The default output file is `output.plot` |
| fn set(&mut self, output: Output) -> &mut Figure { |
| self.output = output.0; |
| self |
| } |
| } |
| |
| impl Set<Size> for Figure { |
| /// Changes the figure size |
| fn set(&mut self, size: Size) -> &mut Figure { |
| self.size = Some((size.0, size.1)); |
| self |
| } |
| } |
| |
| impl Set<Terminal> for Figure { |
| /// Changes the output terminal |
| /// |
| /// **Note** By default, the terminal is set to `Svg` |
| fn set(&mut self, terminal: Terminal) -> &mut Figure { |
| self.terminal = terminal; |
| self |
| } |
| } |
| |
| impl Set<Title> for Figure { |
| /// Sets the title |
| fn set(&mut self, title: Title) -> &mut Figure { |
| self.title = Some(title.0); |
| self |
| } |
| } |
| |
| impl Default for Figure { |
| fn default() -> Self { |
| Self::new() |
| } |
| } |
| |
| /// Box width for box-related plots: bars, candlesticks, etc |
| #[derive(Clone, Copy)] |
| pub struct BoxWidth(pub f64); |
| |
| /// A font name |
| pub struct Font(Cow<'static, str>); |
| |
| /// The size of a font |
| #[derive(Clone, Copy)] |
| pub struct FontSize(pub f64); |
| |
| /// The key or legend |
| #[derive(Clone, Copy)] |
| pub struct Key; |
| |
| /// Plot label |
| pub struct Label(Cow<'static, str>); |
| |
| /// Width of the lines |
| #[derive(Clone, Copy)] |
| pub struct LineWidth(pub f64); |
| |
| /// Fill color opacity |
| #[derive(Clone, Copy)] |
| pub struct Opacity(pub f64); |
| |
| /// Output file path |
| pub struct Output(Cow<'static, Path>); |
| |
| /// Size of the points |
| #[derive(Clone, Copy)] |
| pub struct PointSize(pub f64); |
| |
| /// Axis range |
| #[derive(Clone, Copy)] |
| pub enum Range { |
| /// Autoscale the axis |
| Auto, |
| /// Set the limits of the axis |
| Limits(f64, f64), |
| } |
| |
| /// Figure size |
| #[derive(Clone, Copy)] |
| pub struct Size(pub usize, pub usize); |
| |
| /// Labels attached to the tics of an axis |
| pub struct TicLabels<P, L> { |
| /// Labels to attach to the tics |
| pub labels: L, |
| /// Position of the tics on the axis |
| pub positions: P, |
| } |
| |
| /// Figure title |
| pub struct Title(Cow<'static, str>); |
| |
| /// A pair of axes that define a coordinate system |
| #[allow(missing_docs)] |
| #[derive(Clone, Copy)] |
| pub enum Axes { |
| BottomXLeftY, |
| BottomXRightY, |
| TopXLeftY, |
| TopXRightY, |
| } |
| |
| /// A coordinate axis |
| #[derive(Clone, Copy)] |
| pub enum Axis { |
| /// X axis on the bottom side of the figure |
| BottomX, |
| /// Y axis on the left side of the figure |
| LeftY, |
| /// Y axis on the right side of the figure |
| RightY, |
| /// X axis on the top side of the figure |
| TopX, |
| } |
| |
| impl Axis { |
| fn next(self) -> Option<Axis> { |
| use crate::Axis::*; |
| |
| match self { |
| BottomX => Some(LeftY), |
| LeftY => Some(RightY), |
| RightY => Some(TopX), |
| TopX => None, |
| } |
| } |
| } |
| |
| /// Color |
| #[allow(missing_docs)] |
| #[derive(Clone, Copy)] |
| pub enum Color { |
| Black, |
| Blue, |
| Cyan, |
| DarkViolet, |
| ForestGreen, |
| Gold, |
| Gray, |
| Green, |
| Magenta, |
| Red, |
| /// Custom RGB color |
| Rgb(u8, u8, u8), |
| White, |
| Yellow, |
| } |
| |
| /// Grid line |
| #[derive(Clone, Copy)] |
| pub enum Grid { |
| /// Major gridlines |
| Major, |
| /// Minor gridlines |
| Minor, |
| } |
| |
| impl Grid { |
| fn next(self) -> Option<Grid> { |
| use crate::Grid::*; |
| |
| match self { |
| Major => Some(Minor), |
| Minor => None, |
| } |
| } |
| } |
| |
| /// Line type |
| #[allow(missing_docs)] |
| #[derive(Clone, Copy)] |
| pub enum LineType { |
| Dash, |
| Dot, |
| DotDash, |
| DotDotDash, |
| /// Line made of minimally sized dots |
| SmallDot, |
| Solid, |
| } |
| |
| /// Point type |
| #[allow(missing_docs)] |
| #[derive(Clone, Copy)] |
| pub enum PointType { |
| Circle, |
| FilledCircle, |
| FilledSquare, |
| FilledTriangle, |
| Plus, |
| Square, |
| Star, |
| Triangle, |
| X, |
| } |
| |
| /// Axis scale |
| #[allow(missing_docs)] |
| #[derive(Clone, Copy)] |
| pub enum Scale { |
| Linear, |
| Logarithmic, |
| } |
| |
| /// Axis scale factor |
| #[allow(missing_docs)] |
| #[derive(Clone, Copy)] |
| pub struct ScaleFactor(pub f64); |
| |
| /// Output terminal |
| #[allow(missing_docs)] |
| #[derive(Clone, Copy)] |
| pub enum Terminal { |
| Svg, |
| } |
| |
| /// Not public version of `std::default::Default`, used to not leak default constructors into the |
| /// public API |
| trait Default { |
| /// Creates `Properties` with default configuration |
| fn default() -> Self; |
| } |
| |
| /// Enums that can produce gnuplot code |
| trait Display<S> { |
| /// Translates the enum in gnuplot code |
| fn display(&self) -> S; |
| } |
| |
| /// Curve variant of Default |
| trait CurveDefault<S> { |
| /// Creates `curve::Properties` with default configuration |
| fn default(s: S) -> Self; |
| } |
| |
| /// Error bar variant of Default |
| trait ErrorBarDefault<S> { |
| /// Creates `errorbar::Properties` with default configuration |
| fn default(s: S) -> Self; |
| } |
| |
| /// Structs that can produce gnuplot code |
| trait Script { |
| /// Translates some configuration struct into gnuplot code |
| fn script(&self) -> String; |
| } |
| |
| #[derive(Clone)] |
| struct Plot { |
| data: Matrix, |
| script: String, |
| } |
| |
| impl Plot { |
| fn new<S>(data: Matrix, script: &S) -> Plot |
| where |
| S: Script, |
| { |
| Plot { |
| data, |
| script: script.script(), |
| } |
| } |
| |
| fn data(&self) -> &Matrix { |
| &self.data |
| } |
| |
| fn script(&self) -> &str { |
| &self.script |
| } |
| } |
| |
| /// Possible errors when parsing gnuplot's version string |
| #[derive(Debug)] |
| pub enum VersionError { |
| /// The `gnuplot` command couldn't be executed |
| Exec(io::Error), |
| /// The `gnuplot` command returned an error message |
| Error(String), |
| /// The `gnuplot` command returned invalid utf-8 |
| OutputError, |
| /// The `gnuplot` command returned an unparseable string |
| ParseError(String), |
| } |
| impl fmt::Display for VersionError { |
| fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| match self { |
| VersionError::Exec(err) => write!(f, "`gnuplot --version` failed: {}", err), |
| VersionError::Error(msg) => { |
| write!(f, "`gnuplot --version` failed with error message:\n{}", msg) |
| } |
| VersionError::OutputError => write!(f, "`gnuplot --version` returned invalid utf-8"), |
| VersionError::ParseError(msg) => write!( |
| f, |
| "`gnuplot --version` returned an unparseable version string: {}", |
| msg |
| ), |
| } |
| } |
| } |
| impl ::std::error::Error for VersionError { |
| fn description(&self) -> &str { |
| match self { |
| VersionError::Exec(_) => "Execution Error", |
| VersionError::Error(_) => "Other Error", |
| VersionError::OutputError => "Output Error", |
| VersionError::ParseError(_) => "Parse Error", |
| } |
| } |
| |
| fn cause(&self) -> Option<&dyn ::std::error::Error> { |
| match self { |
| VersionError::Exec(err) => Some(err), |
| _ => None, |
| } |
| } |
| } |
| |
| /// Structure representing a gnuplot version number. |
| pub struct Version { |
| /// The major version number |
| pub major: usize, |
| /// The minor version number |
| pub minor: usize, |
| /// The patch level |
| pub patch: String, |
| } |
| |
| /// Returns `gnuplot` version |
| pub fn version() -> Result<Version, VersionError> { |
| let command_output = Command::new("gnuplot") |
| .arg("--version") |
| .output() |
| .map_err(VersionError::Exec)?; |
| if !command_output.status.success() { |
| let error = |
| String::from_utf8(command_output.stderr).map_err(|_| VersionError::OutputError)?; |
| return Err(VersionError::Error(error)); |
| } |
| |
| let output = String::from_utf8(command_output.stdout).map_err(|_| VersionError::OutputError)?; |
| |
| parse_version(&output).map_err(|_| VersionError::ParseError(output.clone())) |
| } |
| |
| fn parse_version(version_str: &str) -> Result<Version, Option<ParseIntError>> { |
| let mut words = version_str.split_whitespace().skip(1); |
| let mut version = words.next().ok_or(None)?.split('.'); |
| let major = version.next().ok_or(None)?.parse()?; |
| let minor = version.next().ok_or(None)?.parse()?; |
| let patchlevel = words.nth(1).ok_or(None)?.to_owned(); |
| |
| Ok(Version { |
| major, |
| minor, |
| patch: patchlevel, |
| }) |
| } |
| |
| fn scale_factor(map: &map::axis::Map<axis::Properties>, axes: Axes) -> (f64, f64) { |
| use crate::Axes::*; |
| use crate::Axis::*; |
| |
| match axes { |
| BottomXLeftY => ( |
| map.get(BottomX).map_or(1., ScaleFactorTrait::scale_factor), |
| map.get(LeftY).map_or(1., ScaleFactorTrait::scale_factor), |
| ), |
| BottomXRightY => ( |
| map.get(BottomX).map_or(1., ScaleFactorTrait::scale_factor), |
| map.get(RightY).map_or(1., ScaleFactorTrait::scale_factor), |
| ), |
| TopXLeftY => ( |
| map.get(TopX).map_or(1., ScaleFactorTrait::scale_factor), |
| map.get(LeftY).map_or(1., ScaleFactorTrait::scale_factor), |
| ), |
| TopXRightY => ( |
| map.get(TopX).map_or(1., ScaleFactorTrait::scale_factor), |
| map.get(RightY).map_or(1., ScaleFactorTrait::scale_factor), |
| ), |
| } |
| } |
| |
| // XXX :-1: to intra-crate privacy rules |
| /// Private |
| trait ScaleFactorTrait { |
| /// Private |
| fn scale_factor(&self) -> f64; |
| } |
| |
| #[cfg(test)] |
| mod test { |
| #[test] |
| fn version() { |
| if let Ok(version) = super::version() { |
| assert!(version.major >= 4); |
| } else { |
| println!("Gnuplot not installed."); |
| } |
| } |
| |
| #[test] |
| fn test_parse_version_on_valid_string() { |
| let string = "gnuplot 5.0 patchlevel 7"; |
| let version = super::parse_version(&string).unwrap(); |
| assert_eq!(5, version.major); |
| assert_eq!(0, version.minor); |
| assert_eq!("7", &version.patch); |
| } |
| |
| #[test] |
| fn test_parse_gentoo_version() { |
| let string = "gnuplot 5.2 patchlevel 5a (Gentoo revision r0)"; |
| let version = super::parse_version(&string).unwrap(); |
| assert_eq!(5, version.major); |
| assert_eq!(2, version.minor); |
| assert_eq!("5a", &version.patch); |
| } |
| |
| #[test] |
| fn test_parse_version_returns_error_on_invalid_strings() { |
| let strings = [ |
| "", |
| "foobar", |
| "gnuplot 50 patchlevel 7", |
| "gnuplot 5.0 patchlevel", |
| "gnuplot foo.bar patchlevel 7", |
| ]; |
| for string in &strings { |
| assert!(super::parse_version(string).is_err()); |
| } |
| } |
| } |