Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add methods to zoom a Plot programmatically #2714

Merged
merged 3 commits into from
Jan 6, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions crates/egui_plot/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1393,6 +1398,27 @@ impl PlotUi {
&self.response
}

/// Scale the plot bounds around a position in screen coordinates. Can be useful for
YgorSouza marked this conversation as resolved.
Show resolved Hide resolved
/// 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<PlotPoint> {
// We need to subtract the drag delta to keep in sync with the frame-delayed screen transform:
Expand Down
12 changes: 8 additions & 4 deletions crates/egui_plot/src/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
19 changes: 19 additions & 0 deletions examples/custom_plot_manipulation/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "custom_plot_manipulation"
YgorSouza marked this conversation as resolved.
Show resolved Hide resolved
version = "0.1.0"
authors = ["Ygor Souza <[email protected]>"]
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 = { version = "0.10", default-features = false, features = [
"auto-color",
"humantime",
] }
7 changes: 7 additions & 0 deletions examples/custom_plot_manipulation/README.md
Original file line number Diff line number Diff line change
@@ -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)
128 changes: 128 additions & 0 deletions examples/custom_plot_manipulation/src/main.rs
Original file line number Diff line number Diff line change
@@ -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::<PlotExample>::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 {
YgorSouza marked this conversation as resolved.
Show resolved Hide resolved
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 {
YgorSouza marked this conversation as resolved.
Show resolved Hide resolved
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"));
});
});
}
}
Loading