1use std::sync::atomic::{AtomicU64, Ordering};
2use std::time::{SystemTime, UNIX_EPOCH};
3use std::{fs::File, io::Write, path::Path};
4
5use askama::Template;
6use dyn_clone::DynClone;
7use erased_serde::Serialize as ErasedSerialize;
8#[cfg(feature = "kaleido")]
9use plotly_kaleido::ImageFormat;
10#[cfg(feature = "plotly_static")]
11use plotly_static::ImageFormat;
12use rand::{
13 distr::{Alphanumeric, SampleString},
14 rngs::SmallRng,
15 SeedableRng,
16};
17use serde::Serialize;
18
19use crate::{layout::Frame, Configuration, Layout};
20
21static SEED_COUNTER: AtomicU64 = AtomicU64::new(0);
22
23#[derive(Template)]
24#[template(path = "plot.html", escape = "none")]
25struct PlotTemplate<'a> {
26 plot: &'a Plot,
27 js_scripts: &'a str,
28}
29
30#[cfg(any(feature = "kaleido", feature = "plotly_static"))]
31#[derive(Template)]
32#[template(path = "static_plot.html", escape = "none")]
33#[cfg(all(not(target_family = "wasm"), not(target_os = "android")))]
34struct StaticPlotTemplate<'a> {
35 plot: &'a Plot,
36 format: ImageFormat,
37 js_scripts: &'a str,
38 width: usize,
39 height: usize,
40}
41
42#[derive(Template)]
43#[template(path = "inline_plot.html", escape = "none")]
44struct InlinePlotTemplate<'a> {
45 plot: &'a Plot,
46 plot_div_id: &'a str,
47}
48
49#[derive(Template)]
50#[template(path = "jupyter_notebook_plot.html", escape = "none")]
51struct JupyterNotebookPlotTemplate<'a> {
52 plot: &'a Plot,
53 plot_div_id: &'a str,
54}
55
56#[cfg(all(not(target_family = "wasm"), not(target_os = "android")))]
57const DEFAULT_HTML_APP_NOT_FOUND: &str = r#"Could not find default application for HTML files.
58Consider using the `to_html` method obtain a string representation instead. If using the `kaleido` or `plotly_static` feature the
59`write_image` method can be used to produce a static image in one of the following formats:
60- ImageFormat::PNG
61- ImageFormat::JPEG
62- ImageFormat::WEBP
63- ImageFormat::SVG
64- ImageFormat::PDF
65- ImageFormat::EPS // will be removed in version 0.15.0
66
67Used as follows:
68let plot = Plot::new();
69...
70let width = 1024;
71let height = 680;
72let scale = 1.0;
73plot.write_image("filename", ImageFormat::PNG, width, height, scale);
74
75See https://plotly.github.io/plotly.rs/content/getting_started.html for further details.
76"#;
77
78pub trait Trace: DynClone + ErasedSerialize {
81 fn to_json(&self) -> String;
82}
83
84dyn_clone::clone_trait_object!(Trace);
85erased_serde::serialize_trait_object!(Trace);
86
87#[derive(Default, Serialize, Clone)]
88#[serde(transparent)]
89pub struct Traces {
90 traces: Vec<Box<dyn Trace>>,
91}
92
93impl Traces {
94 pub fn new() -> Self {
95 Self {
96 traces: Vec::with_capacity(1),
97 }
98 }
99
100 pub fn push(&mut self, trace: Box<dyn Trace>) {
101 self.traces.push(trace)
102 }
103
104 pub fn len(&self) -> usize {
105 self.traces.len()
106 }
107
108 pub fn is_empty(&self) -> bool {
109 self.traces.is_empty()
110 }
111
112 pub fn iter(&self) -> std::slice::Iter<'_, Box<dyn Trace>> {
113 self.traces.iter()
114 }
115
116 pub fn to_json(&self) -> String {
117 serde_json::to_string(self).unwrap()
118 }
119}
120
121#[derive(Serialize, Clone)]
160pub struct Plot {
161 #[serde(rename = "data")]
162 traces: Traces,
163 layout: Layout,
164 #[serde(rename = "config")]
165 configuration: Configuration,
166 frames: Option<Vec<Frame>>,
168 #[serde(skip)]
169 js_scripts: String,
170}
171
172impl Default for Plot {
173 fn default() -> Self {
174 Self::new()
175 }
176}
177
178impl Plot {
179 pub fn new() -> Plot {
181 Plot {
182 traces: Traces::new(),
183 layout: Layout::default(),
184 configuration: Configuration::default(),
185 frames: None,
186 js_scripts: Self::js_scripts(),
187 }
188 }
189
190 #[cfg(feature = "plotly_embed_js")]
196 pub fn use_cdn_js(&mut self) {
197 self.js_scripts = Self::online_cdn_js();
198 }
199
200 pub fn add_trace(&mut self, trace: Box<dyn Trace>) {
202 self.traces.push(trace);
203 }
204
205 pub fn add_traces(&mut self, traces: Vec<Box<dyn Trace>>) {
207 for trace in traces {
208 self.add_trace(trace);
209 }
210 }
211
212 pub fn set_layout(&mut self, layout: Layout) {
214 self.layout = layout;
215 }
216
217 pub fn set_configuration(&mut self, configuration: Configuration) {
219 self.configuration = configuration;
220 }
221
222 pub fn data(&self) -> &Traces {
224 &self.traces
225 }
226
227 pub fn layout(&self) -> &Layout {
229 &self.layout
230 }
231
232 pub fn configuration(&self) -> &Configuration {
234 &self.configuration
235 }
236
237 pub fn add_frame(&mut self, frame: Frame) -> &mut Self {
239 if self.frames.is_none() {
240 self.frames = Some(Vec::new());
241 }
242 self.frames.as_mut().unwrap().push(frame);
243 self
244 }
245
246 pub fn add_frames(&mut self, frames: &[Frame]) -> &mut Self {
248 if self.frames.is_none() {
249 self.frames = Some(frames.to_vec());
250 }
251 self.frames.as_mut().unwrap().extend(frames.iter().cloned());
252 self
253 }
254
255 pub fn clear_frames(&mut self) -> &mut Self {
256 self.frames = None;
257 self
258 }
259
260 pub fn frame_count(&self) -> usize {
261 self.frames.as_ref().map(|f| f.len()).unwrap_or(0)
262 }
263
264 pub fn frames_mut(&mut self) -> Option<&mut Vec<Frame>> {
266 self.frames.as_mut()
267 }
268
269 pub fn frames(&self) -> Option<&[Frame]> {
271 self.frames.as_deref()
272 }
273
274 #[cfg(all(not(target_family = "wasm"), not(target_os = "android")))]
279 pub fn show(&self) {
280 use std::env;
281 let rendered = self.render();
282
283 let mut temp = env::temp_dir();
285 let mut plot_name =
286 Alphanumeric.sample_string(&mut SmallRng::seed_from_u64(Self::generate_seed()), 22);
287 plot_name.push_str(".html");
288 plot_name = format!("plotly_{plot_name}");
289 temp.push(plot_name);
290
291 let temp_path = temp.to_str().unwrap();
293
294 {
295 let mut file = File::create(temp_path).unwrap();
296 file.write_all(rendered.as_bytes())
297 .expect("failed to write html output");
298 file.flush().unwrap();
299 }
300
301 Plot::show_with_default_app(temp_path);
303 }
304
305 #[cfg(all(not(target_family = "wasm"), not(target_os = "android")))]
311 pub fn show_html<P: AsRef<Path> + std::clone::Clone>(&self, filename: P) {
312 let path = filename.as_ref().to_str().unwrap();
313 self.write_html(filename.clone());
314 Plot::show_with_default_app(path);
316 }
317
318 #[cfg(all(not(target_family = "wasm"), not(target_os = "android")))]
321 #[cfg(any(feature = "kaleido", feature = "plotly_static"))]
322 pub fn show_image(&self, format: ImageFormat, width: usize, height: usize) {
323 use std::env;
324
325 let rendered = self.render_static(&format, width, height);
326
327 let mut temp = env::temp_dir();
329 let mut plot_name =
330 Alphanumeric.sample_string(&mut SmallRng::seed_from_u64(Self::generate_seed()), 22);
331 plot_name.push_str(".html");
332 plot_name = format!("plotly_{plot_name}");
333 temp.push(plot_name);
334
335 let temp_path = temp.to_str().unwrap();
337
338 {
339 let mut file = File::create(temp_path).unwrap();
340 file.write_all(rendered.as_bytes())
341 .expect("failed to write html output");
342 file.flush().unwrap();
343 }
344
345 Plot::show_with_default_app(temp_path);
347 }
348
349 pub fn write_html<P: AsRef<Path>>(&self, filename: P) {
354 let rendered = self.to_html();
355
356 let mut file =
357 File::create(filename).expect("Provided filepath does not exist or is not accessible");
358 file.write_all(rendered.as_bytes())
359 .expect("failed to write html output");
360 file.flush().unwrap();
361 }
362
363 pub fn to_html(&self) -> String {
369 self.render()
370 }
371
372 pub fn to_inline_html(&self, plot_div_id: Option<&str>) -> String {
386 let plot_div_id = match plot_div_id {
387 Some(id) => id.to_string(),
388 None => {
389 Alphanumeric.sample_string(&mut SmallRng::seed_from_u64(Self::generate_seed()), 20)
390 }
391 };
392 self.render_inline(&plot_div_id)
393 }
394
395 fn to_jupyter_notebook_html(&self) -> String {
396 let plot_div_id =
397 Alphanumeric.sample_string(&mut SmallRng::seed_from_u64(Self::generate_seed()), 20);
398
399 let tmpl = JupyterNotebookPlotTemplate {
400 plot: self,
401 plot_div_id: &plot_div_id,
402 };
403 tmpl.render().unwrap()
404 }
405
406 pub fn notebook_display(&self) {
408 let plot_data = self.to_jupyter_notebook_html();
409 println!("EVCXR_BEGIN_CONTENT text/html\n{plot_data}\nEVCXR_END_CONTENT");
410 }
411
412 pub fn lab_display(&self) {
414 let plot_data = self.to_json();
415 println!(
416 "EVCXR_BEGIN_CONTENT application/vnd.plotly.v1+json\n{plot_data}\nEVCXR_END_CONTENT"
417 );
418 }
419
420 pub fn evcxr_display(&self) {
423 self.lab_display();
424 }
425
426 #[deprecated(
433 since = "0.13.0",
434 note = "kaleido-based implementation is deprecated. Use plotly_static feature instead. The kaleido implementation will be removed in version 0.15.0"
435 )]
436 #[cfg(feature = "kaleido")]
437 pub fn write_image<P: AsRef<Path>>(
438 &self,
439 filename: P,
440 format: ImageFormat,
441 width: usize,
442 height: usize,
443 scale: f64,
444 ) {
445 let kaleido = plotly_kaleido::Kaleido::new();
446 kaleido
447 .save(
448 filename.as_ref(),
449 &serde_json::to_value(self).unwrap(),
450 format,
451 width,
452 height,
453 scale,
454 )
455 .unwrap_or_else(|_| panic!("failed to export plot to {:?}", filename.as_ref()));
456 }
457
458 #[deprecated(
466 since = "0.13.0",
467 note = "kaleido-based implementation is deprecated. Use plotly_static feature instead. The kaleido implementation will be removed in version 0.15.0"
468 )]
469 #[cfg(feature = "kaleido")]
470 pub fn to_base64(
471 &self,
472 format: ImageFormat,
473 width: usize,
474 height: usize,
475 scale: f64,
476 ) -> String {
477 match format {
478 ImageFormat::JPEG | ImageFormat::PNG | ImageFormat::WEBP => {
479 let kaleido = plotly_kaleido::Kaleido::new();
480 kaleido
481 .image_to_string(
482 &serde_json::to_value(self).unwrap(),
483 format,
484 width,
485 height,
486 scale,
487 )
488 .unwrap_or_else(|_| panic!("Kaleido failed to generate image"))
489 }
490 _ => {
491 eprintln!("Cannot generate base64 string for ImageFormat:{format}. Allowed formats are JPEG, PNG, WEBP");
492 String::default()
493 }
494 }
495 }
496
497 #[deprecated(
503 since = "0.13.0",
504 note = "kaleido-based implementation is deprecated. Use plotly_static feature instead. The kaleido implementation will be removed in version 0.15.0"
505 )]
506 #[cfg(feature = "kaleido")]
507 pub fn to_svg(&self, width: usize, height: usize, scale: f64) -> String {
508 let kaleido = plotly_kaleido::Kaleido::new();
509 kaleido
510 .image_to_string(
511 &serde_json::to_value(self).unwrap(),
512 ImageFormat::SVG,
513 width,
514 height,
515 scale,
516 )
517 .unwrap_or_else(|_| panic!("Kaleido failed to generate image"))
518 }
519
520 #[cfg(feature = "plotly_static")]
533 pub fn write_image<P: AsRef<Path>>(
534 &self,
535 filename: P,
536 format: ImageFormat,
537 width: usize,
538 height: usize,
539 scale: f64,
540 ) -> Result<(), Box<dyn std::error::Error>> {
541 use crate::prelude::*;
542 let mut exporter = plotly_static::StaticExporterBuilder::default()
543 .build()
544 .map_err(|e| format!("Failed to create StaticExporter: {e}"))?;
545 let result = exporter.write_image(self, filename, format, width, height, scale);
546 exporter.close();
547 result
548 }
549
550 #[cfg(feature = "plotly_static")]
566 pub fn to_base64(
567 &self,
568 format: ImageFormat,
569 width: usize,
570 height: usize,
571 scale: f64,
572 ) -> Result<String, Box<dyn std::error::Error>> {
573 use crate::prelude::*;
574 let mut exporter = plotly_static::StaticExporterBuilder::default()
575 .build()
576 .map_err(|e| format!("Failed to create StaticExporter: {e}"))?;
577 let result = exporter.to_base64(self, format, width, height, scale);
578 exporter.close();
579 result
580 }
581
582 #[cfg(feature = "plotly_static")]
594 pub fn to_svg(
595 &self,
596 width: usize,
597 height: usize,
598 scale: f64,
599 ) -> Result<String, Box<dyn std::error::Error>> {
600 use crate::prelude::*;
601 let mut exporter = plotly_static::StaticExporterBuilder::default()
602 .build()
603 .map_err(|e| format!("Failed to create StaticExporter: {e}"))?;
604 let result = exporter.to_svg(self, width, height, scale);
605 exporter.close();
606 result
607 }
608
609 #[deprecated(
611 note = "Use exporter.write_image(&plot, ...) from plotly::export::sync::ExporterSyncExt"
612 )]
613 #[cfg(feature = "plotly_static")]
614 pub fn write_image_with_exporter<P: AsRef<Path>>(
615 &self,
616 exporter: &mut plotly_static::StaticExporter,
617 filename: P,
618 format: ImageFormat,
619 width: usize,
620 height: usize,
621 scale: f64,
622 ) -> Result<(), Box<dyn std::error::Error>> {
623 exporter.write_fig(
624 filename.as_ref(),
625 &serde_json::to_value(self)?,
626 format,
627 width,
628 height,
629 scale,
630 )
631 }
632
633 #[deprecated(
635 note = "Use exporter.to_base64(&plot, ...) from plotly::export::sync::ExporterSyncExt"
636 )]
637 #[cfg(feature = "plotly_static")]
638 pub fn to_base64_with_exporter(
639 &self,
640 exporter: &mut plotly_static::StaticExporter,
641 format: ImageFormat,
642 width: usize,
643 height: usize,
644 scale: f64,
645 ) -> Result<String, Box<dyn std::error::Error>> {
646 match format {
647 ImageFormat::JPEG | ImageFormat::PNG | ImageFormat::WEBP => {
648 exporter.write_to_string(
649 &serde_json::to_value(self)?,
650 format,
651 width,
652 height,
653 scale,
654 )
655 }
656 _ => {
657 Err(format!("Cannot generate base64 string for ImageFormat:{format}. Allowed formats are JPEG, PNG, WEBP").into())
658 }
659 }
660 }
661
662 #[deprecated(
664 note = "Use exporter.to_svg(&plot, ...) from plotly::export::sync::ExporterSyncExt"
665 )]
666 #[cfg(feature = "plotly_static")]
667 pub fn to_svg_with_exporter(
668 &self,
669 exporter: &mut plotly_static::StaticExporter,
670 width: usize,
671 height: usize,
672 scale: f64,
673 ) -> Result<String, Box<dyn std::error::Error>> {
674 exporter.write_to_string(
675 &serde_json::to_value(self)?,
676 ImageFormat::SVG,
677 width,
678 height,
679 scale,
680 )
681 }
682
683 fn render(&self) -> String {
684 let tmpl = PlotTemplate {
685 plot: self,
686 js_scripts: &self.js_scripts,
687 };
688 tmpl.render().unwrap()
689 }
690
691 #[cfg(all(not(target_family = "wasm"), not(target_os = "android")))]
692 #[cfg(any(feature = "kaleido", feature = "plotly_static"))]
693 pub fn render_static(&self, format: &ImageFormat, width: usize, height: usize) -> String {
694 let tmpl = StaticPlotTemplate {
695 plot: self,
696 format: format.clone(),
697 js_scripts: &self.js_scripts,
698 width,
699 height,
700 };
701 tmpl.render().unwrap()
702 }
703
704 fn render_inline(&self, plot_div_id: &str) -> String {
705 let tmpl = InlinePlotTemplate {
706 plot: self,
707 plot_div_id,
708 };
709 tmpl.render().unwrap()
710 }
711
712 fn js_scripts() -> String {
713 if cfg!(feature = "plotly_embed_js") {
714 Self::offline_js_sources()
715 } else {
716 Self::online_cdn_js()
717 }
718 }
719
720 pub fn offline_js_sources() -> String {
732 let local_tex_svg_js = include_str!("../resource/tex-svg-3.2.2.js");
735 let local_plotly_js = include_str!("../resource/plotly.min.js");
736
737 format!(
738 "<script type=\"text/javascript\">{local_plotly_js}</script>\n
739 <script type=\"text/javascript\">{local_tex_svg_js}</script>\n",
740 )
741 .to_string()
742 }
743
744 pub fn online_cdn_js() -> String {
757 r##"<script src="https://cdn.jsdelivr.net/npm/mathjax@3.2.2/es5/tex-svg.js"></script>
760 <script src="https://cdn.plot.ly/plotly-3.0.1.min.js"></script>
761 "##
762 .to_string()
763 }
764
765 pub fn to_json(&self) -> String {
766 serde_json::to_string(self).unwrap()
767 }
768
769 #[cfg(target_family = "wasm")]
770 pub fn to_js_object(&self) -> wasm_bindgen_futures::js_sys::Object {
772 use wasm_bindgen_futures::js_sys;
773 use wasm_bindgen_futures::wasm_bindgen::JsCast;
774 js_sys::JSON::parse(&self.to_json())
778 .expect("Invalid JSON")
779 .dyn_into::<js_sys::Object>()
780 .expect("Invalid JSON structure - expected a top-level Object")
781 }
782
783 #[cfg(all(unix, not(target_os = "android"), not(target_os = "macos")))]
784 fn show_with_default_app(temp_path: &str) {
785 use std::process::Command;
786 Command::new("xdg-open")
787 .args([temp_path])
788 .output()
789 .expect(DEFAULT_HTML_APP_NOT_FOUND);
790 }
791
792 #[cfg(target_os = "macos")]
793 fn show_with_default_app(temp_path: &str) {
794 use std::process::Command;
795 Command::new("open")
796 .args([temp_path])
797 .output()
798 .expect(DEFAULT_HTML_APP_NOT_FOUND);
799 }
800
801 #[cfg(target_os = "windows")]
802 fn show_with_default_app(temp_path: &str) {
803 use std::process::Command;
804 Command::new("explorer")
805 .arg(temp_path)
806 .spawn()
807 .expect(DEFAULT_HTML_APP_NOT_FOUND);
808 }
809
810 pub(crate) fn generate_seed() -> u64 {
813 let time = SystemTime::now()
814 .duration_since(UNIX_EPOCH)
815 .unwrap_or_default()
816 .as_nanos() as u64;
817 let counter = SEED_COUNTER.fetch_add(1, Ordering::Relaxed);
818 time ^ counter
819 }
820}
821
822impl PartialEq for Plot {
823 fn eq(&self, other: &Self) -> bool {
824 self.to_json() == other.to_json()
825 }
826}
827
828#[cfg(test)]
829mod tests {
830 use std::path::PathBuf;
831
832 #[cfg(feature = "kaleido")]
833 use plotly_kaleido::ImageFormat;
834 #[cfg(feature = "plotly_static")]
835 use plotly_static::ImageFormat;
836 use serde_json::{json, to_value};
837 #[cfg(any(feature = "kaleido", feature = "plotly_static"))]
838 use {base64::engine::general_purpose, base64::Engine};
839
840 use super::*;
841 #[cfg(feature = "plotly_static")]
842 use crate::export::sync::ExporterSyncExt;
843 use crate::Scatter;
844
845 fn create_test_plot() -> Plot {
846 let trace1 = Scatter::new(vec![0, 1, 2], vec![6, 10, 2]).name("trace1");
847 let mut plot = Plot::new();
848 plot.add_trace(trace1);
849 plot
850 }
851
852 #[test]
853 fn inline_plot() {
854 let plot = create_test_plot();
855 let inline_plot_data = plot.to_inline_html(Some("replace_this_with_the_div_id"));
856 assert!(inline_plot_data.contains("replace_this_with_the_div_id"));
857 plot.to_inline_html(None);
858 }
859
860 #[test]
861 fn jupyter_notebook_plot() {
862 let plot = create_test_plot();
863 plot.to_jupyter_notebook_html();
864 }
865
866 #[test]
867 fn notebook_display() {
868 let plot = create_test_plot();
869 plot.notebook_display();
870 }
871
872 #[test]
873 fn lab_display() {
874 let plot = create_test_plot();
875 plot.lab_display();
876 }
877
878 #[test]
879 fn plot_serialize_simple() {
880 let plot = create_test_plot();
881 let expected = json!({
882 "data": [
883 {
884 "type": "scatter",
885 "name": "trace1",
886 "x": [0, 1, 2],
887 "y": [6, 10, 2]
888 }
889 ],
890 "layout": {},
891 "config": {},
892 "frames": null,
893 });
894
895 assert_eq!(to_value(plot).unwrap(), expected);
896 }
897
898 #[test]
899 fn plot_serialize_with_layout() {
900 let mut plot = create_test_plot();
901 let layout = Layout::new().title("Title");
902 plot.set_layout(layout);
903
904 let expected = json!({
905 "data": [
906 {
907 "type": "scatter",
908 "name": "trace1",
909 "x": [0, 1, 2],
910 "y": [6, 10, 2]
911 }
912 ],
913 "layout": {
914 "title": {
915 "text": "Title"
916 }
917 },
918 "config": {},
919 "frames": null,
920 });
921
922 assert_eq!(to_value(plot).unwrap(), expected);
923 }
924
925 #[test]
926 fn data_to_json() {
927 let plot = create_test_plot();
928 let expected = json!([
929 {
930 "type": "scatter",
931 "name": "trace1",
932 "x": [0, 1, 2],
933 "y": [6, 10, 2]
934 }
935 ]);
936
937 assert_eq!(to_value(plot.data()).unwrap(), expected);
938 }
939
940 #[test]
941 fn empty_layout_to_json() {
942 let plot = create_test_plot();
943 let expected = json!({});
944
945 assert_eq!(to_value(plot.layout()).unwrap(), expected);
946 }
947
948 #[test]
949 fn layout_to_json() {
950 let mut plot = create_test_plot();
951 let layout = Layout::new().title("TestTitle");
952 plot.set_layout(layout);
953
954 let expected = json!({
955 "title": {"text": "TestTitle"}
956 });
957
958 assert_eq!(to_value(plot.layout()).unwrap(), expected);
959 }
960
961 #[test]
962 fn plot_eq() {
963 let plot1 = create_test_plot();
964 let plot2 = create_test_plot();
965
966 assert!(plot1 == plot2);
967 }
968
969 #[test]
970 fn plot_neq() {
971 let plot1 = create_test_plot();
972 let trace2 = Scatter::new(vec![10, 1, 2], vec![6, 10, 2]).name("trace2");
973 let mut plot2 = Plot::new();
974 plot2.add_trace(trace2);
975
976 assert!(plot1 != plot2);
977 }
978
979 #[test]
980 fn plot_clone() {
981 let plot1 = create_test_plot();
982 let plot2 = plot1.clone();
983
984 assert!(plot1 == plot2);
985 }
986
987 #[test]
988 fn save_html() {
989 let plot = create_test_plot();
990 let dst = PathBuf::from("plotly_example.html");
991 plot.write_html(&dst);
992 assert!(dst.exists());
993 #[cfg(not(feature = "debug"))]
994 assert!(std::fs::remove_file(&dst).is_ok());
995 }
996
997 #[cfg(feature = "plotly_static")]
999 fn get_unique_port() -> u32 {
1000 use std::sync::atomic::{AtomicU32, Ordering};
1001 static PORT_COUNTER: AtomicU32 = AtomicU32::new(5144);
1002 PORT_COUNTER.fetch_add(1, Ordering::SeqCst)
1003 }
1004
1005 #[test]
1006 #[cfg(feature = "plotly_static")]
1007 fn save_to_png() {
1008 let plot = create_test_plot();
1009 let dst = PathBuf::from("plotly_example.png");
1010 let mut exporter = plotly_static::StaticExporterBuilder::default()
1011 .webdriver_port(get_unique_port())
1012 .build()
1013 .unwrap();
1014 exporter
1015 .write_image(&plot, &dst, ImageFormat::PNG, 1024, 680, 1.0)
1016 .unwrap();
1017 assert!(dst.exists());
1018 let metadata = std::fs::metadata(&dst).expect("Could not retrieve file metadata");
1019 let file_size = metadata.len();
1020 assert!(file_size > 0,);
1021 #[cfg(not(feature = "debug"))]
1022 assert!(std::fs::remove_file(&dst).is_ok());
1023 exporter.close();
1024 }
1025
1026 #[test]
1027 #[cfg(feature = "plotly_static")]
1028 fn save_to_jpeg() {
1029 let plot = create_test_plot();
1030 let dst = PathBuf::from("plotly_example.jpeg");
1031 let mut exporter = plotly_static::StaticExporterBuilder::default()
1032 .webdriver_port(get_unique_port())
1033 .build()
1034 .unwrap();
1035 exporter
1036 .write_image(&plot, &dst, ImageFormat::JPEG, 1024, 680, 1.0)
1037 .unwrap();
1038 assert!(dst.exists());
1039 let metadata = std::fs::metadata(&dst).expect("Could not retrieve file metadata");
1040 let file_size = metadata.len();
1041 assert!(file_size > 0,);
1042 #[cfg(not(feature = "debug"))]
1043 assert!(std::fs::remove_file(&dst).is_ok());
1044 exporter.close();
1045 }
1046
1047 #[test]
1048 #[cfg(feature = "plotly_static")]
1049 fn save_to_svg() {
1050 let plot = create_test_plot();
1051 let dst = PathBuf::from("plotly_example.svg");
1052 let mut exporter = plotly_static::StaticExporterBuilder::default()
1053 .webdriver_port(get_unique_port())
1054 .build()
1055 .unwrap();
1056 exporter
1057 .write_image(&plot, &dst, ImageFormat::SVG, 1024, 680, 1.0)
1058 .unwrap();
1059 assert!(dst.exists());
1060 let metadata = std::fs::metadata(&dst).expect("Could not retrieve file metadata");
1061 let file_size = metadata.len();
1062 assert!(file_size > 0,);
1063 #[cfg(not(feature = "debug"))]
1064 assert!(std::fs::remove_file(&dst).is_ok());
1065 exporter.close();
1066 }
1067
1068 #[test]
1069 #[cfg(feature = "plotly_static")]
1070 fn save_to_pdf() {
1071 let plot = create_test_plot();
1072 let dst = PathBuf::from("plotly_example.pdf");
1073 #[cfg(feature = "debug")]
1074 let mut exporter = plotly_static::StaticExporterBuilder::default()
1075 .spawn_webdriver(true)
1076 .webdriver_port(get_unique_port())
1077 .pdf_export_timeout(750)
1078 .build()
1079 .unwrap();
1080 #[cfg(not(feature = "debug"))]
1081 let mut exporter = plotly_static::StaticExporterBuilder::default()
1082 .webdriver_port(get_unique_port())
1083 .build()
1084 .unwrap();
1085 exporter
1086 .write_image(&plot, &dst, ImageFormat::PDF, 1024, 680, 1.0)
1087 .unwrap();
1088 assert!(dst.exists());
1089 let metadata = std::fs::metadata(&dst).expect("Could not retrieve file metadata");
1090 let file_size = metadata.len();
1091 assert!(file_size > 0,);
1092 #[cfg(not(feature = "debug"))]
1093 assert!(std::fs::remove_file(&dst).is_ok());
1094 exporter.close();
1095 }
1096
1097 #[test]
1098 #[cfg(feature = "plotly_static")]
1099 fn save_to_webp() {
1100 let plot = create_test_plot();
1101 let dst = PathBuf::from("plotly_example.webp");
1102 let mut exporter = plotly_static::StaticExporterBuilder::default()
1103 .webdriver_port(get_unique_port())
1104 .build()
1105 .unwrap();
1106 exporter
1107 .write_image(&plot, &dst, ImageFormat::WEBP, 1024, 680, 1.0)
1108 .unwrap();
1109 assert!(dst.exists());
1110 let metadata = std::fs::metadata(&dst).expect("Could not retrieve file metadata");
1111 let file_size = metadata.len();
1112 assert!(file_size > 0,);
1113 #[cfg(not(feature = "debug"))]
1114 assert!(std::fs::remove_file(&dst).is_ok());
1115 exporter.close();
1116 }
1117
1118 #[test]
1119 #[cfg(feature = "plotly_static")]
1120 fn image_to_base64() {
1121 let plot = create_test_plot();
1122 let mut exporter = plotly_static::StaticExporterBuilder::default()
1123 .webdriver_port(get_unique_port())
1124 .build()
1125 .unwrap();
1126
1127 let image_base64 = exporter
1128 .to_base64(&plot, ImageFormat::PNG, 200, 150, 1.0)
1129 .unwrap();
1130
1131 assert!(!image_base64.is_empty());
1132
1133 let result_decoded = general_purpose::STANDARD.decode(image_base64).unwrap();
1134 let expected = "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";
1135 let expected_decoded = general_purpose::STANDARD.decode(expected).unwrap();
1136
1137 assert_eq!(expected_decoded[..2], result_decoded[..2]);
1141 exporter.close();
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 = exporter.to_svg(&plot, 200, 150, 1.0).unwrap();
1153
1154 assert!(!image_svg.is_empty());
1155
1156 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>";
1157 const LEN: usize = 10;
1160 assert_eq!(expected[..LEN], image_svg[..LEN]);
1161 exporter.close();
1162 }
1163
1164 #[test]
1165 #[cfg(feature = "plotly_static")]
1166 fn save_surface_to_png() {
1167 use crate::Surface;
1168 let mut plot = Plot::new();
1169 let z_matrix = vec![
1170 vec![1.0, 2.0, 3.0],
1171 vec![4.0, 5.0, 6.0],
1172 vec![7.0, 8.0, 9.0],
1173 ];
1174 let x_unique = vec![1.0, 2.0, 3.0];
1175 let y_unique = vec![4.0, 5.0, 6.0];
1176 let surface = Surface::new(z_matrix)
1177 .x(x_unique)
1178 .y(y_unique)
1179 .name("Surface");
1180
1181 plot.add_trace(surface);
1182 let dst = PathBuf::from("plotly_example_surface.png");
1183 let mut exporter = plotly_static::StaticExporterBuilder::default()
1184 .webdriver_port(get_unique_port())
1185 .build()
1186 .unwrap();
1187
1188 assert!(!exporter
1189 .to_base64(&plot, ImageFormat::PNG, 1024, 680, 1.0)
1190 .unwrap()
1191 .is_empty());
1192
1193 exporter
1194 .write_image(&plot, &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 exporter.close();
1204 }
1205}