Skip to main content

forge_charts/
hover.rs

1//! Hover state + pure pixel-to-index math.
2
3/// Snapshot of where the cursor is in the chart.
4///
5/// `client_x` / `client_y` are viewport-relative CSS pixels (what
6/// `MouseEvent.client_x()` reports). The tooltip div uses them to
7/// position itself relative to its containing block via inline style.
8#[derive(Debug, Clone, Copy, PartialEq)]
9pub struct HoverState {
10    /// Index into the chart's data array.
11    pub index: usize,
12    /// Viewport X in CSS pixels (for tooltip positioning).
13    pub client_x: f64,
14    /// Viewport Y in CSS pixels.
15    pub client_y: f64,
16}
17
18/// Snap a client-X pixel coordinate to the nearest data-point index.
19///
20/// `plot_left` and `plot_width` come from the SVG element's
21/// `getBoundingClientRect()`. With one point, always returns 0.
22/// Out-of-range pixels clamp to the nearest endpoint.
23#[must_use]
24pub fn pixel_to_index(client_x: f64, plot_left: f64, plot_width: f64, n_points: usize) -> usize {
25    if n_points <= 1 || plot_width <= 0.0 {
26        return 0;
27    }
28    let last = n_points - 1;
29    let fraction = ((client_x - plot_left) / plot_width).clamp(0.0, 1.0);
30    let raw = fraction * last as f64;
31    let snapped = raw.round() as usize;
32    snapped.min(last)
33}
34
35#[cfg(test)]
36mod tests {
37    use super::*;
38
39    #[test]
40    fn pixel_to_index_snaps_to_nearest() {
41        // 10 points (indices 0..=9), plot 0..1000 px wide.
42        assert_eq!(pixel_to_index(0.0, 0.0, 1000.0, 10), 0);
43        assert_eq!(pixel_to_index(1000.0, 0.0, 1000.0, 10), 9);
44        // Middle of the plot → middle index.
45        assert_eq!(pixel_to_index(500.0, 0.0, 1000.0, 10), 5);
46        // Just past midpoint between indices snaps to the higher one.
47        assert_eq!(pixel_to_index(56.0, 0.0, 1000.0, 10), 1);
48        assert_eq!(pixel_to_index(54.0, 0.0, 1000.0, 10), 0);
49    }
50
51    #[test]
52    fn pixel_to_index_clamps_at_bounds() {
53        assert_eq!(pixel_to_index(-100.0, 0.0, 1000.0, 5), 0);
54        assert_eq!(pixel_to_index(2000.0, 0.0, 1000.0, 5), 4);
55    }
56
57    #[test]
58    fn pixel_to_index_handles_single_point() {
59        assert_eq!(pixel_to_index(500.0, 0.0, 1000.0, 1), 0);
60    }
61
62    #[test]
63    fn pixel_to_index_respects_plot_left_offset() {
64        // Plot starts at x=200 in viewport, 800 wide.
65        // Click at viewport x=600 → 50% into the plot → middle index.
66        assert_eq!(pixel_to_index(600.0, 200.0, 800.0, 11), 5);
67    }
68
69    #[test]
70    fn pixel_to_index_handles_zero_width() {
71        assert_eq!(pixel_to_index(500.0, 0.0, 0.0, 5), 0);
72    }
73}