From ec3bda0683354361fde563c5c53e9fe9a8e0dde7 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Sun, 18 Apr 2021 17:49:34 +0200 Subject: [PATCH 01/15] drag and zoom support for plots --- egui/src/widgets/plot.rs | 311 ++++++++++++------ egui_demo_lib/src/apps/demo/plot_demo.rs | 3 +- egui_demo_lib/src/apps/demo/widget_gallery.rs | 2 +- 3 files changed, 215 insertions(+), 101 deletions(-) diff --git a/egui/src/widgets/plot.rs b/egui/src/widgets/plot.rs index 4435a54ac25..f64519f738d 100644 --- a/egui/src/widgets/plot.rs +++ b/egui/src/widgets/plot.rs @@ -3,6 +3,7 @@ #![allow(clippy::comparison_chain)] use color::Hsva; +use serde::{Deserialize, Serialize}; use crate::*; @@ -35,7 +36,7 @@ impl Value { /// 2D bounding box of f64 precision. /// The range of data values we show. -#[derive(Clone, Copy, PartialEq)] +#[derive(Clone, Copy, PartialEq, Serialize, Deserialize, Debug)] struct Bounds { min: [f64; 2], max: [f64; 2], @@ -47,6 +48,13 @@ impl Bounds { max: [-f64::INFINITY; 2], }; + pub fn new_symmetrical(half_extent: f64) -> Self { + Self { + min: [-half_extent; 2], + max: [half_extent; 2], + } + } + pub fn width(&self) -> f64 { self.max[0] - self.min[0] } @@ -89,12 +97,34 @@ impl Bounds { self.max[1] += pad; } - pub fn union_mut(&mut self, other: &Bounds) { + pub fn merge(&mut self, other: &Bounds) { self.min[0] = self.min[0].min(other.min[0]); self.min[1] = self.min[1].min(other.min[1]); self.max[0] = self.max[0].max(other.max[0]); self.max[1] = self.max[1].max(other.max[1]); } + + pub fn shift_x(&mut self, delta: f64) { + self.min[0] += delta; + self.max[0] += delta; + } + + pub fn shift_y(&mut self, delta: f64) { + self.min[1] += delta; + self.max[1] += delta; + } + + pub fn shift(&mut self, delta: Vec2) { + self.shift_x(delta.x as f64); + self.shift_y(delta.y as f64); + } + + pub fn add_relative_margin(&mut self, margin_fraction: Vec2) { + let width = self.width(); + let height = self.height(); + self.expand_x(margin_fraction.x as f64 * width); + self.expand_y(margin_fraction.y as f64 * height); + } } // ---------------------------------------------------------------------------- @@ -200,6 +230,22 @@ impl Curve { // ---------------------------------------------------------------------------- +/// Information about the plot that has to persist between frames. +#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)] +struct PlotMemory { + bounds: Bounds, +} + +impl Default for PlotMemory { + fn default() -> Self { + Self { + bounds: Bounds::new_symmetrical(1.), + } + } +} + +// ---------------------------------------------------------------------------- + /// A 2D plot, e.g. a graph of a function. /// /// `Plot` supports multiple curves. @@ -218,15 +264,16 @@ impl Curve { /// ``` #[derive(Clone, PartialEq)] pub struct Plot { + name: String, next_auto_color_idx: usize, curves: Vec, hlines: Vec, vlines: Vec, - bounds: Bounds, symmetrical_x_bounds: bool, symmetrical_y_bounds: bool, + automatic_bounds: bool, margin_fraction: Vec2, min_size: Vec2, @@ -239,18 +286,19 @@ pub struct Plot { show_y: bool, } -impl Default for Plot { - fn default() -> Self { +impl Plot { + pub fn new(name: impl Into) -> Self { Self { + name: name.into(), next_auto_color_idx: 0, curves: Default::default(), hlines: Default::default(), vlines: Default::default(), - bounds: Bounds::NOTHING, symmetrical_x_bounds: false, symmetrical_y_bounds: false, + automatic_bounds: false, margin_fraction: Vec2::splat(0.05), min_size: Vec2::splat(64.0), @@ -263,9 +311,7 @@ impl Default for Plot { show_y: true, } } -} -impl Plot { fn auto_color(&mut self, color: &mut Color32) { if *color == Color32::TRANSPARENT { let i = self.next_auto_color_idx; @@ -279,9 +325,10 @@ impl Plot { /// Add a data curve. /// You can add multiple curves. pub fn curve(mut self, mut curve: Curve) -> Self { - self.auto_color(&mut curve.stroke.color); - self.bounds.union_mut(&curve.bounds); - self.curves.push(curve); + if !curve.values.is_empty() { + self.auto_color(&mut curve.stroke.color); + self.curves.push(curve); + } self } @@ -290,7 +337,6 @@ impl Plot { /// Always fills the full width of the plot. pub fn hline(mut self, mut hline: HLine) -> Self { self.auto_color(&mut hline.stroke.color); - self = self.include_y(hline.y); self.hlines.push(hline); self } @@ -300,24 +346,10 @@ impl Plot { /// Always fills the full height of the plot. pub fn vline(mut self, mut vline: VLine) -> Self { self.auto_color(&mut vline.stroke.color); - self = self.include_x(vline.x); self.vlines.push(vline); self } - /// Expand bounds to include the given x value. - pub fn include_x(mut self, x: impl Into) -> Self { - self.bounds.extend_with_x(x.into()); - self - } - - /// Expand bounds to include the given y value. - /// For instance, to always show the x axis, call `plot.include_y(0.0)`. - pub fn include_y(mut self, y: impl Into) -> Self { - self.bounds.extend_with_y(y.into()); - self - } - /// If true, the x-bounds will be symmetrical, so that the x=0 zero line /// is always in the center. pub fn symmetrical_x_bounds(mut self, symmetrical_x_bounds: bool) -> Self { @@ -332,6 +364,12 @@ impl Plot { self } + /// If true, the bounds will be set based on the data. + pub fn automatic_bounds(mut self, enabled: bool) -> Self { + self.automatic_bounds = enabled; + self + } + /// width / height ratio of the data. /// For instance, it can be useful to set this to `1.0` for when the two axes show the same unit. pub fn data_aspect(mut self, data_aspect: f32) -> Self { @@ -384,11 +422,11 @@ impl Plot { impl Widget for Plot { fn ui(self, ui: &mut Ui) -> Response { let Self { + name, next_auto_color_idx: _, curves, hlines, vlines, - bounds, symmetrical_x_bounds, symmetrical_y_bounds, margin_fraction, @@ -399,12 +437,19 @@ impl Widget for Plot { view_aspect, show_x, show_y, + automatic_bounds, } = self; - let size = { - let width = width.map(|w| w.at_least(min_size.x)); - let height = height.map(|w| w.at_least(min_size.y)); + let plot_id = ui.make_persistent_id(name); + let memory = ui + .memory() + .id_data + .get_or_default::(plot_id) + .clone(); + let PlotMemory { mut bounds } = memory; + + let size = { let width = width.unwrap_or_else(|| { if let (Some(height), Some(aspect)) = (height, view_aspect) { height * aspect @@ -425,9 +470,15 @@ impl Widget for Plot { vec2(width, height) }; - let (rect, response) = ui.allocate_exact_size(size, Sense::hover()); + let (rect, response) = ui.allocate_exact_size(size, Sense::drag()); - let mut bounds = bounds; + if automatic_bounds || response.double_clicked_by(PointerButton::Primary) { + bounds = Bounds::NOTHING; + hlines.iter().for_each(|line| bounds.extend_with_y(line.y)); + vlines.iter().for_each(|line| bounds.extend_with_x(line.x)); + curves.iter().for_each(|curve| bounds.merge(&curve.bounds)); + bounds.add_relative_margin(margin_fraction); + } if symmetrical_x_bounds { let x_abs = bounds.min[0].abs().max(bounds.max[0].abs()); @@ -440,17 +491,16 @@ impl Widget for Plot { bounds.max[1] = y_abs; }; - bounds.expand_x(margin_fraction.x as f64 * bounds.width()); - bounds.expand_y(margin_fraction.y as f64 * bounds.height()); - if let Some(data_aspect) = data_aspect { let data_aspect = data_aspect as f64; let rw = rect.width() as f64; let rh = rect.height() as f64; let current_data_aspect = (bounds.width() / rw) / (bounds.height() / rh); - if current_data_aspect < data_aspect { + + let margin = 1e-5; + if current_data_aspect < data_aspect - margin { bounds.expand_x((data_aspect / current_data_aspect - 1.0) * bounds.width() * 0.5); - } else { + } else if current_data_aspect > data_aspect + margin { bounds.expand_y((current_data_aspect / data_aspect - 1.0) * bounds.height() * 0.5); } } @@ -464,14 +514,28 @@ impl Widget for Plot { }); if bounds.is_finite() && bounds.width() > 0.0 && bounds.height() > 0.0 { + let mut transform = ScreenTransform { bounds, rect }; + if response.dragged_by(PointerButton::Primary) { + transform.shift_bounds(-response.drag_delta()); + } + if let Some(hover_pos) = response.hover_pos() { + transform.zoom(-0.01 * ui.input().scroll_delta[1], hover_pos); + } + + ui.memory().id_data.insert( + plot_id, + PlotMemory { + bounds: *transform.bounds(), + }, + ); + let prepared = Prepared { curves, hlines, vlines, - rect, - bounds, show_x, show_y, + transform, }; prepared.ui(ui, &response); } @@ -480,18 +544,44 @@ impl Widget for Plot { } } -struct Prepared { - curves: Vec, - hlines: Vec, - vlines: Vec, - /// Screen space position of the plot +/// Contains the screen rectangle and the plot bounds and provides methods to transform them. +struct ScreenTransform { + /// The screen rectangle. rect: Rect, + /// The plot bounds. bounds: Bounds, - show_x: bool, - show_y: bool, } -impl Prepared { +impl ScreenTransform { + fn rect(&self) -> &Rect { + &self.rect + } + + fn bounds(&self) -> &Bounds { + &self.bounds + } + + fn shift_bounds(&mut self, mut delta_pos: Vec2) { + delta_pos.x *= self.dvalue_dpos()[0] as f32; + delta_pos.y *= self.dvalue_dpos()[1] as f32; + self.bounds.shift(delta_pos); + } + + /// Zoom by a relative amount with the given screen position as center. + fn zoom(&mut self, delta: f32, center: Pos2) { + let delta = delta.clamp(-1., 1.); + let rect_width = self.rect.width(); + let rect_height = self.rect.height(); + let bounds_width = self.bounds.width() as f32; + let bounds_height = self.bounds.height() as f32; + let t_x = (center.x - self.rect.min[0]) / rect_width; + let t_y = (self.rect.max[1] - center.y) / rect_height; + self.bounds.min[0] -= ((t_x * delta) * bounds_width) as f64; + self.bounds.min[1] -= ((t_y * delta) * bounds_height) as f64; + self.bounds.max[0] += (((1. - t_x) * delta) * bounds_width) as f64; + self.bounds.max[1] += (((1. - t_y) * delta) * bounds_height) as f64; + } + fn position_from_value(&self, value: &Value) -> Pos2 { let x = remap( value.x, @@ -506,6 +596,20 @@ impl Prepared { pos2(x as f32, y as f32) } + fn value_from_position(&self, pos: Pos2) -> Value { + let x = remap( + pos.x as f64, + (self.rect.left() as f64)..=(self.rect.right() as f64), + self.bounds.min[0]..=self.bounds.max[0], + ); + let y = remap( + pos.y as f64, + (self.rect.bottom() as f64)..=(self.rect.top() as f64), // negated y axis! + self.bounds.min[1]..=self.bounds.max[1], + ); + Value::new(x, y) + } + /// delta position / delta value fn dpos_dvalue_x(&self) -> f64 { self.rect.width() as f64 / self.bounds.width() @@ -525,23 +629,22 @@ impl Prepared { fn dvalue_dpos(&self) -> [f64; 2] { [1.0 / self.dpos_dvalue_x(), 1.0 / self.dpos_dvalue_y()] } +} - fn value_from_position(&self, pos: Pos2) -> Value { - let x = remap( - pos.x as f64, - (self.rect.left() as f64)..=(self.rect.right() as f64), - self.bounds.min[0]..=self.bounds.max[0], - ); - let y = remap( - pos.y as f64, - (self.rect.bottom() as f64)..=(self.rect.top() as f64), // negated y axis! - self.bounds.min[1]..=self.bounds.max[1], - ); - Value::new(x, y) - } +struct Prepared { + curves: Vec, + hlines: Vec, + vlines: Vec, + show_x: bool, + show_y: bool, + transform: ScreenTransform, +} +impl Prepared { fn ui(&self, ui: &mut Ui, response: &Response) { - let mut shapes = Vec::with_capacity(self.hlines.len() + self.curves.len() + 2); + let Self { transform, .. } = self; + + let mut shapes = Vec::new(); for d in 0..2 { self.paint_axis(ui, d, &mut shapes); @@ -550,8 +653,8 @@ impl Prepared { for &hline in &self.hlines { let HLine { y, stroke } = hline; let points = [ - self.position_from_value(&Value::new(self.bounds.min[0], y)), - self.position_from_value(&Value::new(self.bounds.max[0], y)), + transform.position_from_value(&Value::new(transform.bounds().min[0], y)), + transform.position_from_value(&Value::new(transform.bounds().max[0], y)), ]; shapes.push(Shape::line_segment(points, stroke)); } @@ -559,8 +662,8 @@ impl Prepared { for &vline in &self.vlines { let VLine { x, stroke } = vline; let points = [ - self.position_from_value(&Value::new(x, self.bounds.min[1])), - self.position_from_value(&Value::new(x, self.bounds.max[1])), + transform.position_from_value(&Value::new(x, transform.bounds().min[1])), + transform.position_from_value(&Value::new(x, transform.bounds().max[1])), ]; shapes.push(Shape::line_segment(points, stroke)); } @@ -568,39 +671,41 @@ impl Prepared { for curve in &self.curves { let stroke = curve.stroke; let values = &curve.values; - if values.len() == 1 { - let point = self.position_from_value(&values[0]); - shapes.push(Shape::circle_filled( - point, - stroke.width / 2.0, - stroke.color, - )); - } else if values.len() > 1 { - shapes.push(Shape::line( - values.iter().map(|v| self.position_from_value(v)).collect(), + let shape = if values.len() == 1 { + let point = transform.position_from_value(&values[0]); + Shape::circle_filled(point, stroke.width / 2.0, stroke.color) + } else { + Shape::line( + values + .iter() + .map(|v| transform.position_from_value(v)) + .collect(), stroke, - )); - } + ) + }; + shapes.push(shape); } if let Some(pointer) = response.hover_pos() { self.hover(ui, pointer, &mut shapes); } - ui.painter().sub_region(self.rect).extend(shapes); + ui.painter().sub_region(*transform.rect()).extend(shapes); } fn paint_axis(&self, ui: &Ui, axis: usize, shapes: &mut Vec) { - let bounds = self.bounds; + let Self { transform, .. } = self; + + let bounds = transform.bounds(); let text_style = TextStyle::Body; let base: f64 = 10.0; let min_label_spacing_in_points = 60.0; // TODO: large enough for a wide label - let step_size = self.dvalue_dpos()[axis] * min_label_spacing_in_points; + let step_size = transform.dvalue_dpos()[axis] * min_label_spacing_in_points; let step_size = base.powi(step_size.abs().log(base).ceil() as i32); - let step_size_in_points = (self.dpos_dvalue()[axis] * step_size) as f32; + let step_size_in_points = (transform.dpos_dvalue()[axis] * step_size) as f32; // Where on the cross-dimension to show the label values let value_cross = 0.0_f64.clamp(bounds.min[1 - axis], bounds.max[1 - axis]); @@ -616,7 +721,7 @@ impl Prepared { } else { Value::new(value_cross, value_main) }; - let pos_in_gui = self.position_from_value(&value); + let pos_in_gui = transform.position_from_value(&value); { // Grid: subdivide each label tick in `n` grid lines: @@ -642,8 +747,8 @@ impl Prepared { pos_in_gui[axis] += step_size_in_points * (i as f32) / (n as f32); let mut p0 = pos_in_gui; let mut p1 = pos_in_gui; - p0[1 - axis] = self.rect.min[1 - axis]; - p1[1 - axis] = self.rect.max[1 - axis]; + p0[1 - axis] = transform.rect.min[1 - axis]; + p1[1 - axis] = transform.rect.max[1 - axis]; shapes.push(Shape::line_segment([p0, p1], Stroke::new(1.0, color))); } } @@ -656,8 +761,8 @@ impl Prepared { // Make sure we see the labels, even if the axis is off-screen: text_pos[1 - axis] = text_pos[1 - axis] - .at_most(self.rect.max[1 - axis] - galley.size[1 - axis] - 2.0) - .at_least(self.rect.min[1 - axis] + 1.0); + .at_most(transform.rect.max[1 - axis] - galley.size[1 - axis] - 2.0) + .at_least(transform.rect.min[1 - axis] + 1.0); shapes.push(Shape::Text { pos: text_pos, @@ -669,7 +774,15 @@ impl Prepared { } fn hover(&self, ui: &Ui, pointer: Pos2, shapes: &mut Vec) { - if !self.show_x && !self.show_y { + let Self { + transform, + show_x, + show_y, + curves, + .. + } = self; + + if !show_x && !show_y { return; } @@ -677,9 +790,9 @@ impl Prepared { let mut closest_value = None; let mut closest_curve = None; let mut closest_dist_sq = interact_radius.powi(2); - for curve in &self.curves { + for curve in curves { for value in &curve.values { - let pos = self.position_from_value(value); + let pos = transform.position_from_value(value); let dist_sq = pointer.distance_sq(pos); if dist_sq < closest_dist_sq { closest_dist_sq = dist_sq; @@ -699,24 +812,24 @@ impl Prepared { let line_color = line_color(ui, Strength::Strong); let value = if let Some(value) = closest_value { - let position = self.position_from_value(value); + let position = transform.position_from_value(value); shapes.push(Shape::circle_filled(position, 3.0, line_color)); *value } else { - self.value_from_position(pointer) + transform.value_from_position(pointer) }; - let pointer = self.position_from_value(&value); + let pointer = transform.position_from_value(&value); - let rect = self.rect; + let rect = transform.rect(); - if self.show_x { + if *show_x { // vertical line shapes.push(Shape::line_segment( [pos2(pointer.x, rect.top()), pos2(pointer.x, rect.bottom())], (1.0, line_color), )); } - if self.show_y { + if *show_y { // horizontal line shapes.push(Shape::line_segment( [pos2(rect.left(), pointer.y), pos2(rect.right(), pointer.y)], @@ -725,17 +838,17 @@ impl Prepared { } let text = { - let scale = self.dvalue_dpos(); + let scale = transform.dvalue_dpos(); let x_decimals = ((-scale[0].abs().log10()).ceil().at_least(0.0) as usize).at_most(6); let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).at_most(6); - if self.show_x && self.show_y { + if *show_x && *show_y { format!( "{}x = {:.*}\ny = {:.*}", prefix, x_decimals, value.x, y_decimals, value.y ) - } else if self.show_x { + } else if *show_x { format!("{}x = {:.*}", prefix, x_decimals, value.x) - } else if self.show_y { + } else if *show_y { format!("{}y = {:.*}", prefix, y_decimals, value.y) } else { unreachable!() diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index 10d48841477..3e3906e3892 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -139,10 +139,11 @@ impl super::View for PlotDemo { self.time += ui.input().unstable_dt.at_most(1.0 / 30.0) as f64; }; - let mut plot = Plot::default() + let mut plot = Plot::new("Demo Plot") .curve(self.circle()) .curve(self.sin()) .curve(self.thingy()) + .automatic_bounds(false) .min_size(Vec2::new(256.0, 200.0)); if self.square { plot = plot.view_aspect(1.0); diff --git a/egui_demo_lib/src/apps/demo/widget_gallery.rs b/egui_demo_lib/src/apps/demo/widget_gallery.rs index 661fa4bdd4b..0561657dab8 100644 --- a/egui_demo_lib/src/apps/demo/widget_gallery.rs +++ b/egui_demo_lib/src/apps/demo/widget_gallery.rs @@ -227,7 +227,7 @@ fn example_plot() -> egui::plot::Plot { let x = egui::remap(i as f64, 0.0..=(n as f64), -TAU..=TAU); egui::plot::Value::new(x, x.sin()) })); - egui::plot::Plot::default() + egui::plot::Plot::new("Example Plot") .curve(curve) .height(32.0) .data_aspect(1.0) From a3981830171d76f51f65f723ceed69067f9ac834 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Sun, 18 Apr 2021 18:05:14 +0200 Subject: [PATCH 02/15] update doctest --- egui/src/widgets/plot.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/egui/src/widgets/plot.rs b/egui/src/widgets/plot.rs index f64519f738d..1d795acd397 100644 --- a/egui/src/widgets/plot.rs +++ b/egui/src/widgets/plot.rs @@ -259,7 +259,7 @@ impl Default for PlotMemory { /// }); /// let curve = Curve::from_values_iter(sin); /// ui.add( -/// Plot::default().curve(curve).view_aspect(2.0) +/// Plot::new("Test Plot").curve(curve).view_aspect(2.0) /// ); /// ``` #[derive(Clone, PartialEq)] From 50222798bce50db2f1063628fce4ed25e9d9cdd1 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Sun, 18 Apr 2021 19:00:42 +0200 Subject: [PATCH 03/15] use impl ToString --- egui/src/widgets/plot.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/egui/src/widgets/plot.rs b/egui/src/widgets/plot.rs index 1d795acd397..74ad613a866 100644 --- a/egui/src/widgets/plot.rs +++ b/egui/src/widgets/plot.rs @@ -222,8 +222,8 @@ impl Curve { } /// Name of this curve. - pub fn name(mut self, name: impl Into) -> Self { - self.name = name.into(); + pub fn name(mut self, name: impl ToString) -> Self { + self.name = name.to_string(); self } } @@ -287,9 +287,9 @@ pub struct Plot { } impl Plot { - pub fn new(name: impl Into) -> Self { + pub fn new(name: impl ToString) -> Self { Self { - name: name.into(), + name: name.to_string(), next_auto_color_idx: 0, curves: Default::default(), From a245b9f9ba1666eb287a6c8138a74316c1b7baf4 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Sun, 18 Apr 2021 19:51:41 +0200 Subject: [PATCH 04/15] revert back to Into until #302 is solved --- egui/src/widgets/plot.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/egui/src/widgets/plot.rs b/egui/src/widgets/plot.rs index 74ad613a866..1d795acd397 100644 --- a/egui/src/widgets/plot.rs +++ b/egui/src/widgets/plot.rs @@ -222,8 +222,8 @@ impl Curve { } /// Name of this curve. - pub fn name(mut self, name: impl ToString) -> Self { - self.name = name.to_string(); + pub fn name(mut self, name: impl Into) -> Self { + self.name = name.into(); self } } @@ -287,9 +287,9 @@ pub struct Plot { } impl Plot { - pub fn new(name: impl ToString) -> Self { + pub fn new(name: impl Into) -> Self { Self { - name: name.to_string(), + name: name.into(), next_auto_color_idx: 0, curves: Default::default(), From bbd6d2899349b92c86a4837a53a9f5a1164508e6 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Mon, 19 Apr 2021 07:39:56 +0200 Subject: [PATCH 05/15] Apply suggestions from code review Co-authored-by: ilya sheprut --- egui/src/widgets/plot.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/egui/src/widgets/plot.rs b/egui/src/widgets/plot.rs index 1d795acd397..a5aca5d0a70 100644 --- a/egui/src/widgets/plot.rs +++ b/egui/src/widgets/plot.rs @@ -3,7 +3,6 @@ #![allow(clippy::comparison_chain)] use color::Hsva; -use serde::{Deserialize, Serialize}; use crate::*; @@ -36,7 +35,8 @@ impl Value { /// 2D bounding box of f64 precision. /// The range of data values we show. -#[derive(Clone, Copy, PartialEq, Serialize, Deserialize, Debug)] +#[derive(Clone, Copy, PartialEq, Debug)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] struct Bounds { min: [f64; 2], max: [f64; 2], From 1def4ca48f740fd9a6673427b586d781cef693e9 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Mon, 19 Apr 2021 07:50:40 +0200 Subject: [PATCH 06/15] use persistence feature for PlotMemory --- egui/src/widgets/plot.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/egui/src/widgets/plot.rs b/egui/src/widgets/plot.rs index a5aca5d0a70..e96e37dd68d 100644 --- a/egui/src/widgets/plot.rs +++ b/egui/src/widgets/plot.rs @@ -231,7 +231,8 @@ impl Curve { // ---------------------------------------------------------------------------- /// Information about the plot that has to persist between frames. -#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] +#[derive(Clone)] struct PlotMemory { bounds: Bounds, } From b3002fff391fa39207d2d5139010dcd332cc646b Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Mon, 19 Apr 2021 23:36:47 +0200 Subject: [PATCH 07/15] rename shift -> translate --- egui/src/widgets/plot.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/egui/src/widgets/plot.rs b/egui/src/widgets/plot.rs index e96e37dd68d..56779a0a26d 100644 --- a/egui/src/widgets/plot.rs +++ b/egui/src/widgets/plot.rs @@ -104,19 +104,19 @@ impl Bounds { self.max[1] = self.max[1].max(other.max[1]); } - pub fn shift_x(&mut self, delta: f64) { + pub fn translate_x(&mut self, delta: f64) { self.min[0] += delta; self.max[0] += delta; } - pub fn shift_y(&mut self, delta: f64) { + pub fn translate_y(&mut self, delta: f64) { self.min[1] += delta; self.max[1] += delta; } - pub fn shift(&mut self, delta: Vec2) { - self.shift_x(delta.x as f64); - self.shift_y(delta.y as f64); + pub fn translate(&mut self, delta: Vec2) { + self.translate_x(delta.x as f64); + self.translate_y(delta.y as f64); } pub fn add_relative_margin(&mut self, margin_fraction: Vec2) { @@ -565,7 +565,7 @@ impl ScreenTransform { fn shift_bounds(&mut self, mut delta_pos: Vec2) { delta_pos.x *= self.dvalue_dpos()[0] as f32; delta_pos.y *= self.dvalue_dpos()[1] as f32; - self.bounds.shift(delta_pos); + self.bounds.translate(delta_pos); } /// Zoom by a relative amount with the given screen position as center. From c1dd793c41f74c9da1eb77405856edf50f017fd1 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Tue, 20 Apr 2021 18:47:53 +0200 Subject: [PATCH 08/15] remove automatic bounds --- egui/src/widgets/plot.rs | 30 +++++++++++++++++------- egui_demo_lib/src/apps/demo/plot_demo.rs | 1 - 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/egui/src/widgets/plot.rs b/egui/src/widgets/plot.rs index 56779a0a26d..8ffdd8c9ec2 100644 --- a/egui/src/widgets/plot.rs +++ b/egui/src/widgets/plot.rs @@ -48,6 +48,11 @@ impl Bounds { max: [-f64::INFINITY; 2], }; + pub const EMPTY: Self = Self { + min: [0.0; 2], + max: [0.0; 2], + }; + pub fn new_symmetrical(half_extent: f64) -> Self { Self { min: [-half_extent; 2], @@ -274,7 +279,6 @@ pub struct Plot { symmetrical_x_bounds: bool, symmetrical_y_bounds: bool, - automatic_bounds: bool, margin_fraction: Vec2, min_size: Vec2, @@ -283,6 +287,8 @@ pub struct Plot { data_aspect: Option, view_aspect: Option, + bounds: Bounds, + show_x: bool, show_y: bool, } @@ -299,7 +305,6 @@ impl Plot { symmetrical_x_bounds: false, symmetrical_y_bounds: false, - automatic_bounds: false, margin_fraction: Vec2::splat(0.05), min_size: Vec2::splat(64.0), @@ -308,6 +313,8 @@ impl Plot { data_aspect: None, view_aspect: None, + bounds: Bounds::EMPTY, + show_x: true, show_y: true, } @@ -365,9 +372,16 @@ impl Plot { self } - /// If true, the bounds will be set based on the data. - pub fn automatic_bounds(mut self, enabled: bool) -> Self { - self.automatic_bounds = enabled; + /// Expand bounds to include the given x value. + pub fn include_x(mut self, x: impl Into) -> Self { + self.bounds.extend_with_x(x.into()); + self + } + + /// Expand bounds to include the given y value. + /// For instance, to always show the x axis, call `plot.include_y(0.0)`. + pub fn include_y(mut self, y: impl Into) -> Self { + self.bounds.extend_with_y(y.into()); self } @@ -438,14 +452,14 @@ impl Widget for Plot { view_aspect, show_x, show_y, - automatic_bounds, + bounds, } = self; let plot_id = ui.make_persistent_id(name); let memory = ui .memory() .id_data - .get_or_default::(plot_id) + .get_mut_or_insert_with(plot_id, || PlotMemory { bounds }) .clone(); let PlotMemory { mut bounds } = memory; @@ -473,7 +487,7 @@ impl Widget for Plot { let (rect, response) = ui.allocate_exact_size(size, Sense::drag()); - if automatic_bounds || response.double_clicked_by(PointerButton::Primary) { + if response.double_clicked_by(PointerButton::Primary) || bounds == Bounds::EMPTY { bounds = Bounds::NOTHING; hlines.iter().for_each(|line| bounds.extend_with_y(line.y)); vlines.iter().for_each(|line| bounds.extend_with_x(line.x)); diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index 3e3906e3892..3b17953fade 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -143,7 +143,6 @@ impl super::View for PlotDemo { .curve(self.circle()) .curve(self.sin()) .curve(self.thingy()) - .automatic_bounds(false) .min_size(Vec2::new(256.0, 200.0)); if self.square { plot = plot.view_aspect(1.0); From 5ab7f6bebe634540c575ab31dcf819aab0ed9d38 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Tue, 20 Apr 2021 18:50:31 +0200 Subject: [PATCH 09/15] removed unused methods --- egui/src/widgets/plot.rs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/egui/src/widgets/plot.rs b/egui/src/widgets/plot.rs index 8ffdd8c9ec2..fbd3d82c08f 100644 --- a/egui/src/widgets/plot.rs +++ b/egui/src/widgets/plot.rs @@ -53,13 +53,6 @@ impl Bounds { max: [0.0; 2], }; - pub fn new_symmetrical(half_extent: f64) -> Self { - Self { - min: [-half_extent; 2], - max: [half_extent; 2], - } - } - pub fn width(&self) -> f64 { self.max[0] - self.min[0] } @@ -242,14 +235,6 @@ struct PlotMemory { bounds: Bounds, } -impl Default for PlotMemory { - fn default() -> Self { - Self { - bounds: Bounds::new_symmetrical(1.), - } - } -} - // ---------------------------------------------------------------------------- /// A 2D plot, e.g. a graph of a function. From 80599ab7a2ae1c913c7d1d583cfb29b9c15bd807 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Tue, 20 Apr 2021 19:17:12 +0200 Subject: [PATCH 10/15] Into -> ToString --- egui/src/widgets/plot.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/egui/src/widgets/plot.rs b/egui/src/widgets/plot.rs index fbd3d82c08f..d61ed051fef 100644 --- a/egui/src/widgets/plot.rs +++ b/egui/src/widgets/plot.rs @@ -220,8 +220,8 @@ impl Curve { } /// Name of this curve. - pub fn name(mut self, name: impl Into) -> Self { - self.name = name.into(); + pub fn name(mut self, name: impl ToString) -> Self { + self.name = name.to_string(); self } } @@ -279,9 +279,9 @@ pub struct Plot { } impl Plot { - pub fn new(name: impl Into) -> Self { + pub fn new(name: impl ToString) -> Self { Self { - name: name.into(), + name: name.to_string(), next_auto_color_idx: 0, curves: Default::default(), From 80ed5e19783c2af99c95fb1790916cbf50f593dd Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Wed, 21 Apr 2021 00:05:03 +0200 Subject: [PATCH 11/15] Apply suggestions from code review Co-authored-by: Emil Ernerfeldt --- egui/src/widgets/plot.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/egui/src/widgets/plot.rs b/egui/src/widgets/plot.rs index d61ed051fef..eb834a565ea 100644 --- a/egui/src/widgets/plot.rs +++ b/egui/src/widgets/plot.rs @@ -220,6 +220,7 @@ impl Curve { } /// Name of this curve. + #[allow(clippy::needless_pass_by_value)] pub fn name(mut self, name: impl ToString) -> Self { self.name = name.to_string(); self @@ -279,6 +280,7 @@ pub struct Plot { } impl Plot { + #[allow(clippy::needless_pass_by_value)] pub fn new(name: impl ToString) -> Self { Self { name: name.to_string(), From 1b00e7fb2541787f70ddac834a305100ccc3c6f3 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Wed, 21 Apr 2021 00:07:48 +0200 Subject: [PATCH 12/15] avoid potential invalid bounds bug --- egui/src/widgets/plot.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/egui/src/widgets/plot.rs b/egui/src/widgets/plot.rs index eb834a565ea..61781908dd1 100644 --- a/egui/src/widgets/plot.rs +++ b/egui/src/widgets/plot.rs @@ -68,6 +68,10 @@ impl Bounds { && self.max[1].is_finite() } + pub fn is_valid(&self) -> bool { + self.is_finite() && self.width() > 0.0 && self.height() > 0.0 + } + pub fn extend_with(&mut self, value: &Value) { self.extend_with_x(value.x); self.extend_with_y(value.y); @@ -474,7 +478,7 @@ impl Widget for Plot { let (rect, response) = ui.allocate_exact_size(size, Sense::drag()); - if response.double_clicked_by(PointerButton::Primary) || bounds == Bounds::EMPTY { + if response.double_clicked_by(PointerButton::Primary) || !bounds.is_valid() { bounds = Bounds::NOTHING; hlines.iter().for_each(|line| bounds.extend_with_y(line.y)); vlines.iter().for_each(|line| bounds.extend_with_x(line.x)); @@ -499,10 +503,10 @@ impl Widget for Plot { let rh = rect.height() as f64; let current_data_aspect = (bounds.width() / rw) / (bounds.height() / rh); - let margin = 1e-5; - if current_data_aspect < data_aspect - margin { + let epsilon = 1e-5; + if current_data_aspect < data_aspect - epsilon { bounds.expand_x((data_aspect / current_data_aspect - 1.0) * bounds.width() * 0.5); - } else if current_data_aspect > data_aspect + margin { + } else if current_data_aspect > data_aspect + epsilon { bounds.expand_y((current_data_aspect / data_aspect - 1.0) * bounds.height() * 0.5); } } From 35e11f309f89d8e283ce1586a1f93d75da9e098c Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Wed, 21 Apr 2021 00:08:32 +0200 Subject: [PATCH 13/15] use new is_valid method --- egui/src/widgets/plot.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/egui/src/widgets/plot.rs b/egui/src/widgets/plot.rs index 61781908dd1..15ef1216748 100644 --- a/egui/src/widgets/plot.rs +++ b/egui/src/widgets/plot.rs @@ -519,7 +519,7 @@ impl Widget for Plot { stroke: ui.visuals().window_stroke(), }); - if bounds.is_finite() && bounds.width() > 0.0 && bounds.height() > 0.0 { + if bounds.is_valid() { let mut transform = ScreenTransform { bounds, rect }; if response.dragged_by(PointerButton::Primary) { transform.shift_bounds(-response.drag_delta()); From 2ce32426fe6cf8fb725e7f6d3fde3afa18942865 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Wed, 21 Apr 2021 00:29:01 +0200 Subject: [PATCH 14/15] improve auto bounds behavior as suggested --- egui/src/widgets/plot.rs | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/egui/src/widgets/plot.rs b/egui/src/widgets/plot.rs index 15ef1216748..d243d82645b 100644 --- a/egui/src/widgets/plot.rs +++ b/egui/src/widgets/plot.rs @@ -238,6 +238,7 @@ impl Curve { #[derive(Clone)] struct PlotMemory { bounds: Bounds, + auto_bounds: bool, } // ---------------------------------------------------------------------------- @@ -277,7 +278,7 @@ pub struct Plot { data_aspect: Option, view_aspect: Option, - bounds: Bounds, + min_auto_bounds: Bounds, show_x: bool, show_y: bool, @@ -304,7 +305,7 @@ impl Plot { data_aspect: None, view_aspect: None, - bounds: Bounds::EMPTY, + min_auto_bounds: Bounds::EMPTY, show_x: true, show_y: true, @@ -365,14 +366,14 @@ impl Plot { /// Expand bounds to include the given x value. pub fn include_x(mut self, x: impl Into) -> Self { - self.bounds.extend_with_x(x.into()); + self.min_auto_bounds.extend_with_x(x.into()); self } /// Expand bounds to include the given y value. /// For instance, to always show the x axis, call `plot.include_y(0.0)`. pub fn include_y(mut self, y: impl Into) -> Self { - self.bounds.extend_with_y(y.into()); + self.min_auto_bounds.extend_with_y(y.into()); self } @@ -443,17 +444,23 @@ impl Widget for Plot { view_aspect, show_x, show_y, - bounds, + min_auto_bounds, } = self; let plot_id = ui.make_persistent_id(name); let memory = ui .memory() .id_data - .get_mut_or_insert_with(plot_id, || PlotMemory { bounds }) + .get_mut_or_insert_with(plot_id, || PlotMemory { + bounds: min_auto_bounds, + auto_bounds: true, + }) .clone(); - let PlotMemory { mut bounds } = memory; + let PlotMemory { + mut bounds, + mut auto_bounds, + } = memory; let size = { let width = width.unwrap_or_else(|| { @@ -478,8 +485,10 @@ impl Widget for Plot { let (rect, response) = ui.allocate_exact_size(size, Sense::drag()); - if response.double_clicked_by(PointerButton::Primary) || !bounds.is_valid() { - bounds = Bounds::NOTHING; + auto_bounds |= response.double_clicked_by(PointerButton::Primary); + + if auto_bounds || !bounds.is_valid() { + bounds = min_auto_bounds; hlines.iter().for_each(|line| bounds.extend_with_y(line.y)); vlines.iter().for_each(|line| bounds.extend_with_x(line.x)); curves.iter().for_each(|curve| bounds.merge(&curve.bounds)); @@ -523,15 +532,21 @@ impl Widget for Plot { let mut transform = ScreenTransform { bounds, rect }; if response.dragged_by(PointerButton::Primary) { transform.shift_bounds(-response.drag_delta()); + auto_bounds = false; } if let Some(hover_pos) = response.hover_pos() { - transform.zoom(-0.01 * ui.input().scroll_delta[1], hover_pos); + let scroll_delta = ui.input().scroll_delta[1]; + if scroll_delta != 0. { + transform.zoom(-0.01 * scroll_delta, hover_pos); + auto_bounds = false; + } } ui.memory().id_data.insert( plot_id, PlotMemory { bounds: *transform.bounds(), + auto_bounds, }, ); From 7ba683a9344fb7bd968a51d0de3f6e79666ee260 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Wed, 21 Apr 2021 00:31:45 +0200 Subject: [PATCH 15/15] use NOTHING to initialize min_auto_bounds --- egui/src/widgets/plot.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/egui/src/widgets/plot.rs b/egui/src/widgets/plot.rs index d243d82645b..6ae234557a8 100644 --- a/egui/src/widgets/plot.rs +++ b/egui/src/widgets/plot.rs @@ -48,11 +48,6 @@ impl Bounds { max: [-f64::INFINITY; 2], }; - pub const EMPTY: Self = Self { - min: [0.0; 2], - max: [0.0; 2], - }; - pub fn width(&self) -> f64 { self.max[0] - self.min[0] } @@ -305,7 +300,7 @@ impl Plot { data_aspect: None, view_aspect: None, - min_auto_bounds: Bounds::EMPTY, + min_auto_bounds: Bounds::NOTHING, show_x: true, show_y: true,