diff --git a/egui_plot/src/items/series.rs b/egui_plot/src/items/series.rs index 95ade02e..497ff52b 100644 --- a/egui_plot/src/items/series.rs +++ b/egui_plot/src/items/series.rs @@ -9,6 +9,7 @@ use egui::Shape; use egui::Stroke; use egui::Ui; use egui::epaint::PathStroke; +use emath::Float as _; use emath::NumExt as _; use emath::Pos2; use emath::Rect; @@ -20,10 +21,11 @@ use crate::bounds::PlotBounds; use crate::bounds::PlotPoint; use crate::colors::DEFAULT_FILL_ALPHA; use crate::data::PlotPoints; +use crate::items::ClosestElem; use crate::items::PlotGeometry; use crate::items::PlotItem; use crate::items::PlotItemBase; -use crate::math::y_intersection; +use crate::math::{dist_sq_to_segment, y_intersection}; /// A series of values forming a path. pub struct Line<'a> { @@ -239,6 +241,41 @@ impl PlotItem for Line<'_> { style.style_line(values_tf, final_stroke, base.highlight, shapes); } + fn find_closest(&self, point: Pos2, transform: &PlotTransform) -> Option { + let points = self.series.points(); + + // Fallback for 0 or 1 point: behave like PlotGeometry::Points and + // pick the closest point (if any), so single-point lines remain hoverable. + if points.len() <= 1 { + return points + .iter() + .enumerate() + .map(|(index, value)| { + let pos = transform.position_from_point(value); + let dist_sq = point.distance_sq(pos); + ClosestElem { index, dist_sq } + }) + .min_by_key(|e| e.dist_sq.ord()); + } + + points + .windows(2) + .enumerate() + .map(|(i, w)| { + let p0 = transform.position_from_point(&w[0]); + let p1 = transform.position_from_point(&w[1]); + let dist_sq = dist_sq_to_segment(point, p0, p1); + // Pick the closer endpoint so the tooltip shows a real data point + let index = if point.distance_sq(p0) <= point.distance_sq(p1) { + i + } else { + i + 1 + }; + ClosestElem { index, dist_sq } + }) + .min_by_key(|e| e.dist_sq.ord()) + } + fn initialize(&mut self, x_range: RangeInclusive) { self.series.generate_points(x_range); } diff --git a/egui_plot/src/math.rs b/egui_plot/src/math.rs index 2b2163f4..2671f1d0 100644 --- a/egui_plot/src/math.rs +++ b/egui_plot/src/math.rs @@ -12,6 +12,23 @@ pub fn y_intersection(p1: &Pos2, p2: &Pos2, y: f32) -> Option { .then_some(((y * (p1.x - p2.x)) - (p1.x * p2.y - p1.y * p2.x)) / (p1.y - p2.y)) } +/// Squared distance from point `p` to the line segment `a`–`b`. +pub fn dist_sq_to_segment(p: Pos2, a: Pos2, b: Pos2) -> f32 { + let ab = b - a; + let ab_len_sq = ab.length_sq(); + + if ab_len_sq == 0.0 { + // Degenerate segment: treat as a single point. + return p.distance_sq(a); + } + + let ap = p - a; + let t = ab.dot(ap) / ab_len_sq; + let t = t.clamp(0.0, 1.0); + let closest = a + t * ab; + p.distance_sq(closest) +} + pub fn find_closest_rect<'a, T>( rects: impl IntoIterator, point: Pos2,