Skip to content

Commit 879fb3d

Browse files
lucasmerlinCopilotemilk
authored
Find closest line segment for Line plots (#234)
It will now find the line closest to the cursor, so you don't need to hover the points exactly https://github.com/user-attachments/assets/6aec87b3-97ad-4f41-94a4-f38ea1b985da --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
1 parent c51d2b5 commit 879fb3d

2 files changed

Lines changed: 55 additions & 1 deletion

File tree

egui_plot/src/items/series.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use egui::Shape;
99
use egui::Stroke;
1010
use egui::Ui;
1111
use egui::epaint::PathStroke;
12+
use emath::Float as _;
1213
use emath::NumExt as _;
1314
use emath::Pos2;
1415
use emath::Rect;
@@ -20,10 +21,11 @@ use crate::bounds::PlotBounds;
2021
use crate::bounds::PlotPoint;
2122
use crate::colors::DEFAULT_FILL_ALPHA;
2223
use crate::data::PlotPoints;
24+
use crate::items::ClosestElem;
2325
use crate::items::PlotGeometry;
2426
use crate::items::PlotItem;
2527
use crate::items::PlotItemBase;
26-
use crate::math::y_intersection;
28+
use crate::math::{dist_sq_to_segment, y_intersection};
2729

2830
/// A series of values forming a path.
2931
pub struct Line<'a> {
@@ -240,6 +242,41 @@ impl PlotItem for Line<'_> {
240242
style.style_line(values_tf, final_stroke, base.highlight, shapes);
241243
}
242244

245+
fn find_closest(&self, point: Pos2, transform: &PlotTransform) -> Option<ClosestElem> {
246+
let points = self.series.points();
247+
248+
// Fallback for 0 or 1 point: behave like PlotGeometry::Points and
249+
// pick the closest point (if any), so single-point lines remain hoverable.
250+
if points.len() <= 1 {
251+
return points
252+
.iter()
253+
.enumerate()
254+
.map(|(index, value)| {
255+
let pos = transform.position_from_point(value);
256+
let dist_sq = point.distance_sq(pos);
257+
ClosestElem { index, dist_sq }
258+
})
259+
.min_by_key(|e| e.dist_sq.ord());
260+
}
261+
262+
points
263+
.windows(2)
264+
.enumerate()
265+
.map(|(i, w)| {
266+
let p0 = transform.position_from_point(&w[0]);
267+
let p1 = transform.position_from_point(&w[1]);
268+
let dist_sq = dist_sq_to_segment(point, [p0, p1]);
269+
// Pick the closer endpoint so the tooltip shows a real data point
270+
let index = if point.distance_sq(p0) <= point.distance_sq(p1) {
271+
i
272+
} else {
273+
i + 1
274+
};
275+
ClosestElem { index, dist_sq }
276+
})
277+
.min_by_key(|e| e.dist_sq.ord())
278+
}
279+
243280
fn initialize(&mut self, x_range: RangeInclusive<f64>) {
244281
self.series.generate_points(x_range);
245282
}

egui_plot/src/math.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,23 @@ pub fn y_intersection(p1: &Pos2, p2: &Pos2, y: f32) -> Option<f32> {
1212
.then_some(((y * (p1.x - p2.x)) - (p1.x * p2.y - p1.y * p2.x)) / (p1.y - p2.y))
1313
}
1414

15+
/// Squared distance from point `p` to the line segment `a`–`b`.
16+
pub fn dist_sq_to_segment(p: Pos2, [a, b]: [Pos2; 2]) -> f32 {
17+
let ab = b - a;
18+
let ab_len_sq = ab.length_sq();
19+
20+
if ab_len_sq == 0.0 {
21+
// Degenerate segment: treat as a single point.
22+
return p.distance_sq(a);
23+
}
24+
25+
let ap = p - a;
26+
let t = ab.dot(ap) / ab_len_sq;
27+
let t = t.clamp(0.0, 1.0);
28+
let closest = a + t * ab;
29+
p.distance_sq(closest)
30+
}
31+
1532
pub fn find_closest_rect<'a, T>(
1633
rects: impl IntoIterator<Item = &'a T>,
1734
point: Pos2,

0 commit comments

Comments
 (0)