gnuplot/
figure.rs

1// Copyright (c) 2013-2014 by SiegeLord
2//
3// All rights reserved. Distributed under LGPL 3.0. For full terms see the file LICENSE.
4
5use crate::error_types::*;
6
7use self::AxesVariant::*;
8use crate::axes2d::*;
9use crate::axes3d::*;
10
11use crate::options::{GnuplotVersion, MultiplotFillDirection, MultiplotFillOrder};
12use crate::util::escape;
13use crate::writer::Writer;
14use std::fs::File;
15use std::io::{BufWriter, Write};
16use std::path::{Path, PathBuf};
17use std::process::{Child, Command, Stdio};
18use std::str;
19use tempfile;
20
21enum AxesVariant
22{
23	Axes2DType(Axes2D),
24	Axes3DType(Axes3D),
25	NewPage,
26}
27
28impl AxesVariant
29{
30	fn write_out(
31		&self, data_directory: Option<&str>, writer: &mut dyn Writer, auto_layout: bool,
32		version: GnuplotVersion,
33	)
34	{
35		match *self
36		{
37			Axes2DType(ref a) => a.write_out(data_directory, writer, auto_layout, version),
38			Axes3DType(ref a) => a.write_out(data_directory, writer, auto_layout, version),
39			NewPage =>
40			{
41				writeln!(writer, "unset multiplot");
42				writeln!(writer, "set multiplot");
43			}
44		}
45	}
46
47	fn reset_state(&self, writer: &mut dyn Writer)
48	{
49		match *self
50		{
51			Axes2DType(ref a) => a.reset_state(writer),
52			Axes3DType(ref a) => a.reset_state(writer),
53			_ => (),
54		}
55	}
56}
57
58/// A struct that contains all the multiplot layout options
59struct MultiplotOptions
60{
61	rows: usize,
62	columns: usize,
63	title: Option<String>,
64	scale_x: Option<f32>,
65	scale_y: Option<f32>,
66	offset_x: Option<f32>,
67	offset_y: Option<f32>,
68	fill_order: Option<MultiplotFillOrder>,
69	fill_direction: Option<MultiplotFillDirection>,
70}
71
72impl MultiplotOptions
73{
74	pub fn new() -> MultiplotOptions
75	{
76		MultiplotOptions {
77			rows: 1,
78			columns: 1,
79			title: None,
80			scale_x: None,
81			scale_y: None,
82			offset_x: None,
83			offset_y: None,
84			fill_order: None,
85			fill_direction: None,
86		}
87	}
88}
89
90/// A sentinel that represents a gnuplot waiting to close.
91pub struct CloseSentinel
92{
93	gnuplot: Child,
94}
95
96impl CloseSentinel
97{
98	fn new(gnuplot: Child) -> Self
99	{
100		CloseSentinel { gnuplot }
101	}
102
103	/// Waits until the gnuplot process exits. See `std::process::Child::wait`.
104	pub fn wait(&mut self) -> std::io::Result<std::process::ExitStatus>
105	{
106		self.gnuplot.wait()
107	}
108
109	/// Waits until the gnuplot process exits. See
110	/// `std::process::Child::try_wait`.
111	pub fn try_wait(&mut self) -> std::io::Result<Option<std::process::ExitStatus>>
112	{
113		self.gnuplot.try_wait()
114	}
115}
116
117impl Drop for CloseSentinel
118{
119	fn drop(&mut self)
120	{
121		self.wait().unwrap();
122	}
123}
124
125/// A figure that may contain multiple axes.
126pub struct Figure
127{
128	axes: Vec<AxesVariant>,
129	terminal: String,
130	enhanced_text: bool,
131	output_file: Option<PathBuf>,
132	post_commands: String,
133	pre_commands: String,
134	// RefCell so that we can echo to it
135	gnuplot: Option<Child>,
136	version: Option<GnuplotVersion>,
137	multiplot_options: Option<MultiplotOptions>,
138	data_directory: Option<String>,
139	data_tempdir: Option<tempfile::TempDir>,
140}
141
142impl Default for GnuplotVersion
143{
144	fn default() -> GnuplotVersion
145	{
146		GnuplotVersion { major: 5, minor: 0 }
147	}
148}
149
150impl Default for Figure
151{
152	fn default() -> Self
153	{
154		Self::new()
155	}
156}
157
158impl Figure
159{
160	/// Creates a new figure.
161	pub fn new() -> Figure
162	{
163		let data_tempdir = tempfile::tempdir().ok();
164		Figure {
165			axes: Vec::new(),
166			terminal: "".into(),
167			enhanced_text: true,
168			output_file: None,
169			gnuplot: None,
170			post_commands: "".into(),
171			pre_commands: "".into(),
172			version: None,
173			multiplot_options: None,
174			data_directory: data_tempdir
175				.as_ref()
176				.and_then(|d| d.path().to_str())
177				.map(|s| s.into()),
178			data_tempdir: data_tempdir,
179		}
180	}
181
182	/// Set the directory where to write the data.
183	///
184	/// Gnuplot needs to reify the data before it can be plotted. By default, this is done by
185	/// writing out data files into a temporary directory. This behavior can be restored by passing
186	/// in `Some("".into())`.
187	///
188	/// This can be set to `None`, in which case the data is written inline, without a temporary
189	/// directory. Note that this has somewhat spotty support in gnuplot, so should probably be
190	/// avoided.
191	pub fn set_data_directory(&mut self, data_directory: Option<String>) -> &mut Self
192	{
193		self.data_directory = data_directory;
194		if self
195			.data_directory
196			.as_ref()
197			.map(|s| s == "")
198			.unwrap_or(false)
199		{
200			self.data_directory = self
201				.data_tempdir
202				.as_ref()
203				.and_then(|d| d.path().to_str())
204				.map(|s| s.into())
205		}
206		self
207	}
208
209	/// Sets the terminal for gnuplot to use, as well as the file to output the figure to.
210	/// Terminals that spawn a GUI don't need an output file, so pass an empty string for those.
211	///
212	/// There are a quite a number of terminals, here are some commonly used ones:
213	///
214	/// * wxt - Interactive GUI
215	/// * pdfcairo - Saves the figure as a PDF file
216	/// * epscairo - Saves the figure as a EPS file
217	/// * pngcairo - Saves the figure as a PNG file
218	/// * svg - Saves the figure as a SVG file
219	/// * canvas - Saves the figure as an HTML5 canvas element
220	///
221	/// As of now you can hack the canvas size in by using "pngcairo size 600, 400" for `terminal`.
222	/// Be prepared for that kludge to go away, though.
223	pub fn set_terminal<'l>(&'l mut self, terminal: &str, output_file: &str) -> &'l mut Figure
224	{
225		self.terminal = terminal.into();
226		self.output_file = if output_file.is_empty()
227		{
228			None
229		}
230		else
231		{
232			Some(output_file.into())
233		};
234		self
235	}
236
237	/// Set or unset text enhancements
238	pub fn set_enhanced_text(&mut self, enhanced: bool) -> &mut Figure
239	{
240		self.enhanced_text = enhanced;
241		self
242	}
243
244	/// Sets commands to send to gnuplot after all the plotting commands.
245	pub fn set_post_commands(&mut self, post_commands: &str) -> &mut Figure
246	{
247		self.post_commands = post_commands.into();
248		self
249	}
250
251	/// Sets commands to send to gnuplot before any plotting commands.
252	pub fn set_pre_commands(&mut self, pre_commands: &str) -> &mut Figure
253	{
254		self.pre_commands = pre_commands.into();
255		self
256	}
257
258	/// Sets the Gnuplot version.
259	///
260	/// By default, we assume version 5.0. If `show` is called, it will attempt
261	/// to parse Gnuplot's version string as well.
262	pub fn set_gnuplot_version(&mut self, version: Option<GnuplotVersion>) -> &mut Figure
263	{
264		self.version = version;
265		self
266	}
267
268	/// Returns the Gnuplot version.
269	pub fn get_gnuplot_version(&self) -> GnuplotVersion
270	{
271		self.version.unwrap_or_default()
272	}
273
274	/// Define the layout for the multiple plots
275	/// # Arguments
276	/// * `rows` - Number of rows
277	/// * `columns` - Number of columns
278	pub fn set_multiplot_layout(&mut self, rows: usize, columns: usize) -> &mut Self
279	{
280		let multiplot_options = self
281			.multiplot_options
282			.get_or_insert(MultiplotOptions::new());
283		multiplot_options.rows = rows;
284		multiplot_options.columns = columns;
285
286		self
287	}
288
289	/// Set the multiplot title
290	/// # Arguments
291	/// * `title` - Name of the file
292	pub fn set_title(&mut self, title: &str) -> &mut Self
293	{
294		let multiplot_options = self
295			.multiplot_options
296			.get_or_insert(MultiplotOptions::new());
297		multiplot_options.title = Some(title.into());
298
299		self
300	}
301
302	/// Applies a horizontal and vertical scale to each plot
303	/// # Arguments
304	/// * `scale_x` - Horizonal scale applied to each plot
305	/// * `scale_y` - Vertical scale applied to each plot
306	pub fn set_scale(&mut self, scale_x: f32, scale_y: f32) -> &mut Self
307	{
308		let multiplot_options = self
309			.multiplot_options
310			.get_or_insert(MultiplotOptions::new());
311		multiplot_options.scale_x = Some(scale_x);
312		multiplot_options.scale_y = Some(scale_y);
313
314		self
315	}
316
317	/// Applies a horizontal and vertical offset to each plot
318	/// # Arguments
319	/// * `offset_x` - Horizontal offset applied to each plot
320	/// * `offset_y` - Horizontal offset applied to each plot
321	pub fn set_offset(&mut self, offset_x: f32, offset_y: f32) -> &mut Self
322	{
323		let multiplot_options = self
324			.multiplot_options
325			.get_or_insert(MultiplotOptions::new());
326		multiplot_options.offset_x = Some(offset_x);
327		multiplot_options.offset_y = Some(offset_y);
328
329		self
330	}
331
332	/// Defines the order in which plots fill the layout. Default is RowsFirst and Downwards.
333	/// # Arguments
334	/// * `order` - Options: RowsFirst, ColumnsFirst
335	/// * `direction` - Options: Downwards, Upwards
336	pub fn set_multiplot_fill_order(
337		&mut self, order: MultiplotFillOrder, direction: MultiplotFillDirection,
338	) -> &mut Self
339	{
340		let multiplot_options = self
341			.multiplot_options
342			.get_or_insert(MultiplotOptions::new());
343		multiplot_options.fill_order = Some(order);
344		multiplot_options.fill_direction = Some(direction);
345
346		self
347	}
348
349	/// Creates a set of 2D axes
350	pub fn axes2d(&mut self) -> &mut Axes2D
351	{
352		self.axes.push(Axes2DType(Axes2D::new()));
353		let l = self.axes.len();
354		match self.axes[l - 1]
355		{
356			Axes2DType(ref mut a) => a,
357			_ => unreachable!(),
358		}
359	}
360
361	/// Creates a set of 3D axes
362	pub fn axes3d(&mut self) -> &mut Axes3D
363	{
364		self.axes.push(Axes3DType(Axes3D::new()));
365		let l = self.axes.len();
366		match self.axes[l - 1]
367		{
368			Axes3DType(ref mut a) => a,
369			_ => unreachable!(),
370		}
371	}
372
373	/// Creates a new page.
374	///
375	/// Some terminals support multiple pages or frames, e.g. to create an
376	/// animation. Call this function between sets of plots to indicate that a
377	/// new page should be started. Note that this is implicit before any
378	/// `axes2d`/`axes3d` calls, so make sure to call this only between pages
379	/// (not once before every page).
380	pub fn new_page(&mut self) -> &mut Figure
381	{
382		self.axes.push(NewPage);
383		self
384	}
385
386	/// Launch a gnuplot process, if it hasn't been spawned already by a call to
387	/// this function, and display the figure on it.
388	///
389	/// Usually you should prefer using `show` instead. This method is primarily
390	/// useful when you wish to call this multiple times, e.g. to redraw an
391	/// existing plot window.
392	pub fn show_and_keep_running(&mut self) -> Result<&mut Figure, GnuplotInitError>
393	{
394		if self.axes.is_empty()
395		{
396			return Ok(self);
397		}
398
399		if self.version.is_none()
400		{
401			let output = Command::new("gnuplot").arg("--version").output()?;
402
403			if let Ok(version_string) = str::from_utf8(&output.stdout)
404			{
405				let parts: Vec<_> = version_string.split(|c| c == ' ' || c == '.').collect();
406				if parts.len() > 2 && parts[0] == "gnuplot"
407				{
408					if let (Ok(major), Ok(minor)) =
409						(parts[1].parse::<i32>(), parts[2].parse::<i32>())
410					{
411						self.version = Some(GnuplotVersion { major, minor });
412					}
413				}
414			}
415		}
416
417		if self.gnuplot.is_none()
418		{
419			self.gnuplot = Some(
420				Command::new("gnuplot")
421					.arg("-p")
422					.stdin(Stdio::piped())
423					.spawn()
424					.expect(
425						"Couldn't spawn gnuplot. Make sure it is installed and available in PATH.",
426					),
427			);
428		}
429
430		{
431			let mut gnuplot = self.gnuplot.take();
432			if let Some(p) = gnuplot.as_mut()
433			{
434				let stdin = p.stdin.as_mut().expect("No stdin!?");
435				self.echo(stdin);
436				stdin.flush();
437			};
438			self.gnuplot = gnuplot;
439		}
440
441		Ok(self)
442	}
443
444	/// Launch a gnuplot process, if it hasn't been spawned already and
445	/// display the figure on it.
446	///
447	/// Unlike `show_and_keep_running`, this also instructs gnuplot to close if
448	/// you close all of the plot windows. You can use the returned
449	/// `CloseSentinel` to wait until this happens.
450	pub fn show(&mut self) -> Result<CloseSentinel, GnuplotInitError>
451	{
452		self.show_and_keep_running()?;
453		let mut gnuplot = self.gnuplot.take().expect("No gnuplot?");
454		{
455			let stdin = gnuplot.stdin.as_mut().expect("No stdin!?");
456			writeln!(stdin, "pause mouse close");
457			writeln!(stdin, "quit");
458		};
459		Ok(CloseSentinel::new(gnuplot))
460	}
461
462	/// Save the figure to a png file.
463	///
464	/// # Arguments
465	/// * `filename` - Path to the output file (png)
466	/// * `width_px` - output image width (in pixels)
467	/// * `height_px` - output image height (in pixels)
468	pub fn save_to_png<P: AsRef<Path>>(
469		&mut self, filename: P, width_px: u32, height_px: u32,
470	) -> Result<(), GnuplotInitError>
471	{
472		let former_term = self.terminal.clone();
473		let former_output_file = self.output_file.clone();
474		self.terminal = format!("pngcairo size {},{}", width_px, height_px);
475		self.output_file = Some(filename.as_ref().into());
476		self.show()?;
477		self.terminal = former_term;
478		self.output_file = former_output_file;
479
480		Ok(())
481	}
482
483	/// Save the figure to a svg file.
484	///
485	/// # Arguments
486	/// * `filename` - Path to the output file (svg)
487	/// * `width_px` - output image width (in pixels)
488	/// * `height_px` - output image height (in pixels)
489	pub fn save_to_svg<P: AsRef<Path>>(
490		&mut self, filename: P, width_px: u32, height_px: u32,
491	) -> Result<(), GnuplotInitError>
492	{
493		let former_term = self.terminal.clone();
494		let former_output_file = self.output_file.clone();
495		self.terminal = format!("svg size {},{}", width_px, height_px);
496		self.output_file = Some(filename.as_ref().into());
497		self.show()?;
498		self.terminal = former_term;
499		self.output_file = former_output_file;
500
501		Ok(())
502	}
503
504	/// Save the figure to a pdf file.
505	///
506	/// # Arguments
507	/// * `filename` - Path to the output file (pdf)
508	/// * `width_in` - output image width (in inches)
509	/// * `height_in` - output image height (in inches)
510	pub fn save_to_pdf<P: AsRef<Path>>(
511		&mut self, filename: P, width_in: f32, height_in: f32,
512	) -> Result<(), GnuplotInitError>
513	{
514		let former_term = self.terminal.clone();
515		let former_output_file = self.output_file.clone();
516		self.terminal = format!("pdfcairo size {},{}", width_in, height_in);
517		self.output_file = Some(filename.as_ref().into());
518		self.show()?;
519		self.terminal = former_term;
520		self.output_file = former_output_file;
521
522		Ok(())
523	}
524
525	/// Save the figure to an eps file
526	///
527	/// # Arguments
528	/// * `filename` - Path to the output file (eps)
529	/// * `width_in` - output image width (in inches)
530	/// * `height_in` - output image height (in inches)
531	pub fn save_to_eps<P: AsRef<Path>>(
532		&mut self, filename: P, width_in: f32, height_in: f32,
533	) -> Result<(), GnuplotInitError>
534	{
535		let former_term = self.terminal.clone();
536		let former_output_file = self.output_file.clone();
537		self.terminal = format!("epscairo size {},{}", width_in, height_in);
538		self.output_file = Some(filename.as_ref().into());
539		self.show()?;
540		self.terminal = former_term;
541		self.output_file = former_output_file;
542
543		Ok(())
544	}
545
546	/// Save the figure to a HTML5 canvas file
547	///
548	/// # Arguments
549	/// * `filename` - Path to the output file (canvas)
550	/// * `width_px` - output image width (in pixels)
551	/// * `height_px` - output image height (in pixels)
552	pub fn save_to_canvas<P: AsRef<Path>>(
553		&mut self, filename: P, width_px: u32, height_px: u32,
554	) -> Result<(), GnuplotInitError>
555	{
556		let former_term = self.terminal.clone();
557		let former_output_file = self.output_file.clone();
558		self.terminal = format!("canvas size {},{}", width_px, height_px);
559		self.output_file = Some(filename.as_ref().into());
560		self.show()?;
561		self.terminal = former_term;
562		self.output_file = former_output_file;
563
564		Ok(())
565	}
566
567	/// Closes the gnuplot process.
568	///
569	/// This can be useful if you're your plot output is a file and you need to
570	/// that it was written.
571	pub fn close(&mut self) -> &mut Figure
572	{
573		if self.gnuplot.is_none()
574		{
575			return self;
576		}
577
578		{
579			if let Some(p) = self.gnuplot.as_mut()
580			{
581				{
582					let stdin = p.stdin.as_mut().expect("No stdin!?");
583					writeln!(stdin, "quit");
584				}
585				p.wait();
586			};
587			self.gnuplot = None;
588		}
589
590		self
591	}
592
593	/// Clears all axes on this figure.
594	pub fn clear_axes(&mut self) -> &mut Figure
595	{
596		self.axes.clear();
597		self
598	}
599
600	/// Echo the commands that if piped to a gnuplot process would display the figure
601	/// # Arguments
602	/// * `writer` - A function pointer that will be called multiple times with the command text and data
603	pub fn echo<T: Writer>(&self, writer: &mut T) -> &Figure
604	{
605		let w = writer as &mut dyn Writer;
606		writeln!(w, "{}", &self.pre_commands);
607
608		if self.axes.is_empty()
609		{
610			return self;
611		}
612
613		writeln!(w, "set encoding utf8");
614		if !self.terminal.is_empty()
615		{
616			writeln!(w, "set terminal {}", self.terminal);
617		}
618
619		if let Some(ref output_file) = self.output_file
620		{
621			writeln!(
622				w,
623				"set output \"{}\"",
624				escape(output_file.to_str().unwrap())
625			);
626		}
627
628		writeln!(w, "set termoption dashed");
629		writeln!(
630			w,
631			"set termoption {}",
632			if self.enhanced_text
633			{
634				"enhanced"
635			}
636			else
637			{
638				"noenhanced"
639			}
640		);
641
642		if self.axes.len() > 1 || self.multiplot_options.is_some()
643		{
644			let mut multiplot_options_string = "".to_string();
645			if let Some(m) = &self.multiplot_options
646			{
647				let fill_order = match m.fill_order
648				{
649					None => "",
650					Some(fo) => match fo
651					{
652						MultiplotFillOrder::RowsFirst => " rowsfirst",
653						MultiplotFillOrder::ColumnsFirst => " columnsfirst",
654					},
655				};
656
657				let fill_direction = match m.fill_direction
658				{
659					None => "",
660					Some(fd) => match fd
661					{
662						MultiplotFillDirection::Downwards => " downwards",
663						MultiplotFillDirection::Upwards => " upwards",
664					},
665				};
666
667				let title = m
668					.title
669					.as_ref()
670					.map_or("".to_string(), |t| format!(" title \"{}\"", escape(t)));
671				let scale = m.scale_x.map_or("".to_string(), |s| {
672					format!(" scale {},{}", s, m.scale_y.unwrap())
673				});
674				let offset = m.offset_x.map_or("".to_string(), |o| {
675					format!(" offset {},{}", o, m.offset_y.unwrap())
676				});
677
678				multiplot_options_string = format!(
679					" layout {},{}{}{}{}{}{}",
680					m.rows, m.columns, fill_order, fill_direction, title, scale, offset
681				);
682			}
683
684			writeln!(w, "set multiplot{}", multiplot_options_string);
685		}
686
687		let mut prev_e: Option<&AxesVariant> = None;
688		for (i, e) in self.axes.iter().enumerate()
689		{
690			if let Some(prev_e) = prev_e
691			{
692				prev_e.reset_state(w);
693			}
694			let out_path = self.data_directory.as_ref().and_then(|d| {
695				Path::new(&d)
696					.join(i.to_string())
697					.to_str()
698					.map(|s| s.to_string())
699			});
700			if let Some(out_path) = out_path.as_ref()
701			{
702				std::fs::create_dir_all(out_path).ok();
703			}
704			e.write_out(
705				out_path.as_deref(),
706				w,
707				self.multiplot_options.is_some(),
708				self.get_gnuplot_version(),
709			);
710			prev_e = Some(e);
711		}
712
713		if self.axes.len() > 1 || self.multiplot_options.is_some()
714		{
715			writeln!(w, "unset multiplot");
716		}
717		writeln!(w, "{}", &self.post_commands);
718		self
719	}
720
721	/// Save to a file the the commands that if piped to a gnuplot process would display the figure
722	/// # Arguments
723	/// * `filename` - Name of the file
724	pub fn echo_to_file<P: AsRef<Path>>(&self, filename: P) -> &Figure
725	{
726		if self.axes.is_empty()
727		{
728			return self;
729		}
730
731		let mut file = BufWriter::new(File::create(filename).unwrap());
732		self.echo(&mut file);
733		file.flush();
734		self
735	}
736}
737
738impl Drop for Figure
739{
740	fn drop(&mut self)
741	{
742		self.close();
743	}
744}
745
746#[test]
747fn flush_test()
748{
749	use std::fs;
750	use tempfile::TempDir;
751
752	let tmp_path = TempDir::new().unwrap().into_path();
753	let filename = tmp_path.join("plot.png");
754	let mut fg = Figure::new();
755	fg.axes2d().boxes(0..5, 0..5, &[]);
756	fg.set_terminal("pngcairo", &*filename.to_string_lossy());
757	fg.show();
758	fs::read(filename).unwrap();
759	fs::remove_dir_all(&tmp_path);
760}