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
47pub enum ImageFormat {
49 PNG,
50 JPEG,
51 WEBP,
52 SVG,
53 PDF,
54 EPS,
55}
56
57pub trait Trace {
59 fn serialize(&self) -> String;
60}
61
62#[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 pub fn new() -> Plot {
125 Plot {
126 traces: Vec::with_capacity(1),
127 remote_plotly_js: true,
128 ..Default::default()
129 }
130 }
131
132 pub fn use_local_plotly(&mut self) {
135 self.remote_plotly_js = false;
136 }
137
138 pub fn add_trace(&mut self, trace: Box<dyn Trace>) {
140 self.traces.push(trace);
141 }
142
143 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 pub fn set_layout(&mut self, layout: Layout) {
152 self.layout = Some(layout);
153 }
154
155 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 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 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 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 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 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 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 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 pub fn evcxr_display(&self) {
313 self.lab_display();
314 }
315
316 #[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("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}