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