use crate::coord::PlotArea;
use crate::mark::rect::sub_band_range;
use crate::scale::{BandScale, Scale};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HitZone {
Plot,
AxisX,
AxisY,
Outside,
}
pub fn hit_zone(area: &PlotArea, x: f64, y: f64) -> HitZone {
let r = area.rect;
let within_x = x >= r.x && x <= r.right();
let within_y = y >= r.y && y <= r.bottom();
match (within_x, within_y) {
(true, true) => HitZone::Plot,
(true, false) if y > r.bottom() => HitZone::AxisX,
(false, true) if x < r.x => HitZone::AxisY,
_ => HitZone::Outside,
}
}
pub fn nearest_point_x(area: &PlotArea, xscale: &dyn Scale, yscale: &dyn Scale, points: &[(f64, f64)], px: f64) -> Option<usize> {
let mut best: Option<(usize, f64)> = None;
for (i, &(dx, dy)) in points.iter().enumerate() {
if !dx.is_finite() || !dy.is_finite() {
continue; }
let screen_y = area.y(yscale, dy);
if screen_y < area.rect.y - 1e-6 || screen_y > area.rect.bottom() + 1e-6 {
continue;
}
let screen_x = area.x(xscale, dx);
let dist = (screen_x - px).abs();
if best.map_or(true, |(_, best_dist)| dist < best_dist) {
best = Some((i, dist));
}
}
best.map(|(i, _)| i)
}
pub fn bar_index_at(area: &PlotArea, band: &BandScale, px: f64) -> Option<usize> {
(0..band.len()).find(|&i| {
let (x0, x1) = area.x_band(band, i);
px >= x0 && px <= x1
})
}
pub fn nearest_point_xy(area: &PlotArea, xscale: &dyn Scale, yscale: &dyn Scale, points: &[(f64, f64)], px: f64, py: f64) -> Option<usize> {
let mut best: Option<(usize, f64)> = None;
for (i, &(dx, dy)) in points.iter().enumerate() {
if !dx.is_finite() || !dy.is_finite() {
continue; }
let sx = area.x(xscale, dx);
let sy = area.y(yscale, dy);
let dist = ((sx - px).powi(2) + (sy - py).powi(2)).sqrt();
if best.map_or(true, |(_, best_dist)| dist < best_dist) {
best = Some((i, dist));
}
}
best.map(|(i, _)| i)
}
pub fn nearest_point_x_multi(area: &PlotArea, xscale: &dyn Scale, yscale: &dyn Scale, series: &[&[(f64, f64)]], px: f64) -> Option<(usize, usize)> {
let mut best: Option<(usize, usize, f64)> = None;
for (si, points) in series.iter().enumerate() {
for (pi, &(dx, dy)) in points.iter().enumerate() {
if !dx.is_finite() || !dy.is_finite() {
continue; }
let screen_y = area.y(yscale, dy);
if screen_y < area.rect.y - 1e-6 || screen_y > area.rect.bottom() + 1e-6 {
continue;
}
let screen_x = area.x(xscale, dx);
let dist = (screen_x - px).abs();
if best.map_or(true, |(_, _, best_dist)| dist < best_dist) {
best = Some((si, pi, dist));
}
}
}
best.map(|(si, pi, _)| (si, pi))
}
pub fn bar_series_at(x0: f64, x1: f64, series_count: usize, px: f64) -> Option<usize> {
(0..series_count).find(|&i| {
let (sx0, sx1) = sub_band_range(x0, x1, series_count, i);
px >= sx0 && px <= sx1
})
}
pub fn stacked_series_at(segments: &[(f64, f64)], py: f64) -> Option<usize> {
segments.iter().position(|&(top, bottom)| {
let (lo, hi) = (top.min(bottom), top.max(bottom));
py >= lo && py <= hi
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scale::LinearScale;
use uzor::types::Rect;
fn area() -> PlotArea {
PlotArea::new(Rect::new(50.0, 50.0, 100.0, 80.0))
}
#[test]
fn hit_zone_corners_are_inclusive_plot() {
let a = area();
assert_eq!(hit_zone(&a, 50.0, 50.0), HitZone::Plot); assert_eq!(hit_zone(&a, 150.0, 50.0), HitZone::Plot); assert_eq!(hit_zone(&a, 50.0, 130.0), HitZone::Plot); assert_eq!(hit_zone(&a, 150.0, 130.0), HitZone::Plot); }
#[test]
fn hit_zone_below_and_left_of_plot_are_axis_strips() {
let a = area();
assert_eq!(hit_zone(&a, 75.0, 140.0), HitZone::AxisX);
assert_eq!(hit_zone(&a, 10.0, 75.0), HitZone::AxisY);
}
#[test]
fn hit_zone_elsewhere_is_outside() {
let a = area();
assert_eq!(hit_zone(&a, 10.0, 10.0), HitZone::Outside); assert_eq!(hit_zone(&a, 75.0, 10.0), HitZone::Outside); assert_eq!(hit_zone(&a, 200.0, 75.0), HitZone::Outside); }
#[test]
fn nearest_point_x_picks_exact_index() {
let a = PlotArea::new(Rect::new(0.0, 0.0, 200.0, 100.0));
let xscale = LinearScale::new(0.0, 20.0);
let yscale = LinearScale::new(0.0, 10.0);
let points = [(0.0, 0.0), (10.0, 5.0), (20.0, 0.0)];
let px = a.x(&xscale, 10.0);
assert_eq!(nearest_point_x(&a, &xscale, &yscale, &points, px), Some(1));
}
#[test]
fn nearest_point_x_picks_closer_of_two_at_a_mid_position() {
let a = PlotArea::new(Rect::new(0.0, 0.0, 200.0, 100.0));
let xscale = LinearScale::new(0.0, 20.0);
let yscale = LinearScale::new(0.0, 10.0);
let points = [(0.0, 0.0), (10.0, 5.0), (20.0, 0.0)];
let px = a.x(&xscale, 6.0);
assert_eq!(nearest_point_x(&a, &xscale, &yscale, &points, px), Some(1));
}
#[test]
fn nearest_point_x_empty_points_is_none() {
let a = PlotArea::new(Rect::new(0.0, 0.0, 200.0, 100.0));
let xscale = LinearScale::new(0.0, 20.0);
let yscale = LinearScale::new(0.0, 10.0);
assert_eq!(nearest_point_x(&a, &xscale, &yscale, &[], 50.0), None);
}
#[test]
fn bar_index_at_inside_and_outside_bands() {
let a = PlotArea::new(Rect::new(0.0, 0.0, 300.0, 100.0));
let band = BandScale::new(vec!["a".to_owned(), "b".to_owned(), "c".to_owned()], 0.3);
let (x0, x1) = a.x_band(&band, 1);
let center = (x0 + x1) / 2.0;
assert_eq!(bar_index_at(&a, &band, center), Some(1));
assert_eq!(bar_index_at(&a, &band, x0 - 1.0), None);
assert_eq!(bar_index_at(&a, &band, 10_000.0), None);
}
#[test]
fn bar_index_at_empty_band_is_always_none() {
let a = PlotArea::new(Rect::new(0.0, 0.0, 300.0, 100.0));
let band = BandScale::new(Vec::new(), 0.1);
assert_eq!(bar_index_at(&a, &band, 50.0), None);
}
#[test]
fn nearest_point_x_multi_picks_the_globally_nearest_point_across_series() {
let a = PlotArea::new(Rect::new(0.0, 0.0, 200.0, 100.0));
let xscale = LinearScale::new(0.0, 20.0);
let yscale = LinearScale::new(0.0, 10.0);
let series_a: Vec<(f64, f64)> = vec![(0.0, 0.0), (20.0, 0.0)];
let series_b: Vec<(f64, f64)> = vec![(10.0, 5.0)];
let series: Vec<&[(f64, f64)]> = vec![&series_a, &series_b];
let px = a.x(&xscale, 10.0);
assert_eq!(nearest_point_x_multi(&a, &xscale, &yscale, &series, px), Some((1, 0)));
}
#[test]
fn nearest_point_x_multi_empty_series_is_none() {
let a = PlotArea::new(Rect::new(0.0, 0.0, 200.0, 100.0));
let xscale = LinearScale::new(0.0, 20.0);
let yscale = LinearScale::new(0.0, 10.0);
assert_eq!(nearest_point_x_multi(&a, &xscale, &yscale, &[], 50.0), None);
let empty: Vec<(f64, f64)> = Vec::new();
assert_eq!(nearest_point_x_multi(&a, &xscale, &yscale, &[&empty], 50.0), None);
}
#[test]
fn nearest_point_xy_picks_the_true_2d_nearest_not_just_nearest_by_x() {
let a = PlotArea::new(Rect::new(0.0, 0.0, 200.0, 100.0));
let xscale = LinearScale::new(0.0, 20.0);
let yscale = LinearScale::new(0.0, 10.0);
let points = [(10.0, 0.0), (11.0, 9.0)];
let px = a.x(&xscale, 10.5);
let py = a.y(&yscale, 9.0);
assert_eq!(nearest_point_xy(&a, &xscale, &yscale, &points, px, py), Some(1));
}
#[test]
fn nearest_point_xy_empty_points_is_none() {
let a = PlotArea::new(Rect::new(0.0, 0.0, 200.0, 100.0));
let xscale = LinearScale::new(0.0, 20.0);
let yscale = LinearScale::new(0.0, 10.0);
assert_eq!(nearest_point_xy(&a, &xscale, &yscale, &[], 50.0, 50.0), None);
}
#[test]
fn nearest_point_x_skips_a_non_finite_sample_instead_of_corrupting_the_distance_comparison() {
let a = PlotArea::new(Rect::new(0.0, 0.0, 200.0, 100.0));
let xscale = LinearScale::new(0.0, 20.0);
let yscale = LinearScale::new(0.0, 10.0);
let points = [(f64::NAN, f64::NAN), (10.0, 5.0), (0.0, 0.0)];
let px = a.x(&xscale, 10.0);
assert_eq!(nearest_point_x(&a, &xscale, &yscale, &points, px), Some(1), "the finite nearest point must win, never the leading NaN");
}
#[test]
fn nearest_point_xy_skips_non_finite_samples() {
let a = PlotArea::new(Rect::new(0.0, 0.0, 200.0, 100.0));
let xscale = LinearScale::new(0.0, 20.0);
let yscale = LinearScale::new(0.0, 10.0);
let points = [(f64::NAN, f64::NAN), (10.0, 5.0)];
let px = a.x(&xscale, 10.0);
let py = a.y(&yscale, 5.0);
assert_eq!(nearest_point_xy(&a, &xscale, &yscale, &points, px, py), Some(1));
}
#[test]
fn nearest_point_x_multi_skips_non_finite_samples_across_series() {
let a = PlotArea::new(Rect::new(0.0, 0.0, 200.0, 100.0));
let xscale = LinearScale::new(0.0, 20.0);
let yscale = LinearScale::new(0.0, 10.0);
let series_a: Vec<(f64, f64)> = vec![(f64::NAN, f64::NAN), (0.0, 0.0)];
let series_b: Vec<(f64, f64)> = vec![(10.0, 5.0)];
let series: Vec<&[(f64, f64)]> = vec![&series_a, &series_b];
let px = a.x(&xscale, 10.0);
assert_eq!(nearest_point_x_multi(&a, &xscale, &yscale, &series, px), Some((1, 0)));
}
#[test]
fn bar_series_at_resolves_the_correct_sub_band_and_none_in_the_gap() {
let (x0, x1) = (0.0, 300.0);
let n = 3;
let (sx0, sx1) = sub_band_range(x0, x1, n, 1);
let center = (sx0 + sx1) / 2.0;
assert_eq!(bar_series_at(x0, x1, n, center), Some(1));
let (_, prev_end) = sub_band_range(x0, x1, n, 0);
let (next_start, _) = sub_band_range(x0, x1, n, 1);
if next_start > prev_end {
let gap_mid = (prev_end + next_start) / 2.0;
assert_eq!(bar_series_at(x0, x1, n, gap_mid), None);
}
}
#[test]
fn stacked_series_at_resolves_the_segment_containing_py_and_none_outside_all() {
let segments = [(80.0, 100.0), (50.0, 80.0), (20.0, 50.0)];
assert_eq!(stacked_series_at(&segments, 90.0), Some(0));
assert_eq!(stacked_series_at(&segments, 65.0), Some(1));
assert_eq!(stacked_series_at(&segments, 30.0), Some(2));
assert_eq!(stacked_series_at(&segments, 10.0), None);
}
}