1use std::{fs::File, io::Write, path::Path};
2
3use askama::Template;
4use dyn_clone::DynClone;
5use erased_serde::Serialize as ErasedSerialize;
6#[cfg(feature = "kaleido")]
7use plotly_kaleido::ImageFormat;
8#[cfg(feature = "plotly_static")]
9use plotly_static::ImageFormat;
10use rand::{
11 distr::{Alphanumeric, SampleString},
12 rng,
13};
14use serde::Serialize;
15
16use crate::{Configuration, Layout};
17
18#[derive(Template)]
19#[template(path = "plot.html", escape = "none")]
20struct PlotTemplate<'a> {
21 plot: &'a Plot,
22 js_scripts: &'a str,
23}
24
25#[cfg(any(feature = "kaleido", feature = "plotly_static"))]
26#[derive(Template)]
27#[template(path = "static_plot.html", escape = "none")]
28#[cfg(all(not(target_family = "wasm"), not(target_os = "android")))]
29struct StaticPlotTemplate<'a> {
30 plot: &'a Plot,
31 format: ImageFormat,
32 js_scripts: &'a str,
33 width: usize,
34 height: usize,
35}
36
37#[derive(Template)]
38#[template(path = "inline_plot.html", escape = "none")]
39struct InlinePlotTemplate<'a> {
40 plot: &'a Plot,
41 plot_div_id: &'a str,
42}
43
44#[derive(Template)]
45#[template(path = "jupyter_notebook_plot.html", escape = "none")]
46struct JupyterNotebookPlotTemplate<'a> {
47 plot: &'a Plot,
48 plot_div_id: &'a str,
49}
50
51#[cfg(all(not(target_family = "wasm"), not(target_os = "android")))]
52const DEFAULT_HTML_APP_NOT_FOUND: &str = r#"Could not find default application for HTML files.
53Consider using the `to_html` method obtain a string representation instead. If using the `kaleido` or `plotly_static` feature the
54`write_image` method can be used to produce a static image in one of the following formats:
55- ImageFormat::PNG
56- ImageFormat::JPEG
57- ImageFormat::WEBP
58- ImageFormat::SVG
59- ImageFormat::PDF
60- ImageFormat::EPS // will be removed in version 0.14.0
61
62Used as follows:
63let plot = Plot::new();
64...
65let width = 1024;
66let height = 680;
67let scale = 1.0;
68plot.write_image("filename", ImageFormat::PNG, width, height, scale);
69
70See https://plotly.github.io/plotly.rs/content/getting_started.html for further details.
71"#;
72
73pub trait Trace: DynClone + ErasedSerialize {
76 fn to_json(&self) -> String;
77}
78
79dyn_clone::clone_trait_object!(Trace);
80erased_serde::serialize_trait_object!(Trace);
81
82#[derive(Default, Serialize, Clone)]
83#[serde(transparent)]
84pub struct Traces {
85 traces: Vec<Box<dyn Trace>>,
86}
87
88impl Traces {
89 pub fn new() -> Self {
90 Self {
91 traces: Vec::with_capacity(1),
92 }
93 }
94
95 pub fn push(&mut self, trace: Box<dyn Trace>) {
96 self.traces.push(trace)
97 }
98
99 pub fn len(&self) -> usize {
100 self.traces.len()
101 }
102
103 pub fn is_empty(&self) -> bool {
104 self.traces.is_empty()
105 }
106
107 pub fn iter(&self) -> std::slice::Iter<'_, Box<dyn Trace>> {
108 self.traces.iter()
109 }
110
111 pub fn to_json(&self) -> String {
112 serde_json::to_string(self).unwrap()
113 }
114}
115
116#[derive(Default, Serialize, Clone)]
155pub struct Plot {
156 #[serde(rename = "data")]
157 traces: Traces,
158 layout: Layout,
159 #[serde(rename = "config")]
160 configuration: Configuration,
161 #[serde(skip)]
162 js_scripts: String,
163}
164
165impl Plot {
166 pub fn new() -> Plot {
168 Plot {
169 traces: Traces::new(),
170 js_scripts: Self::js_scripts(),
171 ..Default::default()
172 }
173 }
174
175 #[cfg(feature = "plotly_embed_js")]
181 pub fn use_cdn_js(&mut self) {
182 self.js_scripts = Self::online_cdn_js();
183 }
184
185 pub fn add_trace(&mut self, trace: Box<dyn Trace>) {
187 self.traces.push(trace);
188 }
189
190 pub fn add_traces(&mut self, traces: Vec<Box<dyn Trace>>) {
192 for trace in traces {
193 self.add_trace(trace);
194 }
195 }
196
197 pub fn set_layout(&mut self, layout: Layout) {
199 self.layout = layout;
200 }
201
202 pub fn set_configuration(&mut self, configuration: Configuration) {
204 self.configuration = configuration;
205 }
206
207 pub fn data(&self) -> &Traces {
209 &self.traces
210 }
211
212 pub fn layout(&self) -> &Layout {
214 &self.layout
215 }
216
217 pub fn configuration(&self) -> &Configuration {
219 &self.configuration
220 }
221
222 #[cfg(all(not(target_family = "wasm"), not(target_os = "android")))]
227 pub fn show(&self) {
228 use std::env;
229
230 let rendered = self.render();
231
232 let mut temp = env::temp_dir();
234 let mut plot_name = Alphanumeric.sample_string(&mut rng(), 22);
235 plot_name.push_str(".html");
236 plot_name = format!("plotly_{plot_name}");
237 temp.push(plot_name);
238
239 let temp_path = temp.to_str().unwrap();
241
242 {
243 let mut file = File::create(temp_path).unwrap();
244 file.write_all(rendered.as_bytes())
245 .expect("failed to write html output");
246 file.flush().unwrap();
247 }
248
249 Plot::show_with_default_app(temp_path);
251 }
252
253 #[cfg(all(not(target_family = "wasm"), not(target_os = "android")))]
259 pub fn show_html<P: AsRef<Path> + std::clone::Clone>(&self, filename: P) {
260 let path = filename.as_ref().to_str().unwrap();
261 self.write_html(filename.clone());
262 Plot::show_with_default_app(path);
264 }
265
266 #[cfg(all(not(target_family = "wasm"), not(target_os = "android")))]
269 #[cfg(any(feature = "kaleido", feature = "plotly_static"))]
270 pub fn show_image(&self, format: ImageFormat, width: usize, height: usize) {
271 use std::env;
272
273 let rendered = self.render_static(&format, width, height);
274
275 let mut temp = env::temp_dir();
277 let mut plot_name = Alphanumeric.sample_string(&mut rng(), 22);
278 plot_name.push_str(".html");
279 plot_name = format!("plotly_{plot_name}");
280 temp.push(plot_name);
281
282 let temp_path = temp.to_str().unwrap();
284
285 {
286 let mut file = File::create(temp_path).unwrap();
287 file.write_all(rendered.as_bytes())
288 .expect("failed to write html output");
289 file.flush().unwrap();
290 }
291
292 Plot::show_with_default_app(temp_path);
294 }
295
296 pub fn write_html<P: AsRef<Path>>(&self, filename: P) {
301 let rendered = self.to_html();
302
303 let mut file =
304 File::create(filename).expect("Provided filepath does not exist or is not accessible");
305 file.write_all(rendered.as_bytes())
306 .expect("failed to write html output");
307 file.flush().unwrap();
308 }
309
310 pub fn to_html(&self) -> String {
316 self.render()
317 }
318
319 pub fn to_inline_html(&self, plot_div_id: Option<&str>) -> String {
333 let plot_div_id = match plot_div_id {
334 Some(id) => id.to_string(),
335 None => Alphanumeric.sample_string(&mut rng(), 20),
336 };
337 self.render_inline(&plot_div_id)
338 }
339
340 fn to_jupyter_notebook_html(&self) -> String {
341 let plot_div_id = Alphanumeric.sample_string(&mut rng(), 20);
342
343 let tmpl = JupyterNotebookPlotTemplate {
344 plot: self,
345 plot_div_id: &plot_div_id,
346 };
347 tmpl.render().unwrap()
348 }
349
350 pub fn notebook_display(&self) {
352 let plot_data = self.to_jupyter_notebook_html();
353 println!("EVCXR_BEGIN_CONTENT text/html\n{plot_data}\nEVCXR_END_CONTENT");
354 }
355
356 pub fn lab_display(&self) {
358 let plot_data = self.to_json();
359 println!(
360 "EVCXR_BEGIN_CONTENT application/vnd.plotly.v1+json\n{plot_data}\nEVCXR_END_CONTENT"
361 );
362 }
363
364 pub fn evcxr_display(&self) {
367 self.lab_display();
368 }
369
370 #[deprecated(
377 since = "0.13.0",
378 note = "kaleido-based implementation is deprecated. Use plotly_static feature instead. The kaleido implementation will be removed in version 0.14.0"
379 )]
380 #[cfg(feature = "kaleido")]
381 pub fn write_image<P: AsRef<Path>>(
382 &self,
383 filename: P,
384 format: ImageFormat,
385 width: usize,
386 height: usize,
387 scale: f64,
388 ) {
389 let kaleido = plotly_kaleido::Kaleido::new();
390 kaleido
391 .save(
392 filename.as_ref(),
393 &serde_json::to_value(self).unwrap(),
394 format,
395 width,
396 height,
397 scale,
398 )
399 .unwrap_or_else(|_| panic!("failed to export plot to {:?}", filename.as_ref()));
400 }
401
402 #[deprecated(
410 since = "0.13.0",
411 note = "kaleido-based implementation is deprecated. Use plotly_static feature instead. The kaleido implementation will be removed in version 0.14.0"
412 )]
413 #[cfg(feature = "kaleido")]
414 pub fn to_base64(
415 &self,
416 format: ImageFormat,
417 width: usize,
418 height: usize,
419 scale: f64,
420 ) -> String {
421 match format {
422 ImageFormat::JPEG | ImageFormat::PNG | ImageFormat::WEBP => {
423 let kaleido = plotly_kaleido::Kaleido::new();
424 kaleido
425 .image_to_string(
426 &serde_json::to_value(self).unwrap(),
427 format,
428 width,
429 height,
430 scale,
431 )
432 .unwrap_or_else(|_| panic!("Kaleido failed to generate image"))
433 }
434 _ => {
435 eprintln!("Cannot generate base64 string for ImageFormat:{format}. Allowed formats are JPEG, PNG, WEBP");
436 String::default()
437 }
438 }
439 }
440
441 #[deprecated(
447 since = "0.13.0",
448 note = "kaleido-based implementation is deprecated. Use plotly_static feature instead. The kaleido implementation will be removed in version 0.14.0"
449 )]
450 #[cfg(feature = "kaleido")]
451 pub fn to_svg(&self, width: usize, height: usize, scale: f64) -> String {
452 let kaleido = plotly_kaleido::Kaleido::new();
453 kaleido
454 .image_to_string(
455 &serde_json::to_value(self).unwrap(),
456 ImageFormat::SVG,
457 width,
458 height,
459 scale,
460 )
461 .unwrap_or_else(|_| panic!("Kaleido failed to generate image"))
462 }
463
464 #[cfg(feature = "plotly_static")]
476 pub fn write_image<P: AsRef<Path>>(
477 &self,
478 filename: P,
479 format: ImageFormat,
480 width: usize,
481 height: usize,
482 scale: f64,
483 ) -> Result<(), Box<dyn std::error::Error>> {
484 let mut exporter = plotly_static::StaticExporterBuilder::default()
485 .build()
486 .map_err(|e| format!("Failed to create StaticExporter: {e}"))?;
487 self.write_image_with_exporter(&mut exporter, filename, format, width, height, scale)
488 }
489
490 #[cfg(feature = "plotly_static")]
505 pub fn to_base64(
506 &self,
507 format: ImageFormat,
508 width: usize,
509 height: usize,
510 scale: f64,
511 ) -> Result<String, Box<dyn std::error::Error>> {
512 let mut exporter = plotly_static::StaticExporterBuilder::default()
513 .build()
514 .map_err(|e| format!("Failed to create StaticExporter: {e}"))?;
515 self.to_base64_with_exporter(&mut exporter, format, width, height, scale)
516 }
517
518 #[cfg(feature = "plotly_static")]
529 pub fn to_svg(
530 &self,
531 width: usize,
532 height: usize,
533 scale: f64,
534 ) -> Result<String, Box<dyn std::error::Error>> {
535 let mut exporter = plotly_static::StaticExporterBuilder::default()
536 .build()
537 .map_err(|e| format!("Failed to create StaticExporter: {e}"))?;
538 self.to_svg_with_exporter(&mut exporter, width, height, scale)
539 }
540
541 #[cfg(feature = "plotly_static")]
578 pub fn write_image_with_exporter<P: AsRef<Path>>(
579 &self,
580 exporter: &mut plotly_static::StaticExporter,
581 filename: P,
582 format: ImageFormat,
583 width: usize,
584 height: usize,
585 scale: f64,
586 ) -> Result<(), Box<dyn std::error::Error>> {
587 exporter.write_fig(
588 filename.as_ref(),
589 &serde_json::to_value(self)?,
590 format,
591 width,
592 height,
593 scale,
594 )
595 }
596
597 #[cfg(feature = "plotly_static")]
633 pub fn to_base64_with_exporter(
634 &self,
635 exporter: &mut plotly_static::StaticExporter,
636 format: ImageFormat,
637 width: usize,
638 height: usize,
639 scale: f64,
640 ) -> Result<String, Box<dyn std::error::Error>> {
641 match format {
642 ImageFormat::JPEG | ImageFormat::PNG | ImageFormat::WEBP => {
643 exporter.write_to_string(
644 &serde_json::to_value(self)?,
645 format,
646 width,
647 height,
648 scale,
649 )
650 }
651 _ => {
652 Err(format!("Cannot generate base64 string for ImageFormat:{format}. Allowed formats are JPEG, PNG, WEBP").into())
653 }
654 }
655 }
656
657 #[cfg(feature = "plotly_static")]
691 pub fn to_svg_with_exporter(
692 &self,
693 exporter: &mut plotly_static::StaticExporter,
694 width: usize,
695 height: usize,
696 scale: f64,
697 ) -> Result<String, Box<dyn std::error::Error>> {
698 exporter.write_to_string(
699 &serde_json::to_value(self)?,
700 ImageFormat::SVG,
701 width,
702 height,
703 scale,
704 )
705 }
706
707 fn render(&self) -> String {
708 let tmpl = PlotTemplate {
709 plot: self,
710 js_scripts: &self.js_scripts,
711 };
712 tmpl.render().unwrap()
713 }
714
715 #[cfg(all(not(target_family = "wasm"), not(target_os = "android")))]
716 #[cfg(any(feature = "kaleido", feature = "plotly_static"))]
717 pub fn render_static(&self, format: &ImageFormat, width: usize, height: usize) -> String {
718 let tmpl = StaticPlotTemplate {
719 plot: self,
720 format: format.clone(),
721 js_scripts: &self.js_scripts,
722 width,
723 height,
724 };
725 tmpl.render().unwrap()
726 }
727
728 fn render_inline(&self, plot_div_id: &str) -> String {
729 let tmpl = InlinePlotTemplate {
730 plot: self,
731 plot_div_id,
732 };
733 tmpl.render().unwrap()
734 }
735
736 fn js_scripts() -> String {
737 if cfg!(feature = "plotly_embed_js") {
738 Self::offline_js_sources()
739 } else {
740 Self::online_cdn_js()
741 }
742 }
743
744 pub fn offline_js_sources() -> String {
756 let local_tex_svg_js = include_str!("../resource/tex-svg-3.2.2.js");
759 let local_plotly_js = include_str!("../resource/plotly.min.js");
760
761 format!(
762 "<script type=\"text/javascript\">{local_plotly_js}</script>\n
763 <script type=\"text/javascript\">{local_tex_svg_js}</script>\n",
764 )
765 .to_string()
766 }
767
768 pub fn online_cdn_js() -> String {
781 r##"<script src="https://cdn.jsdelivr.net/npm/mathjax@3.2.2/es5/tex-svg.js"></script>
784 <script src="https://cdn.plot.ly/plotly-3.0.1.min.js"></script>
785 "##
786 .to_string()
787 }
788
789 pub fn to_json(&self) -> String {
790 serde_json::to_string(self).unwrap()
791 }
792
793 #[cfg(target_family = "wasm")]
794 pub fn to_js_object(&self) -> wasm_bindgen_futures::js_sys::Object {
796 use wasm_bindgen_futures::js_sys;
797 use wasm_bindgen_futures::wasm_bindgen::JsCast;
798 js_sys::JSON::parse(&self.to_json())
802 .expect("Invalid JSON")
803 .dyn_into::<js_sys::Object>()
804 .expect("Invalid JSON structure - expected a top-level Object")
805 }
806
807 #[cfg(all(unix, not(target_os = "android"), not(target_os = "macos")))]
808 fn show_with_default_app(temp_path: &str) {
809 use std::process::Command;
810 Command::new("xdg-open")
811 .args([temp_path])
812 .output()
813 .expect(DEFAULT_HTML_APP_NOT_FOUND);
814 }
815
816 #[cfg(target_os = "macos")]
817 fn show_with_default_app(temp_path: &str) {
818 use std::process::Command;
819 Command::new("open")
820 .args([temp_path])
821 .output()
822 .expect(DEFAULT_HTML_APP_NOT_FOUND);
823 }
824
825 #[cfg(target_os = "windows")]
826 fn show_with_default_app(temp_path: &str) {
827 use std::process::Command;
828 Command::new("cmd")
829 .args(&["/C", "start", &format!(r#"{}"#, temp_path)])
830 .spawn()
831 .expect(DEFAULT_HTML_APP_NOT_FOUND);
832 }
833}
834
835impl PartialEq for Plot {
836 fn eq(&self, other: &Self) -> bool {
837 self.to_json() == other.to_json()
838 }
839}
840
841#[cfg(test)]
842mod tests {
843 use std::path::PathBuf;
844 use std::sync::atomic::{AtomicU32, Ordering};
845
846 #[cfg(feature = "kaleido")]
847 use plotly_kaleido::ImageFormat;
848 #[cfg(feature = "plotly_static")]
849 use plotly_static::ImageFormat;
850 use serde_json::{json, to_value};
851 #[cfg(any(feature = "kaleido", feature = "plotly_static"))]
852 use {base64::engine::general_purpose, base64::Engine};
853
854 use super::*;
855 use crate::Scatter;
856
857 fn create_test_plot() -> Plot {
858 let trace1 = Scatter::new(vec![0, 1, 2], vec![6, 10, 2]).name("trace1");
859 let mut plot = Plot::new();
860 plot.add_trace(trace1);
861 plot
862 }
863
864 #[test]
865 fn inline_plot() {
866 let plot = create_test_plot();
867 let inline_plot_data = plot.to_inline_html(Some("replace_this_with_the_div_id"));
868 assert!(inline_plot_data.contains("replace_this_with_the_div_id"));
869 plot.to_inline_html(None);
870 }
871
872 #[test]
873 fn jupyter_notebook_plot() {
874 let plot = create_test_plot();
875 plot.to_jupyter_notebook_html();
876 }
877
878 #[test]
879 fn notebook_display() {
880 let plot = create_test_plot();
881 plot.notebook_display();
882 }
883
884 #[test]
885 fn lab_display() {
886 let plot = create_test_plot();
887 plot.lab_display();
888 }
889
890 #[test]
891 fn plot_serialize_simple() {
892 let plot = create_test_plot();
893 let expected = json!({
894 "data": [
895 {
896 "type": "scatter",
897 "name": "trace1",
898 "x": [0, 1, 2],
899 "y": [6, 10, 2]
900 }
901 ],
902 "layout": {},
903 "config": {},
904 });
905
906 assert_eq!(to_value(plot).unwrap(), expected);
907 }
908
909 #[test]
910 fn plot_serialize_with_layout() {
911 let mut plot = create_test_plot();
912 let layout = Layout::new().title("Title");
913 plot.set_layout(layout);
914
915 let expected = json!({
916 "data": [
917 {
918 "type": "scatter",
919 "name": "trace1",
920 "x": [0, 1, 2],
921 "y": [6, 10, 2]
922 }
923 ],
924 "layout": {
925 "title": {
926 "text": "Title"
927 }
928 },
929 "config": {},
930 });
931
932 assert_eq!(to_value(plot).unwrap(), expected);
933 }
934
935 #[test]
936 fn data_to_json() {
937 let plot = create_test_plot();
938 let expected = json!([
939 {
940 "type": "scatter",
941 "name": "trace1",
942 "x": [0, 1, 2],
943 "y": [6, 10, 2]
944 }
945 ]);
946
947 assert_eq!(to_value(plot.data()).unwrap(), expected);
948 }
949
950 #[test]
951 fn empty_layout_to_json() {
952 let plot = create_test_plot();
953 let expected = json!({});
954
955 assert_eq!(to_value(plot.layout()).unwrap(), expected);
956 }
957
958 #[test]
959 fn layout_to_json() {
960 let mut plot = create_test_plot();
961 let layout = Layout::new().title("TestTitle");
962 plot.set_layout(layout);
963
964 let expected = json!({
965 "title": {"text": "TestTitle"}
966 });
967
968 assert_eq!(to_value(plot.layout()).unwrap(), expected);
969 }
970
971 #[test]
972 fn plot_eq() {
973 let plot1 = create_test_plot();
974 let plot2 = create_test_plot();
975
976 assert!(plot1 == plot2);
977 }
978
979 #[test]
980 fn plot_neq() {
981 let plot1 = create_test_plot();
982 let trace2 = Scatter::new(vec![10, 1, 2], vec![6, 10, 2]).name("trace2");
983 let mut plot2 = Plot::new();
984 plot2.add_trace(trace2);
985
986 assert!(plot1 != plot2);
987 }
988
989 #[test]
990 fn plot_clone() {
991 let plot1 = create_test_plot();
992 let plot2 = plot1.clone();
993
994 assert!(plot1 == plot2);
995 }
996
997 #[test]
998 fn save_html() {
999 let plot = create_test_plot();
1000 let dst = PathBuf::from("plotly_example.html");
1001 plot.write_html(&dst);
1002 assert!(dst.exists());
1003 #[cfg(not(feature = "debug"))]
1004 assert!(std::fs::remove_file(&dst).is_ok());
1005 }
1006
1007 #[cfg(feature = "plotly_static")]
1008 static PORT_COUNTER: AtomicU32 = AtomicU32::new(4444);
1010
1011 #[cfg(feature = "plotly_static")]
1012 fn get_unique_port() -> u32 {
1013 PORT_COUNTER.fetch_add(1, Ordering::SeqCst)
1014 }
1015
1016 #[test]
1017 #[cfg(feature = "plotly_static")]
1018 fn save_to_png() {
1019 let plot = create_test_plot();
1020 let dst = PathBuf::from("plotly_example.png");
1021 let mut exporter = plotly_static::StaticExporterBuilder::default()
1022 .webdriver_port(get_unique_port())
1023 .build()
1024 .unwrap();
1025 plot.write_image_with_exporter(&mut exporter, &dst, ImageFormat::PNG, 1024, 680, 1.0)
1026 .unwrap();
1027 assert!(dst.exists());
1028 let metadata = std::fs::metadata(&dst).expect("Could not retrieve file metadata");
1029 let file_size = metadata.len();
1030 assert!(file_size > 0,);
1031 #[cfg(not(feature = "debug"))]
1032 assert!(std::fs::remove_file(&dst).is_ok());
1033 }
1034
1035 #[test]
1036 #[cfg(feature = "plotly_static")]
1037 fn save_to_jpeg() {
1038 let plot = create_test_plot();
1039 let dst = PathBuf::from("plotly_example.jpeg");
1040 let mut exporter = plotly_static::StaticExporterBuilder::default()
1041 .webdriver_port(get_unique_port())
1042 .build()
1043 .unwrap();
1044 plot.write_image_with_exporter(&mut exporter, &dst, ImageFormat::JPEG, 1024, 680, 1.0)
1045 .unwrap();
1046 assert!(dst.exists());
1047 let metadata = std::fs::metadata(&dst).expect("Could not retrieve file metadata");
1048 let file_size = metadata.len();
1049 assert!(file_size > 0,);
1050 #[cfg(not(feature = "debug"))]
1051 assert!(std::fs::remove_file(&dst).is_ok());
1052 }
1053
1054 #[test]
1055 #[cfg(feature = "plotly_static")]
1056 fn save_to_svg() {
1057 let plot = create_test_plot();
1058 let dst = PathBuf::from("plotly_example.svg");
1059 let mut exporter = plotly_static::StaticExporterBuilder::default()
1060 .webdriver_port(get_unique_port())
1061 .build()
1062 .unwrap();
1063 plot.write_image_with_exporter(&mut exporter, &dst, ImageFormat::SVG, 1024, 680, 1.0)
1064 .unwrap();
1065 assert!(dst.exists());
1066 let metadata = std::fs::metadata(&dst).expect("Could not retrieve file metadata");
1067 let file_size = metadata.len();
1068 assert!(file_size > 0,);
1069 #[cfg(not(feature = "debug"))]
1070 assert!(std::fs::remove_file(&dst).is_ok());
1071 }
1072
1073 #[test]
1074 #[cfg(feature = "plotly_static")]
1075 fn save_to_pdf() {
1076 let plot = create_test_plot();
1077 let dst = PathBuf::from("plotly_example.pdf");
1078 #[cfg(feature = "debug")]
1079 let mut exporter = plotly_static::StaticExporterBuilder::default()
1080 .spawn_webdriver(true)
1081 .webdriver_port(get_unique_port())
1082 .pdf_export_timeout(750)
1083 .build()
1084 .unwrap();
1085 #[cfg(not(feature = "debug"))]
1086 let mut exporter = plotly_static::StaticExporterBuilder::default()
1087 .webdriver_port(get_unique_port())
1088 .build()
1089 .unwrap();
1090 plot.write_image_with_exporter(&mut exporter, &dst, ImageFormat::PDF, 1024, 680, 1.0)
1091 .unwrap();
1092 assert!(dst.exists());
1093 let metadata = std::fs::metadata(&dst).expect("Could not retrieve file metadata");
1094 let file_size = metadata.len();
1095 assert!(file_size > 0,);
1096 #[cfg(not(feature = "debug"))]
1097 assert!(std::fs::remove_file(&dst).is_ok());
1098 }
1099
1100 #[test]
1101 #[cfg(feature = "plotly_static")]
1102 fn save_to_webp() {
1103 let plot = create_test_plot();
1104 let dst = PathBuf::from("plotly_example.webp");
1105 let mut exporter = plotly_static::StaticExporterBuilder::default()
1106 .webdriver_port(get_unique_port())
1107 .build()
1108 .unwrap();
1109 plot.write_image_with_exporter(&mut exporter, &dst, ImageFormat::WEBP, 1024, 680, 1.0)
1110 .unwrap();
1111 assert!(dst.exists());
1112 let metadata = std::fs::metadata(&dst).expect("Could not retrieve file metadata");
1113 let file_size = metadata.len();
1114 assert!(file_size > 0,);
1115 #[cfg(not(feature = "debug"))]
1116 assert!(std::fs::remove_file(&dst).is_ok());
1117 }
1118
1119 #[test]
1120 #[cfg(feature = "plotly_static")]
1121 fn image_to_base64() {
1122 let plot = create_test_plot();
1123 let mut exporter = plotly_static::StaticExporterBuilder::default()
1124 .webdriver_port(get_unique_port())
1125 .build()
1126 .unwrap();
1127
1128 let image_base64 = plot
1129 .to_base64_with_exporter(&mut exporter, ImageFormat::PNG, 200, 150, 1.0)
1130 .unwrap();
1131
1132 assert!(!image_base64.is_empty());
1133
1134 let result_decoded = general_purpose::STANDARD.decode(image_base64).unwrap();
1135 let expected = "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";
1136 let expected_decoded = general_purpose::STANDARD.decode(expected).unwrap();
1137
1138 assert_eq!(expected_decoded[..2], result_decoded[..2]);
1142 }
1143
1144 #[test]
1145 #[cfg(feature = "plotly_static")]
1146 fn image_to_svg_string() {
1147 let plot = create_test_plot();
1148 let mut exporter = plotly_static::StaticExporterBuilder::default()
1149 .webdriver_port(get_unique_port())
1150 .build()
1151 .unwrap();
1152 let image_svg = plot
1153 .to_svg_with_exporter(&mut exporter, 200, 150, 1.0)
1154 .unwrap();
1155
1156 assert!(!image_svg.is_empty());
1157
1158 let expected = "<svg class=\"main-svg\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"200\" height=\"150\" style=\"\" viewBox=\"0 0 200 150\"><rect x=\"0\" y=\"0\" width=\"200\" height=\"150\" style=\"fill: rgb(255, 255, 255); fill-opacity: 1;\"/><defs id=\"defs-2dc70a\"><g class=\"clips\"><clipPath id=\"clip2dc70axyplot\" class=\"plotclip\"><rect width=\"40\" height=\"2\"/></clipPath><clipPath class=\"axesclip\" id=\"clip2dc70ax\"><rect x=\"80\" y=\"0\" width=\"40\" height=\"150\"/></clipPath><clipPath class=\"axesclip\" id=\"clip2dc70ay\"><rect x=\"0\" y=\"82\" width=\"200\" height=\"2\"/></clipPath><clipPath class=\"axesclip\" id=\"clip2dc70axy\"><rect x=\"80\" y=\"82\" width=\"40\" height=\"2\"/></clipPath></g><g class=\"gradients\"/></defs><g class=\"bglayer\"/><g class=\"layer-below\"><g class=\"imagelayer\"/><g class=\"shapelayer\"/></g><g class=\"cartesianlayer\"><g class=\"subplot xy\"><g class=\"layer-subplot\"><g class=\"shapelayer\"/><g class=\"imagelayer\"/></g><g class=\"gridlayer\"><g class=\"x\"><path class=\"xgrid crisp\" transform=\"translate(100,0)\" d=\"M0,82v2\" style=\"stroke: rgb(238, 238, 238); stroke-opacity: 1; stroke-width: 1px;\"/><path class=\"xgrid crisp\" transform=\"translate(114.25,0)\" d=\"M0,82v2\" style=\"stroke: rgb(238, 238, 238); stroke-opacity: 1; stroke-width: 1px;\"/></g><g class=\"y\"/></g><g class=\"zerolinelayer\"><path class=\"xzl zl crisp\" transform=\"translate(85.75,0)\" d=\"M0,82v2\" style=\"stroke: rgb(68, 68, 68); stroke-opacity: 1; stroke-width: 1px;\"/></g><path class=\"xlines-below\"/><path class=\"ylines-below\"/><g class=\"overlines-below\"/><g class=\"xaxislayer-below\"/><g class=\"yaxislayer-below\"/><g class=\"overaxes-below\"/><g class=\"plot\" transform=\"translate(80,82)\" clip-path=\"url('#clip2dc70axyplot')\"><g class=\"scatterlayer mlayer\"><g class=\"trace scatter trace86f735\" style=\"stroke-miterlimit: 2; opacity: 1;\"><g class=\"fills\"/><g class=\"errorbars\"/><g class=\"lines\"><path class=\"js-line\" d=\"M5.75,1L20,0L34.25,2\" style=\"vector-effect: non-scaling-stroke; fill: none; stroke: rgb(31, 119, 180); stroke-opacity: 1; stroke-width: 2px; opacity: 1;\"/></g><g class=\"points\"><path class=\"point\" transform=\"translate(5.75,1)\" d=\"M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z\" style=\"opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;\"/><path class=\"point\" transform=\"translate(20,0)\" d=\"M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z\" style=\"opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;\"/><path class=\"point\" transform=\"translate(34.25,2)\" d=\"M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z\" style=\"opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;\"/></g><g class=\"text\"/></g></g></g><g class=\"overplot\"/><path class=\"xlines-above crisp\" d=\"M0,0\" style=\"fill: none;\"/><path class=\"ylines-above crisp\" d=\"M0,0\" style=\"fill: none;\"/><g class=\"overlines-above\"/><g class=\"xaxislayer-above\"><g class=\"xtick\"><text text-anchor=\"middle\" x=\"0\" y=\"97\" transform=\"translate(85.75,0)\" style=\"font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(68, 68, 68); fill-opacity: 1; white-space: pre;\">0</text></g><g class=\"xtick\"><text text-anchor=\"middle\" x=\"0\" y=\"97\" transform=\"translate(100,0)\" style=\"font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(68, 68, 68); fill-opacity: 1; white-space: pre;\">1</text></g><g class=\"xtick\"><text text-anchor=\"middle\" x=\"0\" y=\"97\" transform=\"translate(114.25,0)\" style=\"font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(68, 68, 68); fill-opacity: 1; white-space: pre;\">2</text></g></g><g class=\"yaxislayer-above\"><g class=\"ytick\"><text text-anchor=\"end\" x=\"79\" y=\"4.199999999999999\" transform=\"translate(0,84)\" style=\"font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(68, 68, 68); fill-opacity: 1; white-space: pre;\">2</text></g><g class=\"ytick\"><text text-anchor=\"end\" x=\"79\" y=\"4.199999999999999\" transform=\"translate(0,83.5)\" style=\"font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(68, 68, 68); fill-opacity: 1; white-space: pre;\">4</text></g><g class=\"ytick\"><text text-anchor=\"end\" x=\"79\" y=\"4.199999999999999\" transform=\"translate(0,83)\" style=\"font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(68, 68, 68); fill-opacity: 1; white-space: pre;\">6</text></g><g class=\"ytick\"><text text-anchor=\"end\" x=\"79\" y=\"4.199999999999999\" transform=\"translate(0,82.5)\" style=\"font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(68, 68, 68); fill-opacity: 1; white-space: pre;\">8</text></g><g class=\"ytick\"><text text-anchor=\"end\" x=\"79\" y=\"4.199999999999999\" transform=\"translate(0,82)\" style=\"font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(68, 68, 68); fill-opacity: 1; white-space: pre;\">10</text></g></g><g class=\"overaxes-above\"/></g></g><g class=\"polarlayer\"/><g class=\"ternarylayer\"/><g class=\"geolayer\"/><g class=\"funnelarealayer\"/><g class=\"pielayer\"/><g class=\"treemaplayer\"/><g class=\"sunburstlayer\"/><g class=\"glimages\"/><defs id=\"topdefs-2dc70a\"><g class=\"clips\"/></defs><g class=\"layer-above\"><g class=\"imagelayer\"/><g class=\"shapelayer\"/></g><g class=\"infolayer\"><g class=\"g-gtitle\"/><g class=\"g-xtitle\"/><g class=\"g-ytitle\"/></g></svg>";
1159 const LEN: usize = 10;
1162 assert_eq!(expected[..LEN], image_svg[..LEN]);
1163 }
1164
1165 #[test]
1166 #[cfg(feature = "plotly_static")]
1167 fn save_surface_to_png() {
1168 use crate::Surface;
1169 let mut plot = Plot::new();
1170 let z_matrix = vec![
1171 vec![1.0, 2.0, 3.0],
1172 vec![4.0, 5.0, 6.0],
1173 vec![7.0, 8.0, 9.0],
1174 ];
1175 let x_unique = vec![1.0, 2.0, 3.0];
1176 let y_unique = vec![4.0, 5.0, 6.0];
1177 let surface = Surface::new(z_matrix)
1178 .x(x_unique)
1179 .y(y_unique)
1180 .name("Surface");
1181
1182 plot.add_trace(surface);
1183 let dst = PathBuf::from("plotly_example_surface.png");
1184 let mut exporter = plotly_static::StaticExporterBuilder::default()
1185 .webdriver_port(get_unique_port())
1186 .build()
1187 .unwrap();
1188
1189 assert!(!plot
1190 .to_base64_with_exporter(&mut exporter, ImageFormat::PNG, 1024, 680, 1.0)
1191 .unwrap()
1192 .is_empty());
1193
1194 plot.write_image_with_exporter(&mut exporter, &dst, ImageFormat::PNG, 800, 600, 1.0)
1195 .unwrap();
1196 assert!(dst.exists());
1197
1198 let metadata = std::fs::metadata(&dst).expect("Could not retrieve file metadata");
1199 let file_size = metadata.len();
1200 assert!(file_size > 0,);
1201 #[cfg(not(feature = "debug"))]
1202 assert!(std::fs::remove_file(&dst).is_ok());
1203 }
1204}