Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plot: Linked axis support #1184

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
96 changes: 94 additions & 2 deletions egui/src/widgets/plot/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//! Simple plotting library.

use std::{cell::RefCell, rc::Rc};

use crate::*;
use epaint::ahash::AHashSet;
use epaint::color::Hsva;
Expand Down Expand Up @@ -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.
///
/// `Plot` supports multiple lines and points.
Expand All @@ -73,6 +132,7 @@ pub struct Plot {
allow_drag: bool,
min_auto_bounds: PlotBounds,
margin_fraction: Vec2,
linked_axes: Option<LinkedAxisGroup>,

min_size: Vec2,
width: Option<f32>,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<R>(self, ui: &mut Ui, build_fn: impl FnOnce(&mut PlotUi) -> R) -> InnerResponse<R> {
let Self {
Expand All @@ -303,6 +371,7 @@ impl Plot {
legend_config,
show_background,
show_axes,
linked_axes,
} = self;

// Determine the size of the plot in the UI
Expand Down Expand Up @@ -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);

Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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<Box<dyn PlotItem>>,
next_auto_color_idx: usize,
Expand Down
15 changes: 11 additions & 4 deletions egui/src/widgets/plot/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
80 changes: 79 additions & 1 deletion egui_demo_lib/src/apps/demo/plot_demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<egui::TextureHandle>,
Expand Down Expand Up @@ -639,11 +711,12 @@ enum Panel {
Charts,
Items,
Interaction,
LinkedAxes,
}

impl Default for Panel {
fn default() -> Self {
Self::Charts
Self::Lines
}
}

Expand All @@ -655,6 +728,7 @@ pub struct PlotDemo {
charts_demo: ChartsDemo,
items_demo: ItemsDemo,
interaction_demo: InteractionDemo,
linked_axes_demo: LinkedAxisDemo,
open_panel: Panel,
}

Expand Down Expand Up @@ -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();

Expand All @@ -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);
}
}
}
}
Expand Down