From 53ff83737b6e2cb44772e5f494657f6e5cace4ea Mon Sep 17 00:00:00 2001 From: Zoxc Date: Sun, 2 Oct 2022 08:44:59 +0200 Subject: [PATCH] Plot: Add `auto_bounds_x`, `auto_bounds_y` and `reset` (#2029) * Make double click reset to the initial view * Allow forcing auto bounds * Allow plots to be reset * Rename `modified` to `bounds_modified` --- crates/egui/src/widgets/plot/mod.rs | 131 ++++++++++++---------- crates/egui/src/widgets/plot/transform.rs | 23 +++- 2 files changed, 92 insertions(+), 62 deletions(-) diff --git a/crates/egui/src/widgets/plot/mod.rs b/crates/egui/src/widgets/plot/mod.rs index 9e82d2565e5..14d1fa0f133 100644 --- a/crates/egui/src/widgets/plot/mod.rs +++ b/crates/egui/src/widgets/plot/mod.rs @@ -73,25 +73,15 @@ impl Default for CoordinatesFormatter { const MIN_LINE_SPACING_IN_POINTS: f64 = 6.0; // TODO(emilk): large enough for a wide label #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[derive(Clone)] -struct AutoBounds { +#[derive(Copy, Clone)] +struct AxisBools { x: bool, y: bool, } -impl AutoBounds { - fn from_bool(val: bool) -> Self { - AutoBounds { x: val, y: val } - } - - fn any(&self) -> bool { - self.x || self.y - } -} - -impl From for AutoBounds { +impl From for AxisBools { fn from(val: bool) -> Self { - AutoBounds::from_bool(val) + AxisBools { x: val, y: val } } } @@ -99,10 +89,11 @@ impl From for AutoBounds { #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[derive(Clone)] struct PlotMemory { - auto_bounds: AutoBounds, + /// Indicates if the user has modified the bounds, for example by moving or zooming, + /// or if the bounds should be calculated based by included point or auto bounds. + bounds_modified: AxisBools, hovered_entry: Option, hidden_items: ahash::HashSet, - min_auto_bounds: PlotBounds, last_screen_transform: ScreenTransform, /// Allows to remember the first click position when performing a boxed zoom last_click_pos_for_zoom: Option, @@ -268,6 +259,7 @@ pub struct Plot { allow_zoom: bool, allow_drag: bool, allow_scroll: bool, + auto_bounds: AxisBools, min_auto_bounds: PlotBounds, margin_fraction: Vec2, allow_boxed_zoom: bool, @@ -281,6 +273,8 @@ pub struct Plot { data_aspect: Option, view_aspect: Option, + reset: bool, + show_x: bool, show_y: bool, label_formatter: LabelFormatter, @@ -303,6 +297,7 @@ impl Plot { allow_zoom: true, allow_drag: true, allow_scroll: true, + auto_bounds: false.into(), min_auto_bounds: PlotBounds::NOTHING, margin_fraction: Vec2::splat(0.05), allow_boxed_zoom: true, @@ -316,6 +311,8 @@ impl Plot { data_aspect: None, view_aspect: None, + reset: false, + show_x: true, show_y: true, label_formatter: None, @@ -402,7 +399,7 @@ impl Plot { self } - /// Set the side margin as a fraction of the plot size. + /// Set the side margin as a fraction of the plot size. Only used for auto bounds. /// /// For instance, a value of `0.1` will add 10% space on both sides. pub fn set_margin_fraction(mut self, margin_fraction: Vec2) -> Self { @@ -556,6 +553,18 @@ impl Plot { self } + /// Expand bounds to fit all items across the x axis, including values given by `include_x`. + pub fn auto_bounds_x(mut self) -> Self { + self.auto_bounds.x = true; + self + } + + /// Expand bounds to fit all items across the y axis, including values given by `include_y`. + pub fn auto_bounds_y(mut self) -> Self { + self.auto_bounds.y = true; + self + } + /// Show a legend including all named items. pub fn legend(mut self, legend: Legend) -> Self { self.legend_config = Some(legend); @@ -592,6 +601,12 @@ impl Plot { self } + /// Resets the plot. + pub fn reset(mut self) -> Self { + self.reset = true; + self + } + /// Interact with and add items to the plot and finally draw it. pub fn show(self, ui: &mut Ui, build_fn: impl FnOnce(&mut PlotUi) -> R) -> InnerResponse { self.show_dyn(ui, Box::new(build_fn)) @@ -611,6 +626,7 @@ impl Plot { allow_drag, allow_boxed_zoom, boxed_zoom_pointer_button: boxed_zoom_pointer, + auto_bounds, min_auto_bounds, margin_fraction, width, @@ -624,6 +640,7 @@ impl Plot { coordinates_formatter, axis_formatters, legend_config, + reset, show_background, show_axes, linked_axes, @@ -661,11 +678,19 @@ impl Plot { // Load or initialize the memory. let plot_id = ui.make_persistent_id(id_source); ui.ctx().check_for_id_clash(plot_id, rect, "Plot"); - let mut memory = PlotMemory::load(ui.ctx(), plot_id).unwrap_or_else(|| PlotMemory { - auto_bounds: (!min_auto_bounds.is_valid()).into(), + let memory = if reset { + if let Some(axes) = linked_axes.as_ref() { + axes.bounds.set(None); + }; + + None + } else { + PlotMemory::load(ui.ctx(), plot_id) + } + .unwrap_or_else(|| PlotMemory { + bounds_modified: false.into(), hovered_entry: None, hidden_items: Default::default(), - min_auto_bounds, last_screen_transform: ScreenTransform::new( rect, min_auto_bounds, @@ -675,24 +700,12 @@ impl Plot { last_click_pos_for_zoom: None, }); - // If the min bounds changed, recalculate everything. - if min_auto_bounds != memory.min_auto_bounds { - memory = PlotMemory { - auto_bounds: (!min_auto_bounds.is_valid()).into(), - hovered_entry: None, - min_auto_bounds, - ..memory - }; - memory.clone().store(ui.ctx(), plot_id); - } - let PlotMemory { - mut auto_bounds, + mut bounds_modified, mut hovered_entry, mut hidden_items, last_screen_transform, mut last_click_pos_for_zoom, - .. } = memory; // Call the plot build function. @@ -774,52 +787,51 @@ impl Plot { if let Some(linked_bounds) = axes.get() { if axes.link_x { bounds.set_x(&linked_bounds); - // Turn off auto bounds to keep it from overriding what we just set. - auto_bounds.x = false; + // Mark the axis as modified to prevent it from being changed. + bounds_modified.x = true; } if axes.link_y { bounds.set_y(&linked_bounds); - // Turn off auto bounds to keep it from overriding what we just set. - auto_bounds.y = false; + // Mark the axis as modified to prevent it from being changed. + bounds_modified.y = true; } } }; - // Allow double clicking to reset to automatic bounds. + // Allow double clicking to reset to the initial bounds. if response.double_clicked_by(PointerButton::Primary) { - auto_bounds = true.into(); + bounds_modified = false.into(); } - if !bounds.is_valid() { - auto_bounds = true.into(); + // Reset bounds to initial bounds if we haven't been modified. + if !bounds_modified.x { + bounds.set_x(&min_auto_bounds); + } + if !bounds_modified.y { + bounds.set_y(&min_auto_bounds); } - // Set bounds automatically based on content. - if auto_bounds.any() { - if auto_bounds.x { - bounds.set_x(&min_auto_bounds); - } - - if auto_bounds.y { - bounds.set_y(&min_auto_bounds); - } + let auto_x = !bounds_modified.x && (!min_auto_bounds.is_valid_x() || auto_bounds.x); + let auto_y = !bounds_modified.y && (!min_auto_bounds.is_valid_y() || auto_bounds.y); + // Set bounds automatically based on content. + if auto_x || auto_y { for item in &items { let item_bounds = item.bounds(); - if auto_bounds.x { + if auto_x { bounds.merge_x(&item_bounds); } - if auto_bounds.y { + if auto_y { bounds.merge_y(&item_bounds); } } - if auto_bounds.x { + if auto_x { bounds.add_relative_margin_x(margin_fraction); } - if auto_bounds.y { + if auto_y { bounds.add_relative_margin_y(margin_fraction); } } @@ -840,7 +852,7 @@ impl Plot { if allow_drag && response.dragged_by(PointerButton::Primary) { response = response.on_hover_cursor(CursorIcon::Grabbing); transform.translate_bounds(-response.drag_delta()); - auto_bounds = false.into(); + bounds_modified = true.into(); } // Zooming @@ -887,7 +899,7 @@ impl Plot { }; if new_bounds.is_valid() { transform.set_bounds(new_bounds); - auto_bounds = false.into(); + bounds_modified = true.into(); } // reset the boxed zoom state last_click_pos_for_zoom = None; @@ -904,14 +916,14 @@ impl Plot { }; if zoom_factor != Vec2::splat(1.0) { transform.zoom(zoom_factor, hover_pos); - auto_bounds = false.into(); + bounds_modified = true.into(); } } if allow_scroll { let scroll_delta = ui.input().scroll_delta; if scroll_delta != Vec2::ZERO { transform.translate_bounds(-scroll_delta); - auto_bounds = false.into(); + bounds_modified = true.into(); } } } @@ -961,10 +973,9 @@ impl Plot { } let memory = PlotMemory { - auto_bounds, + bounds_modified, hovered_entry, hidden_items, - min_auto_bounds, last_screen_transform: transform, last_click_pos_for_zoom, }; diff --git a/crates/egui/src/widgets/plot/transform.rs b/crates/egui/src/widgets/plot/transform.rs index 584d0a0a82f..6abfe2616e3 100644 --- a/crates/egui/src/widgets/plot/transform.rs +++ b/crates/egui/src/widgets/plot/transform.rs @@ -40,10 +40,26 @@ impl PlotBounds { && self.max[1].is_finite() } + pub fn is_finite_x(&self) -> bool { + self.min[0].is_finite() && self.max[0].is_finite() + } + + pub fn is_finite_y(&self) -> bool { + self.min[1].is_finite() && self.max[1].is_finite() + } + pub fn is_valid(&self) -> bool { self.is_finite() && self.width() > 0.0 && self.height() > 0.0 } + pub fn is_valid_x(&self) -> bool { + self.is_finite_x() && self.width() > 0.0 + } + + pub fn is_valid_y(&self) -> bool { + self.is_finite_y() && self.height() > 0.0 + } + pub fn width(&self) -> f64 { self.max[0] - self.min[0] } @@ -181,8 +197,11 @@ pub(crate) struct ScreenTransform { impl ScreenTransform { 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() { - bounds = PlotBounds::new_symmetrical(1.0); + 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)); } // Scale axes so that the origin is in the center.