1#![allow(dead_code)]
7#![allow(missing_docs)]
8
9use crate::error::{IoError, Result};
10use crate::metadata::Metadata;
11use scirs2_core::ndarray::Array2;
12use serde::{Deserialize, Serialize};
13use std::fs::File;
14use std::io::Write;
15use std::path::Path;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum VisualizationFormat {
20 PlotlyJson,
22 MatplotlibPython,
24 Gnuplot,
26 D3Json,
28 VegaLite,
30 BokehJson,
32 Svg,
34 Html,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40#[serde(rename_all = "lowercase")]
41pub enum PlotType {
42 Line,
43 Scatter,
44 Bar,
45 Histogram,
46 Heatmap,
47 Surface,
48 Contour,
49 Box,
50 Violin,
51 Pie,
52 Area,
53 Stream,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct AxisConfig {
59 pub title: Option<String>,
60 pub range: Option<[f64; 2]>,
61 pub scale: Option<ScaleType>,
62 pub tick_format: Option<String>,
63 pub grid: bool,
64}
65
66impl Default for AxisConfig {
67 fn default() -> Self {
68 Self {
69 title: None,
70 range: None,
71 scale: None,
72 tick_format: None,
73 grid: true,
74 }
75 }
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80#[serde(rename_all = "lowercase")]
81pub enum ScaleType {
82 Linear,
83 Log,
84 SymLog,
85 Sqrt,
86 Power(f64),
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct PlotConfig {
92 pub title: Option<String>,
93 pub width: Option<u32>,
94 pub height: Option<u32>,
95 pub x_axis: AxisConfig,
96 pub y_axis: AxisConfig,
97 pub z_axis: Option<AxisConfig>,
98 pub color_scale: Option<String>,
99 pub theme: Option<String>,
100 pub annotations: Vec<Annotation>,
101}
102
103impl Default for PlotConfig {
104 fn default() -> Self {
105 Self {
106 title: None,
107 width: Some(800),
108 height: Some(600),
109 x_axis: AxisConfig::default(),
110 y_axis: AxisConfig::default(),
111 z_axis: None,
112 color_scale: None,
113 theme: None,
114 annotations: Vec::new(),
115 }
116 }
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct Annotation {
122 pub text: String,
123 pub x: f64,
124 pub y: f64,
125 pub arrow: bool,
126}
127
128#[derive(Debug, Clone)]
130pub struct VisualizationBuilder {
131 data: Vec<DataSeries>,
132 config: PlotConfig,
133 metadata: Metadata,
134}
135
136#[derive(Debug, Clone)]
138pub struct DataSeries {
139 pub name: Option<String>,
140 pub x: Option<Vec<f64>>,
141 pub y: Vec<f64>,
142 pub z: Option<Vec<f64>>,
143 pub plot_type: PlotType,
144 pub style: SeriesStyle,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct SeriesStyle {
150 pub color: Option<String>,
151 pub line_style: Option<String>,
152 pub marker: Option<String>,
153 pub opacity: Option<f64>,
154 pub size: Option<f64>,
155}
156
157impl Default for SeriesStyle {
158 fn default() -> Self {
159 Self {
160 color: None,
161 line_style: None,
162 marker: None,
163 opacity: Some(1.0),
164 size: None,
165 }
166 }
167}
168
169impl Default for VisualizationBuilder {
170 fn default() -> Self {
171 Self::new()
172 }
173}
174
175impl VisualizationBuilder {
176 pub fn new() -> Self {
178 Self {
179 data: Vec::new(),
180 config: PlotConfig::default(),
181 metadata: Metadata::new(),
182 }
183 }
184
185 pub fn title(mut self, title: impl Into<String>) -> Self {
187 self.config.title = Some(title.into());
188 self
189 }
190
191 pub fn dimensions(mut self, width: u32, height: u32) -> Self {
193 self.config.width = Some(width);
194 self.config.height = Some(height);
195 self
196 }
197
198 pub fn x_axis(mut self, title: impl Into<String>) -> Self {
200 self.config.x_axis.title = Some(title.into());
201 self
202 }
203
204 pub fn y_axis(mut self, title: impl Into<String>) -> Self {
206 self.config.y_axis.title = Some(title.into());
207 self
208 }
209
210 pub fn add_line(mut self, x: &[f64], y: &[f64], name: Option<&str>) -> Self {
212 self.data.push(DataSeries {
213 name: name.map(|s| s.to_string()),
214 x: Some(x.to_vec()),
215 y: y.to_vec(),
216 z: None,
217 plot_type: PlotType::Line,
218 style: SeriesStyle::default(),
219 });
220 self
221 }
222
223 pub fn add_scatter(mut self, x: &[f64], y: &[f64], name: Option<&str>) -> Self {
225 self.data.push(DataSeries {
226 name: name.map(|s| s.to_string()),
227 x: Some(x.to_vec()),
228 y: y.to_vec(),
229 z: None,
230 plot_type: PlotType::Scatter,
231 style: SeriesStyle::default(),
232 });
233 self
234 }
235
236 pub fn add_histogram(mut self, values: &[f64], name: Option<&str>) -> Self {
238 self.data.push(DataSeries {
239 name: name.map(|s| s.to_string()),
240 x: None,
241 y: values.to_vec(),
242 z: None,
243 plot_type: PlotType::Histogram,
244 style: SeriesStyle::default(),
245 });
246 self
247 }
248
249 pub fn add_heatmap(mut self, z: Array2<f64>, name: Option<&str>) -> Self {
251 let flat_z: Vec<f64> = z.iter().cloned().collect();
252 self.data.push(DataSeries {
253 name: name.map(|s| s.to_string()),
254 x: Some(vec![z.shape()[1] as f64]), y: vec![z.shape()[0] as f64],
256 z: Some(flat_z),
257 plot_type: PlotType::Heatmap,
258 style: SeriesStyle::default(),
259 });
260 self
261 }
262
263 pub fn export(self, format: VisualizationFormat, path: impl AsRef<Path>) -> Result<()> {
265 let exporter = get_exporter(format);
266 exporter.export(&self.data, &self.config, &self.metadata, path.as_ref())
267 }
268
269 pub fn to_string(self, format: VisualizationFormat) -> Result<String> {
271 let exporter = get_exporter(format);
272 exporter.to_string(&self.data, &self.config, &self.metadata)
273 }
274}
275
276trait VisualizationExporter {
278 fn export(
279 &self,
280 data: &[DataSeries],
281 config: &PlotConfig,
282 metadata: &Metadata,
283 path: &Path,
284 ) -> Result<()>;
285 fn to_string(
286 &self,
287 data: &[DataSeries],
288 config: &PlotConfig,
289 metadata: &Metadata,
290 ) -> Result<String>;
291}
292
293#[allow(dead_code)]
295fn get_exporter(format: VisualizationFormat) -> Box<dyn VisualizationExporter> {
296 match format {
297 VisualizationFormat::PlotlyJson => Box::new(PlotlyExporter),
298 VisualizationFormat::MatplotlibPython => Box::new(MatplotlibExporter),
299 VisualizationFormat::Gnuplot => Box::new(GnuplotExporter),
300 VisualizationFormat::VegaLite => Box::new(VegaLiteExporter),
301 VisualizationFormat::D3Json => Box::new(PlotlyExporter), VisualizationFormat::BokehJson => Box::new(PlotlyExporter), VisualizationFormat::Svg => Box::new(PlotlyExporter), VisualizationFormat::Html => Box::new(PlotlyExporter), }
306}
307
308struct PlotlyExporter;
310
311impl VisualizationExporter for PlotlyExporter {
312 fn export(
313 &self,
314 data: &[DataSeries],
315 config: &PlotConfig,
316 metadata: &Metadata,
317 path: &Path,
318 ) -> Result<()> {
319 let json_str = self.to_string(data, config, metadata)?;
320 let mut file = File::create(path).map_err(IoError::Io)?;
321 file.write_all(json_str.as_bytes()).map_err(IoError::Io)?;
322 Ok(())
323 }
324
325 fn to_string(
326 &self,
327 data: &[DataSeries],
328 config: &PlotConfig,
329 metadata: &Metadata,
330 ) -> Result<String> {
331 let mut traces = Vec::new();
332
333 for series in data {
334 let trace = match series.plot_type {
335 PlotType::Line | PlotType::Scatter => {
336 serde_json::json!({
337 "type": "scatter",
338 "mode": if matches!(series.plot_type, PlotType::Line) { "lines" } else { "markers" },
339 "name": series.name,
340 "x": series.x,
341 "y": series.y,
342 "line": {
343 "color": series.style.color,
344 "dash": series.style.line_style,
345 },
346 "marker": {
347 "symbol": series.style.marker,
348 "size": series.style.size,
349 },
350 "opacity": series.style.opacity,
351 })
352 }
353 PlotType::Histogram => {
354 serde_json::json!({
355 "type": "histogram",
356 "name": series.name,
357 "x": series.y,
358 "opacity": series.style.opacity,
359 })
360 }
361 PlotType::Heatmap => {
362 let cols = series.x.as_ref().unwrap()[0] as usize;
363 let _rows = series.y[0] as usize;
364 let z_data: Vec<Vec<f64>> = series
365 .z
366 .as_ref()
367 .unwrap()
368 .chunks(cols)
369 .map(|chunk| chunk.to_vec())
370 .collect();
371
372 serde_json::json!({
373 "type": "heatmap",
374 "name": series.name,
375 "z": z_data,
376 "colorscale": config.color_scale,
377 })
378 }
379 _ => continue,
380 };
381 traces.push(trace);
382 }
383
384 let layout = serde_json::json!({
385 "title": config.title,
386 "width": config.width,
387 "height": config.height,
388 "xaxis": {
389 "title": config.x_axis.title,
390 "range": config.x_axis.range,
391 "showgrid": config.x_axis.grid,
392 },
393 "yaxis": {
394 "title": config.y_axis.title,
395 "range": config.y_axis.range,
396 "showgrid": config.y_axis.grid,
397 },
398 "annotations": config.annotations.iter().map(|ann| {
399 serde_json::json!({
400 "text": ann.text,
401 "x": ann.x,
402 "y": ann.y,
403 "showarrow": ann.arrow,
404 })
405 }).collect::<Vec<_>>(),
406 });
407
408 let plot_data = serde_json::json!({
409 "data": traces,
410 "layout": layout,
411 });
412
413 serde_json::to_string_pretty(&plot_data)
414 .map_err(|e| IoError::SerializationError(e.to_string()))
415 }
416}
417
418struct MatplotlibExporter;
420
421impl VisualizationExporter for MatplotlibExporter {
422 fn export(
423 &self,
424 data: &[DataSeries],
425 config: &PlotConfig,
426 metadata: &Metadata,
427 path: &Path,
428 ) -> Result<()> {
429 let script = self.to_string(data, config, metadata)?;
430 let mut file = File::create(path).map_err(IoError::Io)?;
431 file.write_all(script.as_bytes()).map_err(IoError::Io)?;
432 Ok(())
433 }
434
435 fn to_string(
436 &self,
437 data: &[DataSeries],
438 config: &PlotConfig,
439 metadata: &Metadata,
440 ) -> Result<String> {
441 let mut script = String::from("import matplotlib.pyplot as plt\nimport numpy as np\n\n");
442
443 script.push_str(&format!(
445 "fig, ax = plt.subplots(figsize=({}, {}))\n\n",
446 config.width.unwrap_or(800) as f64 / 100.0,
447 config.height.unwrap_or(600) as f64 / 100.0
448 ));
449
450 for series in data {
452 match series.plot_type {
453 PlotType::Line => {
454 if let Some(x) = &series.x {
455 script.push_str(&format!("ax.plot({:?}, {:?}", x, series.y));
456 if let Some(name) = &series.name {
457 script.push_str(&format!(", label='{}'", name));
458 }
459 script.push_str(")\n");
460 }
461 }
462 PlotType::Scatter => {
463 if let Some(x) = &series.x {
464 script.push_str(&format!("ax.scatter({:?}, {:?}", x, series.y));
465 if let Some(name) = &series.name {
466 script.push_str(&format!(", label='{}'", name));
467 }
468 script.push_str(")\n");
469 }
470 }
471 PlotType::Histogram => {
472 script.push_str(&format!("ax.hist({:?}", series.y));
473 if let Some(name) = &series.name {
474 script.push_str(&format!(", label='{}'", name));
475 }
476 script.push_str(")\n");
477 }
478 _ => continue,
479 }
480 }
481
482 if let Some(title) = &config.title {
484 script.push_str(&format!("\nax.set_title('{}')\n", title));
485 }
486 if let Some(xlabel) = &config.x_axis.title {
487 script.push_str(&format!("ax.set_xlabel('{}')\n", xlabel));
488 }
489 if let Some(ylabel) = &config.y_axis.title {
490 script.push_str(&format!("ax.set_ylabel('{}')\n", ylabel));
491 }
492
493 script.push_str("\nax.grid(True)\n");
494 script.push_str("ax.legend()\n");
495 script.push_str("plt.tight_layout()\n");
496 script.push_str("plt.show()\n");
497
498 Ok(script)
499 }
500}
501
502struct GnuplotExporter;
504
505impl VisualizationExporter for GnuplotExporter {
506 fn export(
507 &self,
508 data: &[DataSeries],
509 config: &PlotConfig,
510 metadata: &Metadata,
511 path: &Path,
512 ) -> Result<()> {
513 let script = self.to_string(data, config, metadata)?;
514 let mut file = File::create(path).map_err(IoError::Io)?;
515 file.write_all(script.as_bytes()).map_err(IoError::Io)?;
516 Ok(())
517 }
518
519 fn to_string(
520 &self,
521 data: &[DataSeries],
522 config: &PlotConfig,
523 metadata: &Metadata,
524 ) -> Result<String> {
525 let mut script = String::new();
526
527 script.push_str("set terminal png size ");
529 script.push_str(&format!(
530 "{},{}\n",
531 config.width.unwrap_or(800),
532 config.height.unwrap_or(600)
533 ));
534 script.push_str("set output 'plot.png'\n\n");
535
536 if let Some(title) = &config.title {
538 script.push_str(&format!("set title '{}'\n", title));
539 }
540 if let Some(xlabel) = &config.x_axis.title {
541 script.push_str(&format!("set xlabel '{}'\n", xlabel));
542 }
543 if let Some(ylabel) = &config.y_axis.title {
544 script.push_str(&format!("set ylabel '{}'\n", ylabel));
545 }
546
547 script.push_str("set grid\n\n");
548
549 script.push_str("plot ");
551 let mut first = true;
552
553 for (i, series) in data.iter().enumerate() {
554 if !first {
555 script.push_str(", ");
556 }
557 first = false;
558
559 match series.plot_type {
560 PlotType::Line => {
561 script.push_str(&format!(
562 "'-' using 1:2 with lines title '{}'",
563 series.name.as_deref().unwrap_or(&format!("Series {}", i))
564 ));
565 }
566 PlotType::Scatter => {
567 script.push_str(&format!(
568 "'-' using 1:2 with points title '{}'",
569 series.name.as_deref().unwrap_or(&format!("Series {}", i))
570 ));
571 }
572 _ => continue,
573 }
574 }
575 script.push_str("\n\n");
576
577 for series in data {
579 if let Some(x) = &series.x {
580 for (xi, yi) in x.iter().zip(series.y.iter()) {
581 script.push_str(&format!("{} {}\n", xi, yi));
582 }
583 }
584 script.push_str("e\n");
585 }
586
587 Ok(script)
588 }
589}
590
591struct VegaLiteExporter;
593
594impl VisualizationExporter for VegaLiteExporter {
595 fn export(
596 &self,
597 data: &[DataSeries],
598 config: &PlotConfig,
599 metadata: &Metadata,
600 path: &Path,
601 ) -> Result<()> {
602 let spec = self.to_string(data, config, metadata)?;
603 let mut file = File::create(path).map_err(IoError::Io)?;
604 file.write_all(spec.as_bytes()).map_err(IoError::Io)?;
605 Ok(())
606 }
607
608 fn to_string(
609 &self,
610 data: &[DataSeries],
611 config: &PlotConfig,
612 metadata: &Metadata,
613 ) -> Result<String> {
614 let mut data_values = Vec::new();
615
616 for series in data {
617 if let Some(x) = &series.x {
618 for (xi, yi) in x.iter().zip(series.y.iter()) {
619 data_values.push(serde_json::json!({
620 "x": xi,
621 "y": yi,
622 "series": series.name.as_deref().unwrap_or("default"),
623 }));
624 }
625 }
626 }
627
628 let spec = serde_json::json!({
629 "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
630 "title": config.title,
631 "width": config.width,
632 "height": config.height,
633 "data": {
634 "values": data_values
635 },
636 "mark": "line",
637 "encoding": {
638 "x": {
639 "field": "x",
640 "type": "quantitative",
641 "title": config.x_axis.title,
642 },
643 "y": {
644 "field": "y",
645 "type": "quantitative",
646 "title": config.y_axis.title,
647 },
648 "color": {
649 "field": "series",
650 "type": "nominal"
651 }
652 }
653 });
654
655 serde_json::to_string_pretty(&spec).map_err(|e| IoError::SerializationError(e.to_string()))
656 }
657}
658
659pub mod quick {
661 use super::*;
662
663 pub fn plot_line(x: &[f64], y: &[f64], output: impl AsRef<Path>) -> Result<()> {
665 VisualizationBuilder::new()
666 .title("Line Plot")
667 .add_line(x, y, None)
668 .export(VisualizationFormat::PlotlyJson, output)
669 }
670
671 pub fn plot_scatter(x: &[f64], y: &[f64], output: impl AsRef<Path>) -> Result<()> {
673 VisualizationBuilder::new()
674 .title("Scatter Plot")
675 .add_scatter(x, y, None)
676 .export(VisualizationFormat::PlotlyJson, output)
677 }
678
679 pub fn plot_histogram(values: &[f64], output: impl AsRef<Path>) -> Result<()> {
681 VisualizationBuilder::new()
682 .title("Histogram")
683 .add_histogram(values, None)
684 .export(VisualizationFormat::PlotlyJson, output)
685 }
686
687 pub fn plot_heatmap(z: &Array2<f64>, output: impl AsRef<Path>) -> Result<()> {
689 VisualizationBuilder::new()
690 .title("Heatmap")
691 .add_heatmap(z.clone(), None)
692 .export(VisualizationFormat::PlotlyJson, output)
693 }
694}
695
696#[cfg(test)]
697mod tests {
698 use super::*;
699
700 #[test]
701 fn test_visualization_builder() {
702 let x = vec![0.0, 1.0, 2.0, 3.0, 4.0];
703 let y = vec![0.0, 1.0, 4.0, 9.0, 16.0];
704
705 let result = VisualizationBuilder::new()
706 .title("Test Plot")
707 .x_axis("X values")
708 .y_axis("Y values")
709 .add_line(&x, &y, Some("y = x²"))
710 .to_string(VisualizationFormat::PlotlyJson);
711
712 assert!(result.is_ok());
713 let json_str = result.unwrap();
714 assert!(json_str.contains("Test Plot"));
715 assert!(json_str.contains("y = x²"));
716 }
717
718 #[test]
719 fn test_matplotlib_export() {
720 let x = vec![0.0, 1.0, 2.0, 3.0];
721 let y = vec![0.0, 1.0, 4.0, 9.0];
722
723 let result = VisualizationBuilder::new()
724 .title("Matplotlib Test")
725 .add_scatter(&x, &y, Some("data"))
726 .to_string(VisualizationFormat::MatplotlibPython);
727
728 assert!(result.is_ok());
729 let script = result.unwrap();
730 assert!(script.contains("import matplotlib.pyplot"));
731 assert!(script.contains("ax.scatter"));
732 }
733}
734
735#[cfg(feature = "async")]
738use futures::StreamExt;
739#[cfg(feature = "async")]
740use tokio::sync::mpsc;
741
742#[cfg(feature = "async")]
744pub struct VisualizationServer {
745 port: u16,
746 update_channel: mpsc::Sender<PlotUpdate>,
747}
748
749#[cfg(feature = "async")]
750#[derive(Debug, Clone)]
751pub struct PlotUpdate {
752 pub plot_id: String,
753 pub data: DataSeries,
754 pub action: UpdateAction,
755}
756
757#[cfg(feature = "async")]
758#[derive(Debug, Clone)]
759pub enum UpdateAction {
760 Append,
761 Replace,
762 Remove,
763}
764
765#[cfg(feature = "async")]
766impl VisualizationServer {
767 pub async fn new(port: u16) -> Result<Self> {
769 let (tx, mut rx) = mpsc::channel(100);
770
771 tokio::spawn(async move {
773 while let Some(_update) = rx.recv().await {
777 }
779 });
780
781 Ok(Self {
782 port,
783 update_channel: tx,
784 })
785 }
786
787 pub async fn update_plot(&self, update: PlotUpdate) -> Result<()> {
789 self.update_channel
790 .send(update)
791 .await
792 .map_err(|_| IoError::Other("Failed to send update".to_string()))
793 }
794
795 pub fn url(&self) -> String {
797 format!("http://localhost:{}", self.port)
798 }
799}
800
801#[derive(Debug, Clone)]
803pub struct DataSeries3D {
804 pub name: Option<String>,
805 pub x: Vec<f64>,
806 pub y: Vec<f64>,
807 pub z: Vec<f64>,
808 pub plot_type: PlotType3D,
809 pub style: SeriesStyle,
810}
811
812#[derive(Debug, Clone, Serialize, Deserialize)]
813#[serde(rename_all = "lowercase")]
814pub enum PlotType3D {
815 Scatter3D,
816 Surface,
817 Mesh3D,
818 Line3D,
819 Isosurface,
820 Volume,
821}
822
823pub struct Visualization3DBuilder {
825 data: Vec<DataSeries3D>,
826 config: Plot3DConfig,
827 metadata: Metadata,
828}
829
830#[derive(Debug, Clone, Serialize, Deserialize)]
831pub struct Plot3DConfig {
832 pub title: Option<String>,
833 pub width: Option<u32>,
834 pub height: Option<u32>,
835 pub x_axis: AxisConfig,
836 pub y_axis: AxisConfig,
837 pub z_axis: AxisConfig,
838 pub camera: CameraConfig,
839 pub lighting: LightingConfig,
840}
841
842#[derive(Debug, Clone, Serialize, Deserialize)]
843pub struct CameraConfig {
844 pub eye: [f64; 3],
845 pub center: [f64; 3],
846 pub up: [f64; 3],
847}
848
849impl Default for CameraConfig {
850 fn default() -> Self {
851 Self {
852 eye: [1.25, 1.25, 1.25],
853 center: [0.0, 0.0, 0.0],
854 up: [0.0, 0.0, 1.0],
855 }
856 }
857}
858
859#[derive(Debug, Clone, Serialize, Deserialize)]
860pub struct LightingConfig {
861 pub ambient: f64,
862 pub diffuse: f64,
863 pub specular: f64,
864 pub roughness: f64,
865}
866
867impl Default for LightingConfig {
868 fn default() -> Self {
869 Self {
870 ambient: 0.8,
871 diffuse: 0.8,
872 specular: 0.2,
873 roughness: 0.5,
874 }
875 }
876}
877
878impl Default for Plot3DConfig {
879 fn default() -> Self {
880 Self {
881 title: None,
882 width: Some(800),
883 height: Some(600),
884 x_axis: AxisConfig::default(),
885 y_axis: AxisConfig::default(),
886 z_axis: AxisConfig::default(),
887 camera: CameraConfig::default(),
888 lighting: LightingConfig::default(),
889 }
890 }
891}
892
893impl Default for Visualization3DBuilder {
894 fn default() -> Self {
895 Self::new()
896 }
897}
898
899impl Visualization3DBuilder {
900 pub fn new() -> Self {
901 Self {
902 data: Vec::new(),
903 config: Plot3DConfig::default(),
904 metadata: Metadata::new(),
905 }
906 }
907
908 pub fn add_scatter3d(mut self, x: &[f64], y: &[f64], z: &[f64], name: Option<&str>) -> Self {
910 self.data.push(DataSeries3D {
911 name: name.map(|s| s.to_string()),
912 x: x.to_vec(),
913 y: y.to_vec(),
914 z: z.to_vec(),
915 plot_type: PlotType3D::Scatter3D,
916 style: SeriesStyle::default(),
917 });
918 self
919 }
920
921 pub fn add_surface(
923 mut self,
924 x: &[f64],
925 y: &[f64],
926 z: &Array2<f64>,
927 name: Option<&str>,
928 ) -> Self {
929 let z_flat: Vec<f64> = z.iter().cloned().collect();
930 self.data.push(DataSeries3D {
931 name: name.map(|s| s.to_string()),
932 x: x.to_vec(),
933 y: y.to_vec(),
934 z: z_flat,
935 plot_type: PlotType3D::Surface,
936 style: SeriesStyle::default(),
937 });
938 self
939 }
940
941 pub fn export(self, format: VisualizationFormat, path: impl AsRef<Path>) -> Result<()> {
943 let exporter = get_3d_exporter(format);
945 exporter.export_3d(&self.data, &self.config, &self.metadata, path.as_ref())
946 }
947}
948
949#[derive(Debug, Clone)]
951pub struct AnimationFrame {
952 pub time: f64,
953 pub data: DataSeries,
954}
955
956#[derive(Debug, Clone)]
957pub struct AnimatedVisualization {
958 pub frames: Vec<AnimationFrame>,
959 pub config: AnimationConfig,
960}
961
962#[derive(Debug, Clone, Serialize, Deserialize)]
963pub struct AnimationConfig {
964 pub duration: f64,
965 pub fps: u32,
966 pub loop_mode: LoopMode,
967 pub transition: TransitionType,
968}
969
970#[derive(Debug, Clone, Serialize, Deserialize)]
971#[serde(rename_all = "lowercase")]
972pub enum LoopMode {
973 Once,
974 Loop,
975 PingPong,
976}
977
978#[derive(Debug, Clone, Serialize, Deserialize)]
979#[serde(rename_all = "lowercase")]
980pub enum TransitionType {
981 Linear,
982 EaseIn,
983 EaseOut,
984 EaseInOut,
985}
986
987pub struct DashboardBuilder {
989 plots: Vec<DashboardPlot>,
990 layout: DashboardLayout,
991 config: DashboardConfig,
992}
993
994#[derive(Debug, Clone)]
995pub struct DashboardPlot {
996 pub plot: VisualizationBuilder,
997 pub position: GridPosition,
998}
999
1000#[derive(Debug, Clone)]
1001pub struct GridPosition {
1002 pub row: usize,
1003 pub col: usize,
1004 pub row_span: usize,
1005 pub col_span: usize,
1006}
1007
1008#[derive(Debug, Clone)]
1009pub struct DashboardLayout {
1010 pub rows: usize,
1011 pub cols: usize,
1012 pub spacing: f64,
1013}
1014
1015#[derive(Debug, Clone)]
1016pub struct DashboardConfig {
1017 pub title: Option<String>,
1018 pub width: u32,
1019 pub height: u32,
1020 pub theme: Option<String>,
1021 pub auto_refresh: Option<u32>, }
1023
1024impl DashboardBuilder {
1025 pub fn new(rows: usize, cols: usize) -> Self {
1026 Self {
1027 plots: Vec::new(),
1028 layout: DashboardLayout {
1029 rows,
1030 cols,
1031 spacing: 10.0,
1032 },
1033 config: DashboardConfig {
1034 title: None,
1035 width: 1200,
1036 height: 800,
1037 theme: None,
1038 auto_refresh: None,
1039 },
1040 }
1041 }
1042
1043 pub fn add_plot(mut self, plot: VisualizationBuilder, row: usize, col: usize) -> Self {
1045 self.plots.push(DashboardPlot {
1046 plot,
1047 position: GridPosition {
1048 row,
1049 col,
1050 row_span: 1,
1051 col_span: 1,
1052 },
1053 });
1054 self
1055 }
1056
1057 pub fn export_html(self, path: impl AsRef<Path>) -> Result<()> {
1059 let html = self.generate_html()?;
1060 let mut file = File::create(path).map_err(IoError::Io)?;
1061 file.write_all(html.as_bytes()).map_err(IoError::Io)?;
1062 Ok(())
1063 }
1064
1065 fn generate_html(&self) -> Result<String> {
1066 let mut html = String::from(
1067 r#"<!DOCTYPE html>
1068<html>
1069<head>
1070 <title>Dashboard</title>
1071 <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
1072 <style>
1073 .dashboard-grid {
1074 display: grid;
1075 grid-template-columns: repeat({cols}, 1fr);
1076 grid-template-rows: repeat({rows}, 1fr);
1077 gap: {spacing}px;
1078 width: {width}px;
1079 height: {height}px;
1080 }
1081 .plot-container {
1082 width: 100%;
1083 height: 100%;
1084 }
1085 </style>
1086</head>
1087<body>
1088 <div class="dashboard-grid">
1089"#,
1090 );
1091
1092 for (i, dashboard_plot) in self.plots.iter().enumerate() {
1094 let plot_data = dashboard_plot
1095 .plot
1096 .clone()
1097 .to_string(VisualizationFormat::PlotlyJson)?;
1098
1099 html.push_str(&format!(
1100 r#"
1101 <div class="plot-container" style="grid-row: {}; grid-column: {};">
1102 <div id="plot{}" style="width: 100%; height: 100%;"></div>
1103 <script>
1104 Plotly.newPlot('plot{}', {});
1105 </script>
1106 </div>
1107"#,
1108 dashboard_plot.position.row + 1,
1109 dashboard_plot.position.col + 1,
1110 i,
1111 i,
1112 plot_data
1113 ));
1114 }
1115
1116 html.push_str(
1117 r#"
1118 </div>
1119</body>
1120</html>
1121"#,
1122 );
1123
1124 Ok(html
1125 .replace("{cols}", &self.layout.cols.to_string())
1126 .replace("{rows}", &self.layout.rows.to_string())
1127 .replace("{spacing}", &self.layout.spacing.to_string())
1128 .replace("{width}", &self.config.width.to_string())
1129 .replace("{height}", &self.config.height.to_string()))
1130 }
1131}
1132
1133struct D3Exporter;
1135
1136impl VisualizationExporter for D3Exporter {
1137 fn export(
1138 &self,
1139 data: &[DataSeries],
1140 config: &PlotConfig,
1141 metadata: &Metadata,
1142 path: &Path,
1143 ) -> Result<()> {
1144 let html = self.to_string(data, config, metadata)?;
1145 let mut file = File::create(path).map_err(IoError::Io)?;
1146 file.write_all(html.as_bytes()).map_err(IoError::Io)?;
1147 Ok(())
1148 }
1149
1150 fn to_string(
1151 &self,
1152 _data: &[DataSeries],
1153 config: &PlotConfig,
1154 metadata: &Metadata,
1155 ) -> Result<String> {
1156 let mut html = String::from(
1157 r#"<!DOCTYPE html>
1158<html>
1159<head>
1160 <script src="https://d3js.org/d3.v7.min.js"></script>
1161 <style>
1162 .line { fill: none; stroke-width: 2; }
1163 .axis { font-size: 12px; }
1164 .grid { stroke: lightgray; stroke-opacity: 0.7; }
1165 </style>
1166</head>
1167<body>
1168 <svg id="chart"></svg>
1169 <script>
1170"#,
1171 );
1172
1173 html.push_str(&format!(
1175 " const margin = {{top: 20, right: 20, bottom: 30, left: 50}};\n\
1176 const width = {} - margin.left - margin.right;\n\
1177 const height = {} - margin.top - margin.bottom;\n\
1178 \n\
1179 const svg = d3.select(\"#chart\")\n\
1180 .attr(\"width\", width + margin.left + margin.right)\n\
1181 .attr(\"height\", height + margin.top + margin.bottom)\n\
1182 .append(\"g\")\n\
1183 .attr(\"transform\", \"translate(\" + margin.left + \",\" + margin.top + \")\");\n",
1184 config.width.unwrap_or(800),
1185 config.height.unwrap_or(600)
1186 ));
1187
1188 html.push_str(
1192 r#"
1193 </script>
1194</body>
1195</html>
1196"#,
1197 );
1198
1199 Ok(html)
1200 }
1201}
1202
1203struct BokehExporter;
1205
1206impl VisualizationExporter for BokehExporter {
1207 fn export(
1208 &self,
1209 data: &[DataSeries],
1210 config: &PlotConfig,
1211 metadata: &Metadata,
1212 path: &Path,
1213 ) -> Result<()> {
1214 let json = self.to_string(data, config, metadata)?;
1215 let mut file = File::create(path).map_err(IoError::Io)?;
1216 file.write_all(json.as_bytes()).map_err(IoError::Io)?;
1217 Ok(())
1218 }
1219
1220 fn to_string(
1221 &self,
1222 _data: &[DataSeries],
1223 config: &PlotConfig,
1224 metadata: &Metadata,
1225 ) -> Result<String> {
1226 let doc = serde_json::json!({
1227 "version": "2.4.0",
1228 "title": config.title,
1229 "roots": []
1230 });
1231
1232 serde_json::to_string_pretty(&doc).map_err(|e| IoError::SerializationError(e.to_string()))
1236 }
1237}
1238
1239#[allow(dead_code)]
1241fn get_3d_exporter(format: VisualizationFormat) -> Box<dyn Visualization3DExporter> {
1242 match format {
1243 VisualizationFormat::PlotlyJson => Box::new(Plotly3DExporter),
1244 _ => Box::new(Plotly3DExporter), }
1246}
1247
1248trait Visualization3DExporter {
1250 fn export_3d(
1251 &self,
1252 data: &[DataSeries3D],
1253 config: &Plot3DConfig,
1254 metadata: &Metadata,
1255 path: &Path,
1256 ) -> Result<()>;
1257}
1258
1259struct Plotly3DExporter;
1261
1262impl Visualization3DExporter for Plotly3DExporter {
1263 fn export_3d(
1264 &self,
1265 data: &[DataSeries3D],
1266 config: &Plot3DConfig,
1267 metadata: &Metadata,
1268 path: &Path,
1269 ) -> Result<()> {
1270 let mut traces = Vec::new();
1271
1272 for series in data {
1273 let trace = match series.plot_type {
1274 PlotType3D::Scatter3D => {
1275 serde_json::json!({
1276 "type": "scatter3d",
1277 "mode": "markers",
1278 "name": series.name,
1279 "x": series.x,
1280 "y": series.y,
1281 "z": series.z,
1282 "marker": {
1283 "size": series.style.size.unwrap_or(5.0),
1284 "color": series.style.color,
1285 }
1286 })
1287 }
1288 PlotType3D::Surface => {
1289 serde_json::json!({
1290 "type": "surface",
1291 "name": series.name,
1292 "x": series.x,
1293 "y": series.y,
1294 "z": series.z,
1295 })
1296 }
1297 _ => continue,
1298 };
1299 traces.push(trace);
1300 }
1301
1302 let layout = serde_json::json!({
1303 "title": config.title,
1304 "width": config.width,
1305 "height": config.height,
1306 "scene": {
1307 "xaxis": {"title": config.x_axis.title},
1308 "yaxis": {"title": config.y_axis.title},
1309 "zaxis": {"title": config.z_axis.title},
1310 "camera": {
1311 "eye": {"x": config.camera.eye[0], "y": config.camera.eye[1], "z": config.camera.eye[2]},
1312 "center": {"x": config.camera.center[0], "y": config.camera.center[1], "z": config.camera.center[2]},
1313 "up": {"x": config.camera.up[0], "y": config.camera.up[1], "z": config.camera.up[2]},
1314 }
1315 }
1316 });
1317
1318 let plot_data = serde_json::json!({
1319 "data": traces,
1320 "layout": layout,
1321 });
1322
1323 let json_str = serde_json::to_string_pretty(&plot_data)
1324 .map_err(|e| IoError::SerializationError(e.to_string()))?;
1325
1326 let mut file = File::create(path).map_err(IoError::Io)?;
1327 file.write_all(json_str.as_bytes()).map_err(IoError::Io)?;
1328 Ok(())
1329 }
1330}
1331
1332pub mod external {
1334 use super::*;
1335
1336 pub struct PlotlyCloud {
1338 api_key: String,
1339 username: String,
1340 }
1341
1342 impl PlotlyCloud {
1343 pub fn new(_apikey: String, username: String) -> Self {
1344 Self {
1345 api_key: _apikey,
1346 username,
1347 }
1348 }
1349
1350 #[cfg(feature = "reqwest")]
1352 pub fn upload(&self, plotdata: &str, filename: &str) -> Result<String> {
1353 Ok(format!("https://plot.ly/~{}/{}", self.username, filename))
1355 }
1356 }
1357
1358 pub struct JupyterIntegration;
1360
1361 impl JupyterIntegration {
1362 pub fn create_cell(viz: &VisualizationBuilder) -> serde_json::Value {
1364 serde_json::json!({
1365 "cell_type": "code",
1366 "execution_count": null,
1367 "metadata": {},
1368 "outputs": [],
1369 "source": [
1370 "# Generated visualization\n",
1371 "import plotly.graph_objects as go\n",
1372 "# ... visualization code ..."
1373 ]
1374 })
1375 }
1376 }
1377}