From 44a99fc0f18cb37dd15f8eec5460db9f9bc507d0 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Mon, 31 Jan 2022 15:11:25 +0100 Subject: [PATCH 1/2] add linked axis support to the plots --- egui/src/widgets/plot/mod.rs | 96 +++++++++++++++++++++++- egui/src/widgets/plot/transform.rs | 15 +++- egui_demo_lib/src/apps/demo/plot_demo.rs | 80 +++++++++++++++++++- 3 files changed, 184 insertions(+), 7 deletions(-) diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 9550d98f0aa..0f718c6f6bc 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -1,5 +1,7 @@ //! Simple plotting library. +use std::{cell::RefCell, rc::Rc}; + use crate::*; use epaint::ahash::AHashSet; use epaint::color::Hsva; @@ -49,6 +51,63 @@ impl PlotMemory { // ---------------------------------------------------------------------------- +/// Defines how multiple plots share the same range for one or both of their axes. Can be added while building +/// a plot with [`Plot::link_axis`]. Contains an internal state, meaning that this object should be stored by +/// the user between frames. +#[derive(Clone, PartialEq)] +pub struct LinkedAxisGroup { + pub(crate) link_x: bool, + pub(crate) link_y: bool, + pub(crate) bounds: Rc>>, +} + +impl LinkedAxisGroup { + pub fn new(link_x: bool, link_y: bool) -> Self { + Self { + link_x, + link_y, + bounds: Rc::new(RefCell::new(None)), + } + } + + /// Only link the x-axis. + pub fn x() -> Self { + Self::new(true, false) + } + + /// Only link the y-axis. + pub fn y() -> Self { + Self::new(false, true) + } + + /// Link both axes. Note that this still respects the aspect ratio of the individual plots. + pub fn both() -> Self { + Self::new(true, true) + } + + /// Change whether the x-axis is linked for this group. Using this after plots in this group have been + /// drawn in this frame already may lead to unexpected results. + pub fn set_link_x(&mut self, link: bool) { + self.link_x = link; + } + + /// Change whether the y-axis is linked for this group. Using this after plots in this group have been + /// drawn in this frame already may lead to unexpected results. + pub fn set_link_y(&mut self, link: bool) { + self.link_y = link; + } + + fn get(&self) -> Option { + *self.bounds.borrow() + } + + fn set(&self, bounds: PlotBounds) { + *self.bounds.borrow_mut() = Some(bounds); + } +} + +// ---------------------------------------------------------------------------- + /// A 2D plot, e.g. a graph of a function. /// /// `Plot` supports multiple lines and points. @@ -73,6 +132,7 @@ pub struct Plot { allow_drag: bool, min_auto_bounds: PlotBounds, margin_fraction: Vec2, + linked_axes: Option, min_size: Vec2, width: Option, @@ -101,6 +161,7 @@ impl Plot { allow_drag: true, min_auto_bounds: PlotBounds::NOTHING, margin_fraction: Vec2::splat(0.05), + linked_axes: None, min_size: Vec2::splat(64.0), width: None, @@ -281,6 +342,13 @@ impl Plot { self } + /// Add a [`LinkedAxisGroup`] so that this plot will share the bounds with other plots that have this + /// group assigned. A plot cannot belong to more than one group. + pub fn link_axis(mut self, group: LinkedAxisGroup) -> Self { + self.linked_axes = Some(group); + self + } + /// Interact with and add items to the plot and finally draw it. pub fn show(self, ui: &mut Ui, build_fn: impl FnOnce(&mut PlotUi) -> R) -> InnerResponse { let Self { @@ -303,6 +371,7 @@ impl Plot { legend_config, show_background, show_axes, + linked_axes, } = self; // Determine the size of the plot in the UI @@ -415,6 +484,22 @@ impl Plot { // --- Bound computation --- let mut bounds = *last_screen_transform.bounds(); + // Transfer the bounds from a link group. + if let Some(axes) = linked_axes.as_ref() { + if let Some(linked_bounds) = axes.get() { + if axes.link_x { + bounds.min[0] = linked_bounds.min[0]; + bounds.max[0] = linked_bounds.max[0]; + } + if axes.link_y { + bounds.min[1] = linked_bounds.min[1]; + bounds.max[1] = linked_bounds.max[1]; + } + // Turn off auto bounds to keep it from overriding what we just set. + auto_bounds = false; + } + } + // Allow double clicking to reset to automatic bounds. auto_bounds |= response.double_clicked_by(PointerButton::Primary); @@ -431,7 +516,10 @@ impl Plot { // Enforce equal aspect ratio. if let Some(data_aspect) = data_aspect { - transform.set_aspect(data_aspect as f64); + let preserve_y = linked_axes + .as_ref() + .map_or(false, |group| group.link_y && !group.link_x); + transform.set_aspect(data_aspect as f64, preserve_y); } // Dragging @@ -484,6 +572,10 @@ impl Plot { hovered_entry = legend.get_hovered_entry_name(); } + if let Some(group) = linked_axes.as_ref() { + group.set(*transform.bounds()); + } + let memory = PlotMemory { auto_bounds, hovered_entry, @@ -504,7 +596,7 @@ impl Plot { } /// Provides methods to interact with a plot while building it. It is the single argument of the closure -/// provided to `Plot::show`. See [`Plot`] for an example of how to use it. +/// provided to [`Plot::show`]. See [`Plot`] for an example of how to use it. pub struct PlotUi { items: Vec>, next_auto_color_idx: usize, diff --git a/egui/src/widgets/plot/transform.rs b/egui/src/widgets/plot/transform.rs index 53007950338..211818b7ae9 100644 --- a/egui/src/widgets/plot/transform.rs +++ b/egui/src/widgets/plot/transform.rs @@ -273,13 +273,20 @@ impl ScreenTransform { (self.bounds.width() / rw) / (self.bounds.height() / rh) } - pub fn set_aspect(&mut self, aspect: f64) { - let epsilon = 1e-5; + /// Sets the aspect ratio by either expanding the x-axis or contracting the y-axis. + pub fn set_aspect(&mut self, aspect: f64, preserve_y: bool) { let current_aspect = self.get_aspect(); - if current_aspect < aspect - epsilon { + + let epsilon = 1e-5; + if (current_aspect - aspect).abs() < epsilon { + // Don't make any changes when the aspect is already almost correct. + return; + } + + if preserve_y { self.bounds .expand_x((aspect / current_aspect - 1.0) * self.bounds.width() * 0.5); - } else if current_aspect > aspect + epsilon { + } else { self.bounds .expand_y((current_aspect / aspect - 1.0) * self.bounds.height() * 0.5); } diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index 931bc40a779..565660daae2 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -300,6 +300,78 @@ impl Widget for &mut LegendDemo { } } +#[derive(PartialEq)] +struct LinkedAxisDemo { + link_x: bool, + link_y: bool, + group: plot::LinkedAxisGroup, +} + +impl Default for LinkedAxisDemo { + fn default() -> Self { + let link_x = true; + let link_y = false; + Self { + link_x, + link_y, + group: plot::LinkedAxisGroup::new(link_x, link_y), + } + } +} + +impl LinkedAxisDemo { + fn line_with_slope(slope: f64) -> Line { + Line::new(Values::from_explicit_callback(move |x| slope * x, .., 100)) + } + fn sin() -> Line { + Line::new(Values::from_explicit_callback(move |x| x.sin(), .., 100)) + } + fn cos() -> Line { + Line::new(Values::from_explicit_callback(move |x| x.cos(), .., 100)) + } + + fn configure_plot(plot_ui: &mut plot::PlotUi) { + plot_ui.line(LinkedAxisDemo::line_with_slope(0.5)); + plot_ui.line(LinkedAxisDemo::line_with_slope(1.0)); + plot_ui.line(LinkedAxisDemo::line_with_slope(2.0)); + plot_ui.line(LinkedAxisDemo::sin()); + plot_ui.line(LinkedAxisDemo::cos()); + } +} + +impl Widget for &mut LinkedAxisDemo { + fn ui(self, ui: &mut Ui) -> Response { + ui.horizontal(|ui| { + ui.label("Linked axes:"); + ui.checkbox(&mut self.link_x, "X"); + ui.checkbox(&mut self.link_y, "Y"); + }); + self.group.set_link_x(self.link_x); + self.group.set_link_y(self.link_y); + ui.horizontal(|ui| { + Plot::new("linked_axis_1") + .data_aspect(1.0) + .width(250.0) + .height(250.0) + .link_axis(self.group.clone()) + .show(ui, LinkedAxisDemo::configure_plot); + Plot::new("linked_axis_2") + .data_aspect(2.0) + .width(150.0) + .height(250.0) + .link_axis(self.group.clone()) + .show(ui, LinkedAxisDemo::configure_plot); + }); + Plot::new("linked_axis_3") + .data_aspect(0.5) + .width(250.0) + .height(150.0) + .link_axis(self.group.clone()) + .show(ui, LinkedAxisDemo::configure_plot) + .response + } +} + #[derive(PartialEq, Default)] struct ItemsDemo { texture: Option, @@ -639,11 +711,12 @@ enum Panel { Charts, Items, Interaction, + LinkedAxes, } impl Default for Panel { fn default() -> Self { - Self::Charts + Self::Lines } } @@ -655,6 +728,7 @@ pub struct PlotDemo { charts_demo: ChartsDemo, items_demo: ItemsDemo, interaction_demo: InteractionDemo, + linked_axes_demo: LinkedAxisDemo, open_panel: Panel, } @@ -698,6 +772,7 @@ impl super::View for PlotDemo { ui.selectable_value(&mut self.open_panel, Panel::Charts, "Charts"); ui.selectable_value(&mut self.open_panel, Panel::Items, "Items"); ui.selectable_value(&mut self.open_panel, Panel::Interaction, "Interaction"); + ui.selectable_value(&mut self.open_panel, Panel::LinkedAxes, "Linked Axes"); }); ui.separator(); @@ -720,6 +795,9 @@ impl super::View for PlotDemo { Panel::Interaction => { ui.add(&mut self.interaction_demo); } + Panel::LinkedAxes => { + ui.add(&mut self.linked_axes_demo); + } } } } From 3191a17402ed63744db5ccb51cade9f150b97c9f Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Mon, 31 Jan 2022 15:17:21 +0100 Subject: [PATCH 2/2] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6454b4aa85..901ce5ad813 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w * Added `CollapsingHeader::icon` to override the default open/close icon using a custom function. ([1147](https://github.com/emilk/egui/pull/1147)). * Added `Plot::x_axis_formatter` and `Plot::y_axis_formatter` for custom axis labels ([#1130](https://github.com/emilk/egui/pull/1130)). * Added `ui.data()`, `ctx.data()`, `ctx.options()` and `ctx.tessellation_options()` ([#1175](https://github.com/emilk/egui/pull/1175)). +* Added linked axis support for plots via `plot::LinkedAxisGroup` ([#1184](https://github.com/emilk/egui/pull/1184)). ### Changed 🔧 * ⚠️ `Context::input` and `Ui::input` now locks a mutex. This can lead to a dead-lock is used in an `if let` binding!