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 let seq = super::stacking_sequence(data, params.group_order.as_deref());
209 for positive in [true, false] {
210 let mut totals: HashMap<Cow<'_, str>, f64> = HashMap::new();
211 for (x, y) in x_col.iter().zip(y_col.iter()) {
212 let row = super::stack_row(y);
213 if row.is_positive_bucket() != positive {
214 continue;
215 }
216 *totals.entry(x.key_str()).or_insert(0.0) += row.contribution();
217 }
218
219 let mut consumed: HashMap<Cow<'_, str>, f64> = HashMap::new();
220 for &i in &seq {
221 let (x, y) = (&x_col[i], &y_col[i]);
222 let row = super::stack_row(y);
223 if row.is_positive_bucket() != positive {
224 continue;
225 }
226 let x_key = x.key_str();
227 let total = totals.get(&x_key).copied().unwrap_or(0.0);
228 let run = consumed.get(&x_key).copied().unwrap_or(0.0);
229
230 let Some(y_val) = row.value() else {
231 tops[i] = f64::NAN;
236 bottoms[i] = f64::NAN;
237 continue;
238 };
239
240 tops[i] = total - run;
243 bottoms[i] = total - run - y_val;
244
245 *consumed.entry(x_key).or_insert(0.0) += y_val;
246 }
247 }
248
249 if self.offset != StackOffset::Zero {
250 let shift = baseline_shift(self.offset, data, &x_col, &y_col);
251 for i in 0..n {
252 tops[i] += shift[i];
253 bottoms[i] += shift[i];
254 }
255 }
256
257 let vjust = if self.vjust.is_finite() {
260 self.vjust
261 } else {
262 1.0
263 };
264
265 let write_ymax = (vjust - 1.0).abs() > f64::EPSILON;
266 let lo_hi: Vec<(f64, f64)> = tops
267 .iter()
268 .zip(bottoms.iter())
269 .map(|(&t, &b)| (t.min(b), t.max(b)))
270 .collect();
271 let new_y: Vec<Value> = if write_ymax {
272 lo_hi
273 .iter()
274 .map(|&(lo, hi)| Value::Float((1.0 - vjust) * lo + vjust * hi))
275 .collect()
276 } else {
277 lo_hi.iter().map(|&(_, hi)| Value::Float(hi)).collect()
278 };
279 let ymin_vals: Vec<Value> = lo_hi.iter().map(|&(lo, _)| Value::Float(lo)).collect();
280
281 if let Some(col) = data.column_mut("y") {
282 *col = new_y;
283 }
284 if !data.has_column("ymin") {
285 data.add_column("ymin".to_string(), ymin_vals);
286 } else if let Some(col) = data.column_mut("ymin") {
287 *col = ymin_vals;
288 }
289 if write_ymax {
290 let ymax_vals: Vec<Value> = lo_hi.iter().map(|&(_, hi)| Value::Float(hi)).collect();
291 if !data.has_column("ymax") {
292 data.add_column("ymax".to_string(), ymax_vals);
293 } else if let Some(col) = data.column_mut("ymax") {
294 *col = ymax_vals;
295 }
296 }
297 }
298
299 fn name(&self) -> &str {
300 "stack"
301 }
302
303 fn group_order(&self, data: &DataFrame) -> Option<Vec<String>> {
304 super::dodge::group_keys(data).map(|keys| super::dodge::dedup_order(&keys))
305 }
306
307 fn per_panel(&self) -> bool {
308 true
309 }
310
311 fn validate(&self, warnings: &mut Vec<String>) {
312 if !self.vjust.is_finite() {
313 warnings.push(format!(
314 "position_stack: vjust({}) must be finite; falling back to 1",
315 self.vjust
316 ));
317 }
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 fn frame(x: &[f64], g: &[&str], y: &[f64]) -> DataFrame {
326 let mut df = DataFrame::new();
327 df.add_column("x".into(), x.iter().map(|v| Value::Float(*v)).collect());
328 df.add_column(
329 "fill".into(),
330 g.iter().map(|s| Value::Str(s.to_string())).collect(),
331 );
332 df.add_column("y".into(), y.iter().map(|v| Value::Float(*v)).collect());
333 df
334 }
335
336 fn col(df: &DataFrame, c: &str) -> Vec<f64> {
337 df.column(c)
338 .unwrap()
339 .iter()
340 .map(|v| v.as_f64().unwrap())
341 .collect()
342 }
343
344 #[test]
351 fn negative_and_positive_values_stack_independently() {
352 let mut df = frame(&[1.0; 4], &["a", "b", "c", "d"], &[3.0, -2.0, 4.0, -1.0]);
353 PositionStack::default()
354 .with_vjust(0.5)
355 .compute(&mut df, &PositionParams::default());
356 let y = col(&df, "y");
357 let ymin = col(&df, "ymin");
358 let ymax = col(&df, "ymax");
359 for (i, (want_y, want_min, want_max)) in [
360 (5.5, 4.0, 7.0),
361 (-2.0, -3.0, -1.0),
362 (2.0, 0.0, 4.0),
363 (-0.5, -1.0, 0.0),
364 ]
365 .into_iter()
366 .enumerate()
367 {
368 assert!((y[i] - want_y).abs() < 1e-9, "y[{i}]={}", y[i]);
369 assert!((ymin[i] - want_min).abs() < 1e-9, "ymin[{i}]={}", ymin[i]);
370 assert!((ymax[i] - want_max).abs() < 1e-9, "ymax[{i}]={}", ymax[i]);
371 }
372 }
373
374 #[test]
377 fn all_positive_values_are_unaffected() {
378 let mut df = frame(&[1.0, 1.0], &["a", "b"], &[3.0, 5.0]);
379 PositionStack.compute(&mut df, &PositionParams::default());
380 assert_eq!(col(&df, "y"), vec![8.0, 5.0]);
381 assert_eq!(col(&df, "ymin"), vec![5.0, 0.0]);
382 }
383
384 #[test]
395 fn missing_y_contributes_zero_and_does_not_poison_its_bucket() {
396 let mut df = frame(&[1.0, 1.0, 1.0], &["a", "b", "c"], &[3.0, f64::NAN, -2.0]);
397 PositionStack.compute(&mut df, &PositionParams::default());
398 let y = col(&df, "y");
399 let ymin = col(&df, "ymin");
400 assert_eq!(
401 (y[0], ymin[0]),
402 (3.0, 0.0),
403 "a must stack as if b's row weren't there at all: y={y:?} ymin={ymin:?}"
404 );
405 assert!(
406 y[1].is_nan() && ymin[1].is_nan(),
407 "b's own y/ymin must stay NaN (not drawn): y={y:?} ymin={ymin:?}"
408 );
409 assert_eq!(
410 (y[2], ymin[2]),
411 (0.0, -2.0),
412 "c (negative bucket) must be unaffected: y={y:?} ymin={ymin:?}"
413 );
414 }
415
416 #[test]
423 fn value_na_is_treated_like_float_nan_not_zero() {
424 let mut df = DataFrame::new();
425 df.add_column(
426 "x".into(),
427 vec![Value::Float(1.0), Value::Float(1.0), Value::Float(1.0)],
428 );
429 df.add_column(
430 "fill".into(),
431 vec![
432 Value::Str("a".into()),
433 Value::Str("b".into()),
434 Value::Str("c".into()),
435 ],
436 );
437 df.add_column(
438 "y".into(),
439 vec![Value::Float(3.0), Value::Na, Value::Float(-2.0)],
440 );
441 PositionStack.compute(&mut df, &PositionParams::default());
442 let y = col(&df, "y");
443 let ymin = col(&df, "ymin");
444 assert_eq!(
445 (y[0], ymin[0]),
446 (3.0, 0.0),
447 "a must be unaffected by b's Value::Na: y={y:?} ymin={ymin:?}"
448 );
449 assert!(
450 y[1].is_nan() && ymin[1].is_nan(),
451 "a Value::Na row must not become a real point at the stack top: \
452 y={y:?} ymin={ymin:?}"
453 );
454 }
455
456 #[test]
457 fn silhouette_centres_each_stack_on_zero() {
458 let mut df = frame(
459 &[1.0, 1.0, 2.0, 2.0],
460 &["a", "b", "a", "b"],
461 &[2.0, 4.0, 1.0, 5.0],
462 );
463 position_stack()
464 .with_offset(StackOffset::Silhouette)
465 .compute(&mut df, &PositionParams::default());
466 assert_eq!(col(&df, "ymin"), vec![1.0, -3.0, 2.0, -3.0]);
468 assert_eq!(col(&df, "y"), vec![3.0, 1.0, 3.0, 2.0]);
469 }
470
471 #[test]
472 fn wiggle_is_centred_and_keeps_thickness() {
473 let x = [1.0, 1.0, 2.0, 2.0, 3.0, 3.0];
474 let mut df = frame(
475 &x,
476 &["a", "b", "a", "b", "a", "b"],
477 &[1.0, 3.0, 4.0, 2.0, 2.0, 2.0],
478 );
479 position_stack()
480 .with_offset(StackOffset::Wiggle)
481 .compute(&mut df, &PositionParams::default());
482 let (lo, hi) = (col(&df, "ymin"), col(&df, "y"));
483 for i in 0..6 {
484 let want = [1.0, 3.0, 4.0, 2.0, 2.0, 2.0][i];
485 assert!((hi[i] - lo[i] - want).abs() < 1e-12, "row {i}");
486 }
487 let mid: f64 = [0usize, 2, 4]
489 .iter()
490 .map(|&i| (hi[i] + lo[i + 1]) / 2.0)
491 .sum::<f64>();
492 assert!(mid.abs() < 1e-9, "{mid}");
493 }
494}
495
496#[cfg(test)]
497mod level_order_tests {
498 fn stacks(svg: &str) -> Vec<Vec<String>> {
500 let mut bars: Vec<(f64, f64, String)> = Vec::new();
501 for tag in svg
502 .split('<')
503 .filter(|t| t.starts_with("rect") && t.contains("gg-bar"))
504 {
505 let attr = |n: &str| {
506 let k = format!(" {n}=\"");
507 tag.find(&k).map(|i| {
508 let r = &tag[i + k.len()..];
509 r[..r.find('"').unwrap()].to_string()
510 })
511 };
512 let (Some(x), Some(y), Some(fill)) = (attr("x"), attr("y"), attr("fill")) else {
513 continue;
514 };
515 bars.push((x.parse().unwrap(), y.parse().unwrap(), fill));
516 }
517 let mut xs: Vec<f64> = bars.iter().map(|b| b.0).collect();
518 xs.sort_by(f64::total_cmp);
519 xs.dedup();
520 xs.iter()
521 .map(|&x| {
522 let mut col: Vec<&(f64, f64, String)> = bars.iter().filter(|b| b.0 == x).collect();
523 col.sort_by(|a, b| b.1.total_cmp(&a.1));
525 col.iter().map(|b| b.2.clone()).collect()
526 })
527 .collect()
528 }
529
530 #[test]
532 fn stacks_follow_the_fill_levels() {
533 use crate::aes::Aesthetic;
534 use crate::prelude::*;
535 use crate::scale::color::ScaleColorDiscrete;
536 let txt = |v: &[&str]| {
537 v.iter()
538 .map(|s| Value::Str(s.to_string()))
539 .collect::<Vec<_>>()
540 };
541 for fill in [false, true] {
542 let mut plot = GGPlot::new(vec![
543 ("x".to_string(), txt(&["a", "a", "a", "b", "b", "b"])),
544 (
545 "n".to_string(),
546 [1.0, 2.0, 3.0, 3.0, 2.0, 1.0].map(Value::Float).to_vec(),
547 ),
548 (
549 "k".to_string(),
550 txt(&["ok", "miss", "early", "early", "ok", "miss"]),
551 ),
552 ])
553 .aes(Aes::new().x("x").y("n").fill("k"))
554 .scale_fill(ScaleColorDiscrete::new(Aesthetic::Fill).with_levels(vec![
555 "miss".into(),
556 "early".into(),
557 "ok".into(),
558 ]));
559 plot = if fill {
560 plot.geom_col()
561 .position(crate::position::fill::PositionFill::default())
562 } else {
563 plot.geom_col().position(super::position_stack())
564 };
565 let svg = plot.render_svg_native_at(0.0, 0.0, 300, 200).unwrap();
566 let s = stacks(&svg);
567 assert_eq!(s.len(), 2, "fill={fill}");
568 assert_eq!(s[0], s[1], "fill={fill}: both bars stack alike");
569 }
570 }
571}