1use crate::aes::Aesthetic;
16use crate::data::{DataFrame, Value};
17use crate::scale::ScaleSet;
18
19use super::Stat;
20
21pub(crate) type Rect = (f64, f64, f64, f64);
23
24#[derive(Clone, Debug)]
26pub struct StatTreemap {
27 pub aspect: f64,
29 pub group_padding: f64,
31}
32
33impl Default for StatTreemap {
34 fn default() -> Self {
35 StatTreemap {
36 aspect: 1.6,
37 group_padding: 0.006,
38 }
39 }
40}
41
42fn worst(row: &[f64], side: f64) -> f64 {
44 let sum: f64 = row.iter().sum();
45 let (mx, mn) = row
46 .iter()
47 .fold((f64::MIN, f64::MAX), |(a, b), &v| (a.max(v), b.min(v)));
48 let (s2, sum2) = (side * side, sum * sum);
49 f64::max(s2 * mx / sum2, sum2 / (s2 * mn))
50}
51
52pub(crate) fn squarify(values: &[f64], rect: Rect) -> Vec<Rect> {
56 let mut out = vec![(0.0, 0.0, 0.0, 0.0); values.len()];
57 let total: f64 = values.iter().sum();
58 let (_, _, w0, h0) = rect;
59 if values.is_empty() || total <= 0.0 || w0 <= 0.0 || h0 <= 0.0 {
60 return out;
61 }
62 let scale = w0 * h0 / total;
63 let mut order: Vec<usize> = (0..values.len()).collect();
64 order.sort_by(|&a, &b| values[b].total_cmp(&values[a]));
65 let areas: Vec<f64> = order.iter().map(|&i| values[i] * scale).collect();
66
67 let (mut x, y, mut w, mut h) = rect;
68 let mut start = 0;
69 while start < areas.len() {
70 let side = w.min(h);
71 let mut end = start + 1;
72 while end < areas.len()
73 && worst(&areas[start..=end], side) <= worst(&areas[start..end], side)
74 {
75 end += 1;
76 }
77 let row = &areas[start..end];
78 let sum: f64 = row.iter().sum();
79 if w >= h {
80 let sw = if h > 0.0 { sum / h } else { 0.0 };
82 let mut top = y + h;
83 for (k, a) in row.iter().enumerate() {
84 let th = if sw > 0.0 { a / sw } else { 0.0 };
85 out[order[start + k]] = (x, top - th, sw, th);
86 top -= th;
87 }
88 x += sw;
89 w -= sw;
90 } else {
91 let sh = if w > 0.0 { sum / w } else { 0.0 };
93 let mut left = x;
94 for (k, a) in row.iter().enumerate() {
95 let tw = if sh > 0.0 { a / sh } else { 0.0 };
96 out[order[start + k]] = (left, y + h - sh, tw, sh);
97 left += tw;
98 }
99 h -= sh;
100 }
101 start = end;
102 }
103 out
104}
105
106impl Stat for StatTreemap {
107 fn compute_group(&self, data: &DataFrame, _scales: &ScaleSet) -> DataFrame {
108 let Some(weight) = data.column("weight") else {
109 return DataFrame::new();
110 };
111 let rows: Vec<usize> = (0..data.nrows())
112 .filter(|&i| weight[i].as_f64().is_some_and(|w| w.is_finite() && w > 0.0))
113 .collect();
114 if rows.is_empty() {
115 return DataFrame::new();
116 }
117 let w = |i: usize| weight[i].as_f64().unwrap_or(0.0);
118 let full: Rect = (0.0, 0.0, self.aspect.max(1e-6), 1.0);
119 let mut tile = vec![(0.0, 0.0, 0.0, 0.0); data.nrows()];
120 let mut groups: Vec<(Value, f64, Rect, usize)> = Vec::new();
121
122 if let Some(sub) = data.column("subgroup") {
123 let mut members: indexmap::IndexMap<String, (Value, Vec<usize>)> =
124 indexmap::IndexMap::new();
125 for &i in &rows {
126 members
127 .entry(sub[i].key_str().into_owned())
128 .or_insert_with(|| (sub[i].clone(), Vec::new()))
129 .1
130 .push(i);
131 }
132 let totals: Vec<f64> = members
133 .values()
134 .map(|(_, m)| m.iter().map(|&i| w(i)).sum())
135 .collect();
136 let rects = squarify(&totals, full);
137 let p = self.group_padding;
138 for (((val, m), total), r) in members.into_values().zip(&totals).zip(rects) {
139 let inner = (
140 r.0 + p,
141 r.1 + p,
142 (r.2 - 2.0 * p).max(0.0),
143 (r.3 - 2.0 * p).max(0.0),
144 );
145 let vals: Vec<f64> = m.iter().map(|&i| w(i)).collect();
146 for (&i, t) in m.iter().zip(squarify(&vals, inner)) {
147 tile[i] = t;
148 }
149 groups.push((val, *total, r, m[0]));
150 }
151 } else {
152 let vals: Vec<f64> = rows.iter().map(|&i| w(i)).collect();
153 for (&i, t) in rows.iter().zip(squarify(&vals, full)) {
154 tile[i] = t;
155 }
156 }
157
158 let n = rows.len() + groups.len();
159 let mut out = DataFrame::new();
160 let mut part = vec![Value::Str("tile".into()); rows.len()];
161 part.extend(std::iter::repeat_n(
162 Value::Str("group".into()),
163 groups.len(),
164 ));
165 out.add_column("part".into(), part);
166 let edge = |f: fn(&Rect) -> f64| -> Vec<Value> {
167 rows.iter()
168 .map(|&i| Value::Float(f(&tile[i])))
169 .chain(groups.iter().map(|g| Value::Float(f(&g.2))))
170 .collect()
171 };
172 out.add_column("xmin".into(), edge(|r| r.0));
173 out.add_column("xmax".into(), edge(|r| r.0 + r.2));
174 out.add_column("ymin".into(), edge(|r| r.1));
175 out.add_column("ymax".into(), edge(|r| r.1 + r.3));
176 out.add_column(
177 "area".into(),
178 rows.iter()
179 .map(|&i| Value::Float(w(i)))
180 .chain(groups.iter().map(|g| Value::Float(g.1)))
181 .collect(),
182 );
183 for name in data.column_names() {
184 if out.has_column(name) {
185 continue;
186 }
187 let col = data.column(name).unwrap_or_default();
188 let mut vals: Vec<Value> = rows.iter().map(|&i| col[i].clone()).collect();
189 if name == "subgroup" {
190 vals.extend(groups.iter().map(|g| g.0.clone()));
191 } else if name == "fill" {
192 vals.extend(groups.iter().map(|g| col[g.3].clone()));
195 } else {
196 vals.extend(std::iter::repeat_n(Value::Na, groups.len()));
197 }
198 debug_assert_eq!(vals.len(), n);
199 out.add_column(name.to_string(), vals);
200 }
201 out
202 }
203
204 fn required_aes(&self) -> Vec<Aesthetic> {
205 vec![Aesthetic::Weight]
206 }
207
208 fn panelwise(&self) -> bool {
210 true
211 }
212
213 fn name(&self) -> &str {
214 "treemap"
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn squarify_tiles_exactly_and_keeps_areas() {
224 let vals = [6.0, 6.0, 4.0, 3.0, 2.0, 2.0, 1.0];
225 let rect = (0.0, 0.0, 6.0, 4.0);
226 let r = squarify(&vals, rect);
227 let total: f64 = r.iter().map(|t| t.2 * t.3).sum();
228 assert!((total - 24.0).abs() < 1e-9);
229 for (v, t) in vals.iter().zip(&r) {
230 assert!((t.2 * t.3 - v).abs() < 1e-9, "{t:?}");
231 assert!(t.0 >= -1e-9 && t.0 + t.2 <= 6.0 + 1e-9);
232 assert!(t.1 >= -1e-9 && t.1 + t.3 <= 4.0 + 1e-9);
233 }
234 let worst = r
236 .iter()
237 .map(|t| (t.2 / t.3).max(t.3 / t.2))
238 .fold(0.0, f64::max);
239 assert!(worst < 3.0, "{worst}");
240 for i in 0..r.len() {
242 for j in i + 1..r.len() {
243 let (a, b) = (r[i], r[j]);
244 let ox = (a.0 + a.2).min(b.0 + b.2) - a.0.max(b.0);
245 let oy = (a.1 + a.3).min(b.1 + b.3) - a.1.max(b.1);
246 assert!(ox <= 1e-9 || oy <= 1e-9, "{a:?} overlaps {b:?}");
247 }
248 }
249 }
250
251 #[test]
252 fn subgroups_nest_tiles_inside_group_rects() {
253 let mut d = DataFrame::new();
254 let s = |v: &[&str]| v.iter().map(|x| Value::Str(x.to_string())).collect();
255 d.add_column("label".into(), s(&["a", "b", "c", "d", "e"]));
256 d.add_column("subgroup".into(), s(&["G1", "G1", "G2", "G2", "G2"]));
257 d.add_column(
258 "weight".into(),
259 [5.0, 3.0, 4.0, 2.0, 2.0]
260 .iter()
261 .map(|&v| Value::Float(v))
262 .collect(),
263 );
264 let out = StatTreemap::default().compute_group(&d, &ScaleSet::new());
265 assert_eq!(out.nrows(), 5 + 2);
266 let f = |c: &str| -> Vec<f64> {
267 out.column(c)
268 .unwrap()
269 .iter()
270 .map(|v| v.as_f64().unwrap())
271 .collect()
272 };
273 let (x0, x1, y0, y1) = (f("xmin"), f("xmax"), f("ymin"), f("ymax"));
274 let sub = |i: usize| out.column("subgroup").unwrap()[i].key_str().into_owned();
275 for t in 0..5 {
276 let g = if sub(t) == "G1" { 5 } else { 6 };
277 assert!(x0[t] >= x0[g] && x1[t] <= x1[g] && y0[t] >= y0[g] && y1[t] <= y1[g]);
278 }
279 let ga = (x1[5] - x0[5]) * (y1[5] - y0[5]);
281 assert!((ga - 0.8).abs() < 1e-9, "{ga}");
282 assert_eq!(out.column("label").unwrap()[5], Value::Na);
283 }
284}