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