Skip to content

Commit

Permalink
Improve behavior of plot auto-bounds with reduced data (#4632)
Browse files Browse the repository at this point in the history
* Fixes #3808
* Fixes #2307

This PR improves the behaviour of auto-bounds with data that:
- is a single point
- where all X values are the same (e.g. vertical line)
- where all Y values are the same (e.g. horizontal line)

In all case, the auto-bound now aim to center on the data. For span,
when available, it use the same as the other axis. If the data range of
the other axis is also degenerate, then it defaults to +/- 1.0.


https://github.com/emilk/egui/assets/49431240/a62d2b5b-7856-4415-8534-83dc58cfac98


<details>
<summary>Test code</summary>

```rust
#![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 eframe::egui;
use egui_plot::{Legend, Line, Plot, PlotPoints, Points};

fn main() -> Result<(), eframe::Error> {
    env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).

    let options = eframe::NativeOptions {
        viewport: egui::ViewportBuilder::default().with_inner_size([350.0, 200.0]),
        ..Default::default()
    };
    eframe::run_native(
        "My egui App with a plot",
        options,
        Box::new(|_cc| Ok(Box::<MyApp>::default())),
    )
}

#[derive(Default)]
struct MyApp {}

impl eframe::App for MyApp {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        let mut plot_rect = None;
        egui::CentralPanel::default().show(ctx, |ui| {
            if ui.button("Save Plot").clicked() {
                ctx.send_viewport_cmd(egui::ViewportCommand::Screenshot);
            }

            let my_plot = Plot::new("My Plot").legend(Legend::default());

            // let's create a dummy line in the plot
            let inner = my_plot.show(ui, |plot_ui| {
                plot_ui.line(
                    Line::new(PlotPoints::from(vec![
                        [0.0, 10.0],
                        [2.0, 10.0],
                        [3.0, 10.0],
                    ]))
                    .name("y = 10.0"),
                );

                plot_ui.line(
                    Line::new(PlotPoints::from(vec![
                        [10.0, 10.0],
                        [10.0, 11.0],
                        [10.0, 12.0],
                    ]))
                    .name("x = 10.0"),
                );
                plot_ui.points(
                    Points::new(PlotPoints::from(vec![[5.0, 5.0]]))
                        .name("(5,5)")
                        .radius(3.0),
                );
                plot_ui.points(
                    Points::new(PlotPoints::from(vec![[5.0, 7.0]]))
                        .name("(5,7)")
                        .radius(3.0),
                );
            });
            // Remember the position of the plot
            plot_rect = Some(inner.response.rect);
        });

        // Check for returned screenshot:
        let screenshot = ctx.input(|i| {
            for event in &i.raw.events {
                if let egui::Event::Screenshot { image, .. } = event {
                    return Some(image.clone());
                }
            }
            None
        });

        if let (Some(screenshot), Some(plot_location)) = (screenshot, plot_rect) {
            if let Some(mut path) = rfd::FileDialog::new().save_file() {
                path.set_extension("png");

                // for a full size application, we should put this in a different thread,
                // so that the GUI doesn't lag during saving

                let pixels_per_point = ctx.pixels_per_point();
                let plot = screenshot.region(&plot_location, Some(pixels_per_point));
                // save the plot to png
                image::save_buffer(
                    &path,
                    plot.as_raw(),
                    plot.width() as u32,
                    plot.height() as u32,
                    image::ColorType::Rgba8,
                )
                .unwrap();
                eprintln!("Image saved to {path:?}.");
            }
        }
    }
}
```

</details>
  • Loading branch information
abey79 authored Jun 7, 2024
1 parent 2545939 commit 9f12432
Showing 1 changed file with 50 additions and 11 deletions.
61 changes: 50 additions & 11 deletions crates/egui_plot/src/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,24 @@ impl PlotBounds {
self.max[0] = other.max[0];
}

#[inline]
pub fn set_x_center_width(&mut self, x: f64, width: f64) {
self.min[0] = x - width / 2.0;
self.max[0] = x + width / 2.0;
}

#[inline]
pub fn set_y(&mut self, other: &Self) {
self.min[1] = other.min[1];
self.max[1] = other.max[1];
}

#[inline]
pub fn set_y_center_height(&mut self, y: f64, height: f64) {
self.min[1] = y - height / 2.0;
self.max[1] = y + height / 2.0;
}

#[inline]
pub fn merge(&mut self, other: &Self) {
self.min[0] = self.min[0].min(other.min[0]);
Expand Down Expand Up @@ -240,26 +252,53 @@ pub struct PlotTransform {
}

impl PlotTransform {
pub fn new(frame: Rect, mut bounds: PlotBounds, x_centered: bool, y_centered: bool) -> Self {
// Make sure they are not empty.
if !bounds.is_valid_x() {
bounds.set_x(&PlotBounds::new_symmetrical(1.0));
}
if !bounds.is_valid_y() {
bounds.set_y(&PlotBounds::new_symmetrical(1.0));
}
pub fn new(frame: Rect, bounds: PlotBounds, x_centered: bool, y_centered: bool) -> Self {
// Since the current Y bounds an affect the final X bounds and vice versa, we need to keep
// the original version of the `bounds` before we start modifying it.
let mut new_bounds = bounds;

// Sanitize bounds.
//
// When a given bound axis is "thin" (e.g. width or height is 0) but finite, we center the
// bounds around that value. If the other axis is "fat", we reuse its extent for the thin
// axis, and default to +/- 1.0 otherwise.
if !bounds.is_finite_x() {
new_bounds.set_x(&PlotBounds::new_symmetrical(1.0));
} else if bounds.width() == 0.0 {
new_bounds.set_x_center_width(
bounds.center().x,
if bounds.is_valid_y() {
bounds.height()
} else {
1.0
},
);
};

if !bounds.is_finite_y() {
new_bounds.set_y(&PlotBounds::new_symmetrical(1.0));
} else if bounds.height() == 0.0 {
new_bounds.set_y_center_height(
bounds.center().y,
if bounds.is_valid_x() {
bounds.width()
} else {
1.0
},
);
};

// Scale axes so that the origin is in the center.
if x_centered {
bounds.make_x_symmetrical();
new_bounds.make_x_symmetrical();
};
if y_centered {
bounds.make_y_symmetrical();
new_bounds.make_y_symmetrical();
};

Self {
frame,
bounds,
bounds: new_bounds,
x_centered,
y_centered,
}
Expand Down

0 comments on commit 9f12432

Please sign in to comment.