1use std::collections::HashMap;
17
18use crate::aes::Aesthetic;
19use crate::data::{DataFrame, Value};
20use crate::scale::ScaleSet;
21
22use super::Stat;
23
24#[derive(Clone, Debug)]
26pub struct StatSankey {
27 pub node_width: f64,
29 pub node_padding: f64,
31 pub justify: bool,
34 pub iterations: usize,
36}
37
38impl Default for StatSankey {
39 fn default() -> Self {
40 StatSankey {
41 node_width: 0.12,
42 node_padding: 0.08,
43 justify: false,
44 iterations: 6,
45 }
46 }
47}
48
49struct Layout {
50 names: Vec<String>,
51 column: Vec<usize>,
52 value: Vec<f64>,
53 top: Vec<f64>,
54 links: Vec<(usize, usize, f64)>,
56}
57
58impl StatSankey {
59 fn layout(&self, data: &DataFrame) -> Option<Layout> {
60 let (from, to) = (data.column("from")?, data.column("to")?);
61 let weight = data.column("weight");
62 let mut index: HashMap<String, usize> = HashMap::new();
63 let mut names = Vec::new();
64 let mut id = |v: &Value| -> Option<usize> {
65 if v.is_na() {
66 return None;
67 }
68 let key = v.key_str().into_owned();
69 Some(*index.entry(key.clone()).or_insert_with(|| {
70 names.push(key);
71 names.len() - 1
72 }))
73 };
74 let mut pairs: indexmap::IndexMap<(usize, usize), f64> = indexmap::IndexMap::new();
75 for i in 0..data.nrows() {
76 let (Some(a), Some(b)) = (id(&from[i]), id(&to[i])) else {
77 continue;
78 };
79 let w = match weight {
80 Some(c) => match c[i].as_f64() {
81 Some(w) if w.is_finite() && w > 0.0 => w,
82 _ => continue,
83 },
84 None => 1.0,
85 };
86 if a != b {
87 *pairs.entry((a, b)).or_insert(0.0) += w;
88 }
89 }
90 let n = names.len();
91 if n == 0 {
92 return None;
93 }
94 let links: Vec<(usize, usize, f64)> =
95 pairs.into_iter().map(|((a, b), w)| (a, b, w)).collect();
96
97 let mut column = vec![0usize; n];
99 for _ in 0..n {
100 let mut changed = false;
101 for &(a, b, _) in &links {
102 if column[b] < column[a] + 1 && column[a] + 1 < n {
103 column[b] = column[a] + 1;
104 changed = true;
105 }
106 }
107 if !changed {
108 break;
109 }
110 }
111 let (mut inflow, mut outflow) = (vec![0.0; n], vec![0.0; n]);
112 for &(a, b, w) in &links {
113 outflow[a] += w;
114 inflow[b] += w;
115 }
116 let last = column.iter().copied().max().unwrap_or(0);
117 if self.justify {
118 for k in 0..n {
119 if outflow[k] == 0.0 {
120 column[k] = last;
121 }
122 }
123 }
124 let value: Vec<f64> = (0..n).map(|k| f64::max(inflow[k], outflow[k])).collect();
125
126 let mut cols: Vec<Vec<usize>> = vec![Vec::new(); last + 1];
127 for k in 0..n {
128 cols[column[k]].push(k);
129 }
130 let col_total = |c: &Vec<usize>| c.iter().map(|&k| value[k]).sum::<f64>();
131 let max_total = cols.iter().map(col_total).fold(0.0, f64::max);
132 let pad = self.node_padding * max_total;
133 let height = cols
134 .iter()
135 .map(|c| col_total(c) + pad * c.len().saturating_sub(1) as f64)
136 .fold(0.0, f64::max);
137
138 let mut top = vec![0.0; n];
139 let stack = |c: &Vec<usize>, top: &mut Vec<f64>| {
140 let h = col_total(c) + pad * c.len().saturating_sub(1) as f64;
141 let mut y = height - (height - h) / 2.0;
142 for &k in c {
143 top[k] = y;
144 y -= value[k] + pad;
145 }
146 };
147 for c in &cols {
148 stack(c, &mut top);
149 }
150 for _ in 0..self.iterations {
151 for forward in [true, false] {
152 let order: Vec<usize> = if forward {
153 (1..cols.len()).collect()
154 } else {
155 (0..cols.len().saturating_sub(1)).rev().collect()
156 };
157 for ci in order {
158 let center = |k: usize| top[k] - value[k] / 2.0;
159 let mut keyed: Vec<(f64, usize)> = cols[ci]
160 .iter()
161 .map(|&k| {
162 let (mut sw, mut sy) = (0.0, 0.0);
163 for &(a, b, w) in &links {
164 let other = if forward && b == k {
165 a
166 } else if !forward && a == k {
167 b
168 } else {
169 continue;
170 };
171 sw += w;
172 sy += w * center(other);
173 }
174 (if sw > 0.0 { sy / sw } else { center(k) }, k)
175 })
176 .collect();
177 keyed.sort_by(|a, b| b.0.total_cmp(&a.0));
179 cols[ci] = keyed.into_iter().map(|(_, k)| k).collect();
180 stack(&cols[ci], &mut top);
181 }
182 }
183 }
184 Some(Layout {
185 names,
186 column,
187 value,
188 top,
189 links,
190 })
191 }
192}
193
194impl Stat for StatSankey {
195 fn compute_group(&self, data: &DataFrame, _scales: &ScaleSet) -> DataFrame {
196 let Some(l) = self.layout(data) else {
197 return DataFrame::new();
198 };
199 let n = l.names.len();
200 let half = self.node_width / 2.0;
201 let center = |k: usize| l.top[k] - l.value[k] / 2.0;
202
203 let mut rows: Vec<[f64; 6]> = Vec::new();
204 let mut text: Vec<[String; 4]> = Vec::new();
205 let mut value = Vec::new();
206 let mut column = Vec::new();
207 for k in 0..n {
208 let x = l.column[k] as f64;
209 let (lo, hi) = (l.top[k] - l.value[k], l.top[k]);
210 rows.push([x - half, x + half, lo, hi, lo, hi]);
211 let name = l.names[k].clone();
212 text.push(["node".into(), name.clone(), name.clone(), name]);
213 value.push(l.value[k]);
214 column.push(l.column[k] as f64);
215 }
216 let mut out_off = vec![0.0; n];
219 let mut in_off = vec![0.0; n];
220 let mut by_source: Vec<usize> = (0..l.links.len()).collect();
221 by_source.sort_by(|&i, &j| {
222 let (a1, b1, _) = l.links[i];
223 let (a2, b2, _) = l.links[j];
224 a1.cmp(&a2).then(center(b2).total_cmp(¢er(b1)))
225 });
226 let mut src_band = vec![(0.0, 0.0); l.links.len()];
227 for i in by_source {
228 let (a, _, w) = l.links[i];
229 let hi = l.top[a] - out_off[a];
230 src_band[i] = (hi - w, hi);
231 out_off[a] += w;
232 }
233 let mut by_target: Vec<usize> = (0..l.links.len()).collect();
234 by_target.sort_by(|&i, &j| {
235 let (a1, b1, _) = l.links[i];
236 let (a2, b2, _) = l.links[j];
237 b1.cmp(&b2).then(center(a2).total_cmp(¢er(a1)))
238 });
239 let mut dst_band = vec![(0.0, 0.0); l.links.len()];
240 for i in by_target {
241 let (_, b, w) = l.links[i];
242 let hi = l.top[b] - in_off[b];
243 dst_band[i] = (hi - w, hi);
244 in_off[b] += w;
245 }
246 for (i, &(a, b, w)) in l.links.iter().enumerate() {
247 let (x0, x1) = (l.column[a] as f64 + half, l.column[b] as f64 - half);
248 rows.push([
249 x0,
250 x1,
251 src_band[i].0,
252 src_band[i].1,
253 dst_band[i].0,
254 dst_band[i].1,
255 ]);
256 let (fa, tb) = (l.names[a].clone(), l.names[b].clone());
257 text.push(["link".into(), fa.clone(), fa, tb]);
258 value.push(w);
259 column.push(l.column[a] as f64);
260 }
261
262 let num = |k: usize| rows.iter().map(|r| Value::Float(r[k])).collect::<Vec<_>>();
263 let txt = |k: usize| {
264 text.iter()
265 .map(|r| Value::Str(r[k].clone()))
266 .collect::<Vec<_>>()
267 };
268 let mut out = DataFrame::new();
269 out.add_column("part".into(), txt(0));
270 out.add_column("xmin".into(), num(0));
271 out.add_column("xmax".into(), num(1));
272 out.add_column("ymin".into(), num(2));
273 out.add_column("ymax".into(), num(3));
274 out.add_column("yend_min".into(), num(4));
275 out.add_column("yend_max".into(), num(5));
276 out.add_column("node".into(), txt(1));
277 out.add_column("from".into(), txt(2));
278 out.add_column("to".into(), txt(3));
279 out.add_column(
280 "value".into(),
281 value.into_iter().map(Value::Float).collect(),
282 );
283 out.add_column(
284 "column".into(),
285 column.into_iter().map(Value::Float).collect(),
286 );
287 out
288 }
289
290 fn required_aes(&self) -> Vec<Aesthetic> {
291 vec![Aesthetic::From, Aesthetic::To]
292 }
293
294 fn panelwise(&self) -> bool {
296 true
297 }
298
299 fn name(&self) -> &str {
300 "sankey"
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 fn edges(e: &[(&str, &str, f64)]) -> DataFrame {
309 let mut d = DataFrame::new();
310 d.add_column(
311 "from".into(),
312 e.iter().map(|r| Value::Str(r.0.to_string())).collect(),
313 );
314 d.add_column(
315 "to".into(),
316 e.iter().map(|r| Value::Str(r.1.to_string())).collect(),
317 );
318 d.add_column(
319 "weight".into(),
320 e.iter().map(|r| Value::Float(r.2)).collect(),
321 );
322 d
323 }
324
325 #[test]
326 fn columns_values_and_conservation() {
327 let d = edges(&[
328 ("Coal", "Power", 40.0),
329 ("Gas", "Power", 30.0),
330 ("Gas", "Heat", 20.0),
331 ("Power", "Homes", 50.0),
332 ("Power", "Industry", 20.0),
333 ("Gas", "Power", 5.0),
334 ]);
335 let l = StatSankey::default().layout(&d).unwrap();
336 let idx = |n: &str| l.names.iter().position(|x| x == n).unwrap();
337 assert_eq!(l.column[idx("Coal")], 0);
338 assert_eq!(l.column[idx("Power")], 1);
339 assert_eq!(l.column[idx("Homes")], 2);
340 assert_eq!(l.column[idx("Heat")], 1);
341 assert_eq!(l.value[idx("Power")], 75.0, "max(in 75, out 70)");
342 assert_eq!(l.value[idx("Gas")], 55.0, "repeated pair summed");
343 assert_eq!(l.links.len(), 5);
344
345 let out = StatSankey::default().compute_group(&d, &ScaleSet::new());
346 assert_eq!(out.nrows(), 6 + 5);
347 let f = |c: &str| -> Vec<f64> {
349 out.column(c)
350 .unwrap()
351 .iter()
352 .map(|v| v.as_f64().unwrap())
353 .collect()
354 };
355 let (ymin, ymax, emin, emax) = (f("ymin"), f("ymax"), f("yend_min"), f("yend_max"));
356 let s = |c: &str, i: usize| out.column(c).unwrap()[i].key_str().into_owned();
357 let node_row = |n: &str| (0..6).find(|&i| s("node", i) == n).unwrap();
358 for i in 6..11 {
359 let (a, b) = (node_row(&s("from", i)), node_row(&s("to", i)));
360 assert!(ymin[i] >= ymin[a] - 1e-9 && ymax[i] <= ymax[a] + 1e-9);
361 assert!(emin[i] >= ymin[b] - 1e-9 && emax[i] <= ymax[b] + 1e-9);
362 }
363 }
364
365 #[test]
366 fn justify_moves_sinks_to_the_last_column() {
367 let d = edges(&[("A", "B", 1.0), ("B", "C", 1.0), ("A", "D", 1.0)]);
368 let l = StatSankey {
369 justify: true,
370 ..Default::default()
371 }
372 .layout(&d)
373 .unwrap();
374 let idx = |n: &str| l.names.iter().position(|x| x == n).unwrap();
375 assert_eq!(l.column[idx("D")], 2);
376 }
377
378 #[test]
379 fn cycles_terminate() {
380 let d = edges(&[("A", "B", 1.0), ("B", "A", 1.0)]);
381 assert!(StatSankey::default().layout(&d).is_some());
382 }
383}