1use crate::aes::Aesthetic;
20use crate::data::{DataFrame, Value};
21use crate::scale::ScaleSet;
22
23use super::Stat;
24
25#[derive(Debug, Clone, Copy, Default)]
28pub struct StatAlign;
29
30impl Stat for StatAlign {
31 fn compute_group(&self, data: &DataFrame, _scales: &ScaleSet) -> DataFrame {
32 align_panel(data)
33 }
34
35 fn required_aes(&self) -> Vec<Aesthetic> {
36 vec![Aesthetic::X, Aesthetic::Y]
37 }
38
39 fn panelwise(&self) -> bool {
40 true
41 }
42
43 fn name(&self) -> &str {
44 "align"
45 }
46}
47
48struct Group {
51 pts: Vec<(f64, f64)>,
52 first: usize,
53}
54
55fn align_panel(data: &DataFrame) -> DataFrame {
56 let (Some(xs), Some(ys)) = (data.column("x"), data.column("y")) else {
57 return data.clone();
58 };
59 let xf: Option<Vec<f64>> = xs.iter().map(Value::as_f64).collect();
61 let yf: Option<Vec<f64>> = ys.iter().map(Value::as_f64).collect();
62 let (Some(xf), Some(yf)) = (xf, yf) else {
63 return data.clone();
64 };
65 let keys: Vec<String> = crate::build::detect_group_columns(data)
66 .into_iter()
67 .filter(|c| c != "x")
68 .collect();
69 let mut order: Vec<String> = Vec::new();
71 let mut rows: std::collections::HashMap<String, Vec<usize>> = Default::default();
72 for i in 0..data.nrows() {
73 let k = keys
74 .iter()
75 .map(|c| {
76 data.column(c)
77 .map_or(String::new(), |v| v[i].to_group_key())
78 })
79 .collect::<Vec<_>>()
80 .join("\u{1f}");
81 rows.entry(k.clone())
82 .or_insert_with(|| {
83 order.push(k.clone());
84 Vec::new()
85 })
86 .push(i);
87 }
88 if order.len() < 2 {
89 return data.clone();
90 }
91 let groups: Vec<Group> = order
92 .iter()
93 .map(|k| {
94 let ix = &rows[k];
95 let mut pts: Vec<(f64, f64)> = ix.iter().map(|&i| (xf[i], yf[i])).collect();
96 pts.sort_by(|a, b| a.0.total_cmp(&b.0));
97 Group { pts, first: ix[0] }
98 })
99 .collect();
100
101 let mut locs: Vec<f64> = xf.iter().copied().filter(|x| x.is_finite()).collect();
103 for g in &groups {
104 for w in g.pts.windows(2) {
105 let ((x0, y0), (x1, y1)) = (w[0], w[1]);
106 if (y0 < 0.0) != (y1 < 0.0) && y1 != y0 {
107 locs.push(x0 - y0 * (x1 - x0) / (y1 - y0));
108 }
109 }
110 }
111 sort_dedup(&mut locs);
112 if locs.len() < 2 {
113 return data.clone();
114 }
115 let range = locs[locs.len() - 1] - locs[0];
116 let min_gap = locs
117 .windows(2)
118 .map(|w| w[1] - w[0])
119 .fold(f64::INFINITY, f64::min);
120 let adjust = (range * 0.001).min(min_gap / 3.0);
121 let mut all: Vec<f64> = locs
122 .iter()
123 .flat_map(|&l| [l - adjust, l, l + adjust])
124 .collect();
125 sort_dedup(&mut all);
126
127 let datetime = xs.iter().all(Value::is_datetime);
128 let as_x = |v: f64| {
129 if datetime {
130 Value::DateTime(v.round() as i64)
131 } else {
132 Value::Float(v)
133 }
134 };
135 let names: Vec<String> = data
136 .column_names()
137 .into_iter()
138 .filter(|n| *n != "x" && *n != "y")
139 .map(str::to_string)
140 .collect();
141 let mut out_x: Vec<Value> = Vec::new();
142 let mut out_y: Vec<Value> = Vec::new();
143 let mut out_rest: Vec<Vec<Value>> = vec![Vec::new(); names.len()];
144 for g in &groups {
145 if g.pts.iter().all(|p| p.0 == g.pts[0].0) {
146 continue; }
148 let pts = dedup_steps(&g.pts, adjust);
149 let (lo, hi) = (pts[0].0, pts[pts.len() - 1].0);
150 let mut gx = vec![lo - adjust];
151 let mut gy = vec![0.0];
152 for &l in all.iter().filter(|&&l| l >= lo && l <= hi) {
153 gx.push(l);
154 gy.push(interpolate(&pts, l));
155 }
156 gx.push(hi + adjust);
157 gy.push(0.0);
158 for (x, y) in gx.into_iter().zip(gy) {
159 out_x.push(as_x(x));
160 out_y.push(Value::Float(y));
161 for (c, n) in names.iter().enumerate() {
162 out_rest[c].push(data.column(n).map_or(Value::Na, |v| v[g.first].clone()));
163 }
164 }
165 }
166 let mut out = DataFrame::new();
167 out.add_column("x".into(), out_x);
168 out.add_column("y".into(), out_y);
169 for (n, v) in names.into_iter().zip(out_rest) {
170 out.add_column(n, v);
171 }
172 out
173}
174
175fn sort_dedup(v: &mut Vec<f64>) {
176 v.retain(|x| x.is_finite());
177 v.sort_by(f64::total_cmp);
178 v.dedup();
179}
180
181fn dedup_steps(pts: &[(f64, f64)], adjust: f64) -> Vec<(f64, f64)> {
184 let mut out = Vec::with_capacity(pts.len());
185 let mut i = 0;
186 while i < pts.len() {
187 let mut j = i;
188 while j + 1 < pts.len() && pts[j + 1].0 == pts[i].0 {
189 j += 1;
190 }
191 if j == i {
192 out.push(pts[i]);
193 } else {
194 out.push((pts[i].0 - adjust, pts[i].1));
195 out.push(pts[j]);
196 }
197 i = j + 1;
198 }
199 out
200}
201
202fn interpolate(pts: &[(f64, f64)], x: f64) -> f64 {
205 let k = pts.partition_point(|p| p.0 < x);
206 if k < pts.len() && pts[k].0 == x {
207 return pts[k].1;
208 }
209 if k == 0 || k == pts.len() {
210 return f64::NAN; }
212 let ((x0, y0), (x1, y1)) = (pts[k - 1], pts[k]);
213 y0 + (y1 - y0) * (x - x0) / (x1 - x0)
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 fn panel(rows: &[(f64, f64, &str)]) -> DataFrame {
221 let mut d = DataFrame::new();
222 d.add_column("x".into(), rows.iter().map(|r| Value::Float(r.0)).collect());
223 d.add_column("y".into(), rows.iter().map(|r| Value::Float(r.1)).collect());
224 d.add_column(
225 "fill".into(),
226 rows.iter().map(|r| Value::Str(r.2.into())).collect(),
227 );
228 d
229 }
230 fn col(d: &DataFrame, n: &str) -> Vec<f64> {
231 d.column(n)
232 .unwrap()
233 .iter()
234 .map(|v| v.as_f64().unwrap())
235 .collect()
236 }
237
238 #[test]
239 fn matches_ggplot2_on_a_gap() {
240 let d = panel(&[
243 (1.0, 1.0, "a"),
244 (2.0, 1.0, "a"),
245 (3.0, 1.0, "a"),
246 (4.0, 1.0, "a"),
247 (1.0, 2.0, "b"),
248 (3.0, 2.0, "b"),
249 (4.0, 2.0, "b"),
250 ]);
251 let out = align_panel(&d);
252 let x = col(&out, "x");
253 let y = col(&out, "y");
254 let want_x = [
255 0.997, 1.0, 1.003, 1.997, 2.0, 2.003, 2.997, 3.0, 3.003, 3.997, 4.0, 4.003,
256 ];
257 assert_eq!(out.nrows(), 24);
258 for (i, w) in want_x.iter().enumerate() {
259 assert!((x[i] - w).abs() < 1e-9, "a x[{i}] = {}", x[i]);
260 assert!((x[12 + i] - w).abs() < 1e-9, "b x[{i}] = {}", x[12 + i]);
261 }
262 assert_eq!((y[0], y[11], y[12], y[23]), (0.0, 0.0, 0.0, 0.0));
265 assert!(y[1..11].iter().all(|&v| v == 1.0));
266 assert!(y[13..23].iter().all(|&v| v == 2.0));
267 assert_eq!(out.column("fill").unwrap()[13].as_str(), Some("b"));
268 }
269
270 #[test]
271 fn one_group_passes_through() {
272 let d = panel(&[(1.0, 1.0, "a"), (3.0, 4.0, "a")]);
273 let out = align_panel(&d);
274 assert_eq!(col(&out, "x"), vec![1.0, 3.0]);
275 assert_eq!(col(&out, "y"), vec![1.0, 4.0]);
276 }
277
278 #[test]
279 fn interpolates_and_adds_zero_crossings() {
280 let d = panel(&[
282 (0.0, -1.0, "a"),
283 (2.0, 1.0, "a"),
284 (0.0, 1.0, "b"),
285 (2.0, 3.0, "b"),
286 ]);
287 let out = align_panel(&d);
288 let (x, y) = (col(&out, "x"), col(&out, "y"));
289 let at = |g: usize, want: f64| {
290 let off = if g == 0 { 0 } else { out.nrows() / 2 };
291 let i = (off..off + out.nrows() / 2)
292 .find(|&i| (x[i] - want).abs() < 1e-12)
293 .unwrap();
294 y[i]
295 };
296 assert_eq!(at(0, 1.0), 0.0, "a's zero crossing is a shared position");
297 assert_eq!(at(1, 1.0), 2.0, "b interpolated there");
298 }
299
300 #[test]
301 fn a_single_x_group_is_dropped_and_steps_kept() {
302 let d = panel(&[
303 (1.0, 5.0, "a"),
304 (1.0, 6.0, "a"),
305 (0.0, 1.0, "b"),
306 (2.0, 1.0, "b"),
307 ]);
308 let out = align_panel(&d);
309 assert!(out
310 .column("fill")
311 .unwrap()
312 .iter()
313 .all(|v| v.as_str() == Some("b")));
314 let steps = dedup_steps(&[(0.0, 1.0), (1.0, 2.0), (1.0, 3.0), (2.0, 1.0)], 0.1);
315 assert_eq!(steps, vec![(0.0, 1.0), (0.9, 2.0), (1.0, 3.0), (2.0, 1.0)]);
316 }
317
318 #[test]
319 fn datetimes_stay_datetimes() {
320 let mut d = DataFrame::new();
321 d.add_column(
322 "x".into(),
323 vec![
324 Value::DateTime(0),
325 Value::DateTime(86_400),
326 Value::DateTime(0),
327 Value::DateTime(172_800),
328 ],
329 );
330 d.add_column("y".into(), vec![Value::Float(1.0); 4]);
331 d.add_column(
332 "fill".into(),
333 vec![
334 Value::Str("a".into()),
335 Value::Str("a".into()),
336 Value::Str("b".into()),
337 Value::Str("b".into()),
338 ],
339 );
340 let out = align_panel(&d);
341 assert!(out.column("x").unwrap().iter().all(Value::is_datetime));
342 }
343}