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