scirs2_metrics/visualization/backends/
plotly.rs

1//! Plotly backend for visualization
2//!
3//! This module provides an adapter for rendering visualizations using the plotly crate.
4
5use plotly::{
6    common::{ColorScale, ColorScalePalette, DashType, Line, Marker, Mode, Title},
7    layout::{Annotation, Axis, Layout},
8    Bar, HeatMap, Histogram, Plot, Scatter,
9};
10use std::error::Error;
11use std::path::Path;
12
13use crate::visualization::{
14    ColorMap, PlotType, VisualizationData, VisualizationMetadata, VisualizationOptions,
15};
16
17use super::PlottingBackend;
18
19/// A struct for rendering visualizations using plotly
20pub struct PlotlyBackend;
21
22impl Default for PlotlyBackend {
23    fn default() -> Self {
24        Self::new()
25    }
26}
27
28impl PlotlyBackend {
29    /// Create a new plotly backend
30    pub fn new() -> Self {
31        Self
32    }
33
34    /// Map the scirs2-metrics color map to a plotly color scale
35    fn map_color_scheme(&self, colormap: &ColorMap) -> ColorScale {
36        match colormap {
37            ColorMap::BlueRed => ColorScale::Palette(ColorScalePalette::RdBu),
38            ColorMap::GreenRed => ColorScale::Palette(ColorScalePalette::Greens),
39            ColorMap::Grayscale => ColorScale::Palette(ColorScalePalette::Greys),
40            ColorMap::Viridis => ColorScale::Palette(ColorScalePalette::Viridis),
41            ColorMap::Magma => ColorScale::Palette(ColorScalePalette::Hot),
42        }
43    }
44
45    /// Add line traces to a plot
46    fn add_line_traces(
47        &self,
48        plot: &mut Plot,
49        data: &VisualizationData,
50        metadata: &VisualizationMetadata,
51    ) -> Result<(), Box<dyn Error>> {
52        let default_name = vec!["Series 1".to_string()];
53        let series_names = data.series_names.as_ref().unwrap_or(&default_name);
54
55        let trace = Scatter::new(data.x.clone(), data.y.clone())
56            .mode(Mode::Lines)
57            .name(&series_names[0]);
58
59        plot.add_trace(trace);
60
61        // Add additional series from the series HashMap
62        if !data.series.is_empty() {
63            for (name, series_data) in &data.series {
64                // If we have x data for this series, use it; otherwise reuse main x
65                let x_data = if let Some(x_series) = data.series.get(&format!("{}_x", name)) {
66                    x_series.clone()
67                } else {
68                    data.x.clone()
69                };
70
71                let trace = Scatter::new(x_data, series_data.clone())
72                    .mode(Mode::Lines)
73                    .name(name);
74
75                plot.add_trace(trace);
76            }
77        }
78
79        Ok(())
80    }
81
82    /// Add scatter traces to a plot
83    fn add_scatter_traces(
84        &self,
85        plot: &mut Plot,
86        data: &VisualizationData,
87        metadata: &VisualizationMetadata,
88    ) -> Result<(), Box<dyn Error>> {
89        let default_name = vec!["Series 1".to_string()];
90        let series_names = data.series_names.as_ref().unwrap_or(&default_name);
91
92        let trace = Scatter::new(data.x.clone(), data.y.clone())
93            .mode(Mode::Markers)
94            .name(&series_names[0]);
95
96        plot.add_trace(trace);
97
98        // Add additional series from the series HashMap
99        if !data.series.is_empty() {
100            for (name, series_data) in &data.series {
101                // If we have x data for this series, use it; otherwise reuse main x
102                let x_data = if let Some(x_series) = data.series.get(&format!("{}_x", name)) {
103                    x_series.clone()
104                } else {
105                    data.x.clone()
106                };
107
108                let trace = Scatter::new(x_data, series_data.clone())
109                    .mode(Mode::Markers)
110                    .name(name);
111
112                plot.add_trace(trace);
113            }
114        }
115
116        Ok(())
117    }
118
119    /// Add bar traces to a plot
120    fn add_bar_traces(
121        &self,
122        plot: &mut Plot,
123        data: &VisualizationData,
124        metadata: &VisualizationMetadata,
125    ) -> Result<(), Box<dyn Error>> {
126        let default_name = vec!["Series 1".to_string()];
127        let series_names = data.series_names.as_ref().unwrap_or(&default_name);
128
129        let trace = Bar::new(data.x.clone(), data.y.clone()).name(&series_names[0]);
130
131        plot.add_trace(trace);
132
133        // Add additional series from the series HashMap
134        if !data.series.is_empty() {
135            for (name, series_data) in &data.series {
136                // If we have x data for this series, use it; otherwise reuse main x
137                let x_data = if let Some(x_series) = data.series.get(&format!("{}_x", name)) {
138                    x_series.clone()
139                } else {
140                    data.x.clone()
141                };
142
143                let trace = Bar::new(x_data, series_data.clone()).name(name);
144
145                plot.add_trace(trace);
146            }
147        }
148
149        Ok(())
150    }
151
152    /// Add heatmap traces to a plot
153    fn add_heatmap_traces(
154        &self,
155        plot: &mut Plot,
156        data: &VisualizationData,
157        metadata: &VisualizationMetadata,
158        options: &VisualizationOptions,
159    ) -> Result<(), Box<dyn Error>> {
160        if let Some(z_data) = &data.z {
161            let colorscale = if let Some(ref color_map) = options.color_map {
162                self.map_color_scheme(color_map)
163            } else {
164                ColorScale::Palette(ColorScalePalette::Viridis)
165            };
166
167            let trace = HeatMap::new_z(z_data.clone()).color_scale(colorscale);
168
169            plot.add_trace(trace);
170        }
171
172        Ok(())
173    }
174
175    /// Add histogram traces to a plot
176    fn add_histogram_traces(
177        &self,
178        plot: &mut Plot,
179        data: &VisualizationData,
180        metadata: &VisualizationMetadata,
181    ) -> Result<(), Box<dyn Error>> {
182        let default_name = vec!["Series 1".to_string()];
183        let series_names = data.series_names.as_ref().unwrap_or(&default_name);
184
185        let trace = Histogram::new(data.x.clone()).name(&series_names[0]);
186
187        plot.add_trace(trace);
188
189        Ok(())
190    }
191}
192
193impl PlottingBackend for PlotlyBackend {
194    fn save_to_file(
195        &self,
196        data: &VisualizationData,
197        metadata: &VisualizationMetadata,
198        options: &VisualizationOptions,
199        path: impl AsRef<Path>,
200    ) -> Result<(), Box<dyn Error>> {
201        let mut plot = Plot::new();
202
203        match metadata.plot_type {
204            PlotType::Line => self.add_line_traces(&mut plot, data, metadata)?,
205            PlotType::Scatter => self.add_scatter_traces(&mut plot, data, metadata)?,
206            PlotType::Bar => self.add_bar_traces(&mut plot, data, metadata)?,
207            PlotType::Heatmap => self.add_heatmap_traces(&mut plot, data, metadata, options)?,
208            PlotType::Histogram => self.add_histogram_traces(&mut plot, data, metadata)?,
209        }
210
211        // Set layout options
212        let layout = Layout::new()
213            .title(Title::with_text(&metadata.title))
214            .x_axis(Axis::new().title(Title::with_text(&metadata.x_label)))
215            .y_axis(Axis::new().title(Title::with_text(&metadata.y_label)))
216            .width(options.width)
217            .height(options.height)
218            .show_legend(options.show_legend);
219
220        plot.set_layout(layout);
221
222        // Save to file
223        match path.as_ref().extension().and_then(|e| e.to_str()) {
224            Some("html") => {
225                plot.write_html(path);
226                Ok(())
227            }
228            Some("json") => {
229                let json_data = plot.to_json();
230                std::fs::write(path, json_data)?;
231                Ok(())
232            }
233            _ => Err(Box::new(std::io::Error::new(
234                std::io::ErrorKind::InvalidInput,
235                "Unsupported file extension for plotly output. Only .html and .json are supported.",
236            ))),
237        }
238    }
239
240    fn render_svg(
241        &self,
242        self_data: &VisualizationData,
243        _metadata: &VisualizationMetadata,
244        options: &VisualizationOptions,
245    ) -> Result<Vec<u8>, Box<dyn Error>> {
246        // Plotly doesn't directly support SVG generation in the Rust crate
247        // We'll return an error indicating this limitation
248        Err(Box::new(std::io::Error::new(
249            std::io::ErrorKind::Unsupported,
250            "SVG rendering is not directly supported by the Plotly Rust crate. Use HTML output and convert externally if needed.",
251        )))
252    }
253
254    fn render_png(
255        &self,
256        self_data: &VisualizationData,
257        _metadata: &VisualizationMetadata,
258        options: &VisualizationOptions,
259    ) -> Result<Vec<u8>, Box<dyn Error>> {
260        // Plotly doesn't directly support PNG generation in the Rust crate
261        // We'll return an error indicating this limitation
262        Err(Box::new(std::io::Error::new(
263            std::io::ErrorKind::Unsupported,
264            "PNG rendering is not directly supported by the Plotly Rust crate. Use HTML output and convert externally if needed.",
265        )))
266    }
267}