Skip to main content

ggplot_rs/position/
fill.rs

1use std::borrow::Cow;
2use std::collections::HashMap;
3
4use crate::data::{DataFrame, Value};
5
6use super::{Position, PositionParams};
7
8/// Normalized stacking to 100% (ggplot2's `position_fill(vjust = )`) —
9/// like [`PositionStack`](struct@crate::position::stack::PositionStack) but
10/// scaled to `[0, 1]` per x group.
11///
12/// `vjust` places the row's `y` within its stacked segment, same as
13/// `PositionStack`: `1` (the default) is the segment's top, byte-
14/// identical to before B3-4 (no `ymax` column). Anything else writes
15/// `y = (1 − vjust)·ymin + vjust·ymax` plus the `ymax` column.
16#[derive(Clone, Debug, PartialEq)]
17pub struct PositionFill {
18    pub vjust: f64,
19}
20
21impl Default for PositionFill {
22    fn default() -> Self {
23        PositionFill { vjust: 1.0 }
24    }
25}
26
27/// The default fill (`vjust` 1): lets `.position(PositionFill)` keep
28/// compiling now that the type has fields.
29#[allow(non_upper_case_globals)]
30pub const PositionFill: PositionFill = PositionFill { vjust: 1.0 };
31
32impl PositionFill {
33    /// Place `y` within each stacked segment (`0` = bottom, `0.5` =
34    /// middle, `1` = top, the default).
35    pub fn with_vjust(mut self, vjust: f64) -> Self {
36        self.vjust = vjust;
37        self
38    }
39}
40
41/// `position_fill()` / `position_fill().with_vjust(v)` — see
42/// [`PositionFill`](struct@PositionFill).
43pub fn position_fill() -> PositionFill {
44    PositionFill::default()
45}
46
47impl Position for PositionFill {
48    fn compute(&self, data: &mut DataFrame, _params: &PositionParams) {
49        let x_col = match data.column("x") {
50            Some(c) => c.to_vec(),
51            None => return,
52        };
53        let y_col = match data.column("y") {
54            Some(c) => c.to_vec(),
55            None => return,
56        };
57        super::preserve_raw_y(data, &y_col);
58
59        // First compute totals per x group
60        let mut x_totals: HashMap<Cow<'_, str>, f64> = HashMap::new();
61        for (x, y) in x_col.iter().zip(y_col.iter()) {
62            *x_totals.entry(x.key_str()).or_insert(0.0) += y.as_f64().unwrap_or(0.0);
63        }
64
65        // Then compute normalized stacked positions
66        let mut x_cumsum: HashMap<Cow<'_, str>, f64> = HashMap::new();
67        let mut tops = Vec::with_capacity(y_col.len());
68        let mut bottoms = Vec::with_capacity(y_col.len());
69
70        for (x, y) in x_col.iter().zip(y_col.iter()) {
71            let x_key = x.key_str();
72            let y_val = y.as_f64().unwrap_or(0.0);
73
74            let total = x_totals.get(&x_key).copied().unwrap_or(1.0);
75            let total = if total.abs() < f64::EPSILON {
76                1.0
77            } else {
78                total
79            };
80
81            // ggplot2 puts the first group at the top, so fill downward from 1.
82            let consumed = x_cumsum.get(&x_key).copied().unwrap_or(0.0);
83
84            let norm_y = y_val / total;
85            tops.push(1.0 - consumed);
86            bottoms.push(1.0 - consumed - norm_y);
87
88            *x_cumsum.entry(x_key).or_insert(0.0) += norm_y;
89        }
90
91        let vjust = if self.vjust.is_finite() {
92            self.vjust
93        } else {
94            1.0
95        };
96
97        let write_ymax = (vjust - 1.0).abs() > f64::EPSILON;
98        let new_y: Vec<Value> = if write_ymax {
99            bottoms
100                .iter()
101                .zip(tops.iter())
102                .map(|(&b, &t)| Value::Float((1.0 - vjust) * b + vjust * t))
103                .collect()
104        } else {
105            tops.iter().map(|&t| Value::Float(t)).collect()
106        };
107        let ymin_vals: Vec<Value> = bottoms.into_iter().map(Value::Float).collect();
108
109        if let Some(col) = data.column_mut("y") {
110            *col = new_y;
111        }
112        if !data.has_column("ymin") {
113            data.add_column("ymin".to_string(), ymin_vals);
114        } else if let Some(col) = data.column_mut("ymin") {
115            *col = ymin_vals;
116        }
117        if write_ymax {
118            let ymax_vals: Vec<Value> = tops.into_iter().map(Value::Float).collect();
119            if !data.has_column("ymax") {
120                data.add_column("ymax".to_string(), ymax_vals);
121            } else if let Some(col) = data.column_mut("ymax") {
122                *col = ymax_vals;
123            }
124        }
125    }
126
127    fn name(&self) -> &str {
128        "fill"
129    }
130
131    fn per_panel(&self) -> bool {
132        true
133    }
134
135    fn validate(&self, warnings: &mut Vec<String>) {
136        if !self.vjust.is_finite() {
137            warnings.push(format!(
138                "position_fill: vjust({}) must be finite; falling back to 1",
139                self.vjust
140            ));
141        }
142    }
143}