From ec3bda0683354361fde563c5c53e9fe9a8e0dde7 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Sun, 18 Apr 2021 17:49:34 +0200 Subject: [PATCH 01/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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 01f0276f369409c2b860d3d307155d8711f7abeb Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Tue, 20 Apr 2021 22:10:15 +0200 Subject: [PATCH 07/24] * split plot into multiple files * add curve from function * move more functionality into ScreenTransform struct --- egui/src/widgets/mod.rs | 2 +- egui/src/widgets/plotting/bounds.rs | 246 ++++++++ egui/src/widgets/plotting/items.rs | 160 ++++++ egui/src/widgets/plotting/mod.rs | 8 + egui/src/widgets/{ => plotting}/plot.rs | 524 ++++-------------- egui_demo_lib/src/apps/demo/plot_demo.rs | 15 +- egui_demo_lib/src/apps/demo/widget_gallery.rs | 8 +- 7 files changed, 545 insertions(+), 418 deletions(-) create mode 100644 egui/src/widgets/plotting/bounds.rs create mode 100644 egui/src/widgets/plotting/items.rs create mode 100644 egui/src/widgets/plotting/mod.rs rename egui/src/widgets/{ => plotting}/plot.rs (56%) diff --git a/egui/src/widgets/mod.rs b/egui/src/widgets/mod.rs index 1594f01b9a4..3371c7e3e02 100644 --- a/egui/src/widgets/mod.rs +++ b/egui/src/widgets/mod.rs @@ -12,7 +12,7 @@ pub(crate) mod drag_value; mod hyperlink; mod image; mod label; -pub mod plot; +pub mod plotting; mod selected_label; mod separator; mod slider; diff --git a/egui/src/widgets/plotting/bounds.rs b/egui/src/widgets/plotting/bounds.rs new file mode 100644 index 00000000000..6a1a849c235 --- /dev/null +++ b/egui/src/widgets/plotting/bounds.rs @@ -0,0 +1,246 @@ +use std::ops::RangeInclusive; + +use super::items::Value; +use crate::*; + +/// 2D bounding box of f64 precision. +/// The range of data values we show. +#[derive(Clone, Copy, PartialEq, Debug)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] +pub(crate) struct Bounds { + pub min: [f64; 2], + pub max: [f64; 2], +} + +impl Bounds { + pub const NOTHING: Self = Self { + min: [f64::INFINITY; 2], + 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], + max: [half_extent; 2], + } + } + + pub fn width(&self) -> f64 { + self.max[0] - self.min[0] + } + + pub fn height(&self) -> f64 { + self.max[1] - self.min[1] + } + + pub fn extend_with(&mut self, value: &Value) { + self.extend_with_x(value.x); + self.extend_with_y(value.y); + } + + /// Expand to include the given x coordinate + pub fn extend_with_x(&mut self, x: f64) { + self.min[0] = self.min[0].min(x); + self.max[0] = self.max[0].max(x); + } + + /// Expand to include the given y coordinate + pub fn extend_with_y(&mut self, y: f64) { + self.min[1] = self.min[1].min(y); + self.max[1] = self.max[1].max(y); + } + + pub fn expand_x(&mut self, pad: f64) { + self.min[0] -= pad; + self.max[0] += pad; + } + + pub fn expand_y(&mut self, pad: f64) { + self.min[1] -= pad; + self.max[1] += pad; + } + + 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 translate_x(&mut self, delta: f64) { + self.min[0] += delta; + self.max[0] += delta; + } + + pub fn translate_y(&mut self, delta: f64) { + self.min[1] += delta; + self.max[1] += delta; + } + + 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) { + 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); + } + + pub fn range_x(&self) -> RangeInclusive { + self.min[0]..=self.max[0] + } + + pub fn _range_y(&self) -> RangeInclusive { + self.min[1]..=self.max[1] + } + + pub fn make_x_symmetrical(&mut self) { + let x_abs = self.min[0].abs().max(self.max[0].abs()); + self.min[0] = -x_abs; + self.max[0] = x_abs; + } + + pub fn make_y_symmetrical(&mut self) { + let y_abs = self.min[1].abs().max(self.max[1].abs()); + self.min[1] = -y_abs; + self.max[1] = y_abs; + } +} + +/// Contains the screen rectangle and the plot bounds and provides methods to transform them. +pub(crate) struct ScreenTransform { + /// The screen rectangle. + frame: Rect, + /// The plot bounds. + bounds: Bounds, + /// Whether to always center the x-range of the bounds. + x_centered: bool, + /// Whether to always center the y-range of the bounds. + y_centered: bool, +} + +impl ScreenTransform { + pub fn new(frame: Rect, bounds: Bounds, x_centered: bool, y_centered: bool) -> Self { + Self { + frame, + bounds, + x_centered, + y_centered, + } + } + + pub fn frame(&self) -> &Rect { + &self.frame + } + + pub fn bounds(&self) -> &Bounds { + &self.bounds + } + + pub fn translate_bounds(&mut self, mut delta_pos: Vec2) { + if self.x_centered { + delta_pos.x = 0.; + } + if self.y_centered { + delta_pos.y = 0.; + } + delta_pos.x *= self.dvalue_dpos()[0] as f32; + delta_pos.y *= self.dvalue_dpos()[1] as f32; + self.bounds.translate(delta_pos); + } + + /// Zoom by a relative amount with the given screen position as center. + pub fn zoom(&mut self, delta: f32, mut center: Pos2) { + if self.x_centered { + center.x = self.frame.center().x as f32; + } + if self.y_centered { + center.y = self.frame.center().y as f32; + } + let delta = delta.clamp(-1., 1.); + let frame_width = self.frame.width(); + let frame_height = self.frame.height(); + let bounds_width = self.bounds.width() as f32; + let bounds_height = self.bounds.height() as f32; + let t_x = (center.x - self.frame.min[0]) / frame_width; + let t_y = (self.frame.max[1] - center.y) / frame_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; + } + + pub fn position_from_value(&self, value: &Value) -> Pos2 { + let x = remap( + value.x, + self.bounds.min[0]..=self.bounds.max[0], + (self.frame.left() as f64)..=(self.frame.right() as f64), + ); + let y = remap( + value.y, + self.bounds.min[1]..=self.bounds.max[1], + (self.frame.bottom() as f64)..=(self.frame.top() as f64), // negated y axis! + ); + pos2(x as f32, y as f32) + } + + pub fn value_from_position(&self, pos: Pos2) -> Value { + let x = remap( + pos.x as f64, + (self.frame.left() as f64)..=(self.frame.right() as f64), + self.bounds.min[0]..=self.bounds.max[0], + ); + let y = remap( + pos.y as f64, + (self.frame.bottom() as f64)..=(self.frame.top() as f64), // negated y axis! + self.bounds.min[1]..=self.bounds.max[1], + ); + Value::new(x, y) + } + + /// delta position / delta value + pub fn dpos_dvalue_x(&self) -> f64 { + self.frame.width() as f64 / self.bounds.width() + } + + /// delta position / delta value + pub fn dpos_dvalue_y(&self) -> f64 { + -self.frame.height() as f64 / self.bounds.height() // negated y axis! + } + + /// delta position / delta value + pub fn dpos_dvalue(&self) -> [f64; 2] { + [self.dpos_dvalue_x(), self.dpos_dvalue_y()] + } + + /// delta value / delta position + pub fn dvalue_dpos(&self) -> [f64; 2] { + [1.0 / self.dpos_dvalue_x(), 1.0 / self.dpos_dvalue_y()] + } + + pub fn get_aspect(&self) -> f64 { + let rw = self.frame.width() as f64; + let rh = self.frame.height() as f64; + (self.bounds.width() / rw) / (self.bounds.height() / rh) + } + + pub fn set_aspect(&mut self, aspect: f64) { + let margin = 1e-5; + let current_aspect = self.get_aspect(); + if current_aspect < aspect - margin { + self.bounds + .expand_x((aspect / current_aspect - 1.0) * self.bounds.width() * 0.5); + } else if current_aspect > aspect + margin { + self.bounds + .expand_y((current_aspect / aspect - 1.0) * self.bounds.height() * 0.5); + } + } +} diff --git a/egui/src/widgets/plotting/items.rs b/egui/src/widgets/plotting/items.rs new file mode 100644 index 00000000000..59a5f430fb9 --- /dev/null +++ b/egui/src/widgets/plotting/items.rs @@ -0,0 +1,160 @@ +use std::ops::RangeInclusive; + +use super::bounds::Bounds; +use crate::*; + +/// A value in the value-space of the plot. +/// +/// Uses f64 for improved accuracy to enable plotting +/// large values (e.g. unix time on x axis). +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Value { + /// This is often something monotonically increasing, such as time, but doesn't have to be. + /// Goes from left to right. + pub x: f64, + /// Goes from bottom to top (inverse of everything else in egui!). + pub y: f64, +} + +impl Value { + #[inline(always)] + pub fn new(x: impl Into, y: impl Into) -> Self { + Self { + x: x.into(), + y: y.into(), + } + } +} + +// ---------------------------------------------------------------------------- + +/// A horizontal line in a plot, filling the full width +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct HLine { + pub(crate) y: f64, + pub(crate) stroke: Stroke, +} + +impl HLine { + pub fn new(y: impl Into, stroke: impl Into) -> Self { + Self { + y: y.into(), + stroke: stroke.into(), + } + } +} + +/// A vertical line in a plot, filling the full width +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct VLine { + pub(crate) x: f64, + pub(crate) stroke: Stroke, +} + +impl VLine { + pub fn new(x: impl Into, stroke: impl Into) -> Self { + Self { + x: x.into(), + stroke: stroke.into(), + } + } +} + +// ---------------------------------------------------------------------------- + +/// A series of values forming a path. +pub struct Curve { + pub(crate) values: Vec, + pub(crate) generator_fn: Option f64>>, + pub(crate) bounds: Bounds, + pub(crate) stroke: Stroke, + pub(crate) name: String, +} + +impl Curve { + pub fn from_values(values: Vec) -> Self { + let mut bounds = Bounds::NOTHING; + for value in &values { + bounds.extend_with(value); + } + Self { + values, + generator_fn: None, + bounds, + stroke: Stroke::new(2.0, Color32::TRANSPARENT), + name: Default::default(), + } + } + + pub fn from_values_iter(iter: impl Iterator) -> Self { + Self::from_values(iter.collect()) + } + + pub fn from_function(function: impl Fn(f64) -> f64 + 'static) -> Self { + Self { + values: Vec::new(), + generator_fn: Some(Box::new(function)), + bounds: Bounds::NOTHING, + stroke: Stroke::new(2.0, Color32::TRANSPARENT), + name: Default::default(), + } + } + + /// Returns true if there are no data points available and there is no function to generate any. + pub(crate) fn no_data(&self) -> bool { + self.generator_fn.is_none() && self.values.is_empty() + } + + /// If initialized with a generator function, this will generate `n` evenly spaced points in the + /// given range. + pub(crate) fn generate_points(&mut self, x_range: RangeInclusive, n: usize) { + if let Some(function) = self.generator_fn.as_ref() { + let increment = (x_range.end() - x_range.start()) / (n - 1) as f64; + + self.values = (0..n) + .map(|i| { + let x = x_range.start() + i as f64 * increment; + Value { x, y: function(x) } + }) + .collect(); + } + } + + /// From a series of y-values. + /// The x-values will be the indices of these values + pub fn from_ys_f32(ys: &[f32]) -> Self { + let values: Vec = ys + .iter() + .enumerate() + .map(|(i, &y)| Value { + x: i as f64, + y: y as f64, + }) + .collect(); + Self::from_values(values) + } + + /// Add a stroke. + pub fn stroke(mut self, stroke: impl Into) -> Self { + self.stroke = stroke.into(); + self + } + + /// Stroke width. A high value means the plot thickens. + pub fn width(mut self, width: f32) -> Self { + self.stroke.width = width; + self + } + + /// Stroke color. Default is `Color32::TRANSPARENT` which means a color will be auto-assigned. + pub fn color(mut self, color: impl Into) -> Self { + self.stroke.color = color.into(); + self + } + + /// Name of this curve. + pub fn name(mut self, name: impl ToString) -> Self { + self.name = name.to_string(); + self + } +} diff --git a/egui/src/widgets/plotting/mod.rs b/egui/src/widgets/plotting/mod.rs new file mode 100644 index 00000000000..93afa25a007 --- /dev/null +++ b/egui/src/widgets/plotting/mod.rs @@ -0,0 +1,8 @@ +//! Simple plotting library. + +mod bounds; +mod items; +mod plot; + +pub use items::{Curve, HLine, VLine, Value}; +pub use plot::Plot; diff --git a/egui/src/widgets/plot.rs b/egui/src/widgets/plotting/plot.rs similarity index 56% rename from egui/src/widgets/plot.rs rename to egui/src/widgets/plotting/plot.rs index e96e37dd68d..3944335bec9 100644 --- a/egui/src/widgets/plot.rs +++ b/egui/src/widgets/plotting/plot.rs @@ -1,232 +1,8 @@ -//! Simple plotting library. - -#![allow(clippy::comparison_chain)] - -use color::Hsva; +use super::bounds::{Bounds, ScreenTransform}; +use super::items::{Curve, HLine, VLine, Value}; use crate::*; - -// ---------------------------------------------------------------------------- - -/// A value in the value-space of the plot. -/// -/// Uses f64 for improved accuracy to enable plotting -/// large values (e.g. unix time on x axis). -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct Value { - /// This is often something monotonically increasing, such as time, but doesn't have to be. - /// Goes from left to right. - pub x: f64, - /// Goes from bottom to top (inverse of everything else in egui!). - pub y: f64, -} - -impl Value { - #[inline(always)] - pub fn new(x: impl Into, y: impl Into) -> Self { - Self { - x: x.into(), - y: y.into(), - } - } -} - -// ---------------------------------------------------------------------------- - -/// 2D bounding box of f64 precision. -/// The range of data values we show. -#[derive(Clone, Copy, PartialEq, Debug)] -#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] -struct Bounds { - min: [f64; 2], - max: [f64; 2], -} - -impl Bounds { - pub const NOTHING: Self = Self { - min: [f64::INFINITY; 2], - 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] - } - - pub fn height(&self) -> f64 { - self.max[1] - self.min[1] - } - - pub fn is_finite(&self) -> bool { - self.min[0].is_finite() - && self.min[1].is_finite() - && self.max[0].is_finite() - && self.max[1].is_finite() - } - - pub fn extend_with(&mut self, value: &Value) { - self.extend_with_x(value.x); - self.extend_with_y(value.y); - } - - /// Expand to include the given x coordinate - pub fn extend_with_x(&mut self, x: f64) { - self.min[0] = self.min[0].min(x); - self.max[0] = self.max[0].max(x); - } - - /// Expand to include the given y coordinate - pub fn extend_with_y(&mut self, y: f64) { - self.min[1] = self.min[1].min(y); - self.max[1] = self.max[1].max(y); - } - - pub fn expand_x(&mut self, pad: f64) { - self.min[0] -= pad; - self.max[0] += pad; - } - - pub fn expand_y(&mut self, pad: f64) { - self.min[1] -= pad; - self.max[1] += pad; - } - - 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); - } -} - -// ---------------------------------------------------------------------------- - -/// A horizontal line in a plot, filling the full width -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct HLine { - y: f64, - stroke: Stroke, -} - -impl HLine { - pub fn new(y: impl Into, stroke: impl Into) -> Self { - Self { - y: y.into(), - stroke: stroke.into(), - } - } -} - -/// A vertical line in a plot, filling the full width -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct VLine { - x: f64, - stroke: Stroke, -} - -impl VLine { - pub fn new(x: impl Into, stroke: impl Into) -> Self { - Self { - x: x.into(), - stroke: stroke.into(), - } - } -} - -// ---------------------------------------------------------------------------- - -/// A series of values forming a path. -#[derive(Clone, PartialEq)] -pub struct Curve { - values: Vec, - bounds: Bounds, - stroke: Stroke, - name: String, -} - -impl Curve { - pub fn from_values(values: Vec) -> Self { - let mut bounds = Bounds::NOTHING; - for value in &values { - bounds.extend_with(value); - } - Self { - values, - bounds, - stroke: Stroke::new(2.0, Color32::TRANSPARENT), - name: Default::default(), - } - } - - pub fn from_values_iter(iter: impl Iterator) -> Self { - Self::from_values(iter.collect()) - } - - /// From a series of y-values. - /// The x-values will be the indices of these values - pub fn from_ys_f32(ys: &[f32]) -> Self { - let values: Vec = ys - .iter() - .enumerate() - .map(|(i, &y)| Value { - x: i as f64, - y: y as f64, - }) - .collect(); - Self::from_values(values) - } - - pub fn stroke(mut self, stroke: impl Into) -> Self { - self.stroke = stroke.into(); - self - } - - /// Stroke width. A high value means the plot thickens. - pub fn width(mut self, width: f32) -> Self { - self.stroke.width = width; - self - } - - /// Stroke color. Default is `Color32::TRANSPARENT` which means a color will be auto-assigned. - pub fn color(mut self, color: impl Into) -> Self { - self.stroke.color = color.into(); - self - } - - /// Name of this curve. - pub fn name(mut self, name: impl Into) -> Self { - self.name = name.into(); - self - } -} +use color::Hsva; // ---------------------------------------------------------------------------- @@ -253,7 +29,7 @@ impl Default for PlotMemory { /// /// ``` /// # let ui = &mut egui::Ui::__test(); -/// use egui::plot::{Curve, Plot, Value}; +/// use egui::plotting::{Curve, Plot, Value}; /// let sin = (0..1000).map(|i| { /// let x = i as f64 * 0.01; /// Value::new(x, x.sin()) @@ -263,7 +39,6 @@ impl Default for PlotMemory { /// Plot::new("Test Plot").curve(curve).view_aspect(2.0) /// ); /// ``` -#[derive(Clone, PartialEq)] pub struct Plot { name: String, next_auto_color_idx: usize, @@ -272,9 +47,11 @@ pub struct Plot { hlines: Vec, vlines: Vec, - symmetrical_x_bounds: bool, - symmetrical_y_bounds: bool, - automatic_bounds: bool, + center_x_axis: bool, + center_y_axis: bool, + allow_zoom: bool, + allow_drag: bool, + bounds: Bounds, margin_fraction: Vec2, min_size: Vec2, @@ -288,18 +65,20 @@ 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(), hlines: Default::default(), vlines: Default::default(), - symmetrical_x_bounds: false, - symmetrical_y_bounds: false, - automatic_bounds: false, + center_x_axis: false, + center_y_axis: false, + allow_zoom: true, + allow_drag: true, + bounds: Bounds::EMPTY, margin_fraction: Vec2::splat(0.05), min_size: Vec2::splat(64.0), @@ -326,7 +105,7 @@ impl Plot { /// Add a data curve. /// You can add multiple curves. pub fn curve(mut self, mut curve: Curve) -> Self { - if !curve.values.is_empty() { + if !curve.no_data() { self.auto_color(&mut curve.stroke.color); self.curves.push(curve); } @@ -351,26 +130,6 @@ impl Plot { 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 { - self.symmetrical_x_bounds = symmetrical_x_bounds; - self - } - - /// If true, the y-bounds will be symmetrical, so that the y=0 zero line - /// is always in the center. - pub fn symmetrical_y_bounds(mut self, symmetrical_y_bounds: bool) -> Self { - self.symmetrical_y_bounds = symmetrical_y_bounds; - 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 { @@ -418,6 +177,44 @@ impl Plot { self.show_y = show_y; self } + + /// Always keep the x-axis centered. + pub fn center_x_axis(mut self, on: bool) -> Self { + self.center_x_axis = on; + self + } + + /// Always keep the y-axis centered. + pub fn center_y_axis(mut self, on: bool) -> Self { + self.center_y_axis = on; + self + } + + /// Whether to allow zooming in the plot. + pub fn allow_zoom(mut self, on: bool) -> Self { + self.allow_zoom = on; + self + } + + /// Whether to allow dragging in the plot to move the bounds. + pub fn allow_drag(mut self, on: bool) -> Self { + self.allow_drag = on; + self + } + + /// Expand bounds to include the given x value. + /// For instance, to always show the y axis, call `plot.include_x(0.0)`. + 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 + } } impl Widget for Plot { @@ -425,11 +222,14 @@ impl Widget for Plot { let Self { name, next_auto_color_idx: _, - curves, + mut curves, hlines, vlines, - symmetrical_x_bounds, - symmetrical_y_bounds, + center_x_axis, + center_y_axis, + allow_zoom, + allow_drag, + bounds, margin_fraction, width, height, @@ -438,14 +238,13 @@ impl Widget for Plot { view_aspect, show_x, show_y, - automatic_bounds, } = self; let plot_id = ui.make_persistent_id(name); let memory = ui .memory() .id_data - .get_or_default::(plot_id) + .get_or_insert_with(plot_id, || PlotMemory { bounds }) .clone(); let PlotMemory { mut bounds } = memory; @@ -473,162 +272,79 @@ impl Widget for Plot { let (rect, response) = ui.allocate_exact_size(size, Sense::drag()); - if automatic_bounds || response.double_clicked_by(PointerButton::Primary) { + // Background: + ui.painter().add(Shape::Rect { + rect, + corner_radius: 2.0, + fill: ui.visuals().extreme_bg_color, + stroke: ui.visuals().window_stroke(), + }); + + // Set bounds automatically based on content. + if bounds == Bounds::EMPTY || 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); } + // Make sure they are not empty. + if bounds == Bounds::NOTHING { + bounds = Bounds::new_symmetrical(0.5); + } - if symmetrical_x_bounds { - let x_abs = bounds.min[0].abs().max(bounds.max[0].abs()); - bounds.min[0] = -x_abs; - bounds.max[0] = x_abs; + // Scale axes so that the origin is in the center. + if center_x_axis { + bounds.make_x_symmetrical(); }; - if symmetrical_y_bounds { - let y_abs = bounds.min[1].abs().max(bounds.max[1].abs()); - bounds.min[1] = -y_abs; - bounds.max[1] = y_abs; + if center_y_axis { + bounds.make_y_symmetrical() }; + let mut transform = ScreenTransform::new(rect, bounds, center_x_axis, center_y_axis); + + // Enforce equal aspect ratio. 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); - - 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 if current_data_aspect > data_aspect + margin { - bounds.expand_y((current_data_aspect / data_aspect - 1.0) * bounds.height() * 0.5); - } + transform.set_aspect(data_aspect as f64); } - // Background: - ui.painter().add(Shape::Rect { - rect, - corner_radius: 2.0, - fill: ui.visuals().extreme_bg_color, - stroke: ui.visuals().window_stroke(), - }); + // Dragging + if allow_drag && response.dragged_by(PointerButton::Primary) { + let drag_delta = response.drag_delta(); + transform.translate_bounds(-drag_delta); + } - 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()); - } + // Zooming + if allow_zoom { if let Some(hover_pos) = response.hover_pos() { - transform.zoom(-0.01 * ui.input().scroll_delta[1], hover_pos); + let zoom_center = hover_pos; + transform.zoom(-0.01 * ui.input().scroll_delta[1], zoom_center); } - - ui.memory().id_data.insert( - plot_id, - PlotMemory { - bounds: *transform.bounds(), - }, - ); - - let prepared = Prepared { - curves, - hlines, - vlines, - show_x, - show_y, - transform, - }; - prepared.ui(ui, &response); } - response.on_hover_cursor(CursorIcon::Crosshair) - } -} - -/// 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, -} + // Initialize values from functions. + curves + .iter_mut() + .for_each(|curve| curve.generate_points(transform.bounds().range_x(), 100)); -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, - self.bounds.min[0]..=self.bounds.max[0], - (self.rect.left() as f64)..=(self.rect.right() as f64), + ui.memory().id_data.insert( + plot_id, + PlotMemory { + bounds: *transform.bounds(), + }, ); - let y = remap( - value.y, - self.bounds.min[1]..=self.bounds.max[1], - (self.rect.bottom() as f64)..=(self.rect.top() as f64), // negated y axis! - ); - 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() - } - /// delta position / delta value - fn dpos_dvalue_y(&self) -> f64 { - -self.rect.height() as f64 / self.bounds.height() // negated y axis! - } - - /// delta position / delta value - fn dpos_dvalue(&self) -> [f64; 2] { - [self.dpos_dvalue_x(), self.dpos_dvalue_y()] - } + let prepared = Prepared { + curves, + hlines, + vlines, + show_x, + show_y, + transform, + }; + prepared.ui(ui, &response); - /// delta value / delta position - fn dvalue_dpos(&self) -> [f64; 2] { - [1.0 / self.dpos_dvalue_x(), 1.0 / self.dpos_dvalue_y()] + response.on_hover_cursor(CursorIcon::Crosshair) } } @@ -691,7 +407,7 @@ impl Prepared { self.hover(ui, pointer, &mut shapes); } - ui.painter().sub_region(*transform.rect()).extend(shapes); + ui.painter().sub_region(*transform.frame()).extend(shapes); } fn paint_axis(&self, ui: &Ui, axis: usize, shapes: &mut Vec) { @@ -748,8 +464,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] = transform.rect.min[1 - axis]; - p1[1 - axis] = transform.rect.max[1 - axis]; + p0[1 - axis] = transform.frame().min[1 - axis]; + p1[1 - axis] = transform.frame().max[1 - axis]; shapes.push(Shape::line_segment([p0, p1], Stroke::new(1.0, color))); } } @@ -762,8 +478,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(transform.rect.max[1 - axis] - galley.size[1 - axis] - 2.0) - .at_least(transform.rect.min[1 - axis] + 1.0); + .at_most(transform.frame().max[1 - axis] - galley.size[1 - axis] - 2.0) + .at_least(transform.frame().min[1 - axis] + 1.0); shapes.push(Shape::Text { pos: text_pos, @@ -810,7 +526,7 @@ impl Prepared { } } - let line_color = line_color(ui, Strength::Strong); + let line_color = line_color(ui, Strength::Middle); let value = if let Some(value) = closest_value { let position = transform.position_from_value(value); @@ -821,7 +537,7 @@ impl Prepared { }; let pointer = transform.position_from_value(&value); - let rect = transform.rect(); + let rect = transform.frame(); if *show_x { // vertical line diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index 3e3906e3892..ce06f323b42 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -1,4 +1,4 @@ -use egui::plot::{Curve, Plot, Value}; +use egui::plotting::{Curve, Plot, Value}; use egui::*; use std::f64::consts::TAU; @@ -108,14 +108,10 @@ impl PlotDemo { } fn sin(&self) -> Curve { - let n = 512; - let circle = (0..=n).map(|i| { - let t = remap(i as f64, 0.0..=(n as f64), -TAU..=TAU); - Value::new(t / 5.0, 0.5 * (self.time + t).sin()) - }); - Curve::from_values_iter(circle) + let t = self.time; + Curve::from_function(move |x| 0.5 * (2.0 * x).sin() * t.sin()) .color(Color32::from_rgb(200, 100, 100)) - .name("0.5 * sin(x / 5)") + .name("0.5 * sin(2x) * sin(t)") } fn thingy(&self) -> Curve { @@ -143,7 +139,8 @@ impl super::View for PlotDemo { .curve(self.circle()) .curve(self.sin()) .curve(self.thingy()) - .automatic_bounds(false) + .center_x_axis(true) + .center_y_axis(true) .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 0561657dab8..21ca6d578cb 100644 --- a/egui_demo_lib/src/apps/demo/widget_gallery.rs +++ b/egui_demo_lib/src/apps/demo/widget_gallery.rs @@ -220,14 +220,14 @@ impl WidgetGallery { } } -fn example_plot() -> egui::plot::Plot { +fn example_plot() -> egui::plotting::Plot { let n = 128; - let curve = egui::plot::Curve::from_values_iter((0..=n).map(|i| { + let curve = egui::plotting::Curve::from_values_iter((0..=n).map(|i| { use std::f64::consts::TAU; let x = egui::remap(i as f64, 0.0..=(n as f64), -TAU..=TAU); - egui::plot::Value::new(x, x.sin()) + egui::plotting::Value::new(x, x.sin()) })); - egui::plot::Plot::new("Example Plot") + egui::plotting::Plot::new("Example Plot") .curve(curve) .height(32.0) .data_aspect(1.0) From 12a45c9dc680c3f1818e614e6b08527312f90d4e Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Wed, 21 Apr 2021 18:38:17 +0200 Subject: [PATCH 08/24] changes from code review in base branch --- egui/src/widgets/plotting/bounds.rs | 26 +++++++----- egui/src/widgets/plotting/items.rs | 1 + egui/src/widgets/plotting/plot.rs | 54 +++++++++++++----------- egui_demo_lib/src/apps/demo/plot_demo.rs | 4 +- 4 files changed, 49 insertions(+), 36 deletions(-) diff --git a/egui/src/widgets/plotting/bounds.rs b/egui/src/widgets/plotting/bounds.rs index 6a1a849c235..841d54beb33 100644 --- a/egui/src/widgets/plotting/bounds.rs +++ b/egui/src/widgets/plotting/bounds.rs @@ -18,11 +18,6 @@ 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], @@ -30,6 +25,17 @@ impl Bounds { } } + pub fn is_finite(&self) -> bool { + self.min[0].is_finite() + && self.min[1].is_finite() + && self.max[0].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 width(&self) -> f64 { self.max[0] - self.min[0] } @@ -88,8 +94,8 @@ impl Bounds { } pub fn add_relative_margin(&mut self, margin_fraction: Vec2) { - let width = self.width(); - let height = self.height(); + let width = self.width().max(0.0); + let height = self.height().max(0.0); self.expand_x(margin_fraction.x as f64 * width); self.expand_y(margin_fraction.y as f64 * height); } @@ -233,12 +239,12 @@ impl ScreenTransform { } pub fn set_aspect(&mut self, aspect: f64) { - let margin = 1e-5; + let epsilon = 1e-5; let current_aspect = self.get_aspect(); - if current_aspect < aspect - margin { + if current_aspect < aspect - epsilon { self.bounds .expand_x((aspect / current_aspect - 1.0) * self.bounds.width() * 0.5); - } else if current_aspect > aspect + margin { + } else if current_aspect > aspect + epsilon { self.bounds .expand_y((current_aspect / aspect - 1.0) * self.bounds.height() * 0.5); } diff --git a/egui/src/widgets/plotting/items.rs b/egui/src/widgets/plotting/items.rs index 59a5f430fb9..434e7d67b62 100644 --- a/egui/src/widgets/plotting/items.rs +++ b/egui/src/widgets/plotting/items.rs @@ -153,6 +153,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 diff --git a/egui/src/widgets/plotting/plot.rs b/egui/src/widgets/plotting/plot.rs index 3944335bec9..ef1896e50cc 100644 --- a/egui/src/widgets/plotting/plot.rs +++ b/egui/src/widgets/plotting/plot.rs @@ -11,14 +11,7 @@ use color::Hsva; #[derive(Clone)] struct PlotMemory { bounds: Bounds, -} - -impl Default for PlotMemory { - fn default() -> Self { - Self { - bounds: Bounds::new_symmetrical(1.), - } - } + auto_bounds: bool, } // ---------------------------------------------------------------------------- @@ -51,7 +44,7 @@ pub struct Plot { center_y_axis: bool, allow_zoom: bool, allow_drag: bool, - bounds: Bounds, + min_auto_bounds: Bounds, margin_fraction: Vec2, min_size: Vec2, @@ -65,6 +58,7 @@ pub struct Plot { } impl Plot { + #[allow(clippy::needless_pass_by_value)] pub fn new(name: impl ToString) -> Self { Self { name: name.to_string(), @@ -78,7 +72,7 @@ impl Plot { center_y_axis: false, allow_zoom: true, allow_drag: true, - bounds: Bounds::EMPTY, + min_auto_bounds: Bounds::NOTHING, margin_fraction: Vec2::splat(0.05), min_size: Vec2::splat(64.0), @@ -205,14 +199,14 @@ impl Plot { /// Expand bounds to include the given x value. /// For instance, to always show the y axis, call `plot.include_x(0.0)`. 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 } } @@ -229,7 +223,7 @@ impl Widget for Plot { center_y_axis, allow_zoom, allow_drag, - bounds, + min_auto_bounds, margin_fraction, width, height, @@ -244,10 +238,16 @@ impl Widget for Plot { let memory = ui .memory() .id_data - .get_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(|| { @@ -272,7 +272,7 @@ impl Widget for Plot { let (rect, response) = ui.allocate_exact_size(size, Sense::drag()); - // Background: + // Background ui.painter().add(Shape::Rect { rect, corner_radius: 2.0, @@ -280,17 +280,19 @@ impl Widget for Plot { stroke: ui.visuals().window_stroke(), }); + auto_bounds |= response.double_clicked_by(PointerButton::Primary); + // Set bounds automatically based on content. - if bounds == Bounds::EMPTY || response.double_clicked_by(PointerButton::Primary) { - bounds = Bounds::NOTHING; + 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)); bounds.add_relative_margin(margin_fraction); } // Make sure they are not empty. - if bounds == Bounds::NOTHING { - bounds = Bounds::new_symmetrical(0.5); + if !bounds.is_valid() { + bounds = Bounds::new_symmetrical(1.0); } // Scale axes so that the origin is in the center. @@ -310,15 +312,18 @@ impl Widget for Plot { // Dragging if allow_drag && response.dragged_by(PointerButton::Primary) { - let drag_delta = response.drag_delta(); - transform.translate_bounds(-drag_delta); + transform.translate_bounds(-response.drag_delta()); + auto_bounds = false; } // Zooming if allow_zoom { if let Some(hover_pos) = response.hover_pos() { - let zoom_center = hover_pos; - transform.zoom(-0.01 * ui.input().scroll_delta[1], zoom_center); + let scroll_delta = ui.input().scroll_delta[1]; + if scroll_delta != 0. { + transform.zoom(-0.01 * scroll_delta, hover_pos); + auto_bounds = false; + } } } @@ -331,6 +336,7 @@ impl Widget for Plot { plot_id, PlotMemory { bounds: *transform.bounds(), + auto_bounds, }, ); diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index ce06f323b42..ef33c6b2b91 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -139,8 +139,8 @@ impl super::View for PlotDemo { .curve(self.circle()) .curve(self.sin()) .curve(self.thingy()) - .center_x_axis(true) - .center_y_axis(true) + .center_x_axis(false) + .center_y_axis(false) .min_size(Vec2::new(256.0, 200.0)); if self.square { plot = plot.view_aspect(1.0); From d4b1094a3bb4f05d2be9c524c575aee6356df9f9 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Wed, 21 Apr 2021 19:53:23 +0200 Subject: [PATCH 09/24] let user specify a range for generated functions --- egui/src/widgets/plotting/items.rs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/egui/src/widgets/plotting/items.rs b/egui/src/widgets/plotting/items.rs index 434e7d67b62..52084c77dfa 100644 --- a/egui/src/widgets/plotting/items.rs +++ b/egui/src/widgets/plotting/items.rs @@ -1,6 +1,8 @@ +//! Contains items that can be added to a plot. + use std::ops::RangeInclusive; -use super::bounds::Bounds; +use super::transform::Bounds; use crate::*; /// A value in the value-space of the plot. @@ -65,7 +67,7 @@ impl VLine { /// A series of values forming a path. pub struct Curve { pub(crate) values: Vec, - pub(crate) generator_fn: Option f64>>, + pub(crate) generator_fn: Option<(Box f64>, Option>)>, pub(crate) bounds: Bounds, pub(crate) stroke: Stroke, pub(crate) name: String, @@ -90,10 +92,13 @@ impl Curve { Self::from_values(iter.collect()) } - pub fn from_function(function: impl Fn(f64) -> f64 + 'static) -> Self { + pub fn from_y_function( + function: impl Fn(f64) -> f64 + 'static, + range: Option>, + ) -> Self { Self { values: Vec::new(), - generator_fn: Some(Box::new(function)), + generator_fn: Some((Box::new(function), range)), bounds: Bounds::NOTHING, stroke: Stroke::new(2.0, Color32::TRANSPARENT), name: Default::default(), @@ -107,8 +112,12 @@ impl Curve { /// If initialized with a generator function, this will generate `n` evenly spaced points in the /// given range. - pub(crate) fn generate_points(&mut self, x_range: RangeInclusive, n: usize) { - if let Some(function) = self.generator_fn.as_ref() { + pub(crate) fn generate_points(&mut self, mut x_range: RangeInclusive, n: usize) { + if let Some((function, function_range)) = self.generator_fn.as_ref() { + if let Some(range) = function_range { + x_range = x_range.start().max(*range.start())..=x_range.end().min(*range.end()); + } + let increment = (x_range.end() - x_range.start()) / (n - 1) as f64; self.values = (0..n) From e539c89039941c5d7d234438b742033452a6d106 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Wed, 21 Apr 2021 19:53:28 +0200 Subject: [PATCH 10/24] rename file --- egui/src/widgets/plotting/{bounds.rs => transform.rs} | 4 ---- 1 file changed, 4 deletions(-) rename egui/src/widgets/plotting/{bounds.rs => transform.rs} (98%) diff --git a/egui/src/widgets/plotting/bounds.rs b/egui/src/widgets/plotting/transform.rs similarity index 98% rename from egui/src/widgets/plotting/bounds.rs rename to egui/src/widgets/plotting/transform.rs index 841d54beb33..f57f6d81eb9 100644 --- a/egui/src/widgets/plotting/bounds.rs +++ b/egui/src/widgets/plotting/transform.rs @@ -104,10 +104,6 @@ impl Bounds { self.min[0]..=self.max[0] } - pub fn _range_y(&self) -> RangeInclusive { - self.min[1]..=self.max[1] - } - pub fn make_x_symmetrical(&mut self) { let x_abs = self.min[0].abs().max(self.max[0].abs()); self.min[0] = -x_abs; From 23bd667331f740abc6ba86047afd51bbc3416f25 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Wed, 21 Apr 2021 19:53:40 +0200 Subject: [PATCH 11/24] minor changes --- egui/src/widgets/plotting/mod.rs | 2 +- egui/src/widgets/plotting/plot.rs | 3 ++- egui_demo_lib/src/apps/demo/plot_demo.rs | 6 +++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/egui/src/widgets/plotting/mod.rs b/egui/src/widgets/plotting/mod.rs index 93afa25a007..a0db334bc92 100644 --- a/egui/src/widgets/plotting/mod.rs +++ b/egui/src/widgets/plotting/mod.rs @@ -1,8 +1,8 @@ //! Simple plotting library. -mod bounds; mod items; mod plot; +mod transform; pub use items::{Curve, HLine, VLine, Value}; pub use plot::Plot; diff --git a/egui/src/widgets/plotting/plot.rs b/egui/src/widgets/plotting/plot.rs index ef1896e50cc..dcaf11c1e10 100644 --- a/egui/src/widgets/plotting/plot.rs +++ b/egui/src/widgets/plotting/plot.rs @@ -1,5 +1,5 @@ -use super::bounds::{Bounds, ScreenTransform}; use super::items::{Curve, HLine, VLine, Value}; +use super::transform::{Bounds, ScreenTransform}; use crate::*; use color::Hsva; @@ -328,6 +328,7 @@ impl Widget for Plot { } // Initialize values from functions. + // TODO: Let the user pick a resolution? curves .iter_mut() .for_each(|curve| curve.generate_points(transform.bounds().range_x(), 100)); diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index ef33c6b2b91..da477bfef84 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -6,7 +6,7 @@ use std::f64::consts::TAU; pub struct PlotDemo { animate: bool, time: f64, - circle_radius: f32, + circle_radius: f64, circle_center: Pos2, square: bool, proportional: bool, @@ -96,7 +96,7 @@ impl PlotDemo { let n = 512; let circle = (0..=n).map(|i| { let t = remap(i as f64, 0.0..=(n as f64), 0.0..=TAU); - let r = self.circle_radius as f64; + let r = self.circle_radius; Value::new( r * t.cos() + self.circle_center.x as f64, r * t.sin() + self.circle_center.y as f64, @@ -109,7 +109,7 @@ impl PlotDemo { fn sin(&self) -> Curve { let t = self.time; - Curve::from_function(move |x| 0.5 * (2.0 * x).sin() * t.sin()) + Curve::from_y_function(move |x| 0.5 * (2.0 * x).sin() * t.sin(), Some((-2.)..=2.)) .color(Color32::from_rgb(200, 100, 100)) .name("0.5 * sin(2x) * sin(t)") } From dbac21c909741ce277cd054ee9dc84e6749c6774 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Wed, 21 Apr 2021 20:45:49 +0200 Subject: [PATCH 12/24] improve generator functionality --- egui/src/widgets/plotting/items.rs | 48 +++++++++++++++++------- egui_demo_lib/src/apps/demo/plot_demo.rs | 2 +- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/egui/src/widgets/plotting/items.rs b/egui/src/widgets/plotting/items.rs index 52084c77dfa..60feeddab2f 100644 --- a/egui/src/widgets/plotting/items.rs +++ b/egui/src/widgets/plotting/items.rs @@ -64,16 +64,34 @@ impl VLine { // ---------------------------------------------------------------------------- +/// Contains a function to generate plot points and an optional range for which to generate them. +pub struct ExplicitGenerator { + function: Box f64>, + range: Option>, +} + +// ---------------------------------------------------------------------------- + /// A series of values forming a path. pub struct Curve { pub(crate) values: Vec, - pub(crate) generator_fn: Option<(Box f64>, Option>)>, + pub(crate) generator: Option, pub(crate) bounds: Bounds, pub(crate) stroke: Stroke, pub(crate) name: String, } impl Curve { + fn empty() -> Self { + Self { + values: Vec::new(), + generator: None, + bounds: Bounds::NOTHING, + stroke: Stroke::new(2.0, Color32::TRANSPARENT), + name: Default::default(), + } + } + pub fn from_values(values: Vec) -> Self { let mut bounds = Bounds::NOTHING; for value in &values { @@ -81,10 +99,8 @@ impl Curve { } Self { values, - generator_fn: None, bounds, - stroke: Stroke::new(2.0, Color32::TRANSPARENT), - name: Default::default(), + ..Self::empty() } } @@ -92,29 +108,30 @@ impl Curve { Self::from_values(iter.collect()) } - pub fn from_y_function( + pub fn from_explicit_callback( function: impl Fn(f64) -> f64 + 'static, range: Option>, ) -> Self { + let generator = ExplicitGenerator { + function: Box::new(function), + range, + }; Self { - values: Vec::new(), - generator_fn: Some((Box::new(function), range)), - bounds: Bounds::NOTHING, - stroke: Stroke::new(2.0, Color32::TRANSPARENT), - name: Default::default(), + generator: Some(generator), + ..Self::empty() } } /// Returns true if there are no data points available and there is no function to generate any. pub(crate) fn no_data(&self) -> bool { - self.generator_fn.is_none() && self.values.is_empty() + self.generator.is_none() && self.values.is_empty() } /// If initialized with a generator function, this will generate `n` evenly spaced points in the /// given range. pub(crate) fn generate_points(&mut self, mut x_range: RangeInclusive, n: usize) { - if let Some((function, function_range)) = self.generator_fn.as_ref() { - if let Some(range) = function_range { + if let Some(function) = self.generator.as_ref() { + if let Some(range) = &function.range { x_range = x_range.start().max(*range.start())..=x_range.end().min(*range.end()); } @@ -123,7 +140,10 @@ impl Curve { self.values = (0..n) .map(|i| { let x = x_range.start() + i as f64 * increment; - Value { x, y: function(x) } + Value { + x, + y: (function.function)(x), + } }) .collect(); } diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index da477bfef84..be6d437d6a4 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -109,7 +109,7 @@ impl PlotDemo { fn sin(&self) -> Curve { let t = self.time; - Curve::from_y_function(move |x| 0.5 * (2.0 * x).sin() * t.sin(), Some((-2.)..=2.)) + Curve::from_explicit_callback(move |x| 0.5 * (2.0 * x).sin() * t.sin(), Some((-2.)..=2.)) .color(Color32::from_rgb(200, 100, 100)) .name("0.5 * sin(2x) * sin(t)") } From 0f053755386e30f8c2c5388878e4d55084e0c9b1 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Wed, 21 Apr 2021 21:44:40 +0200 Subject: [PATCH 13/24] improve callback and add parametric callback --- egui/src/widgets/plotting/items.rs | 72 ++++++++++++++++-------- egui/src/widgets/plotting/plot.rs | 2 +- egui_demo_lib/src/apps/demo/plot_demo.rs | 26 +++++---- 3 files changed, 63 insertions(+), 37 deletions(-) diff --git a/egui/src/widgets/plotting/items.rs b/egui/src/widgets/plotting/items.rs index 60feeddab2f..eb867c4b0c9 100644 --- a/egui/src/widgets/plotting/items.rs +++ b/egui/src/widgets/plotting/items.rs @@ -64,10 +64,11 @@ impl VLine { // ---------------------------------------------------------------------------- -/// Contains a function to generate plot points and an optional range for which to generate them. -pub struct ExplicitGenerator { - function: Box f64>, - range: Option>, +enum Generator { + /// Describes a function y = f(x) with an optional range for x and a number of points. + Explicit(Box f64>, Option>, usize), + /// Describes a function (x,y) = f(t) with a range for t and a number of points. + Parametric(Box (f64, f64)>, RangeInclusive, usize), } // ---------------------------------------------------------------------------- @@ -75,7 +76,7 @@ pub struct ExplicitGenerator { /// A series of values forming a path. pub struct Curve { pub(crate) values: Vec, - pub(crate) generator: Option, + generator: Option, pub(crate) bounds: Bounds, pub(crate) stroke: Stroke, pub(crate) name: String, @@ -111,11 +112,21 @@ impl Curve { pub fn from_explicit_callback( function: impl Fn(f64) -> f64 + 'static, range: Option>, + points: usize, ) -> Self { - let generator = ExplicitGenerator { - function: Box::new(function), - range, - }; + let generator = Generator::Explicit(Box::new(function), range, points); + Self { + generator: Some(generator), + ..Self::empty() + } + } + + pub fn from_parametric_callback( + function: impl Fn(f64) -> (f64, f64) + 'static, + t_range: RangeInclusive, + points: usize, + ) -> Self { + let generator = Generator::Parametric(Box::new(function), t_range, points); Self { generator: Some(generator), ..Self::empty() @@ -129,23 +140,34 @@ impl Curve { /// If initialized with a generator function, this will generate `n` evenly spaced points in the /// given range. - pub(crate) fn generate_points(&mut self, mut x_range: RangeInclusive, n: usize) { - if let Some(function) = self.generator.as_ref() { - if let Some(range) = &function.range { - x_range = x_range.start().max(*range.start())..=x_range.end().min(*range.end()); + pub(crate) fn generate_points(&mut self, mut x_range: RangeInclusive) { + let range_union = |range1: &RangeInclusive, range2: &RangeInclusive| { + range1.start().max(*range2.start())..=range1.end().min(*range2.end()) + }; + match &self.generator { + Some(Generator::Explicit(fun, maybe_range, n)) => { + if let Some(range) = maybe_range { + x_range = range_union(&x_range, range); + } + let increment = (x_range.end() - x_range.start()) / (n - 1) as f64; + self.values = (0..*n) + .map(|i| { + let x = x_range.start() + i as f64 * increment; + Value { x, y: fun(x) } + }) + .collect(); } - - let increment = (x_range.end() - x_range.start()) / (n - 1) as f64; - - self.values = (0..n) - .map(|i| { - let x = x_range.start() + i as f64 * increment; - Value { - x, - y: (function.function)(x), - } - }) - .collect(); + Some(Generator::Parametric(fun, range, n)) => { + let increment = (range.end() - range.start()) / (n - 1) as f64; + self.values = (0..*n) + .map(|i| { + let t = range.start() + i as f64 * increment; + let (x, y) = fun(t); + Value { x, y } + }) + .collect(); + } + None => {} } } diff --git a/egui/src/widgets/plotting/plot.rs b/egui/src/widgets/plotting/plot.rs index dcaf11c1e10..eba3c2f2482 100644 --- a/egui/src/widgets/plotting/plot.rs +++ b/egui/src/widgets/plotting/plot.rs @@ -331,7 +331,7 @@ impl Widget for Plot { // TODO: Let the user pick a resolution? curves .iter_mut() - .for_each(|curve| curve.generate_points(transform.bounds().range_x(), 100)); + .for_each(|curve| curve.generate_points(transform.bounds().range_x())); ui.memory().id_data.insert( plot_id, diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index be6d437d6a4..021d5deaef0 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -109,20 +109,24 @@ impl PlotDemo { fn sin(&self) -> Curve { let t = self.time; - Curve::from_explicit_callback(move |x| 0.5 * (2.0 * x).sin() * t.sin(), Some((-2.)..=2.)) - .color(Color32::from_rgb(200, 100, 100)) - .name("0.5 * sin(2x) * sin(t)") + Curve::from_explicit_callback( + move |x| 0.5 * (2.0 * x).sin() * t.sin(), + Some((-2.)..=2.), + 512, + ) + .color(Color32::from_rgb(200, 100, 100)) + .name("0.5 * sin(2x) * sin(t)") } fn thingy(&self) -> Curve { - let n = 512; - let complex_curve = (0..=n).map(|i| { - let t = remap(i as f64, 0.0..=(n as f64), 0.0..=TAU); - Value::new((2.0 * t + self.time).sin(), (3.0 * t).sin()) - }); - Curve::from_values_iter(complex_curve) - .color(Color32::from_rgb(100, 150, 250)) - .name("x = sin(2t), y = sin(3t)") + let time = self.time; + Curve::from_parametric_callback( + move |t| ((2.0 * t + time).sin(), (3.0 * t).sin()), + 0.0..=TAU, + 512, + ) + .color(Color32::from_rgb(100, 150, 250)) + .name("x = sin(2t), y = sin(3t)") } } From f0e22c5df066836644a856944febf2361cb3ed82 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Wed, 21 Apr 2021 21:46:59 +0200 Subject: [PATCH 14/24] minor changes --- egui_demo_lib/src/apps/demo/plot_demo.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index 021d5deaef0..fc2ca3960c3 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -17,7 +17,7 @@ impl Default for PlotDemo { Self { animate: true, time: 0.0, - circle_radius: 0.5, + circle_radius: 1.5, circle_center: Pos2::new(0.0, 0.0), square: false, proportional: true, @@ -108,9 +108,9 @@ impl PlotDemo { } fn sin(&self) -> Curve { - let t = self.time; + let time = self.time; Curve::from_explicit_callback( - move |x| 0.5 * (2.0 * x).sin() * t.sin(), + move |x| 0.5 * (2.0 * x).sin() * time.sin(), Some((-2.)..=2.), 512, ) From 91688e4989a0fc9d1f4b0941db9bbe6e90b03a36 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Wed, 21 Apr 2021 23:13:20 +0200 Subject: [PATCH 15/24] add documentation --- egui/src/widgets/plotting/items.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/egui/src/widgets/plotting/items.rs b/egui/src/widgets/plotting/items.rs index eb867c4b0c9..f39dd3e3101 100644 --- a/egui/src/widgets/plotting/items.rs +++ b/egui/src/widgets/plotting/items.rs @@ -109,6 +109,7 @@ impl Curve { Self::from_values(iter.collect()) } + /// Draw a curve based on a function `y=f(x)`, an optional range for x and the number of points. pub fn from_explicit_callback( function: impl Fn(f64) -> f64 + 'static, range: Option>, @@ -121,6 +122,7 @@ impl Curve { } } + /// Draw a curve based on a function `(x,y)=f(t)`, a range for t and the number of points. pub fn from_parametric_callback( function: impl Fn(f64) -> (f64, f64) + 'static, t_range: RangeInclusive, From ae375c1447657e24b28514daad9fa15883864ece Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Wed, 21 Apr 2021 23:26:43 +0200 Subject: [PATCH 16/24] fix merge issues --- egui/src/widgets/plotting/plot.rs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/egui/src/widgets/plotting/plot.rs b/egui/src/widgets/plotting/plot.rs index cacd3f8b3fa..eba3c2f2482 100644 --- a/egui/src/widgets/plotting/plot.rs +++ b/egui/src/widgets/plotting/plot.rs @@ -16,16 +16,6 @@ struct PlotMemory { // ---------------------------------------------------------------------------- -/// Information about the plot that has to persist between frames. -#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] -#[derive(Clone)] -struct PlotMemory { - bounds: Bounds, - auto_bounds: bool, -} - -// ---------------------------------------------------------------------------- - /// A 2D plot, e.g. a graph of a function. /// /// `Plot` supports multiple curves. @@ -63,8 +53,6 @@ pub struct Plot { data_aspect: Option, view_aspect: Option, - min_auto_bounds: Bounds, - show_x: bool, show_y: bool, } @@ -93,8 +81,6 @@ impl Plot { data_aspect: None, view_aspect: None, - min_auto_bounds: Bounds::NOTHING, - show_x: true, show_y: true, } @@ -246,7 +232,6 @@ impl Widget for Plot { view_aspect, show_x, show_y, - min_auto_bounds, } = self; let plot_id = ui.make_persistent_id(name); From 2abfb0fde165235ced69c66c22ff09daa33b13a8 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Sat, 24 Apr 2021 13:30:31 +0200 Subject: [PATCH 17/24] changes based on review --- egui/src/widgets/plotting/items.rs | 83 +++++++++++++----------- egui/src/widgets/plotting/plot.rs | 25 +++++-- egui_demo_lib/src/apps/demo/plot_demo.rs | 4 +- 3 files changed, 66 insertions(+), 46 deletions(-) diff --git a/egui/src/widgets/plotting/items.rs b/egui/src/widgets/plotting/items.rs index f39dd3e3101..46c40e1d458 100644 --- a/egui/src/widgets/plotting/items.rs +++ b/egui/src/widgets/plotting/items.rs @@ -64,11 +64,11 @@ impl VLine { // ---------------------------------------------------------------------------- -enum Generator { - /// Describes a function y = f(x) with an optional range for x and a number of points. - Explicit(Box f64>, Option>, usize), - /// Describes a function (x,y) = f(t) with a range for t and a number of points. - Parametric(Box (f64, f64)>, RangeInclusive, usize), +/// Describes a function y = f(x) with an optional range for x and a number of points. +struct ExplicitGenerator { + function: Box f64>, + x_range: RangeInclusive, + points: usize, } // ---------------------------------------------------------------------------- @@ -76,7 +76,7 @@ enum Generator { /// A series of values forming a path. pub struct Curve { pub(crate) values: Vec, - generator: Option, + generator: Option, pub(crate) bounds: Bounds, pub(crate) stroke: Stroke, pub(crate) name: String, @@ -112,27 +112,41 @@ impl Curve { /// Draw a curve based on a function `y=f(x)`, an optional range for x and the number of points. pub fn from_explicit_callback( function: impl Fn(f64) -> f64 + 'static, - range: Option>, + x_range: RangeInclusive, points: usize, ) -> Self { - let generator = Generator::Explicit(Box::new(function), range, points); + let mut bounds = Bounds::NOTHING; + if x_range.start().is_finite() && x_range.end().is_finite() { + bounds.min[0] = *x_range.start(); + bounds.max[0] = *x_range.end(); + } + + let generator = ExplicitGenerator { + function: Box::new(function), + x_range, + points, + }; + Self { generator: Some(generator), + bounds, ..Self::empty() } } /// Draw a curve based on a function `(x,y)=f(t)`, a range for t and the number of points. pub fn from_parametric_callback( - function: impl Fn(f64) -> (f64, f64) + 'static, + function: impl Fn(f64) -> (f64, f64), t_range: RangeInclusive, points: usize, ) -> Self { - let generator = Generator::Parametric(Box::new(function), t_range, points); - Self { - generator: Some(generator), - ..Self::empty() - } + let increment = (t_range.end() - t_range.start()) / (points - 1) as f64; + let values = (0..points).map(|i| { + let t = t_range.start() + i as f64 * increment; + let (x, y) = function(t); + Value { x, y } + }); + Self::from_values_iter(values) } /// Returns true if there are no data points available and there is no function to generate any. @@ -140,36 +154,31 @@ impl Curve { self.generator.is_none() && self.values.is_empty() } + /// Returns the intersection of two ranges if they intersect. + fn range_intersection( + range1: &RangeInclusive, + range2: &RangeInclusive, + ) -> Option> { + let start = range1.start().max(*range2.start()); + let end = range1.end().min(*range2.end()); + (start < end).then(|| start..=end) + } + /// If initialized with a generator function, this will generate `n` evenly spaced points in the /// given range. - pub(crate) fn generate_points(&mut self, mut x_range: RangeInclusive) { - let range_union = |range1: &RangeInclusive, range2: &RangeInclusive| { - range1.start().max(*range2.start())..=range1.end().min(*range2.end()) - }; - match &self.generator { - Some(Generator::Explicit(fun, maybe_range, n)) => { - if let Some(range) = maybe_range { - x_range = range_union(&x_range, range); - } - let increment = (x_range.end() - x_range.start()) / (n - 1) as f64; - self.values = (0..*n) - .map(|i| { - let x = x_range.start() + i as f64 * increment; - Value { x, y: fun(x) } - }) - .collect(); - } - Some(Generator::Parametric(fun, range, n)) => { - let increment = (range.end() - range.start()) / (n - 1) as f64; - self.values = (0..*n) + pub(crate) fn generate_points(&mut self, x_range: RangeInclusive) { + if let Some(generator) = self.generator.take() { + if let Some(intersection) = Self::range_intersection(&x_range, &generator.x_range) { + let increment = + (intersection.end() - intersection.start()) / (generator.points - 1) as f64; + self.values = (0..generator.points) .map(|i| { - let t = range.start() + i as f64 * increment; - let (x, y) = fun(t); + let x = intersection.start() + i as f64 * increment; + let y = (generator.function)(x); Value { x, y } }) .collect(); } - None => {} } } diff --git a/egui/src/widgets/plotting/plot.rs b/egui/src/widgets/plotting/plot.rs index eba3c2f2482..b6f98fea1f5 100644 --- a/egui/src/widgets/plotting/plot.rs +++ b/egui/src/widgets/plotting/plot.rs @@ -125,7 +125,9 @@ impl Plot { } /// 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. + /// For instance, it can be useful to set this to `1.0` for when the two axes show the same + /// unit. + /// By default the plot window's aspect ratio is used. pub fn data_aspect(mut self, data_aspect: f32) -> Self { self.data_aspect = Some(data_aspect); self @@ -172,25 +174,37 @@ impl Plot { self } - /// Always keep the x-axis centered. + #[deprecated = "Renamed center_x_axis"] + pub fn symmetrical_x_axis(mut self, on: bool) -> Self { + self.center_x_axis = on; + self + } + + #[deprecated = "Renamed center_y_axis"] + pub fn symmetrical_y_axis(mut self, on: bool) -> Self { + self.center_y_axis = on; + self + } + + /// Always keep the x-axis centered. Default: `false`. pub fn center_x_axis(mut self, on: bool) -> Self { self.center_x_axis = on; self } - /// Always keep the y-axis centered. + /// Always keep the y-axis centered. Default: `false`. pub fn center_y_axis(mut self, on: bool) -> Self { self.center_y_axis = on; self } - /// Whether to allow zooming in the plot. + /// Whether to allow zooming in the plot. Default: `true`. pub fn allow_zoom(mut self, on: bool) -> Self { self.allow_zoom = on; self } - /// Whether to allow dragging in the plot to move the bounds. + /// Whether to allow dragging in the plot to move the bounds. Default: `true`. pub fn allow_drag(mut self, on: bool) -> Self { self.allow_drag = on; self @@ -328,7 +342,6 @@ impl Widget for Plot { } // Initialize values from functions. - // TODO: Let the user pick a resolution? curves .iter_mut() .for_each(|curve| curve.generate_points(transform.bounds().range_x())); diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index fc2ca3960c3..6a0c9d2d6ac 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -111,7 +111,7 @@ impl PlotDemo { let time = self.time; Curve::from_explicit_callback( move |x| 0.5 * (2.0 * x).sin() * time.sin(), - Some((-2.)..=2.), + f64::NEG_INFINITY..=f64::INFINITY, 512, ) .color(Color32::from_rgb(200, 100, 100)) @@ -143,8 +143,6 @@ impl super::View for PlotDemo { .curve(self.circle()) .curve(self.sin()) .curve(self.thingy()) - .center_x_axis(false) - .center_y_axis(false) .min_size(Vec2::new(256.0, 200.0)); if self.square { plot = plot.view_aspect(1.0); From 2782625a6ea1353e93ff6cd28dd4f26edf7527f4 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Sat, 24 Apr 2021 13:34:38 +0200 Subject: [PATCH 18/24] rename folder --- egui/src/widgets/{plotting => plot}/items.rs | 0 egui/src/widgets/{plotting => plot}/mod.rs | 0 egui/src/widgets/{plotting => plot}/plot.rs | 0 egui/src/widgets/{plotting => plot}/transform.rs | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename egui/src/widgets/{plotting => plot}/items.rs (100%) rename egui/src/widgets/{plotting => plot}/mod.rs (100%) rename egui/src/widgets/{plotting => plot}/plot.rs (100%) rename egui/src/widgets/{plotting => plot}/transform.rs (100%) diff --git a/egui/src/widgets/plotting/items.rs b/egui/src/widgets/plot/items.rs similarity index 100% rename from egui/src/widgets/plotting/items.rs rename to egui/src/widgets/plot/items.rs diff --git a/egui/src/widgets/plotting/mod.rs b/egui/src/widgets/plot/mod.rs similarity index 100% rename from egui/src/widgets/plotting/mod.rs rename to egui/src/widgets/plot/mod.rs diff --git a/egui/src/widgets/plotting/plot.rs b/egui/src/widgets/plot/plot.rs similarity index 100% rename from egui/src/widgets/plotting/plot.rs rename to egui/src/widgets/plot/plot.rs diff --git a/egui/src/widgets/plotting/transform.rs b/egui/src/widgets/plot/transform.rs similarity index 100% rename from egui/src/widgets/plotting/transform.rs rename to egui/src/widgets/plot/transform.rs From e121938dd90dd6994b38cead91994a69e7ecc2de Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Sat, 24 Apr 2021 13:36:23 +0200 Subject: [PATCH 19/24] make plot.rs the mod.rs file --- egui/src/widgets/plot/plot.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/egui/src/widgets/plot/plot.rs b/egui/src/widgets/plot/plot.rs index b6f98fea1f5..c147e55f5d4 100644 --- a/egui/src/widgets/plot/plot.rs +++ b/egui/src/widgets/plot/plot.rs @@ -1,5 +1,13 @@ -use super::items::{Curve, HLine, VLine, Value}; -use super::transform::{Bounds, ScreenTransform}; +//! Simple plotting library. + +mod items; +mod plot; +mod transform; + +pub use items::{Curve, HLine, VLine, Value}; +pub use plot::Plot; + +use transform::{Bounds, ScreenTransform}; use crate::*; use color::Hsva; From 5cf493e560ece81f0b191a9142bb2add19f9d0da Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Sat, 24 Apr 2021 13:36:46 +0200 Subject: [PATCH 20/24] remove mod.rs --- egui/src/widgets/plot/mod.rs | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 egui/src/widgets/plot/mod.rs diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs deleted file mode 100644 index a0db334bc92..00000000000 --- a/egui/src/widgets/plot/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -//! Simple plotting library. - -mod items; -mod plot; -mod transform; - -pub use items::{Curve, HLine, VLine, Value}; -pub use plot::Plot; From f1968cd89527c8b3e182daecf9664732e6ef0aa9 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Sat, 24 Apr 2021 13:37:03 +0200 Subject: [PATCH 21/24] rename file --- egui/src/widgets/plot/{plot.rs => mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename egui/src/widgets/plot/{plot.rs => mod.rs} (100%) diff --git a/egui/src/widgets/plot/plot.rs b/egui/src/widgets/plot/mod.rs similarity index 100% rename from egui/src/widgets/plot/plot.rs rename to egui/src/widgets/plot/mod.rs From fd109351a26e8327fe30be0843ac2005ccee47f3 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Sat, 24 Apr 2021 13:40:55 +0200 Subject: [PATCH 22/24] namespace changes --- egui/src/widgets/mod.rs | 2 +- egui/src/widgets/plot/mod.rs | 6 ++---- egui_demo_lib/src/apps/demo/plot_demo.rs | 2 +- egui_demo_lib/src/apps/demo/widget_gallery.rs | 8 ++++---- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/egui/src/widgets/mod.rs b/egui/src/widgets/mod.rs index 3371c7e3e02..1594f01b9a4 100644 --- a/egui/src/widgets/mod.rs +++ b/egui/src/widgets/mod.rs @@ -12,7 +12,7 @@ pub(crate) mod drag_value; mod hyperlink; mod image; mod label; -pub mod plotting; +pub mod plot; mod selected_label; mod separator; mod slider; diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index c147e55f5d4..91379cbe7dd 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -1,12 +1,10 @@ //! Simple plotting library. mod items; -mod plot; mod transform; -pub use items::{Curve, HLine, VLine, Value}; -pub use plot::Plot; - +pub use items::{Curve, Value}; +use items::{HLine, VLine}; use transform::{Bounds, ScreenTransform}; use crate::*; diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index 6a0c9d2d6ac..58c7abd2870 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -1,4 +1,4 @@ -use egui::plotting::{Curve, Plot, Value}; +use egui::plot::{Curve, Plot, Value}; use egui::*; use std::f64::consts::TAU; diff --git a/egui_demo_lib/src/apps/demo/widget_gallery.rs b/egui_demo_lib/src/apps/demo/widget_gallery.rs index 21ca6d578cb..0561657dab8 100644 --- a/egui_demo_lib/src/apps/demo/widget_gallery.rs +++ b/egui_demo_lib/src/apps/demo/widget_gallery.rs @@ -220,14 +220,14 @@ impl WidgetGallery { } } -fn example_plot() -> egui::plotting::Plot { +fn example_plot() -> egui::plot::Plot { let n = 128; - let curve = egui::plotting::Curve::from_values_iter((0..=n).map(|i| { + let curve = egui::plot::Curve::from_values_iter((0..=n).map(|i| { use std::f64::consts::TAU; let x = egui::remap(i as f64, 0.0..=(n as f64), -TAU..=TAU); - egui::plotting::Value::new(x, x.sin()) + egui::plot::Value::new(x, x.sin()) })); - egui::plotting::Plot::new("Example Plot") + egui::plot::Plot::new("Example Plot") .curve(curve) .height(32.0) .data_aspect(1.0) From 12c3bbf6e4ca6e59a5d8f04be5b9d3eb64fb91b3 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Sat, 24 Apr 2021 13:49:22 +0200 Subject: [PATCH 23/24] fix doctest --- egui/src/widgets/plot/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 91379cbe7dd..16a8b5b8225 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -28,7 +28,7 @@ struct PlotMemory { /// /// ``` /// # let ui = &mut egui::Ui::__test(); -/// use egui::plotting::{Curve, Plot, Value}; +/// use egui::plot::{Curve, Plot, Value}; /// let sin = (0..1000).map(|i| { /// let x = i as f64 * 0.01; /// Value::new(x, x.sin()) From 12ddf5adc3140e04f3150fbdf6ebee1f2cf76897 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Sat, 24 Apr 2021 13:54:45 +0200 Subject: [PATCH 24/24] Update egui/src/widgets/plot/items.rs Co-authored-by: Emil Ernerfeldt --- egui/src/widgets/plot/items.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/egui/src/widgets/plot/items.rs b/egui/src/widgets/plot/items.rs index 46c40e1d458..e954355f1f8 100644 --- a/egui/src/widgets/plot/items.rs +++ b/egui/src/widgets/plot/items.rs @@ -109,7 +109,7 @@ impl Curve { Self::from_values(iter.collect()) } - /// Draw a curve based on a function `y=f(x)`, an optional range for x and the number of points. + /// Draw a curve based on a function `y=f(x)`, a range (which can be infinite) for x and the number of points. pub fn from_explicit_callback( function: impl Fn(f64) -> f64 + 'static, x_range: RangeInclusive,