ggplot_rs/position/
fill.rs1use 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] {
73 let sign_total = if positive { 1.0 } else { -1.0 };
74 let mut totals: HashMap<Cow<'_, str>, f64> = HashMap::new();
75 for (x, y) in x_col.iter().zip(y_col.iter()) {
76 let y_val = y.as_f64().unwrap_or(0.0);
77 if (y_val >= 0.0) != positive {
78 continue;
79 }
80 *totals.entry(x.key_str()).or_insert(0.0) += y_val;
81 }
82
83 let mut consumed: HashMap<Cow<'_, str>, f64> = HashMap::new();
84 for (i, (x, y)) in x_col.iter().zip(y_col.iter()).enumerate() {
85 let y_val = y.as_f64().unwrap_or(0.0);
86 if (y_val >= 0.0) != positive {
87 continue;
88 }
89 let x_key = x.key_str();
90 let total = totals.get(&x_key).copied().unwrap_or(0.0);
91 let denom = if total.abs() < f64::EPSILON {
92 1.0
93 } else {
94 total.abs()
95 };
96 let run = consumed.get(&x_key).copied().unwrap_or(0.0);
97
98 let norm_y = y_val / denom;
99 tops[i] = sign_total - run;
100 bottoms[i] = sign_total - run - norm_y;
101
102 *consumed.entry(x_key).or_insert(0.0) += norm_y;
103 }
104 }
105
106 let vjust = if self.vjust.is_finite() {
107 self.vjust
108 } else {
109 1.0
110 };
111
112 let write_ymax = (vjust - 1.0).abs() > f64::EPSILON;
113 let lo_hi: Vec<(f64, f64)> = tops
114 .iter()
115 .zip(bottoms.iter())
116 .map(|(&t, &b)| (t.min(b), t.max(b)))
117 .collect();
118 let new_y: Vec<Value> = if write_ymax {
119 lo_hi
120 .iter()
121 .map(|&(lo, hi)| Value::Float((1.0 - vjust) * lo + vjust * hi))
122 .collect()
123 } else {
124 lo_hi.iter().map(|&(_, hi)| Value::Float(hi)).collect()
125 };
126 let ymin_vals: Vec<Value> = lo_hi.iter().map(|&(lo, _)| Value::Float(lo)).collect();
127
128 if let Some(col) = data.column_mut("y") {
129 *col = new_y;
130 }
131 if !data.has_column("ymin") {
132 data.add_column("ymin".to_string(), ymin_vals);
133 } else if let Some(col) = data.column_mut("ymin") {
134 *col = ymin_vals;
135 }
136 if write_ymax {
137 let ymax_vals: Vec<Value> = lo_hi.iter().map(|&(_, hi)| Value::Float(hi)).collect();
138 if !data.has_column("ymax") {
139 data.add_column("ymax".to_string(), ymax_vals);
140 } else if let Some(col) = data.column_mut("ymax") {
141 *col = ymax_vals;
142 }
143 }
144 }
145
146 fn name(&self) -> &str {
147 "fill"
148 }
149
150 fn per_panel(&self) -> bool {
151 true
152 }
153
154 fn validate(&self, warnings: &mut Vec<String>) {
155 if !self.vjust.is_finite() {
156 warnings.push(format!(
157 "position_fill: vjust({}) must be finite; falling back to 1",
158 self.vjust
159 ));
160 }
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 fn frame(x: &[f64], g: &[&str], y: &[f64]) -> DataFrame {
169 let mut df = DataFrame::new();
170 df.add_column("x".into(), x.iter().map(|v| Value::Float(*v)).collect());
171 df.add_column(
172 "fill".into(),
173 g.iter().map(|s| Value::Str(s.to_string())).collect(),
174 );
175 df.add_column("y".into(), y.iter().map(|v| Value::Float(*v)).collect());
176 df
177 }
178
179 fn col(df: &DataFrame, c: &str) -> Vec<f64> {
180 df.column(c)
181 .unwrap()
182 .iter()
183 .map(|v| v.as_f64().unwrap())
184 .collect()
185 }
186
187 #[test]
195 fn negative_and_positive_values_fill_independently() {
196 let mut df = frame(&[1.0; 4], &["a", "b", "c", "d"], &[3.0, -2.0, 4.0, -1.0]);
197 PositionFill::default()
198 .with_vjust(0.0)
199 .compute(&mut df, &PositionParams::default());
200 let y = col(&df, "y");
201 let ymin = col(&df, "ymin");
202 let ymax = col(&df, "ymax");
203 for (i, (want_y, want_min, want_max)) in [
204 (4.0 / 7.0, 4.0 / 7.0, 1.0),
205 (-1.0, -1.0, -1.0 / 3.0),
206 (0.0, 0.0, 4.0 / 7.0),
207 (-1.0 / 3.0, -1.0 / 3.0, 0.0),
208 ]
209 .into_iter()
210 .enumerate()
211 {
212 assert!((y[i] - want_y).abs() < 1e-6, "y[{i}]={}", y[i]);
213 assert!((ymin[i] - want_min).abs() < 1e-6, "ymin[{i}]={}", ymin[i]);
214 assert!((ymax[i] - want_max).abs() < 1e-6, "ymax[{i}]={}", ymax[i]);
215 }
216 }
217
218 #[test]
221 fn all_positive_values_are_unaffected() {
222 let mut df = frame(&[1.0, 1.0], &["a", "b"], &[3.0, 5.0]);
223 PositionFill.compute(&mut df, &PositionParams::default());
224 let y = col(&df, "y");
225 assert!((y[0] - 1.0).abs() < 1e-9);
226 assert!((y[1] - 0.625).abs() < 1e-9);
227 }
228}