1use std::f64::consts::PI;
18
19use crate::aes::Aesthetic;
20use crate::data::{DataFrame, Value};
21use crate::scale::ScaleSet;
22
23use super::Stat;
24
25pub(crate) fn lens_area(r1: f64, r2: f64, d: f64) -> f64 {
27 if d >= r1 + r2 {
28 return 0.0;
29 }
30 if d <= (r1 - r2).abs() {
31 return PI * r1.min(r2).powi(2);
32 }
33 let a = r1
34 * r1
35 * ((d * d + r1 * r1 - r2 * r2) / (2.0 * d * r1))
36 .clamp(-1.0, 1.0)
37 .acos();
38 let b = r2
39 * r2
40 * ((d * d + r2 * r2 - r1 * r1) / (2.0 * d * r2))
41 .clamp(-1.0, 1.0)
42 .acos();
43 let c = 0.5
44 * ((-d + r1 + r2) * (d + r1 - r2) * (d - r1 + r2) * (d + r1 + r2))
45 .max(0.0)
46 .sqrt();
47 a + b - c
48}
49
50pub(crate) fn distance_for_overlap(r1: f64, r2: f64, overlap: f64) -> f64 {
53 let full = PI * r1.min(r2).powi(2);
54 if overlap <= 0.0 {
55 return r1 + r2 + 0.05 * (r1 + r2);
56 }
57 if overlap >= full {
58 return (r1 - r2).abs();
59 }
60 let (mut lo, mut hi) = ((r1 - r2).abs(), r1 + r2);
61 for _ in 0..100 {
62 let mid = (lo + hi) / 2.0;
63 if lens_area(r1, r2, mid) > overlap {
64 lo = mid;
65 } else {
66 hi = mid;
67 }
68 }
69 (lo + hi) / 2.0
70}
71
72#[derive(Clone, Debug, Default)]
74pub struct StatVenn;
75
76impl Stat for StatVenn {
77 fn compute_group(&self, data: &DataFrame, _scales: &ScaleSet) -> DataFrame {
78 let Some(sets_col) = data.column("sets") else {
79 return DataFrame::new();
80 };
81 let weight = data.column("weight");
82 let parse = |v: &Value| -> Vec<String> {
83 let mut s: Vec<String> = v
84 .key_str()
85 .split('&')
86 .map(|p| p.trim().to_string())
87 .filter(|p| !p.is_empty())
88 .collect();
89 s.sort();
90 s.dedup();
91 s
92 };
93 let mut names: Vec<String> = Vec::new();
95 let mut combos: Vec<(Vec<String>, f64)> = Vec::new();
96 for i in 0..data.nrows() {
97 let combo = parse(&sets_col[i]);
98 let size = match weight {
99 Some(c) => c[i].as_f64().unwrap_or(f64::NAN),
100 None => 1.0,
101 };
102 if combo.is_empty() || !size.is_finite() || size < 0.0 {
103 continue;
104 }
105 for s in &combo {
106 if !names.contains(s) {
107 names.push(s.clone());
108 }
109 }
110 combos.push((combo, size));
111 }
112 let n = names.len();
113 if n == 0 {
114 return DataFrame::new();
115 }
116 let idx = |s: &str| names.iter().position(|m| m == s).unwrap();
117 let mut size = vec![0.0; n];
118 let mut pair = vec![vec![0.0; n]; n];
119 for (combo, s) in &combos {
120 match combo.as_slice() {
121 [a] => size[idx(a)] = *s,
122 [a, b] => {
123 let (i, j) = (idx(a), idx(b));
124 pair[i][j] = *s;
125 pair[j][i] = *s;
126 }
127 _ => {}
128 }
129 }
130 for i in 0..n {
132 let implied = (0..n).map(|j| pair[i][j]).fold(0.0, f64::max);
133 size[i] = size[i].max(implied).max(1e-9);
134 }
135 let r: Vec<f64> = size.iter().map(|s| (s / PI).sqrt()).collect();
136 let mut d = vec![vec![0.0; n]; n];
137 for i in 0..n {
138 for j in i + 1..n {
139 let dij = distance_for_overlap(r[i], r[j], pair[i][j].min(size[i]).min(size[j]));
140 d[i][j] = dij;
141 d[j][i] = dij;
142 }
143 }
144 let mut pos = vec![(0.0, 0.0); n];
145 if n >= 2 {
146 pos[1] = (d[0][1], 0.0);
147 }
148 if n >= 3 {
149 let (a, b, c) = (d[0][1], d[0][2], d[1][2]);
152 let x = if a > 0.0 {
153 (a * a + b * b - c * c) / (2.0 * a)
154 } else {
155 0.0
156 };
157 pos[2] = (x, -(b * b - x * x).max(0.0).sqrt());
158 }
159 if n >= 4 {
160 let spread = d.iter().flatten().fold(0.0f64, |m, &v| m.max(v));
163 for (k, p) in pos.iter_mut().enumerate().skip(3) {
164 let a = 2.0 * PI * k as f64 / n as f64;
165 *p = (spread * a.cos() / 2.0, spread * a.sin() / 2.0);
166 }
167 for it in 0..2000 {
168 let step = 0.05 * (1.0 - it as f64 / 2000.0) + 0.002;
169 let mut grad = vec![(0.0, 0.0); n];
170 for i in 0..n {
171 for j in 0..n {
172 if i == j {
173 continue;
174 }
175 let (dx, dy) = (pos[i].0 - pos[j].0, pos[i].1 - pos[j].1);
176 let dist = dx.hypot(dy).max(1e-9);
177 let e = (dist - d[i][j]) / dist;
178 grad[i].0 += e * dx;
179 grad[i].1 += e * dy;
180 }
181 }
182 for i in 0..n {
183 pos[i].0 -= step * grad[i].0;
184 pos[i].1 -= step * grad[i].1;
185 }
186 }
187 }
188 let (mut x0, mut x1, mut y0, mut y1) = (f64::MAX, f64::MIN, f64::MAX, f64::MIN);
190 for k in 0..n {
191 x0 = x0.min(pos[k].0 - r[k]);
192 x1 = x1.max(pos[k].0 + r[k]);
193 y0 = y0.min(pos[k].1 - r[k]);
194 y1 = y1.max(pos[k].1 + r[k]);
195 }
196 let (cx, cy) = ((x0 + x1) / 2.0, (y0 + y1) / 2.0);
197 for p in pos.iter_mut() {
198 p.0 -= cx;
199 p.1 -= cy;
200 }
201 let half = ((x1 - x0).max(y1 - y0) / 2.0) * 1.08;
202
203 let inside = |p: (f64, f64), k: usize| (p.0 - pos[k].0).hypot(p.1 - pos[k].1) < r[k];
206 let margin =
207 |p: (f64, f64), k: usize| (r[k] - (p.0 - pos[k].0).hypot(p.1 - pos[k].1)).abs();
208 let grid = 80;
209 let mut labels: Vec<(f64, f64, String, usize)> = Vec::new();
210 for (combo, s) in &combos {
211 let members: Vec<usize> = combo.iter().map(|c| idx(c)).collect();
212 let mut best: Option<((f64, f64), f64)> = None;
213 for gx in 0..=grid {
214 for gy in 0..=grid {
215 let p = (
216 -half + 2.0 * half * gx as f64 / grid as f64,
217 -half + 2.0 * half * gy as f64 / grid as f64,
218 );
219 if !(0..n).all(|k| inside(p, k) == members.contains(&k)) {
220 continue;
221 }
222 let m = (0..n).map(|k| margin(p, k)).fold(f64::INFINITY, f64::min);
223 if best.is_none_or(|b| m > b.1) {
224 best = Some((p, m));
225 }
226 }
227 }
228 if let Some((p, _)) = best {
229 let text = if members.len() == 1 {
230 names[members[0]].clone()
231 } else {
232 crate::format::format_value(&Value::Float(*s))
233 };
234 labels.push((p.0, p.1, text, members[0]));
235 }
236 }
237
238 let rows = n + labels.len();
239 let s = |v: &str| Value::Str(v.to_string());
240 let mut part = vec![s("circle"); n];
241 part.extend(std::iter::repeat_n(s("label"), labels.len()));
242 let xs: Vec<Value> = pos
243 .iter()
244 .map(|p| Value::Float(p.0))
245 .chain(labels.iter().map(|l| Value::Float(l.0)))
246 .collect();
247 let ys: Vec<Value> = pos
248 .iter()
249 .map(|p| Value::Float(p.1))
250 .chain(labels.iter().map(|l| Value::Float(l.1)))
251 .collect();
252 let rs: Vec<Value> = r
253 .iter()
254 .map(|&v| Value::Float(v))
255 .chain(std::iter::repeat_n(Value::Float(0.0), labels.len()))
256 .collect();
257 let set: Vec<Value> = names
260 .iter()
261 .map(|nm| s(nm))
262 .chain(labels.iter().map(|l| s(&names[l.3])))
263 .collect();
264 let text: Vec<Value> = names
265 .iter()
266 .map(|_| Value::Na)
267 .chain(labels.iter().map(|l| s(&l.2)))
268 .collect();
269 let sizes: Vec<Value> = size
270 .iter()
271 .map(|&v| Value::Float(v))
272 .chain(std::iter::repeat_n(Value::Na, labels.len()))
273 .collect();
274 let mut out = DataFrame::new();
275 out.add_column("part".into(), part);
276 out.add_column("x".into(), xs);
277 out.add_column("y".into(), ys);
278 out.add_column("r".into(), rs);
279 out.add_column("set".into(), set);
280 out.add_column("text".into(), text);
281 out.add_column("size".into(), sizes);
282 for (c, v) in [
283 ("xmin", -half),
284 ("xmax", half),
285 ("ymin", -half),
286 ("ymax", half),
287 ] {
288 out.add_column(c.into(), vec![Value::Float(v); rows]);
289 }
290 out
291 }
292
293 fn required_aes(&self) -> Vec<Aesthetic> {
294 vec![Aesthetic::Sets]
295 }
296
297 fn panelwise(&self) -> bool {
299 true
300 }
301
302 fn name(&self) -> &str {
303 "venn"
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 fn data(rows: &[(&str, f64)]) -> DataFrame {
312 let mut d = DataFrame::new();
313 d.add_column(
314 "sets".into(),
315 rows.iter().map(|r| Value::Str(r.0.into())).collect(),
316 );
317 d.add_column(
318 "weight".into(),
319 rows.iter().map(|r| Value::Float(r.1)).collect(),
320 );
321 d
322 }
323
324 fn f(d: &DataFrame, c: &str) -> Vec<f64> {
325 d.column(c)
326 .unwrap()
327 .iter()
328 .map(|v| v.as_f64().unwrap_or(f64::NAN))
329 .collect()
330 }
331
332 #[test]
333 fn two_sets_overlap_exactly() {
334 let out = StatVenn.compute_group(
335 &data(&[("A", 100.0), ("B", 60.0), ("A&B", 25.0)]),
336 &ScaleSet::new(),
337 );
338 let (x, y, r) = (f(&out, "x"), f(&out, "y"), f(&out, "r"));
339 assert!((PI * r[0] * r[0] - 100.0).abs() < 1e-9);
340 let d = (x[0] - x[1]).hypot(y[0] - y[1]);
341 assert!((lens_area(r[0], r[1], d) - 25.0).abs() < 1e-6);
342 let texts: Vec<String> = out.column("text").unwrap()[2..]
344 .iter()
345 .map(|v| v.key_str().into_owned())
346 .collect();
347 assert_eq!(texts, ["A", "B", "25"]);
348 }
349
350 #[test]
351 fn three_and_four_sets_fit_pairwise_overlaps() {
352 let rows = [
353 ("A", 50.0),
354 ("B", 50.0),
355 ("C", 50.0),
356 ("A&B", 10.0),
357 ("A&C", 10.0),
358 ("B&C", 10.0),
359 ];
360 let out = StatVenn.compute_group(&data(&rows), &ScaleSet::new());
361 let (x, y, r) = (f(&out, "x"), f(&out, "y"), f(&out, "r"));
362 for (i, j) in [(0, 1), (0, 2), (1, 2)] {
363 let d = (x[i] - x[j]).hypot(y[i] - y[j]);
364 assert!((lens_area(r[i], r[j], d) - 10.0).abs() < 1e-6, "{i}{j}");
365 }
366 let four = [
367 ("A", 40.0),
368 ("B", 40.0),
369 ("C", 40.0),
370 ("D", 40.0),
371 ("A&B", 8.0),
372 ("B&C", 8.0),
373 ("C&D", 8.0),
374 ("A&D", 8.0),
375 ];
376 let out = StatVenn.compute_group(&data(&four), &ScaleSet::new());
377 let (x, y, r) = (f(&out, "x"), f(&out, "y"), f(&out, "r"));
378 for (i, j) in [(0, 1), (1, 2), (2, 3), (0, 3)] {
379 let d = (x[i] - x[j]).hypot(y[i] - y[j]);
380 let got = lens_area(r[i], r[j], d);
381 assert!((got - 8.0).abs() < 3.0, "{i}{j}: {got}");
382 }
383 }
384}