Skip to main content

ggplot_rs/stat/
pack.rs

1//! `StatPack`: packed-bubble / circle-packing layout (d3-hierarchy's
2//! `packSiblings` front-chain algorithm by Wang et al., with Welzl's
3//! smallest enclosing circle), deterministic.
4//!
5//! Each row is a circle with area ∝ `weight` (1 when unmapped). Without a
6//! `subgroup` mapping all circles are packed together; with one, each
7//! group is packed on its own, wrapped in its enclosing circle, and the
8//! group circles are packed in turn (Highcharts' clustered packed bubble).
9//!
10//! Output: one row per circle (`part = "leaf"`, every input column carried
11//! through) then one per group (`part = "group"`, `subgroup` set), each
12//! with centre `x`/`y` and radius `r` in layout units (a group row's
13//! `fill` is its first member's), plus constant
14//! `xmin`/`xmax`/`ymin`/`ymax` framing the whole pack as a square.
15
16use crate::aes::Aesthetic;
17use crate::data::{DataFrame, Value};
18use crate::scale::ScaleSet;
19
20use super::Stat;
21
22#[derive(Clone, Copy, Debug, PartialEq)]
23pub(crate) struct Circle {
24    pub x: f64,
25    pub y: f64,
26    pub r: f64,
27}
28
29fn place(b: Circle, a: Circle, c: &mut Circle) {
30    let (dx, dy) = (b.x - a.x, b.y - a.y);
31    let d2 = dx * dx + dy * dy;
32    if d2 > 0.0 {
33        let a2 = (a.r + c.r).powi(2);
34        let b2 = (b.r + c.r).powi(2);
35        if a2 > b2 {
36            let x = (d2 + b2 - a2) / (2.0 * d2);
37            let y = (b2 / d2 - x * x).max(0.0).sqrt();
38            c.x = b.x - x * dx - y * dy;
39            c.y = b.y - x * dy + y * dx;
40        } else {
41            let x = (d2 + a2 - b2) / (2.0 * d2);
42            let y = (a2 / d2 - x * x).max(0.0).sqrt();
43            c.x = a.x + x * dx - y * dy;
44            c.y = a.y + x * dy + y * dx;
45        }
46    } else {
47        c.x = a.x + c.r;
48        c.y = a.y;
49    }
50}
51
52fn intersects(a: Circle, b: Circle) -> bool {
53    let dr = a.r + b.r - 1e-6;
54    let (dx, dy) = (b.x - a.x, b.y - a.y);
55    dr > 0.0 && dr * dr > dx * dx + dy * dy
56}
57
58/// Pack `circles` (radii set; positions overwritten) around the origin
59/// without overlap; returns the radius of their enclosing circle, which is
60/// centred on the origin afterwards.
61pub(crate) fn pack_siblings(circles: &mut [Circle]) -> f64 {
62    let n = circles.len();
63    if n == 0 {
64        return 0.0;
65    }
66    circles[0].x = 0.0;
67    circles[0].y = 0.0;
68    if n == 1 {
69        return circles[0].r;
70    }
71    circles[0].x = -circles[1].r;
72    circles[1].x = circles[0].r;
73    circles[1].y = 0.0;
74    if n == 2 {
75        return circles[0].r + circles[1].r;
76    }
77    let (c0, c1) = (circles[0], circles[1]);
78    place(c1, c0, &mut circles[2]);
79
80    // Front chain as a doubly linked list over circle indices.
81    let mut next = vec![usize::MAX; n];
82    let mut prev = vec![usize::MAX; n];
83    let (mut a, mut b) = (0usize, 1usize);
84    next[0] = 1;
85    prev[1] = 0;
86    next[1] = 2;
87    prev[2] = 1;
88    next[2] = 0;
89    prev[0] = 2;
90    let score = |c: &[Circle], i: usize, next: &[usize]| {
91        let (p, q) = (c[i], c[next[i]]);
92        let ab = p.r + q.r;
93        let dx = (p.x * q.r + q.x * p.r) / ab;
94        let dy = (p.y * q.r + q.y * p.r) / ab;
95        dx * dx + dy * dy
96    };
97
98    let mut i = 3;
99    'pack: while i < n {
100        let (ca, cb) = (circles[a], circles[b]);
101        place(ca, cb, &mut circles[i]);
102        let c = i;
103        let (mut j, mut k) = (next[b], prev[a]);
104        let (mut sj, mut sk) = (circles[b].r, circles[a].r);
105        loop {
106            if sj <= sk {
107                if intersects(circles[j], circles[c]) {
108                    b = j;
109                    next[a] = b;
110                    prev[b] = a;
111                    continue 'pack;
112                }
113                sj += circles[j].r;
114                j = next[j];
115            } else {
116                if intersects(circles[k], circles[c]) {
117                    a = k;
118                    next[a] = b;
119                    prev[b] = a;
120                    continue 'pack;
121                }
122                sk += circles[k].r;
123                k = prev[k];
124            }
125            if j == next[k] {
126                break;
127            }
128        }
129        // Insert c between a and b.
130        prev[c] = a;
131        next[c] = b;
132        next[a] = c;
133        prev[b] = c;
134        b = c;
135        // New closest pair to the centroid.
136        let mut best = score(circles, a, &next);
137        let mut cur = next[c];
138        while cur != b {
139            let s = score(circles, cur, &next);
140            if s < best {
141                a = cur;
142                best = s;
143            }
144            cur = next[cur];
145        }
146        b = next[a];
147        i += 1;
148    }
149
150    let mut chain = vec![circles[b]];
151    let mut cur = next[b];
152    while cur != b {
153        chain.push(circles[cur]);
154        cur = next[cur];
155    }
156    let e = enclose(&chain);
157    for c in circles.iter_mut() {
158        c.x -= e.x;
159        c.y -= e.y;
160    }
161    e.r
162}
163
164fn encloses_not(a: Circle, b: Circle) -> bool {
165    let dr = a.r - b.r;
166    let (dx, dy) = (b.x - a.x, b.y - a.y);
167    dr < 0.0 || dr * dr < dx * dx + dy * dy
168}
169
170fn encloses_weak(a: Circle, b: Circle) -> bool {
171    let dr = a.r - b.r + a.r.max(b.r).max(1.0) * 1e-9;
172    let (dx, dy) = (b.x - a.x, b.y - a.y);
173    dr > 0.0 && dr * dr > dx * dx + dy * dy
174}
175
176fn encloses_weak_all(a: Circle, set: &[Circle]) -> bool {
177    set.iter().all(|&b| encloses_weak(a, b))
178}
179
180fn basis2(a: Circle, b: Circle) -> Circle {
181    let (x21, y21, r21) = (b.x - a.x, b.y - a.y, b.r - a.r);
182    let l = (x21 * x21 + y21 * y21).sqrt();
183    if l == 0.0 {
184        return if a.r >= b.r { a } else { b };
185    }
186    Circle {
187        x: (a.x + b.x + x21 / l * r21) / 2.0,
188        y: (a.y + b.y + y21 / l * r21) / 2.0,
189        r: (l + a.r + b.r) / 2.0,
190    }
191}
192
193fn basis3(a: Circle, b: Circle, c: Circle) -> Circle {
194    let (x1, y1, r1) = (a.x, a.y, a.r);
195    let (a2, a3, b2, b3) = (x1 - b.x, x1 - c.x, y1 - b.y, y1 - c.y);
196    let (c2, c3) = (b.r - r1, c.r - r1);
197    let d1 = x1 * x1 + y1 * y1 - r1 * r1;
198    let d2 = d1 - b.x * b.x - b.y * b.y + b.r * b.r;
199    let d3 = d1 - c.x * c.x - c.y * c.y + c.r * c.r;
200    let ab = a3 * b2 - a2 * b3;
201    let xa = (b2 * d3 - b3 * d2) / (ab * 2.0) - x1;
202    let xb = (b3 * c2 - b2 * c3) / ab;
203    let ya = (a3 * d2 - a2 * d3) / (ab * 2.0) - y1;
204    let yb = (a2 * c3 - a3 * c2) / ab;
205    let qa = xb * xb + yb * yb - 1.0;
206    let qb = 2.0 * (r1 + xa * xb + ya * yb);
207    let qc = xa * xa + ya * ya - r1 * r1;
208    let r = -(if qa.abs() > 1e-6 {
209        (qb + (qb * qb - 4.0 * qa * qc).sqrt()) / (2.0 * qa)
210    } else {
211        qc / qb
212    });
213    Circle {
214        x: x1 + xa + xb * r,
215        y: y1 + ya + yb * r,
216        r,
217    }
218}
219
220fn basis(set: &[Circle]) -> Circle {
221    match set {
222        [a] => *a,
223        [a, b] => basis2(*a, *b),
224        [a, b, c] => basis3(*a, *b, *c),
225        _ => Circle {
226            x: 0.0,
227            y: 0.0,
228            r: 0.0,
229        },
230    }
231}
232
233fn extend_basis(set: &[Circle], p: Circle) -> Vec<Circle> {
234    if encloses_weak_all(p, set) {
235        return vec![p];
236    }
237    for &b in set {
238        if encloses_not(p, b) && encloses_weak_all(basis2(b, p), set) {
239            return vec![b, p];
240        }
241    }
242    for i in 0..set.len().saturating_sub(1) {
243        for j in i + 1..set.len() {
244            let (bi, bj) = (set[i], set[j]);
245            if encloses_not(basis2(bi, bj), p)
246                && encloses_not(basis2(bi, p), bj)
247                && encloses_not(basis2(bj, p), bi)
248                && encloses_weak_all(basis3(bi, bj, p), set)
249            {
250                return vec![bi, bj, p];
251            }
252        }
253    }
254    // Numerical corner case: fall back to the pair basis.
255    vec![set[0], p]
256}
257
258/// Smallest circle enclosing `circles` (Welzl / d3's `packEnclose`), with a
259/// fixed-seed shuffle so the result is deterministic.
260pub(crate) fn enclose(circles: &[Circle]) -> Circle {
261    let mut c = circles.to_vec();
262    // Fisher–Yates with a fixed LCG (d3 uses a seeded LCG too).
263    let mut state: u64 = 0x9E37_79B9_7F4A_7C15;
264    for i in (1..c.len()).rev() {
265        state = state
266            .wrapping_mul(6364136223846793005)
267            .wrapping_add(1442695040888963407);
268        let j = (state >> 33) as usize % (i + 1);
269        c.swap(i, j);
270    }
271    let (mut i, mut set, mut e): (usize, Vec<Circle>, Option<Circle>) = (0, Vec::new(), None);
272    let mut guard = 0usize;
273    while i < c.len() {
274        let p = c[i];
275        if e.is_some_and(|e| encloses_weak(e, p)) {
276            i += 1;
277        } else {
278            set = extend_basis(&set, p);
279            e = Some(basis(&set));
280            i = 0;
281            guard += 1;
282            if guard > 10 * c.len() * c.len() + 100 {
283                break;
284            }
285        }
286    }
287    e.unwrap_or(Circle {
288        x: 0.0,
289        y: 0.0,
290        r: 0.0,
291    })
292}
293
294/// One packed group: its subgroup value, `(row, radius)` members, their
295/// circles and the enclosing radius.
296type PackedGroup = (Value, Vec<(usize, f64)>, Vec<Circle>, f64);
297
298/// Packed-bubble stat behind [`GeomPackedBubble`](crate::geom::pack::GeomPackedBubble).
299#[derive(Clone, Debug)]
300pub struct StatPack {
301    /// Pack larger circles first (tighter, more regular packs).
302    pub sort: bool,
303    /// Gap between circles as a fraction of the mean leaf radius.
304    pub padding: f64,
305}
306
307impl Default for StatPack {
308    fn default() -> Self {
309        StatPack {
310            sort: true,
311            padding: 0.08,
312        }
313    }
314}
315
316impl Stat for StatPack {
317    fn compute_group(&self, data: &DataFrame, _scales: &ScaleSet) -> DataFrame {
318        let weight = data.column("weight");
319        let rows: Vec<(usize, f64)> = (0..data.nrows())
320            .filter_map(|i| {
321                let w = match weight {
322                    Some(c) => c[i].as_f64()?,
323                    None => 1.0,
324                };
325                (w.is_finite() && w > 0.0).then_some((i, w.sqrt()))
326            })
327            .collect();
328        if rows.is_empty() {
329            return DataFrame::new();
330        }
331        let mean_r = rows.iter().map(|r| r.1).sum::<f64>() / rows.len() as f64;
332        let pad = self.padding * mean_r;
333
334        let sub = data.column("subgroup");
335        let mut groups: indexmap::IndexMap<String, (Value, Vec<(usize, f64)>)> =
336            indexmap::IndexMap::new();
337        for &(i, r) in &rows {
338            let (key, val) = match sub {
339                Some(c) => (c[i].key_str().into_owned(), c[i].clone()),
340                None => (String::new(), Value::Na),
341            };
342            groups
343                .entry(key)
344                .or_insert_with(|| (val, Vec::new()))
345                .1
346                .push((i, r));
347        }
348
349        let pack = |members: &mut Vec<(usize, f64)>| -> (Vec<Circle>, f64) {
350            if self.sort {
351                members.sort_by(|a, b| b.1.total_cmp(&a.1));
352            }
353            let mut circles: Vec<Circle> = members
354                .iter()
355                .map(|&(_, r)| Circle {
356                    x: 0.0,
357                    y: 0.0,
358                    r: r + pad / 2.0,
359                })
360                .collect();
361            let outer = pack_siblings(&mut circles);
362            for c in circles.iter_mut() {
363                c.r -= pad / 2.0;
364            }
365            (circles, outer)
366        };
367
368        let mut leaves: Vec<(usize, Circle)> = Vec::new();
369        let mut group_rows: Vec<(Value, Circle, usize)> = Vec::new();
370        let grouped = sub.is_some();
371        let mut packed: Vec<PackedGroup> = groups
372            .into_values()
373            .map(|(val, mut m)| {
374                let (c, outer) = pack(&mut m);
375                (val, m, c, outer)
376            })
377            .collect();
378        if grouped {
379            let mut outers: Vec<Circle> = packed
380                .iter()
381                .map(|g| Circle {
382                    x: 0.0,
383                    y: 0.0,
384                    r: g.3 + pad * 1.5,
385                })
386                .collect();
387            pack_siblings(&mut outers);
388            for (g, o) in packed.iter_mut().zip(&outers) {
389                for c in g.2.iter_mut() {
390                    c.x += o.x;
391                    c.y += o.y;
392                }
393                group_rows.push((g.0.clone(), Circle { r: g.3, ..*o }, g.1[0].0));
394            }
395        }
396        for g in &packed {
397            for (&(i, _), &c) in g.1.iter().zip(&g.2) {
398                leaves.push((i, c));
399            }
400        }
401        let all: Vec<Circle> = leaves
402            .iter()
403            .map(|l| l.1)
404            .chain(group_rows.iter().map(|g| g.1))
405            .collect();
406        let half = all
407            .iter()
408            .map(|c| c.x.abs().max(c.y.abs()) + c.r)
409            .fold(0.0, f64::max);
410
411        let n = leaves.len() + group_rows.len();
412        let mut out = DataFrame::new();
413        let mut part = vec![Value::Str("leaf".into()); leaves.len()];
414        part.extend(std::iter::repeat_n(
415            Value::Str("group".into()),
416            group_rows.len(),
417        ));
418        out.add_column("part".into(), part);
419        out.add_column("x".into(), all.iter().map(|c| Value::Float(c.x)).collect());
420        out.add_column("y".into(), all.iter().map(|c| Value::Float(c.y)).collect());
421        out.add_column("r".into(), all.iter().map(|c| Value::Float(c.r)).collect());
422        for (c, v) in [
423            ("xmin", -half),
424            ("xmax", half),
425            ("ymin", -half),
426            ("ymax", half),
427        ] {
428            out.add_column(c.into(), vec![Value::Float(v); n]);
429        }
430        for name in data.column_names() {
431            if out.has_column(name) {
432                continue;
433            }
434            let col = data.column(name).unwrap_or_default();
435            let mut vals: Vec<Value> = leaves.iter().map(|l| col[l.0].clone()).collect();
436            if name == "subgroup" {
437                vals.extend(group_rows.iter().map(|g| g.0.clone()));
438            } else if name == "fill" {
439                // Unfilled, but must not train an NA level: first member's.
440                vals.extend(group_rows.iter().map(|g| col[g.2].clone()));
441            } else {
442                vals.extend(std::iter::repeat_n(Value::Na, group_rows.len()));
443            }
444            out.add_column(name.to_string(), vals);
445        }
446        out
447    }
448
449    fn required_aes(&self) -> Vec<Aesthetic> {
450        Vec::new()
451    }
452
453    /// One pack per panel.
454    fn panelwise(&self) -> bool {
455        true
456    }
457
458    fn name(&self) -> &str {
459        "pack"
460    }
461}
462
463#[cfg(test)]
464mod tests {
465    use super::*;
466
467    fn no_overlap(c: &[Circle]) {
468        for i in 0..c.len() {
469            for j in i + 1..c.len() {
470                let d = (c[i].x - c[j].x).hypot(c[i].y - c[j].y);
471                assert!(d >= c[i].r + c[j].r - 1e-6, "{i} and {j} overlap: {d}");
472            }
473        }
474    }
475
476    #[test]
477    fn siblings_never_overlap_and_fit_their_enclosure() {
478        let mut c: Vec<Circle> = (1..=40)
479            .map(|k| Circle {
480                x: 0.0,
481                y: 0.0,
482                r: ((k * 37 % 11) as f64 + 1.0).sqrt(),
483            })
484            .collect();
485        let outer = pack_siblings(&mut c);
486        no_overlap(&c);
487        for k in &c {
488            assert!(k.x.hypot(k.y) + k.r <= outer + 1e-6);
489        }
490        // Reasonably tight: the enclosure isn't far above the area bound.
491        let area: f64 = c.iter().map(|k| k.r * k.r).sum();
492        assert!(outer * outer < 2.2 * area, "{outer}");
493    }
494
495    #[test]
496    fn enclose_three_known_circles() {
497        let e = enclose(&[
498            Circle {
499                x: -1.0,
500                y: 0.0,
501                r: 1.0,
502            },
503            Circle {
504                x: 1.0,
505                y: 0.0,
506                r: 1.0,
507            },
508            Circle {
509                x: 0.0,
510                y: 0.0,
511                r: 0.5,
512            },
513        ]);
514        assert!((e.r - 2.0).abs() < 1e-9 && e.x.abs() < 1e-9 && e.y.abs() < 1e-9);
515    }
516
517    #[test]
518    fn grouped_leaves_sit_inside_their_group_circle() {
519        let mut d = DataFrame::new();
520        let s = |v: &[&str]| v.iter().map(|x| Value::Str(x.to_string())).collect();
521        d.add_column("subgroup".into(), s(&["A", "A", "A", "B", "B", "C"]));
522        d.add_column(
523            "weight".into(),
524            [9.0, 4.0, 1.0, 16.0, 2.0, 5.0]
525                .iter()
526                .map(|&v| Value::Float(v))
527                .collect(),
528        );
529        let out = StatPack::default().compute_group(&d, &ScaleSet::new());
530        assert_eq!(out.nrows(), 6 + 3);
531        let f = |c: &str, i: usize| out.column(c).unwrap()[i].as_f64().unwrap();
532        let sub = |i: usize| out.column("subgroup").unwrap()[i].key_str().into_owned();
533        let leaves: Vec<Circle> = (0..6)
534            .map(|i| Circle {
535                x: f("x", i),
536                y: f("y", i),
537                r: f("r", i),
538            })
539            .collect();
540        no_overlap(&leaves);
541        for i in 0..6 {
542            let g = (6..9).find(|&g| sub(g) == sub(i)).unwrap();
543            let d = (f("x", i) - f("x", g)).hypot(f("y", i) - f("y", g));
544            assert!(
545                d + f("r", i) <= f("r", g) + 1e-6,
546                "leaf {i} outside its group"
547            );
548        }
549        // Area ∝ weight: radius 3 vs 2 for weights 9 and 4.
550        let r: Vec<f64> = (0..3).map(|i| f("r", i)).collect();
551        assert!((r[0] / r[1] - 1.5).abs() < 0.05, "{r:?}");
552    }
553}