dataviz\figure\display/
hoverhistogram.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
use ab_glyph::FontRef;
use image::ImageBuffer;
use imageproc::drawing::{draw_line_segment_mut, draw_text_mut, text_size};

use crate::figure::{canvas::pixelcanvas::PixelCanvas, figuretypes::histogram::Histogram};

use super::hover::Hover;

impl Hover for Histogram {
    fn find_closest_point(
        &self,
        mouse_x: u32,
        _mouse_y: u32,
        canvas: &PixelCanvas,
    ) -> Option<((f64, f64), f64)> {
        let bin_data = self.calculate_bins();

        let bin_width = (bin_data[1].0 - bin_data[0].0).abs();
        let scale_x = (canvas.width - 2 * canvas.margin) as f64 / self.bins as f64;

        let mut closest_bin = None;
        let mut min_distance = f64::MAX;

        for (i, &(bin_start, freq)) in bin_data.iter().enumerate() {
            let bin_end = bin_start + bin_width; // End of the bin
            let bin_x = canvas.margin as f64 + i as f64 * scale_x;

            let distance = (mouse_x as f64 - bin_x).abs(); // Distance to mouse x
            if distance < min_distance {
                min_distance = distance;
                closest_bin = Some(((bin_start, bin_end), freq)); // Bin range and frequency
            }
        }

        closest_bin
    }

    fn to_canvas_coordinates(&self, x: f64, y: f64, canvas: &PixelCanvas) -> (u32, u32) {
        let bin_data = self.calculate_bins();
        let bin_width = (bin_data[1].0 - bin_data[0].0).abs();
        let x_min = bin_data[0].0; // Start of the first bin
        let x_max = x_min + bin_width * self.bins as f64;

        let scale_x = (canvas.width - 2 * canvas.margin) as f64 / (x_max - x_min);
        let scale_y = (canvas.height - 2 * canvas.margin) as f64
            / bin_data.iter().map(|(_, f)| *f).fold(0.0, f64::max);

        let px = ((x - x_min) * scale_x + canvas.margin as f64) as u32;
        let py = (canvas.height as f64 - canvas.margin as f64 - y * scale_y) as u32;

        (px, py)
    }

    /// Handle hover functionality and return updated buffer if applicable
    fn handle_hover(&self, mouse_x: u32, mouse_y: u32, canvas: &PixelCanvas) -> Option<Vec<u32>> {
        // Find the closest bin's range and total value
        if let Some(((bin_start, bin_end), freq)) =
            self.find_closest_point(mouse_x, mouse_y, canvas)
        {
            let mut img =
                ImageBuffer::from_raw(canvas.width, canvas.height, canvas.buffer.clone()).unwrap();

            // Format the text as: ([x-start, x-end], total: y-value)
            let bin_info = format!("([{:.2}, {:.2}], total: {:.2})", bin_start, bin_end, freq);

            // Calculate text size for background rectangle
            let font = self.get_font(self.config.font_label.as_bytes());
            let scale = ab_glyph::PxScale { x: 12.0, y: 12.0 };
            let text_size = text_size(scale, &font, &bin_info).0 as i32;

            let rect_x = mouse_x as i32 + 15; // Rectangle start X, slightly offset from mouse
            let rect_y = mouse_y as i32 - 20; // Rectangle start Y
            let rect_width = text_size + 20; // Add padding for rectangle width
            let rect_height = 25; // Height of the rectangle

            // Adjust rectangle coordinates to stay within canvas bounds
            let rect_x = rect_x.max(0).min((canvas.width as i32 - rect_width) as i32);
            let rect_y = rect_y
                .max(0)
                .min((canvas.height as i32 - rect_height) as i32);

            // Draw white rectangle as background
            for y in rect_y..(rect_y + rect_height) {
                for x in rect_x..(rect_x + rect_width) {
                    if x >= 0 && y >= 0 && (x as u32) < canvas.width && (y as u32) < canvas.height {
                        img.put_pixel(x as u32, y as u32, image::Rgb([255, 255, 255]));
                        // White
                    }
                }
            }

            // Draw edges for the rectangle
            for x in rect_x..(rect_x + rect_width) {
                if rect_y >= 0 && (x as u32) < canvas.width {
                    img.put_pixel(x as u32, rect_y as u32, image::Rgb([0, 0, 0])); // Top edge
                    img.put_pixel(
                        x as u32,
                        (rect_y + rect_height - 1) as u32,
                        image::Rgb([0, 0, 0]),
                    ); // Bottom edge
                }
            }
            for y in rect_y..(rect_y + rect_height) {
                if rect_x >= 0 && (y as u32) < canvas.height {
                    img.put_pixel(rect_x as u32, y as u32, image::Rgb([0, 0, 0])); // Left edge
                    img.put_pixel(
                        (rect_x + rect_width - 1) as u32,
                        y as u32,
                        image::Rgb([0, 0, 0]),
                    ); // Right edge
                }
            }

            // Draw the formatted text on top of the rectangle
            draw_text_mut(
                &mut img,
                image::Rgb([0, 0, 0]), // Text color
                rect_x + 10,           // Add padding to position text
                rect_y + 5,
                scale,
                &font,
                &bin_info,
            );

            // Draw a line from the bin's top point to the mouse location
            if let Some((bin_px, bin_py)) = self
                .to_canvas_coordinates((bin_start + bin_end) / 2.0, freq, canvas)
                .into()
            {
                draw_line_segment_mut(
                    &mut img,
                    (bin_px as f32, bin_py as f32),
                    (mouse_x as f32, mouse_y as f32),
                    image::Rgb([255, 0, 0]), // Line color
                );
            }

            return Some(
                img.pixels()
                    .map(|pixel: &image::Rgb<u8>| {
                        let [r, g, b] = pixel.0;
                        (r as u32) << 16 | (g as u32) << 8 | b as u32
                    })
                    .collect(),
            );
        }
        None
    }

    fn get_font<'a>(&self, font_data: &'a [u8]) -> FontRef<'a> {
        FontRef::try_from_slice(&font_data).unwrap()
    }
}