Skip to main content

audio_lab/plot/
data.rs

1use crate::signal::data::RealData;
2use eframe::egui;
3use egui_plot::{GridInput, GridMark, Legend, Line, Plot, PlotPoint, PlotPoints};
4
5use crate::plot::native_options_any_thread;
6use crate::plot::save::SavePlotState;
7
8#[derive(Clone, Copy, Debug, PartialEq, Default)]
9pub struct RealDataPlotOptions {
10    pub log_x: bool,
11}
12
13/// Returns log10-spaced grid marks (same tiers as spectrum_grid_spacer in freq.rs).
14fn log_grid_spacer(input: GridInput) -> Vec<GridMark> {
15    let (lo, hi) = (input.bounds.0, input.bounds.1);
16    let mut marks = Vec::new();
17
18    let start_exp = lo.floor() as i32 - 1;
19    let end_exp = hi.ceil() as i32 + 1;
20
21    for exp in start_exp..=end_exp {
22        let decade = 10f64.powi(exp);
23        for mult in 1..10 {
24            let val = decade * mult as f64;
25            let log_pos = val.log10();
26            if log_pos < lo || log_pos > hi {
27                continue;
28            }
29            let step_size = match mult {
30                1 => 1.0,
31                2 | 5 => 0.3,
32                _ => 0.1,
33            };
34            marks.push(GridMark {
35                value: log_pos,
36                step_size,
37            });
38        }
39    }
40    marks
41}
42
43pub(crate) struct RealDataPlot {
44    channels: Vec<Vec<PlotPoint>>,
45    options: RealDataPlotOptions,
46    x_label: String,
47    y_label: String,
48    save: SavePlotState,
49}
50
51impl RealDataPlot {
52    pub(crate) fn new(
53        data: &RealData,
54        title: &str,
55        x_label: impl Into<String>,
56        y_label: impl Into<String>,
57        options: RealDataPlotOptions,
58    ) -> Self {
59        let x_axis = data.x_axis();
60        let channels = data
61            .channel_iter()
62            .map(|ch| {
63                x_axis
64                    .iter()
65                    .zip(ch.iter())
66                    .filter(|&(x, _)| !options.log_x || x > 0.0)
67                    .map(|(x, &y)| {
68                        let px = if options.log_x { x.log10() } else { x };
69                        PlotPoint::new(px, y)
70                    })
71                    .collect()
72            })
73            .collect();
74        Self {
75            channels,
76            options,
77            x_label: x_label.into(),
78            y_label: y_label.into(),
79            save: SavePlotState::new(title),
80        }
81    }
82}
83
84impl eframe::App for RealDataPlot {
85    fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
86        egui::Panel::top("controls").show(ui, |ui| {
87            self.save.show_panel(ui);
88        });
89        egui::CentralPanel::default().show(ui, |ui| {
90            let mut plot = Plot::new("real_data")
91                .x_axis_label(&self.x_label)
92                .y_axis_label(&self.y_label)
93                .legend(Legend::default());
94
95            if self.options.log_x {
96                plot = plot
97                    .x_grid_spacer(log_grid_spacer)
98                    .x_axis_formatter(|mark, _range| {
99                        let v = 10f64.powf(mark.value);
100                        if v >= 1000.0 {
101                            format!("{:.3}k", v / 1000.0)
102                        } else {
103                            format!("{v:.4}")
104                        }
105                    })
106                    .label_formatter(|pos| {
107                        let (name, point) = match pos {
108                            egui_plot::HoverPosition::NearDataPoint {
109                                plot_name,
110                                position,
111                                ..
112                            } => (*plot_name, position),
113                            egui_plot::HoverPosition::Elsewhere { position } => ("", position),
114                        };
115                        let v = 10f64.powf(point.x);
116                        let x_str = if v >= 1000.0 {
117                            format!("{:.3}k", v / 1000.0)
118                        } else {
119                            format!("{v:.4}")
120                        };
121                        Some(if name.is_empty() {
122                            format!("{x_str}\n{:.4}", point.y)
123                        } else {
124                            format!("{name}\n{x_str}\n{:.4}", point.y)
125                        })
126                    });
127            }
128
129            plot.show(ui, |plot_ui| {
130                for (i, points) in self.channels.iter().enumerate() {
131                    plot_ui.line(Line::new(
132                        format!("Channel {i}"),
133                        PlotPoints::Borrowed(points),
134                    ));
135                }
136            });
137        });
138        self.save.handle_screenshot(ui.ctx());
139    }
140}
141
142pub fn show_real_data(
143    title: &str,
144    data: &RealData,
145    x_label: impl Into<String>,
146    y_label: impl Into<String>,
147    options: RealDataPlotOptions,
148) -> Result<(), crate::plot::Error> {
149    Ok(eframe::run_native(
150        title,
151        native_options_any_thread(),
152        Box::new(|_cc| {
153            Ok(Box::new(RealDataPlot::new(
154                data, title, x_label, y_label, options,
155            )))
156        }),
157    )?)
158}
159
160#[cfg(test)]
161mod tests {
162    use crate::signal::data::RealData;
163
164    use super::*;
165
166    #[test]
167    #[ignore]
168    fn test_show_real_data() {
169        use crate::signal::ndarray::{Array1, Axis, stack};
170
171        let x = Array1::linspace(0.0, 1.0, 500);
172        let data = RealData::new(
173            x.clone(),
174            stack![Axis(0), x.mapv(|t| t.sin()), x.mapv(|t| t.cos())],
175        )
176        .unwrap();
177        show_real_data(
178            "Real Data Test",
179            &data,
180            "x",
181            "y",
182            RealDataPlotOptions::default(),
183        )
184        .unwrap();
185    }
186}