Plot items now have optional id which is returned in the plot's response when hovered (#3920)

This allows users to check which item the user interacts with in the
plot.



https://github.com/emilk/egui/assets/1220815/1a174b38-8414-49be-a802-d187cd93d154

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
Andreas Reich 2024-01-30 15:55:56 +01:00 committed by GitHub
parent 945a69d2f2
commit ca513ce241
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 180 additions and 18 deletions

View File

@ -791,8 +791,28 @@ impl InteractionDemo {
let PlotResponse { let PlotResponse {
response, response,
inner: (screen_pos, pointer_coordinate, pointer_coordinate_drag_delta, bounds, hovered), inner: (screen_pos, pointer_coordinate, pointer_coordinate_drag_delta, bounds, hovered),
hovered_plot_item,
.. ..
} = plot.show(ui, |plot_ui| { } = plot.show(ui, |plot_ui| {
plot_ui.line(
Line::new(PlotPoints::from_explicit_callback(
move |x| x.sin(),
..,
100,
))
.color(Color32::RED)
.id(egui::Id::new("sin")),
);
plot_ui.line(
Line::new(PlotPoints::from_explicit_callback(
move |x| x.cos(),
..,
100,
))
.color(Color32::BLUE)
.id(egui::Id::new("cos")),
);
( (
plot_ui.screen_from_plot(PlotPoint::new(0.0, 0.0)), plot_ui.screen_from_plot(PlotPoint::new(0.0, 0.0)),
plot_ui.pointer_coordinate(), plot_ui.pointer_coordinate(),
@ -824,6 +844,15 @@ impl InteractionDemo {
); );
ui.label(format!("pointer coordinate drag delta: {coordinate_text}")); ui.label(format!("pointer coordinate drag delta: {coordinate_text}"));
let hovered_item = if hovered_plot_item == Some(egui::Id::new("sin")) {
"red sin"
} else if hovered_plot_item == Some(egui::Id::new("cos")) {
"blue cos"
} else {
"none"
};
ui.label(format!("hovered plot item: {hovered_item}"));
response response
} }
} }

View File

@ -49,6 +49,8 @@ pub(super) trait PlotItem {
fn bounds(&self) -> PlotBounds; fn bounds(&self) -> PlotBounds;
fn id(&self) -> Option<Id>;
fn find_closest(&self, point: Pos2, transform: &PlotTransform) -> Option<ClosestElem> { fn find_closest(&self, point: Pos2, transform: &PlotTransform) -> Option<ClosestElem> {
match self.geometry() { match self.geometry() {
PlotGeometry::None => None, PlotGeometry::None => None,
@ -120,6 +122,7 @@ pub struct HLine {
pub(super) name: String, pub(super) name: String,
pub(super) highlight: bool, pub(super) highlight: bool,
pub(super) style: LineStyle, pub(super) style: LineStyle,
id: Option<Id>,
} }
impl HLine { impl HLine {
@ -130,6 +133,7 @@ impl HLine {
name: String::default(), name: String::default(),
highlight: false, highlight: false,
style: LineStyle::Solid, style: LineStyle::Solid,
id: None,
} }
} }
@ -180,6 +184,13 @@ impl HLine {
self.name = name.to_string(); self.name = name.to_string();
self self
} }
/// Set the line's id which is used to identify it in the plot's response.
#[inline]
pub fn id(mut self, id: Id) -> Self {
self.id = Some(id);
self
}
} }
impl PlotItem for HLine { impl PlotItem for HLine {
@ -232,6 +243,10 @@ impl PlotItem for HLine {
bounds.max[1] = self.y; bounds.max[1] = self.y;
bounds bounds
} }
fn id(&self) -> Option<Id> {
self.id
}
} }
/// A vertical line in a plot, filling the full width /// A vertical line in a plot, filling the full width
@ -242,6 +257,7 @@ pub struct VLine {
pub(super) name: String, pub(super) name: String,
pub(super) highlight: bool, pub(super) highlight: bool,
pub(super) style: LineStyle, pub(super) style: LineStyle,
id: Option<Id>,
} }
impl VLine { impl VLine {
@ -252,6 +268,7 @@ impl VLine {
name: String::default(), name: String::default(),
highlight: false, highlight: false,
style: LineStyle::Solid, style: LineStyle::Solid,
id: None,
} }
} }
@ -302,6 +319,13 @@ impl VLine {
self.name = name.to_string(); self.name = name.to_string();
self self
} }
/// Set the line's id which is used to identify it in the plot's response.
#[inline]
pub fn id(mut self, id: Id) -> Self {
self.id = Some(id);
self
}
} }
impl PlotItem for VLine { impl PlotItem for VLine {
@ -354,6 +378,10 @@ impl PlotItem for VLine {
bounds.max[0] = self.x; bounds.max[0] = self.x;
bounds bounds
} }
fn id(&self) -> Option<Id> {
self.id
}
} }
/// A series of values forming a path. /// A series of values forming a path.
@ -364,6 +392,7 @@ pub struct Line {
pub(super) highlight: bool, pub(super) highlight: bool,
pub(super) fill: Option<f32>, pub(super) fill: Option<f32>,
pub(super) style: LineStyle, pub(super) style: LineStyle,
id: Option<Id>,
} }
impl Line { impl Line {
@ -375,6 +404,7 @@ impl Line {
highlight: false, highlight: false,
fill: None, fill: None,
style: LineStyle::Solid, style: LineStyle::Solid,
id: None,
} }
} }
@ -432,6 +462,13 @@ impl Line {
self.name = name.to_string(); self.name = name.to_string();
self self
} }
/// Set the line's id which is used to identify it in the plot's response.
#[inline]
pub fn id(mut self, id: Id) -> Self {
self.id = Some(id);
self
}
} }
/// Returns the x-coordinate of a possible intersection between a line segment from `p1` to `p2` and /// Returns the x-coordinate of a possible intersection between a line segment from `p1` to `p2` and
@ -528,6 +565,10 @@ impl PlotItem for Line {
fn bounds(&self) -> PlotBounds { fn bounds(&self) -> PlotBounds {
self.series.bounds() self.series.bounds()
} }
fn id(&self) -> Option<Id> {
self.id
}
} }
/// A convex polygon. /// A convex polygon.
@ -538,6 +579,7 @@ pub struct Polygon {
pub(super) highlight: bool, pub(super) highlight: bool,
pub(super) fill_color: Option<Color32>, pub(super) fill_color: Option<Color32>,
pub(super) style: LineStyle, pub(super) style: LineStyle,
id: Option<Id>,
} }
impl Polygon { impl Polygon {
@ -549,6 +591,7 @@ impl Polygon {
highlight: false, highlight: false,
fill_color: None, fill_color: None,
style: LineStyle::Solid, style: LineStyle::Solid,
id: None,
} }
} }
@ -600,6 +643,13 @@ impl Polygon {
self.name = name.to_string(); self.name = name.to_string();
self self
} }
/// Set the polygon's id which is used to identify it in the plot's response.
#[inline]
pub fn id(mut self, id: Id) -> Self {
self.id = Some(id);
self
}
} }
impl PlotItem for Polygon { impl PlotItem for Polygon {
@ -654,6 +704,10 @@ impl PlotItem for Polygon {
fn bounds(&self) -> PlotBounds { fn bounds(&self) -> PlotBounds {
self.series.bounds() self.series.bounds()
} }
fn id(&self) -> Option<Id> {
self.id
}
} }
/// Text inside the plot. /// Text inside the plot.
@ -665,6 +719,7 @@ pub struct Text {
pub(super) highlight: bool, pub(super) highlight: bool,
pub(super) color: Color32, pub(super) color: Color32,
pub(super) anchor: Align2, pub(super) anchor: Align2,
id: Option<Id>,
} }
impl Text { impl Text {
@ -676,6 +731,7 @@ impl Text {
highlight: false, highlight: false,
color: Color32::TRANSPARENT, color: Color32::TRANSPARENT,
anchor: Align2::CENTER_CENTER, anchor: Align2::CENTER_CENTER,
id: None,
} }
} }
@ -712,6 +768,13 @@ impl Text {
self.name = name.to_string(); self.name = name.to_string();
self self
} }
/// Set the text's id which is used to identify it in the plot's response.
#[inline]
pub fn id(mut self, id: Id) -> Self {
self.id = Some(id);
self
}
} }
impl PlotItem for Text { impl PlotItem for Text {
@ -768,6 +831,10 @@ impl PlotItem for Text {
bounds.extend_with(&self.position); bounds.extend_with(&self.position);
bounds bounds
} }
fn id(&self) -> Option<Id> {
self.id
}
} }
/// A set of points. /// A set of points.
@ -790,6 +857,7 @@ pub struct Points {
pub(super) highlight: bool, pub(super) highlight: bool,
pub(super) stems: Option<f32>, pub(super) stems: Option<f32>,
id: Option<Id>,
} }
impl Points { impl Points {
@ -803,6 +871,7 @@ impl Points {
name: Default::default(), name: Default::default(),
highlight: false, highlight: false,
stems: None, stems: None,
id: None,
} }
} }
@ -860,6 +929,13 @@ impl Points {
self.name = name.to_string(); self.name = name.to_string();
self self
} }
/// Set the points' id which is used to identify them in the plot's response.
#[inline]
pub fn id(mut self, id: Id) -> Self {
self.id = Some(id);
self
}
} }
impl PlotItem for Points { impl PlotItem for Points {
@ -1018,6 +1094,10 @@ impl PlotItem for Points {
fn bounds(&self) -> PlotBounds { fn bounds(&self) -> PlotBounds {
self.series.bounds() self.series.bounds()
} }
fn id(&self) -> Option<Id> {
self.id
}
} }
/// A set of arrows. /// A set of arrows.
@ -1028,6 +1108,7 @@ pub struct Arrows {
pub(super) color: Color32, pub(super) color: Color32,
pub(super) name: String, pub(super) name: String,
pub(super) highlight: bool, pub(super) highlight: bool,
id: Option<Id>,
} }
impl Arrows { impl Arrows {
@ -1039,6 +1120,7 @@ impl Arrows {
color: Color32::TRANSPARENT, color: Color32::TRANSPARENT,
name: Default::default(), name: Default::default(),
highlight: false, highlight: false,
id: None,
} }
} }
@ -1075,6 +1157,13 @@ impl Arrows {
self.name = name.to_string(); self.name = name.to_string();
self self
} }
/// Set the arrows' id which is used to identify them in the plot's response.
#[inline]
pub fn id(mut self, id: Id) -> Self {
self.id = Some(id);
self
}
} }
impl PlotItem for Arrows { impl PlotItem for Arrows {
@ -1150,6 +1239,10 @@ impl PlotItem for Arrows {
fn bounds(&self) -> PlotBounds { fn bounds(&self) -> PlotBounds {
self.origins.bounds() self.origins.bounds()
} }
fn id(&self) -> Option<Id> {
self.id
}
} }
/// An image in the plot. /// An image in the plot.
@ -1164,6 +1257,7 @@ pub struct PlotImage {
pub(super) tint: Color32, pub(super) tint: Color32,
pub(super) highlight: bool, pub(super) highlight: bool,
pub(super) name: String, pub(super) name: String,
id: Option<Id>,
} }
impl PlotImage { impl PlotImage {
@ -1183,6 +1277,7 @@ impl PlotImage {
rotation: 0.0, rotation: 0.0,
bg_fill: Default::default(), bg_fill: Default::default(),
tint: Color32::WHITE, tint: Color32::WHITE,
id: None,
} }
} }
@ -1330,6 +1425,10 @@ impl PlotItem for PlotImage {
bounds.extend_with(&right_bottom); bounds.extend_with(&right_bottom);
bounds bounds
} }
fn id(&self) -> Option<Id> {
self.id
}
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -1344,6 +1443,7 @@ pub struct BarChart {
pub(super) element_formatter: Option<Box<dyn Fn(&Bar, &BarChart) -> String>>, pub(super) element_formatter: Option<Box<dyn Fn(&Bar, &BarChart) -> String>>,
highlight: bool, highlight: bool,
id: Option<Id>,
} }
impl BarChart { impl BarChart {
@ -1355,6 +1455,7 @@ impl BarChart {
name: String::new(), name: String::new(),
element_formatter: None, element_formatter: None,
highlight: false, highlight: false,
id: None,
} }
} }
@ -1454,6 +1555,13 @@ impl BarChart {
} }
self self
} }
/// Set the bar chart's id which is used to identify it in the plot's response.
#[inline]
pub fn id(mut self, id: Id) -> Self {
self.id = Some(id);
self
}
} }
impl PlotItem for BarChart { impl PlotItem for BarChart {
@ -1512,6 +1620,10 @@ impl PlotItem for BarChart {
bar.add_shapes(plot.transform, true, shapes); bar.add_shapes(plot.transform, true, shapes);
bar.add_rulers_and_text(self, plot, shapes, cursors); bar.add_rulers_and_text(self, plot, shapes, cursors);
} }
fn id(&self) -> Option<Id> {
self.id
}
} }
/// A diagram containing a series of [`BoxElem`] elements. /// A diagram containing a series of [`BoxElem`] elements.
@ -1524,6 +1636,7 @@ pub struct BoxPlot {
pub(super) element_formatter: Option<Box<dyn Fn(&BoxElem, &BoxPlot) -> String>>, pub(super) element_formatter: Option<Box<dyn Fn(&BoxElem, &BoxPlot) -> String>>,
highlight: bool, highlight: bool,
id: Option<Id>,
} }
impl BoxPlot { impl BoxPlot {
@ -1535,6 +1648,7 @@ impl BoxPlot {
name: String::new(), name: String::new(),
element_formatter: None, element_formatter: None,
highlight: false, highlight: false,
id: None,
} }
} }
@ -1602,6 +1716,13 @@ impl BoxPlot {
self.element_formatter = Some(formatter); self.element_formatter = Some(formatter);
self self
} }
/// Set the box plot's id which is used to identify it in the plot's response.
#[inline]
pub fn id(mut self, id: Id) -> Self {
self.id = Some(id);
self
}
} }
impl PlotItem for BoxPlot { impl PlotItem for BoxPlot {
@ -1660,6 +1781,10 @@ impl PlotItem for BoxPlot {
box_plot.add_shapes(plot.transform, true, shapes); box_plot.add_shapes(plot.transform, true, shapes);
box_plot.add_rulers_and_text(self, plot, shapes, cursors); box_plot.add_rulers_and_text(self, plot, shapes, cursors);
} }
fn id(&self) -> Option<Id> {
self.id
}
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------

View File

@ -117,6 +117,11 @@ pub struct PlotResponse<R> {
/// The transform between screen coordinates and plot coordinates. /// The transform between screen coordinates and plot coordinates.
pub transform: PlotTransform, pub transform: PlotTransform,
/// The id of a currently hovered item if any.
///
/// This is `None` if either no item was hovered, or the hovered item didn't provide an id.
pub hovered_plot_item: Option<Id>,
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -803,7 +808,7 @@ impl Plot {
} }
.unwrap_or_else(|| PlotMemory { .unwrap_or_else(|| PlotMemory {
auto_bounds: default_auto_bounds, auto_bounds: default_auto_bounds,
hovered_item: None, hovered_legend_item: None,
hidden_items: Default::default(), hidden_items: Default::default(),
transform: PlotTransform::new(plot_rect, min_auto_bounds, center_axis.x, center_axis.y), transform: PlotTransform::new(plot_rect, min_auto_bounds, center_axis.x, center_axis.y),
last_click_pos_for_zoom: None, last_click_pos_for_zoom: None,
@ -848,14 +853,14 @@ impl Plot {
let legend = legend_config let legend = legend_config
.and_then(|config| LegendWidget::try_new(plot_rect, config, &items, &mem.hidden_items)); .and_then(|config| LegendWidget::try_new(plot_rect, config, &items, &mem.hidden_items));
// Don't show hover cursor when hovering over legend. // Don't show hover cursor when hovering over legend.
if mem.hovered_item.is_some() { if mem.hovered_legend_item.is_some() {
show_x = false; show_x = false;
show_y = false; show_y = false;
} }
// Remove the deselected items. // Remove the deselected items.
items.retain(|item| !mem.hidden_items.contains(item.name())); items.retain(|item| !mem.hidden_items.contains(item.name()));
// Highlight the hovered items. // Highlight the hovered items.
if let Some(hovered_name) = &mem.hovered_item { if let Some(hovered_name) = &mem.hovered_legend_item {
items items
.iter_mut() .iter_mut()
.filter(|entry| entry.name() == hovered_name) .filter(|entry| entry.name() == hovered_name)
@ -1137,7 +1142,7 @@ impl Plot {
clamp_grid, clamp_grid,
}; };
let plot_cursors = prepared.ui(ui, &response); let (plot_cursors, hovered_plot_item) = prepared.ui(ui, &response);
if let Some(boxed_zoom_rect) = boxed_zoom_rect { if let Some(boxed_zoom_rect) = boxed_zoom_rect {
ui.painter() ui.painter()
@ -1151,7 +1156,7 @@ impl Plot {
if let Some(mut legend) = legend { if let Some(mut legend) = legend {
ui.add(&mut legend); ui.add(&mut legend);
mem.hidden_items = legend.hidden_items(); mem.hidden_items = legend.hidden_items();
mem.hovered_item = legend.hovered_item_name(); mem.hovered_legend_item = legend.hovered_item_name();
} }
if let Some((id, _)) = linked_cursors.as_ref() { if let Some((id, _)) = linked_cursors.as_ref() {
@ -1195,6 +1200,7 @@ impl Plot {
inner, inner,
response, response,
transform, transform,
hovered_plot_item,
} }
} }
} }
@ -1645,7 +1651,7 @@ struct PreparedPlot {
} }
impl PreparedPlot { impl PreparedPlot {
fn ui(self, ui: &mut Ui, response: &Response) -> Vec<Cursor> { fn ui(self, ui: &mut Ui, response: &Response) -> (Vec<Cursor>, Option<Id>) {
let mut axes_shapes = Vec::new(); let mut axes_shapes = Vec::new();
if self.show_grid.x { if self.show_grid.x {
@ -1669,10 +1675,10 @@ impl PreparedPlot {
} }
let hover_pos = response.hover_pos(); let hover_pos = response.hover_pos();
let cursors = if let Some(pointer) = hover_pos { let (cursors, hovered_item_id) = if let Some(pointer) = hover_pos {
self.hover(ui, pointer, &mut shapes) self.hover(ui, pointer, &mut shapes)
} else { } else {
Vec::new() (Vec::new(), None)
}; };
// Draw cursors // Draw cursors
@ -1726,7 +1732,7 @@ impl PreparedPlot {
} }
} }
cursors (cursors, hovered_item_id)
} }
fn paint_grid(&self, ui: &Ui, shapes: &mut Vec<(Shape, f32)>, axis: Axis, fade_range: Rangef) { fn paint_grid(&self, ui: &Ui, shapes: &mut Vec<(Shape, f32)>, axis: Axis, fade_range: Rangef) {
@ -1826,7 +1832,7 @@ impl PreparedPlot {
} }
} }
fn hover(&self, ui: &Ui, pointer: Pos2, shapes: &mut Vec<Shape>) -> Vec<Cursor> { fn hover(&self, ui: &Ui, pointer: Pos2, shapes: &mut Vec<Shape>) -> (Vec<Cursor>, Option<Id>) {
let Self { let Self {
transform, transform,
show_x, show_x,
@ -1837,7 +1843,7 @@ impl PreparedPlot {
} = self; } = self;
if !show_x && !show_y { if !show_x && !show_y {
return Vec::new(); return (Vec::new(), None);
} }
let interact_radius_sq = (16.0_f32).powi(2); let interact_radius_sq = (16.0_f32).powi(2);
@ -1853,8 +1859,6 @@ impl PreparedPlot {
.min_by_key(|(_, elem)| elem.dist_sq.ord()) .min_by_key(|(_, elem)| elem.dist_sq.ord())
.filter(|(_, elem)| elem.dist_sq <= interact_radius_sq); .filter(|(_, elem)| elem.dist_sq <= interact_radius_sq);
let mut cursors = Vec::new();
let plot = items::PlotConfig { let plot = items::PlotConfig {
ui, ui,
transform, transform,
@ -1862,8 +1866,11 @@ impl PreparedPlot {
show_y: *show_y, show_y: *show_y,
}; };
if let Some((item, elem)) = closest { let mut cursors = Vec::new();
let hovered_plot_item_id = if let Some((item, elem)) = closest {
item.on_hover(elem, shapes, &mut cursors, &plot, label_formatter); item.on_hover(elem, shapes, &mut cursors, &plot, label_formatter);
item.id()
} else { } else {
let value = transform.value_from_position(pointer); let value = transform.value_from_position(pointer);
items::rulers_at_value( items::rulers_at_value(
@ -1875,9 +1882,10 @@ impl PreparedPlot {
&mut cursors, &mut cursors,
label_formatter, label_formatter,
); );
} None
};
cursors (cursors, hovered_plot_item_id)
} }
} }

View File

@ -14,8 +14,8 @@ pub struct PlotMemory {
/// the bounds, for example by moving or zooming. /// the bounds, for example by moving or zooming.
pub auto_bounds: Vec2b, pub auto_bounds: Vec2b,
/// Which item is hovered? /// Display string of the hovered legend item if any.
pub hovered_item: Option<String>, pub hovered_legend_item: Option<String>,
/// Which items _not_ to show? /// Which items _not_ to show?
pub hidden_items: ahash::HashSet<String>, pub hidden_items: ahash::HashSet<String>,