diff --git a/demo/src/plot_demo.rs b/demo/src/plot_demo.rs index 70f2421f..163af259 100644 --- a/demo/src/plot_demo.rs +++ b/demo/src/plot_demo.rs @@ -8,8 +8,8 @@ use egui::{ use egui_plot::{ Arrows, AxisHints, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, CoordinatesFormatter, Corner, - GridInput, GridMark, HLine, Legend, Line, LineStyle, MarkerShape, Plot, PlotImage, PlotPoint, - PlotPoints, PlotResponse, Points, Polygon, Text, VLine, + GridInput, GridMark, HLine, HoverPosition, Legend, Line, LineStyle, MarkerShape, Plot, + PlotImage, PlotPoint, PlotPoints, PlotResponse, Points, Polygon, Text, VLine, }; // ---------------------------------------------------------------------------- @@ -636,14 +636,19 @@ impl CustomAxesDemo { } }; - let label_fmt = |_s: &str, val: &PlotPoint| { - format!( + let label_fmt = |position: &HoverPosition<'_>| match position { + HoverPosition::NearDataPoint { + plot_name: _, + position, + index: _, + } => Some(format!( "Day {d}, {h}:{m:02}\n{p:.2}%", - d = day(val.x), - h = hour(val.x), - m = minute(val.x), - p = percent(val.y) - ) + d = day(position.x), + h = hour(position.x), + m = minute(position.x), + p = percent(position.y) + )), + HoverPosition::Elsewhere { position: _ } => None, }; ui.label("Zoom in on the X-axis to see hours and minutes"); @@ -669,7 +674,7 @@ impl CustomAxesDemo { .custom_x_axes(x_axes) .custom_y_axes(y_axes) .x_grid_spacer(Self::x_grid) - .label_formatter(label_fmt) + .enumerated_label_formatter(label_fmt) .show(ui, |plot_ui| { plot_ui.line(Self::logistic_fn()); }) diff --git a/egui_plot/src/items/mod.rs b/egui_plot/src/items/mod.rs index 0bf255dc..bfaf80d2 100644 --- a/egui_plot/src/items/mod.rs +++ b/egui_plot/src/items/mod.rs @@ -14,7 +14,9 @@ use egui::{ use emath::Float as _; use rect_elem::{RectElement, highlighted_color}; -use super::{Cursor, LabelFormatter, PlotBounds, PlotTransform}; +use crate::HoverPosition; + +use super::{Cursor, NewLabelFormatter, PlotBounds, PlotTransform}; pub use bar::Bar; pub use box_elem::{BoxElem, BoxSpread}; @@ -161,7 +163,7 @@ pub trait PlotItem { shapes: &mut Vec, cursors: &mut Vec, plot: &PlotConfig<'_>, - label_formatter: &LabelFormatter<'_>, + label_formatter: &NewLabelFormatter<'_>, ) { let points = match self.geometry() { PlotGeometry::Points(points) => points, @@ -187,7 +189,7 @@ pub trait PlotItem { rulers_and_tooltip_at_value( plot_area_response, value, - self.name(), + Some((self.name(), elem.index)), plot, cursors, label_formatter, @@ -1441,7 +1443,7 @@ impl PlotItem for BarChart { shapes: &mut Vec, cursors: &mut Vec, plot: &PlotConfig<'_>, - _: &LabelFormatter<'_>, + _: &NewLabelFormatter<'_>, ) { let bar = &self.bars[elem.index]; @@ -1568,7 +1570,7 @@ impl PlotItem for BoxPlot { shapes: &mut Vec, cursors: &mut Vec, plot: &PlotConfig<'_>, - _: &LabelFormatter<'_>, + _: &NewLabelFormatter<'_>, ) { let box_plot = &self.boxes[elem.index]; @@ -1690,14 +1692,15 @@ fn add_rulers_and_text( /// and a label describing the coordinate. /// /// `value` is used to for text displaying X/Y coordinates. +/// `nearest_point` contains the nearest point from a plot with its name and index. #[allow(clippy::too_many_arguments)] pub(super) fn rulers_and_tooltip_at_value( plot_area_response: &egui::Response, value: PlotPoint, - name: &str, + nearest_point: Option<(&str, usize)>, plot: &PlotConfig<'_>, cursors: &mut Vec, - label_formatter: &LabelFormatter<'_>, + label_formatter: &NewLabelFormatter<'_>, ) { if plot.show_x { cursors.push(Cursor::Vertical { x: value.x }); @@ -1707,17 +1710,25 @@ pub(super) fn rulers_and_tooltip_at_value( } let text = if let Some(custom_label) = label_formatter { - custom_label(name, &value) + let hover_position = match nearest_point { + Some((name, index)) => HoverPosition::NearDataPoint { + plot_name: name, + position: value, + index: index, + }, + None => HoverPosition::Elsewhere { position: value }, + }; + custom_label(&hover_position) } else { - let prefix = if name.is_empty() { - String::new() - } else { + let prefix = if let Some((name, _)) = nearest_point { format!("{name}\n") + } else { + String::new() }; 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 { + let result = if plot.show_x && plot.show_y { format!( "{}x = {:.*}\ny = {:.*}", prefix, x_decimals, value.x, y_decimals, value.y @@ -1728,25 +1739,28 @@ pub(super) fn rulers_and_tooltip_at_value( format!("{}y = {:.*}", prefix, y_decimals, value.y) } else { unreachable!() - } + }; + Some(result) }; - // We show the tooltip as soon as we're hovering the plot area: - let mut tooltip = egui::Tooltip::always_open( - plot_area_response.ctx.clone(), - plot_area_response.layer_id, - plot_area_response.id, - PopupAnchor::Pointer, - ); + if let Some(text) = text { + // We show the tooltip as soon as we're hovering the plot area: + let mut tooltip = egui::Tooltip::always_open( + plot_area_response.ctx.clone(), + plot_area_response.layer_id, + plot_area_response.id, + PopupAnchor::Pointer, + ); - let tooltip_width = plot_area_response.ctx.style().spacing.tooltip_width; + let tooltip_width = plot_area_response.ctx.style().spacing.tooltip_width; - tooltip.popup = tooltip.popup.width(tooltip_width); + tooltip.popup = tooltip.popup.width(tooltip_width); - tooltip.gap(12.0).show(|ui| { - ui.set_max_width(tooltip_width); - ui.label(text); - }); + tooltip.gap(12.0).show(|ui| { + ui.set_max_width(tooltip_width); + ui.label(text); + }); + } } fn find_closest_rect<'a, T>( diff --git a/egui_plot/src/lib.rs b/egui_plot/src/lib.rs index c597d257..209c9ef8 100644 --- a/egui_plot/src/lib.rs +++ b/egui_plot/src/lib.rs @@ -45,6 +45,9 @@ use legend::LegendWidget; type LabelFormatterFn<'a> = dyn Fn(&str, &PlotPoint) -> String + 'a; pub type LabelFormatter<'a> = Option>>; +type NewLabelFormatterFn<'a> = dyn Fn(&HoverPosition<'_>) -> Option + 'a; +pub type NewLabelFormatter<'a> = Option>>; + type GridSpacerFn<'a> = dyn Fn(GridInput) -> Vec + 'a; type GridSpacer<'a> = Box>; @@ -92,6 +95,23 @@ pub enum Cursor { Vertical { x: f64 }, } +/// Indicates the position of the cursor in a plot for hover purposes. +#[derive(Copy, Clone, PartialEq)] +pub enum HoverPosition<'a> { + NearDataPoint { + /// The name of the plot whose data point is nearest to the cursor + plot_name: &'a str, + /// The position of the nearest data point + position: PlotPoint, + /// The index of the nearest data point in its plot + index: usize, + }, + Elsewhere { + /// The position in the plot over which the cursor hovers + position: PlotPoint, + }, +} + /// Contains the cursors drawn for a plot widget in a single frame. #[derive(PartialEq, Clone)] struct PlotFrameCursors { @@ -178,7 +198,7 @@ pub struct Plot<'a> { show_x: bool, show_y: bool, - label_formatter: LabelFormatter<'a>, + label_formatter: NewLabelFormatter<'a>, coordinates_formatter: Option<(Corner, CoordinatesFormatter<'a>)>, x_axes: Vec>, // default x axes y_axes: Vec>, // default y axes @@ -425,6 +445,46 @@ impl<'a> Plot<'a> { pub fn label_formatter( mut self, label_formatter: impl Fn(&str, &PlotPoint) -> String + 'a, + ) -> Self { + let inner_box = Box::new(label_formatter); + self.label_formatter = Some(Box::new(move |position| { + Some(match position { + HoverPosition::NearDataPoint { + plot_name, + position, + index: _, + } => inner_box(plot_name, &position), + HoverPosition::Elsewhere { position: _ } => "".to_owned(), + }) + })); + self + } + + /// Provide a function to customize the on-hover label for the x and y axis. + /// This is a generalized version of `label_formatter` that also provides the point's index, + /// and allows for the tooltip to be hidden conditionally by returning an Option + /// + /// ``` + /// # 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()] + /// }).collect(); + /// let line = Line::new("sin", sin); + /// Plot::new("my_plot").view_aspect(2.0) + /// .enumerated_label_formatter(|context| { + /// match context { + /// Some((name, index)) => format!("{}: {:.*}%", name, 1, index), + /// Elsewhere { ... } => None, + /// } + /// }) + /// .show(ui, |plot_ui| plot_ui.line(line)); + /// # }); + /// ``` + pub fn enumerated_label_formatter( + mut self, + label_formatter: impl Fn(&HoverPosition<'_>) -> Option + 'a, ) -> Self { self.label_formatter = Some(Box::new(label_formatter)); self @@ -1598,7 +1658,7 @@ struct PreparedPlot<'a> { items: Vec>, show_x: bool, show_y: bool, - label_formatter: LabelFormatter<'a>, + label_formatter: NewLabelFormatter<'a>, coordinates_formatter: Option<(Corner, CoordinatesFormatter<'a>)>, // axis_formatters: [AxisFormatter; 2], transform: PlotTransform, @@ -1858,7 +1918,7 @@ impl PreparedPlot<'_> { items::rulers_and_tooltip_at_value( plot_area_response, value, - "", + None, &plot, &mut cursors, label_formatter,