From dbb93ee6f5bfeb7ba51f6f300d906a9964d02ba9 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Fri, 8 Aug 2025 21:03:50 +0200 Subject: [PATCH] feat: custom data with id and hashmap for userdata in plot --- demo/src/plot_demo.rs | 151 ++++++++++++++++++++- demo/tests/snapshots/demos/Charts.png | 4 +- demo/tests/snapshots/demos/Custom Axes.png | 4 +- demo/tests/snapshots/demos/Interaction.png | 4 +- demo/tests/snapshots/demos/Items.png | 4 +- demo/tests/snapshots/demos/Legend.png | 4 +- demo/tests/snapshots/demos/Lines.png | 4 +- demo/tests/snapshots/demos/Linked Axes.png | 4 +- demo/tests/snapshots/demos/Markers.png | 4 +- demo/tests/snapshots/demos/Userdata.png | 3 + demo/tests/snapshots/light_mode.png | 4 +- demo/tests/snapshots/scale_0.50.png | 4 +- demo/tests/snapshots/scale_1.00.png | 4 +- demo/tests/snapshots/scale_1.39.png | 4 +- demo/tests/snapshots/scale_2.00.png | 4 +- egui_plot/src/items/mod.rs | 22 +-- egui_plot/src/items/values.rs | 4 +- egui_plot/src/lib.rs | 11 +- 18 files changed, 201 insertions(+), 42 deletions(-) create mode 100644 demo/tests/snapshots/demos/Userdata.png diff --git a/demo/src/plot_demo.rs b/demo/src/plot_demo.rs index 01d8e219..d9434624 100644 --- a/demo/src/plot_demo.rs +++ b/demo/src/plot_demo.rs @@ -1,8 +1,10 @@ +use std::collections::HashMap; use std::ops::RangeInclusive; use std::{f64::consts::TAU, sync::Arc}; +use egui::mutex::Mutex; use egui::{ - Checkbox, Color32, ComboBox, NumExt as _, Pos2, Response, ScrollArea, Stroke, TextWrapMode, + Checkbox, Color32, ComboBox, Id, NumExt as _, Pos2, Response, ScrollArea, Stroke, TextWrapMode, Vec2b, WidgetInfo, WidgetType, remap, vec2, }; @@ -24,6 +26,7 @@ enum Panel { Interaction, CustomAxes, LinkedAxes, + Userdata, } impl Default for Panel { @@ -44,6 +47,7 @@ pub struct PlotDemo { interaction_demo: InteractionDemo, custom_axes_demo: CustomAxesDemo, linked_axes_demo: LinkedAxesDemo, + userdata_demo: UserdataDemo, open_panel: Panel, } @@ -124,6 +128,7 @@ impl PlotDemo { ui.selectable_value(&mut self.open_panel, Panel::Interaction, "Interaction"); ui.selectable_value(&mut self.open_panel, Panel::CustomAxes, "Custom Axes"); ui.selectable_value(&mut self.open_panel, Panel::LinkedAxes, "Linked Axes"); + ui.selectable_value(&mut self.open_panel, Panel::Userdata, "Userdata"); }); ui.separator(); @@ -152,6 +157,9 @@ impl PlotDemo { Panel::LinkedAxes => { self.linked_axes_demo.ui(ui); } + Panel::Userdata => { + self.userdata_demo.ui(ui); + } } } } @@ -642,7 +650,7 @@ impl CustomAxesDemo { } }; - let label_fmt = |_s: &str, val: &PlotPoint| { + let label_fmt = |_s: &str, val: &PlotPoint, _| { format!( "Day {d}, {h}:{m:02}\n{p:.2}%", d = day(val.x), @@ -1197,6 +1205,145 @@ impl ChartsDemo { } } +#[derive(PartialEq, serde::Deserialize, serde::Serialize, Default)] +struct UserdataDemo {} + +#[derive(Clone)] +struct DemoPoint { + x: f64, + y: f64, + custom_label: String, + importance: f32, +} + +impl UserdataDemo { + #[expect(clippy::unused_self, clippy::significant_drop_tightening)] + fn ui(&self, ui: &mut egui::Ui) -> Response { + ui.label( + "This demo shows how to attach custom data to plot items and display it in tooltips.", + ); + ui.separator(); + + // Create multiple datasets with custom metadata + let sine_points = (0..=500) + .map(|i| { + let x = i as f64 / 100.0; + DemoPoint { + x, + y: x.sin(), + custom_label: format!("Sine #{i}"), + importance: (i % 100) as f32 / 100.0, + } + }) + .collect::>(); + + let cosine_points = (0..=500) + .map(|i| { + let x = i as f64 / 100.0; + DemoPoint { + x, + y: x.cos(), + custom_label: format!("Cosine #{i}"), + importance: (1.0 - (i % 100) as f32 / 100.0), + } + }) + .collect::>(); + + let damped_points = (0..=500) + .map(|i| { + let x = i as f64 / 100.0; + DemoPoint { + x, + y: (-x * 0.5).exp() * (2.0 * x).sin(), + custom_label: format!("Damped #{i}"), + importance: if i % 50 == 0 { 1.0 } else { 0.3 }, + } + }) + .collect::>(); + + // Store custom data in a shared map + let custom_data = Arc::new(Mutex::new(HashMap::>::new())); + + let custom_data_ = custom_data.clone(); + Plot::new("Userdata Plot Demo") + .legend(Legend::default().position(Corner::LeftTop)) + .label_formatter(move |name, value, item| { + if let Some((id, index)) = item { + let lock = custom_data_.lock(); + if let Some(points) = lock.get(&id) { + if let Some(point) = points.get(index) { + return format!( + "{}\nPosition: ({:.3}, {:.3})\nLabel: {}\nImportance: {:.1}%", + name, + value.x, + value.y, + point.custom_label, + point.importance * 100.0 + ); + } + } + } + format!("{}\n({:.3}, {:.3})", name, value.x, value.y) + }) + .show(ui, |plot_ui| { + let mut lock = custom_data.lock(); + + // Sine wave with custom data + let sine_id = Id::new("sine_wave"); + lock.insert(sine_id, sine_points.clone()); + plot_ui.line( + Line::new( + "sin(x)", + sine_points.iter().map(|p| [p.x, p.y]).collect::>(), + ) + .id(sine_id) + .color(Color32::from_rgb(200, 100, 100)), + ); + + // Cosine wave with custom data + let cosine_id = Id::new("cosine_wave"); + lock.insert(cosine_id, cosine_points.clone()); + plot_ui.line( + Line::new( + "cos(x)", + cosine_points.iter().map(|p| [p.x, p.y]).collect::>(), + ) + .id(cosine_id) + .color(Color32::from_rgb(100, 200, 100)), + ); + + // Damped sine wave with custom data + let damped_id = Id::new("damped_wave"); + lock.insert(damped_id, damped_points.clone()); + plot_ui.line( + Line::new( + "e^(-0.5x) ยท sin(2x)", + damped_points.iter().map(|p| [p.x, p.y]).collect::>(), + ) + .id(damped_id) + .color(Color32::from_rgb(100, 100, 200)), + ); + + // Add some points with high importance as markers + let important_points: Vec<_> = damped_points + .iter() + .filter(|p| p.importance > 0.9) + .map(|p| [p.x, p.y]) + .collect(); + + if !important_points.is_empty() { + plot_ui.points( + Points::new("Important Points", important_points) + .color(Color32::from_rgb(255, 150, 0)) + .radius(4.0) + .shape(MarkerShape::Diamond), + ); + } + }) + .response + } +} + fn is_approx_zero(val: f64) -> bool { val.abs() < 1e-6 } diff --git a/demo/tests/snapshots/demos/Charts.png b/demo/tests/snapshots/demos/Charts.png index 55b7adfd..c61b9ff4 100644 --- a/demo/tests/snapshots/demos/Charts.png +++ b/demo/tests/snapshots/demos/Charts.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a8a4457888659af1357055da72f8b0cbc6a8c87fb1d48f53e6e3686edd862d21 -size 85593 +oid sha256:9372ba45fddf534b36248c8d2b34d7677f2da9db27f72fa762c424dfaa7c9477 +size 86660 diff --git a/demo/tests/snapshots/demos/Custom Axes.png b/demo/tests/snapshots/demos/Custom Axes.png index 16ff1d8d..f3b96936 100644 --- a/demo/tests/snapshots/demos/Custom Axes.png +++ b/demo/tests/snapshots/demos/Custom Axes.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f761e349271bc852c61854b52e206ee9fa2130526207f0d4e0f199df3fc7fe7 -size 70547 +oid sha256:066cb2cdd5e7b2c8c34ae2e533776ed84102504b854520140dcf74338449ea56 +size 71614 diff --git a/demo/tests/snapshots/demos/Interaction.png b/demo/tests/snapshots/demos/Interaction.png index 6b18aa84..6433c853 100644 --- a/demo/tests/snapshots/demos/Interaction.png +++ b/demo/tests/snapshots/demos/Interaction.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cd029ed29d21fcaa9ea707dca29b4aa165915399bcb97c2f33b0c53f8749092a -size 77248 +oid sha256:53b8495aa2d2d97659a0be13ac6b31fb68430e7c1dfac24530fb0de2b809dcfb +size 78315 diff --git a/demo/tests/snapshots/demos/Items.png b/demo/tests/snapshots/demos/Items.png index 75defb6b..7b4c7a7f 100644 --- a/demo/tests/snapshots/demos/Items.png +++ b/demo/tests/snapshots/demos/Items.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb41447732a0cadb2ceedc0412bfcba684d6cdc065b3cbbafb28af9bee04fec1 -size 109204 +oid sha256:13ace52cf67632c33a8750acf070476b2835201a975396ddd7b594ccd769027a +size 110294 diff --git a/demo/tests/snapshots/demos/Legend.png b/demo/tests/snapshots/demos/Legend.png index e1dbe30c..d8afb5a2 100644 --- a/demo/tests/snapshots/demos/Legend.png +++ b/demo/tests/snapshots/demos/Legend.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f390bfb068ad93738a8fed063e8f9e79a7f7c91d3af959262a3e6b0f0c6dd736 -size 131120 +oid sha256:e05997256c0c527d133c2ed17aadade499afd7839af20a140062521dcd019149 +size 132187 diff --git a/demo/tests/snapshots/demos/Lines.png b/demo/tests/snapshots/demos/Lines.png index 7d03fba0..f5d1ac6d 100644 --- a/demo/tests/snapshots/demos/Lines.png +++ b/demo/tests/snapshots/demos/Lines.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb6bc7412c003876604b206052eb91de42009e992a4577b3216195edc66d7ac1 -size 122333 +oid sha256:a86c2944ac2459c7b8f8d6c94860420274f51c32f4a4d65a734e332205668e8e +size 123372 diff --git a/demo/tests/snapshots/demos/Linked Axes.png b/demo/tests/snapshots/demos/Linked Axes.png index b71adeb4..5923da33 100644 --- a/demo/tests/snapshots/demos/Linked Axes.png +++ b/demo/tests/snapshots/demos/Linked Axes.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28a6deff5454549b42e080eae6ad215cd668b6a343569e92b4e9396b5f49cc97 -size 83985 +oid sha256:927406de29c6f9574e0fb9f38da28ca453001ec296ede75f30cf99018b7a9434 +size 85050 diff --git a/demo/tests/snapshots/demos/Markers.png b/demo/tests/snapshots/demos/Markers.png index 229aceb7..d420d5a2 100644 --- a/demo/tests/snapshots/demos/Markers.png +++ b/demo/tests/snapshots/demos/Markers.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ee6b61d601076c9345362382d8abad26019452cffbc85fa9ad26b47bfb760452 -size 105842 +oid sha256:0a2f76270c84fb9eff06d5c8a33eac8f6e6a4a12ebf747d83cc02036022f010b +size 106907 diff --git a/demo/tests/snapshots/demos/Userdata.png b/demo/tests/snapshots/demos/Userdata.png new file mode 100644 index 00000000..58f9f881 --- /dev/null +++ b/demo/tests/snapshots/demos/Userdata.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e0f58b352f28df0925aadb0f6a7976b35049addb627197371045b3e8c50f296 +size 110698 diff --git a/demo/tests/snapshots/light_mode.png b/demo/tests/snapshots/light_mode.png index 1d0f9265..59aa7853 100644 --- a/demo/tests/snapshots/light_mode.png +++ b/demo/tests/snapshots/light_mode.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a95e6beb9c992efe9e98bd9b461831c7feaff9bffd0f9a724282571e983d34b2 -size 118734 +oid sha256:49b820a89bb8302353aec4ed2e03a0bbce7a7a18bc6a596de1b0f244742bc955 +size 119729 diff --git a/demo/tests/snapshots/scale_0.50.png b/demo/tests/snapshots/scale_0.50.png index bee8f591..62c66f32 100644 --- a/demo/tests/snapshots/scale_0.50.png +++ b/demo/tests/snapshots/scale_0.50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:070365cd0e9b3ef6891bf830dea35bfd4849520f8e511eb5086780ea7beffb70 -size 61532 +oid sha256:ee73720322c458d1d4397ce7b9d86a2ee2b0846d6a2cb187a1ac0119a64ea812 +size 62002 diff --git a/demo/tests/snapshots/scale_1.00.png b/demo/tests/snapshots/scale_1.00.png index e3d09998..3a0c1955 100644 --- a/demo/tests/snapshots/scale_1.00.png +++ b/demo/tests/snapshots/scale_1.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48c84fd502e599ab4fcd3e2950eea41e23585e3f35d613f39f8987782af92c3e -size 122190 +oid sha256:a869b00f49c899715de175b4af9e5ab4ad0929546627ccf58abb96d2af569dcf +size 123230 diff --git a/demo/tests/snapshots/scale_1.39.png b/demo/tests/snapshots/scale_1.39.png index bd6c4b08..9fc0be9b 100644 --- a/demo/tests/snapshots/scale_1.39.png +++ b/demo/tests/snapshots/scale_1.39.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bd3685434100daab51c8fc057f5d3e9b818c4873f96a62e071dfb267f8b6d763 -size 227418 +oid sha256:ba5ab71ceb9ebb222594c30d4cb2f9af6e5e18f418f5e1355bc4cc4d5fd74db2 +size 232153 diff --git a/demo/tests/snapshots/scale_2.00.png b/demo/tests/snapshots/scale_2.00.png index f95c5512..0952aaea 100644 --- a/demo/tests/snapshots/scale_2.00.png +++ b/demo/tests/snapshots/scale_2.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93b014741cbfb4d18ed805871e967796bcd1d6749487dc23828b644b599df745 -size 259898 +oid sha256:b1a2070026776750fc1e69c3407dd7f0add8956619442daa323bec9f129c3fde +size 262268 diff --git a/egui_plot/src/items/mod.rs b/egui_plot/src/items/mod.rs index afdfc8ab..c0f8ecc0 100644 --- a/egui_plot/src/items/mod.rs +++ b/egui_plot/src/items/mod.rs @@ -143,7 +143,7 @@ pub trait PlotItem { match self.geometry() { PlotGeometry::None => None, - PlotGeometry::Points(points) => points + PlotGeometry::Points(points, _) => points .iter() .enumerate() .map(|(index, value)| { @@ -168,8 +168,8 @@ pub trait PlotItem { plot: &PlotConfig<'_>, label_formatter: &LabelFormatter<'_>, ) { - let points = match self.geometry() { - PlotGeometry::Points(points) => points, + let (points, id) = match self.geometry() { + PlotGeometry::Points(points, id) => (points, id), PlotGeometry::None => { panic!("If the PlotItem has no geometry, on_hover() must not be called") } @@ -192,6 +192,7 @@ pub trait PlotItem { rulers_and_tooltip_at_value( plot_area_response, value, + id.map(|id| (id, elem.index)), self.name(), plot, cursors, @@ -603,7 +604,7 @@ impl PlotItem for Line<'_> { } fn geometry(&self) -> PlotGeometry<'_> { - PlotGeometry::Points(self.series.points()) + PlotGeometry::Points(self.series.points(), Some(self.id())) } fn bounds(&self) -> PlotBounds { @@ -705,7 +706,7 @@ impl PlotItem for Polygon<'_> { } fn geometry(&self) -> PlotGeometry<'_> { - PlotGeometry::Points(self.series.points()) + PlotGeometry::Points(self.series.points(), Some(self.id())) } fn bounds(&self) -> PlotBounds { @@ -1025,7 +1026,7 @@ impl PlotItem for Points<'_> { } fn geometry(&self) -> PlotGeometry<'_> { - PlotGeometry::Points(self.series.points()) + PlotGeometry::Points(self.series.points(), Some(self.id())) } fn bounds(&self) -> PlotBounds { @@ -1136,7 +1137,7 @@ impl PlotItem for Arrows<'_> { } fn geometry(&self) -> PlotGeometry<'_> { - PlotGeometry::Points(self.origins.points()) + PlotGeometry::Points(self.origins.points(), Some(self.id())) } fn bounds(&self) -> PlotBounds { @@ -1705,6 +1706,7 @@ fn add_rulers_and_text( pub(super) fn rulers_and_tooltip_at_value( plot_area_response: &egui::Response, value: PlotPoint, + item: Option<(Id, usize)>, name: &str, plot: &PlotConfig<'_>, cursors: &mut Vec, @@ -1718,7 +1720,7 @@ pub(super) fn rulers_and_tooltip_at_value( } let text = if let Some(custom_label) = label_formatter { - let label = custom_label(name, &value); + let label = custom_label(name, &value, None); if label.is_empty() { return; } @@ -1732,7 +1734,9 @@ pub(super) fn rulers_and_tooltip_at_value( let scale = plot.transform.dvalue_dpos(); let x_decimals = ((-scale[0].abs().log10()).ceil().at_least(0.0) as usize).clamp(1, 6); let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).clamp(1, 6); - if plot.show_x && plot.show_y { + if let Some(custom_label) = label_formatter { + custom_label(name, &value, item) + } else if plot.show_x && plot.show_y { format!( "{}x = {:.*}\ny = {:.*}", prefix, x_decimals, value.x, y_decimals, value.y diff --git a/egui_plot/src/items/values.rs b/egui_plot/src/items/values.rs index a5d93ca6..22d419a4 100644 --- a/egui_plot/src/items/values.rs +++ b/egui_plot/src/items/values.rs @@ -1,7 +1,7 @@ use std::ops::{Bound, RangeBounds, RangeInclusive}; use egui::{ - Pos2, Rect, Shape, Stroke, Vec2, + Id, Pos2, Rect, Shape, Stroke, Vec2, epaint::{ColorMode, PathStroke}, lerp, pos2, }; @@ -397,7 +397,7 @@ pub enum PlotGeometry<'a> { None, /// Point values (X-Y graphs) - Points(&'a [PlotPoint]), + Points(&'a [PlotPoint], Option), /// Rectangles (examples: boxes or bars) // Has currently no data, as it would require copying rects or iterating a list of pointers. diff --git a/egui_plot/src/lib.rs b/egui_plot/src/lib.rs index afc721be..144993b8 100644 --- a/egui_plot/src/lib.rs +++ b/egui_plot/src/lib.rs @@ -41,7 +41,7 @@ use axis::AxisWidget; use items::{horizontal_line, rulers_color, vertical_line}; use legend::LegendWidget; -type LabelFormatterFn<'a> = dyn Fn(&str, &PlotPoint) -> String + 'a; +type LabelFormatterFn<'a> = dyn Fn(&str, &PlotPoint, Option<(Id, usize)>) -> String + 'a; pub type LabelFormatter<'a> = Option>>; type GridSpacerFn<'a> = dyn Fn(GridInput) -> Vec + 'a; @@ -434,6 +434,10 @@ impl<'a> Plot<'a> { } /// Provide a function to customize the on-hover label for the x and y axis + /// ## `build_fn` parameter + /// The third parameter, `build_fn`, is a closure that is called with a mutable reference to a [`PlotUi`] instance. + /// Within this closure, you can add items (such as lines, points, bars, etc.) to the plot and interact with the plot UI. + /// The closure should return any value you wish to propagate from the plot construction (for example, a reference to a plot item, or a custom result). /// /// ``` /// # egui::__run_test_ui(|ui| { @@ -444,7 +448,7 @@ impl<'a> Plot<'a> { /// }).collect(); /// let line = Line::new("sin", sin); /// Plot::new("my_plot").view_aspect(2.0) - /// .label_formatter(|name, value| { + /// .label_formatter(|name, value, _| { /// if !name.is_empty() { /// format!("{}: {:.*}%", name, 1, value.y) /// } else { @@ -456,7 +460,7 @@ impl<'a> Plot<'a> { /// ``` pub fn label_formatter( mut self, - label_formatter: impl Fn(&str, &PlotPoint) -> String + 'a, + label_formatter: impl Fn(&str, &PlotPoint, Option<(Id, usize)>) -> String + 'a, ) -> Self { self.label_formatter = Some(Box::new(label_formatter)); self @@ -1904,6 +1908,7 @@ impl PreparedPlot<'_> { items::rulers_and_tooltip_at_value( plot_area_response, value, + None, "", &plot, &mut cursors,