1 | //! [Criterion]'s plotting library. |
2 | //! |
3 | //! [Criterion]: https://github.com/bheisler/criterion.rs |
4 | //! |
5 | //! **WARNING** This library is criterion's implementation detail and there no plans to stabilize |
6 | //! it. In other words, the API may break at any time without notice. |
7 | //! |
8 | //! # Examples |
9 | //! |
10 | //! - Simple "curves" (based on [`simple.dem`](http://gnuplot.sourceforge.net/demo/simple.html)) |
11 | //! |
12 | //! ![Plot](curve.svg) |
13 | //! |
14 | //! ``` |
15 | //! # use std::fs; |
16 | //! # use std::path::Path; |
17 | //! use itertools_num::linspace; |
18 | //! use criterion_plot::prelude::*; |
19 | //! |
20 | //! # if let Err(_) = criterion_plot::version() { |
21 | //! # return; |
22 | //! # } |
23 | //! let ref xs = linspace::<f64>(-10., 10., 51).collect::<Vec<_>>(); |
24 | //! |
25 | //! # fs::create_dir_all(Path::new("target/doc/criterion_plot" )).unwrap(); |
26 | //! # assert_eq!(Some(String::new()), |
27 | //! Figure::new() |
28 | //! # .set(Font("Helvetica" )) |
29 | //! # .set(FontSize(12.)) |
30 | //! # .set(Output(Path::new("target/doc/criterion_plot/curve.svg" ))) |
31 | //! # .set(Size(1280, 720)) |
32 | //! .configure(Key, |k| { |
33 | //! k.set(Boxed::Yes) |
34 | //! .set(Position::Inside(Vertical::Top, Horizontal::Left)) |
35 | //! }) |
36 | //! .plot(LinesPoints { |
37 | //! x: xs, |
38 | //! y: xs.iter().map(|x| x.sin()), |
39 | //! }, |
40 | //! |lp| { |
41 | //! lp.set(Color::DarkViolet) |
42 | //! .set(Label("sin(x)" )) |
43 | //! .set(LineType::Dash) |
44 | //! .set(PointSize(1.5)) |
45 | //! .set(PointType::Circle) |
46 | //! }) |
47 | //! .plot(Steps { |
48 | //! x: xs, |
49 | //! y: xs.iter().map(|x| x.atan()), |
50 | //! }, |
51 | //! |s| { |
52 | //! s.set(Color::Rgb(0, 158, 115)) |
53 | //! .set(Label("atan(x)" )) |
54 | //! .set(LineWidth(2.)) |
55 | //! }) |
56 | //! .plot(Impulses { |
57 | //! x: xs, |
58 | //! y: xs.iter().map(|x| x.atan().cos()), |
59 | //! }, |
60 | //! |i| { |
61 | //! i.set(Color::Rgb(86, 180, 233)) |
62 | //! .set(Label("cos(atan(x))" )) |
63 | //! }) |
64 | //! .draw() // (rest of the chain has been omitted) |
65 | //! # .ok() |
66 | //! # .and_then(|gnuplot| { |
67 | //! # gnuplot.wait_with_output().ok().and_then(|p| String::from_utf8(p.stderr).ok()) |
68 | //! # })); |
69 | //! ``` |
70 | //! |
71 | //! - error bars (based on |
72 | //! [Julia plotting tutorial](https://plot.ly/julia/error-bars/#Colored-and-Styled-Error-Bars)) |
73 | //! |
74 | //! ![Plot](error_bar.svg) |
75 | //! |
76 | //! ``` |
77 | //! # use std::fs; |
78 | //! # use std::path::Path; |
79 | //! use std::f64::consts::PI; |
80 | //! |
81 | //! use itertools_num::linspace; |
82 | //! use rand::Rng; |
83 | //! use criterion_plot::prelude::*; |
84 | //! |
85 | //! fn sinc(mut x: f64) -> f64 { |
86 | //! if x == 0. { |
87 | //! 1. |
88 | //! } else { |
89 | //! x *= PI; |
90 | //! x.sin() / x |
91 | //! } |
92 | //! } |
93 | //! |
94 | //! # if let Err(_) = criterion_plot::version() { |
95 | //! # return; |
96 | //! # } |
97 | //! let ref xs_ = linspace::<f64>(-4., 4., 101).collect::<Vec<_>>(); |
98 | //! |
99 | //! // Fake some data |
100 | //! let ref mut rng = rand::thread_rng(); |
101 | //! let xs = linspace::<f64>(-4., 4., 13).skip(1).take(11); |
102 | //! let ys = xs.map(|x| sinc(x) + 0.05 * rng.gen::<f64>() - 0.025).collect::<Vec<_>>(); |
103 | //! let y_low = ys.iter().map(|&y| y - 0.025 - 0.075 * rng.gen::<f64>()).collect::<Vec<_>>(); |
104 | //! let y_high = ys.iter().map(|&y| y + 0.025 + 0.075 * rng.gen::<f64>()).collect::<Vec<_>>(); |
105 | //! let xs = linspace::<f64>(-4., 4., 13).skip(1).take(11); |
106 | //! let xs = xs.map(|x| x + 0.2 * rng.gen::<f64>() - 0.1); |
107 | //! |
108 | //! # fs::create_dir_all(Path::new("target/doc/criterion_plot" )).unwrap(); |
109 | //! # assert_eq!(Some(String::new()), |
110 | //! Figure::new() |
111 | //! # .set(Font("Helvetica" )) |
112 | //! # .set(FontSize(12.)) |
113 | //! # .set(Output(Path::new("target/doc/criterion_plot/error_bar.svg" ))) |
114 | //! # .set(Size(1280, 720)) |
115 | //! .configure(Axis::BottomX, |a| { |
116 | //! a.set(TicLabels { |
117 | //! labels: &["-π" , "0" , "π" ], |
118 | //! positions: &[-PI, 0., PI], |
119 | //! }) |
120 | //! }) |
121 | //! .configure(Key, |
122 | //! |k| k.set(Position::Outside(Vertical::Top, Horizontal::Right))) |
123 | //! .plot(Lines { |
124 | //! x: xs_, |
125 | //! y: xs_.iter().cloned().map(sinc), |
126 | //! }, |
127 | //! |l| { |
128 | //! l.set(Color::Rgb(0, 158, 115)) |
129 | //! .set(Label("sinc(x)" )) |
130 | //! .set(LineWidth(2.)) |
131 | //! }) |
132 | //! .plot(YErrorBars { |
133 | //! x: xs, |
134 | //! y: &ys, |
135 | //! y_low: &y_low, |
136 | //! y_high: &y_high, |
137 | //! }, |
138 | //! |eb| { |
139 | //! eb.set(Color::DarkViolet) |
140 | //! .set(LineWidth(2.)) |
141 | //! .set(PointType::FilledCircle) |
142 | //! .set(Label("measured" )) |
143 | //! }) |
144 | //! .draw() // (rest of the chain has been omitted) |
145 | //! # .ok() |
146 | //! # .and_then(|gnuplot| { |
147 | //! # gnuplot.wait_with_output().ok().and_then(|p| String::from_utf8(p.stderr).ok()) |
148 | //! # })); |
149 | //! ``` |
150 | //! |
151 | //! - Candlesticks (based on |
152 | //! [`candlesticks.dem`](http://gnuplot.sourceforge.net/demo/candlesticks.html)) |
153 | //! |
154 | //! ![Plot](candlesticks.svg) |
155 | //! |
156 | //! ``` |
157 | //! # use std::fs; |
158 | //! # use std::path::Path; |
159 | //! use criterion_plot::prelude::*; |
160 | //! use rand::Rng; |
161 | //! |
162 | //! # if let Err(_) = criterion_plot::version() { |
163 | //! # return; |
164 | //! # } |
165 | //! let xs = 1..11; |
166 | //! |
167 | //! // Fake some data |
168 | //! let mut rng = rand::thread_rng(); |
169 | //! let bh = xs.clone().map(|_| 5f64 + 2.5 * rng.gen::<f64>()).collect::<Vec<_>>(); |
170 | //! let bm = xs.clone().map(|_| 2.5f64 + 2.5 * rng.gen::<f64>()).collect::<Vec<_>>(); |
171 | //! let wh = bh.iter().map(|&y| y + (10. - y) * rng.gen::<f64>()).collect::<Vec<_>>(); |
172 | //! let wm = bm.iter().map(|&y| y * rng.gen::<f64>()).collect::<Vec<_>>(); |
173 | //! let m = bm.iter().zip(bh.iter()).map(|(&l, &h)| (h - l) * rng.gen::<f64>() + l) |
174 | //! .collect::<Vec<_>>(); |
175 | //! |
176 | //! # fs::create_dir_all(Path::new("target/doc/criterion_plot" )).unwrap(); |
177 | //! # assert_eq!(Some(String::new()), |
178 | //! Figure::new() |
179 | //! # .set(Font("Helvetica" )) |
180 | //! # .set(FontSize(12.)) |
181 | //! # .set(Output(Path::new("target/doc/criterion_plot/candlesticks.svg" ))) |
182 | //! # .set(Size(1280, 720)) |
183 | //! .set(BoxWidth(0.2)) |
184 | //! .configure(Axis::BottomX, |a| a.set(Range::Limits(0., 11.))) |
185 | //! .plot(Candlesticks { |
186 | //! x: xs.clone(), |
187 | //! whisker_min: &wm, |
188 | //! box_min: &bm, |
189 | //! box_high: &bh, |
190 | //! whisker_high: &wh, |
191 | //! }, |
192 | //! |cs| { |
193 | //! cs.set(Color::Rgb(86, 180, 233)) |
194 | //! .set(Label("Quartiles" )) |
195 | //! .set(LineWidth(2.)) |
196 | //! }) |
197 | //! // trick to plot the median |
198 | //! .plot(Candlesticks { |
199 | //! x: xs, |
200 | //! whisker_min: &m, |
201 | //! box_min: &m, |
202 | //! box_high: &m, |
203 | //! whisker_high: &m, |
204 | //! }, |
205 | //! |cs| { |
206 | //! cs.set(Color::Black) |
207 | //! .set(LineWidth(2.)) |
208 | //! }) |
209 | //! .draw() // (rest of the chain has been omitted) |
210 | //! # .ok() |
211 | //! # .and_then(|gnuplot| { |
212 | //! # gnuplot.wait_with_output().ok().and_then(|p| String::from_utf8(p.stderr).ok()) |
213 | //! # })); |
214 | //! ``` |
215 | //! |
216 | //! - Multiaxis (based on [`multiaxis.dem`](http://gnuplot.sourceforge.net/demo/multiaxis.html)) |
217 | //! |
218 | //! ![Plot](multiaxis.svg) |
219 | //! |
220 | //! ``` |
221 | //! # use std::fs; |
222 | //! # use std::path::Path; |
223 | //! use std::f64::consts::PI; |
224 | //! |
225 | //! use itertools_num::linspace; |
226 | //! use num_complex::Complex; |
227 | //! use criterion_plot::prelude::*; |
228 | //! |
229 | //! fn tf(x: f64) -> Complex<f64> { |
230 | //! Complex::new(0., x) / Complex::new(10., x) / Complex::new(1., x / 10_000.) |
231 | //! } |
232 | //! |
233 | //! # if let Err(_) = criterion_plot::version() { |
234 | //! # return; |
235 | //! # } |
236 | //! let (start, end): (f64, f64) = (1.1, 90_000.); |
237 | //! let ref xs = linspace(start.ln(), end.ln(), 101).map(|x| x.exp()).collect::<Vec<_>>(); |
238 | //! let phase = xs.iter().map(|&x| tf(x).arg() * 180. / PI); |
239 | //! let magnitude = xs.iter().map(|&x| tf(x).norm()); |
240 | //! |
241 | //! # fs::create_dir_all(Path::new("target/doc/criterion_plot" )).unwrap(); |
242 | //! # assert_eq!(Some(String::new()), |
243 | //! Figure::new(). |
244 | //! # set(Font("Helvetica" )). |
245 | //! # set(FontSize(12.)). |
246 | //! # set(Output(Path::new("target/doc/criterion_plot/multiaxis.svg" ))). |
247 | //! # set(Size(1280, 720)). |
248 | //! set(Title("Frequency response" )). |
249 | //! configure(Axis::BottomX, |a| a. |
250 | //! configure(Grid::Major, |g| g. |
251 | //! show()). |
252 | //! set(Label("Angular frequency (rad/s)" )). |
253 | //! set(Range::Limits(start, end)). |
254 | //! set(Scale::Logarithmic)). |
255 | //! configure(Axis::LeftY, |a| a. |
256 | //! set(Label("Gain" )). |
257 | //! set(Scale::Logarithmic)). |
258 | //! configure(Axis::RightY, |a| a. |
259 | //! configure(Grid::Major, |g| g. |
260 | //! show()). |
261 | //! set(Label("Phase shift (°)" ))). |
262 | //! configure(Key, |k| k. |
263 | //! set(Position::Inside(Vertical::Top, Horizontal::Center)). |
264 | //! set(Title(" " ))). |
265 | //! plot(Lines { |
266 | //! x: xs, |
267 | //! y: magnitude, |
268 | //! }, |l| l. |
269 | //! set(Color::DarkViolet). |
270 | //! set(Label("Magnitude" )). |
271 | //! set(LineWidth(2.))). |
272 | //! plot(Lines { |
273 | //! x: xs, |
274 | //! y: phase, |
275 | //! }, |l| l. |
276 | //! set(Axes::BottomXRightY). |
277 | //! set(Color::Rgb(0, 158, 115)). |
278 | //! set(Label("Phase" )). |
279 | //! set(LineWidth(2.))). |
280 | //! draw(). // (rest of the chain has been omitted) |
281 | //! # ok().and_then(|gnuplot| { |
282 | //! # gnuplot.wait_with_output().ok().and_then(|p| { |
283 | //! # String::from_utf8(p.stderr).ok() |
284 | //! # }) |
285 | //! # })); |
286 | //! ``` |
287 | //! - Filled curves (based on |
288 | //! [`transparent.dem`](http://gnuplot.sourceforge.net/demo/transparent.html)) |
289 | //! |
290 | //! ![Plot](filled_curve.svg) |
291 | //! |
292 | //! ``` |
293 | //! # use std::fs; |
294 | //! # use std::path::Path; |
295 | //! use std::f64::consts::PI; |
296 | //! use std::iter; |
297 | //! |
298 | //! use itertools_num::linspace; |
299 | //! use criterion_plot::prelude::*; |
300 | //! |
301 | //! # if let Err(_) = criterion_plot::version() { |
302 | //! # return; |
303 | //! # } |
304 | //! let (start, end) = (-5., 5.); |
305 | //! let ref xs = linspace(start, end, 101).collect::<Vec<_>>(); |
306 | //! let zeros = iter::repeat(0); |
307 | //! |
308 | //! fn gaussian(x: f64, mu: f64, sigma: f64) -> f64 { |
309 | //! 1. / (((x - mu).powi(2) / 2. / sigma.powi(2)).exp() * sigma * (2. * PI).sqrt()) |
310 | //! } |
311 | //! |
312 | //! # fs::create_dir_all(Path::new("target/doc/criterion_plot" )).unwrap(); |
313 | //! # assert_eq!(Some(String::new()), |
314 | //! Figure::new() |
315 | //! # .set(Font("Helvetica" )) |
316 | //! # .set(FontSize(12.)) |
317 | //! # .set(Output(Path::new("target/doc/criterion_plot/filled_curve.svg" ))) |
318 | //! # .set(Size(1280, 720)) |
319 | //! .set(Title("Transparent filled curve" )) |
320 | //! .configure(Axis::BottomX, |a| a.set(Range::Limits(start, end))) |
321 | //! .configure(Axis::LeftY, |a| a.set(Range::Limits(0., 1.))) |
322 | //! .configure(Key, |k| { |
323 | //! k.set(Justification::Left) |
324 | //! .set(Order::SampleText) |
325 | //! .set(Position::Inside(Vertical::Top, Horizontal::Left)) |
326 | //! .set(Title("Gaussian Distribution" )) |
327 | //! }) |
328 | //! .plot(FilledCurve { |
329 | //! x: xs, |
330 | //! y1: xs.iter().map(|&x| gaussian(x, 0.5, 0.5)), |
331 | //! y2: zeros.clone(), |
332 | //! }, |
333 | //! |fc| { |
334 | //! fc.set(Color::ForestGreen) |
335 | //! .set(Label("μ = 0.5 σ = 0.5" )) |
336 | //! }) |
337 | //! .plot(FilledCurve { |
338 | //! x: xs, |
339 | //! y1: xs.iter().map(|&x| gaussian(x, 2.0, 1.0)), |
340 | //! y2: zeros.clone(), |
341 | //! }, |
342 | //! |fc| { |
343 | //! fc.set(Color::Gold) |
344 | //! .set(Label("μ = 2.0 σ = 1.0" )) |
345 | //! .set(Opacity(0.5)) |
346 | //! }) |
347 | //! .plot(FilledCurve { |
348 | //! x: xs, |
349 | //! y1: xs.iter().map(|&x| gaussian(x, -1.0, 2.0)), |
350 | //! y2: zeros, |
351 | //! }, |
352 | //! |fc| { |
353 | //! fc.set(Color::Red) |
354 | //! .set(Label("μ = -1.0 σ = 2.0" )) |
355 | //! .set(Opacity(0.5)) |
356 | //! }) |
357 | //! .draw() |
358 | //! .ok() |
359 | //! .and_then(|gnuplot| { |
360 | //! gnuplot.wait_with_output().ok().and_then(|p| String::from_utf8(p.stderr).ok()) |
361 | //! })); |
362 | //! ``` |
363 | |
364 | #![deny (missing_docs)] |
365 | #![deny (warnings)] |
366 | #![deny (bare_trait_objects)] |
367 | // This lint has lots of false positives ATM, see |
368 | // https://github.com/Manishearth/rust-clippy/issues/761 |
369 | #![cfg_attr (feature = "cargo-clippy" , allow(clippy::new_without_default))] |
370 | // False positives with images |
371 | #![cfg_attr (feature = "cargo-clippy" , allow(clippy::doc_markdown))] |
372 | #![cfg_attr (feature = "cargo-clippy" , allow(clippy::many_single_char_names))] |
373 | |
374 | extern crate cast; |
375 | #[macro_use ] |
376 | extern crate itertools; |
377 | |
378 | use std::borrow::Cow; |
379 | use std::fmt; |
380 | use std::fs::File; |
381 | use std::io; |
382 | use std::num::ParseIntError; |
383 | use std::path::Path; |
384 | use std::process::{Child, Command}; |
385 | use std::str; |
386 | |
387 | use crate::data::Matrix; |
388 | use crate::traits::{Configure, Set}; |
389 | |
390 | mod data; |
391 | mod display; |
392 | mod map; |
393 | |
394 | pub mod axis; |
395 | pub mod candlestick; |
396 | pub mod curve; |
397 | pub mod errorbar; |
398 | pub mod filledcurve; |
399 | pub mod grid; |
400 | pub mod key; |
401 | pub mod prelude; |
402 | pub mod proxy; |
403 | pub mod traits; |
404 | |
405 | /// Plot container |
406 | #[derive(Clone)] |
407 | pub struct Figure { |
408 | alpha: Option<f64>, |
409 | axes: map::axis::Map<axis::Properties>, |
410 | box_width: Option<f64>, |
411 | font: Option<Cow<'static, str>>, |
412 | font_size: Option<f64>, |
413 | key: Option<key::Properties>, |
414 | output: Cow<'static, Path>, |
415 | plots: Vec<Plot>, |
416 | size: Option<(usize, usize)>, |
417 | terminal: Terminal, |
418 | tics: map::axis::Map<String>, |
419 | title: Option<Cow<'static, str>>, |
420 | } |
421 | |
422 | impl Figure { |
423 | /// Creates an empty figure |
424 | pub fn new() -> Figure { |
425 | Figure { |
426 | alpha: None, |
427 | axes: map::axis::Map::new(), |
428 | box_width: None, |
429 | font: None, |
430 | font_size: None, |
431 | key: None, |
432 | output: Cow::Borrowed(Path::new("output.plot" )), |
433 | plots: Vec::new(), |
434 | size: None, |
435 | terminal: Terminal::Svg, |
436 | tics: map::axis::Map::new(), |
437 | title: None, |
438 | } |
439 | } |
440 | |
441 | // Allow clippy::format_push_string even with older versions of rust (<1.62) which |
442 | // don't have it defined. |
443 | #[allow (clippy::all)] |
444 | fn script(&self) -> Vec<u8> { |
445 | let mut s = String::new(); |
446 | |
447 | s.push_str(&format!( |
448 | "set output '{}' \n" , |
449 | self.output.display().to_string().replace(' \'' , "''" ) |
450 | )); |
451 | |
452 | if let Some(width) = self.box_width { |
453 | s.push_str(&format!("set boxwidth {} \n" , width)) |
454 | } |
455 | |
456 | if let Some(ref title) = self.title { |
457 | s.push_str(&format!("set title '{}' \n" , title)) |
458 | } |
459 | |
460 | for axis in self.axes.iter() { |
461 | s.push_str(&axis.script()); |
462 | } |
463 | |
464 | for (_, script) in self.tics.iter() { |
465 | s.push_str(script); |
466 | } |
467 | |
468 | if let Some(ref key) = self.key { |
469 | s.push_str(&key.script()) |
470 | } |
471 | |
472 | if let Some(alpha) = self.alpha { |
473 | s.push_str(&format!("set style fill transparent solid {} \n" , alpha)) |
474 | } |
475 | |
476 | s.push_str(&format!("set terminal {} dashed" , self.terminal.display())); |
477 | |
478 | if let Some((width, height)) = self.size { |
479 | s.push_str(&format!(" size {}, {}" , width, height)) |
480 | } |
481 | |
482 | if let Some(ref name) = self.font { |
483 | if let Some(size) = self.font_size { |
484 | s.push_str(&format!(" font '{},{}'" , name, size)) |
485 | } else { |
486 | s.push_str(&format!(" font '{}'" , name)) |
487 | } |
488 | } |
489 | |
490 | // TODO This removes the crossbars from the ends of error bars, but should be configurable |
491 | s.push_str(" \nunset bars \n" ); |
492 | |
493 | let mut is_first_plot = true; |
494 | for plot in &self.plots { |
495 | let data = plot.data(); |
496 | |
497 | if data.bytes().is_empty() { |
498 | continue; |
499 | } |
500 | |
501 | if is_first_plot { |
502 | s.push_str("plot " ); |
503 | is_first_plot = false; |
504 | } else { |
505 | s.push_str(", " ); |
506 | } |
507 | |
508 | s.push_str(&format!( |
509 | "'-' binary endian=little record={} format='%float64' using " , |
510 | data.nrows() |
511 | )); |
512 | |
513 | let mut is_first_col = true; |
514 | for col in 0..data.ncols() { |
515 | if is_first_col { |
516 | is_first_col = false; |
517 | } else { |
518 | s.push(':' ); |
519 | } |
520 | s.push_str(&(col + 1).to_string()); |
521 | } |
522 | s.push(' ' ); |
523 | |
524 | s.push_str(plot.script()); |
525 | } |
526 | |
527 | let mut buffer = s.into_bytes(); |
528 | let mut is_first = true; |
529 | for plot in &self.plots { |
530 | if is_first { |
531 | is_first = false; |
532 | buffer.push(b' \n' ); |
533 | } |
534 | buffer.extend_from_slice(plot.data().bytes()); |
535 | } |
536 | |
537 | buffer |
538 | } |
539 | |
540 | /// Spawns a drawing child process |
541 | /// |
542 | /// NOTE: stderr, stdin, and stdout are piped |
543 | pub fn draw(&mut self) -> io::Result<Child> { |
544 | use std::process::Stdio; |
545 | |
546 | let mut gnuplot = Command::new("gnuplot" ) |
547 | .stderr(Stdio::piped()) |
548 | .stdin(Stdio::piped()) |
549 | .stdout(Stdio::piped()) |
550 | .spawn()?; |
551 | self.dump(gnuplot.stdin.as_mut().unwrap())?; |
552 | Ok(gnuplot) |
553 | } |
554 | |
555 | /// Dumps the script required to produce the figure into `sink` |
556 | pub fn dump<W>(&mut self, sink: &mut W) -> io::Result<&mut Figure> |
557 | where |
558 | W: io::Write, |
559 | { |
560 | sink.write_all(&self.script())?; |
561 | Ok(self) |
562 | } |
563 | |
564 | /// Saves the script required to produce the figure to `path` |
565 | pub fn save(&self, path: &Path) -> io::Result<&Figure> { |
566 | use std::io::Write; |
567 | |
568 | File::create(path)?.write_all(&self.script())?; |
569 | Ok(self) |
570 | } |
571 | } |
572 | |
573 | impl Configure<Axis> for Figure { |
574 | type Properties = axis::Properties; |
575 | |
576 | /// Configures an axis |
577 | fn configure<F>(&mut self, axis: Axis, configure: F) -> &mut Figure |
578 | where |
579 | F: FnOnce(&mut axis::Properties) -> &mut axis::Properties, |
580 | { |
581 | if self.axes.contains_key(axis) { |
582 | configure(self.axes.get_mut(axis).unwrap()); |
583 | } else { |
584 | let mut properties = Default::default(); |
585 | configure(&mut properties); |
586 | self.axes.insert(axis, properties); |
587 | } |
588 | self |
589 | } |
590 | } |
591 | |
592 | impl Configure<Key> for Figure { |
593 | type Properties = key::Properties; |
594 | |
595 | /// Configures the key (legend) |
596 | fn configure<F>(&mut self, _: Key, configure: F) -> &mut Figure |
597 | where |
598 | F: FnOnce(&mut key::Properties) -> &mut key::Properties, |
599 | { |
600 | if self.key.is_some() { |
601 | configure(self.key.as_mut().unwrap()); |
602 | } else { |
603 | let mut key = Default::default(); |
604 | configure(&mut key); |
605 | self.key = Some(key); |
606 | } |
607 | self |
608 | } |
609 | } |
610 | |
611 | impl Set<BoxWidth> for Figure { |
612 | /// Changes the box width of all the box related plots (bars, candlesticks, etc) |
613 | /// |
614 | /// **Note** The default value is 0 |
615 | /// |
616 | /// # Panics |
617 | /// |
618 | /// Panics if `width` is a negative value |
619 | fn set(&mut self, width: BoxWidth) -> &mut Figure { |
620 | let width = width.0; |
621 | |
622 | assert!(width >= 0.); |
623 | |
624 | self.box_width = Some(width); |
625 | self |
626 | } |
627 | } |
628 | |
629 | impl Set<Font> for Figure { |
630 | /// Changes the font |
631 | fn set(&mut self, font: Font) -> &mut Figure { |
632 | self.font = Some(font.0); |
633 | self |
634 | } |
635 | } |
636 | |
637 | impl Set<FontSize> for Figure { |
638 | /// Changes the size of the font |
639 | /// |
640 | /// # Panics |
641 | /// |
642 | /// Panics if `size` is a non-positive value |
643 | fn set(&mut self, size: FontSize) -> &mut Figure { |
644 | let size = size.0; |
645 | |
646 | assert!(size >= 0.); |
647 | |
648 | self.font_size = Some(size); |
649 | self |
650 | } |
651 | } |
652 | |
653 | impl Set<Output> for Figure { |
654 | /// Changes the output file |
655 | /// |
656 | /// **Note** The default output file is `output.plot` |
657 | fn set(&mut self, output: Output) -> &mut Figure { |
658 | self.output = output.0; |
659 | self |
660 | } |
661 | } |
662 | |
663 | impl Set<Size> for Figure { |
664 | /// Changes the figure size |
665 | fn set(&mut self, size: Size) -> &mut Figure { |
666 | self.size = Some((size.0, size.1)); |
667 | self |
668 | } |
669 | } |
670 | |
671 | impl Set<Terminal> for Figure { |
672 | /// Changes the output terminal |
673 | /// |
674 | /// **Note** By default, the terminal is set to `Svg` |
675 | fn set(&mut self, terminal: Terminal) -> &mut Figure { |
676 | self.terminal = terminal; |
677 | self |
678 | } |
679 | } |
680 | |
681 | impl Set<Title> for Figure { |
682 | /// Sets the title |
683 | fn set(&mut self, title: Title) -> &mut Figure { |
684 | self.title = Some(title.0); |
685 | self |
686 | } |
687 | } |
688 | |
689 | impl Default for Figure { |
690 | fn default() -> Self { |
691 | Self::new() |
692 | } |
693 | } |
694 | |
695 | /// Box width for box-related plots: bars, candlesticks, etc |
696 | #[derive(Clone, Copy)] |
697 | pub struct BoxWidth(pub f64); |
698 | |
699 | /// A font name |
700 | pub struct Font(Cow<'static, str>); |
701 | |
702 | /// The size of a font |
703 | #[derive(Clone, Copy)] |
704 | pub struct FontSize(pub f64); |
705 | |
706 | /// The key or legend |
707 | #[derive(Clone, Copy)] |
708 | pub struct Key; |
709 | |
710 | /// Plot label |
711 | pub struct Label(Cow<'static, str>); |
712 | |
713 | /// Width of the lines |
714 | #[derive(Clone, Copy)] |
715 | pub struct LineWidth(pub f64); |
716 | |
717 | /// Fill color opacity |
718 | #[derive(Clone, Copy)] |
719 | pub struct Opacity(pub f64); |
720 | |
721 | /// Output file path |
722 | pub struct Output(Cow<'static, Path>); |
723 | |
724 | /// Size of the points |
725 | #[derive(Clone, Copy)] |
726 | pub struct PointSize(pub f64); |
727 | |
728 | /// Axis range |
729 | #[derive(Clone, Copy)] |
730 | pub enum Range { |
731 | /// Autoscale the axis |
732 | Auto, |
733 | /// Set the limits of the axis |
734 | Limits(f64, f64), |
735 | } |
736 | |
737 | /// Figure size |
738 | #[derive(Clone, Copy)] |
739 | pub struct Size(pub usize, pub usize); |
740 | |
741 | /// Labels attached to the tics of an axis |
742 | pub struct TicLabels<P, L> { |
743 | /// Labels to attach to the tics |
744 | pub labels: L, |
745 | /// Position of the tics on the axis |
746 | pub positions: P, |
747 | } |
748 | |
749 | /// Figure title |
750 | pub struct Title(Cow<'static, str>); |
751 | |
752 | /// A pair of axes that define a coordinate system |
753 | #[allow (missing_docs)] |
754 | #[derive(Clone, Copy)] |
755 | pub enum Axes { |
756 | BottomXLeftY, |
757 | BottomXRightY, |
758 | TopXLeftY, |
759 | TopXRightY, |
760 | } |
761 | |
762 | /// A coordinate axis |
763 | #[derive(Clone, Copy)] |
764 | pub enum Axis { |
765 | /// X axis on the bottom side of the figure |
766 | BottomX, |
767 | /// Y axis on the left side of the figure |
768 | LeftY, |
769 | /// Y axis on the right side of the figure |
770 | RightY, |
771 | /// X axis on the top side of the figure |
772 | TopX, |
773 | } |
774 | |
775 | impl Axis { |
776 | fn next(self) -> Option<Axis> { |
777 | use crate::Axis::*; |
778 | |
779 | match self { |
780 | BottomX => Some(LeftY), |
781 | LeftY => Some(RightY), |
782 | RightY => Some(TopX), |
783 | TopX => None, |
784 | } |
785 | } |
786 | } |
787 | |
788 | /// Color |
789 | #[allow (missing_docs)] |
790 | #[derive(Clone, Copy)] |
791 | pub enum Color { |
792 | Black, |
793 | Blue, |
794 | Cyan, |
795 | DarkViolet, |
796 | ForestGreen, |
797 | Gold, |
798 | Gray, |
799 | Green, |
800 | Magenta, |
801 | Red, |
802 | /// Custom RGB color |
803 | Rgb(u8, u8, u8), |
804 | White, |
805 | Yellow, |
806 | } |
807 | |
808 | /// Grid line |
809 | #[derive(Clone, Copy)] |
810 | pub enum Grid { |
811 | /// Major gridlines |
812 | Major, |
813 | /// Minor gridlines |
814 | Minor, |
815 | } |
816 | |
817 | impl Grid { |
818 | fn next(self) -> Option<Grid> { |
819 | use crate::Grid::*; |
820 | |
821 | match self { |
822 | Major => Some(Minor), |
823 | Minor => None, |
824 | } |
825 | } |
826 | } |
827 | |
828 | /// Line type |
829 | #[allow (missing_docs)] |
830 | #[derive(Clone, Copy)] |
831 | pub enum LineType { |
832 | Dash, |
833 | Dot, |
834 | DotDash, |
835 | DotDotDash, |
836 | /// Line made of minimally sized dots |
837 | SmallDot, |
838 | Solid, |
839 | } |
840 | |
841 | /// Point type |
842 | #[allow (missing_docs)] |
843 | #[derive(Clone, Copy)] |
844 | pub enum PointType { |
845 | Circle, |
846 | FilledCircle, |
847 | FilledSquare, |
848 | FilledTriangle, |
849 | Plus, |
850 | Square, |
851 | Star, |
852 | Triangle, |
853 | X, |
854 | } |
855 | |
856 | /// Axis scale |
857 | #[allow (missing_docs)] |
858 | #[derive(Clone, Copy)] |
859 | pub enum Scale { |
860 | Linear, |
861 | Logarithmic, |
862 | } |
863 | |
864 | /// Axis scale factor |
865 | #[allow (missing_docs)] |
866 | #[derive(Clone, Copy)] |
867 | pub struct ScaleFactor(pub f64); |
868 | |
869 | /// Output terminal |
870 | #[allow (missing_docs)] |
871 | #[derive(Clone, Copy)] |
872 | pub enum Terminal { |
873 | Svg, |
874 | } |
875 | |
876 | /// Not public version of `std::default::Default`, used to not leak default constructors into the |
877 | /// public API |
878 | trait Default { |
879 | /// Creates `Properties` with default configuration |
880 | fn default() -> Self; |
881 | } |
882 | |
883 | /// Enums that can produce gnuplot code |
884 | trait Display<S> { |
885 | /// Translates the enum in gnuplot code |
886 | fn display(&self) -> S; |
887 | } |
888 | |
889 | /// Curve variant of Default |
890 | trait CurveDefault<S> { |
891 | /// Creates `curve::Properties` with default configuration |
892 | fn default(s: S) -> Self; |
893 | } |
894 | |
895 | /// Error bar variant of Default |
896 | trait ErrorBarDefault<S> { |
897 | /// Creates `errorbar::Properties` with default configuration |
898 | fn default(s: S) -> Self; |
899 | } |
900 | |
901 | /// Structs that can produce gnuplot code |
902 | trait Script { |
903 | /// Translates some configuration struct into gnuplot code |
904 | fn script(&self) -> String; |
905 | } |
906 | |
907 | #[derive(Clone)] |
908 | struct Plot { |
909 | data: Matrix, |
910 | script: String, |
911 | } |
912 | |
913 | impl Plot { |
914 | fn new<S>(data: Matrix, script: &S) -> Plot |
915 | where |
916 | S: Script, |
917 | { |
918 | Plot { |
919 | data, |
920 | script: script.script(), |
921 | } |
922 | } |
923 | |
924 | fn data(&self) -> &Matrix { |
925 | &self.data |
926 | } |
927 | |
928 | fn script(&self) -> &str { |
929 | &self.script |
930 | } |
931 | } |
932 | |
933 | /// Possible errors when parsing gnuplot's version string |
934 | #[derive(Debug)] |
935 | pub enum VersionError { |
936 | /// The `gnuplot` command couldn't be executed |
937 | Exec(io::Error), |
938 | /// The `gnuplot` command returned an error message |
939 | Error(String), |
940 | /// The `gnuplot` command returned invalid utf-8 |
941 | OutputError, |
942 | /// The `gnuplot` command returned an unparseable string |
943 | ParseError(String), |
944 | } |
945 | impl fmt::Display for VersionError { |
946 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
947 | match self { |
948 | VersionError::Exec(err) => write!(f, "`gnuplot --version` failed: {}" , err), |
949 | VersionError::Error(msg) => { |
950 | write!(f, "`gnuplot --version` failed with error message: \n{}" , msg) |
951 | } |
952 | VersionError::OutputError => write!(f, "`gnuplot --version` returned invalid utf-8" ), |
953 | VersionError::ParseError(msg) => write!( |
954 | f, |
955 | "`gnuplot --version` returned an unparseable version string: {}" , |
956 | msg |
957 | ), |
958 | } |
959 | } |
960 | } |
961 | impl ::std::error::Error for VersionError { |
962 | fn description(&self) -> &str { |
963 | match self { |
964 | VersionError::Exec(_) => "Execution Error" , |
965 | VersionError::Error(_) => "Other Error" , |
966 | VersionError::OutputError => "Output Error" , |
967 | VersionError::ParseError(_) => "Parse Error" , |
968 | } |
969 | } |
970 | |
971 | fn cause(&self) -> Option<&dyn ::std::error::Error> { |
972 | match self { |
973 | VersionError::Exec(err) => Some(err), |
974 | _ => None, |
975 | } |
976 | } |
977 | } |
978 | |
979 | /// Structure representing a gnuplot version number. |
980 | pub struct Version { |
981 | /// The major version number |
982 | pub major: usize, |
983 | /// The minor version number |
984 | pub minor: usize, |
985 | /// The patch level |
986 | pub patch: String, |
987 | } |
988 | |
989 | /// Returns `gnuplot` version |
990 | pub fn version() -> Result<Version, VersionError> { |
991 | let command_output = Command::new("gnuplot" ) |
992 | .arg("--version" ) |
993 | .output() |
994 | .map_err(VersionError::Exec)?; |
995 | if !command_output.status.success() { |
996 | let error = |
997 | String::from_utf8(command_output.stderr).map_err(|_| VersionError::OutputError)?; |
998 | return Err(VersionError::Error(error)); |
999 | } |
1000 | |
1001 | let output = String::from_utf8(command_output.stdout).map_err(|_| VersionError::OutputError)?; |
1002 | |
1003 | parse_version(&output).map_err(|_| VersionError::ParseError(output.clone())) |
1004 | } |
1005 | |
1006 | fn parse_version(version_str: &str) -> Result<Version, Option<ParseIntError>> { |
1007 | let mut words = version_str.split_whitespace().skip(1); |
1008 | let mut version = words.next().ok_or(None)?.split('.' ); |
1009 | let major = version.next().ok_or(None)?.parse()?; |
1010 | let minor = version.next().ok_or(None)?.parse()?; |
1011 | let patchlevel = words.nth(1).ok_or(None)?.to_owned(); |
1012 | |
1013 | Ok(Version { |
1014 | major, |
1015 | minor, |
1016 | patch: patchlevel, |
1017 | }) |
1018 | } |
1019 | |
1020 | fn scale_factor(map: &map::axis::Map<axis::Properties>, axes: Axes) -> (f64, f64) { |
1021 | use crate::Axes::*; |
1022 | use crate::Axis::*; |
1023 | |
1024 | match axes { |
1025 | BottomXLeftY => ( |
1026 | map.get(BottomX).map_or(1., ScaleFactorTrait::scale_factor), |
1027 | map.get(LeftY).map_or(1., ScaleFactorTrait::scale_factor), |
1028 | ), |
1029 | BottomXRightY => ( |
1030 | map.get(BottomX).map_or(1., ScaleFactorTrait::scale_factor), |
1031 | map.get(RightY).map_or(1., ScaleFactorTrait::scale_factor), |
1032 | ), |
1033 | TopXLeftY => ( |
1034 | map.get(TopX).map_or(1., ScaleFactorTrait::scale_factor), |
1035 | map.get(LeftY).map_or(1., ScaleFactorTrait::scale_factor), |
1036 | ), |
1037 | TopXRightY => ( |
1038 | map.get(TopX).map_or(1., ScaleFactorTrait::scale_factor), |
1039 | map.get(RightY).map_or(1., ScaleFactorTrait::scale_factor), |
1040 | ), |
1041 | } |
1042 | } |
1043 | |
1044 | // XXX :-1: to intra-crate privacy rules |
1045 | /// Private |
1046 | trait ScaleFactorTrait { |
1047 | /// Private |
1048 | fn scale_factor(&self) -> f64; |
1049 | } |
1050 | |
1051 | #[cfg (test)] |
1052 | mod test { |
1053 | #[test] |
1054 | fn version() { |
1055 | if let Ok(version) = super::version() { |
1056 | assert!(version.major >= 4); |
1057 | } else { |
1058 | println!("Gnuplot not installed." ); |
1059 | } |
1060 | } |
1061 | |
1062 | #[test] |
1063 | fn test_parse_version_on_valid_string() { |
1064 | let string = "gnuplot 5.0 patchlevel 7" ; |
1065 | let version = super::parse_version(&string).unwrap(); |
1066 | assert_eq!(5, version.major); |
1067 | assert_eq!(0, version.minor); |
1068 | assert_eq!("7" , &version.patch); |
1069 | } |
1070 | |
1071 | #[test] |
1072 | fn test_parse_gentoo_version() { |
1073 | let string = "gnuplot 5.2 patchlevel 5a (Gentoo revision r0)" ; |
1074 | let version = super::parse_version(&string).unwrap(); |
1075 | assert_eq!(5, version.major); |
1076 | assert_eq!(2, version.minor); |
1077 | assert_eq!("5a" , &version.patch); |
1078 | } |
1079 | |
1080 | #[test] |
1081 | fn test_parse_version_returns_error_on_invalid_strings() { |
1082 | let strings = [ |
1083 | "" , |
1084 | "foobar" , |
1085 | "gnuplot 50 patchlevel 7" , |
1086 | "gnuplot 5.0 patchlevel" , |
1087 | "gnuplot foo.bar patchlevel 7" , |
1088 | ]; |
1089 | for string in &strings { |
1090 | assert!(super::parse_version(string).is_err()); |
1091 | } |
1092 | } |
1093 | } |
1094 | |