Skip to main content

ggplot_rs/position/
fill.rs

1use crate::data::{DataFrame, Value};
2
3use super::{Position, PositionParams};
4
5/// Normalized stacking to 100% — like PositionStack but scales to [0, 1].
6pub struct PositionFill;
7
8impl Position for PositionFill {
9    fn compute(&self, data: &mut DataFrame, _params: &PositionParams) {
10        let x_col = match data.column("x") {
11            Some(c) => c.to_vec(),
12            None => return,
13        };
14        let y_col = match data.column("y") {
15            Some(c) => c.to_vec(),
16            None => return,
17        };
18
19        // First compute totals per x group
20        let mut x_totals: Vec<(String, f64)> = Vec::new();
21        for (x, y) in x_col.iter().zip(y_col.iter()) {
22            let x_key = x.to_group_key();
23            let y_val = y.as_f64().unwrap_or(0.0);
24
25            if let Some(entry) = x_totals.iter_mut().find(|(k, _)| k == &x_key) {
26                entry.1 += y_val;
27            } else {
28                x_totals.push((x_key, y_val));
29            }
30        }
31
32        // Then compute normalized stacked positions
33        let mut x_cumsum: Vec<(String, f64)> = Vec::new();
34        let mut new_y = Vec::with_capacity(y_col.len());
35        let mut ymin_vals = Vec::with_capacity(y_col.len());
36
37        for (x, y) in x_col.iter().zip(y_col.iter()) {
38            let x_key = x.to_group_key();
39            let y_val = y.as_f64().unwrap_or(0.0);
40
41            let total = x_totals
42                .iter()
43                .find(|(k, _)| k == &x_key)
44                .map(|(_, v)| *v)
45                .unwrap_or(1.0);
46            let total = if total.abs() < f64::EPSILON {
47                1.0
48            } else {
49                total
50            };
51
52            // ggplot2 puts the first group at the top, so fill downward from 1.
53            let consumed = x_cumsum
54                .iter()
55                .find(|(k, _)| k == &x_key)
56                .map(|(_, v)| *v)
57                .unwrap_or(0.0);
58
59            let norm_y = y_val / total;
60            new_y.push(Value::Float(1.0 - consumed));
61            ymin_vals.push(Value::Float(1.0 - consumed - norm_y));
62
63            if let Some(entry) = x_cumsum.iter_mut().find(|(k, _)| k == &x_key) {
64                entry.1 += norm_y;
65            } else {
66                x_cumsum.push((x_key, norm_y));
67            }
68        }
69
70        if let Some(col) = data.column_mut("y") {
71            *col = new_y;
72        }
73        if !data.has_column("ymin") {
74            data.add_column("ymin".to_string(), ymin_vals);
75        } else if let Some(col) = data.column_mut("ymin") {
76            *col = ymin_vals;
77        }
78    }
79
80    fn name(&self) -> &str {
81        "fill"
82    }
83}