Improved plot groups and bounds handling (#2410)

* improve plot groups and bounds handling

* changelog entry

* fix potential deadlock

* fix two more potential deadlocks

* syntax fix

* move changelog entry

* move category

* Update crates/egui/src/widgets/plot/mod.rs

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>

* Update crates/egui_demo_lib/src/demo/plot_demo.rs

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>

* clean up suggestions

* address comments

* use the new methods

* fix locked bounds

* Sync bounds_modified along with the bounds themselves

* move changelog entry

* Remove set_bounds_auto - not necessary any more

* add a comment about bounds modifications

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
Co-authored-by: Jackson Kruger <jackson@farprobe.com>
This commit is contained in:
Sven Niederberger 2023-04-18 16:27:00 +02:00 committed by GitHub
parent 8a2cfbd131
commit 69b568aeb4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 151 additions and 195 deletions

View File

@ -6,6 +6,8 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG
## Unreleased ## Unreleased
* Add `char_limit` to `TextEdit` singleline mode to limit the amount of characters * Add `char_limit` to `TextEdit` singleline mode to limit the amount of characters
* ⚠️ BREAKING: `Plot::link_axis` and `Plot::link_cursor` now take the name of the group ([#2410](https://github.com/emilk/egui/pull/2410)).
## 0.21.0 - 2023-02-08 - Deadlock fix and style customizability ## 0.21.0 - 2023-02-08 - Deadlock fix and style customizability
* ⚠️ BREAKING: `egui::Context` now use closures for locking ([#2625](https://github.com/emilk/egui/pull/2625)): * ⚠️ BREAKING: `egui::Context` now use closures for locking ([#2625](https://github.com/emilk/egui/pull/2625)):

View File

@ -1,10 +1,7 @@
//! Simple plotting library. //! Simple plotting library.
use std::{ use ahash::HashMap;
cell::{Cell, RefCell}, use std::ops::RangeInclusive;
ops::RangeInclusive,
rc::Rc,
};
use crate::*; use crate::*;
use epaint::util::FloatOrd; use epaint::util::FloatOrd;
@ -35,9 +32,11 @@ type AxisFormatter = Option<Box<AxisFormatterFn>>;
type GridSpacerFn = dyn Fn(GridInput) -> Vec<GridMark>; type GridSpacerFn = dyn Fn(GridInput) -> Vec<GridMark>;
type GridSpacer = Box<GridSpacerFn>; type GridSpacer = Box<GridSpacerFn>;
type CoordinatesFormatterFn = dyn Fn(&PlotPoint, &PlotBounds) -> String;
/// Specifies the coordinates formatting when passed to [`Plot::coordinates_formatter`]. /// Specifies the coordinates formatting when passed to [`Plot::coordinates_formatter`].
pub struct CoordinatesFormatter { pub struct CoordinatesFormatter {
function: Box<dyn Fn(&PlotPoint, &PlotBounds) -> String>, function: Box<CoordinatesFormatterFn>,
} }
impl CoordinatesFormatter { impl CoordinatesFormatter {
@ -126,120 +125,23 @@ enum Cursor {
} }
/// Contains the cursors drawn for a plot widget in a single frame. /// Contains the cursors drawn for a plot widget in a single frame.
#[derive(PartialEq)] #[derive(PartialEq, Clone)]
struct PlotFrameCursors { struct PlotFrameCursors {
id: Id, id: Id,
cursors: Vec<Cursor>, cursors: Vec<Cursor>,
} }
/// Defines how multiple plots share the same cursor for one or both of their axes. Can be added while building #[derive(Default, Clone)]
/// a plot with [`Plot::link_cursor`]. Contains an internal state, meaning that this object should be stored by struct CursorLinkGroups(HashMap<Id, Vec<PlotFrameCursors>>);
/// the user between frames.
#[derive(Clone, PartialEq)] #[derive(Clone)]
pub struct LinkedCursorsGroup { struct LinkedBounds {
link_x: bool, bounds: PlotBounds,
link_y: bool, bounds_modified: AxisBools,
// We store the cursors drawn for each linked plot. Each time a plot in the group is drawn, the
// cursors due to hovering it drew are appended to `frames`, so lower indices are older.
// When a plot is redrawn all entries older than its previous entry are removed. This avoids
// unbounded growth and also ensures entries for plots which are not longer part of the group
// gets removed.
frames: Rc<RefCell<Vec<PlotFrameCursors>>>,
} }
impl LinkedCursorsGroup { #[derive(Default, Clone)]
pub fn new(link_x: bool, link_y: bool) -> Self { struct BoundsLinkGroups(HashMap<Id, LinkedBounds>);
Self {
link_x,
link_y,
frames: Rc::new(RefCell::new(Vec::new())),
}
}
/// Only link the cursor for the x-axis.
pub fn x() -> Self {
Self::new(true, false)
}
/// Only link the cursor for the y-axis.
pub fn y() -> Self {
Self::new(false, true)
}
/// Link the cursors for both axes.
pub fn both() -> Self {
Self::new(true, true)
}
/// Change whether the cursor for 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 cursor for 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;
}
}
// ----------------------------------------------------------------------------
/// 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<Cell<Option<PlotBounds>>>,
}
impl LinkedAxisGroup {
pub fn new(link_x: bool, link_y: bool) -> Self {
Self {
link_x,
link_y,
bounds: Rc::new(Cell::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.get()
}
fn set(&self, bounds: PlotBounds) {
self.bounds.set(Some(bounds));
}
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -272,8 +174,8 @@ pub struct Plot {
min_auto_bounds: PlotBounds, min_auto_bounds: PlotBounds,
margin_fraction: Vec2, margin_fraction: Vec2,
boxed_zoom_pointer_button: PointerButton, boxed_zoom_pointer_button: PointerButton,
linked_axes: Option<LinkedAxisGroup>, linked_axes: Option<(Id, AxisBools)>,
linked_cursors: Option<LinkedCursorsGroup>, linked_cursors: Option<(Id, AxisBools)>,
min_size: Vec2, min_size: Vec2,
width: Option<f32>, width: Option<f32>,
@ -617,17 +519,29 @@ impl Plot {
self self
} }
/// Add a [`LinkedAxisGroup`] so that this plot will share the bounds with other plots that have this /// Add this plot to an axis link group so that this plot will share the bounds with other plots in the
/// group assigned. A plot cannot belong to more than one group. /// same group. A plot cannot belong to more than one axis group.
pub fn link_axis(mut self, group: LinkedAxisGroup) -> Self { pub fn link_axis(mut self, group_id: impl Into<Id>, link_x: bool, link_y: bool) -> Self {
self.linked_axes = Some(group); self.linked_axes = Some((
group_id.into(),
AxisBools {
x: link_x,
y: link_y,
},
));
self self
} }
/// Add a [`LinkedCursorsGroup`] so that this plot will share the bounds with other plots that have this /// Add this plot to a cursor link group so that this plot will share the cursor position with other plots
/// group assigned. A plot cannot belong to more than one group. /// in the same group. A plot cannot belong to more than one cursor group.
pub fn link_cursor(mut self, group: LinkedCursorsGroup) -> Self { pub fn link_cursor(mut self, group_id: impl Into<Id>, link_x: bool, link_y: bool) -> Self {
self.linked_cursors = Some(group); self.linked_cursors = Some((
group_id.into(),
AxisBools {
x: link_x,
y: link_y,
},
));
self self
} }
@ -720,10 +634,13 @@ impl Plot {
let plot_id = ui.make_persistent_id(id_source); let plot_id = ui.make_persistent_id(id_source);
ui.ctx().check_for_id_clash(plot_id, rect, "Plot"); ui.ctx().check_for_id_clash(plot_id, rect, "Plot");
let memory = if reset { let memory = if reset {
if let Some(axes) = linked_axes.as_ref() { if let Some((name, _)) = linked_axes.as_ref() {
axes.bounds.set(None); ui.memory_mut(|memory| {
let link_groups: &mut BoundsLinkGroups =
memory.data.get_temp_mut_or_default(Id::null());
link_groups.0.remove(name);
});
}; };
None None
} else { } else {
PlotMemory::load(ui.ctx(), plot_id) PlotMemory::load(ui.ctx(), plot_id)
@ -742,7 +659,7 @@ impl Plot {
}); });
let PlotMemory { let PlotMemory {
bounds_modified, mut bounds_modified,
mut hovered_entry, mut hovered_entry,
mut hidden_items, mut hidden_items,
last_screen_transform, last_screen_transform,
@ -754,8 +671,8 @@ impl Plot {
items: Vec::new(), items: Vec::new(),
next_auto_color_idx: 0, next_auto_color_idx: 0,
last_screen_transform, last_screen_transform,
bounds_modified,
response, response,
bounds_modifications: Vec::new(),
ctx: ui.ctx().clone(), ctx: ui.ctx().clone(),
}; };
let inner = build_fn(&mut plot_ui); let inner = build_fn(&mut plot_ui);
@ -763,7 +680,7 @@ impl Plot {
mut items, mut items,
mut response, mut response,
last_screen_transform, last_screen_transform,
mut bounds_modified, bounds_modifications,
.. ..
} = plot_ui; } = plot_ui;
@ -801,52 +718,71 @@ impl Plot {
let mut bounds = *last_screen_transform.bounds(); let mut bounds = *last_screen_transform.bounds();
// Find the cursors from other plots we need to draw // Find the cursors from other plots we need to draw
let draw_cursors: Vec<Cursor> = if let Some(group) = linked_cursors.as_ref() { let draw_cursors: Vec<Cursor> = if let Some((id, _)) = linked_cursors.as_ref() {
let mut frames = group.frames.borrow_mut(); ui.memory_mut(|memory| {
let frames: &mut CursorLinkGroups = memory.data.get_temp_mut_or_default(Id::null());
let cursors = frames.0.entry(*id).or_default();
// Look for our previous frame // Look for our previous frame
let index = frames let index = cursors
.iter() .iter()
.enumerate() .enumerate()
.find(|(_, frame)| frame.id == plot_id) .find(|(_, frame)| frame.id == plot_id)
.map(|(i, _)| i); .map(|(i, _)| i);
// Remove our previous frame and all older frames as these are no longer displayed. This avoids // Remove our previous frame and all older frames as these are no longer displayed. This avoids
// unbounded growth, as we add an entry each time we draw a plot. // unbounded growth, as we add an entry each time we draw a plot.
index.map(|index| frames.drain(0..=index)); index.map(|index| cursors.drain(0..=index));
// Gather all cursors of the remaining frames. This will be all the cursors of the // Gather all cursors of the remaining frames. This will be all the cursors of the
// other plots in the group. We want to draw these in the current plot too. // other plots in the group. We want to draw these in the current plot too.
frames cursors
.iter() .iter()
.flat_map(|frame| frame.cursors.iter().copied()) .flat_map(|frame| frame.cursors.iter().copied())
.collect() .collect()
})
} else { } else {
Vec::new() Vec::new()
}; };
// Transfer the bounds from a link group. // Transfer the bounds from a link group.
if let Some(axes) = linked_axes.as_ref() { if let Some((id, axes)) = linked_axes.as_ref() {
if let Some(linked_bounds) = axes.get() { ui.memory_mut(|memory| {
if axes.link_x { let link_groups: &mut BoundsLinkGroups =
bounds.set_x(&linked_bounds); memory.data.get_temp_mut_or_default(Id::null());
// Mark the axis as modified to prevent it from being changed. if let Some(linked_bounds) = link_groups.0.get(id) {
bounds_modified.x = true; if axes.x {
} bounds.set_x(&linked_bounds.bounds);
if axes.link_y { bounds_modified.x = linked_bounds.bounds_modified.x;
bounds.set_y(&linked_bounds); }
// Mark the axis as modified to prevent it from being changed. if axes.y {
bounds_modified.y = true; bounds.set_y(&linked_bounds.bounds);
} bounds_modified.y = linked_bounds.bounds_modified.y;
} }
};
});
}; };
// Allow double clicking to reset to the initial bounds? // Allow double clicking to reset to the initial bounds.
if allow_double_click_reset && response.double_clicked_by(PointerButton::Primary) { if allow_double_click_reset && response.double_clicked() {
bounds_modified = false.into(); bounds_modified = false.into();
} }
// Reset bounds to initial bounds if we haven't been modified. // Apply bounds modifications.
for modification in bounds_modifications {
match modification {
BoundsModification::Set(new_bounds) => {
bounds = new_bounds;
bounds_modified = true.into();
}
BoundsModification::Translate(delta) => {
bounds.translate(delta);
bounds_modified = true.into();
}
}
}
// Reset bounds to initial bounds if they haven't been modified.
if !bounds_modified.x { if !bounds_modified.x {
bounds.set_x(&min_auto_bounds); bounds.set_x(&min_auto_bounds);
} }
@ -861,7 +797,6 @@ impl Plot {
if auto_x || auto_y { if auto_x || auto_y {
for item in &items { for item in &items {
let item_bounds = item.bounds(); let item_bounds = item.bounds();
if auto_x { if auto_x {
bounds.merge_x(&item_bounds); bounds.merge_x(&item_bounds);
} }
@ -883,8 +818,8 @@ impl Plot {
// Enforce aspect ratio // Enforce aspect ratio
if let Some(data_aspect) = data_aspect { if let Some(data_aspect) = data_aspect {
if let Some(linked_axes) = &linked_axes { if let Some((_, linked_axes)) = &linked_axes {
let change_x = linked_axes.link_y && !linked_axes.link_x; let change_x = linked_axes.y && !linked_axes.x;
transform.set_aspect_by_changing_axis(data_aspect as f64, change_x); transform.set_aspect_by_changing_axis(data_aspect as f64, change_x);
} else if auto_bounds.any() { } else if auto_bounds.any() {
transform.set_aspect_by_expanding(data_aspect as f64); transform.set_aspect_by_expanding(data_aspect as f64);
@ -952,7 +887,8 @@ impl Plot {
} }
} }
if let Some(hover_pos) = response.hover_pos() { let hover_pos = response.hover_pos();
if let Some(hover_pos) = hover_pos {
if allow_zoom { if allow_zoom {
let zoom_factor = if data_aspect.is_some() { let zoom_factor = if data_aspect.is_some() {
Vec2::splat(ui.input(|i| i.zoom_delta())) Vec2::splat(ui.input(|i| i.zoom_delta()))
@ -987,8 +923,8 @@ impl Plot {
axis_formatters, axis_formatters,
show_axes, show_axes,
transform: transform.clone(), transform: transform.clone(),
draw_cursor_x: linked_cursors.as_ref().map_or(false, |group| group.link_x), draw_cursor_x: linked_cursors.as_ref().map_or(false, |(_, group)| group.x),
draw_cursor_y: linked_cursors.as_ref().map_or(false, |group| group.link_y), draw_cursor_y: linked_cursors.as_ref().map_or(false, |(_, group)| group.y),
draw_cursors, draw_cursors,
grid_spacers, grid_spacers,
sharp_grid_lines, sharp_grid_lines,
@ -1007,16 +943,31 @@ impl Plot {
hovered_entry = legend.hovered_entry_name(); hovered_entry = legend.hovered_entry_name();
} }
if let Some(group) = linked_cursors.as_ref() { if let Some((id, _)) = linked_cursors.as_ref() {
// Push the frame we just drew to the list of frames // Push the frame we just drew to the list of frames
group.frames.borrow_mut().push(PlotFrameCursors { ui.memory_mut(|memory| {
id: plot_id, let frames: &mut CursorLinkGroups = memory.data.get_temp_mut_or_default(Id::null());
cursors: plot_cursors, let cursors = frames.0.entry(*id).or_default();
cursors.push(PlotFrameCursors {
id: plot_id,
cursors: plot_cursors,
});
}); });
} }
if let Some(group) = linked_axes.as_ref() { if let Some((id, _)) = linked_axes.as_ref() {
group.set(*transform.bounds()); // Save the linked bounds.
ui.memory_mut(|memory| {
let link_groups: &mut BoundsLinkGroups =
memory.data.get_temp_mut_or_default(Id::null());
link_groups.0.insert(
*id,
LinkedBounds {
bounds: *transform.bounds(),
bounds_modified,
},
);
});
} }
let memory = PlotMemory { let memory = PlotMemory {
@ -1038,14 +989,21 @@ impl Plot {
} }
} }
/// User-requested modifications to the plot bounds. We collect them in the plot build function to later apply
/// them at the right time, as other modifications need to happen first.
enum BoundsModification {
Set(PlotBounds),
Translate(Vec2),
}
/// 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,
last_screen_transform: ScreenTransform, last_screen_transform: ScreenTransform,
bounds_modified: AxisBools,
response: Response, response: Response,
bounds_modifications: Vec<BoundsModification>,
ctx: Context, ctx: Context,
} }
@ -1071,14 +1029,14 @@ impl PlotUi {
/// Set the plot bounds. Can be useful for implementing alternative plot navigation methods. /// Set the plot bounds. Can be useful for implementing alternative plot navigation methods.
pub fn set_plot_bounds(&mut self, plot_bounds: PlotBounds) { pub fn set_plot_bounds(&mut self, plot_bounds: PlotBounds) {
self.last_screen_transform.set_bounds(plot_bounds); self.bounds_modifications
self.bounds_modified = true.into(); .push(BoundsModification::Set(plot_bounds));
} }
/// Move the plot bounds. Can be useful for implementing alternative plot navigation methods. /// Move the plot bounds. Can be useful for implementing alternative plot navigation methods.
pub fn translate_bounds(&mut self, delta_pos: Vec2) { pub fn translate_bounds(&mut self, delta_pos: Vec2) {
self.last_screen_transform.translate_bounds(delta_pos); self.bounds_modifications
self.bounds_modified = true.into(); .push(BoundsModification::Translate(delta_pos));
} }
/// Returns `true` if the plot area is currently hovered. /// Returns `true` if the plot area is currently hovered.
@ -1355,7 +1313,8 @@ impl PreparedPlot {
item.shapes(&mut plot_ui, transform, &mut shapes); item.shapes(&mut plot_ui, transform, &mut shapes);
} }
let cursors = if let Some(pointer) = response.hover_pos() { let hover_pos = response.hover_pos();
let cursors = if let Some(pointer) = hover_pos {
self.hover(ui, pointer, &mut shapes) self.hover(ui, pointer, &mut shapes)
} else { } else {
Vec::new() Vec::new()
@ -1396,7 +1355,8 @@ impl PreparedPlot {
painter.extend(shapes); painter.extend(shapes);
if let Some((corner, formatter)) = self.coordinates_formatter.as_ref() { if let Some((corner, formatter)) = self.coordinates_formatter.as_ref() {
if let Some(pointer) = response.hover_pos() { let hover_pos = response.hover_pos();
if let Some(pointer) = hover_pos {
let font_id = TextStyle::Monospace.resolve(ui.style()); let font_id = TextStyle::Monospace.resolve(ui.style());
let coordinate = transform.value_from_position(pointer); let coordinate = transform.value_from_position(pointer);
let text = formatter.format(&coordinate, transform.bounds()); let text = formatter.format(&coordinate, transform.bounds());

View File

@ -576,8 +576,6 @@ impl CustomAxisDemo {
struct LinkedAxisDemo { struct LinkedAxisDemo {
link_x: bool, link_x: bool,
link_y: bool, link_y: bool,
group: plot::LinkedAxisGroup,
cursor_group: plot::LinkedCursorsGroup,
link_cursor_x: bool, link_cursor_x: bool,
link_cursor_y: bool, link_cursor_y: bool,
} }
@ -591,8 +589,6 @@ impl Default for LinkedAxisDemo {
Self { Self {
link_x, link_x,
link_y, link_y,
group: plot::LinkedAxisGroup::new(link_x, link_y),
cursor_group: plot::LinkedCursorsGroup::new(link_cursor_x, link_cursor_y),
link_cursor_x, link_cursor_x,
link_cursor_y, link_cursor_y,
} }
@ -638,37 +634,35 @@ impl LinkedAxisDemo {
ui.checkbox(&mut self.link_x, "X"); ui.checkbox(&mut self.link_x, "X");
ui.checkbox(&mut self.link_y, "Y"); 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| { ui.horizontal(|ui| {
ui.label("Linked cursors:"); ui.label("Linked cursors:");
ui.checkbox(&mut self.link_cursor_x, "X"); ui.checkbox(&mut self.link_cursor_x, "X");
ui.checkbox(&mut self.link_cursor_y, "Y"); ui.checkbox(&mut self.link_cursor_y, "Y");
}); });
self.cursor_group.set_link_x(self.link_cursor_x);
self.cursor_group.set_link_y(self.link_cursor_y); let link_group_id = ui.id().with("linked_demo");
ui.horizontal(|ui| { ui.horizontal(|ui| {
Plot::new("linked_axis_1") Plot::new("linked_axis_1")
.data_aspect(1.0) .data_aspect(1.0)
.width(250.0) .width(250.0)
.height(250.0) .height(250.0)
.link_axis(self.group.clone()) .link_axis(link_group_id, self.link_x, self.link_y)
.link_cursor(self.cursor_group.clone()) .link_cursor(link_group_id, self.link_cursor_x, self.link_cursor_y)
.show(ui, LinkedAxisDemo::configure_plot); .show(ui, LinkedAxisDemo::configure_plot);
Plot::new("linked_axis_2") Plot::new("linked_axis_2")
.data_aspect(2.0) .data_aspect(2.0)
.width(150.0) .width(150.0)
.height(250.0) .height(250.0)
.link_axis(self.group.clone()) .link_axis(link_group_id, self.link_x, self.link_y)
.link_cursor(self.cursor_group.clone()) .link_cursor(link_group_id, self.link_cursor_x, self.link_cursor_y)
.show(ui, LinkedAxisDemo::configure_plot); .show(ui, LinkedAxisDemo::configure_plot);
}); });
Plot::new("linked_axis_3") Plot::new("linked_axis_3")
.data_aspect(0.5) .data_aspect(0.5)
.width(250.0) .width(250.0)
.height(150.0) .height(150.0)
.link_axis(self.group.clone()) .link_axis(link_group_id, self.link_x, self.link_y)
.link_cursor(self.cursor_group.clone()) .link_cursor(link_group_id, self.link_cursor_x, self.link_cursor_y)
.show(ui, LinkedAxisDemo::configure_plot) .show(ui, LinkedAxisDemo::configure_plot)
.response .response
} }