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)]
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#[allow(non_upper_case_globals)]
30pub const PositionFill: PositionFill = PositionFill { vjust: 1.0 };
31
32impl PositionFill {
33 pub fn with_vjust(mut self, vjust: f64) -> Self {
36 self.vjust = vjust;
37 self
38 }
39}
40
41pub 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 let n = y_col.len();
60 let mut tops = vec![0.0_f64; n];
61 let mut bottoms = vec![0.0_f64; n];
62
63 let seq = super::stacking_sequence(data, params.group_order.as_deref());
78 for positive in [true, false] {
79 let sign_total = if positive { 1.0 } else { -1.0 };
80 let mut totals: HashMap<Cow<'_, str>, f64> = HashMap::new();
81 for (x, y) in x_col.iter().zip(y_col.iter()) {
82 let row = super::stack_row(y);
83 if row.is_positive_bucket() != positive {
84 continue;
85 }
86 *totals.entry(x.key_str()).or_insert(0.0) += row.contribution();
87 }
88
89 let mut consumed: HashMap<Cow<'_, str>, f64> = HashMap::new();
90 for &i in &seq {
91 let (x, y) = (&x_col[i], &y_col[i]);
92 let row = super::stack_row(y);
93 if row.is_positive_bucket() != positive {
94 continue;
95 }
96 let x_key = x.key_str();
97 let total = totals.get(&x_key).copied().unwrap_or(0.0);
98 let denom = if total.abs() < f64::EPSILON {
99 1.0
100 } else {
101 total.abs()
102 };
103 let run = consumed.get(&x_key).copied().unwrap_or(0.0);
104
105 let Some(y_val) = row.value() else {
106 tops[i] = f64::NAN;
107 bottoms[i] = f64::NAN;
108 continue;
109 };
110
111 let norm_y = y_val / denom;
112 tops[i] = sign_total - run;
113 bottoms[i] = sign_total - run - norm_y;
114
115 *consumed.entry(x_key).or_insert(0.0) += norm_y;
116 }
117 }
118
119 let vjust = if self.vjust.is_finite() {
120 self.vjust
121 } else {
122 1.0
123 };
124
125 let write_ymax = (vjust - 1.0).abs() > f64::EPSILON;
126 let lo_hi: Vec<(f64, f64)> = tops
127 .iter()
128 .zip(bottoms.iter())
129 .map(|(&t, &b)| (t.min(b), t.max(b)))
130 .collect();
131 let new_y: Vec<Value> = if write_ymax {
132 lo_hi
133 .iter()
134 .map(|&(lo, hi)| Value::Float((1.0 - vjust) * lo + vjust * hi))
135 .collect()
136 } else {
137 lo_hi.iter().map(|&(_, hi)| Value::Float(hi)).collect()
138 };
139 let ymin_vals: Vec<Value> = lo_hi.iter().map(|&(lo, _)| Value::Float(lo)).collect();
140
141 if let Some(col) = data.column_mut("y") {
142 *col = new_y;
143 }
144 if !data.has_column("ymin") {
145 data.add_column("ymin".to_string(), ymin_vals);
146 } else if let Some(col) = data.column_mut("ymin") {
147 *col = ymin_vals;
148 }
149 if write_ymax {
150 let ymax_vals: Vec<Value> = lo_hi.iter().map(|&(_, hi)| Value::Float(hi)).collect();
151 if !data.has_column("ymax") {
152 data.add_column("ymax".to_string(), ymax_vals);
153 } else if let Some(col) = data.column_mut("ymax") {
154 *col = ymax_vals;
155 }
156 }
157 }
158
159 fn name(&self) -> &str {
160 "fill"
161 }
162
163 fn group_order(&self, data: &DataFrame) -> Option<Vec<String>> {
164 super::dodge::group_keys(data).map(|keys| super::dodge::dedup_order(&keys))
165 }
166
167 fn per_panel(&self) -> bool {
168 true
169 }
170
171 fn validate(&self, warnings: &mut Vec<String>) {
172 if !self.vjust.is_finite() {
173 warnings.push(format!(
174 "position_fill: vjust({}) must be finite; falling back to 1",
175 self.vjust
176 ));
177 }
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 fn frame(x: &[f64], g: &[&str], y: &[f64]) -> DataFrame {
186 let mut df = DataFrame::new();
187 df.add_column("x".into(), x.iter().map(|v| Value::Float(*v)).collect());
188 df.add_column(
189 "fill".into(),
190 g.iter().map(|s| Value::Str(s.to_string())).collect(),
191 );
192 df.add_column("y".into(), y.iter().map(|v| Value::Float(*v)).collect());
193 df
194 }
195
196 fn col(df: &DataFrame, c: &str) -> Vec<f64> {
197 df.column(c)
198 .unwrap()
199 .iter()
200 .map(|v| v.as_f64().unwrap())
201 .collect()
202 }
203
204 #[test]
212 fn negative_and_positive_values_fill_independently() {
213 let mut df = frame(&[1.0; 4], &["a", "b", "c", "d"], &[3.0, -2.0, 4.0, -1.0]);
214 PositionFill::default()
215 .with_vjust(0.0)
216 .compute(&mut df, &PositionParams::default());
217 let y = col(&df, "y");
218 let ymin = col(&df, "ymin");
219 let ymax = col(&df, "ymax");
220 for (i, (want_y, want_min, want_max)) in [
221 (4.0 / 7.0, 4.0 / 7.0, 1.0),
222 (-1.0, -1.0, -1.0 / 3.0),
223 (0.0, 0.0, 4.0 / 7.0),
224 (-1.0 / 3.0, -1.0 / 3.0, 0.0),
225 ]
226 .into_iter()
227 .enumerate()
228 {
229 assert!((y[i] - want_y).abs() < 1e-6, "y[{i}]={}", y[i]);
230 assert!((ymin[i] - want_min).abs() < 1e-6, "ymin[{i}]={}", ymin[i]);
231 assert!((ymax[i] - want_max).abs() < 1e-6, "ymax[{i}]={}", ymax[i]);
232 }
233 }
234
235 #[test]
238 fn all_positive_values_are_unaffected() {
239 let mut df = frame(&[1.0, 1.0], &["a", "b"], &[3.0, 5.0]);
240 PositionFill.compute(&mut df, &PositionParams::default());
241 let y = col(&df, "y");
242 assert!((y[0] - 1.0).abs() < 1e-9);
243 assert!((y[1] - 0.625).abs() < 1e-9);
244 }
245
246 #[test]
253 fn missing_y_contributes_zero_and_does_not_poison_its_bucket() {
254 let mut df = frame(&[1.0, 1.0, 1.0], &["a", "b", "c"], &[3.0, f64::NAN, -2.0]);
255 PositionFill.compute(&mut df, &PositionParams::default());
256 let y = col(&df, "y");
257 let ymin = col(&df, "ymin");
258 assert_eq!(
259 (y[0], ymin[0]),
260 (1.0, 0.0),
261 "a must normalize as if b's row weren't there at all: y={y:?} ymin={ymin:?}"
262 );
263 assert!(
264 y[1].is_nan() && ymin[1].is_nan(),
265 "b's own y/ymin must stay NaN (not drawn): y={y:?} ymin={ymin:?}"
266 );
267 assert_eq!(
268 (y[2], ymin[2]),
269 (0.0, -1.0),
270 "c (negative bucket) must be unaffected: y={y:?} ymin={ymin:?}"
271 );
272 }
273}