aboutsummaryrefslogtreecommitdiff
path: root/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib.rs')
-rwxr-xr-xsrc/lib.rs1090
1 files changed, 1090 insertions, 0 deletions
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100755
index 0000000..151f019
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,1090 @@
+//! [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());
+ }
+ }
+}