Browse Source

Plot: Linked axis support (#1184)

pull/1193/head
Sven Niederberger 3 years ago
committed by GitHub
parent
commit
4e99d8f409
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 96
      egui/src/widgets/plot/mod.rs
  3. 15
      egui/src/widgets/plot/transform.rs
  4. 80
      egui_demo_lib/src/apps/demo/plot_demo.rs

1
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 `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 `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 `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 🔧 ### Changed 🔧
* ⚠️ `Context::input` and `Ui::input` now locks a mutex. This can lead to a dead-lock is used in an `if let` binding! * ⚠️ `Context::input` and `Ui::input` now locks a mutex. This can lead to a dead-lock is used in an `if let` binding!

96
egui/src/widgets/plot/mod.rs

@ -1,5 +1,7 @@
//! Simple plotting library. //! Simple plotting library.
use std::{cell::RefCell, rc::Rc};
use crate::*; use crate::*;
use epaint::ahash::AHashSet; use epaint::ahash::AHashSet;
use epaint::color::Hsva; 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<RefCell<Option<PlotBounds>>>,
}
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<PlotBounds> {
*self.bounds.borrow()
}
fn set(&self, bounds: PlotBounds) {
*self.bounds.borrow_mut() = Some(bounds);
}
}
// ----------------------------------------------------------------------------
/// A 2D plot, e.g. a graph of a function. /// A 2D plot, e.g. a graph of a function.
/// ///
/// `Plot` supports multiple lines and points. /// `Plot` supports multiple lines and points.
@ -73,6 +132,7 @@ pub struct Plot {
allow_drag: bool, allow_drag: bool,
min_auto_bounds: PlotBounds, min_auto_bounds: PlotBounds,
margin_fraction: Vec2, margin_fraction: Vec2,
linked_axes: Option<LinkedAxisGroup>,
min_size: Vec2, min_size: Vec2,
width: Option<f32>, width: Option<f32>,
@ -101,6 +161,7 @@ impl Plot {
allow_drag: true, allow_drag: true,
min_auto_bounds: PlotBounds::NOTHING, min_auto_bounds: PlotBounds::NOTHING,
margin_fraction: Vec2::splat(0.05), margin_fraction: Vec2::splat(0.05),
linked_axes: None,
min_size: Vec2::splat(64.0), min_size: Vec2::splat(64.0),
width: None, width: None,
@ -281,6 +342,13 @@ impl Plot {
self 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. /// Interact with and add items to the plot and finally draw it.
pub fn show<R>(self, ui: &mut Ui, build_fn: impl FnOnce(&mut PlotUi) -> R) -> InnerResponse<R> { pub fn show<R>(self, ui: &mut Ui, build_fn: impl FnOnce(&mut PlotUi) -> R) -> InnerResponse<R> {
let Self { let Self {
@ -303,6 +371,7 @@ impl Plot {
legend_config, legend_config,
show_background, show_background,
show_axes, show_axes,
linked_axes,
} = self; } = self;
// Determine the size of the plot in the UI // Determine the size of the plot in the UI
@ -415,6 +484,22 @@ impl Plot {
// --- Bound computation --- // --- Bound computation ---
let mut bounds = *last_screen_transform.bounds(); 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. // Allow double clicking to reset to automatic bounds.
auto_bounds |= response.double_clicked_by(PointerButton::Primary); auto_bounds |= response.double_clicked_by(PointerButton::Primary);
@ -431,7 +516,10 @@ impl Plot {
// Enforce equal aspect ratio. // Enforce equal aspect ratio.
if let Some(data_aspect) = data_aspect { 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 // Dragging
@ -484,6 +572,10 @@ impl Plot {
hovered_entry = legend.get_hovered_entry_name(); hovered_entry = legend.get_hovered_entry_name();
} }
if let Some(group) = linked_axes.as_ref() {
group.set(*transform.bounds());
}
let memory = PlotMemory { let memory = PlotMemory {
auto_bounds, auto_bounds,
hovered_entry, 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 /// 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 { pub struct PlotUi {
items: Vec<Box<dyn PlotItem>>, items: Vec<Box<dyn PlotItem>>,
next_auto_color_idx: usize, next_auto_color_idx: usize,

15
egui/src/widgets/plot/transform.rs

@ -273,13 +273,20 @@ impl ScreenTransform {
(self.bounds.width() / rw) / (self.bounds.height() / rh) (self.bounds.width() / rw) / (self.bounds.height() / rh)
} }
pub fn set_aspect(&mut self, aspect: f64) { /// Sets the aspect ratio by either expanding the x-axis or contracting the y-axis.
let epsilon = 1e-5; pub fn set_aspect(&mut self, aspect: f64, preserve_y: bool) {
let current_aspect = self.get_aspect(); 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 self.bounds
.expand_x((aspect / current_aspect - 1.0) * self.bounds.width() * 0.5); .expand_x((aspect / current_aspect - 1.0) * self.bounds.width() * 0.5);
} else if current_aspect > aspect + epsilon { } else {
self.bounds self.bounds
.expand_y((current_aspect / aspect - 1.0) * self.bounds.height() * 0.5); .expand_y((current_aspect / aspect - 1.0) * self.bounds.height() * 0.5);
} }

80
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)] #[derive(PartialEq, Default)]
struct ItemsDemo { struct ItemsDemo {
texture: Option<egui::TextureHandle>, texture: Option<egui::TextureHandle>,
@ -639,11 +711,12 @@ enum Panel {
Charts, Charts,
Items, Items,
Interaction, Interaction,
LinkedAxes,
} }
impl Default for Panel { impl Default for Panel {
fn default() -> Self { fn default() -> Self {
Self::Charts Self::Lines
} }
} }
@ -655,6 +728,7 @@ pub struct PlotDemo {
charts_demo: ChartsDemo, charts_demo: ChartsDemo,
items_demo: ItemsDemo, items_demo: ItemsDemo,
interaction_demo: InteractionDemo, interaction_demo: InteractionDemo,
linked_axes_demo: LinkedAxisDemo,
open_panel: Panel, 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::Charts, "Charts");
ui.selectable_value(&mut self.open_panel, Panel::Items, "Items"); 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::Interaction, "Interaction");
ui.selectable_value(&mut self.open_panel, Panel::LinkedAxes, "Linked Axes");
}); });
ui.separator(); ui.separator();
@ -720,6 +795,9 @@ impl super::View for PlotDemo {
Panel::Interaction => { Panel::Interaction => {
ui.add(&mut self.interaction_demo); ui.add(&mut self.interaction_demo);
} }
Panel::LinkedAxes => {
ui.add(&mut self.linked_axes_demo);
}
} }
} }
} }

Loading…
Cancel
Save