1use plotters::backend::SVGBackend;
2use plotters::drawing::IntoDrawingArea;
3use plotters::element::DashedPathElement;
4use plotters::prelude::*;
5
6use crate::abnormal::{AbnormalSample, abnormal_smaples_series};
7use crate::chart_data::ChannelChartData;
8use crate::config::SnapshotConfig;
9use crate::util::{
10 INPUT_CHANNEL_COLORS, OUTPUT_CHANNEL_COLORS, get_contrasting_color, num_x_labels,
11 parse_hex_color, time_formatter,
12};
13
14#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
18pub enum Layout {
19 #[default]
21 SeparateChannels,
22 CombinedPerChannelType,
26 Combined,
28}
29
30pub(crate) fn generate_svg(
31 input_data: &[Vec<f32>],
32 output_data: &[Vec<f32>],
33 abnormalities: &[Vec<(usize, AbnormalSample)>],
34 config: &SnapshotConfig,
35) -> String {
36 let height_per_channel = config.svg_height_per_channel;
37 let num_channels = output_data.len()
38 + if config.with_inputs {
39 input_data.len()
40 } else {
41 0
42 };
43 let num_samples = output_data.first().map(|c| c.len()).unwrap_or(0);
44
45 if num_samples == 0 || num_channels == 0 {
46 return "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\"><text>Empty</text></svg>".to_string();
47 }
48
49 let svg_width = config.svg_width.unwrap_or(config.num_samples * 2) as u32;
50 let total_height = (height_per_channel * num_channels) as u32;
51
52 let mut svg_buffer = String::new();
54 {
55 let root =
56 SVGBackend::with_string(&mut svg_buffer, (svg_width, total_height)).into_drawing_area();
57
58 let bg_color = parse_hex_color(&config.background_color);
60 root.fill(&bg_color).unwrap();
61
62 let current_area = if let Some(ref title) = config.chart_title {
64 let title_color = get_contrasting_color(&bg_color);
65 let text_style = TextStyle::from(("sans-serif", 20)).color(&title_color);
66 root.titled(title, text_style).unwrap()
67 } else {
68 root
69 };
70
71 let input_charts: Vec<ChannelChartData> = if config.with_inputs {
72 input_data
73 .iter()
74 .enumerate()
75 .map(|(i, data)| ChannelChartData::from_input_data(data, i, config))
76 .collect()
77 } else {
78 vec![]
79 };
80
81 let output_charts: Vec<ChannelChartData> = output_data
82 .iter()
83 .zip(abnormalities)
84 .enumerate()
85 .map(|(i, (data, abnormalities))| {
86 ChannelChartData::from_output_data(data, abnormalities, i, config)
87 })
88 .collect();
89
90 let start_sample = config.warm_up.num_samples(config.sample_rate);
91
92 let output_axis_color = parse_hex_color(OUTPUT_CHANNEL_COLORS[0]);
93 let input_axis_color = parse_hex_color(INPUT_CHANNEL_COLORS[0]);
94
95 match config.chart_layout {
96 Layout::SeparateChannels => {
97 let areas = current_area.split_evenly((num_channels, 1));
99 for (chart, area) in input_charts
100 .into_iter()
101 .chain(output_charts.into_iter())
102 .zip(areas)
103 {
104 one_channel_chart(chart, config, start_sample, &area);
105 }
106 }
107 Layout::CombinedPerChannelType => {
108 if config.with_inputs {
109 let areas = current_area.split_evenly((2, 1));
110
111 multi_channel_chart(
112 input_charts,
113 config,
114 true,
115 start_sample,
116 input_axis_color,
117 &areas[0],
118 );
119 multi_channel_chart(
120 output_charts,
121 config,
122 true,
123 start_sample,
124 output_axis_color,
125 &areas[1],
126 );
127 } else {
128 multi_channel_chart(
129 output_charts,
130 config,
131 true,
132 start_sample,
133 output_axis_color,
134 ¤t_area,
135 );
136 }
137 }
138 Layout::Combined => {
139 let charts = output_charts.into_iter().chain(input_charts).collect();
140 multi_channel_chart(
141 charts,
142 config,
143 false,
144 start_sample,
145 output_axis_color,
146 ¤t_area,
147 );
148 }
149 }
150
151 current_area.present().unwrap();
152 }
153
154 svg_buffer
155}
156
157fn multi_channel_chart(
158 charts_data: Vec<ChannelChartData>,
159 config: &SnapshotConfig,
160 solid_input: bool,
161 start_from: usize,
162 axis_color: RGBColor,
163 area: &DrawingArea<SVGBackend<'_>, plotters::coord::Shift>,
164) {
165 let num_samples = charts_data
166 .iter()
167 .map(|chart| chart.data.len())
168 .max()
169 .unwrap_or_default();
170 let min_val = charts_data
171 .iter()
172 .flat_map(|c| c.data.iter())
173 .cloned()
174 .fold(f32::INFINITY, f32::min);
175 let max_val = charts_data
176 .iter()
177 .flat_map(|c| c.data.iter())
178 .cloned()
179 .fold(f32::NEG_INFINITY, f32::max);
180
181 let range = (max_val - min_val).max(f32::EPSILON);
182 let y_min = (min_val - range * 0.1) as f64;
183 let y_max = (max_val + range * 0.1) as f64;
184
185 let mut chart = ChartBuilder::on(area)
187 .margin(5)
188 .x_label_area_size(35)
189 .y_label_area_size(50)
190 .build_cartesian_2d(start_from as f64..num_samples as f64, y_min..y_max)
191 .unwrap();
192
193 let mut mesh = chart.configure_mesh();
194
195 mesh.axis_style(axis_color.mix(0.3));
196
197 if !config.show_grid {
198 mesh.disable_mesh();
199 } else {
200 mesh.light_line_style(axis_color.mix(0.1))
201 .bold_line_style(axis_color.mix(0.2));
202 }
203
204 if config.show_labels {
205 let x_labels = num_x_labels(num_samples, config.sample_rate);
206 mesh.x_labels(
207 config
208 .max_labels_x_axis
209 .map(|mx| x_labels.min(mx))
210 .unwrap_or(x_labels),
211 )
212 .y_labels(3)
213 .label_style(("sans-serif", 10, &axis_color));
214 }
215
216 let formatter = |v: &f64| time_formatter(*v as usize, config.sample_rate);
217 if config.format_x_axis_labels_as_time {
218 mesh.x_label_formatter(&formatter);
219 }
220
221 mesh.draw().unwrap();
222
223 let mut has_legend = false;
224
225 let ctx = chart
226 .draw_series(
227 charts_data
228 .iter()
229 .filter(|d| !d.is_input || solid_input)
230 .map(|entry| {
231 let ChannelChartData {
232 data: channel_data,
233 color,
234 ..
235 } = entry;
236
237 let line_style = ShapeStyle {
238 color: color.to_rgba(),
239 filled: false,
240 stroke_width: config.line_width as u32,
241 };
242
243 PathElement::new(
244 channel_data
245 .iter()
246 .enumerate()
247 .map(|(i, &sample)| ((i + start_from) as f64, sample as f64))
248 .collect::<Vec<(f64, f64)>>(),
249 line_style,
250 )
251 }),
252 )
253 .unwrap();
254
255 for entry in charts_data
256 .iter()
257 .filter(|d| d.label.is_some() && (!d.is_input || solid_input))
258 {
259 ctx.label(entry.label.as_ref().unwrap())
260 .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], entry.color));
261
262 has_legend = true;
263 }
264
265 if !solid_input && charts_data.iter().any(|d| d.is_input) {
266 let ctx = chart
267 .draw_series(charts_data.iter().filter(|d| d.is_input).map(|entry| {
268 let ChannelChartData {
269 data: channel_data,
270 color,
271 ..
272 } = entry;
273
274 let line_style = ShapeStyle {
275 color: color.to_rgba(),
276 filled: false,
277 stroke_width: config.line_width as u32,
278 };
279
280 DashedPathElement::new(
281 channel_data
282 .iter()
283 .enumerate()
284 .map(|(i, &sample)| ((i + start_from) as f64, sample as f64))
285 .collect::<Vec<(f64, f64)>>(),
286 2,
287 3,
288 line_style,
289 )
290 }))
291 .unwrap();
292
293 for entry in charts_data
294 .iter()
295 .filter(|d| d.label.is_some() && d.is_input)
296 {
297 ctx.label(entry.label.as_ref().unwrap()).legend(|(x, y)| {
298 DashedPathElement::new(vec![(x, y), (x + 20, y)], 2, 3, entry.color)
299 });
300
301 has_legend = true;
302 }
303 }
304
305 abnormal_smaples_series(&charts_data, &mut chart, y_min, y_max);
306
307 if has_legend {
308 let background = parse_hex_color(&config.background_color);
309 let contrasting = get_contrasting_color(&background);
310
311 chart
312 .configure_series_labels()
313 .border_style(contrasting)
314 .background_style(background)
315 .label_font(TextStyle::from(("sans-serif", 10)).color(&contrasting))
316 .draw()
317 .unwrap();
318 }
319}
320
321fn one_channel_chart(
322 chart_data: ChannelChartData,
323 config: &SnapshotConfig,
324 start_from: usize,
325 area: &DrawingArea<SVGBackend<'_>, plotters::coord::Shift>,
326) {
327 let ChannelChartData {
328 data: channel_data,
329 color,
330 label,
331 ..
332 } = &chart_data;
333
334 let num_samples = channel_data.len();
335
336 let min_val = channel_data.iter().cloned().fold(f32::INFINITY, f32::min);
338 let max_val = channel_data
339 .iter()
340 .cloned()
341 .fold(f32::NEG_INFINITY, f32::max);
342 let range = (max_val - min_val).max(f32::EPSILON);
343 let y_min = (min_val - range * 0.1) as f64;
344 let y_max = (max_val + range * 0.1) as f64;
345
346 let mut chart = ChartBuilder::on(area)
348 .margin(5)
349 .x_label_area_size(if label.is_some() { 35 } else { 0 })
350 .y_label_area_size(if label.is_some() { 50 } else { 0 })
351 .build_cartesian_2d(start_from as f64..num_samples as f64, y_min..y_max)
352 .unwrap();
353
354 let mut mesh = chart.configure_mesh();
355
356 mesh.axis_style(color.mix(0.3));
357
358 if !config.show_grid {
359 mesh.disable_mesh();
360 } else {
361 mesh.light_line_style(color.mix(0.1))
362 .bold_line_style(color.mix(0.2));
363 }
364
365 if let Some(label) = label {
366 let x_labels = num_x_labels(num_samples, config.sample_rate);
367 mesh.x_labels(
368 config
369 .max_labels_x_axis
370 .map(|mx| x_labels.min(mx))
371 .unwrap_or(x_labels),
372 )
373 .y_labels(3)
374 .x_desc(label)
375 .label_style(("sans-serif", 10, &color));
376 }
377
378 let formatter = |v: &f64| time_formatter(*v as usize, config.sample_rate);
379 if config.format_x_axis_labels_as_time {
380 mesh.x_label_formatter(&formatter);
381 }
382
383 mesh.draw().unwrap();
384
385 let line_style = ShapeStyle {
387 color: color.to_rgba(),
388 filled: false,
389 stroke_width: config.line_width as u32,
390 };
391
392 chart
393 .draw_series(std::iter::once(PathElement::new(
394 channel_data
395 .iter()
396 .enumerate()
397 .map(|(i, &sample)| ((i + start_from) as f64, sample as f64))
398 .collect::<Vec<(f64, f64)>>(),
399 line_style,
400 )))
401 .unwrap();
402
403 abnormal_smaples_series(&[chart_data], &mut chart, y_min, y_max);
404}