Skip to main content

orbital_charts/engine/
hit_test.rs

1//! Nearest-neighbor hit testing for scatter charts.
2
3use crate::PlacedScatterPoint;
4
5/// Hit target returned from pointer lookup.
6#[derive(Clone, Debug, PartialEq)]
7pub struct HitTarget {
8    /// Series identifier.
9    pub series_id: String,
10    /// Data index within the series.
11    pub data_index: usize,
12    /// Distance from pointer to point in pixels.
13    pub distance: f64,
14}
15
16/// Find the nearest scatter point within an optional max radius.
17pub fn find_nearest_point(
18    px: f64,
19    py: f64,
20    points: &[PlacedScatterPoint],
21    max_radius: Option<f64>,
22) -> Option<HitTarget> {
23    let mut best: Option<HitTarget> = None;
24
25    for point in points {
26        let dx = px - point.px;
27        let dy = py - point.py;
28        let distance = (dx * dx + dy * dy).sqrt();
29
30        if let Some(max) = max_radius {
31            if distance > max {
32                continue;
33            }
34        }
35
36        if best.as_ref().is_none_or(|b| distance < b.distance) {
37            best = Some(HitTarget {
38                series_id: point.series_id.clone(),
39                data_index: point.data_index,
40                distance,
41            });
42        }
43    }
44
45    best
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51
52    fn point(px: f64, py: f64, series_id: &str, data_index: usize) -> PlacedScatterPoint {
53        PlacedScatterPoint {
54            px,
55            py,
56            series_id: series_id.into(),
57            data_index,
58        }
59    }
60
61    #[test]
62    fn find_nearest_point_picks_closest() {
63        let points = vec![point(10.0, 10.0, "a", 0), point(50.0, 50.0, "a", 1)];
64        let hit = find_nearest_point(12.0, 11.0, &points, None).unwrap();
65        assert_eq!(hit.data_index, 0);
66    }
67
68    #[test]
69    fn find_nearest_point_respects_max_radius() {
70        let points = vec![point(100.0, 100.0, "a", 0)];
71        assert!(find_nearest_point(0.0, 0.0, &points, Some(10.0)).is_none());
72        assert!(find_nearest_point(95.0, 100.0, &points, Some(10.0)).is_some());
73    }
74}