Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 15 additions & 10 deletions demo/src/plot_demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

// ----------------------------------------------------------------------------
Expand Down Expand Up @@ -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");
Expand All @@ -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());
})
Expand Down
66 changes: 40 additions & 26 deletions egui_plot/src/items/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -161,7 +163,7 @@ pub trait PlotItem {
shapes: &mut Vec<Shape>,
cursors: &mut Vec<Cursor>,
plot: &PlotConfig<'_>,
label_formatter: &LabelFormatter<'_>,
label_formatter: &NewLabelFormatter<'_>,
) {
let points = match self.geometry() {
PlotGeometry::Points(points) => points,
Expand All @@ -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,
Expand Down Expand Up @@ -1441,7 +1443,7 @@ impl PlotItem for BarChart {
shapes: &mut Vec<Shape>,
cursors: &mut Vec<Cursor>,
plot: &PlotConfig<'_>,
_: &LabelFormatter<'_>,
_: &NewLabelFormatter<'_>,
) {
let bar = &self.bars[elem.index];

Expand Down Expand Up @@ -1568,7 +1570,7 @@ impl PlotItem for BoxPlot {
shapes: &mut Vec<Shape>,
cursors: &mut Vec<Cursor>,
plot: &PlotConfig<'_>,
_: &LabelFormatter<'_>,
_: &NewLabelFormatter<'_>,
) {
let box_plot = &self.boxes[elem.index];

Expand Down Expand Up @@ -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<Cursor>,
label_formatter: &LabelFormatter<'_>,
label_formatter: &NewLabelFormatter<'_>,
) {
if plot.show_x {
cursors.push(Cursor::Vertical { x: value.x });
Expand All @@ -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
Expand All @@ -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>(
Expand Down
66 changes: 63 additions & 3 deletions egui_plot/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ use legend::LegendWidget;
type LabelFormatterFn<'a> = dyn Fn(&str, &PlotPoint) -> String + 'a;
pub type LabelFormatter<'a> = Option<Box<LabelFormatterFn<'a>>>;

type NewLabelFormatterFn<'a> = dyn Fn(&HoverPosition<'_>) -> Option<String> + 'a;
pub type NewLabelFormatter<'a> = Option<Box<NewLabelFormatterFn<'a>>>;

type GridSpacerFn<'a> = dyn Fn(GridInput) -> Vec<GridMark> + 'a;
type GridSpacer<'a> = Box<GridSpacerFn<'a>>;

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<AxisHints<'a>>, // default x axes
y_axes: Vec<AxisHints<'a>>, // default y axes
Expand Down Expand Up @@ -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<String>
///
/// ```
/// # 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<String> + 'a,
) -> Self {
self.label_formatter = Some(Box::new(label_formatter));
self
Expand Down Expand Up @@ -1598,7 +1658,7 @@ struct PreparedPlot<'a> {
items: Vec<Box<dyn PlotItem + 'a>>,
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,
Expand Down Expand Up @@ -1858,7 +1918,7 @@ impl PreparedPlot<'_> {
items::rulers_and_tooltip_at_value(
plot_area_response,
value,
"",
None,
&plot,
&mut cursors,
label_formatter,
Expand Down