diff --git a/Cargo.lock b/Cargo.lock index 12c7560b907..81b6b3b6cef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2825,6 +2825,15 @@ dependencies = [ "time", ] +[[package]] +name = "plot_log_scale" +version = "0.1.0" +dependencies = [ + "eframe", + "egui_plot", + "env_logger", +] + [[package]] name = "png" version = "0.17.10" diff --git a/examples/plot_log_scale/Cargo.toml b/examples/plot_log_scale/Cargo.toml new file mode 100644 index 00000000000..0b26cb56d45 --- /dev/null +++ b/examples/plot_log_scale/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "plot_log_scale" +version = "0.1.0" +authors = ["Ygor Souza "] +license = "MIT OR Apache-2.0" +edition = "2021" +rust-version = "1.76" +publish = false + +[lints] +workspace = true + + +[dependencies] +eframe = { workspace = true, features = [ + "default", + "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO +] } +egui_plot.workspace = true +env_logger = { version = "0.10", default-features = false, features = [ + "auto-color", + "humantime", +] } diff --git a/examples/plot_log_scale/README.md b/examples/plot_log_scale/README.md new file mode 100644 index 00000000000..1b119f921bb --- /dev/null +++ b/examples/plot_log_scale/README.md @@ -0,0 +1,7 @@ +Example how to display semi-log and log-log plots + +```sh +cargo run -p plot_log_scale +``` + +![](screenshot.png) diff --git a/examples/plot_log_scale/src/main.rs b/examples/plot_log_scale/src/main.rs new file mode 100644 index 00000000000..d0e0315189c --- /dev/null +++ b/examples/plot_log_scale/src/main.rs @@ -0,0 +1,181 @@ +//! This example shows how to implement semi-log and log-log plots +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +#![allow(rustdoc::missing_crate_level_docs)] // it's an example + +use std::ops::RangeInclusive; + +use eframe::egui::{self}; +use egui_plot::{GridInput, GridMark, Legend, Line}; + +fn main() -> Result<(), eframe::Error> { + env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). + let options = eframe::NativeOptions::default(); + eframe::run_native( + "Plot", + options, + Box::new(|_cc| Ok(Box::::default())), + ) +} + +struct PlotExample { + log_x: bool, + log_y: bool, + signals: Vec, +} + +struct Signal { + name: &'static str, + points: Vec<[f64; 2]>, +} + +impl Default for PlotExample { + fn default() -> Self { + let x = (-2000..2000).map(|x| x as f64 / 100.0); + let signals = vec![ + Signal { + name: "y=x", + points: x.clone().map(|x| [x, x]).collect(), + }, + Signal { + name: "y=x^2", + points: x.clone().map(|x| [x, x.powi(2)]).collect(), + }, + Signal { + name: "y=exp(x)", + points: x.clone().map(|x| [x, x.exp()]).collect(), + }, + Signal { + name: "y=ln(x)", + points: x.clone().map(|x| [x, x.ln()]).collect(), + }, + ]; + Self { + log_x: true, + log_y: true, + signals, + } + } +} + +impl eframe::App for PlotExample { + fn update(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) { + egui::SidePanel::left("options").show(ctx, |ui| { + ui.checkbox(&mut self.log_x, "X axis log scale"); + ui.checkbox(&mut self.log_y, "Y axis log scale"); + }); + let log_x = self.log_x; + let log_y = self.log_y; + egui::CentralPanel::default().show(ctx, |ui| { + let mut plot = egui_plot::Plot::new("plot") + .legend(Legend::default()) + .label_formatter(|name, value| { + let x = if log_x { + 10.0f64.powf(value.x) + } else { + value.x + }; + let y = if log_y { + 10.0f64.powf(value.y) + } else { + value.y + }; + if !name.is_empty() { + format!("{name}: {x:.3}, {y:.3}") + } else { + format!("{x:.3}, {y:.3}") + } + }); + if log_x { + plot = plot + .x_grid_spacer(log_axis_spacer) + .x_axis_formatter(log_axis_formatter); + } + if log_y { + plot = plot + .y_grid_spacer(log_axis_spacer) + .y_axis_formatter(log_axis_formatter); + } + plot.show(ui, |plot_ui| { + for signal in &self.signals { + let points: Vec<_> = signal + .points + .iter() + .copied() + .map(|[x, y]| { + let x = if log_x { x.log10() } else { x }; + let y = if log_y { y.log10() } else { y }; + [x, y] + }) + .collect(); + plot_ui.line(Line::new(points).name(signal.name)); + } + }); + }); + } +} + +#[allow(clippy::needless_pass_by_value)] +fn log_axis_spacer(input: GridInput) -> Vec { + let (min, max) = input.bounds; + let min_decade = min.floor().max(-300.0) as i32; + let max_decade = max.ceil().min(300.0) as i32; + let span = max_decade - min_decade; + let mut marks = vec![]; + for i in min_decade..=max_decade { + if span >= 100 { + let value = i as f64; + let step_size = if i % 10 == 0 { 10.0 } else { 1.0 }; + let mark = GridMark { value, step_size }; + marks.push(mark); + } else if span >= 10 { + marks.extend( + (1..10) + .map(|j| { + let value = i as f64 + (j as f64).log10(); + let step_size = if j == 1 && i % 10 == 0 { + 10.0 + } else if j == 1 { + 1.0 + } else { + 0.1 + }; + GridMark { value, step_size } + }) + .filter(|gm| (min..=max).contains(&gm.value)), + ); + } else { + marks.extend( + (10..100) + .map(|j| { + let value = i as f64 + (j as f64).log10() - 1.0; + let step_size = if j == 10 { + 1.0 + } else if j % 10 == 0 { + 0.1 + } else { + 0.01 + }; + GridMark { value, step_size } + }) + .filter(|gm| (min..=max).contains(&gm.value)), + ); + } + } + marks +} + +fn log_axis_formatter(gm: GridMark, _bounds: &RangeInclusive) -> String { + let max_size = 5; + let min_precision = (-gm.value + 1.0).ceil().clamp(1.0, 10.0) as usize; + let digits = (gm.value).ceil().max(1.0) as usize; + let size = digits + min_precision + 1; + let value = 10.0f64.powf(gm.value); + if size < max_size { + let precision = max_size.saturating_sub(digits + 1).max(1); + egui::emath::format_with_decimals_in_range(value, 1..=precision) + } else { + let exp_digits = (digits as f64).log10() as usize; + let precision = max_size.saturating_sub(exp_digits).saturating_sub(3); + format!("{value:.precision$e}") + } +}