Skip to main content

ggplot_rs/stat/
binhex.rs

1use crate::aes::Aesthetic;
2use crate::data::{DataFrame, Value};
3use crate::scale::ScaleSet;
4
5use super::Stat;
6
7/// Hexagonal binning using axial coordinates.
8/// Output: x, y (hex centers), fill (count).
9pub struct StatBinHex {
10    pub bins_x: usize,
11    pub bins_y: usize,
12}
13
14impl Default for StatBinHex {
15    fn default() -> Self {
16        StatBinHex {
17            bins_x: 30,
18            bins_y: 30,
19        }
20    }
21}
22
23impl Stat for StatBinHex {
24    fn compute_group(&self, data: &DataFrame, _scales: &ScaleSet) -> DataFrame {
25        let x_col = match data.column("x") {
26            Some(c) => c,
27            None => return DataFrame::new(),
28        };
29        let y_col = match data.column("y") {
30            Some(c) => c,
31            None => return DataFrame::new(),
32        };
33
34        let xs: Vec<f64> = x_col.iter().filter_map(|v| v.as_f64()).collect();
35        let ys: Vec<f64> = y_col.iter().filter_map(|v| v.as_f64()).collect();
36        let n = xs.len().min(ys.len());
37        if n == 0 {
38            return DataFrame::new();
39        }
40
41        let x_min = xs.iter().cloned().fold(f64::INFINITY, f64::min);
42        let x_max = xs.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
43        let y_min = ys.iter().cloned().fold(f64::INFINITY, f64::min);
44        let y_max = ys.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
45
46        let x_range = if (x_max - x_min).abs() < f64::EPSILON {
47            1.0
48        } else {
49            x_max - x_min
50        };
51        let y_range = if (y_max - y_min).abs() < f64::EPSILON {
52            1.0
53        } else {
54            y_max - y_min
55        };
56
57        // Hex size
58        let hex_w = x_range / self.bins_x as f64;
59        let hex_h = y_range / self.bins_y as f64;
60
61        // Use HashMap with (col, row) keys for hex bins
62        let mut counts: std::collections::HashMap<(i64, i64), usize> =
63            std::collections::HashMap::new();
64
65        for i in 0..n {
66            // Convert to hex grid coordinates
67            let col = ((xs[i] - x_min) / hex_w).floor() as i64;
68            let row = ((ys[i] - y_min) / hex_h).floor() as i64;
69
70            // For offset rows, shift x
71            let adj_col = if row % 2 != 0 {
72                ((xs[i] - x_min - hex_w * 0.5) / hex_w).floor() as i64
73            } else {
74                col
75            };
76
77            *counts.entry((adj_col, row)).or_insert(0) += 1;
78        }
79
80        let mut x_vals = Vec::new();
81        let mut y_vals = Vec::new();
82        let mut fill_vals = Vec::new();
83
84        for (&(col, row), &count) in &counts {
85            if count == 0 {
86                continue;
87            }
88            // Hex center
89            let cx =
90                x_min + (col as f64 + 0.5) * hex_w + if row % 2 != 0 { hex_w * 0.5 } else { 0.0 };
91            let cy = y_min + (row as f64 + 0.5) * hex_h;
92
93            x_vals.push(Value::Float(cx));
94            y_vals.push(Value::Float(cy));
95            fill_vals.push(Value::Float(count as f64));
96        }
97
98        let mut result = DataFrame::new();
99        result.add_column("x".to_string(), x_vals);
100        result.add_column("y".to_string(), y_vals);
101        result.add_column("fill".to_string(), fill_vals);
102
103        result
104    }
105
106    fn required_aes(&self) -> Vec<Aesthetic> {
107        vec![Aesthetic::X, Aesthetic::Y]
108    }
109
110    fn name(&self) -> &str {
111        "binhex"
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn test_binhex_basic() {
121        let mut data = DataFrame::new();
122        let x_vals: Vec<Value> = (0..100).map(|i| Value::Float(i as f64 / 10.0)).collect();
123        let y_vals: Vec<Value> = (0..100).map(|i| Value::Float(i as f64 / 5.0)).collect();
124        data.add_column("x".to_string(), x_vals);
125        data.add_column("y".to_string(), y_vals);
126
127        let stat = StatBinHex {
128            bins_x: 5,
129            bins_y: 5,
130        };
131        let scales = ScaleSet::new();
132        let result = stat.compute_group(&data, &scales);
133
134        assert!(result.nrows() > 0);
135        assert!(result.column("x").is_some());
136        assert!(result.column("y").is_some());
137        assert!(result.column("fill").is_some());
138    }
139}