1use crate::data::{DataFrame, Value};
2
3use super::{Position, PositionParams, DODGE_N_COL, DODGE_OFFSET_COL};
4
5#[derive(Clone, Debug, Default, PartialEq)]
20pub struct PositionDodge {
21 pub width: Option<f64>,
23 pub reverse: bool,
25 pub preserve_single: bool,
29}
30
31#[allow(non_upper_case_globals)]
34pub const PositionDodge: PositionDodge = PositionDodge {
35 width: None,
36 reverse: false,
37 preserve_single: false,
38};
39
40impl PositionDodge {
41 pub fn new(width: f64) -> Self {
43 PositionDodge {
44 width: Some(width),
45 reverse: false,
46 preserve_single: false,
47 }
48 }
49
50 pub fn with_reverse(mut self, reverse: bool) -> Self {
52 self.reverse = reverse;
53 self
54 }
55
56 pub fn with_preserve_single(mut self) -> Self {
58 self.preserve_single = true;
59 self
60 }
61}
62
63pub fn position_dodge(width: f64) -> PositionDodge {
65 PositionDodge::new(width)
66}
67
68impl Position for PositionDodge {
69 fn compute(&self, data: &mut DataFrame, params: &PositionParams) {
70 dodge_rows(
71 data,
72 self.width.unwrap_or(params.width),
73 None,
74 self.reverse,
75 self.preserve_single,
76 params.group_order.as_deref(),
77 );
78 }
79
80 fn name(&self) -> &str {
81 "dodge"
82 }
83
84 fn per_panel(&self) -> bool {
89 true
90 }
91
92 fn group_order(&self, data: &DataFrame) -> Option<Vec<String>> {
99 group_keys(data).map(|keys| dedup_order(&keys))
100 }
101}
102
103pub(crate) fn dedup_order(keys: &[String]) -> Vec<String> {
107 let set: indexmap::IndexSet<&str> = keys.iter().map(|k| k.as_str()).collect();
108 set.into_iter().map(str::to_string).collect()
109}
110
111pub(crate) fn group_keys(data: &DataFrame) -> Option<Vec<String>> {
114 let cols: Vec<&[Value]> = ["group", "fill", "color"]
115 .iter()
116 .filter_map(|c| data.column(c))
117 .collect();
118 if cols.is_empty() {
119 return None;
120 }
121 Some(
122 (0..data.nrows())
123 .map(|i| {
124 cols.iter()
125 .map(|c| c[i].to_group_key())
126 .collect::<Vec<_>>()
127 .join("\u{1f}")
128 })
129 .collect(),
130 )
131}
132
133pub(crate) fn dodge_rows(
144 data: &mut DataFrame,
145 width: f64,
146 padding: Option<f64>,
147 reverse: bool,
148 preserve_single: bool,
149 external_order: Option<&[String]>,
150) {
151 let Some(x_col) = data.column("x").map(|c| c.to_vec()) else {
152 return;
153 };
154 let Some(keys) = group_keys(data) else {
155 return;
156 };
157 let width = if width.is_finite() && width > 0.0 {
158 width
159 } else {
160 0.9
161 };
162 let mut order: indexmap::IndexSet<&str> = match external_order {
168 Some(ext) => ext.iter().map(|s| s.as_str()).collect(),
169 None => keys.iter().map(|k| k.as_str()).collect(),
170 };
171 if order.len() <= 1 {
172 return;
173 }
174 if reverse {
175 order.reverse();
176 }
177 let rank = |k: &str| order.get_index_of(k).unwrap_or(0);
178
179 let mut slots: indexmap::IndexMap<String, Vec<usize>> = indexmap::IndexMap::new();
181 for (i, x) in x_col.iter().enumerate() {
182 if x.is_na() {
183 continue;
184 }
185 let present = slots.entry(x.to_group_key()).or_default();
186 let r = rank(&keys[i]);
187 if !present.contains(&r) {
188 present.push(r);
189 }
190 }
191 for present in slots.values_mut() {
192 present.sort_unstable();
193 }
194 let max_n = slots.values().map(Vec::len).max().unwrap_or(1) as f64;
196
197 let discrete = x_col
198 .iter()
199 .any(|v| matches!(v, Value::Str(_) | Value::Bool(_)));
200 let mut offsets = vec![Value::Float(0.0); x_col.len()];
201 let mut counts = vec![Value::Float(1.0); x_col.len()];
202 let mut new_x = x_col.clone();
203 let mut new_xmin = data.column("xmin").map(|c| c.to_vec());
204 let mut new_xmax = data.column("xmax").map(|c| c.to_vec());
205 for (i, x) in x_col.iter().enumerate() {
206 let Some(present) = slots.get(&x.to_group_key()).filter(|_| !x.is_na()) else {
207 continue;
208 };
209 let n = present.len() as f64;
210 let k = present
211 .iter()
212 .position(|&r| r == rank(&keys[i]))
213 .unwrap_or(0) as f64;
214 let n_width = if preserve_single { max_n } else { n };
215 let group_width = width / n_width;
216 let offset = (k - (n - 1.0) / 2.0) * group_width;
217 if discrete {
218 offsets[i] = Value::Float(offset);
219 counts[i] = Value::Float(n_width);
220 continue;
221 }
222 let Some(xv) = x.as_f64() else {
223 continue;
224 };
225 let center = xv + offset;
226 new_x[i] = Value::Float(center);
227 if let Some(pad) = padding {
228 let half = group_width * (1.0 - pad) / 2.0;
229 if let Some(c) = new_xmin.as_mut() {
230 c[i] = Value::Float(center - half);
231 }
232 if let Some(c) = new_xmax.as_mut() {
233 c[i] = Value::Float(center + half);
234 }
235 }
236 }
237
238 if discrete {
239 if let Some(col) = data.column_mut(DODGE_OFFSET_COL) {
240 *col = offsets;
241 } else {
242 data.add_column(DODGE_OFFSET_COL.to_string(), offsets);
243 }
244 if let Some(col) = data.column_mut(DODGE_N_COL) {
245 *col = counts;
246 } else {
247 data.add_column(DODGE_N_COL.to_string(), counts);
248 }
249 return;
250 }
251 if let Some(col) = data.column_mut("x") {
252 *col = new_x;
253 }
254 if padding.is_some() {
255 for (name, vals) in [("xmin", new_xmin), ("xmax", new_xmax)] {
256 if let (Some(vals), Some(col)) = (vals, data.column_mut(name)) {
257 *col = vals;
258 }
259 }
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 fn frame(x: Vec<Value>, g: &[&str]) -> DataFrame {
268 let mut df = DataFrame::new();
269 df.add_column("x".into(), x);
270 df.add_column(
271 "color".into(),
272 g.iter().map(|s| Value::Str(s.to_string())).collect(),
273 );
274 df
275 }
276
277 fn col(df: &DataFrame, c: &str) -> Vec<f64> {
278 df.column(c)
279 .unwrap()
280 .iter()
281 .map(|v| v.as_f64().unwrap())
282 .collect()
283 }
284
285 #[test]
286 fn continuous_dodge_matches_ggplot2() {
287 let mut df = frame(vec![Value::Float(1.0); 3], &["a", "b", "c"]);
290 PositionDodge::new(0.6).compute(&mut df, &PositionParams::default());
291 let x = col(&df, "x");
292 for (got, want) in x.iter().zip([0.8, 1.0, 1.2]) {
293 assert!((got - want).abs() < 1e-12);
294 }
295 let mut df = frame(vec![Value::Float(1.0); 3], &["a", "b", "c"]);
296 PositionDodge::new(0.6)
297 .with_reverse(true)
298 .compute(&mut df, &PositionParams::default());
299 let x = col(&df, "x");
300 assert!((x[0] - 1.2).abs() < 1e-12 && (x[2] - 0.8).abs() < 1e-12);
301 }
302
303 #[test]
304 fn discrete_dodge_stores_offsets_and_centres_partial_slots() {
305 let x: Vec<Value> = ["t1", "t1", "t2"]
306 .iter()
307 .map(|s| Value::Str(s.to_string()))
308 .collect();
309 let mut df = frame(x, &["m1", "m2", "m2"]);
310 PositionDodge::new(0.5).compute(&mut df, &PositionParams::default());
311 assert_eq!(df.column("x").unwrap()[0].as_str(), Some("t1"));
313 let off = col(&df, DODGE_OFFSET_COL);
314 assert!((off[0] + 0.125).abs() < 1e-12);
315 assert!((off[1] - 0.125).abs() < 1e-12);
316 assert_eq!(off[2], 0.0);
318 }
319
320 #[test]
321 fn preserve_single_keeps_element_width_in_partial_slots() {
322 let x: Vec<Value> = ["t1", "t1", "t1", "t2"]
323 .iter()
324 .map(|s| Value::Str(s.to_string()))
325 .collect();
326 let mut df = frame(x, &["a", "b", "c", "a"]);
327 PositionDodge::new(0.9)
328 .with_preserve_single()
329 .compute(&mut df, &PositionParams::default());
330 assert_eq!(col(&df, DODGE_N_COL), vec![3.0, 3.0, 3.0, 3.0]);
333 assert_eq!(col(&df, DODGE_OFFSET_COL)[3], 0.0);
334 let mut df = frame(
336 vec![Value::Float(1.0), Value::Float(1.0), Value::Float(2.0)],
337 &["a", "b", "a"],
338 );
339 PositionDodge::new(0.6)
340 .with_preserve_single()
341 .compute(&mut df, &PositionParams::default());
342 let xs = col(&df, "x");
343 assert!((xs[0] - 0.85).abs() < 1e-12 && (xs[1] - 1.15).abs() < 1e-12);
344 assert!((xs[2] - 2.0).abs() < 1e-12);
345 }
346
347 #[test]
348 fn unit_value_still_works() {
349 let mut df = frame(vec![Value::Float(1.0); 2], &["a", "b"]);
350 PositionDodge.compute(&mut df, &PositionParams::default());
351 let x = col(&df, "x");
352 assert!((x[0] - 0.775).abs() < 1e-12 && (x[1] - 1.225).abs() < 1e-12);
353 }
354}