scirs2_metrics/visualization/backends/
plotly.rs1use 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
19pub struct PlotlyBackend;
21
22impl Default for PlotlyBackend {
23 fn default() -> Self {
24 Self::new()
25 }
26}
27
28impl PlotlyBackend {
29 pub fn new() -> Self {
31 Self
32 }
33
34 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 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 if !data.series.is_empty() {
63 for (name, series_data) in &data.series {
64 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 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 if !data.series.is_empty() {
100 for (name, series_data) in &data.series {
101 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 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 if !data.series.is_empty() {
135 for (name, series_data) in &data.series {
136 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 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 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 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 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 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 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}