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 {
response,
inner: (screen_pos, pointer_coordinate, pointer_coordinate_drag_delta, bounds, hovered),
hovered_plot_item,
..
} = 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.pointer_coordinate(),
@ -824,6 +844,15 @@ impl InteractionDemo {
);
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
}
}

View File

@ -49,6 +49,8 @@ pub(super) trait PlotItem {
fn bounds(&self) -> PlotBounds;
fn id(&self) -> Option<Id>;
fn find_closest(&self, point: Pos2, transform: &PlotTransform) -> Option<ClosestElem> {
match self.geometry() {
PlotGeometry::None => None,
@ -120,6 +122,7 @@ pub struct HLine {
pub(super) name: String,
pub(super) highlight: bool,
pub(super) style: LineStyle,
id: Option<Id>,
}
impl HLine {
@ -130,6 +133,7 @@ impl HLine {
name: String::default(),
highlight: false,
style: LineStyle::Solid,
id: None,
}
}
@ -180,6 +184,13 @@ impl HLine {
self.name = name.to_string();
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 {
@ -232,6 +243,10 @@ impl PlotItem for HLine {
bounds.max[1] = self.y;
bounds
}
fn id(&self) -> Option<Id> {
self.id
}
}
/// A vertical line in a plot, filling the full width
@ -242,6 +257,7 @@ pub struct VLine {
pub(super) name: String,
pub(super) highlight: bool,
pub(super) style: LineStyle,
id: Option<Id>,
}
impl VLine {
@ -252,6 +268,7 @@ impl VLine {
name: String::default(),
highlight: false,
style: LineStyle::Solid,
id: None,
}
}
@ -302,6 +319,13 @@ impl VLine {
self.name = name.to_string();
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 {
@ -354,6 +378,10 @@ impl PlotItem for VLine {
bounds.max[0] = self.x;
bounds
}
fn id(&self) -> Option<Id> {
self.id
}
}
/// A series of values forming a path.
@ -364,6 +392,7 @@ pub struct Line {
pub(super) highlight: bool,
pub(super) fill: Option<f32>,
pub(super) style: LineStyle,
id: Option<Id>,
}
impl Line {
@ -375,6 +404,7 @@ impl Line {
highlight: false,
fill: None,
style: LineStyle::Solid,
id: None,
}
}
@ -432,6 +462,13 @@ impl Line {
self.name = name.to_string();
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
@ -528,6 +565,10 @@ impl PlotItem for Line {
fn bounds(&self) -> PlotBounds {
self.series.bounds()
}
fn id(&self) -> Option<Id> {
self.id
}
}
/// A convex polygon.
@ -538,6 +579,7 @@ pub struct Polygon {
pub(super) highlight: bool,
pub(super) fill_color: Option<Color32>,
pub(super) style: LineStyle,
id: Option<Id>,
}
impl Polygon {
@ -549,6 +591,7 @@ impl Polygon {
highlight: false,
fill_color: None,
style: LineStyle::Solid,
id: None,
}
}
@ -600,6 +643,13 @@ impl Polygon {
self.name = name.to_string();
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 {
@ -654,6 +704,10 @@ impl PlotItem for Polygon {
fn bounds(&self) -> PlotBounds {
self.series.bounds()
}
fn id(&self) -> Option<Id> {
self.id
}
}
/// Text inside the plot.
@ -665,6 +719,7 @@ pub struct Text {
pub(super) highlight: bool,
pub(super) color: Color32,
pub(super) anchor: Align2,
id: Option<Id>,
}
impl Text {
@ -676,6 +731,7 @@ impl Text {
highlight: false,
color: Color32::TRANSPARENT,
anchor: Align2::CENTER_CENTER,
id: None,
}
}
@ -712,6 +768,13 @@ impl Text {
self.name = name.to_string();
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 {
@ -768,6 +831,10 @@ impl PlotItem for Text {
bounds.extend_with(&self.position);
bounds
}
fn id(&self) -> Option<Id> {
self.id
}
}
/// A set of points.
@ -790,6 +857,7 @@ pub struct Points {
pub(super) highlight: bool,
pub(super) stems: Option<f32>,
id: Option<Id>,
}
impl Points {
@ -803,6 +871,7 @@ impl Points {
name: Default::default(),
highlight: false,
stems: None,
id: None,
}
}
@ -860,6 +929,13 @@ impl Points {
self.name = name.to_string();
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 {
@ -1018,6 +1094,10 @@ impl PlotItem for Points {
fn bounds(&self) -> PlotBounds {
self.series.bounds()
}
fn id(&self) -> Option<Id> {
self.id
}
}
/// A set of arrows.
@ -1028,6 +1108,7 @@ pub struct Arrows {
pub(super) color: Color32,
pub(super) name: String,
pub(super) highlight: bool,
id: Option<Id>,
}
impl Arrows {
@ -1039,6 +1120,7 @@ impl Arrows {
color: Color32::TRANSPARENT,
name: Default::default(),
highlight: false,
id: None,
}
}
@ -1075,6 +1157,13 @@ impl Arrows {
self.name = name.to_string();
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 {
@ -1150,6 +1239,10 @@ impl PlotItem for Arrows {
fn bounds(&self) -> PlotBounds {
self.origins.bounds()
}
fn id(&self) -> Option<Id> {
self.id
}
}
/// An image in the plot.
@ -1164,6 +1257,7 @@ pub struct PlotImage {
pub(super) tint: Color32,
pub(super) highlight: bool,
pub(super) name: String,
id: Option<Id>,
}
impl PlotImage {
@ -1183,6 +1277,7 @@ impl PlotImage {
rotation: 0.0,
bg_fill: Default::default(),
tint: Color32::WHITE,
id: None,
}
}
@ -1330,6 +1425,10 @@ impl PlotItem for PlotImage {
bounds.extend_with(&right_bottom);
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>>,
highlight: bool,
id: Option<Id>,
}
impl BarChart {
@ -1355,6 +1455,7 @@ impl BarChart {
name: String::new(),
element_formatter: None,
highlight: false,
id: None,
}
}
@ -1454,6 +1555,13 @@ impl BarChart {
}
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 {
@ -1512,6 +1620,10 @@ impl PlotItem for BarChart {
bar.add_shapes(plot.transform, true, shapes);
bar.add_rulers_and_text(self, plot, shapes, cursors);
}
fn id(&self) -> Option<Id> {
self.id
}
}
/// 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>>,
highlight: bool,
id: Option<Id>,
}
impl BoxPlot {
@ -1535,6 +1648,7 @@ impl BoxPlot {
name: String::new(),
element_formatter: None,
highlight: false,
id: None,
}
}
@ -1602,6 +1716,13 @@ impl BoxPlot {
self.element_formatter = Some(formatter);
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 {
@ -1660,6 +1781,10 @@ impl PlotItem for BoxPlot {
box_plot.add_shapes(plot.transform, true, shapes);
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.
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 {
auto_bounds: default_auto_bounds,
hovered_item: None,
hovered_legend_item: None,
hidden_items: Default::default(),
transform: PlotTransform::new(plot_rect, min_auto_bounds, center_axis.x, center_axis.y),
last_click_pos_for_zoom: None,
@ -848,14 +853,14 @@ impl Plot {
let legend = legend_config
.and_then(|config| LegendWidget::try_new(plot_rect, config, &items, &mem.hidden_items));
// 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_y = false;
}
// Remove the deselected items.
items.retain(|item| !mem.hidden_items.contains(item.name()));
// Highlight the hovered items.
if let Some(hovered_name) = &mem.hovered_item {
if let Some(hovered_name) = &mem.hovered_legend_item {
items
.iter_mut()
.filter(|entry| entry.name() == hovered_name)
@ -1137,7 +1142,7 @@ impl Plot {
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 {
ui.painter()
@ -1151,7 +1156,7 @@ impl Plot {
if let Some(mut legend) = legend {
ui.add(&mut legend);
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() {
@ -1195,6 +1200,7 @@ impl Plot {
inner,
response,
transform,
hovered_plot_item,
}
}
}
@ -1645,7 +1651,7 @@ struct 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();
if self.show_grid.x {
@ -1669,10 +1675,10 @@ impl PreparedPlot {
}
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)
} else {
Vec::new()
(Vec::new(), None)
};
// 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) {
@ -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 {
transform,
show_x,
@ -1837,7 +1843,7 @@ impl PreparedPlot {
} = self;
if !show_x && !show_y {
return Vec::new();
return (Vec::new(), None);
}
let interact_radius_sq = (16.0_f32).powi(2);
@ -1853,8 +1859,6 @@ impl PreparedPlot {
.min_by_key(|(_, elem)| elem.dist_sq.ord())
.filter(|(_, elem)| elem.dist_sq <= interact_radius_sq);
let mut cursors = Vec::new();
let plot = items::PlotConfig {
ui,
transform,
@ -1862,8 +1866,11 @@ impl PreparedPlot {
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.id()
} else {
let value = transform.value_from_position(pointer);
items::rulers_at_value(
@ -1875,9 +1882,10 @@ impl PreparedPlot {
&mut cursors,
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.
pub auto_bounds: Vec2b,
/// Which item is hovered?
pub hovered_item: Option<String>,
/// Display string of the hovered legend item if any.
pub hovered_legend_item: Option<String>,
/// Which items _not_ to show?
pub hidden_items: ahash::HashSet<String>,