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.14.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(Default, 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 Plot {
173 pub fn new() -> Plot {
175 Plot {
176 traces: Traces::new(),
177 js_scripts: Self::js_scripts(),
178 ..Default::default()
179 }
180 }
181
182 #[cfg(feature = "plotly_embed_js")]
188 pub fn use_cdn_js(&mut self) {
189 self.js_scripts = Self::online_cdn_js();
190 }
191
192 pub fn add_trace(&mut self, trace: Box<dyn Trace>) {
194 self.traces.push(trace);
195 }
196
197 pub fn add_traces(&mut self, traces: Vec<Box<dyn Trace>>) {
199 for trace in traces {
200 self.add_trace(trace);
201 }
202 }
203
204 pub fn set_layout(&mut self, layout: Layout) {
206 self.layout = layout;
207 }
208
209 pub fn set_configuration(&mut self, configuration: Configuration) {
211 self.configuration = configuration;
212 }
213
214 pub fn data(&self) -> &Traces {
216 &self.traces
217 }
218
219 pub fn layout(&self) -> &Layout {
221 &self.layout
222 }
223
224 pub fn configuration(&self) -> &Configuration {
226 &self.configuration
227 }
228
229 pub fn add_frame(&mut self, frame: Frame) -> &mut Self {
231 if self.frames.is_none() {
232 self.frames = Some(Vec::new());
233 }
234 self.frames.as_mut().unwrap().push(frame);
235 self
236 }
237
238 pub fn add_frames(&mut self, frames: &[Frame]) -> &mut Self {
240 if self.frames.is_none() {
241 self.frames = Some(frames.to_vec());
242 }
243 self.frames.as_mut().unwrap().extend(frames.iter().cloned());
244 self
245 }
246
247 pub fn clear_frames(&mut self) -> &mut Self {
248 self.frames = None;
249 self
250 }
251
252 pub fn frame_count(&self) -> usize {
253 self.frames.as_ref().map(|f| f.len()).unwrap_or(0)
254 }
255
256 pub fn frames_mut(&mut self) -> Option<&mut Vec<Frame>> {
258 self.frames.as_mut()
259 }
260
261 pub fn frames(&self) -> Option<&[Frame]> {
263 self.frames.as_deref()
264 }
265
266 #[cfg(all(not(target_family = "wasm"), not(target_os = "android")))]
271 pub fn show(&self) {
272 use std::env;
273 let rendered = self.render();
274
275 let mut temp = env::temp_dir();
277 let mut plot_name =
278 Alphanumeric.sample_string(&mut SmallRng::seed_from_u64(Self::generate_seed()), 22);
279 plot_name.push_str(".html");
280 plot_name = format!("plotly_{plot_name}");
281 temp.push(plot_name);
282
283 let temp_path = temp.to_str().unwrap();
285
286 {
287 let mut file = File::create(temp_path).unwrap();
288 file.write_all(rendered.as_bytes())
289 .expect("failed to write html output");
290 file.flush().unwrap();
291 }
292
293 Plot::show_with_default_app(temp_path);
295 }
296
297 #[cfg(all(not(target_family = "wasm"), not(target_os = "android")))]
303 pub fn show_html<P: AsRef<Path> + std::clone::Clone>(&self, filename: P) {
304 let path = filename.as_ref().to_str().unwrap();
305 self.write_html(filename.clone());
306 Plot::show_with_default_app(path);
308 }
309
310 #[cfg(all(not(target_family = "wasm"), not(target_os = "android")))]
313 #[cfg(any(feature = "kaleido", feature = "plotly_static"))]
314 pub fn show_image(&self, format: ImageFormat, width: usize, height: usize) {
315 use std::env;
316
317 let rendered = self.render_static(&format, width, height);
318
319 let mut temp = env::temp_dir();
321 let mut plot_name =
322 Alphanumeric.sample_string(&mut SmallRng::seed_from_u64(Self::generate_seed()), 22);
323 plot_name.push_str(".html");
324 plot_name = format!("plotly_{plot_name}");
325 temp.push(plot_name);
326
327 let temp_path = temp.to_str().unwrap();
329
330 {
331 let mut file = File::create(temp_path).unwrap();
332 file.write_all(rendered.as_bytes())
333 .expect("failed to write html output");
334 file.flush().unwrap();
335 }
336
337 Plot::show_with_default_app(temp_path);
339 }
340
341 pub fn write_html<P: AsRef<Path>>(&self, filename: P) {
346 let rendered = self.to_html();
347
348 let mut file =
349 File::create(filename).expect("Provided filepath does not exist or is not accessible");
350 file.write_all(rendered.as_bytes())
351 .expect("failed to write html output");
352 file.flush().unwrap();
353 }
354
355 pub fn to_html(&self) -> String {
361 self.render()
362 }
363
364 pub fn to_inline_html(&self, plot_div_id: Option<&str>) -> String {
378 let plot_div_id = match plot_div_id {
379 Some(id) => id.to_string(),
380 None => {
381 Alphanumeric.sample_string(&mut SmallRng::seed_from_u64(Self::generate_seed()), 20)
382 }
383 };
384 self.render_inline(&plot_div_id)
385 }
386
387 fn to_jupyter_notebook_html(&self) -> String {
388 let plot_div_id =
389 Alphanumeric.sample_string(&mut SmallRng::seed_from_u64(Self::generate_seed()), 20);
390
391 let tmpl = JupyterNotebookPlotTemplate {
392 plot: self,
393 plot_div_id: &plot_div_id,
394 };
395 tmpl.render().unwrap()
396 }
397
398 pub fn notebook_display(&self) {
400 let plot_data = self.to_jupyter_notebook_html();
401 println!("EVCXR_BEGIN_CONTENT text/html\n{plot_data}\nEVCXR_END_CONTENT");
402 }
403
404 pub fn lab_display(&self) {
406 let plot_data = self.to_json();
407 println!(
408 "EVCXR_BEGIN_CONTENT application/vnd.plotly.v1+json\n{plot_data}\nEVCXR_END_CONTENT"
409 );
410 }
411
412 pub fn evcxr_display(&self) {
415 self.lab_display();
416 }
417
418 #[deprecated(
425 since = "0.13.0",
426 note = "kaleido-based implementation is deprecated. Use plotly_static feature instead. The kaleido implementation will be removed in version 0.14.0"
427 )]
428 #[cfg(feature = "kaleido")]
429 pub fn write_image<P: AsRef<Path>>(
430 &self,
431 filename: P,
432 format: ImageFormat,
433 width: usize,
434 height: usize,
435 scale: f64,
436 ) {
437 let kaleido = plotly_kaleido::Kaleido::new();
438 kaleido
439 .save(
440 filename.as_ref(),
441 &serde_json::to_value(self).unwrap(),
442 format,
443 width,
444 height,
445 scale,
446 )
447 .unwrap_or_else(|_| panic!("failed to export plot to {:?}", filename.as_ref()));
448 }
449
450 #[deprecated(
458 since = "0.13.0",
459 note = "kaleido-based implementation is deprecated. Use plotly_static feature instead. The kaleido implementation will be removed in version 0.14.0"
460 )]
461 #[cfg(feature = "kaleido")]
462 pub fn to_base64(
463 &self,
464 format: ImageFormat,
465 width: usize,
466 height: usize,
467 scale: f64,
468 ) -> String {
469 match format {
470 ImageFormat::JPEG | ImageFormat::PNG | ImageFormat::WEBP => {
471 let kaleido = plotly_kaleido::Kaleido::new();
472 kaleido
473 .image_to_string(
474 &serde_json::to_value(self).unwrap(),
475 format,
476 width,
477 height,
478 scale,
479 )
480 .unwrap_or_else(|_| panic!("Kaleido failed to generate image"))
481 }
482 _ => {
483 eprintln!("Cannot generate base64 string for ImageFormat:{format}. Allowed formats are JPEG, PNG, WEBP");
484 String::default()
485 }
486 }
487 }
488
489 #[deprecated(
495 since = "0.13.0",
496 note = "kaleido-based implementation is deprecated. Use plotly_static feature instead. The kaleido implementation will be removed in version 0.14.0"
497 )]
498 #[cfg(feature = "kaleido")]
499 pub fn to_svg(&self, width: usize, height: usize, scale: f64) -> String {
500 let kaleido = plotly_kaleido::Kaleido::new();
501 kaleido
502 .image_to_string(
503 &serde_json::to_value(self).unwrap(),
504 ImageFormat::SVG,
505 width,
506 height,
507 scale,
508 )
509 .unwrap_or_else(|_| panic!("Kaleido failed to generate image"))
510 }
511
512 #[cfg(feature = "plotly_static")]
525 pub fn write_image<P: AsRef<Path>>(
526 &self,
527 filename: P,
528 format: ImageFormat,
529 width: usize,
530 height: usize,
531 scale: f64,
532 ) -> Result<(), Box<dyn std::error::Error>> {
533 use crate::prelude::*;
534 let mut exporter = plotly_static::StaticExporterBuilder::default()
535 .build()
536 .map_err(|e| format!("Failed to create StaticExporter: {e}"))?;
537 let result = exporter.write_image(self, filename, format, width, height, scale);
538 exporter.close();
539 result
540 }
541
542 #[cfg(feature = "plotly_static")]
558 pub fn to_base64(
559 &self,
560 format: ImageFormat,
561 width: usize,
562 height: usize,
563 scale: f64,
564 ) -> Result<String, Box<dyn std::error::Error>> {
565 use crate::prelude::*;
566 let mut exporter = plotly_static::StaticExporterBuilder::default()
567 .build()
568 .map_err(|e| format!("Failed to create StaticExporter: {e}"))?;
569 let result = exporter.to_base64(self, format, width, height, scale);
570 exporter.close();
571 result
572 }
573
574 #[cfg(feature = "plotly_static")]
586 pub fn to_svg(
587 &self,
588 width: usize,
589 height: usize,
590 scale: f64,
591 ) -> Result<String, Box<dyn std::error::Error>> {
592 use crate::prelude::*;
593 let mut exporter = plotly_static::StaticExporterBuilder::default()
594 .build()
595 .map_err(|e| format!("Failed to create StaticExporter: {e}"))?;
596 let result = exporter.to_svg(self, width, height, scale);
597 exporter.close();
598 result
599 }
600
601 #[deprecated(
603 note = "Use exporter.write_image(&plot, ...) from plotly::export::sync::ExporterSyncExt"
604 )]
605 #[cfg(feature = "plotly_static")]
606 pub fn write_image_with_exporter<P: AsRef<Path>>(
607 &self,
608 exporter: &mut plotly_static::StaticExporter,
609 filename: P,
610 format: ImageFormat,
611 width: usize,
612 height: usize,
613 scale: f64,
614 ) -> Result<(), Box<dyn std::error::Error>> {
615 exporter.write_fig(
616 filename.as_ref(),
617 &serde_json::to_value(self)?,
618 format,
619 width,
620 height,
621 scale,
622 )
623 }
624
625 #[deprecated(
627 note = "Use exporter.to_base64(&plot, ...) from plotly::export::sync::ExporterSyncExt"
628 )]
629 #[cfg(feature = "plotly_static")]
630 pub fn to_base64_with_exporter(
631 &self,
632 exporter: &mut plotly_static::StaticExporter,
633 format: ImageFormat,
634 width: usize,
635 height: usize,
636 scale: f64,
637 ) -> Result<String, Box<dyn std::error::Error>> {
638 match format {
639 ImageFormat::JPEG | ImageFormat::PNG | ImageFormat::WEBP => {
640 exporter.write_to_string(
641 &serde_json::to_value(self)?,
642 format,
643 width,
644 height,
645 scale,
646 )
647 }
648 _ => {
649 Err(format!("Cannot generate base64 string for ImageFormat:{format}. Allowed formats are JPEG, PNG, WEBP").into())
650 }
651 }
652 }
653
654 #[deprecated(
656 note = "Use exporter.to_svg(&plot, ...) from plotly::export::sync::ExporterSyncExt"
657 )]
658 #[cfg(feature = "plotly_static")]
659 pub fn to_svg_with_exporter(
660 &self,
661 exporter: &mut plotly_static::StaticExporter,
662 width: usize,
663 height: usize,
664 scale: f64,
665 ) -> Result<String, Box<dyn std::error::Error>> {
666 exporter.write_to_string(
667 &serde_json::to_value(self)?,
668 ImageFormat::SVG,
669 width,
670 height,
671 scale,
672 )
673 }
674
675 fn render(&self) -> String {
676 let tmpl = PlotTemplate {
677 plot: self,
678 js_scripts: &self.js_scripts,
679 };
680 tmpl.render().unwrap()
681 }
682
683 #[cfg(all(not(target_family = "wasm"), not(target_os = "android")))]
684 #[cfg(any(feature = "kaleido", feature = "plotly_static"))]
685 pub fn render_static(&self, format: &ImageFormat, width: usize, height: usize) -> String {
686 let tmpl = StaticPlotTemplate {
687 plot: self,
688 format: format.clone(),
689 js_scripts: &self.js_scripts,
690 width,
691 height,
692 };
693 tmpl.render().unwrap()
694 }
695
696 fn render_inline(&self, plot_div_id: &str) -> String {
697 let tmpl = InlinePlotTemplate {
698 plot: self,
699 plot_div_id,
700 };
701 tmpl.render().unwrap()
702 }
703
704 fn js_scripts() -> String {
705 if cfg!(feature = "plotly_embed_js") {
706 Self::offline_js_sources()
707 } else {
708 Self::online_cdn_js()
709 }
710 }
711
712 pub fn offline_js_sources() -> String {
724 let local_tex_svg_js = include_str!("../resource/tex-svg-3.2.2.js");
727 let local_plotly_js = include_str!("../resource/plotly.min.js");
728
729 format!(
730 "<script type=\"text/javascript\">{local_plotly_js}</script>\n
731 <script type=\"text/javascript\">{local_tex_svg_js}</script>\n",
732 )
733 .to_string()
734 }
735
736 pub fn online_cdn_js() -> String {
749 r##"<script src="https://cdn.jsdelivr.net/npm/mathjax@3.2.2/es5/tex-svg.js"></script>
752 <script src="https://cdn.plot.ly/plotly-3.0.1.min.js"></script>
753 "##
754 .to_string()
755 }
756
757 pub fn to_json(&self) -> String {
758 serde_json::to_string(self).unwrap()
759 }
760
761 #[cfg(target_family = "wasm")]
762 pub fn to_js_object(&self) -> wasm_bindgen_futures::js_sys::Object {
764 use wasm_bindgen_futures::js_sys;
765 use wasm_bindgen_futures::wasm_bindgen::JsCast;
766 js_sys::JSON::parse(&self.to_json())
770 .expect("Invalid JSON")
771 .dyn_into::<js_sys::Object>()
772 .expect("Invalid JSON structure - expected a top-level Object")
773 }
774
775 #[cfg(all(unix, not(target_os = "android"), not(target_os = "macos")))]
776 fn show_with_default_app(temp_path: &str) {
777 use std::process::Command;
778 Command::new("xdg-open")
779 .args([temp_path])
780 .output()
781 .expect(DEFAULT_HTML_APP_NOT_FOUND);
782 }
783
784 #[cfg(target_os = "macos")]
785 fn show_with_default_app(temp_path: &str) {
786 use std::process::Command;
787 Command::new("open")
788 .args([temp_path])
789 .output()
790 .expect(DEFAULT_HTML_APP_NOT_FOUND);
791 }
792
793 #[cfg(target_os = "windows")]
794 fn show_with_default_app(temp_path: &str) {
795 use std::process::Command;
796 Command::new("explorer")
797 .arg(temp_path)
798 .spawn()
799 .expect(DEFAULT_HTML_APP_NOT_FOUND);
800 }
801
802 pub(crate) fn generate_seed() -> u64 {
805 let time = SystemTime::now()
806 .duration_since(UNIX_EPOCH)
807 .unwrap_or_default()
808 .as_nanos() as u64;
809 let counter = SEED_COUNTER.fetch_add(1, Ordering::Relaxed);
810 time ^ counter
811 }
812}
813
814impl PartialEq for Plot {
815 fn eq(&self, other: &Self) -> bool {
816 self.to_json() == other.to_json()
817 }
818}
819
820#[cfg(test)]
821mod tests {
822 use std::path::PathBuf;
823
824 #[cfg(feature = "kaleido")]
825 use plotly_kaleido::ImageFormat;
826 #[cfg(feature = "plotly_static")]
827 use plotly_static::ImageFormat;
828 use serde_json::{json, to_value};
829 #[cfg(any(feature = "kaleido", feature = "plotly_static"))]
830 use {base64::engine::general_purpose, base64::Engine};
831
832 use super::*;
833 #[cfg(feature = "plotly_static")]
834 use crate::export::sync::ExporterSyncExt;
835 use crate::Scatter;
836
837 fn create_test_plot() -> Plot {
838 let trace1 = Scatter::new(vec![0, 1, 2], vec![6, 10, 2]).name("trace1");
839 let mut plot = Plot::new();
840 plot.add_trace(trace1);
841 plot
842 }
843
844 #[test]
845 fn inline_plot() {
846 let plot = create_test_plot();
847 let inline_plot_data = plot.to_inline_html(Some("replace_this_with_the_div_id"));
848 assert!(inline_plot_data.contains("replace_this_with_the_div_id"));
849 plot.to_inline_html(None);
850 }
851
852 #[test]
853 fn jupyter_notebook_plot() {
854 let plot = create_test_plot();
855 plot.to_jupyter_notebook_html();
856 }
857
858 #[test]
859 fn notebook_display() {
860 let plot = create_test_plot();
861 plot.notebook_display();
862 }
863
864 #[test]
865 fn lab_display() {
866 let plot = create_test_plot();
867 plot.lab_display();
868 }
869
870 #[test]
871 fn plot_serialize_simple() {
872 let plot = create_test_plot();
873 let expected = json!({
874 "data": [
875 {
876 "type": "scatter",
877 "name": "trace1",
878 "x": [0, 1, 2],
879 "y": [6, 10, 2]
880 }
881 ],
882 "layout": {},
883 "config": {},
884 "frames": null,
885 });
886
887 assert_eq!(to_value(plot).unwrap(), expected);
888 }
889
890 #[test]
891 fn plot_serialize_with_layout() {
892 let mut plot = create_test_plot();
893 let layout = Layout::new().title("Title");
894 plot.set_layout(layout);
895
896 let expected = json!({
897 "data": [
898 {
899 "type": "scatter",
900 "name": "trace1",
901 "x": [0, 1, 2],
902 "y": [6, 10, 2]
903 }
904 ],
905 "layout": {
906 "title": {
907 "text": "Title"
908 }
909 },
910 "config": {},
911 "frames": null,
912 });
913
914 assert_eq!(to_value(plot).unwrap(), expected);
915 }
916
917 #[test]
918 fn data_to_json() {
919 let plot = create_test_plot();
920 let expected = json!([
921 {
922 "type": "scatter",
923 "name": "trace1",
924 "x": [0, 1, 2],
925 "y": [6, 10, 2]
926 }
927 ]);
928
929 assert_eq!(to_value(plot.data()).unwrap(), expected);
930 }
931
932 #[test]
933 fn empty_layout_to_json() {
934 let plot = create_test_plot();
935 let expected = json!({});
936
937 assert_eq!(to_value(plot.layout()).unwrap(), expected);
938 }
939
940 #[test]
941 fn layout_to_json() {
942 let mut plot = create_test_plot();
943 let layout = Layout::new().title("TestTitle");
944 plot.set_layout(layout);
945
946 let expected = json!({
947 "title": {"text": "TestTitle"}
948 });
949
950 assert_eq!(to_value(plot.layout()).unwrap(), expected);
951 }
952
953 #[test]
954 fn plot_eq() {
955 let plot1 = create_test_plot();
956 let plot2 = create_test_plot();
957
958 assert!(plot1 == plot2);
959 }
960
961 #[test]
962 fn plot_neq() {
963 let plot1 = create_test_plot();
964 let trace2 = Scatter::new(vec![10, 1, 2], vec![6, 10, 2]).name("trace2");
965 let mut plot2 = Plot::new();
966 plot2.add_trace(trace2);
967
968 assert!(plot1 != plot2);
969 }
970
971 #[test]
972 fn plot_clone() {
973 let plot1 = create_test_plot();
974 let plot2 = plot1.clone();
975
976 assert!(plot1 == plot2);
977 }
978
979 #[test]
980 fn save_html() {
981 let plot = create_test_plot();
982 let dst = PathBuf::from("plotly_example.html");
983 plot.write_html(&dst);
984 assert!(dst.exists());
985 #[cfg(not(feature = "debug"))]
986 assert!(std::fs::remove_file(&dst).is_ok());
987 }
988
989 #[cfg(feature = "plotly_static")]
991 fn get_unique_port() -> u32 {
992 use std::sync::atomic::{AtomicU32, Ordering};
993 static PORT_COUNTER: AtomicU32 = AtomicU32::new(5144);
994 PORT_COUNTER.fetch_add(1, Ordering::SeqCst)
995 }
996
997 #[test]
998 #[cfg(feature = "plotly_static")]
999 fn save_to_png() {
1000 let plot = create_test_plot();
1001 let dst = PathBuf::from("plotly_example.png");
1002 let mut exporter = plotly_static::StaticExporterBuilder::default()
1003 .webdriver_port(get_unique_port())
1004 .build()
1005 .unwrap();
1006 exporter
1007 .write_image(&plot, &dst, ImageFormat::PNG, 1024, 680, 1.0)
1008 .unwrap();
1009 assert!(dst.exists());
1010 let metadata = std::fs::metadata(&dst).expect("Could not retrieve file metadata");
1011 let file_size = metadata.len();
1012 assert!(file_size > 0,);
1013 #[cfg(not(feature = "debug"))]
1014 assert!(std::fs::remove_file(&dst).is_ok());
1015 exporter.close();
1016 }
1017
1018 #[test]
1019 #[cfg(feature = "plotly_static")]
1020 fn save_to_jpeg() {
1021 let plot = create_test_plot();
1022 let dst = PathBuf::from("plotly_example.jpeg");
1023 let mut exporter = plotly_static::StaticExporterBuilder::default()
1024 .webdriver_port(get_unique_port())
1025 .build()
1026 .unwrap();
1027 exporter
1028 .write_image(&plot, &dst, ImageFormat::JPEG, 1024, 680, 1.0)
1029 .unwrap();
1030 assert!(dst.exists());
1031 let metadata = std::fs::metadata(&dst).expect("Could not retrieve file metadata");
1032 let file_size = metadata.len();
1033 assert!(file_size > 0,);
1034 #[cfg(not(feature = "debug"))]
1035 assert!(std::fs::remove_file(&dst).is_ok());
1036 exporter.close();
1037 }
1038
1039 #[test]
1040 #[cfg(feature = "plotly_static")]
1041 fn save_to_svg() {
1042 let plot = create_test_plot();
1043 let dst = PathBuf::from("plotly_example.svg");
1044 let mut exporter = plotly_static::StaticExporterBuilder::default()
1045 .webdriver_port(get_unique_port())
1046 .build()
1047 .unwrap();
1048 exporter
1049 .write_image(&plot, &dst, ImageFormat::SVG, 1024, 680, 1.0)
1050 .unwrap();
1051 assert!(dst.exists());
1052 let metadata = std::fs::metadata(&dst).expect("Could not retrieve file metadata");
1053 let file_size = metadata.len();
1054 assert!(file_size > 0,);
1055 #[cfg(not(feature = "debug"))]
1056 assert!(std::fs::remove_file(&dst).is_ok());
1057 exporter.close();
1058 }
1059
1060 #[test]
1061 #[cfg(feature = "plotly_static")]
1062 fn save_to_pdf() {
1063 let plot = create_test_plot();
1064 let dst = PathBuf::from("plotly_example.pdf");
1065 #[cfg(feature = "debug")]
1066 let mut exporter = plotly_static::StaticExporterBuilder::default()
1067 .spawn_webdriver(true)
1068 .webdriver_port(get_unique_port())
1069 .pdf_export_timeout(750)
1070 .build()
1071 .unwrap();
1072 #[cfg(not(feature = "debug"))]
1073 let mut exporter = plotly_static::StaticExporterBuilder::default()
1074 .webdriver_port(get_unique_port())
1075 .build()
1076 .unwrap();
1077 exporter
1078 .write_image(&plot, &dst, ImageFormat::PDF, 1024, 680, 1.0)
1079 .unwrap();
1080 assert!(dst.exists());
1081 let metadata = std::fs::metadata(&dst).expect("Could not retrieve file metadata");
1082 let file_size = metadata.len();
1083 assert!(file_size > 0,);
1084 #[cfg(not(feature = "debug"))]
1085 assert!(std::fs::remove_file(&dst).is_ok());
1086 exporter.close();
1087 }
1088
1089 #[test]
1090 #[cfg(feature = "plotly_static")]
1091 fn save_to_webp() {
1092 let plot = create_test_plot();
1093 let dst = PathBuf::from("plotly_example.webp");
1094 let mut exporter = plotly_static::StaticExporterBuilder::default()
1095 .webdriver_port(get_unique_port())
1096 .build()
1097 .unwrap();
1098 exporter
1099 .write_image(&plot, &dst, ImageFormat::WEBP, 1024, 680, 1.0)
1100 .unwrap();
1101 assert!(dst.exists());
1102 let metadata = std::fs::metadata(&dst).expect("Could not retrieve file metadata");
1103 let file_size = metadata.len();
1104 assert!(file_size > 0,);
1105 #[cfg(not(feature = "debug"))]
1106 assert!(std::fs::remove_file(&dst).is_ok());
1107 exporter.close();
1108 }
1109
1110 #[test]
1111 #[cfg(feature = "plotly_static")]
1112 fn image_to_base64() {
1113 let plot = create_test_plot();
1114 let mut exporter = plotly_static::StaticExporterBuilder::default()
1115 .webdriver_port(get_unique_port())
1116 .build()
1117 .unwrap();
1118
1119 let image_base64 = exporter
1120 .to_base64(&plot, ImageFormat::PNG, 200, 150, 1.0)
1121 .unwrap();
1122
1123 assert!(!image_base64.is_empty());
1124
1125 let result_decoded = general_purpose::STANDARD.decode(image_base64).unwrap();
1126 let expected = "iVBORw0KGgoAAAANSUhEUgAAAMgAAACWCAYAAACb3McZAAAH0klEQVR4Xu2bSWhVZxiGv2gC7SKJWrRWxaGoULsW7L7gXlAMKApiN7pxI46ggnNQcDbOoAZUcCG4CCiIQ4MSkWKFLNSCihTR2ESTCNVb/lMTEmvu8OYuTN/nQBHb895zv+f9H+6ZWpHL5XLBBgEIfJZABYKwMiAwMAEEYXVAIA8BBGF5QABBWAMQ0AjwC6JxI2VCAEFMimZMjQCCaNxImRBAEJOiGVMjgCAaN1ImBBDEpGjG1AggiMaNlAkBBDEpmjE1AgiicSNlQgBBTIpmTI0AgmjcSJkQQBCTohlTI4AgGjdSJgQQxKRoxtQIIIjGjZQJAQQxKZoxNQIIonEjZUIAQUyKZkyNAIJo3EiZEEAQk6IZUyOAIBo3UiYEEMSkaMbUCCCIxo2UCQEEMSmaMTUCCKJxI2VCAEFMimZMjQCCaNxImRBAEJOiGVMjgCAaN1ImBBDEpGjG1AggiMaNlAkBBDEpmjE1AgiicSNlQgBBTIpmTI0AgmjcSJkQQBCTohlTI4AgGjdSJgQQxKRoxtQIIIjGjZQJAQQxKZoxNQIIonEjZUIAQUyKZkyNAIJo3EiZEEAQk6IZUyOAIBo3UiYEEMSkaMbUCCCIxo2UCQEEMSmaMTUCCPKR26NHj+LUqVNx69atuHDhQtTW1vYSvX37dhw4cCC6u7tj4sSJsXr16hg5cqRGnNSQIoAgH+vavHlzzJ49O9auXRvnzp3rFeTNmzdRV1cXHz58yP7J5XIxbdq02Lt375Aqmi+rEUCQT7glSfoKcunSpdizZ0+MGDEik+PVq1cxfPjwuHz5clRVVWnUSQ0ZAghSQJA1a9ZEOsVqaGiIHTt2xLNnz6Krqys7HRs/fvyQKZovqhFAkAKCpFOuO3fuxOjRo+Pdu3fR3t6e/ZIcPHgwpk6dqlEnNWQIIEgBQTZu3Bg3b96MioqKmDBhQjx58iQT5OTJk/1+QX599DLqGpr/U3wuF1FRUb71MOv7b6Lmq8qYMa42Hjz/K5p+/7Pfh6f/9tuG2eU7oPknIUgBQbZu3RpXrlyJ7du3Z9ceK1euzAQ5c+ZMjBkzpjc9kCDVaTF/V5PtlxZ3z1bzdVXMGPfvv69vao2WP9r6fZMfx9XEzz98G0/buuJpW2c8eN4eHd1/99tnIPkaf5kVP/U5lvkaH9T4CFJAkBUrVsT9+/dj6dKlkS7YOzo6It3ZOnr0aEyePHlQ8Al/+QQQJCJb9EmAtL18+TJGjRqVnVIdOnQo6uvro7m5Ofv7sGHDslu9aduyZUvMnDnzy2+YbzgoAghSAN/bt29j/vz58f79++zUKv2ZZJo7d+6gwBMeGgQQpEBPTU1NsWvXruw5SNra2tqiuro6Tpw4kf3J9v8mgCBl7Hcwr6Tke9Ul31e8evVqnD59OrsFnW4apGum9DoMW3kIIEh5OGYX7osWLYp012v69OnZon38+HGsX7++qCMM9KpLvnB6aLl8+fLYt29fdsu5sbEx7t69Gzt37izqmOxUmACCFGZU1B7Xrl2LdDqWFnraOjs7Y968eXHx4sWSXkn59FWXfAdP10cvXrzovZv28OHDWLduXSYKW3kIIEh5OGbPRV6/fh3Lli3r/cQkyO7du0t6JaUUQT796ufPn4/W1tZMErbyEECQ8nCM48eP997h6vnIBQsWxIYNG0p6JUUV5N69e9mpVRKy7wPMMo1n+zEIUqbqz549m93h6vsLMmfOnOy1+FJealQEuXHjRhw+fDg2bdoUU6ZMKdNEfEwigCBlWgfXr1/PXoFPF+lpS6dbCxcuzK5BKisriz5KqYKkFyn3798f27Zti7FjxxZ9HHYsjgCCFMep4F7pgnnx4sXZRXq6i3Xs2LHsqXx6d6uUrRRB0jGXLFmSvSc2adKkUg7DvkUSQJAiQRWzW0tLS3ZKle5gpf/rcNWqVUU9TMz3qkvPA8rPHf/Th5g9+xw5cqSo4xYzk/s+COK+Apg/LwEEYYFAIA8BBGF5QABBWAMQ0AjwC6JxI2VCAEFMimZMjQCCaNxImRBAEJOiGVMjgCAaN1ImBBDEpGjG1AggiMaNlAkBBDEpmjE1AgiicSNlQgBBTIpmTI0AgmjcSJkQQBCTohlTI4AgGjdSJgQQxKRoxtQIIIjGjZQJAQQxKZoxNQIIonEjZUIAQUyKZkyNAIJo3EiZEEAQk6IZUyOAIBo3UiYEEMSkaMbUCCCIxo2UCQEEMSmaMTUCCKJxI2VC4B+Ci/5sJeSfvgAAAABJRU5ErkJggg==";
1127 let expected_decoded = general_purpose::STANDARD.decode(expected).unwrap();
1128
1129 assert_eq!(expected_decoded[..2], result_decoded[..2]);
1133 exporter.close();
1134 }
1135
1136 #[test]
1137 #[cfg(feature = "plotly_static")]
1138 fn image_to_svg_string() {
1139 let plot = create_test_plot();
1140 let mut exporter = plotly_static::StaticExporterBuilder::default()
1141 .webdriver_port(get_unique_port())
1142 .build()
1143 .unwrap();
1144 let image_svg = exporter.to_svg(&plot, 200, 150, 1.0).unwrap();
1145
1146 assert!(!image_svg.is_empty());
1147
1148 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>";
1149 const LEN: usize = 10;
1152 assert_eq!(expected[..LEN], image_svg[..LEN]);
1153 exporter.close();
1154 }
1155
1156 #[test]
1157 #[cfg(feature = "plotly_static")]
1158 fn save_surface_to_png() {
1159 use crate::Surface;
1160 let mut plot = Plot::new();
1161 let z_matrix = vec![
1162 vec![1.0, 2.0, 3.0],
1163 vec![4.0, 5.0, 6.0],
1164 vec![7.0, 8.0, 9.0],
1165 ];
1166 let x_unique = vec![1.0, 2.0, 3.0];
1167 let y_unique = vec![4.0, 5.0, 6.0];
1168 let surface = Surface::new(z_matrix)
1169 .x(x_unique)
1170 .y(y_unique)
1171 .name("Surface");
1172
1173 plot.add_trace(surface);
1174 let dst = PathBuf::from("plotly_example_surface.png");
1175 let mut exporter = plotly_static::StaticExporterBuilder::default()
1176 .webdriver_port(get_unique_port())
1177 .build()
1178 .unwrap();
1179
1180 assert!(!exporter
1181 .to_base64(&plot, ImageFormat::PNG, 1024, 680, 1.0)
1182 .unwrap()
1183 .is_empty());
1184
1185 exporter
1186 .write_image(&plot, &dst, ImageFormat::PNG, 800, 600, 1.0)
1187 .unwrap();
1188 assert!(dst.exists());
1189
1190 let metadata = std::fs::metadata(&dst).expect("Could not retrieve file metadata");
1191 let file_size = metadata.len();
1192 assert!(file_size > 0,);
1193 #[cfg(not(feature = "debug"))]
1194 assert!(std::fs::remove_file(&dst).is_ok());
1195 exporter.close();
1196 }
1197}