1use std::f64::consts::PI;
18
19use crate::aes::Aesthetic;
20use crate::data::{DataFrame, Value};
21use crate::scale::ScaleSet;
22
23use super::Stat;
24
25#[derive(Clone, Copy, Debug, PartialEq)]
27pub enum UnitLayout {
28 Waffle { rows: usize },
30 Parliament { rows: usize, inner: f64 },
33}
34
35#[derive(Clone, Debug)]
37pub struct StatWaffle {
38 pub layout: UnitLayout,
39}
40
41impl Default for StatWaffle {
42 fn default() -> Self {
43 StatWaffle {
44 layout: UnitLayout::Waffle { rows: 10 },
45 }
46 }
47}
48
49pub(crate) fn seats_per_row(n: usize, rows: usize, inner: f64) -> Vec<usize> {
52 let radius = |i: usize| {
53 if rows == 1 {
54 1.0
55 } else {
56 inner + (1.0 - inner) * i as f64 / (rows - 1) as f64
57 }
58 };
59 let total_r: f64 = (0..rows).map(radius).sum();
60 let exact: Vec<f64> = (0..rows).map(|i| n as f64 * radius(i) / total_r).collect();
61 let mut seats: Vec<usize> = exact.iter().map(|e| e.floor() as usize).collect();
62 let mut left = n - seats.iter().sum::<usize>();
63 let mut order: Vec<usize> = (0..rows).collect();
65 order.sort_by(|&a, &b| (exact[b] - exact[b].floor()).total_cmp(&(exact[a] - exact[a].floor())));
66 for i in order {
67 if left == 0 {
68 break;
69 }
70 seats[i] += 1;
71 left -= 1;
72 }
73 seats
74}
75
76impl Stat for StatWaffle {
77 fn compute_group(&self, data: &DataFrame, _scales: &ScaleSet) -> DataFrame {
78 let weight = data.column("weight");
79 let fill = data.column("fill");
81 let mut cats: indexmap::IndexMap<String, Vec<usize>> = indexmap::IndexMap::new();
82 for i in 0..data.nrows() {
83 let w = match weight {
84 Some(c) => c[i].as_f64().filter(|w| w.is_finite() && *w > 0.0),
85 None => Some(1.0),
86 };
87 let Some(w) = w else { continue };
88 let key = fill
89 .map(|c| c[i].key_str().into_owned())
90 .unwrap_or_default();
91 let units = cats.entry(key).or_default();
92 units.extend(std::iter::repeat_n(i, w.round() as usize));
93 }
94 let units: Vec<usize> = cats.into_values().flatten().collect();
95 let n = units.len();
96 if n == 0 {
97 return DataFrame::new();
98 }
99
100 let (pos, unit, frame): (Vec<(f64, f64)>, f64, [f64; 4]) = match self.layout {
101 UnitLayout::Waffle { rows } => {
102 let rows = rows.max(1);
103 let cols = n.div_ceil(rows);
104 let pos = (0..n)
105 .map(|k| ((k / rows) as f64 + 1.0, (k % rows) as f64 + 1.0))
106 .collect();
107 (pos, 0.5, [0.5, cols as f64 + 0.5, 0.5, rows as f64 + 0.5])
108 }
109 UnitLayout::Parliament { rows, inner } => {
110 let inner = inner.clamp(0.05, 0.95);
111 let rows = if rows > 0 {
112 rows
113 } else {
114 ((n as f64 / 4.0).sqrt().round() as usize).clamp(1, 40)
115 };
116 let spacing = if rows > 1 {
117 (1.0 - inner) / (rows - 1) as f64
118 } else {
119 1.0 - inner
120 };
121 let mut seats: Vec<(f64, f64, f64)> = Vec::with_capacity(n); for (i, k) in seats_per_row(n, rows, inner).into_iter().enumerate() {
123 let r = if rows == 1 {
124 1.0
125 } else {
126 inner + spacing * i as f64
127 };
128 for j in 0..k {
129 let a = if k == 1 {
130 PI / 2.0
131 } else {
132 PI * (1.0 - j as f64 / (k - 1) as f64)
133 };
134 seats.push((a, r * a.cos(), r * a.sin()));
135 }
136 }
137 seats.sort_by(|a, b| {
139 b.0.total_cmp(&a.0)
140 .then((b.1.hypot(b.2)).total_cmp(&a.1.hypot(a.2)))
141 });
142 let unit = 0.45 * spacing.min(PI / (n as f64 / rows as f64).max(1.0));
145 let pos = seats.into_iter().map(|s| (s.1, s.2)).collect();
146 (pos, unit, [-1.0 - unit, 1.0 + unit, -unit, 1.0 + unit])
147 }
148 };
149
150 let mut out = DataFrame::new();
151 out.add_column("x".into(), pos.iter().map(|p| Value::Float(p.0)).collect());
152 out.add_column("y".into(), pos.iter().map(|p| Value::Float(p.1)).collect());
153 out.add_column("unit".into(), vec![Value::Float(unit); n]);
154 for (c, v) in ["xmin", "xmax", "ymin", "ymax"].iter().zip(frame) {
155 out.add_column(c.to_string(), vec![Value::Float(v); n]);
156 }
157 for name in data.column_names() {
158 if out.has_column(name) {
159 continue;
160 }
161 let col = data.column(name).unwrap_or_default();
162 out.add_column(
163 name.to_string(),
164 units.iter().map(|&i| col[i].clone()).collect(),
165 );
166 }
167 out
168 }
169
170 fn required_aes(&self) -> Vec<Aesthetic> {
171 Vec::new()
172 }
173
174 fn panelwise(&self) -> bool {
176 true
177 }
178
179 fn name(&self) -> &str {
180 "waffle"
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 fn data() -> DataFrame {
189 let mut d = DataFrame::new();
190 d.add_column(
191 "fill".into(),
192 ["A", "B", "C"]
193 .iter()
194 .map(|s| Value::Str(s.to_string()))
195 .collect(),
196 );
197 d.add_column(
198 "weight".into(),
199 [12.0, 7.0, 4.0].iter().map(|&v| Value::Float(v)).collect(),
200 );
201 d
202 }
203
204 #[test]
205 fn waffle_fills_columns_bottom_up_in_category_order() {
206 let out = StatWaffle {
207 layout: UnitLayout::Waffle { rows: 5 },
208 }
209 .compute_group(&data(), &ScaleSet::new());
210 assert_eq!(out.nrows(), 23);
211 let x: Vec<f64> = out
212 .column("x")
213 .unwrap()
214 .iter()
215 .map(|v| v.as_f64().unwrap())
216 .collect();
217 let y: Vec<f64> = out
218 .column("y")
219 .unwrap()
220 .iter()
221 .map(|v| v.as_f64().unwrap())
222 .collect();
223 assert_eq!((x[0], y[0]), (1.0, 1.0));
224 assert_eq!((x[5], y[5]), (2.0, 1.0));
225 assert_eq!(out.column("fill").unwrap()[12], Value::Str("B".into()));
226 assert_eq!(
227 out.column("xmax").unwrap()[0],
228 Value::Float(5.5),
229 "ceil(23/5) cols"
230 );
231 }
232
233 #[test]
234 fn parliament_seats_sum_and_sit_on_the_half_disk() {
235 assert_eq!(seats_per_row(101, 4, 0.4).iter().sum::<usize>(), 101);
236 let out = StatWaffle {
237 layout: UnitLayout::Parliament {
238 rows: 0,
239 inner: 0.4,
240 },
241 }
242 .compute_group(&data(), &ScaleSet::new());
243 assert_eq!(out.nrows(), 23);
244 for i in 0..23 {
245 let (x, y) = (
246 out.column("x").unwrap()[i].as_f64().unwrap(),
247 out.column("y").unwrap()[i].as_f64().unwrap(),
248 );
249 let r = x.hypot(y);
250 assert!(y >= -1e-9 && (0.39..=1.0 + 1e-9).contains(&r), "{x} {y}");
251 }
252 let x0 = out.column("x").unwrap()[0].as_f64().unwrap();
254 assert!(x0 < -0.3, "{x0}");
255 }
256}