Skip to main content

ggplot_rs/position/
jitterdodge.rs

1use crate::data::{DataFrame, Value};
2
3use super::{Position, PositionParams};
4
5/// Combined jitter and dodge position adjustment.
6/// Useful for showing individual points within dodged groups (e.g., over boxplots).
7pub struct PositionJitterDodge {
8    pub jitter_width: f64,
9    pub jitter_height: f64,
10    pub dodge_width: f64,
11    seed: u64,
12}
13
14impl PositionJitterDodge {
15    pub fn new(jitter_width: f64, jitter_height: f64) -> Self {
16        PositionJitterDodge {
17            jitter_width,
18            jitter_height,
19            dodge_width: 0.9,
20            seed: 42,
21        }
22    }
23
24    pub fn with_dodge_width(mut self, width: f64) -> Self {
25        self.dodge_width = width;
26        self
27    }
28
29    pub fn with_seed(mut self, seed: u64) -> Self {
30        self.seed = seed;
31        self
32    }
33
34    fn pseudo_random(seed: u64, i: usize) -> f64 {
35        // Simple deterministic hash → uniform in [-0.5, 0.5]
36        let h = seed
37            .wrapping_mul(6364136223846793005)
38            .wrapping_add(i as u64)
39            .wrapping_mul(1442695040888963407);
40        let h = h ^ (h >> 33);
41        let h = h.wrapping_mul(0xff51afd7ed558ccd);
42        (h as f64 / u64::MAX as f64) - 0.5
43    }
44}
45
46impl Default for PositionJitterDodge {
47    fn default() -> Self {
48        PositionJitterDodge::new(0.4, 0.0)
49    }
50}
51
52impl Position for PositionJitterDodge {
53    fn compute(&self, data: &mut DataFrame, _params: &PositionParams) {
54        let x_col = match data.column("x") {
55            Some(c) => c.to_vec(),
56            None => return,
57        };
58
59        let group_col = data
60            .column("fill")
61            .or_else(|| data.column("color"))
62            .or_else(|| data.column("group"));
63
64        let group_keys: Vec<String> = match group_col {
65            Some(col) => col.iter().map(|v| v.to_group_key()).collect(),
66            None => {
67                // No groups — just jitter
68                let mut new_x = x_col;
69                for (i, v) in new_x.iter_mut().enumerate() {
70                    if let Some(f) = v.as_f64() {
71                        *v =
72                            Value::Float(f + Self::pseudo_random(self.seed, i) * self.jitter_width);
73                    }
74                }
75                if let Some(col) = data.column_mut("x") {
76                    *col = new_x;
77                }
78                return;
79            }
80        };
81
82        let mut unique_groups: Vec<String> = Vec::new();
83        for g in &group_keys {
84            if !unique_groups.contains(g) {
85                unique_groups.push(g.clone());
86            }
87        }
88
89        let n_groups = unique_groups.len() as f64;
90        let group_width = if n_groups > 1.0 {
91            self.dodge_width / n_groups
92        } else {
93            0.0
94        };
95
96        let mut new_x = x_col.clone();
97        for (i, (x, group)) in x_col.iter().zip(group_keys.iter()).enumerate() {
98            if let Some(x_val) = x.as_f64() {
99                // Dodge offset
100                let dodge_offset = if n_groups > 1.0 {
101                    let group_idx = unique_groups.iter().position(|g| g == group).unwrap() as f64;
102                    (group_idx - (n_groups - 1.0) / 2.0) * group_width
103                } else {
104                    0.0
105                };
106
107                // Jitter offset
108                let jitter_x = Self::pseudo_random(self.seed, i) * self.jitter_width;
109
110                new_x[i] = Value::Float(x_val + dodge_offset + jitter_x);
111            }
112        }
113
114        if let Some(col) = data.column_mut("x") {
115            *col = new_x;
116        }
117
118        // Y jitter
119        if self.jitter_height.abs() > f64::EPSILON {
120            if let Some(col) = data.column_mut("y") {
121                for (i, v) in col.iter_mut().enumerate() {
122                    if let Some(f) = v.as_f64() {
123                        *v = Value::Float(
124                            f + Self::pseudo_random(self.seed.wrapping_add(1), i)
125                                * self.jitter_height,
126                        );
127                    }
128                }
129            }
130        }
131    }
132
133    fn name(&self) -> &str {
134        "jitterdodge"
135    }
136}