plotly_patched/
plot.rs

1#[cfg(feature = "kaleido")]
2extern crate plotly_kaleido;
3
4use askama::Template;
5use rand::{thread_rng, Rng};
6use std::env;
7use std::fs::File;
8use std::io::Write;
9use std::path::{Path, PathBuf};
10use std::process::Command;
11
12use crate::Layout;
13use rand_distr::Alphanumeric;
14
15const PLOTLY_JS: &str = "plotly-1.54.6.min.js";
16
17#[derive(Template)]
18#[template(path = "plotly-1.54.6.min.js", escape = "none")]
19struct PlotlyJs;
20
21#[derive(Template)]
22#[template(path = "plot.html", escape = "none")]
23struct PlotTemplate<'a> {
24    plot_data: &'a str,
25    plotly_javascript: &'a str,
26    remote_plotly_js: bool,
27    export_image: bool,
28    image_type: &'a str,
29    image_width: usize,
30    image_height: usize,
31}
32
33#[derive(Template)]
34#[template(path = "inline_plot.html", escape = "none")]
35struct InlinePlotTemplate<'a> {
36    plot_data: &'a str,
37    plot_div_id: &'a str,
38}
39
40#[derive(Template)]
41#[template(path = "jupyter_notebook_plot.html", escape = "none")]
42struct JupyterNotebookPlotTemplate<'a> {
43    plot_data: &'a str,
44    plot_div_id: &'a str,
45}
46
47/// Image format for static image export.
48pub enum ImageFormat {
49    PNG,
50    JPEG,
51    WEBP,
52    SVG,
53    PDF,
54    EPS,
55}
56
57/// A struct that implements `Trace` can be serialized to json format that is understood by Plotly.js.
58pub trait Trace {
59    fn serialize(&self) -> String;
60}
61
62/// Plot is a container for structs that implement the `Trace` trait. Optionally a `Layout` can
63/// also be specified. Its function is to serialize `Trace`s and the `Layout` in html format and
64/// display and/or persist the resulting plot.
65///
66/// # Examples
67///
68/// ```
69/// extern crate plotly;
70/// use plotly::common::Mode;
71/// use plotly::{Plot, Scatter};
72///
73/// fn line_and_scatter_plot() {
74///     let trace1 = Scatter::new(vec![1, 2, 3, 4], vec![10, 15, 13, 17])
75///         .name("trace1")
76///         .mode(Mode::Markers);
77///     let trace2 = Scatter::new(vec![2, 3, 4, 5], vec![16, 5, 11, 9])
78///         .name("trace2")
79///         .mode(Mode::Lines);
80///     let trace3 = Scatter::new(vec![1, 2, 3, 4], vec![12, 9, 15, 12]).name("trace3");
81///
82///     let mut plot = Plot::new();
83///     plot.add_trace(trace1);
84///     plot.add_trace(trace2);
85///     plot.add_trace(trace3);
86///     plot.show();
87/// }
88///
89/// fn main() -> std::io::Result<()> {
90///     line_and_scatter_plot();
91///     Ok(())
92/// }
93/// ```
94#[derive(Default)]
95pub struct Plot {
96    traces: Vec<Box<dyn Trace>>,
97    layout: Option<Layout>,
98    remote_plotly_js: bool,
99}
100
101const DEFAULT_HTML_APP_NOT_FOUND: &str = r#"Could not find default application for HTML files.
102Consider using the `to_html` method to save the plot instead. If using the `kaleido` feature the
103`save` method can be used to produce a static image in one of the following formats:
104- ImageFormat::PNG
105- ImageFormat::JPEG
106- ImageFormat::WEBP
107- ImageFormat::SVG
108- ImageFormat::PDF
109- ImageFormat::EPS
110
111used as follows:
112let plot = Plot::new();
113...
114let width = 1024;
115let height = 680;
116let scale = 1.0;
117plot.save("filename", ImageFormat::PNG, width, height, scale);
118
119See https://igiagkiozis.github.io/plotly/content/getting_started.html for further details.
120"#;
121
122impl Plot {
123    /// Create a new `Plot`.
124    pub fn new() -> Plot {
125        Plot {
126            traces: Vec::with_capacity(1),
127            remote_plotly_js: true,
128            ..Default::default()
129        }
130    }
131
132    /// This option results in the plotly.js library being written directly in the html output. The benefit is that the
133    /// plot will load faster in the browser and the downside is that the resulting html will be much larger.
134    pub fn use_local_plotly(&mut self) {
135        self.remote_plotly_js = false;
136    }
137
138    /// Add a `Trace` to the `Plot`.
139    pub fn add_trace(&mut self, trace: Box<dyn Trace>) {
140        self.traces.push(trace);
141    }
142
143    /// Add multiple `Trace`s to the `Plot`.
144    pub fn add_traces(&mut self, traces: Vec<Box<dyn Trace>>) {
145        for trace in traces {
146            self.add_trace(trace);
147        }
148    }
149
150    /// Set the `Layout` to be used by `Plot`.
151    pub fn set_layout(&mut self, layout: Layout) {
152        self.layout = Some(layout);
153    }
154
155    /// Renders the contents of the `Plot` and displays them in the system default browser.
156    ///
157    /// This will serialize the `Trace`s and `Layout` in an html page which is saved in the temp
158    /// directory. For example on Linux it will generate a file `plotly_<22 random characters>.html`
159    /// in the /tmp directory.
160    pub fn show(&self) {
161        let rendered = self.render(false, "", 0, 0);
162        let rendered = rendered.as_bytes();
163        let mut temp = env::temp_dir();
164
165        let mut plot_name = rand::thread_rng()
166            .sample_iter(&rand::distributions::Alphanumeric)
167            .take(22)
168            .collect::<String>();
169        plot_name.push_str(".html");
170        plot_name = format!("plotly_{}", plot_name);
171
172        temp.push(plot_name);
173        let temp_path = temp.to_str().unwrap();
174        {
175            let mut file = File::create(temp_path).unwrap();
176            file.write_all(rendered)
177                .expect("failed to write html output");
178            file.flush().unwrap();
179        }
180
181        Plot::show_with_default_app(temp_path);
182    }
183
184    /// Renders the contents of the `Plot`, creates a png raster and displays it in the system default browser.
185    ///
186    /// To save the resulting png right-click on the resulting image and select `Save As...`.
187    pub fn show_png(&self, width: usize, height: usize) {
188        let rendered = self.render(true, "png", width, height);
189        let rendered = rendered.as_bytes();
190        let mut temp = env::temp_dir();
191
192        let mut plot_name = rand::thread_rng()
193            .sample_iter(&rand::distributions::Alphanumeric)
194            .take(22)
195            .collect::<String>();
196        plot_name.push_str(".html");
197
198        temp.push(plot_name);
199        let temp_path = temp.to_str().unwrap();
200        {
201            let mut file = File::create(temp_path).unwrap();
202            file.write_all(rendered)
203                .expect("failed to write html output");
204            file.flush().unwrap();
205        }
206
207        Plot::show_with_default_app(temp_path);
208    }
209
210    /// Renders the contents of the `Plot`, creates a jpeg raster and displays it in the system default browser.
211    ///
212    /// To save the resulting png right-click on the resulting image and select `Save As...`.
213    pub fn show_jpeg(&self, width: usize, height: usize) {
214        let rendered = self.render(true, "jpg", width, height);
215        let rendered = rendered.as_bytes();
216        let mut temp = env::temp_dir();
217
218        let mut plot_name = rand::thread_rng()
219            .sample_iter(&rand::distributions::Alphanumeric)
220            .take(22)
221            .collect::<String>();
222        plot_name.push_str(".html");
223
224        temp.push(plot_name);
225        let temp_path = temp.to_str().unwrap();
226        {
227            let mut file = File::create(temp_path).unwrap();
228            file.write_all(rendered)
229                .expect("failed to write html output");
230            file.flush().unwrap();
231        }
232
233        Plot::show_with_default_app(temp_path);
234    }
235
236    /// Renders the contents of the `Plot` and displays it in the system default browser.
237    ///
238    /// In contrast to `Plot::show()` this will save the resulting html in a user specified location
239    /// instead of the system temp directory.
240    ///
241    /// In contrast to `Plot::write_html`, this will save the resulting html to a file located at a
242    /// user specified location, instead of a writing it to anything that implements `std::io::Write`.
243    pub fn to_html<P: AsRef<Path>>(&self, filename: P) {
244        let mut file = File::create(filename.as_ref()).unwrap();
245
246        self.write_html(&mut file);
247    }
248
249    /// Renders the contents of the `Plot` to HTML and outputs them to a writable buffer.
250    ///
251    /// In contrast to `Plot::to_html`, this will save the resulting html to a byte buffer using the
252    /// `std::io::Write` trait, instead of to a user specified file.
253    pub fn write_html<W: Write>(&self, buffer: &mut W) {
254        let rendered = self.render(false, "", 0, 0);
255        let rendered = rendered.as_bytes();
256
257        buffer
258            .write_all(rendered)
259            .expect("failed to write html output");
260    }
261
262    /// Renders the contents of the `Plot` and returns it as a String, for embedding in
263    /// web-pages or Jupyter notebooks. A `div` is generated with the supplied id followed by the
264    /// script that generates the plot. The assumption is that plotly.js is available within the
265    /// html page that this element is embedded. If that assumption is violated then the plot will
266    /// not be displayed.
267    ///
268    /// If `plot_div_id` is `None` the plot div id will be randomly generated, otherwise the user
269    /// supplied div id is used.
270    pub fn to_inline_html<T: Into<Option<&'static str>>>(&self, plot_div_id: T) -> String {
271        let plot_div_id = plot_div_id.into();
272        match plot_div_id {
273            Some(id) => self.render_inline(id.as_ref()),
274            None => {
275                let rand_id: String = thread_rng().sample_iter(&Alphanumeric).take(20).collect();
276                self.render_inline(rand_id.as_str())
277            }
278        }
279    }
280
281    fn to_jupyter_notebook_html(&self) -> String {
282        let plot_div_id: String = thread_rng().sample_iter(&Alphanumeric).take(20).collect();
283        let plot_data = self.render_plot_data();
284
285        let tmpl = JupyterNotebookPlotTemplate {
286            plot_data: plot_data.as_str(),
287            plot_div_id: plot_div_id.as_str(),
288        };
289        tmpl.render().unwrap()
290    }
291
292    /// Display plot in Jupyter Notebook.
293    pub fn notebook_display(&self) {
294        let plot_data = self.to_jupyter_notebook_html();
295        println!(
296            "EVCXR_BEGIN_CONTENT text/html\n{}\nEVCXR_END_CONTENT",
297            plot_data
298        );
299    }
300
301    /// Display plot in Jupyter Lab.
302    pub fn lab_display(&self) {
303        let plot_data = self.to_json();
304        println!(
305            "EVCXR_BEGIN_CONTENT application/vnd.plotly.v1+json\n{}\nEVCXR_END_CONTENT",
306            plot_data
307        );
308    }
309
310    /// Displays the plot in Jupyter Lab; if running a Jupyter Notebook then use the
311    /// `notebook_display()` method instead.
312    pub fn evcxr_display(&self) {
313        self.lab_display();
314    }
315
316    /// Saves the `Plot` to the selected image format.
317    #[cfg(feature = "kaleido")]
318    pub fn save<P: AsRef<Path>>(
319        &self,
320        filename: P,
321        format: ImageFormat,
322        width: usize,
323        height: usize,
324        scale: f64,
325    ) {
326        let kaleido = plotly_kaleido::Kaleido::new();
327        let plot_data = self.to_json();
328        let image_format = match format {
329            ImageFormat::PNG => "png",
330            ImageFormat::JPEG => "jpeg",
331            ImageFormat::SVG => "svg",
332            ImageFormat::PDF => "pdf",
333            ImageFormat::EPS => "eps",
334            ImageFormat::WEBP => "webp",
335        };
336        kaleido
337            .save(
338                filename.as_ref(),
339                plot_data.as_str(),
340                image_format,
341                width,
342                height,
343                scale,
344            )
345            .unwrap_or_else(|_| panic!("failed to export plot to {:?}", filename.as_ref()));
346    }
347
348    fn plotly_js_path() -> PathBuf {
349        let root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
350        let templates = root.join("templates");
351        templates.join(PLOTLY_JS)
352    }
353
354    fn render_plot_data(&self) -> String {
355        let mut plot_data = String::new();
356        for (idx, trace) in self.traces.iter().enumerate() {
357            let s = trace.serialize();
358            plot_data.push_str(format!("var trace_{} = {};\n", idx, s).as_str());
359        }
360        // plot_data.push_str("\n");
361        plot_data.push_str("var data = [");
362        for idx in 0..self.traces.len() {
363            if idx != self.traces.len() - 1 {
364                plot_data.push_str(format!("trace_{},", idx).as_str());
365            } else {
366                plot_data.push_str(format!("trace_{}", idx).as_str());
367            }
368        }
369        plot_data.push_str("];\n");
370        let layout_data = match &self.layout {
371            Some(layout) => format!("var layout = {};", Trace::serialize(layout)),
372            None => {
373                let mut s = String::from("var layout = {");
374                s.push_str("};");
375                s
376            }
377        };
378        plot_data.push_str(layout_data.as_str());
379        plot_data
380    }
381
382    fn render(
383        &self,
384        export_image: bool,
385        image_type: &str,
386        image_width: usize,
387        image_height: usize,
388    ) -> String {
389        let plot_data = self.render_plot_data();
390        let plotly_js = PlotlyJs {}.render().unwrap();
391        let tmpl = PlotTemplate {
392            plot_data: plot_data.as_str(),
393            plotly_javascript: plotly_js.as_str(),
394            remote_plotly_js: self.remote_plotly_js,
395            export_image,
396            image_type,
397            image_width,
398            image_height,
399        };
400        tmpl.render().unwrap()
401    }
402
403    fn render_inline(&self, plot_div_id: &str) -> String {
404        let plot_data = self.render_plot_data();
405
406        let tmpl = InlinePlotTemplate {
407            plot_data: plot_data.as_str(),
408            plot_div_id,
409        };
410        tmpl.render().unwrap()
411    }
412
413    pub fn to_json(&self) -> String {
414        let mut plot_data: Vec<String> = Vec::new();
415        for trace in self.traces.iter() {
416            let s = trace.serialize();
417            plot_data.push(s);
418        }
419        let layout_data = match &self.layout {
420            Some(layout) => Trace::serialize(layout),
421            None => "{}".to_owned(),
422        };
423
424        let mut json_data = String::new();
425        json_data.push_str(r#"{"data": ["#);
426
427        for (index, data) in plot_data.iter().enumerate() {
428            if index < plot_data.len() - 1 {
429                json_data.push_str(data);
430                json_data.push_str(r#","#);
431            } else {
432                json_data.push_str(data);
433                json_data.push_str("]");
434            }
435        }
436        json_data.push_str(format!(r#", "layout": {}"#, layout_data).as_str());
437        json_data.push_str("}");
438        json_data
439    }
440
441    #[cfg(target_os = "linux")]
442    fn show_with_default_app(temp_path: &str) {
443        Command::new("xdg-open")
444            .args(&[temp_path])
445            .output()
446            .expect(DEFAULT_HTML_APP_NOT_FOUND);
447    }
448
449    #[cfg(target_os = "macos")]
450    fn show_with_default_app(temp_path: &str) {
451        Command::new("open")
452            .args(&[temp_path])
453            .output()
454            .expect(DEFAULT_HTML_APP_NOT_FOUND);
455    }
456
457    #[cfg(target_os = "windows")]
458    fn show_with_default_app(temp_path: &str) {
459        Command::new("cmd")
460            .arg("/C")
461            .arg(format!(r#"start {}"#, temp_path))
462            .output()
463            .expect(DEFAULT_HTML_APP_NOT_FOUND);
464    }
465}
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470    use crate::Scatter;
471
472    fn create_test_plot() -> Plot {
473        let trace1 = Scatter::new(vec![0, 1, 2], vec![6, 10, 2]).name("trace1");
474        let mut plot = Plot::new();
475        plot.add_trace(trace1);
476        plot
477    }
478
479    #[test]
480    fn test_to_json() {
481        let plot = create_test_plot();
482        let plot_json = plot.to_json();
483        println!("{}", plot_json);
484    }
485
486    #[test]
487    fn test_inline_plot() {
488        let plot = create_test_plot();
489        let inline_plot_data = plot.to_inline_html("replace_this_with_the_div_id");
490        assert!(inline_plot_data.contains("replace_this_with_the_div_id"));
491        println!("{}", inline_plot_data);
492        let random_div_id = plot.to_inline_html(None);
493        println!("{}", random_div_id);
494    }
495
496    #[test]
497    fn test_jupyter_notebook_plot() {
498        let plot = create_test_plot();
499        let inline_plot_data = plot.to_jupyter_notebook_html();
500        println!("{}", inline_plot_data);
501    }
502
503    #[test]
504    fn test_notebook_display() {
505        let plot = create_test_plot();
506        plot.notebook_display();
507    }
508
509    #[test]
510    fn test_lab_display() {
511        let plot = create_test_plot();
512        plot.lab_display();
513    }
514
515    #[test]
516    #[cfg(feature = "kaleido")]
517    fn test_save_to_png() {
518        let plot = create_test_plot();
519        let dst = PathBuf::from("example.png");
520        plot.save(&dst, ImageFormat::PNG, 1024, 680, 1.0);
521        assert!(dst.exists());
522        assert!(std::fs::remove_file(&dst).is_ok());
523        assert!(!dst.exists());
524    }
525
526    #[test]
527    #[cfg(feature = "kaleido")]
528    fn test_save_to_jpeg() {
529        let plot = create_test_plot();
530        let dst = PathBuf::from("example.jpeg");
531        plot.save(&dst, ImageFormat::JPEG, 1024, 680, 1.0);
532        assert!(dst.exists());
533        assert!(std::fs::remove_file(&dst).is_ok());
534        assert!(!dst.exists());
535    }
536
537    #[test]
538    #[cfg(feature = "kaleido")]
539    fn test_save_to_svg() {
540        let plot = create_test_plot();
541        let dst = PathBuf::from("example.svg");
542        plot.save(&dst, ImageFormat::SVG, 1024, 680, 1.0);
543        assert!(dst.exists());
544        assert!(std::fs::remove_file(&dst).is_ok());
545        assert!(!dst.exists());
546    }
547
548    #[test]
549    #[ignore]
550    #[cfg(feature = "kaleido")]
551    fn test_save_to_eps() {
552        let plot = create_test_plot();
553        let dst = PathBuf::from("example.eps");
554        plot.save(&dst, ImageFormat::EPS, 1024, 680, 1.0);
555        assert!(dst.exists());
556        assert!(std::fs::remove_file(&dst).is_ok());
557        assert!(!dst.exists());
558    }
559
560    #[test]
561    #[cfg(feature = "kaleido")]
562    fn test_save_to_pdf() {
563        let plot = create_test_plot();
564        let dst = PathBuf::from("example.pdf");
565        plot.save(&dst, ImageFormat::PDF, 1024, 680, 1.0);
566        assert!(dst.exists());
567        assert!(std::fs::remove_file(&dst).is_ok());
568        assert!(!dst.exists());
569    }
570
571    #[test]
572    #[cfg(feature = "kaleido")]
573    fn test_save_to_webp() {
574        let plot = create_test_plot();
575        let dst = PathBuf::from("example.webp");
576        plot.save(&dst, ImageFormat::WEBP, 1024, 680, 1.0);
577        assert!(dst.exists());
578        assert!(std::fs::remove_file(&dst).is_ok());
579        assert!(!dst.exists());
580    }
581}