#![deny(missing_docs)]
#![allow(clippy::new_without_default)]
#![allow(clippy::doc_markdown)]
#![allow(clippy::many_single_char_names)]
extern crate byteorder;
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 data::Matrix;
mod data;
mod display;
mod map;
pub mod axis;
pub mod candlestick;
pub mod curve;
pub mod errorbar;
pub mod filledcurve;
pub mod key;
pub mod prelude;
pub mod traits;
use axis::{Axes, Axis, AxisProperties};
use key::KeyProperties;
#[derive(Clone)]
pub struct Figure {
alpha: Option<f64>,
axes: map::axis::Map<axis::AxisProperties>,
box_width: Option<f64>,
font: Option<Cow<'static, str>>,
font_size: Option<f64>,
key: Option<KeyProperties>,
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 {
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,
}
}
pub fn box_width(&mut self, width: f64) -> &mut Figure {
assert!(width >= 0.);
self.box_width = Some(width);
self
}
pub fn font<S>(&mut self, font: S) -> &mut Figure
where
S: Into<Cow<'static, str>>,
{
self.font = Some(font.into());
self
}
pub fn font_size(&mut self, size: f64) -> &mut Figure {
assert!(size >= 0.);
self.font_size = Some(size);
self
}
pub fn output<S>(&mut self, output: S) -> &mut Figure
where
S: Into<Cow<'static, Path>>,
{
self.output = output.into();
self
}
pub fn figure_size(&mut self, width: usize, height: usize) -> &mut Figure {
self.size = Some((width, height));
self
}
pub fn terminal(&mut self, terminal: Terminal) -> &mut Figure {
self.terminal = terminal;
self
}
pub fn title<S>(&mut self, title: S) -> &mut Figure
where
S: Into<Cow<'static, str>>,
{
self.title = Some(title.into());
self
}
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))
}
}
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
}
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)
}
pub fn dump<W>(&mut self, sink: &mut W) -> io::Result<&mut Figure>
where
W: io::Write,
{
sink.write_all(&self.script())?;
Ok(self)
}
pub fn save(&self, path: &Path) -> io::Result<&Figure> {
use std::io::Write;
File::create(path)?.write_all(&self.script())?;
Ok(self)
}
pub fn configure_axis<F: FnOnce(&mut AxisProperties) -> &mut AxisProperties>(
&mut self,
axis: Axis,
configure: F,
) -> &mut Figure {
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
}
pub fn configure_key<F: FnOnce(&mut KeyProperties) -> &mut KeyProperties>(
&mut self,
configure: F,
) -> &mut Figure {
match self.key {
Some(ref mut key) => {
configure(key);
}
None => {
let mut key = Default::default();
configure(&mut key);
self.key = Some(key);
}
}
self
}
}
impl Default for Figure {
fn default() -> Self {
Self::new()
}
}
#[allow(missing_docs)]
#[derive(Clone, Copy)]
pub enum Color {
Black,
Blue,
Cyan,
DarkViolet,
ForestGreen,
Gold,
Gray,
Green,
Magenta,
Red,
Rgb(u8, u8, u8),
White,
Yellow,
}
#[allow(missing_docs)]
#[derive(Clone, Copy)]
pub enum LineType {
Dash,
Dot,
DotDash,
DotDotDash,
SmallDot,
Solid,
}
#[allow(missing_docs)]
#[derive(Clone, Copy)]
pub enum PointType {
Circle,
FilledCircle,
FilledSquare,
FilledTriangle,
Plus,
Square,
Star,
Triangle,
X,
}
#[allow(missing_docs)]
#[derive(Clone, Copy)]
pub enum Terminal {
Svg,
}
trait Default {
fn default() -> Self;
}
trait Display<S> {
fn display(&self) -> S;
}
trait CurveDefault<S> {
fn default(S) -> Self;
}
trait ErrorBarDefault<S> {
fn default(S) -> Self;
}
trait Script {
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
}
}
#[derive(Debug)]
pub enum VersionError {
Exec(io::Error),
Error(String),
OutputError,
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,
}
}
}
pub struct Version {
pub major: usize,
pub minor: usize,
pub patch: String,
}
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<AxisProperties>, axes: Axes) -> (f64, f64) {
use Axes::*;
use Axis::*;
match axes {
BottomXLeftY => (
map.get(BottomX).map_or(1., |props| props.scale_factor()),
map.get(LeftY).map_or(1., |props| props.scale_factor()),
),
BottomXRightY => (
map.get(BottomX).map_or(1., |props| props.scale_factor()),
map.get(RightY).map_or(1., |props| props.scale_factor()),
),
TopXLeftY => (
map.get(TopX).map_or(1., |props| props.scale_factor()),
map.get(LeftY).map_or(1., |props| props.scale_factor()),
),
TopXRightY => (
map.get(TopX).map_or(1., |props| props.scale_factor()),
map.get(RightY).map_or(1., |props| props.scale_factor()),
),
}
}
trait ScaleFactorTrait {
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());
}
}
}