diff --git a/Cargo.toml b/Cargo.toml index a6f16e7..d8b7c4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,3 +4,5 @@ resolver = "2" [workspace.dependencies] serde = { version = "1.0.228", features = ["derive"] } +eframe = { version = "0.31" } +egui_plot = { version = "0.31" } diff --git a/utilities/Cargo.toml b/utilities/Cargo.toml index 5898309..0b26319 100644 --- a/utilities/Cargo.toml +++ b/utilities/Cargo.toml @@ -5,3 +5,5 @@ edition = "2024" [dependencies] serde = { workspace = true } +eframe = { workspace = true } +egui_plot = { workspace = true } \ No newline at end of file diff --git a/utilities/examples/plot_example.rs b/utilities/examples/plot_example.rs new file mode 100644 index 0000000..f55ae75 --- /dev/null +++ b/utilities/examples/plot_example.rs @@ -0,0 +1,13 @@ +use utilities::plot::plot_series; + +fn main() { + let mut sin = Vec::with_capacity(100); + let mut cos = Vec::with_capacity(100); + for i in 0..100 { + let x = i as f64 * 0.1; + sin.push([x, x.sin()]); + cos.push([x, x.cos()]); + } + + plot_series(&[("sin", &sin), ("cos", &cos)]); +} diff --git a/utilities/src/lib.rs b/utilities/src/lib.rs index 4fb6cfe..d5cffa2 100644 --- a/utilities/src/lib.rs +++ b/utilities/src/lib.rs @@ -1 +1,2 @@ pub mod lowpass; +pub mod plot; diff --git a/utilities/src/plot.rs b/utilities/src/plot.rs new file mode 100644 index 0000000..a133a4e --- /dev/null +++ b/utilities/src/plot.rs @@ -0,0 +1,124 @@ +//! Interactive line-plot utility built on `egui`. +//! +//! This module provides a small native plotting helper for manual inspection of +//! numeric data. It opens an `eframe` window and renders one or more line +//! series in a native window. +//! +//! Use [`plot`] for a single series of `[x, y]` points, [`plot_indexed`] for a +//! single series of sampled values plotted against their indices, or +//! [`plot_series`] for multiple named series on the same plot. +//! +//! # Examples +//! +//! ```no_run +//! use utilities::plot; +//! +//! let mut data = Vec::with_capacity(100); +//! for i in 0..100 { +//! let x = i as f64 * 0.1; +//! data.push([x, x.sin()]); +//! } +//! +//! plot(&data); +//! ``` +//! +//! ```no_run +//! use utilities::plot::plot_indexed; +//! +//! let mut data = Vec::with_capacity(100); +//! for i in 0..100 { +//! let x = i as f64 * 0.1; +//! data.push(x.sin()); +//! } +//! +//! plot_indexed(&data); +//! ``` +//! +//! ```no_run +//! use utilities::plot::plot_series; +//! +//! let mut sin = Vec::with_capacity(100); +//! let mut cos = Vec::with_capacity(100); +//! for i in 0..100 { +//! let x = i as f64 * 0.1; +//! sin.push([x, x.sin()]); +//! cos.push([x, x.cos()]); +//! } +//! +//! plot_series(&[("sin", &sin), ("cos", &cos)]); +//! ``` + +use eframe::egui; +use egui_plot::{Legend, Line, Plot, PlotPoints}; + +/// Render a single series in a native window, where each point is `[x, y]`. +pub fn plot(data: &[[f64; 2]]) { + let data = vec![Series { + name: String::new(), + points: data.to_vec(), + }]; + eframe::run_native( + "plot", + eframe::NativeOptions::default(), + Box::new(|_| Ok(Box::new(PlotApp { data }))), + ) + .expect("Failed to open plot window"); +} + +/// Render a single series of sampled values using the sample index as `x`. +pub fn plot_indexed(data: &[f64]) { + let mut points = Vec::with_capacity(data.len()); + for (i, &y) in data.iter().enumerate() { + points.push([i as f64, y]); + } + plot(&points); +} + +/// Render multiple named `[x, y]` series on the same plot. +pub fn plot_series(series: &[(&str, &[[f64; 2]])]) { + let mut data = Vec::with_capacity(series.len()); + for (name, points) in series { + data.push(Series { + name: (*name).to_string(), + points: points.to_vec(), + }); + } + eframe::run_native( + "plot", + eframe::NativeOptions::default(), + Box::new(|_| Ok(Box::new(PlotApp { data }))), + ) + .expect("Failed to open plot window"); +} + +struct Series { + name: String, + points: Vec<[f64; 2]>, +} + +struct PlotApp { + data: Vec, +} + +impl eframe::App for PlotApp { + fn update(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) { + if ctx.input(|i| i.key_pressed(egui::Key::Escape) || !i.keys_down.is_empty()) { + ctx.send_viewport_cmd(egui::ViewportCommand::Close); + } + egui::CentralPanel::default().show(ctx, |ui| { + let mut plot = Plot::new("p"); + if self.data.len() > 1 { + plot = plot.legend(Legend::default()); + } + plot.show(ui, |pu| { + for series in &self.data { + let mut line = Line::new(PlotPoints::from_iter(series.points.iter().copied())); + if !series.name.is_empty() { + line = line.name(&series.name); + } + pu.line(line); + } + }); + }); + } +}