Skip to main content

ggplot_rs/stat/
bindot.rs

1use crate::aes::Aesthetic;
2use crate::data::{DataFrame, Value};
3use crate::scale::ScaleSet;
4
5use super::Stat;
6
7/// Bin-packing stat for dot plots. Bins x values and assigns stacking y positions.
8pub struct StatBindot {
9    pub bins: usize,
10}
11
12impl Default for StatBindot {
13    fn default() -> Self {
14        StatBindot { bins: 30 }
15    }
16}
17
18impl Stat for StatBindot {
19    fn compute_group(&self, data: &DataFrame, _scales: &ScaleSet) -> DataFrame {
20        let x_col = match data.column("x") {
21            Some(c) => c,
22            None => return DataFrame::new(),
23        };
24
25        let values: Vec<f64> = x_col.iter().filter_map(|v| v.as_f64()).collect();
26        if values.is_empty() {
27            return DataFrame::new();
28        }
29
30        let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
31        let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
32
33        let (min, max) = if (max - min).abs() < f64::EPSILON {
34            (min - 0.5, max + 0.5)
35        } else {
36            (min, max)
37        };
38
39        let bin_width = (max - min) / self.bins as f64;
40        let mut bin_counts = vec![0usize; self.bins];
41
42        // Assign each value to a bin and track its stack position
43        let mut x_vals = Vec::with_capacity(values.len());
44        let mut y_vals = Vec::with_capacity(values.len());
45
46        for &v in &values {
47            let bin = ((v - min) / bin_width).floor() as usize;
48            let bin = bin.min(self.bins - 1);
49            let center = min + (bin as f64 + 0.5) * bin_width;
50            let stack_pos = bin_counts[bin];
51            bin_counts[bin] += 1;
52
53            x_vals.push(Value::Float(center));
54            y_vals.push(Value::Float(stack_pos as f64 + 0.5)); // center of dot
55        }
56
57        let mut result = DataFrame::new();
58        result.add_column("x".to_string(), x_vals);
59        result.add_column("y".to_string(), y_vals);
60
61        // Carry over grouping columns
62        for col_name in &["color", "fill", "group"] {
63            if let Some(col) = data.column(col_name) {
64                if col.len() == values.len() {
65                    result.add_column(col_name.to_string(), col.to_vec());
66                }
67            }
68        }
69
70        result
71    }
72
73    fn required_aes(&self) -> Vec<Aesthetic> {
74        vec![Aesthetic::X]
75    }
76
77    fn name(&self) -> &str {
78        "bindot"
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn test_bindot_basic() {
88        let mut data = DataFrame::new();
89        let x_vals: Vec<Value> = vec![1.0, 1.1, 1.2, 2.0, 2.1, 3.0]
90            .into_iter()
91            .map(Value::Float)
92            .collect();
93        data.add_column("x".to_string(), x_vals);
94
95        let stat = StatBindot { bins: 3 };
96        let scales = ScaleSet::new();
97        let result = stat.compute_group(&data, &scales);
98
99        assert_eq!(result.nrows(), 6);
100        assert!(result.column("x").is_some());
101        assert!(result.column("y").is_some());
102    }
103}