scirs2_io/
visualization.rs

1//! Visualization tool integration for scientific data
2//!
3//! Provides interfaces and utilities for integrating with popular visualization
4//! libraries and tools, enabling seamless data export and plotting capabilities.
5
6#![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/// Visualization format types
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum VisualizationFormat {
20    /// Plotly JSON format
21    PlotlyJson,
22    /// Matplotlib script
23    MatplotlibPython,
24    /// Gnuplot script
25    Gnuplot,
26    /// D3.js JSON format
27    D3Json,
28    /// Vega-Lite specification
29    VegaLite,
30    /// Bokeh JSON format
31    BokehJson,
32    /// SVG format
33    Svg,
34    /// HTML with embedded visualization
35    Html,
36}
37
38/// Plot type enumeration
39#[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/// Axis configuration
57#[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/// Scale types for axes
79#[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/// Plot configuration
90#[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/// Annotation for plots
120#[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/// Visualization builder
129#[derive(Debug, Clone)]
130pub struct VisualizationBuilder {
131    data: Vec<DataSeries>,
132    config: PlotConfig,
133    metadata: Metadata,
134}
135
136/// Data series for plotting
137#[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/// Style configuration for data series
148#[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    /// Create a new visualization builder
177    pub fn new() -> Self {
178        Self {
179            data: Vec::new(),
180            config: PlotConfig::default(),
181            metadata: Metadata::new(),
182        }
183    }
184
185    /// Set plot title
186    pub fn title(mut self, title: impl Into<String>) -> Self {
187        self.config.title = Some(title.into());
188        self
189    }
190
191    /// Set plot dimensions
192    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    /// Configure X axis
199    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    /// Configure Y axis
205    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    /// Add a line plot
211    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    /// Add a scatter plot
224    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    /// Add a histogram
237    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    /// Add a heatmap
250    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]), // Store dimensions
255            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    /// Build and export to specified format
264    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    /// Build and return as string
270    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
276/// Trait for visualization exporters
277trait 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/// Get appropriate exporter for format
294#[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), // Placeholder
302        VisualizationFormat::BokehJson => Box::new(PlotlyExporter), // Placeholder
303        VisualizationFormat::Svg => Box::new(PlotlyExporter),    // Placeholder
304        VisualizationFormat::Html => Box::new(PlotlyExporter),   // Placeholder
305    }
306}
307
308/// Plotly JSON exporter
309struct 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
418/// Matplotlib Python script exporter
419struct 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        // Create figure
444        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        // Plot data
451        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        // Configure axes
483        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
502/// Gnuplot script exporter
503struct 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        // Set terminal and output
528        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        // Set title and labels
537        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        // Plot command
550        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        // Data blocks
578        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
591/// Vega-Lite specification exporter
592struct 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
659/// Quick plotting functions
660pub mod quick {
661    use super::*;
662
663    /// Quick line plot
664    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    /// Quick scatter plot
672    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    /// Quick histogram
680    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    /// Quick heatmap
688    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// Advanced Visualization Features
736
737#[cfg(feature = "async")]
738use futures::StreamExt;
739#[cfg(feature = "async")]
740use tokio::sync::mpsc;
741
742/// Interactive visualization server for real-time updates
743#[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    /// Create a new visualization server
768    pub async fn new(port: u16) -> Result<Self> {
769        let (tx, mut rx) = mpsc::channel(100);
770
771        // Spawn server task
772        tokio::spawn(async move {
773            // In a real implementation, this would start an HTTP server
774            // that serves interactive visualizations and handles WebSocket
775            // connections for real-time updates
776            while let Some(_update) = rx.recv().await {
777                // Process update
778            }
779        });
780
781        Ok(Self {
782            port,
783            update_channel: tx,
784        })
785    }
786
787    /// Send update to a plot
788    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    /// Get server URL
796    pub fn url(&self) -> String {
797        format!("http://localhost:{}", self.port)
798    }
799}
800
801/// 3D Visualization support
802#[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
823/// 3D Visualization builder
824pub 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    /// Add 3D scatter plot
909    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    /// Add surface plot
922    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    /// Export 3D visualization
942    pub fn export(self, format: VisualizationFormat, path: impl AsRef<Path>) -> Result<()> {
943        // Implementation would convert 3D data to appropriate format
944        let exporter = get_3d_exporter(format);
945        exporter.export_3d(&self.data, &self.config, &self.metadata, path.as_ref())
946    }
947}
948
949/// Animation support
950#[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
987/// Dashboard builder for composite visualizations
988pub 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>, // seconds
1022}
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    /// Add plot to dashboard
1044    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    /// Export dashboard as HTML
1058    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        // Add plots
1093        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
1133/// D3.js exporter with enhanced features
1134struct 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        // D3.js visualization code
1174        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        // Add implementation for different plot types
1189        // This is a simplified version
1190
1191        html.push_str(
1192            r#"
1193    </script>
1194</body>
1195</html>
1196"#,
1197        );
1198
1199        Ok(html)
1200    }
1201}
1202
1203/// Bokeh JSON exporter
1204struct 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        // Build Bokeh document structure
1233        // This is a simplified version
1234
1235        serde_json::to_string_pretty(&doc).map_err(|e| IoError::SerializationError(e.to_string()))
1236    }
1237}
1238
1239/// Get appropriate 3D exporter
1240#[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), // Default to Plotly for 3D
1245    }
1246}
1247
1248/// Trait for 3D visualization exporters
1249trait Visualization3DExporter {
1250    fn export_3d(
1251        &self,
1252        data: &[DataSeries3D],
1253        config: &Plot3DConfig,
1254        metadata: &Metadata,
1255        path: &Path,
1256    ) -> Result<()>;
1257}
1258
1259/// Plotly 3D exporter
1260struct 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
1332/// Integration with external visualization services
1333pub mod external {
1334    use super::*;
1335
1336    /// Plotly cloud integration
1337    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        /// Upload visualization to Plotly cloud
1351        #[cfg(feature = "reqwest")]
1352        pub fn upload(&self, plotdata: &str, filename: &str) -> Result<String> {
1353            // Implementation would use reqwest to upload to Plotly API
1354            Ok(format!("https://plot.ly/~{}/{}", self.username, filename))
1355        }
1356    }
1357
1358    /// Jupyter notebook integration
1359    pub struct JupyterIntegration;
1360
1361    impl JupyterIntegration {
1362        /// Generate notebook cell with visualization
1363        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}