1use crate::aes::Aesthetic;
2use crate::data::{DataFrame, Value};
3use crate::scale::ScaleSet;
4
5use super::Stat;
6
7pub 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 let hex_w = x_range / self.bins_x as f64;
59 let hex_h = y_range / self.bins_y as f64;
60
61 let mut counts: std::collections::HashMap<(i64, i64), usize> =
63 std::collections::HashMap::new();
64
65 for i in 0..n {
66 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 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 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}