criterion-plot 0.4.0

Criterion's plotting library
Documentation
//! [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)
//!
//! ```
//! extern crate itertools_num;
//! extern crate criterion_plot as plot;
//!
//! # use std::fs;
//! # use std::path::Path;
//! use itertools_num::linspace;
//! use plot::prelude::*;
//!
//! # fn main() {
//! # if let Err(_) = 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)
//!
//! ```
//! extern crate itertools_num;
//! extern crate rand;
//! extern crate criterion_plot as plot;
//!
//! # use std::fs;
//! # use std::path::Path;
//! use std::f64::consts::PI;
//!
//! use itertools_num::linspace;
//! use rand::{Rng, XorShiftRng};
//! use plot::prelude::*;
//!
//! fn sinc(mut x: f64) -> f64 {
//!     if x == 0. {
//!         1.
//!     } else {
//!         x *= PI;
//!         x.sin() / x
//!     }
//! }
//!
//! # fn main() {
//! # if let Err(_) = 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)
//!
//! ```
//! extern crate rand;
//! extern crate criterion_plot as plot;
//!
//! # use std::fs;
//! # use std::path::Path;
//! use plot::prelude::*;
//! use rand::Rng;
//!
//! # fn main() {
//! # if let Err(_) = 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)
//!
//! ```
//! extern crate itertools_num;
//! extern crate num_complex;
//! extern crate criterion_plot as plot;
//!
//! # use std::fs;
//! # use std::path::Path;
//! use std::f64::consts::PI;
//!
//! use itertools_num::linspace;
//! use num_complex::Complex;
//! use plot::prelude::*;
//!
//! fn tf(x: f64) -> Complex<f64> {
//!     Complex::new(0., x) / Complex::new(10., x) / Complex::new(1., x / 10_000.)
//! }
//!
//! # fn main() {
//! # if let Err(_) = 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)
//!
//! ```
//! extern crate itertools_num;
//! extern crate criterion_plot as plot;
//!
//! # use std::fs;
//! # use std::path::Path;
//! use std::f64::consts::PI;
//! use std::iter;
//!
//! use itertools_num::linspace;
//! use plot::prelude::*;
//!
//! # fn main() {
//! # if let Err(_) = 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()));

        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());
        }
    }
}