Skip to main content

insta_fun/
chart.rs

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/// Chart layout
15///
16/// Whether to plot channels on separate charts or combined charts.
17#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
18pub enum Layout {
19    /// Each channel plots on its own chart
20    #[default]
21    SeparateChannels,
22    /// All input channels plot on one chart, all output channels plot on another chart
23    ///
24    /// Same as `Combined` when `config.with_inputs` is `false`
25    CombinedPerChannelType,
26    /// All channels plot on one chart
27    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    // Create SVG backend with buffer
67    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        // Fill background
73        let bg_color = parse_hex_color(&config.background_color);
74        root.fill(&bg_color).unwrap();
75
76        // Add optional title with contrasting color
77        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                // Split area for each channel
110                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                        &current_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                    &current_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    // Build chart
213    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    // Draw outputs (or inputs as solid when `solid_input` is true) one by one,
256    // registering a legend entry per series.
257    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    // Dashed inputs when not solid
291    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    // Calculate data range
361    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    // Build chart
371    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    // Draw waveform
413    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}