From 734efa01470d0e7d2e0549cc3d70b42387869413 Mon Sep 17 00:00:00 2001 From: Ygor Souza Date: Sat, 11 Feb 2023 11:53:01 +0100 Subject: [PATCH 1/3] Add methods to zoom the plot manually So you can bind different keyboard/mouse gestures to it, lock the X or Y axis, customize the zoom speed etc. --- Cargo.lock | 9 ++ crates/egui_plot/src/lib.rs | 26 ++++ crates/egui_plot/src/transform.rs | 12 +- examples/custom_plot_manipulation/Cargo.toml | 16 +++ examples/custom_plot_manipulation/README.md | 7 + examples/custom_plot_manipulation/src/main.rs | 128 ++++++++++++++++++ 6 files changed, 194 insertions(+), 4 deletions(-) create mode 100644 examples/custom_plot_manipulation/Cargo.toml create mode 100644 examples/custom_plot_manipulation/README.md create mode 100644 examples/custom_plot_manipulation/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index ee39fbbaec7..46517158ae0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -993,6 +993,15 @@ dependencies = [ "env_logger", ] +[[package]] +name = "custom_plot_manipulation" +version = "0.1.0" +dependencies = [ + "eframe", + "egui_plot", + "env_logger", +] + [[package]] name = "custom_window_frame" version = "0.1.0" diff --git a/crates/egui_plot/src/lib.rs b/crates/egui_plot/src/lib.rs index a10953311dc..5779fe826a4 100644 --- a/crates/egui_plot/src/lib.rs +++ b/crates/egui_plot/src/lib.rs @@ -991,6 +991,10 @@ impl Plot { auto_bounds = false.into(); } BoundsModification::AutoBounds(new_auto_bounds) => auto_bounds = new_auto_bounds, + BoundsModification::Zoom(zoom_factor, center) => { + bounds.zoom(zoom_factor, center); + auto_bounds = false.into(); + } } } @@ -1330,6 +1334,7 @@ enum BoundsModification { Set(PlotBounds), Translate(Vec2), AutoBounds(Vec2b), + Zoom(Vec2, PlotPoint), } /// Provides methods to interact with a plot while building it. It is the single argument of the closure @@ -1393,6 +1398,27 @@ impl PlotUi { &self.response } + /// Scale the plot bounds around a position in screen coordinates. Can be useful for + /// implementing alternative plot navigation methods. + /// The plot bounds are divided by `zoom_factor`, therefore: + /// `zoom_factor < 1.0` zooms out, i.e., increases the visible range to show more data. + /// `zoom_factor > 1.0` zooms in, i.e., reduces the visible range to show more detail. + pub fn zoom_bounds(&mut self, zoom_factor: Vec2, center: PlotPoint) { + self.bounds_modifications + .push(BoundsModification::Zoom(zoom_factor, center)); + } + + /// Scale the plot bounds around the hovered position, if any. Can be useful for implementing + /// alternative plot navigation methods. + /// The plot bounds are divided by `zoom_factor`, therefore: + /// `zoom_factor < 1.0` zooms out, i.e., increases the visible range to show more data. + /// `zoom_factor > 1.0` zooms in, i.e., reduces the visible range to show more detail. + pub fn zoom_bounds_around_hovered(&mut self, zoom_factor: Vec2) { + if let Some(hover_pos) = self.pointer_coordinate() { + self.zoom_bounds(zoom_factor, hover_pos); + } + } + /// The pointer position in plot coordinates. Independent of whether the pointer is in the plot area. pub fn pointer_coordinate(&self) -> Option { // We need to subtract the drag delta to keep in sync with the frame-delayed screen transform: diff --git a/crates/egui_plot/src/transform.rs b/crates/egui_plot/src/transform.rs index cad4e69458d..ed325ad278a 100644 --- a/crates/egui_plot/src/transform.rs +++ b/crates/egui_plot/src/transform.rs @@ -150,6 +150,13 @@ impl PlotBounds { self.translate_y(delta.y as f64); } + pub(crate) fn zoom(&mut self, zoom_factor: Vec2, center: PlotPoint) { + self.min[0] = center.x + (self.min[0] - center.x) / (zoom_factor.x as f64); + self.max[0] = center.x + (self.max[0] - center.x) / (zoom_factor.x as f64); + self.min[1] = center.y + (self.min[1] - center.y) / (zoom_factor.y as f64); + self.max[1] = center.y + (self.max[1] - center.y) / (zoom_factor.y as f64); + } + pub(crate) fn add_relative_margin_x(&mut self, margin_fraction: Vec2) { let width = self.width().max(0.0); self.expand_x(margin_fraction.x as f64 * width); @@ -255,10 +262,7 @@ impl PlotTransform { let center = self.value_from_position(center); let mut new_bounds = self.bounds; - new_bounds.min[0] = center.x + (new_bounds.min[0] - center.x) / (zoom_factor.x as f64); - new_bounds.max[0] = center.x + (new_bounds.max[0] - center.x) / (zoom_factor.x as f64); - new_bounds.min[1] = center.y + (new_bounds.min[1] - center.y) / (zoom_factor.y as f64); - new_bounds.max[1] = center.y + (new_bounds.max[1] - center.y) / (zoom_factor.y as f64); + new_bounds.zoom(zoom_factor, center); if new_bounds.is_valid() { self.bounds = new_bounds; diff --git a/examples/custom_plot_manipulation/Cargo.toml b/examples/custom_plot_manipulation/Cargo.toml new file mode 100644 index 00000000000..bb851cbbe12 --- /dev/null +++ b/examples/custom_plot_manipulation/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "custom_plot_manipulation" +version = "0.1.0" +authors = ["Ygor Souza "] +license = "MIT OR Apache-2.0" +edition = "2021" +rust-version = "1.72" +publish = false + + +[dependencies] +eframe = { path = "../../crates/eframe", features = [ + "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO +] } +egui_plot = { path = "../../crates/egui_plot" } +env_logger = "0.10" diff --git a/examples/custom_plot_manipulation/README.md b/examples/custom_plot_manipulation/README.md new file mode 100644 index 00000000000..fedd08e574a --- /dev/null +++ b/examples/custom_plot_manipulation/README.md @@ -0,0 +1,7 @@ +Example how to use raw input events to implement alternative controls to pan and zoom the plot + +```sh +cargo run -p custom_plot_manipulation +``` + +![](screenshot.png) diff --git a/examples/custom_plot_manipulation/src/main.rs b/examples/custom_plot_manipulation/src/main.rs new file mode 100644 index 00000000000..4dffaf18ed1 --- /dev/null +++ b/examples/custom_plot_manipulation/src/main.rs @@ -0,0 +1,128 @@ +//! This example shows how to implement custom gestures to pan and zoom in the plot +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release + +use eframe::egui::{self, DragValue, Event, Vec2}; +use egui_plot::{Legend, Line, PlotPoints}; + +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| Box::::default()), + ) +} + +struct PlotExample { + lock_x: bool, + lock_y: bool, + ctrl_to_zoom: bool, + shift_to_horizontal: bool, + zoom_speed: f32, + scroll_speed: f32, +} + +impl Default for PlotExample { + fn default() -> Self { + Self { + lock_x: false, + lock_y: false, + ctrl_to_zoom: false, + shift_to_horizontal: false, + zoom_speed: 1.0, + scroll_speed: 1.0, + } + } +} + +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.lock_x, "Lock x axis").on_hover_text("Check to keep the X axis fixed, i.e., pan and zoom will only affect the Y axis"); + ui.checkbox(&mut self.lock_y, "Lock y axis").on_hover_text("Check to keep the Y axis fixed, i.e., pan and zoom will only affect the X axis"); + ui.checkbox(&mut self.ctrl_to_zoom, "Ctrl to zoom").on_hover_text("If unchecked, the behavior of the Ctrl key is inverted compared to the default controls\ni.e., scrolling the mouse without pressing any keys zooms the plot"); + ui.checkbox(&mut self.shift_to_horizontal, "Shift for horizontal scroll").on_hover_text("If unchecked, the behavior of the shift key is inverted compared to the default controls\ni.e., hold to scroll vertically, release to scroll horizontally"); + ui.horizontal(|ui| { + ui.add( + DragValue::new(&mut self.zoom_speed) + .clamp_range(0.1..=2.0) + .speed(0.1), + ); + ui.label("Zoom speed").on_hover_text("How fast to zoom in and out with the mouse wheel"); + }); + ui.horizontal(|ui| { + ui.add( + DragValue::new(&mut self.scroll_speed) + .clamp_range(0.1..=100.0) + .speed(0.1), + ); + ui.label("Scroll speed").on_hover_text("How fast to pan with the mouse wheel"); + }); + }); + egui::CentralPanel::default().show(ctx, |ui| { + let (scroll, pointer_down, modifiers) = ui.input(|i| { + let scroll = i.events.iter().find_map(|e| match e { + Event::MouseWheel { + unit: _, + delta, + modifiers: _, + } => Some(*delta), + _ => None, + }); + (scroll, i.pointer.primary_down(), i.modifiers) + }); + + ui.label("This example shows how to use raw input events to implement different plot controls than the ones egui provides by default, e.g., default to zooming instead of panning when the Ctrl key is not pressed, or controlling much it zooms with each mouse wheel step."); + + egui_plot::Plot::new("plot") + .allow_zoom(false) + .allow_drag(false) + .allow_scroll(false) + .legend(Legend::default()) + .show(ui, |plot_ui| { + if let Some(mut scroll) = scroll { + if modifiers.ctrl == self.ctrl_to_zoom { + scroll = Vec2::splat(scroll.x + scroll.y); + let mut zoom_factor = Vec2::from([ + (scroll.x * self.zoom_speed / 10.0).exp(), + (scroll.y * self.zoom_speed / 10.0).exp(), + ]); + if self.lock_x { + zoom_factor.x = 1.0; + } + if self.lock_y { + zoom_factor.y = 1.0; + } + plot_ui.zoom_bounds_around_hovered(zoom_factor); + } else { + if modifiers.shift == self.shift_to_horizontal { + scroll = Vec2::new(scroll.y, scroll.x); + } + if self.lock_x { + scroll.x = 0.0; + } + if self.lock_y { + scroll.y = 0.0; + } + let delta_pos = self.scroll_speed * scroll; + plot_ui.translate_bounds(delta_pos); + } + } + if plot_ui.response().hovered() && pointer_down { + let mut pointer_translate = -plot_ui.pointer_coordinate_drag_delta(); + if self.lock_x { + pointer_translate.x = 0.0; + } + if self.lock_y { + pointer_translate.y = 0.0; + } + plot_ui.translate_bounds(pointer_translate); + } + + let sine_points = PlotPoints::from_explicit_callback(|x| x.sin(), .., 5000); + plot_ui.line(Line::new(sine_points).name("Sine")); + }); + }); + } +} From 5c4e65ea3ee3cbf5e66bcba4586dcc1b89ab65ad Mon Sep 17 00:00:00 2001 From: Ygor Souza Date: Wed, 13 Dec 2023 20:49:41 +0100 Subject: [PATCH 2/3] Update example to match the others --- examples/custom_plot_manipulation/Cargo.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/custom_plot_manipulation/Cargo.toml b/examples/custom_plot_manipulation/Cargo.toml index bb851cbbe12..15943555f66 100644 --- a/examples/custom_plot_manipulation/Cargo.toml +++ b/examples/custom_plot_manipulation/Cargo.toml @@ -13,4 +13,7 @@ eframe = { path = "../../crates/eframe", features = [ "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO ] } egui_plot = { path = "../../crates/egui_plot" } -env_logger = "0.10" +env_logger = { version = "0.10", default-features = false, features = [ + "auto-color", + "humantime", +] } From e777653abe69b5f9aeb60ebd29ab1f751add9a07 Mon Sep 17 00:00:00 2001 From: Ygor Souza Date: Sat, 6 Jan 2024 18:40:33 +0100 Subject: [PATCH 3/3] Fix Markdown in docstrings --- crates/egui_plot/src/lib.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/crates/egui_plot/src/lib.rs b/crates/egui_plot/src/lib.rs index 5779fe826a4..11db395c1f6 100644 --- a/crates/egui_plot/src/lib.rs +++ b/crates/egui_plot/src/lib.rs @@ -1398,21 +1398,25 @@ impl PlotUi { &self.response } - /// Scale the plot bounds around a position in screen coordinates. Can be useful for - /// implementing alternative plot navigation methods. + /// Scale the plot bounds around a position in screen coordinates. + /// + /// Can be useful for implementing alternative plot navigation methods. + /// /// The plot bounds are divided by `zoom_factor`, therefore: - /// `zoom_factor < 1.0` zooms out, i.e., increases the visible range to show more data. - /// `zoom_factor > 1.0` zooms in, i.e., reduces the visible range to show more detail. + /// - `zoom_factor < 1.0` zooms out, i.e., increases the visible range to show more data. + /// - `zoom_factor > 1.0` zooms in, i.e., reduces the visible range to show more detail. pub fn zoom_bounds(&mut self, zoom_factor: Vec2, center: PlotPoint) { self.bounds_modifications .push(BoundsModification::Zoom(zoom_factor, center)); } - /// Scale the plot bounds around the hovered position, if any. Can be useful for implementing - /// alternative plot navigation methods. + /// Scale the plot bounds around the hovered position, if any. + /// + /// Can be useful for implementing alternative plot navigation methods. + /// /// The plot bounds are divided by `zoom_factor`, therefore: - /// `zoom_factor < 1.0` zooms out, i.e., increases the visible range to show more data. - /// `zoom_factor > 1.0` zooms in, i.e., reduces the visible range to show more detail. + /// - `zoom_factor < 1.0` zooms out, i.e., increases the visible range to show more data. + /// - `zoom_factor > 1.0` zooms in, i.e., reduces the visible range to show more detail. pub fn zoom_bounds_around_hovered(&mut self, zoom_factor: Vec2) { if let Some(hover_pos) = self.pointer_coordinate() { self.zoom_bounds(zoom_factor, hover_pos);