diff --git a/egui_plot/src/items/bar_chart.rs b/egui_plot/src/items/bar_chart.rs index 10dfc768..64944961 100644 --- a/egui_plot/src/items/bar_chart.rs +++ b/egui_plot/src/items/bar_chart.rs @@ -210,7 +210,7 @@ impl PlotItem for BarChart { shapes: &mut Vec, cursors: &mut Vec, plot: &PlotConfig<'_>, - _: &LabelFormatter<'_>, + _: &Option>, ) { let bar = &self.bars[elem.index]; diff --git a/egui_plot/src/items/box_plot.rs b/egui_plot/src/items/box_plot.rs index 529fc05f..6bb416b7 100644 --- a/egui_plot/src/items/box_plot.rs +++ b/egui_plot/src/items/box_plot.rs @@ -176,7 +176,7 @@ impl PlotItem for BoxPlot { shapes: &mut Vec, cursors: &mut Vec, plot: &PlotConfig<'_>, - _: &LabelFormatter<'_>, + _: &Option>, ) { let box_plot = &self.boxes[elem.index]; diff --git a/egui_plot/src/items/heatmap.rs b/egui_plot/src/items/heatmap.rs index bf549478..2e72548b 100644 --- a/egui_plot/src/items/heatmap.rs +++ b/egui_plot/src/items/heatmap.rs @@ -460,7 +460,7 @@ impl PlotItem for Heatmap { shapes: &mut Vec, _cursors: &mut Vec, plot: &PlotConfig<'_>, - _: &LabelFormatter<'_>, + _: &Option>, ) { let (rect, color, text) = self.tile_view_info(plot.ui, plot.transform, elem.index); let mut mesh = Mesh::default(); diff --git a/egui_plot/src/items/mod.rs b/egui_plot/src/items/mod.rs index 140804ca..99513430 100644 --- a/egui_plot/src/items/mod.rs +++ b/egui_plot/src/items/mod.rs @@ -9,7 +9,6 @@ use std::ops::RangeInclusive; use egui::Align2; use egui::Color32; use egui::Id; -use egui::NumExt as _; use egui::PopupAnchor; use egui::Pos2; use egui::Shape; @@ -92,6 +91,9 @@ pub struct PlotConfig<'a> { /// Whether to show the y-axis value. pub show_y: bool, + + /// Whether to show the crosshair rulers. + pub show_crosshair: bool, } /// Trait shared by things that can be drawn in the plot. @@ -172,7 +174,7 @@ pub trait PlotItem { shapes: &mut Vec, cursors: &mut Vec, plot: &PlotConfig<'_>, - label_formatter: &LabelFormatter<'_>, + label_formatter: &Option>, ) { let points = match self.geometry() { PlotGeometry::Points(points) => points, @@ -273,44 +275,28 @@ pub(super) fn rulers_and_tooltip_at_value( name: &str, plot: &PlotConfig<'_>, cursors: &mut Vec, - label_formatter: &LabelFormatter<'_>, + label_formatter: &Option>, ) { - if plot.show_x { - cursors.push(Cursor::Vertical { x: value.x }); - } - if plot.show_y { - cursors.push(Cursor::Horizontal { y: value.y }); - } - - let text = if let Some(custom_label) = label_formatter { - let label = custom_label(name, &value); - if label.is_empty() { - return; + // Add crosshair rulers if enabled + if plot.show_crosshair { + if plot.show_x { + cursors.push(Cursor::Vertical { x: value.x }); } - label - } else { - let prefix = if name.is_empty() { - String::new() - } else { - format!("{name}\n") - }; - 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 { - format!( - "{}x = {:.*}\ny = {:.*}", - prefix, x_decimals, value.x, y_decimals, value.y - ) - } else if plot.show_x { - format!("{}x = {:.*}", prefix, x_decimals, value.x) - } else if plot.show_y { - format!("{}y = {:.*}", prefix, y_decimals, value.y) - } else { - unreachable!() + if plot.show_y { + cursors.push(Cursor::Horizontal { y: value.y }); } + } + + // Only show tooltip if label_formatter is provided + let Some(custom_label) = label_formatter else { + return; }; + let text = custom_label(name, &value); + if text.is_empty() { + return; + } + // 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(), diff --git a/egui_plot/src/label.rs b/egui_plot/src/label.rs index f84a1ff9..c24cc566 100644 --- a/egui_plot/src/label.rs +++ b/egui_plot/src/label.rs @@ -19,4 +19,15 @@ pub fn format_number(number: f64, num_decimals: usize) -> String { type LabelFormatterFn<'a> = dyn Fn(&str, &PlotPoint) -> String + 'a; /// Optional label formatter function for customizing hover labels. -pub type LabelFormatter<'a> = Option>>; +pub type LabelFormatter<'a> = Box>; + +/// Default label formatter that shows the x and y coordinates with 3 decimal +/// places. +pub fn default_label_formatter(name: &str, value: &PlotPoint) -> String { + let prefix = if name.is_empty() { + String::new() + } else { + format!("{name}\n") + }; + format!("{}x = {:.3}\ny = {:.3}", prefix, value.x, value.y) +} diff --git a/egui_plot/src/lib.rs b/egui_plot/src/lib.rs index 3f349955..fafa3fcf 100644 --- a/egui_plot/src/lib.rs +++ b/egui_plot/src/lib.rs @@ -62,6 +62,7 @@ pub use crate::items::Span; pub use crate::items::Text; pub use crate::items::VLine; pub use crate::label::LabelFormatter; +pub use crate::label::default_label_formatter; pub use crate::label::format_number; pub use crate::memory::PlotMemory; pub use crate::overlays::ColorConflictHandling; diff --git a/egui_plot/src/plot.rs b/egui_plot/src/plot.rs index b6fd3085..cca1f763 100644 --- a/egui_plot/src/plot.rs +++ b/egui_plot/src/plot.rs @@ -115,7 +115,8 @@ pub struct Plot<'a> { show_x: bool, show_y: bool, - label_formatter: LabelFormatter<'a>, + show_crosshair: bool, + label_formatter: Option>, coordinates_formatter: Option<(Corner, CoordinatesFormatter<'a>)>, x_axes: Vec>, // default x axes y_axes: Vec>, // default y axes @@ -166,6 +167,7 @@ impl<'a> Plot<'a> { show_x: true, show_y: true, + show_crosshair: true, label_formatter: None, coordinates_formatter: None, x_axes: vec![AxisHints::new(Axis::X)], @@ -272,6 +274,13 @@ impl<'a> Plot<'a> { self } + /// Show the crosshair when hovering. Default: `true`. + #[inline] + pub fn show_crosshair(mut self, show: bool) -> Self { + self.show_crosshair = show; + self + } + /// Always keep the X-axis centered. Default: `false`. #[inline] pub fn center_x_axis(mut self, on: bool) -> Self { @@ -1544,6 +1553,7 @@ impl<'a> Plot<'a> { transform, show_x: show_xy.x, show_y: show_xy.y, + show_crosshair: self.show_crosshair, }; let mut cursors = Vec::new(); diff --git a/examples/lines/src/app.rs b/examples/lines/src/app.rs index e004fd54..471f7e74 100644 --- a/examples/lines/src/app.rs +++ b/examples/lines/src/app.rs @@ -15,6 +15,7 @@ use egui_plot::Line; use egui_plot::LineStyle; use egui_plot::Plot; use egui_plot::PlotPoints; +use egui_plot::default_label_formatter; #[derive(Clone, Copy, PartialEq)] pub struct LineExample { @@ -27,6 +28,8 @@ pub struct LineExample { coordinates: bool, show_axes: bool, show_grid: bool, + show_crosshair: bool, + show_labels: bool, line_style: LineStyle, gradient: bool, gradient_fill: bool, @@ -46,6 +49,8 @@ impl Default for LineExample { coordinates: true, show_axes: true, show_grid: true, + show_crosshair: true, + show_labels: true, line_style: LineStyle::Solid, gradient: false, gradient_fill: false, @@ -61,31 +66,25 @@ impl LineExample { ui.group(|ui| { ui.vertical(|ui| { ui.label("Circle:"); - ui.add( - egui::DragValue::new(&mut self.circle_radius) - .speed(0.1) - .range(0.0..=f64::INFINITY) - .prefix("r: "), - ); + ui.add(egui::DragValue::new(&mut self.circle_radius).speed(0.1).prefix("r: ")); ui.horizontal(|ui| { ui.add(egui::DragValue::new(&mut self.circle_center.x).speed(0.1).prefix("x: ")); ui.add(egui::DragValue::new(&mut self.circle_center.y).speed(1.0).prefix("y: ")); }); - }) + }); }); ui.vertical(|ui| { ui.checkbox(&mut self.show_axes, "Show axes"); ui.checkbox(&mut self.show_grid, "Show grid"); - ui.checkbox(&mut self.coordinates, "Show coordinates on hover") - .on_hover_text("Can take a custom formatting function."); + ui.checkbox(&mut self.show_crosshair, "Show crosshair"); + ui.checkbox(&mut self.coordinates, "Show coordinates"); + ui.checkbox(&mut self.show_labels, "Show hover labels"); }); ui.vertical(|ui| { ui.style_mut().wrap_mode = Some(TextWrapMode::Extend); ui.checkbox(&mut self.animate, "Animate"); - ui.checkbox(&mut self.square, "Square view") - .on_hover_text("Always keep the viewport square."); - ui.checkbox(&mut self.proportional, "Proportional data axes") - .on_hover_text("Tick are the same size on both axes."); + ui.checkbox(&mut self.square, "Square view"); + ui.checkbox(&mut self.proportional, "Proportional data axes"); ComboBox::from_label("Line style") .selected_text(self.line_style.to_string()) .show_ui(ui, |ui| { @@ -103,8 +102,6 @@ impl LineExample { ui.vertical(|ui| { ui.checkbox(&mut self.gradient, "Gradient line"); ui.add_enabled(self.gradient, Checkbox::new(&mut self.gradient_fill, "Gradient fill")); - }); - ui.vertical(|ui| { ui.checkbox(&mut self.invert_x, "Invert X axis"); ui.checkbox(&mut self.invert_y, "Invert Y axis"); }); @@ -113,10 +110,9 @@ impl LineExample { } fn circle(&self) -> Line<'_> { - let n = 512; - let points: PlotPoints<'_> = (0..=n) + let points: PlotPoints<'_> = (0..=512) .map(|i| { - let t = egui::remap(i as f64, 0.0..=(n as f64), 0.0..=TAU); + let t = egui::remap(i as f64, 0.0..=512.0, 0.0..=TAU); [ self.circle_radius * t.cos() + self.circle_center.x as f64, self.circle_radius * t.sin() + self.circle_center.y as f64, @@ -166,11 +162,11 @@ impl LineExample { ui.ctx().request_repaint(); self.time += ui.input(|i| i.unstable_dt).at_most(1.0 / 30.0) as f64; } - let mut plot = Plot::new("lines_demo") .legend(Legend::default().title("Lines")) .show_axes(self.show_axes) .show_grid(self.show_grid) + .show_crosshair(self.show_crosshair) .invert_x(self.invert_x) .invert_y(self.invert_y); if self.square { @@ -182,6 +178,9 @@ impl LineExample { if self.coordinates { plot = plot.coordinates_formatter(Corner::LeftBottom, CoordinatesFormatter::default()); } + if self.show_labels { + plot = plot.label_formatter(default_label_formatter); + } plot.show(ui, |plot_ui| { plot_ui.line(self.circle()); plot_ui.line(self.sin());