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 format!(r#"<svg width="{svg_width}" height="{total_height}" "#).as_str(),
161 format!(
162 r#"<svg width="100%" height="100%" preserveAspectRatio="{preserve_aspect_ratio}" "#
163 )
164 .as_str(),
165 )
166 } else {
167 svg_buffer
168 }
169}
170
171fn multi_channel_chart(
172 charts_data: Vec<ChannelChartData>,
173 config: &SvgChartConfig,
174 solid_input: bool,
175 start_from: usize,
176 axis_color: RGBColor,
177 area: &DrawingArea<SVGBackend<'_>, plotters::coord::Shift>,
178 sample_rate: f64,
179) {
180 let num_samples = charts_data
181 .iter()
182 .map(|chart| chart.data.len())
183 .max()
184 .unwrap_or_default();
185 let min_val = charts_data
186 .iter()
187 .flat_map(|c| c.data.iter())
188 .cloned()
189 .fold(f32::INFINITY, f32::min);
190 let max_val = charts_data
191 .iter()
192 .flat_map(|c| c.data.iter())
193 .cloned()
194 .fold(f32::NEG_INFINITY, f32::max);
195
196 let range = (max_val - min_val).max(f32::EPSILON);
197 let y_min = (min_val - range * 0.1) as f64;
198 let y_max = (max_val + range * 0.1) as f64;
199
200 let mut chart = ChartBuilder::on(area)
202 .margin(5)
203 .x_label_area_size(35)
204 .y_label_area_size(50)
205 .build_cartesian_2d(
206 start_from as f64..(num_samples + start_from) as f64,
207 y_min..y_max,
208 )
209 .unwrap();
210
211 let mut mesh = chart.configure_mesh();
212
213 mesh.axis_style(axis_color.mix(0.3));
214
215 if !config.show_grid {
216 mesh.disable_mesh();
217 } else {
218 mesh.light_line_style(axis_color.mix(0.1))
219 .bold_line_style(axis_color.mix(0.2));
220 }
221
222 if config.show_labels {
223 let x_labels = num_x_labels(num_samples, sample_rate);
224 mesh.x_labels(
225 config
226 .max_labels_x_axis
227 .map(|mx| x_labels.min(mx))
228 .unwrap_or(x_labels),
229 )
230 .y_labels(3)
231 .label_style(("sans-serif", 10, &axis_color));
232 }
233
234 let formatter = |v: &f64| time_formatter(*v as usize, sample_rate);
235 if config.format_x_axis_labels_as_time {
236 mesh.x_label_formatter(&formatter);
237 }
238
239 mesh.draw().unwrap();
240
241 let mut has_legend = false;
242
243 for entry in charts_data.iter().filter(|d| !d.is_input || solid_input) {
246 let ChannelChartData {
247 data: channel_data,
248 color,
249 label,
250 ..
251 } = entry;
252
253 let line_style = ShapeStyle {
254 color: color.to_rgba(),
255 filled: false,
256 stroke_width: config.line_width as u32,
257 };
258
259 let series = chart
260 .draw_series(std::iter::once(PathElement::new(
261 channel_data
262 .iter()
263 .enumerate()
264 .map(|(i, &sample)| ((i + start_from) as f64, sample as f64))
265 .collect::<Vec<(f64, f64)>>(),
266 line_style,
267 )))
268 .unwrap();
269
270 if let Some(label) = label {
271 series
272 .label(label)
273 .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], entry.color));
274 has_legend = true;
275 }
276 }
277
278 if !solid_input && charts_data.iter().any(|d| d.is_input) {
280 for entry in charts_data.iter().filter(|d| d.is_input) {
281 let ChannelChartData {
282 data: channel_data,
283 color,
284 label,
285 ..
286 } = entry;
287
288 let line_style = ShapeStyle {
289 color: color.to_rgba(),
290 filled: false,
291 stroke_width: config.line_width as u32,
292 };
293
294 let dashed = DashedPathElement::new(
295 channel_data
296 .iter()
297 .enumerate()
298 .map(|(i, &sample)| ((i + start_from) as f64, sample as f64))
299 .collect::<Vec<(f64, f64)>>(),
300 2,
301 3,
302 line_style,
303 );
304
305 let series = chart.draw_series(std::iter::once(dashed)).unwrap();
306
307 if let Some(label) = label {
308 series.label(label).legend(|(x, y)| {
309 DashedPathElement::new(vec![(x, y), (x + 20, y)], 2, 3, entry.color)
310 });
311 has_legend = true;
312 }
313 }
314 }
315
316 abnormal_smaples_series(&charts_data, &mut chart, y_min, y_max);
317
318 if has_legend {
319 let background = parse_hex_color(&config.background_color);
320 let contrasting = get_contrasting_color(&background);
321
322 chart
323 .configure_series_labels()
324 .border_style(contrasting)
325 .background_style(background)
326 .label_font(TextStyle::from(("sans-serif", 10)).color(&contrasting))
327 .draw()
328 .unwrap();
329 }
330}
331
332fn one_channel_chart(
333 chart_data: ChannelChartData,
334 config: &SvgChartConfig,
335 start_from: usize,
336 area: &DrawingArea<SVGBackend<'_>, plotters::coord::Shift>,
337 sample_rate: f64,
338) {
339 let ChannelChartData {
340 data: channel_data,
341 color,
342 label,
343 ..
344 } = &chart_data;
345
346 let num_samples = channel_data.len();
347
348 let min_val = channel_data.iter().cloned().fold(f32::INFINITY, f32::min);
350 let max_val = channel_data
351 .iter()
352 .cloned()
353 .fold(f32::NEG_INFINITY, f32::max);
354 let range = (max_val - min_val).max(f32::EPSILON);
355 let y_min = (min_val - range * 0.1) as f64;
356 let y_max = (max_val + range * 0.1) as f64;
357
358 let mut chart = ChartBuilder::on(area)
360 .margin(5)
361 .x_label_area_size(if label.is_some() { 35 } else { 0 })
362 .y_label_area_size(if label.is_some() { 50 } else { 0 })
363 .build_cartesian_2d(
364 start_from as f64..(num_samples + start_from) as f64,
365 y_min..y_max,
366 )
367 .unwrap();
368
369 let mut mesh = chart.configure_mesh();
370
371 mesh.axis_style(color.mix(0.3));
372
373 if !config.show_grid {
374 mesh.disable_mesh();
375 } else {
376 mesh.light_line_style(color.mix(0.1))
377 .bold_line_style(color.mix(0.2));
378 }
379
380 if let Some(label) = label {
381 let x_labels = num_x_labels(num_samples, sample_rate);
382 mesh.x_labels(
383 config
384 .max_labels_x_axis
385 .map(|mx| x_labels.min(mx))
386 .unwrap_or(x_labels),
387 )
388 .y_labels(3)
389 .x_desc(label)
390 .label_style(("sans-serif", 10, &color));
391 }
392
393 let formatter = |v: &f64| time_formatter(*v as usize, sample_rate);
394 if config.format_x_axis_labels_as_time {
395 mesh.x_label_formatter(&formatter);
396 }
397
398 mesh.draw().unwrap();
399
400 let line_style = ShapeStyle {
402 color: color.to_rgba(),
403 filled: false,
404 stroke_width: config.line_width as u32,
405 };
406
407 chart
408 .draw_series(std::iter::once(PathElement::new(
409 channel_data
410 .iter()
411 .enumerate()
412 .map(|(i, &sample)| ((i + start_from) as f64, sample as f64))
413 .collect::<Vec<(f64, f64)>>(),
414 line_style,
415 )))
416 .unwrap();
417
418 abnormal_smaples_series(&[chart_data], &mut chart, y_min, y_max);
419}