From 988b12cdaf461ceb566b6aa32e676981ddf0b675 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Tue, 10 Mar 2026 14:18:09 +0100 Subject: [PATCH 1/3] Find closest line segment for Line plots --- egui_plot/src/items/series.rs | 24 +++++++++++++++++++++++- egui_plot/src/math.rs | 10 ++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/egui_plot/src/items/series.rs b/egui_plot/src/items/series.rs index 95ade02e..9d670cc6 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,26 @@ impl PlotItem for Line<'_> { style.style_line(values_tf, final_stroke, base.highlight, shapes); } + fn find_closest(&self, point: Pos2, transform: &PlotTransform) -> Option { + self.series + .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..7d08cbc7 100644 --- a/egui_plot/src/math.rs +++ b/egui_plot/src/math.rs @@ -12,6 +12,16 @@ 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 ap = p - a; + let t = ab.dot(ap) / ab.length_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, From b19eef6c460911e440b39e1cde83689db505f14f Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 10 Mar 2026 14:29:39 +0100 Subject: [PATCH 2/3] Handle single point case Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- egui_plot/src/items/series.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/egui_plot/src/items/series.rs b/egui_plot/src/items/series.rs index 9d670cc6..497ff52b 100644 --- a/egui_plot/src/items/series.rs +++ b/egui_plot/src/items/series.rs @@ -242,8 +242,23 @@ impl PlotItem for Line<'_> { } fn find_closest(&self, point: Pos2, transform: &PlotTransform) -> Option { - self.series - .points() + 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)| { From 5c03ce667af451860b6f55df4091338a33c69b19 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 10 Mar 2026 14:31:08 +0100 Subject: [PATCH 3/3] Handle overlapping points Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- egui_plot/src/math.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/egui_plot/src/math.rs b/egui_plot/src/math.rs index 7d08cbc7..2671f1d0 100644 --- a/egui_plot/src/math.rs +++ b/egui_plot/src/math.rs @@ -15,8 +15,15 @@ pub fn y_intersection(p1: &Pos2, p2: &Pos2, y: f32) -> Option { /// 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.length_sq(); + 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)