1use std::collections::HashMap;
17use std::f64::consts::TAU;
18
19use crate::aes::Aesthetic;
20use crate::data::{DataFrame, Value};
21use crate::scale::ScaleSet;
22
23use super::Stat;
24
25#[derive(Clone, Debug)]
27pub struct StatChord {
28 pub pad_angle: f64,
30 pub x_extent: f64,
33 pub y_extent: f64,
35}
36
37impl Default for StatChord {
38 fn default() -> Self {
39 StatChord {
40 pad_angle: 0.04,
41 x_extent: 1.45,
42 y_extent: 1.25,
43 }
44 }
45}
46
47impl Stat for StatChord {
48 fn compute_group(&self, data: &DataFrame, _scales: &ScaleSet) -> DataFrame {
49 let (Some(from), Some(to)) = (data.column("from"), data.column("to")) else {
50 return DataFrame::new();
51 };
52 let weight = data.column("weight");
53 let mut index: HashMap<String, usize> = HashMap::new();
54 let mut names: Vec<String> = Vec::new();
55 let mut id = |v: &Value| -> Option<usize> {
56 if v.is_na() {
57 return None;
58 }
59 let key = v.key_str().into_owned();
60 Some(*index.entry(key.clone()).or_insert_with(|| {
61 names.push(key);
62 names.len() - 1
63 }))
64 };
65 let mut pairs: indexmap::IndexMap<(usize, usize), f64> = indexmap::IndexMap::new();
66 for i in 0..data.nrows() {
67 let (Some(a), Some(b)) = (id(&from[i]), id(&to[i])) else {
68 continue;
69 };
70 let w = match weight {
71 Some(c) => match c[i].as_f64() {
72 Some(w) if w.is_finite() && w > 0.0 => w,
73 _ => continue,
74 },
75 None => 1.0,
76 };
77 if a != b {
78 *pairs.entry((a, b)).or_insert(0.0) += w;
79 }
80 }
81 let n = names.len();
82 let links: Vec<(usize, usize, f64)> =
83 pairs.into_iter().map(|((a, b), w)| (a, b, w)).collect();
84 if n == 0 || links.is_empty() {
85 return DataFrame::new();
86 }
87 let mut total = vec![0.0; n];
88 for &(a, b, w) in &links {
89 total[a] += w;
90 total[b] += w;
91 }
92 let pad = self.pad_angle.clamp(0.0, TAU / n as f64 * 0.5);
93 let k = (TAU - pad * n as f64) / total.iter().sum::<f64>();
94 let mut start = vec![0.0; n];
95 let mut a = 0.0;
96 for i in 0..n {
97 start[i] = a;
98 a += total[i] * k + pad;
99 }
100 let rel = |from: usize, other: usize| (other + n - from) % n;
103 let mut src = vec![(0.0, 0.0); links.len()];
104 let mut dst = vec![(0.0, 0.0); links.len()];
105 for (node, &node_start) in start.iter().enumerate() {
106 let mut cursor = node_start;
107 let mut out: Vec<usize> = (0..links.len()).filter(|&l| links[l].0 == node).collect();
108 out.sort_by_key(|&l| std::cmp::Reverse(rel(node, links[l].1)));
109 for l in out {
110 src[l] = (cursor, cursor + links[l].2 * k);
111 cursor = src[l].1;
112 }
113 let mut inc: Vec<usize> = (0..links.len()).filter(|&l| links[l].1 == node).collect();
114 inc.sort_by_key(|&l| std::cmp::Reverse(rel(node, links[l].0)));
115 for l in inc {
116 dst[l] = (cursor, cursor + links[l].2 * k);
117 cursor = dst[l].1;
118 }
119 }
120
121 let rows = n + links.len();
122 let mut part = Vec::with_capacity(rows);
123 let (mut a0, mut a1, mut b0, mut b1) = (vec![], vec![], vec![], vec![]);
124 let (mut node, mut f, mut t, mut value) = (vec![], vec![], vec![], vec![]);
125 let s = |x: &str| Value::Str(x.to_string());
126 for i in 0..n {
127 let (lo, hi) = (start[i], start[i] + total[i] * k);
128 part.push(s("node"));
129 a0.push(lo);
130 a1.push(hi);
131 b0.push(lo);
132 b1.push(hi);
133 node.push(s(&names[i]));
134 f.push(s(&names[i]));
135 t.push(s(&names[i]));
136 value.push(total[i]);
137 }
138 for (l, &(x, y, w)) in links.iter().enumerate() {
139 part.push(s("link"));
140 a0.push(src[l].0);
141 a1.push(src[l].1);
142 b0.push(dst[l].0);
143 b1.push(dst[l].1);
144 node.push(s(&names[x]));
145 f.push(s(&names[x]));
146 t.push(s(&names[y]));
147 value.push(w);
148 }
149 let floats = |v: Vec<f64>| v.into_iter().map(Value::Float).collect::<Vec<_>>();
150 let mut outdf = DataFrame::new();
151 outdf.add_column("part".into(), part);
152 outdf.add_column("a0".into(), floats(a0));
153 outdf.add_column("a1".into(), floats(a1));
154 outdf.add_column("b0".into(), floats(b0));
155 outdf.add_column("b1".into(), floats(b1));
156 outdf.add_column("node".into(), node);
157 outdf.add_column("from".into(), f);
158 outdf.add_column("to".into(), t);
159 outdf.add_column("value".into(), floats(value));
160 for (c, v) in [
161 ("xmin", -self.x_extent),
162 ("xmax", self.x_extent),
163 ("ymin", -self.y_extent),
164 ("ymax", self.y_extent),
165 ] {
166 outdf.add_column(c.into(), vec![Value::Float(v); rows]);
167 }
168 outdf
169 }
170
171 fn required_aes(&self) -> Vec<Aesthetic> {
172 vec![Aesthetic::From, Aesthetic::To]
173 }
174
175 fn panelwise(&self) -> bool {
177 true
178 }
179
180 fn name(&self) -> &str {
181 "chord"
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 fn edges(e: &[(&str, &str, f64)]) -> DataFrame {
190 let mut d = DataFrame::new();
191 d.add_column(
192 "from".into(),
193 e.iter().map(|r| Value::Str(r.0.to_string())).collect(),
194 );
195 d.add_column(
196 "to".into(),
197 e.iter().map(|r| Value::Str(r.1.to_string())).collect(),
198 );
199 d.add_column(
200 "weight".into(),
201 e.iter().map(|r| Value::Float(r.2)).collect(),
202 );
203 d
204 }
205
206 fn f(d: &DataFrame, c: &str) -> Vec<f64> {
207 d.column(c)
208 .unwrap()
209 .iter()
210 .map(|v| v.as_f64().unwrap())
211 .collect()
212 }
213
214 #[test]
215 fn arcs_fill_the_circle_and_bands_nest_in_their_nodes() {
216 let d = edges(&[
217 ("A", "B", 10.0),
218 ("B", "A", 5.0),
219 ("A", "C", 3.0),
220 ("C", "B", 2.0),
221 ("A", "A", 9.0),
222 ]);
223 let stat = StatChord::default();
224 let out = stat.compute_group(&d, &ScaleSet::new());
225 assert_eq!(out.nrows(), 3 + 4, "self-loop dropped");
226 let (a0, a1, b0, b1) = (f(&out, "a0"), f(&out, "a1"), f(&out, "b0"), f(&out, "b1"));
227 let arcs: f64 = (0..3).map(|i| a1[i] - a0[i]).sum();
228 assert!((arcs + 3.0 * stat.pad_angle - TAU).abs() < 1e-9);
229 let k = arcs / 40.0;
231 assert!((a1[0] - a0[0] - 18.0 * k).abs() < 1e-9);
232 let name = |c: &str, i: usize| out.column(c).unwrap()[i].key_str().into_owned();
233 let node_of = |n: &str| (0..3).find(|&i| name("node", i) == n).unwrap();
234 for l in 3..7 {
235 let (s, t) = (node_of(&name("from", l)), node_of(&name("to", l)));
236 assert!(a0[l] >= a0[s] - 1e-9 && a1[l] <= a1[s] + 1e-9);
237 assert!(b0[l] >= a0[t] - 1e-9 && b1[l] <= a1[t] + 1e-9);
238 }
239 }
240}