Skip to content

Commit

Permalink
Plot: Add auto_bounds_x, auto_bounds_y and reset (#2029)
Browse files Browse the repository at this point in the history
* Make double click reset to the initial view

* Allow forcing auto bounds

* Allow plots to be reset

* Rename `modified` to `bounds_modified`
  • Loading branch information
Zoxc authored Oct 2, 2022
1 parent 5e91a30 commit 53ff837
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 62 deletions.
131 changes: 71 additions & 60 deletions crates/egui/src/widgets/plot/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,36 +73,27 @@ 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<bool> for AutoBounds {
impl From<bool> for AxisBools {
fn from(val: bool) -> Self {
AutoBounds::from_bool(val)
AxisBools { x: val, y: val }
}
}

/// Information about the plot that has to persist between frames.
#[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<String>,
hidden_items: ahash::HashSet<String>,
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<Pos2>,
Expand Down Expand Up @@ -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,
Expand All @@ -281,6 +273,8 @@ pub struct Plot {
data_aspect: Option<f32>,
view_aspect: Option<f32>,

reset: bool,

show_x: bool,
show_y: bool,
label_formatter: LabelFormatter,
Expand All @@ -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,
Expand All @@ -316,6 +311,8 @@ impl Plot {
data_aspect: None,
view_aspect: None,

reset: false,

show_x: true,
show_y: true,
label_formatter: None,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<R>(self, ui: &mut Ui, build_fn: impl FnOnce(&mut PlotUi) -> R) -> InnerResponse<R> {
self.show_dyn(ui, Box::new(build_fn))
Expand All @@ -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,
Expand All @@ -624,6 +640,7 @@ impl Plot {
coordinates_formatter,
axis_formatters,
legend_config,
reset,
show_background,
show_axes,
linked_axes,
Expand Down Expand Up @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -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);
}
}
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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();
}
}
}
Expand Down Expand Up @@ -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,
};
Expand Down
23 changes: 21 additions & 2 deletions crates/egui/src/widgets/plot/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 53ff837

Please sign in to comment.