Skip to content

Commit 4e99d8f

Browse files
authored
Plot: Linked axis support (#1184)
1 parent b5aaa5f commit 4e99d8f

File tree

4 files changed

+185
-7
lines changed

4 files changed

+185
-7
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w
1818
* Added `CollapsingHeader::icon` to override the default open/close icon using a custom function. ([1147](https://github.com/emilk/egui/pull/1147)).
1919
* Added `Plot::x_axis_formatter` and `Plot::y_axis_formatter` for custom axis labels ([#1130](https://github.com/emilk/egui/pull/1130)).
2020
* Added `ui.data()`, `ctx.data()`, `ctx.options()` and `ctx.tessellation_options()` ([#1175](https://github.com/emilk/egui/pull/1175)).
21+
* Added linked axis support for plots via `plot::LinkedAxisGroup` ([#1184](https://github.com/emilk/egui/pull/1184)).
2122

2223
### Changed 🔧
2324
* ⚠️ `Context::input` and `Ui::input` now locks a mutex. This can lead to a dead-lock is used in an `if let` binding!

egui/src/widgets/plot/mod.rs

+94-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
//! Simple plotting library.
22
3+
use std::{cell::RefCell, rc::Rc};
4+
35
use crate::*;
46
use epaint::ahash::AHashSet;
57
use epaint::color::Hsva;
@@ -49,6 +51,63 @@ impl PlotMemory {
4951

5052
// ----------------------------------------------------------------------------
5153

54+
/// Defines how multiple plots share the same range for one or both of their axes. Can be added while building
55+
/// a plot with [`Plot::link_axis`]. Contains an internal state, meaning that this object should be stored by
56+
/// the user between frames.
57+
#[derive(Clone, PartialEq)]
58+
pub struct LinkedAxisGroup {
59+
pub(crate) link_x: bool,
60+
pub(crate) link_y: bool,
61+
pub(crate) bounds: Rc<RefCell<Option<PlotBounds>>>,
62+
}
63+
64+
impl LinkedAxisGroup {
65+
pub fn new(link_x: bool, link_y: bool) -> Self {
66+
Self {
67+
link_x,
68+
link_y,
69+
bounds: Rc::new(RefCell::new(None)),
70+
}
71+
}
72+
73+
/// Only link the x-axis.
74+
pub fn x() -> Self {
75+
Self::new(true, false)
76+
}
77+
78+
/// Only link the y-axis.
79+
pub fn y() -> Self {
80+
Self::new(false, true)
81+
}
82+
83+
/// Link both axes. Note that this still respects the aspect ratio of the individual plots.
84+
pub fn both() -> Self {
85+
Self::new(true, true)
86+
}
87+
88+
/// Change whether the x-axis is linked for this group. Using this after plots in this group have been
89+
/// drawn in this frame already may lead to unexpected results.
90+
pub fn set_link_x(&mut self, link: bool) {
91+
self.link_x = link;
92+
}
93+
94+
/// Change whether the y-axis is linked for this group. Using this after plots in this group have been
95+
/// drawn in this frame already may lead to unexpected results.
96+
pub fn set_link_y(&mut self, link: bool) {
97+
self.link_y = link;
98+
}
99+
100+
fn get(&self) -> Option<PlotBounds> {
101+
*self.bounds.borrow()
102+
}
103+
104+
fn set(&self, bounds: PlotBounds) {
105+
*self.bounds.borrow_mut() = Some(bounds);
106+
}
107+
}
108+
109+
// ----------------------------------------------------------------------------
110+
52111
/// A 2D plot, e.g. a graph of a function.
53112
///
54113
/// `Plot` supports multiple lines and points.
@@ -73,6 +132,7 @@ pub struct Plot {
73132
allow_drag: bool,
74133
min_auto_bounds: PlotBounds,
75134
margin_fraction: Vec2,
135+
linked_axes: Option<LinkedAxisGroup>,
76136

77137
min_size: Vec2,
78138
width: Option<f32>,
@@ -101,6 +161,7 @@ impl Plot {
101161
allow_drag: true,
102162
min_auto_bounds: PlotBounds::NOTHING,
103163
margin_fraction: Vec2::splat(0.05),
164+
linked_axes: None,
104165

105166
min_size: Vec2::splat(64.0),
106167
width: None,
@@ -281,6 +342,13 @@ impl Plot {
281342
self
282343
}
283344

345+
/// Add a [`LinkedAxisGroup`] so that this plot will share the bounds with other plots that have this
346+
/// group assigned. A plot cannot belong to more than one group.
347+
pub fn link_axis(mut self, group: LinkedAxisGroup) -> Self {
348+
self.linked_axes = Some(group);
349+
self
350+
}
351+
284352
/// Interact with and add items to the plot and finally draw it.
285353
pub fn show<R>(self, ui: &mut Ui, build_fn: impl FnOnce(&mut PlotUi) -> R) -> InnerResponse<R> {
286354
let Self {
@@ -303,6 +371,7 @@ impl Plot {
303371
legend_config,
304372
show_background,
305373
show_axes,
374+
linked_axes,
306375
} = self;
307376

308377
// Determine the size of the plot in the UI
@@ -415,6 +484,22 @@ impl Plot {
415484
// --- Bound computation ---
416485
let mut bounds = *last_screen_transform.bounds();
417486

487+
// Transfer the bounds from a link group.
488+
if let Some(axes) = linked_axes.as_ref() {
489+
if let Some(linked_bounds) = axes.get() {
490+
if axes.link_x {
491+
bounds.min[0] = linked_bounds.min[0];
492+
bounds.max[0] = linked_bounds.max[0];
493+
}
494+
if axes.link_y {
495+
bounds.min[1] = linked_bounds.min[1];
496+
bounds.max[1] = linked_bounds.max[1];
497+
}
498+
// Turn off auto bounds to keep it from overriding what we just set.
499+
auto_bounds = false;
500+
}
501+
}
502+
418503
// Allow double clicking to reset to automatic bounds.
419504
auto_bounds |= response.double_clicked_by(PointerButton::Primary);
420505

@@ -431,7 +516,10 @@ impl Plot {
431516

432517
// Enforce equal aspect ratio.
433518
if let Some(data_aspect) = data_aspect {
434-
transform.set_aspect(data_aspect as f64);
519+
let preserve_y = linked_axes
520+
.as_ref()
521+
.map_or(false, |group| group.link_y && !group.link_x);
522+
transform.set_aspect(data_aspect as f64, preserve_y);
435523
}
436524

437525
// Dragging
@@ -484,6 +572,10 @@ impl Plot {
484572
hovered_entry = legend.get_hovered_entry_name();
485573
}
486574

575+
if let Some(group) = linked_axes.as_ref() {
576+
group.set(*transform.bounds());
577+
}
578+
487579
let memory = PlotMemory {
488580
auto_bounds,
489581
hovered_entry,
@@ -504,7 +596,7 @@ impl Plot {
504596
}
505597

506598
/// Provides methods to interact with a plot while building it. It is the single argument of the closure
507-
/// provided to `Plot::show`. See [`Plot`] for an example of how to use it.
599+
/// provided to [`Plot::show`]. See [`Plot`] for an example of how to use it.
508600
pub struct PlotUi {
509601
items: Vec<Box<dyn PlotItem>>,
510602
next_auto_color_idx: usize,

egui/src/widgets/plot/transform.rs

+11-4
Original file line numberDiff line numberDiff line change
@@ -273,13 +273,20 @@ impl ScreenTransform {
273273
(self.bounds.width() / rw) / (self.bounds.height() / rh)
274274
}
275275

276-
pub fn set_aspect(&mut self, aspect: f64) {
277-
let epsilon = 1e-5;
276+
/// Sets the aspect ratio by either expanding the x-axis or contracting the y-axis.
277+
pub fn set_aspect(&mut self, aspect: f64, preserve_y: bool) {
278278
let current_aspect = self.get_aspect();
279-
if current_aspect < aspect - epsilon {
279+
280+
let epsilon = 1e-5;
281+
if (current_aspect - aspect).abs() < epsilon {
282+
// Don't make any changes when the aspect is already almost correct.
283+
return;
284+
}
285+
286+
if preserve_y {
280287
self.bounds
281288
.expand_x((aspect / current_aspect - 1.0) * self.bounds.width() * 0.5);
282-
} else if current_aspect > aspect + epsilon {
289+
} else {
283290
self.bounds
284291
.expand_y((current_aspect / aspect - 1.0) * self.bounds.height() * 0.5);
285292
}

egui_demo_lib/src/apps/demo/plot_demo.rs

+79-1
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,78 @@ impl Widget for &mut LegendDemo {
300300
}
301301
}
302302

303+
#[derive(PartialEq)]
304+
struct LinkedAxisDemo {
305+
link_x: bool,
306+
link_y: bool,
307+
group: plot::LinkedAxisGroup,
308+
}
309+
310+
impl Default for LinkedAxisDemo {
311+
fn default() -> Self {
312+
let link_x = true;
313+
let link_y = false;
314+
Self {
315+
link_x,
316+
link_y,
317+
group: plot::LinkedAxisGroup::new(link_x, link_y),
318+
}
319+
}
320+
}
321+
322+
impl LinkedAxisDemo {
323+
fn line_with_slope(slope: f64) -> Line {
324+
Line::new(Values::from_explicit_callback(move |x| slope * x, .., 100))
325+
}
326+
fn sin() -> Line {
327+
Line::new(Values::from_explicit_callback(move |x| x.sin(), .., 100))
328+
}
329+
fn cos() -> Line {
330+
Line::new(Values::from_explicit_callback(move |x| x.cos(), .., 100))
331+
}
332+
333+
fn configure_plot(plot_ui: &mut plot::PlotUi) {
334+
plot_ui.line(LinkedAxisDemo::line_with_slope(0.5));
335+
plot_ui.line(LinkedAxisDemo::line_with_slope(1.0));
336+
plot_ui.line(LinkedAxisDemo::line_with_slope(2.0));
337+
plot_ui.line(LinkedAxisDemo::sin());
338+
plot_ui.line(LinkedAxisDemo::cos());
339+
}
340+
}
341+
342+
impl Widget for &mut LinkedAxisDemo {
343+
fn ui(self, ui: &mut Ui) -> Response {
344+
ui.horizontal(|ui| {
345+
ui.label("Linked axes:");
346+
ui.checkbox(&mut self.link_x, "X");
347+
ui.checkbox(&mut self.link_y, "Y");
348+
});
349+
self.group.set_link_x(self.link_x);
350+
self.group.set_link_y(self.link_y);
351+
ui.horizontal(|ui| {
352+
Plot::new("linked_axis_1")
353+
.data_aspect(1.0)
354+
.width(250.0)
355+
.height(250.0)
356+
.link_axis(self.group.clone())
357+
.show(ui, LinkedAxisDemo::configure_plot);
358+
Plot::new("linked_axis_2")
359+
.data_aspect(2.0)
360+
.width(150.0)
361+
.height(250.0)
362+
.link_axis(self.group.clone())
363+
.show(ui, LinkedAxisDemo::configure_plot);
364+
});
365+
Plot::new("linked_axis_3")
366+
.data_aspect(0.5)
367+
.width(250.0)
368+
.height(150.0)
369+
.link_axis(self.group.clone())
370+
.show(ui, LinkedAxisDemo::configure_plot)
371+
.response
372+
}
373+
}
374+
303375
#[derive(PartialEq, Default)]
304376
struct ItemsDemo {
305377
texture: Option<egui::TextureHandle>,
@@ -639,11 +711,12 @@ enum Panel {
639711
Charts,
640712
Items,
641713
Interaction,
714+
LinkedAxes,
642715
}
643716

644717
impl Default for Panel {
645718
fn default() -> Self {
646-
Self::Charts
719+
Self::Lines
647720
}
648721
}
649722

@@ -655,6 +728,7 @@ pub struct PlotDemo {
655728
charts_demo: ChartsDemo,
656729
items_demo: ItemsDemo,
657730
interaction_demo: InteractionDemo,
731+
linked_axes_demo: LinkedAxisDemo,
658732
open_panel: Panel,
659733
}
660734

@@ -698,6 +772,7 @@ impl super::View for PlotDemo {
698772
ui.selectable_value(&mut self.open_panel, Panel::Charts, "Charts");
699773
ui.selectable_value(&mut self.open_panel, Panel::Items, "Items");
700774
ui.selectable_value(&mut self.open_panel, Panel::Interaction, "Interaction");
775+
ui.selectable_value(&mut self.open_panel, Panel::LinkedAxes, "Linked Axes");
701776
});
702777
ui.separator();
703778

@@ -720,6 +795,9 @@ impl super::View for PlotDemo {
720795
Panel::Interaction => {
721796
ui.add(&mut self.interaction_demo);
722797
}
798+
Panel::LinkedAxes => {
799+
ui.add(&mut self.linked_axes_demo);
800+
}
723801
}
724802
}
725803
}

0 commit comments

Comments
 (0)