From 2f9a4ca6e8434aaee3569095c0743b73cba26c44 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 23 Jan 2024 09:47:47 +0100 Subject: [PATCH] Make `egui_plot::PlotMemory` public (#3871) This allows users to e.g. read/write the plot bounds/transform before and after showing a `Plot`. --- crates/egui_demo_lib/src/demo/plot_demo.rs | 15 +++- crates/egui_plot/src/legend.rs | 2 +- crates/egui_plot/src/lib.rs | 92 ++++++++-------------- crates/egui_plot/src/memory.rs | 67 ++++++++++++---- crates/egui_plot/src/transform.rs | 85 ++++++++++++++------ 5 files changed, 163 insertions(+), 98 deletions(-) diff --git a/crates/egui_demo_lib/src/demo/plot_demo.rs b/crates/egui_demo_lib/src/demo/plot_demo.rs index 27f48f7ec4c..fef3b4fc6dd 100644 --- a/crates/egui_demo_lib/src/demo/plot_demo.rs +++ b/crates/egui_demo_lib/src/demo/plot_demo.rs @@ -769,7 +769,20 @@ struct InteractionDemo {} impl InteractionDemo { #[allow(clippy::unused_self)] fn ui(&mut self, ui: &mut Ui) -> Response { - let plot = Plot::new("interaction_demo").height(300.0); + let id = ui.make_persistent_id("interaction_demo"); + + // This demonstrates how to read info about the plot _before_ showing it: + let plot_memory = egui_plot::PlotMemory::load(ui.ctx(), id); + if let Some(plot_memory) = plot_memory { + let bounds = plot_memory.bounds(); + ui.label(format!( + "plot bounds: min: {:.02?}, max: {:.02?}", + bounds.min(), + bounds.max() + )); + } + + let plot = Plot::new("interaction_demo").id(id).height(300.0); let PlotResponse { response, diff --git a/crates/egui_plot/src/legend.rs b/crates/egui_plot/src/legend.rs index 339c5d28dbe..a3b353e994c 100644 --- a/crates/egui_plot/src/legend.rs +++ b/crates/egui_plot/src/legend.rs @@ -226,7 +226,7 @@ impl LegendWidget { } // Get the name of the hovered items. - pub fn hovered_entry_name(&self) -> Option { + pub fn hovered_item_name(&self) -> Option { self.entries .iter() .find(|(_, entry)| entry.hovered) diff --git a/crates/egui_plot/src/lib.rs b/crates/egui_plot/src/lib.rs index ec048297193..55ec7007a15 100644 --- a/crates/egui_plot/src/lib.rs +++ b/crates/egui_plot/src/lib.rs @@ -1,9 +1,17 @@ //! Simple plotting library for [`egui`](https://github.com/emilk/egui). //! +//! Check out [`Plot`] for how to get started. +//! //! ## Feature flags #![cfg_attr(feature = "document-features", doc = document_features::document_features!())] //! +mod axis; +mod items; +mod legend; +mod memory; +mod transform; + use std::{ops::RangeInclusive, sync::Arc}; use egui::ahash::HashMap; @@ -26,11 +34,7 @@ pub use transform::{PlotBounds, PlotTransform}; use items::{horizontal_line, rulers_color, vertical_line}; pub use axis::{Axis, AxisHints, HPlacement, Placement, VPlacement}; - -mod axis; -mod items; -mod legend; -mod transform; +pub use memory::PlotMemory; type LabelFormatterFn = dyn Fn(&str, &PlotPoint) -> String; type LabelFormatter = Option>; @@ -77,44 +81,6 @@ impl Default for CoordinatesFormatter { const MIN_LINE_SPACING_IN_POINTS: f64 = 6.0; // TODO(emilk): large enough for a wide label -/// Information about the plot that has to persist between frames. -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[derive(Clone)] -struct PlotMemory { - /// Indicates if the plot uses automatic bounds. This is disengaged whenever the user modifies - /// the bounds, for example by moving or zooming. - auto_bounds: Vec2b, - - hovered_entry: Option, - hidden_items: ahash::HashSet, - last_plot_transform: PlotTransform, - - /// Allows to remember the first click position when performing a boxed zoom - last_click_pos_for_zoom: Option, -} - -#[cfg(feature = "serde")] -impl PlotMemory { - pub fn load(ctx: &Context, id: Id) -> Option { - ctx.data_mut(|d| d.get_persisted(id)) - } - - pub fn store(self, ctx: &Context, id: Id) { - ctx.data_mut(|d| d.insert_persisted(id, self)); - } -} - -#[cfg(not(feature = "serde"))] -impl PlotMemory { - pub fn load(ctx: &Context, id: Id) -> Option { - ctx.data_mut(|d| d.get_temp(id)) - } - - pub fn store(self, ctx: &Context, id: Id) { - ctx.data_mut(|d| d.insert_temp(id, self)); - } -} - // ---------------------------------------------------------------------------- /// Indicates a vertical or horizontal cursor line in plot coordinates. @@ -166,6 +132,7 @@ pub struct PlotResponse { /// ``` /// # egui::__run_test_ui(|ui| { /// use egui_plot::{Line, Plot, PlotPoints}; +/// /// let sin: PlotPoints = (0..1000).map(|i| { /// let x = i as f64 * 0.01; /// [x, x.sin()] @@ -176,6 +143,7 @@ pub struct PlotResponse { /// ``` pub struct Plot { id_source: Id, + id: Option, center_axis: Vec2b, allow_zoom: Vec2b, @@ -218,6 +186,7 @@ impl Plot { pub fn new(id_source: impl std::hash::Hash) -> Self { Self { id_source: Id::new(id_source), + id: None, center_axis: false.into(), allow_zoom: true.into(), @@ -256,6 +225,17 @@ impl Plot { } } + /// Set an explicit (global) id for the plot. + /// + /// This will override the id set by [`Self::new`]. + /// + /// This is the same `Id` that can be used for [`PlotMemory::load`]. + #[inline] + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); + self + } + /// width / height ratio of the data. /// For instance, it can be useful to set this to `1.0` for when the two axes show the same /// unit. @@ -716,6 +696,7 @@ impl Plot { ) -> PlotResponse { let Self { id_source, + id, center_axis, allow_zoom, allow_drag, @@ -850,7 +831,7 @@ impl Plot { let rect = plot_rect; // Load or initialize the memory. - let plot_id = ui.make_persistent_id(id_source); + let plot_id = id.unwrap_or_else(|| ui.make_persistent_id(id_source)); ui.ctx().check_for_id_clash(plot_id, rect, "Plot"); let memory = if reset { if let Some((name, _)) = linked_axes.as_ref() { @@ -865,22 +846,17 @@ impl Plot { } .unwrap_or_else(|| PlotMemory { auto_bounds: default_auto_bounds, - hovered_entry: None, + hovered_item: None, hidden_items: Default::default(), - last_plot_transform: PlotTransform::new( - rect, - min_auto_bounds, - center_axis.x, - center_axis.y, - ), + transform: PlotTransform::new(rect, min_auto_bounds, center_axis.x, center_axis.y), last_click_pos_for_zoom: None, }); let PlotMemory { mut auto_bounds, - mut hovered_entry, + mut hovered_item, mut hidden_items, - last_plot_transform, + transform: last_plot_transform, mut last_click_pos_for_zoom, } = memory; @@ -919,14 +895,14 @@ impl Plot { let legend = legend_config .and_then(|config| LegendWidget::try_new(rect, config, &items, &hidden_items)); // Don't show hover cursor when hovering over legend. - if hovered_entry.is_some() { + if hovered_item.is_some() { show_x = false; show_y = false; } // Remove the deselected items. items.retain(|item| !hidden_items.contains(item.name())); // Highlight the hovered items. - if let Some(hovered_name) = &hovered_entry { + if let Some(hovered_name) = &hovered_item { items .iter_mut() .filter(|entry| entry.name() == hovered_name) @@ -1211,7 +1187,7 @@ impl Plot { if let Some(mut legend) = legend { ui.add(&mut legend); hidden_items = legend.hidden_items(); - hovered_entry = legend.hovered_entry_name(); + hovered_item = legend.hovered_item_name(); } if let Some((id, _)) = linked_cursors.as_ref() { @@ -1242,9 +1218,9 @@ impl Plot { let memory = PlotMemory { auto_bounds, - hovered_entry, + hovered_item, hidden_items, - last_plot_transform: transform, + transform, last_click_pos_for_zoom, }; memory.store(ui.ctx(), plot_id); diff --git a/crates/egui_plot/src/memory.rs b/crates/egui_plot/src/memory.rs index 3e47a508287..c334f115274 100644 --- a/crates/egui_plot/src/memory.rs +++ b/crates/egui_plot/src/memory.rs @@ -1,33 +1,72 @@ -use epaint::Pos2; +use egui::{ahash, Context, Id, Pos2, Vec2b}; -use crate::{Context, Id}; - -use super::{transform::ScreenTransform, AxisBools}; +use crate::{PlotBounds, PlotTransform}; /// Information about the plot that has to persist between frames. #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[derive(Clone)] -pub(super) struct PlotMemory { - /// Indicates if the user has modified the bounds, for example by moving or zooming, - /// or if the bounds should be calculated based by included point or auto bounds. - pub(super) bounds_modified: AxisBools, +pub struct PlotMemory { + /// Indicates if the plot uses automatic bounds. + /// + /// This is set to `false` whenever the user modifies + /// the bounds, for example by moving or zooming. + pub auto_bounds: Vec2b, - pub(super) hovered_entry: Option, + /// Which item is hovered? + pub hovered_item: Option, - pub(super) hidden_items: ahash::HashSet, + /// Which items _not_ to show? + pub hidden_items: ahash::HashSet, - pub(super) last_screen_transform: ScreenTransform, + /// The transform from last frame. + pub(crate) transform: PlotTransform, /// Allows to remember the first click position when performing a boxed zoom - pub(super) last_click_pos_for_zoom: Option, + pub(crate) last_click_pos_for_zoom: Option, +} + +impl PlotMemory { + #[inline] + pub fn transform(&self) -> PlotTransform { + self.transform + } + + #[inline] + pub fn set_transform(&mut self, t: PlotTransform) { + self.transform = t; + } + + /// Plot-space bounds. + #[inline] + pub fn bounds(&self) -> &PlotBounds { + self.transform.bounds() + } + + /// Plot-space bounds. + #[inline] + pub fn set_bounds(&mut self, bounds: PlotBounds) { + self.transform.set_bounds(bounds); + } +} + +#[cfg(feature = "serde")] +impl PlotMemory { + pub fn load(ctx: &Context, id: Id) -> Option { + ctx.data_mut(|d| d.get_persisted(id)) + } + + pub fn store(self, ctx: &Context, id: Id) { + ctx.data_mut(|d| d.insert_persisted(id, self)); + } } +#[cfg(not(feature = "serde"))] impl PlotMemory { pub fn load(ctx: &Context, id: Id) -> Option { - ctx.data().get_persisted(id) + ctx.data_mut(|d| d.get_temp(id)) } pub fn store(self, ctx: &Context, id: Id) { - ctx.data().insert_persisted(id, self); + ctx.data_mut(|d| d.insert_temp(id, self)); } } diff --git a/crates/egui_plot/src/transform.rs b/crates/egui_plot/src/transform.rs index 623c015bce4..722df5fcc7d 100644 --- a/crates/egui_plot/src/transform.rs +++ b/crates/egui_plot/src/transform.rs @@ -4,6 +4,7 @@ use super::PlotPoint; use crate::*; /// 2D bounding box of f64 precision. +/// /// The range of data values we show. #[derive(Clone, Copy, PartialEq, Debug)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -18,25 +19,30 @@ impl PlotBounds { max: [-f64::INFINITY; 2], }; + #[inline] pub fn from_min_max(min: [f64; 2], max: [f64; 2]) -> Self { Self { min, max } } + #[inline] pub fn min(&self) -> [f64; 2] { self.min } + #[inline] pub fn max(&self) -> [f64; 2] { self.max } - pub(crate) fn new_symmetrical(half_extent: f64) -> Self { + #[inline] + pub fn new_symmetrical(half_extent: f64) -> Self { Self { min: [-half_extent; 2], max: [half_extent; 2], } } + #[inline] pub fn is_finite(&self) -> bool { self.min[0].is_finite() && self.min[1].is_finite() @@ -44,34 +50,42 @@ impl PlotBounds { && self.max[1].is_finite() } + #[inline] pub fn is_finite_x(&self) -> bool { self.min[0].is_finite() && self.max[0].is_finite() } + #[inline] pub fn is_finite_y(&self) -> bool { self.min[1].is_finite() && self.max[1].is_finite() } + #[inline] pub fn is_valid(&self) -> bool { self.is_finite() && self.width() > 0.0 && self.height() > 0.0 } + #[inline] pub fn is_valid_x(&self) -> bool { self.is_finite_x() && self.width() > 0.0 } + #[inline] pub fn is_valid_y(&self) -> bool { self.is_finite_y() && self.height() > 0.0 } + #[inline] pub fn width(&self) -> f64 { self.max[0] - self.min[0] } + #[inline] pub fn height(&self) -> f64 { self.max[1] - self.min[1] } + #[inline] pub fn center(&self) -> PlotPoint { [ (self.min[0] + self.max[0]) / 2.0, @@ -81,107 +95,127 @@ impl PlotBounds { } /// Expand to include the given (x,y) value - pub(crate) fn extend_with(&mut self, value: &PlotPoint) { + #[inline] + pub fn extend_with(&mut self, value: &PlotPoint) { self.extend_with_x(value.x); self.extend_with_y(value.y); } /// Expand to include the given x coordinate - pub(crate) fn extend_with_x(&mut self, x: f64) { + #[inline] + pub fn extend_with_x(&mut self, x: f64) { self.min[0] = self.min[0].min(x); self.max[0] = self.max[0].max(x); } /// Expand to include the given y coordinate - pub(crate) fn extend_with_y(&mut self, y: f64) { + #[inline] + pub fn extend_with_y(&mut self, y: f64) { self.min[1] = self.min[1].min(y); self.max[1] = self.max[1].max(y); } - pub(crate) fn expand_x(&mut self, pad: f64) { + #[inline] + pub fn expand_x(&mut self, pad: f64) { self.min[0] -= pad; self.max[0] += pad; } - pub(crate) fn expand_y(&mut self, pad: f64) { + #[inline] + pub fn expand_y(&mut self, pad: f64) { self.min[1] -= pad; self.max[1] += pad; } - pub(crate) fn merge_x(&mut self, other: &Self) { + #[inline] + pub fn merge_x(&mut self, other: &Self) { self.min[0] = self.min[0].min(other.min[0]); self.max[0] = self.max[0].max(other.max[0]); } - pub(crate) fn merge_y(&mut self, other: &Self) { + #[inline] + pub fn merge_y(&mut self, other: &Self) { self.min[1] = self.min[1].min(other.min[1]); self.max[1] = self.max[1].max(other.max[1]); } - pub(crate) fn set_x(&mut self, other: &Self) { + #[inline] + pub fn set_x(&mut self, other: &Self) { self.min[0] = other.min[0]; self.max[0] = other.max[0]; } - pub(crate) fn set_y(&mut self, other: &Self) { + #[inline] + pub fn set_y(&mut self, other: &Self) { self.min[1] = other.min[1]; self.max[1] = other.max[1]; } - pub(crate) fn merge(&mut self, other: &Self) { + #[inline] + pub fn merge(&mut self, other: &Self) { self.min[0] = self.min[0].min(other.min[0]); self.min[1] = self.min[1].min(other.min[1]); self.max[0] = self.max[0].max(other.max[0]); self.max[1] = self.max[1].max(other.max[1]); } - pub(crate) fn translate_x(&mut self, delta: f64) { + #[inline] + pub fn translate_x(&mut self, delta: f64) { self.min[0] += delta; self.max[0] += delta; } - pub(crate) fn translate_y(&mut self, delta: f64) { + #[inline] + pub fn translate_y(&mut self, delta: f64) { self.min[1] += delta; self.max[1] += delta; } - pub(crate) fn translate(&mut self, delta: Vec2) { + #[inline] + pub fn translate(&mut self, delta: Vec2) { self.translate_x(delta.x as f64); self.translate_y(delta.y as f64); } - pub(crate) fn zoom(&mut self, zoom_factor: Vec2, center: PlotPoint) { + #[inline] + pub fn zoom(&mut self, zoom_factor: Vec2, center: PlotPoint) { self.min[0] = center.x + (self.min[0] - center.x) / (zoom_factor.x as f64); self.max[0] = center.x + (self.max[0] - center.x) / (zoom_factor.x as f64); self.min[1] = center.y + (self.min[1] - center.y) / (zoom_factor.y as f64); self.max[1] = center.y + (self.max[1] - center.y) / (zoom_factor.y as f64); } - pub(crate) fn add_relative_margin_x(&mut self, margin_fraction: Vec2) { + #[inline] + pub fn add_relative_margin_x(&mut self, margin_fraction: Vec2) { let width = self.width().max(0.0); self.expand_x(margin_fraction.x as f64 * width); } - pub(crate) fn add_relative_margin_y(&mut self, margin_fraction: Vec2) { + #[inline] + pub fn add_relative_margin_y(&mut self, margin_fraction: Vec2) { let height = self.height().max(0.0); self.expand_y(margin_fraction.y as f64 * height); } - pub(crate) fn range_x(&self) -> RangeInclusive { + #[inline] + pub fn range_x(&self) -> RangeInclusive { self.min[0]..=self.max[0] } - pub(crate) fn range_y(&self) -> RangeInclusive { + #[inline] + pub fn range_y(&self) -> RangeInclusive { self.min[1]..=self.max[1] } - pub(crate) fn make_x_symmetrical(&mut self) { + #[inline] + pub fn make_x_symmetrical(&mut self) { let x_abs = self.min[0].abs().max(self.max[0].abs()); self.min[0] = -x_abs; self.max[0] = x_abs; } - pub(crate) fn make_y_symmetrical(&mut self) { + #[inline] + pub fn make_y_symmetrical(&mut self) { let y_abs = self.min[1].abs().max(self.max[1].abs()); self.min[1] = -y_abs; self.max[1] = y_abs; @@ -232,20 +266,23 @@ impl PlotTransform { } /// ui-space rectangle. + #[inline] pub fn frame(&self) -> &Rect { &self.frame } /// Plot-space bounds. + #[inline] pub fn bounds(&self) -> &PlotBounds { &self.bounds } - pub(crate) fn set_bounds(&mut self, bounds: PlotBounds) { + #[inline] + pub fn set_bounds(&mut self, bounds: PlotBounds) { self.bounds = bounds; } - pub(crate) fn translate_bounds(&mut self, mut delta_pos: Vec2) { + pub fn translate_bounds(&mut self, mut delta_pos: Vec2) { if self.x_centered { delta_pos.x = 0.; } @@ -258,7 +295,7 @@ impl PlotTransform { } /// Zoom by a relative factor with the given screen position as center. - pub(crate) fn zoom(&mut self, zoom_factor: Vec2, center: Pos2) { + pub fn zoom(&mut self, zoom_factor: Vec2, center: Pos2) { let center = self.value_from_position(center); let mut new_bounds = self.bounds;