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 order_by_scale_levels(
118 order: Vec<String>,
119 data: &DataFrame,
120 scales: &crate::scale::ScaleSet,
121) -> Vec<String> {
122 use crate::aes::Aesthetic;
123 let Some(keys) = group_keys(data) else {
124 return order;
125 };
126 let ranks: Vec<Vec<f64>> = [
127 ("group", Aesthetic::Group),
128 ("fill", Aesthetic::Fill),
129 ("color", Aesthetic::Color),
130 ]
131 .iter()
132 .filter_map(|(c, aes)| Some((data.column(c)?, aes)))
133 .map(
134 |(col, aes)| match scales.get(aes).filter(|s| s.is_discrete()) {
135 Some(scale) => {
136 let mut scale = scale.clone_box();
137 scale.train(col);
138 col.iter().map(|v| scale.map(v)).collect()
139 }
140 None => {
141 let seen: indexmap::IndexSet<String> =
142 col.iter().map(Value::to_group_key).collect();
143 col.iter()
144 .map(|v| seen.get_index_of(&v.to_group_key()).unwrap_or(0) as f64)
145 .collect()
146 }
147 },
148 )
149 .collect();
150 let mut rank: indexmap::IndexMap<&str, Vec<f64>> = indexmap::IndexMap::new();
151 for (i, k) in keys.iter().enumerate() {
152 rank.entry(k.as_str())
153 .or_insert_with(|| ranks.iter().map(|r| r[i]).collect());
154 }
155 let mut order = order;
156 order.sort_by(|a, b| {
157 let (ra, rb) = (rank.get(a.as_str()), rank.get(b.as_str()));
158 ra.zip(rb).map_or(std::cmp::Ordering::Equal, |(ra, rb)| {
159 ra.iter()
160 .zip(rb)
161 .map(|(x, y)| x.total_cmp(y))
162 .find(|o| o.is_ne())
163 .unwrap_or(std::cmp::Ordering::Equal)
164 })
165 });
166 order
167}
168
169pub(crate) fn group_keys(data: &DataFrame) -> Option<Vec<String>> {
172 let cols: Vec<&[Value]> = ["group", "fill", "color"]
173 .iter()
174 .filter_map(|c| data.column(c))
175 .collect();
176 if cols.is_empty() {
177 return None;
178 }
179 Some(
180 (0..data.nrows())
181 .map(|i| {
182 cols.iter()
183 .map(|c| c[i].to_group_key())
184 .collect::<Vec<_>>()
185 .join("\u{1f}")
186 })
187 .collect(),
188 )
189}
190
191pub(crate) fn dodge_rows(
202 data: &mut DataFrame,
203 width: f64,
204 padding: Option<f64>,
205 reverse: bool,
206 preserve_single: bool,
207 external_order: Option<&[String]>,
208) {
209 let Some(x_col) = data.column("x").map(|c| c.to_vec()) else {
210 return;
211 };
212 let Some(keys) = group_keys(data) else {
213 return;
214 };
215 let width = if width.is_finite() && width > 0.0 {
216 width
217 } else {
218 0.9
219 };
220 let mut order: indexmap::IndexSet<&str> = match external_order {
226 Some(ext) => ext.iter().map(|s| s.as_str()).collect(),
227 None => keys.iter().map(|k| k.as_str()).collect(),
228 };
229 if order.len() <= 1 {
230 return;
231 }
232 if reverse {
233 order.reverse();
234 }
235 let rank = |k: &str| order.get_index_of(k).unwrap_or(0);
236
237 let mut slots: indexmap::IndexMap<String, Vec<usize>> = indexmap::IndexMap::new();
239 for (i, x) in x_col.iter().enumerate() {
240 if x.is_na() {
241 continue;
242 }
243 let present = slots.entry(x.to_group_key()).or_default();
244 let r = rank(&keys[i]);
245 if !present.contains(&r) {
246 present.push(r);
247 }
248 }
249 for present in slots.values_mut() {
250 present.sort_unstable();
251 }
252 let max_n = slots.values().map(Vec::len).max().unwrap_or(1) as f64;
254
255 let discrete = x_col
256 .iter()
257 .any(|v| matches!(v, Value::Str(_) | Value::Bool(_)));
258 let mut offsets = vec![Value::Float(0.0); x_col.len()];
259 let mut counts = vec![Value::Float(1.0); x_col.len()];
260 let mut new_x = x_col.clone();
261 let mut new_xmin = data.column("xmin").map(|c| c.to_vec());
262 let mut new_xmax = data.column("xmax").map(|c| c.to_vec());
263 for (i, x) in x_col.iter().enumerate() {
264 let Some(present) = slots.get(&x.to_group_key()).filter(|_| !x.is_na()) else {
265 continue;
266 };
267 let n = present.len() as f64;
268 let k = present
269 .iter()
270 .position(|&r| r == rank(&keys[i]))
271 .unwrap_or(0) as f64;
272 let n_width = if preserve_single { max_n } else { n };
273 let group_width = width / n_width;
274 let offset = (k - (n - 1.0) / 2.0) * group_width;
275 if discrete {
276 offsets[i] = Value::Float(offset);
277 counts[i] = Value::Float(n_width);
278 continue;
279 }
280 let Some(xv) = x.as_f64() else {
281 continue;
282 };
283 let center = xv + offset;
284 new_x[i] = Value::Float(center);
285 if let Some(pad) = padding {
286 let half = group_width * (1.0 - pad) / 2.0;
287 if let Some(c) = new_xmin.as_mut() {
288 c[i] = Value::Float(center - half);
289 }
290 if let Some(c) = new_xmax.as_mut() {
291 c[i] = Value::Float(center + half);
292 }
293 }
294 }
295
296 if discrete {
297 if let Some(col) = data.column_mut(DODGE_OFFSET_COL) {
298 *col = offsets;
299 } else {
300 data.add_column(DODGE_OFFSET_COL.to_string(), offsets);
301 }
302 if let Some(col) = data.column_mut(DODGE_N_COL) {
303 *col = counts;
304 } else {
305 data.add_column(DODGE_N_COL.to_string(), counts);
306 }
307 return;
308 }
309 if let Some(col) = data.column_mut("x") {
310 *col = new_x;
311 }
312 if padding.is_some() {
313 for (name, vals) in [("xmin", new_xmin), ("xmax", new_xmax)] {
314 if let (Some(vals), Some(col)) = (vals, data.column_mut(name)) {
315 *col = vals;
316 }
317 }
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 fn frame(x: Vec<Value>, g: &[&str]) -> DataFrame {
326 let mut df = DataFrame::new();
327 df.add_column("x".into(), x);
328 df.add_column(
329 "color".into(),
330 g.iter().map(|s| Value::Str(s.to_string())).collect(),
331 );
332 df
333 }
334
335 fn col(df: &DataFrame, c: &str) -> Vec<f64> {
336 df.column(c)
337 .unwrap()
338 .iter()
339 .map(|v| v.as_f64().unwrap())
340 .collect()
341 }
342
343 #[test]
344 fn continuous_dodge_matches_ggplot2() {
345 let mut df = frame(vec![Value::Float(1.0); 3], &["a", "b", "c"]);
348 PositionDodge::new(0.6).compute(&mut df, &PositionParams::default());
349 let x = col(&df, "x");
350 for (got, want) in x.iter().zip([0.8, 1.0, 1.2]) {
351 assert!((got - want).abs() < 1e-12);
352 }
353 let mut df = frame(vec![Value::Float(1.0); 3], &["a", "b", "c"]);
354 PositionDodge::new(0.6)
355 .with_reverse(true)
356 .compute(&mut df, &PositionParams::default());
357 let x = col(&df, "x");
358 assert!((x[0] - 1.2).abs() < 1e-12 && (x[2] - 0.8).abs() < 1e-12);
359 }
360
361 #[test]
362 fn discrete_dodge_stores_offsets_and_centres_partial_slots() {
363 let x: Vec<Value> = ["t1", "t1", "t2"]
364 .iter()
365 .map(|s| Value::Str(s.to_string()))
366 .collect();
367 let mut df = frame(x, &["m1", "m2", "m2"]);
368 PositionDodge::new(0.5).compute(&mut df, &PositionParams::default());
369 assert_eq!(df.column("x").unwrap()[0].as_str(), Some("t1"));
371 let off = col(&df, DODGE_OFFSET_COL);
372 assert!((off[0] + 0.125).abs() < 1e-12);
373 assert!((off[1] - 0.125).abs() < 1e-12);
374 assert_eq!(off[2], 0.0);
376 }
377
378 #[test]
379 fn preserve_single_keeps_element_width_in_partial_slots() {
380 let x: Vec<Value> = ["t1", "t1", "t1", "t2"]
381 .iter()
382 .map(|s| Value::Str(s.to_string()))
383 .collect();
384 let mut df = frame(x, &["a", "b", "c", "a"]);
385 PositionDodge::new(0.9)
386 .with_preserve_single()
387 .compute(&mut df, &PositionParams::default());
388 assert_eq!(col(&df, DODGE_N_COL), vec![3.0, 3.0, 3.0, 3.0]);
391 assert_eq!(col(&df, DODGE_OFFSET_COL)[3], 0.0);
392 let mut df = frame(
394 vec![Value::Float(1.0), Value::Float(1.0), Value::Float(2.0)],
395 &["a", "b", "a"],
396 );
397 PositionDodge::new(0.6)
398 .with_preserve_single()
399 .compute(&mut df, &PositionParams::default());
400 let xs = col(&df, "x");
401 assert!((xs[0] - 0.85).abs() < 1e-12 && (xs[1] - 1.15).abs() < 1e-12);
402 assert!((xs[2] - 2.0).abs() < 1e-12);
403 }
404
405 #[test]
406 fn unit_value_still_works() {
407 let mut df = frame(vec![Value::Float(1.0); 2], &["a", "b"]);
408 PositionDodge.compute(&mut df, &PositionParams::default());
409 let x = col(&df, "x");
410 assert!((x[0] - 0.775).abs() < 1e-12 && (x[1] - 1.225).abs() < 1e-12);
411 }
412}