1use std::borrow::Cow;
2use std::collections::HashMap;
3
4use crate::data::{DataFrame, Value};
5
6use super::{Position, PositionParams};
7
8#[derive(Clone, Debug, PartialEq)]
21pub struct PositionStack {
22 pub vjust: f64,
23 pub offset: StackOffset,
25}
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
30pub enum StackOffset {
31 #[default]
33 Zero,
34 Silhouette,
36 Wiggle,
39}
40
41impl Default for PositionStack {
42 fn default() -> Self {
43 PositionStack {
44 vjust: 1.0,
45 offset: StackOffset::Zero,
46 }
47 }
48}
49
50#[allow(non_upper_case_globals)]
53pub const PositionStack: PositionStack = PositionStack {
54 vjust: 1.0,
55 offset: StackOffset::Zero,
56};
57
58impl PositionStack {
59 pub fn with_vjust(mut self, vjust: f64) -> Self {
62 self.vjust = vjust;
63 self
64 }
65
66 pub fn with_offset(mut self, offset: StackOffset) -> Self {
68 self.offset = offset;
69 self
70 }
71}
72
73fn baseline_shift(
76 offset: StackOffset,
77 data: &DataFrame,
78 x_col: &[Value],
79 y_col: &[Value],
80) -> Vec<f64> {
81 let mut xs: Vec<(String, Option<f64>)> = Vec::new();
84 let mut x_index: HashMap<String, usize> = HashMap::new();
85 for x in x_col {
86 let k = x.key_str().into_owned();
87 if !x_index.contains_key(&k) {
88 x_index.insert(k.clone(), xs.len());
89 xs.push((k, x.as_f64()));
90 }
91 }
92 let mut order: Vec<usize> = (0..xs.len()).collect();
93 if xs.iter().all(|x| x.1.is_some()) {
94 order.sort_by(|&a, &b| xs[a].1.unwrap().total_cmp(&xs[b].1.unwrap()));
95 }
96 let mut slot = vec![0; xs.len()];
97 for (j, &k) in order.iter().enumerate() {
98 slot[k] = j;
99 }
100 let row_slot: Vec<usize> = x_col
101 .iter()
102 .map(|x| slot[x_index[x.key_str().as_ref()]])
103 .collect();
104 let m = xs.len();
105
106 let series_col = ["group", "fill", "color"]
108 .iter()
109 .find_map(|c| data.column(c));
110 let mut series: Vec<String> = Vec::new();
111 let mut s_index: HashMap<String, usize> = HashMap::new();
112 let row_series: Vec<usize> = (0..x_col.len())
113 .map(|i| {
114 let k = series_col
115 .map(|c| c[i].key_str().into_owned())
116 .unwrap_or_default();
117 *s_index.entry(k.clone()).or_insert_with(|| {
118 series.push(k);
119 series.len() - 1
120 })
121 })
122 .collect();
123 let n = series.len();
124 let mut v = vec![vec![0.0; m]; n];
125 for i in 0..x_col.len() {
126 if let Some(y) = super::stack_row(&y_col[i]).value() {
127 v[n - 1 - row_series[i]][row_slot[i]] += y.max(0.0);
128 }
129 }
130 let totals: Vec<f64> = (0..m).map(|j| (0..n).map(|s| v[s][j]).sum()).collect();
131 let base: Vec<f64> = match offset {
132 StackOffset::Zero => vec![0.0; m],
133 StackOffset::Silhouette => totals.iter().map(|t| -t / 2.0).collect(),
134 StackOffset::Wiggle => {
135 let mut g = vec![0.0; m];
136 let mut y = 0.0;
137 for j in 1..m {
138 let (mut s1, mut s2) = (0.0, 0.0);
139 for i in 0..n {
140 let mut s3 = (v[i][j] - v[i][j - 1]) / 2.0;
141 for row in v.iter().take(i) {
142 s3 += row[j] - row[j - 1];
143 }
144 s1 += v[i][j];
145 s2 += s3 * v[i][j];
146 }
147 if s1 != 0.0 {
148 y -= s2 / s1;
149 }
150 g[j] = y;
151 }
152 let mid =
154 g.iter().zip(&totals).map(|(g, t)| g + t / 2.0).sum::<f64>() / m.max(1) as f64;
155 g.iter().map(|g| g - mid).collect()
156 }
157 };
158 row_slot.iter().map(|&j| base[j]).collect()
159}
160
161pub fn position_stack() -> PositionStack {
164 PositionStack::default()
165}
166
167impl Position for PositionStack {
168 fn compute(&self, data: &mut DataFrame, _params: &PositionParams) {
169 let x_col = match data.column("x") {
171 Some(c) => c.to_vec(),
172 None => return,
173 };
174 let y_col = match data.column("y") {
175 Some(c) => c.to_vec(),
176 None => return,
177 };
178 super::preserve_raw_y(data, &y_col);
179
180 let n = y_col.len();
181 let mut tops = vec![0.0_f64; n];
182 let mut bottoms = vec![0.0_f64; n];
183
184 for positive in [true, false] {
209 let mut totals: HashMap<Cow<'_, str>, f64> = HashMap::new();
210 for (x, y) in x_col.iter().zip(y_col.iter()) {
211 let row = super::stack_row(y);
212 if row.is_positive_bucket() != positive {
213 continue;
214 }
215 *totals.entry(x.key_str()).or_insert(0.0) += row.contribution();
216 }
217
218 let mut consumed: HashMap<Cow<'_, str>, f64> = HashMap::new();
219 for (i, (x, y)) in x_col.iter().zip(y_col.iter()).enumerate() {
220 let row = super::stack_row(y);
221 if row.is_positive_bucket() != positive {
222 continue;
223 }
224 let x_key = x.key_str();
225 let total = totals.get(&x_key).copied().unwrap_or(0.0);
226 let run = consumed.get(&x_key).copied().unwrap_or(0.0);
227
228 let Some(y_val) = row.value() else {
229 tops[i] = f64::NAN;
234 bottoms[i] = f64::NAN;
235 continue;
236 };
237
238 tops[i] = total - run;
241 bottoms[i] = total - run - y_val;
242
243 *consumed.entry(x_key).or_insert(0.0) += y_val;
244 }
245 }
246
247 if self.offset != StackOffset::Zero {
248 let shift = baseline_shift(self.offset, data, &x_col, &y_col);
249 for i in 0..n {
250 tops[i] += shift[i];
251 bottoms[i] += shift[i];
252 }
253 }
254
255 let vjust = if self.vjust.is_finite() {
258 self.vjust
259 } else {
260 1.0
261 };
262
263 let write_ymax = (vjust - 1.0).abs() > f64::EPSILON;
264 let lo_hi: Vec<(f64, f64)> = tops
265 .iter()
266 .zip(bottoms.iter())
267 .map(|(&t, &b)| (t.min(b), t.max(b)))
268 .collect();
269 let new_y: Vec<Value> = if write_ymax {
270 lo_hi
271 .iter()
272 .map(|&(lo, hi)| Value::Float((1.0 - vjust) * lo + vjust * hi))
273 .collect()
274 } else {
275 lo_hi.iter().map(|&(_, hi)| Value::Float(hi)).collect()
276 };
277 let ymin_vals: Vec<Value> = lo_hi.iter().map(|&(lo, _)| Value::Float(lo)).collect();
278
279 if let Some(col) = data.column_mut("y") {
280 *col = new_y;
281 }
282 if !data.has_column("ymin") {
283 data.add_column("ymin".to_string(), ymin_vals);
284 } else if let Some(col) = data.column_mut("ymin") {
285 *col = ymin_vals;
286 }
287 if write_ymax {
288 let ymax_vals: Vec<Value> = lo_hi.iter().map(|&(_, hi)| Value::Float(hi)).collect();
289 if !data.has_column("ymax") {
290 data.add_column("ymax".to_string(), ymax_vals);
291 } else if let Some(col) = data.column_mut("ymax") {
292 *col = ymax_vals;
293 }
294 }
295 }
296
297 fn name(&self) -> &str {
298 "stack"
299 }
300
301 fn per_panel(&self) -> bool {
302 true
303 }
304
305 fn validate(&self, warnings: &mut Vec<String>) {
306 if !self.vjust.is_finite() {
307 warnings.push(format!(
308 "position_stack: vjust({}) must be finite; falling back to 1",
309 self.vjust
310 ));
311 }
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 fn frame(x: &[f64], g: &[&str], y: &[f64]) -> DataFrame {
320 let mut df = DataFrame::new();
321 df.add_column("x".into(), x.iter().map(|v| Value::Float(*v)).collect());
322 df.add_column(
323 "fill".into(),
324 g.iter().map(|s| Value::Str(s.to_string())).collect(),
325 );
326 df.add_column("y".into(), y.iter().map(|v| Value::Float(*v)).collect());
327 df
328 }
329
330 fn col(df: &DataFrame, c: &str) -> Vec<f64> {
331 df.column(c)
332 .unwrap()
333 .iter()
334 .map(|v| v.as_f64().unwrap())
335 .collect()
336 }
337
338 #[test]
345 fn negative_and_positive_values_stack_independently() {
346 let mut df = frame(&[1.0; 4], &["a", "b", "c", "d"], &[3.0, -2.0, 4.0, -1.0]);
347 PositionStack::default()
348 .with_vjust(0.5)
349 .compute(&mut df, &PositionParams::default());
350 let y = col(&df, "y");
351 let ymin = col(&df, "ymin");
352 let ymax = col(&df, "ymax");
353 for (i, (want_y, want_min, want_max)) in [
354 (5.5, 4.0, 7.0),
355 (-2.0, -3.0, -1.0),
356 (2.0, 0.0, 4.0),
357 (-0.5, -1.0, 0.0),
358 ]
359 .into_iter()
360 .enumerate()
361 {
362 assert!((y[i] - want_y).abs() < 1e-9, "y[{i}]={}", y[i]);
363 assert!((ymin[i] - want_min).abs() < 1e-9, "ymin[{i}]={}", ymin[i]);
364 assert!((ymax[i] - want_max).abs() < 1e-9, "ymax[{i}]={}", ymax[i]);
365 }
366 }
367
368 #[test]
371 fn all_positive_values_are_unaffected() {
372 let mut df = frame(&[1.0, 1.0], &["a", "b"], &[3.0, 5.0]);
373 PositionStack.compute(&mut df, &PositionParams::default());
374 assert_eq!(col(&df, "y"), vec![8.0, 5.0]);
375 assert_eq!(col(&df, "ymin"), vec![5.0, 0.0]);
376 }
377
378 #[test]
389 fn missing_y_contributes_zero_and_does_not_poison_its_bucket() {
390 let mut df = frame(&[1.0, 1.0, 1.0], &["a", "b", "c"], &[3.0, f64::NAN, -2.0]);
391 PositionStack.compute(&mut df, &PositionParams::default());
392 let y = col(&df, "y");
393 let ymin = col(&df, "ymin");
394 assert_eq!(
395 (y[0], ymin[0]),
396 (3.0, 0.0),
397 "a must stack as if b's row weren't there at all: y={y:?} ymin={ymin:?}"
398 );
399 assert!(
400 y[1].is_nan() && ymin[1].is_nan(),
401 "b's own y/ymin must stay NaN (not drawn): y={y:?} ymin={ymin:?}"
402 );
403 assert_eq!(
404 (y[2], ymin[2]),
405 (0.0, -2.0),
406 "c (negative bucket) must be unaffected: y={y:?} ymin={ymin:?}"
407 );
408 }
409
410 #[test]
417 fn value_na_is_treated_like_float_nan_not_zero() {
418 let mut df = DataFrame::new();
419 df.add_column(
420 "x".into(),
421 vec![Value::Float(1.0), Value::Float(1.0), Value::Float(1.0)],
422 );
423 df.add_column(
424 "fill".into(),
425 vec![
426 Value::Str("a".into()),
427 Value::Str("b".into()),
428 Value::Str("c".into()),
429 ],
430 );
431 df.add_column(
432 "y".into(),
433 vec![Value::Float(3.0), Value::Na, Value::Float(-2.0)],
434 );
435 PositionStack.compute(&mut df, &PositionParams::default());
436 let y = col(&df, "y");
437 let ymin = col(&df, "ymin");
438 assert_eq!(
439 (y[0], ymin[0]),
440 (3.0, 0.0),
441 "a must be unaffected by b's Value::Na: y={y:?} ymin={ymin:?}"
442 );
443 assert!(
444 y[1].is_nan() && ymin[1].is_nan(),
445 "a Value::Na row must not become a real point at the stack top: \
446 y={y:?} ymin={ymin:?}"
447 );
448 }
449
450 #[test]
451 fn silhouette_centres_each_stack_on_zero() {
452 let mut df = frame(
453 &[1.0, 1.0, 2.0, 2.0],
454 &["a", "b", "a", "b"],
455 &[2.0, 4.0, 1.0, 5.0],
456 );
457 position_stack()
458 .with_offset(StackOffset::Silhouette)
459 .compute(&mut df, &PositionParams::default());
460 assert_eq!(col(&df, "ymin"), vec![1.0, -3.0, 2.0, -3.0]);
462 assert_eq!(col(&df, "y"), vec![3.0, 1.0, 3.0, 2.0]);
463 }
464
465 #[test]
466 fn wiggle_is_centred_and_keeps_thickness() {
467 let x = [1.0, 1.0, 2.0, 2.0, 3.0, 3.0];
468 let mut df = frame(
469 &x,
470 &["a", "b", "a", "b", "a", "b"],
471 &[1.0, 3.0, 4.0, 2.0, 2.0, 2.0],
472 );
473 position_stack()
474 .with_offset(StackOffset::Wiggle)
475 .compute(&mut df, &PositionParams::default());
476 let (lo, hi) = (col(&df, "ymin"), col(&df, "y"));
477 for i in 0..6 {
478 let want = [1.0, 3.0, 4.0, 2.0, 2.0, 2.0][i];
479 assert!((hi[i] - lo[i] - want).abs() < 1e-12, "row {i}");
480 }
481 let mid: f64 = [0usize, 2, 4]
483 .iter()
484 .map(|&i| (hi[i] + lo[i + 1]) / 2.0)
485 .sum::<f64>();
486 assert!(mid.abs() < 1e-9, "{mid}");
487 }
488}