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

Plot: Add auto_bounds_x, auto_bounds_y and reset #2029

Merged
merged 4 commits into from Oct 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
131 changes: 71 additions & 60 deletions crates/egui/src/widgets/plot/mod.rs
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
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