1use std::{fs::File, io::Write, path::Path};
2
3use dyn_clone::DynClone;
4use erased_serde::Serialize as ErasedSerialize;
5use rand::{
6 distributions::{Alphanumeric, DistString},
7 thread_rng,
8};
9use rinja::Template;
10use serde::Serialize;
11
12use crate::{Configuration, Layout};
13
14#[derive(Template)]
15#[template(path = "plot.html", escape = "none")]
16struct PlotTemplate<'a> {
17 plot: &'a Plot,
18 js_scripts: String,
19}
20
21#[derive(Template)]
22#[template(path = "static_plot.html", escape = "none")]
23#[cfg(not(target_family = "wasm"))]
24struct StaticPlotTemplate<'a> {
25 plot: &'a Plot,
26 format: ImageFormat,
27 js_scripts: String,
28 width: usize,
29 height: usize,
30}
31
32#[derive(Template)]
33#[template(path = "inline_plot.html", escape = "none")]
34struct InlinePlotTemplate<'a> {
35 plot: &'a Plot,
36 plot_div_id: &'a str,
37}
38
39#[derive(Template)]
40#[template(path = "jupyter_notebook_plot.html", escape = "none")]
41struct JupyterNotebookPlotTemplate<'a> {
42 plot: &'a Plot,
43 plot_div_id: &'a str,
44}
45
46#[cfg(not(target_family = "wasm"))]
47const DEFAULT_HTML_APP_NOT_FOUND: &str = r#"Could not find default application for HTML files.
48Consider using the `to_html` method obtain a string representation instead. If using the `kaleido` feature the
49`write_image` method can be used to produce a static image in one of the following formats:
50- ImageFormat::PNG
51- ImageFormat::JPEG
52- ImageFormat::WEBP
53- ImageFormat::SVG
54- ImageFormat::PDF
55- ImageFormat::EPS
56
57Used as follows:
58let plot = Plot::new();
59...
60let width = 1024;
61let height = 680;
62let scale = 1.0;
63plot.write_image("filename", ImageFormat::PNG, width, height, scale);
64
65See https://plotly.github.io/plotly.rs/content/getting_started.html for further details.
66"#;
67
68#[derive(Debug)]
70pub enum ImageFormat {
71 PNG,
72 JPEG,
73 WEBP,
74 SVG,
75 PDF,
76 EPS,
77}
78
79impl std::fmt::Display for ImageFormat {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 write!(
82 f,
83 "{}",
84 match self {
85 Self::PNG => "png",
86 Self::JPEG => "jpeg",
87 Self::WEBP => "webp",
88 Self::SVG => "svg",
89 Self::PDF => "pdf",
90 Self::EPS => "eps",
91 }
92 )
93 }
94}
95
96pub trait Trace: DynClone + ErasedSerialize {
99 fn to_json(&self) -> String;
100}
101
102dyn_clone::clone_trait_object!(Trace);
103erased_serde::serialize_trait_object!(Trace);
104
105#[derive(Default, Serialize, Clone)]
106#[serde(transparent)]
107pub struct Traces {
108 traces: Vec<Box<dyn Trace>>,
109}
110
111impl Traces {
112 pub fn new() -> Self {
113 Self {
114 traces: Vec::with_capacity(1),
115 }
116 }
117
118 pub fn push(&mut self, trace: Box<dyn Trace>) {
119 self.traces.push(trace)
120 }
121
122 pub fn len(&self) -> usize {
123 self.traces.len()
124 }
125
126 pub fn is_empty(&self) -> bool {
127 self.traces.is_empty()
128 }
129
130 pub fn iter(&self) -> std::slice::Iter<'_, Box<dyn Trace>> {
131 self.traces.iter()
132 }
133
134 pub fn to_json(&self) -> String {
135 serde_json::to_string(self).unwrap()
136 }
137}
138
139#[derive(Default, Serialize, Clone)]
178pub struct Plot {
179 #[serde(rename = "data")]
180 traces: Traces,
181 layout: Layout,
182 #[serde(rename = "config")]
183 configuration: Configuration,
184 #[serde(skip)]
185 js_scripts: String,
186}
187
188impl Plot {
189 pub fn new() -> Plot {
191 Plot {
192 traces: Traces::new(),
193 js_scripts: Self::js_scripts(),
194 ..Default::default()
195 }
196 }
197
198 #[cfg(feature = "plotly_embed_js")]
204 pub fn use_cdn_js(&mut self) {
205 self.js_scripts = Self::online_cdn_js();
206 }
207
208 pub fn add_trace(&mut self, trace: Box<dyn Trace>) {
210 self.traces.push(trace);
211 }
212
213 pub fn add_traces(&mut self, traces: Vec<Box<dyn Trace>>) {
215 for trace in traces {
216 self.add_trace(trace);
217 }
218 }
219
220 pub fn set_layout(&mut self, layout: Layout) {
222 self.layout = layout;
223 }
224
225 pub fn set_configuration(&mut self, configuration: Configuration) {
227 self.configuration = configuration;
228 }
229
230 pub fn data(&self) -> &Traces {
232 &self.traces
233 }
234
235 pub fn layout(&self) -> &Layout {
237 &self.layout
238 }
239
240 pub fn configuration(&self) -> &Configuration {
242 &self.configuration
243 }
244
245 #[cfg(not(target_family = "wasm"))]
250 pub fn show(&self) {
251 use std::env;
252
253 let rendered = self.render();
254
255 let mut temp = env::temp_dir();
257 let mut plot_name = Alphanumeric.sample_string(&mut thread_rng(), 22);
258 plot_name.push_str(".html");
259 plot_name = format!("plotly_{}", plot_name);
260 temp.push(plot_name);
261
262 let temp_path = temp.to_str().unwrap();
264
265 {
266 let mut file = File::create(temp_path).unwrap();
267 file.write_all(rendered.as_bytes())
268 .expect("failed to write html output");
269 file.flush().unwrap();
270 }
271
272 Plot::show_with_default_app(temp_path);
274 }
275
276 #[cfg(not(target_family = "wasm"))]
282 pub fn show_html<P: AsRef<Path> + std::clone::Clone>(&self, filename: P) {
283 let path = filename.as_ref().to_str().unwrap();
284 self.write_html(filename.clone());
285 Plot::show_with_default_app(path);
287 }
288
289 #[cfg(not(target_family = "wasm"))]
292 pub fn show_image(&self, format: ImageFormat, width: usize, height: usize) {
293 use std::env;
294
295 let rendered = self.render_static(format, width, height);
296
297 let mut temp = env::temp_dir();
299 let mut plot_name = Alphanumeric.sample_string(&mut thread_rng(), 22);
300 plot_name.push_str(".html");
301 plot_name = format!("plotly_{}", plot_name);
302 temp.push(plot_name);
303
304 let temp_path = temp.to_str().unwrap();
306
307 {
308 let mut file = File::create(temp_path).unwrap();
309 file.write_all(rendered.as_bytes())
310 .expect("failed to write html output");
311 file.flush().unwrap();
312 }
313
314 Plot::show_with_default_app(temp_path);
316 }
317
318 pub fn write_html<P: AsRef<Path>>(&self, filename: P) {
323 let rendered = self.to_html();
324
325 let mut file =
326 File::create(filename).expect("Provided filepath does not exist or is not accessible");
327 file.write_all(rendered.as_bytes())
328 .expect("failed to write html output");
329 file.flush().unwrap();
330 }
331
332 pub fn to_html(&self) -> String {
338 self.render()
339 }
340
341 pub fn to_inline_html(&self, plot_div_id: Option<&str>) -> String {
355 let plot_div_id = match plot_div_id {
356 Some(id) => id.to_string(),
357 None => Alphanumeric.sample_string(&mut thread_rng(), 20),
358 };
359 self.render_inline(&plot_div_id)
360 }
361
362 fn to_jupyter_notebook_html(&self) -> String {
363 let plot_div_id = Alphanumeric.sample_string(&mut thread_rng(), 20);
364
365 let tmpl = JupyterNotebookPlotTemplate {
366 plot: self,
367 plot_div_id: &plot_div_id,
368 };
369 tmpl.render().unwrap()
370 }
371
372 pub fn notebook_display(&self) {
374 let plot_data = self.to_jupyter_notebook_html();
375 println!(
376 "EVCXR_BEGIN_CONTENT text/html\n{}\nEVCXR_END_CONTENT",
377 plot_data
378 );
379 }
380
381 pub fn lab_display(&self) {
383 let plot_data = self.to_json();
384 println!(
385 "EVCXR_BEGIN_CONTENT application/vnd.plotly.v1+json\n{}\nEVCXR_END_CONTENT",
386 plot_data
387 );
388 }
389
390 pub fn evcxr_display(&self) {
393 self.lab_display();
394 }
395
396 #[cfg(feature = "kaleido")]
399 pub fn write_image<P: AsRef<Path>>(
400 &self,
401 filename: P,
402 format: ImageFormat,
403 width: usize,
404 height: usize,
405 scale: f64,
406 ) {
407 let kaleido = plotly_kaleido::Kaleido::new();
408 kaleido
409 .save(
410 filename.as_ref(),
411 &serde_json::to_value(self).unwrap(),
412 &format.to_string(),
413 width,
414 height,
415 scale,
416 )
417 .unwrap_or_else(|_| panic!("failed to export plot to {:?}", filename.as_ref()));
418 }
419
420 #[cfg(feature = "kaleido")]
424 pub fn to_base64(
425 &self,
426 format: ImageFormat,
427 width: usize,
428 height: usize,
429 scale: f64,
430 ) -> String {
431 match format {
432 ImageFormat::JPEG | ImageFormat::PNG | ImageFormat::WEBP => {
433 let kaleido = plotly_kaleido::Kaleido::new();
434 kaleido
435 .image_to_string(
436 &serde_json::to_value(self).unwrap(),
437 &format.to_string(),
438 width,
439 height,
440 scale,
441 )
442 .unwrap_or_else(|_| panic!("Kaleido failed to generate image"))
443 }
444 _ => {
445 eprintln!("Cannot generate base64 string for ImageFormat:{format}. Allowed formats are JPEG, PNG, WEBP");
446 String::default()
447 }
448 }
449 }
450
451 #[cfg(feature = "kaleido")]
453 pub fn to_svg(&self, width: usize, height: usize, scale: f64) -> String {
454 let kaleido = plotly_kaleido::Kaleido::new();
455 kaleido
456 .image_to_string(
457 &serde_json::to_value(self).unwrap(),
458 "svg",
459 width,
460 height,
461 scale,
462 )
463 .unwrap_or_else(|_| panic!("Kaleido failed to generate image"))
464 }
465
466 fn render(&self) -> String {
467 let tmpl = PlotTemplate {
468 plot: self,
469 js_scripts: self.js_scripts.clone(),
470 };
471 tmpl.render().unwrap()
472 }
473
474 #[cfg(not(target_family = "wasm"))]
475 fn render_static(&self, format: ImageFormat, width: usize, height: usize) -> String {
476 let tmpl = StaticPlotTemplate {
477 plot: self,
478 format,
479 js_scripts: self.js_scripts.clone(),
480 width,
481 height,
482 };
483 tmpl.render().unwrap()
484 }
485
486 fn render_inline(&self, plot_div_id: &str) -> String {
487 let tmpl = InlinePlotTemplate {
488 plot: self,
489 plot_div_id,
490 };
491 tmpl.render().unwrap()
492 }
493
494 fn js_scripts() -> String {
495 if cfg!(feature = "plotly_embed_js") {
496 Self::offline_js_sources()
497 } else {
498 Self::online_cdn_js()
499 }
500 }
501
502 fn offline_js_sources() -> String {
503 let local_plotly_js = include_str!("../templates/plotly.min.js");
504 let local_tex_mml_js = include_str!("../templates/tex-mml-chtml-3.2.0.js");
505 let local_tex_svg_js = include_str!("../templates/tex-svg-3.2.2.js");
506 format!(
507 "<script type=\"text/javascript\">{}</script>\n
508 <script type=\"text/javascript\">
509 /**
510 * tex-mml-chtml JS script
511 **/
512 {}
513 </script>\n
514 <script type=\"text/javascript\">
515 /**
516 * tex-svg JS script
517 **/
518 {}
519 </script>\n",
520 local_plotly_js, local_tex_mml_js, local_tex_svg_js
521 )
522 .to_string()
523 }
524
525 fn online_cdn_js() -> String {
526 r##"<script src="https://cdn.plot.ly/plotly-2.12.1.min.js"></script>
527 <script src="https://cdn.jsdelivr.net/npm/mathjax@3.2.2/es5/tex-svg.js"></script>
528 <script src="https://cdn.jsdelivr.net/npm/mathjax@3.2.0/es5/tex-mml-chtml.js"></script>
529 "##
530 .to_string()
531 }
532
533 pub fn to_json(&self) -> String {
534 serde_json::to_string(self).unwrap()
535 }
536
537 #[cfg(feature = "wasm")]
538 pub fn to_js_object(&self) -> js_sys::Object {
540 use wasm_bindgen::JsCast;
541 js_sys::JSON::parse(&self.to_json())
545 .expect("Invalid JSON")
546 .dyn_into::<js_sys::Object>()
547 .expect("Invalid JSON structure - expected a top-level Object")
548 }
549
550 #[cfg(all(unix, not(target_os = "android"), not(target_os = "macos")))]
551 fn show_with_default_app(temp_path: &str) {
552 use std::process::Command;
553 Command::new("xdg-open")
554 .args([temp_path])
555 .output()
556 .expect(DEFAULT_HTML_APP_NOT_FOUND);
557 }
558
559 #[cfg(target_os = "macos")]
560 fn show_with_default_app(temp_path: &str) {
561 use std::process::Command;
562 Command::new("open")
563 .args([temp_path])
564 .output()
565 .expect(DEFAULT_HTML_APP_NOT_FOUND);
566 }
567
568 #[cfg(target_os = "windows")]
569 fn show_with_default_app(temp_path: &str) {
570 use std::process::Command;
571 Command::new("cmd")
572 .args(&["/C", "start", &format!(r#"{}"#, temp_path)])
573 .spawn()
574 .expect(DEFAULT_HTML_APP_NOT_FOUND);
575 }
576}
577
578impl PartialEq for Plot {
579 fn eq(&self, other: &Self) -> bool {
580 self.to_json() == other.to_json()
581 }
582}
583
584#[cfg(test)]
585mod tests {
586 use std::path::PathBuf;
587
588 #[cfg(feature = "kaleido")]
589 use base64::{engine::general_purpose, Engine as _};
590 use serde_json::{json, to_value};
591
592 use super::*;
593 use crate::Scatter;
594
595 fn create_test_plot() -> Plot {
596 let trace1 = Scatter::new(vec![0, 1, 2], vec![6, 10, 2]).name("trace1");
597 let mut plot = Plot::new();
598 plot.add_trace(trace1);
599 plot
600 }
601
602 #[test]
603 fn inline_plot() {
604 let plot = create_test_plot();
605 let inline_plot_data = plot.to_inline_html(Some("replace_this_with_the_div_id"));
606 assert!(inline_plot_data.contains("replace_this_with_the_div_id"));
607 plot.to_inline_html(None);
608 }
609
610 #[test]
611 fn jupyter_notebook_plot() {
612 let plot = create_test_plot();
613 plot.to_jupyter_notebook_html();
614 }
615
616 #[test]
617 fn notebook_display() {
618 let plot = create_test_plot();
619 plot.notebook_display();
620 }
621
622 #[test]
623 fn lab_display() {
624 let plot = create_test_plot();
625 plot.lab_display();
626 }
627
628 #[test]
629 fn plot_serialize_simple() {
630 let plot = create_test_plot();
631 let expected = json!({
632 "data": [
633 {
634 "type": "scatter",
635 "name": "trace1",
636 "x": [0, 1, 2],
637 "y": [6, 10, 2]
638 }
639 ],
640 "layout": {},
641 "config": {},
642 });
643
644 assert_eq!(to_value(plot).unwrap(), expected);
645 }
646
647 #[test]
648 fn plot_serialize_with_layout() {
649 let mut plot = create_test_plot();
650 let layout = Layout::new().title("Title");
651 plot.set_layout(layout);
652
653 let expected = json!({
654 "data": [
655 {
656 "type": "scatter",
657 "name": "trace1",
658 "x": [0, 1, 2],
659 "y": [6, 10, 2]
660 }
661 ],
662 "layout": {
663 "title": {
664 "text": "Title"
665 }
666 },
667 "config": {},
668 });
669
670 assert_eq!(to_value(plot).unwrap(), expected);
671 }
672
673 #[test]
674 fn data_to_json() {
675 let plot = create_test_plot();
676 let expected = json!([
677 {
678 "type": "scatter",
679 "name": "trace1",
680 "x": [0, 1, 2],
681 "y": [6, 10, 2]
682 }
683 ]);
684
685 assert_eq!(to_value(plot.data()).unwrap(), expected);
686 }
687
688 #[test]
689 fn empty_layout_to_json() {
690 let plot = create_test_plot();
691 let expected = json!({});
692
693 assert_eq!(to_value(plot.layout()).unwrap(), expected);
694 }
695
696 #[test]
697 fn layout_to_json() {
698 let mut plot = create_test_plot();
699 let layout = Layout::new().title("TestTitle");
700 plot.set_layout(layout);
701
702 let expected = json!({
703 "title": {"text": "TestTitle"}
704 });
705
706 assert_eq!(to_value(plot.layout()).unwrap(), expected);
707 }
708
709 #[test]
710 fn plot_eq() {
711 let plot1 = create_test_plot();
712 let plot2 = create_test_plot();
713
714 assert!(plot1 == plot2);
715 }
716
717 #[test]
718 fn plot_neq() {
719 let plot1 = create_test_plot();
720 let trace2 = Scatter::new(vec![10, 1, 2], vec![6, 10, 2]).name("trace2");
721 let mut plot2 = Plot::new();
722 plot2.add_trace(trace2);
723
724 assert!(plot1 != plot2);
725 }
726
727 #[test]
728 fn plot_clone() {
729 let plot1 = create_test_plot();
730 let plot2 = plot1.clone();
731
732 assert!(plot1 == plot2);
733 }
734
735 #[test]
736 #[ignore] #[cfg(not(feature = "wasm"))]
738 fn show_image() {
739 let plot = create_test_plot();
740 plot.show_image(ImageFormat::PNG, 1024, 680);
741 }
742
743 #[test]
744 fn save_html() {
745 let plot = create_test_plot();
746 let dst = PathBuf::from("example.html");
747 plot.write_html(&dst);
748 assert!(dst.exists());
749 assert!(std::fs::remove_file(&dst).is_ok());
750 assert!(!dst.exists());
751 }
752
753 #[cfg(not(target_os = "macos"))]
754 #[test]
755 #[cfg(feature = "kaleido")]
756 fn save_to_png() {
757 let plot = create_test_plot();
758 let dst = PathBuf::from("example.png");
759 plot.write_image(&dst, ImageFormat::PNG, 1024, 680, 1.0);
760 assert!(dst.exists());
761 assert!(std::fs::remove_file(&dst).is_ok());
762 assert!(!dst.exists());
763 }
764
765 #[cfg(not(target_os = "macos"))]
766 #[test]
767 #[cfg(feature = "kaleido")]
768 fn save_to_jpeg() {
769 let plot = create_test_plot();
770 let dst = PathBuf::from("example.jpeg");
771 plot.write_image(&dst, ImageFormat::JPEG, 1024, 680, 1.0);
772 assert!(dst.exists());
773 assert!(std::fs::remove_file(&dst).is_ok());
774 assert!(!dst.exists());
775 }
776
777 #[cfg(not(target_os = "macos"))]
778 #[test]
779 #[cfg(feature = "kaleido")]
780 fn save_to_svg() {
781 let plot = create_test_plot();
782 let dst = PathBuf::from("example.svg");
783 plot.write_image(&dst, ImageFormat::SVG, 1024, 680, 1.0);
784 assert!(dst.exists());
785 assert!(std::fs::remove_file(&dst).is_ok());
786 assert!(!dst.exists());
787 }
788
789 #[test]
790 #[ignore] #[cfg(feature = "kaleido")]
792 fn save_to_eps() {
793 let plot = create_test_plot();
794 let dst = PathBuf::from("example.eps");
795 plot.write_image(&dst, ImageFormat::EPS, 1024, 680, 1.0);
796 assert!(dst.exists());
797 assert!(std::fs::remove_file(&dst).is_ok());
798 assert!(!dst.exists());
799 }
800
801 #[cfg(not(target_os = "macos"))]
802 #[test]
803 #[cfg(feature = "kaleido")]
804 fn save_to_pdf() {
805 let plot = create_test_plot();
806 let dst = PathBuf::from("example.pdf");
807 plot.write_image(&dst, ImageFormat::PDF, 1024, 680, 1.0);
808 assert!(dst.exists());
809 assert!(std::fs::remove_file(&dst).is_ok());
810 assert!(!dst.exists());
811 }
812
813 #[cfg(not(target_os = "macos"))]
814 #[test]
815 #[cfg(feature = "kaleido")]
816 fn save_to_webp() {
817 let plot = create_test_plot();
818 let dst = PathBuf::from("example.webp");
819 plot.write_image(&dst, ImageFormat::WEBP, 1024, 680, 1.0);
820 assert!(dst.exists());
821 assert!(std::fs::remove_file(&dst).is_ok());
822 assert!(!dst.exists());
823 }
824
825 #[test]
826 #[cfg(not(target_os = "macos"))]
827 #[cfg(feature = "kaleido")]
828 fn image_to_base64() {
829 let plot = create_test_plot();
830
831 let image_base64 = plot.to_base64(ImageFormat::PNG, 200, 150, 1.0);
832
833 assert!(!image_base64.is_empty());
834
835 let result_decoded = general_purpose::STANDARD.decode(image_base64).unwrap();
836 let expected = "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";
837 let expected_decoded = general_purpose::STANDARD.decode(expected).unwrap();
838
839 assert_eq!(expected_decoded[..2], result_decoded[..2]);
843 }
844
845 #[test]
846 #[cfg(feature = "kaleido")]
847 fn image_to_base64_invalid_format() {
848 let plot = create_test_plot();
849 let image_base64 = plot.to_base64(ImageFormat::EPS, 200, 150, 1.0);
850 assert!(image_base64.is_empty());
851 }
852
853 #[test]
854 #[cfg(not(target_os = "macos"))]
855 #[cfg(feature = "kaleido")]
856 fn image_to_svg_string() {
857 let plot = create_test_plot();
858 let image_svg = plot.to_svg(200, 150, 1.0);
859
860 assert!(!image_svg.is_empty());
861
862 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>";
863 const LEN: usize = 10;
866 assert_eq!(expected[..LEN], image_svg[..LEN]);
867 }
868}