1use crate::aes::Aesthetic;
18use crate::data::{DataFrame, Value};
19use crate::scale::ScaleSet;
20
21use super::Stat;
22
23pub(crate) fn column_totals(x: &[Value], weight: Option<&[Value]>) -> Vec<(String, f64)> {
25 let mut cols: indexmap::IndexMap<String, f64> = indexmap::IndexMap::new();
26 for (i, v) in x.iter().enumerate() {
27 if v.is_na() {
28 continue;
29 }
30 let w = weight.map(|c| c[i].as_f64().unwrap_or(0.0)).unwrap_or(1.0);
31 *cols.entry(v.key_str().into_owned()).or_insert(0.0) += w.max(0.0);
32 }
33 cols.into_iter().collect()
34}
35
36pub(crate) fn column_spans(widths: &[f64], gap: f64) -> Vec<(f64, f64)> {
39 let k = widths.len();
40 let total: f64 = widths.iter().sum();
41 if k == 0 || total <= 0.0 {
42 return vec![(0.0, 0.0); k];
43 }
44 let gap = gap.clamp(0.0, 0.5 / k.max(1) as f64);
45 let avail = 1.0 - gap * (k - 1) as f64;
46 let mut x = 0.0;
47 widths
48 .iter()
49 .map(|w| {
50 let span = (x, x + avail * w / total);
51 x = span.1 + gap;
52 span
53 })
54 .collect()
55}
56
57type Cell = (usize, usize, f64, f64, f64, f64);
60
61#[derive(Clone, Debug, Default)]
63pub struct StatMarimekko {
64 pub mosaic: bool,
66 pub gap: f64,
68}
69
70impl Stat for StatMarimekko {
71 fn compute_group(&self, data: &DataFrame, _scales: &ScaleSet) -> DataFrame {
72 let Some(x) = data.column("x") else {
73 return DataFrame::new();
74 };
75 let weight = data.column("weight");
76 let totals = column_totals(x, weight);
77 let widths: Vec<f64> = totals.iter().map(|c| c.1).collect();
78 let spans = column_spans(&widths, self.gap);
79 let grand: f64 = widths.iter().sum();
80 let w = |i: usize| {
81 weight
82 .map(|c| c[i].as_f64().unwrap_or(0.0))
83 .unwrap_or(1.0)
84 .max(0.0)
85 };
86
87 let mut rows: Vec<Cell> = Vec::new();
88 for (k, (key, total)) in totals.iter().enumerate() {
89 let members: Vec<usize> = (0..data.nrows())
90 .filter(|&i| !x[i].is_na() && x[i].key_str() == key.as_str())
91 .collect();
92 if self.mosaic {
93 let fill = data.column("fill");
94 let mut segs: indexmap::IndexMap<String, (usize, f64)> = indexmap::IndexMap::new();
95 for &i in &members {
96 let f = fill
97 .map(|c| c[i].key_str().into_owned())
98 .unwrap_or_default();
99 segs.entry(f).or_insert((i, 0.0)).1 += w(i);
100 }
101 let mut y = 0.0;
102 for (first, v) in segs.into_values() {
103 let share = if *total > 0.0 { v / total } else { 0.0 };
104 rows.push((first, k, y, y + share, v, share));
105 y += share;
106 }
107 } else if let Some(&first) = members.first() {
108 let h = data
109 .column("y")
110 .and_then(|c| c[first].as_f64())
111 .unwrap_or(f64::NAN);
112 let share = if grand > 0.0 { total / grand } else { 0.0 };
113 rows.push((first, k, 0.0_f64.min(h), 0.0_f64.max(h), *total, share));
114 }
115 }
116
117 let f =
118 |g: &dyn Fn(&Cell) -> f64| rows.iter().map(|r| Value::Float(g(r))).collect::<Vec<_>>();
119 let mut out = DataFrame::new();
120 out.add_column("xmin".into(), f(&|r| spans[r.1].0));
121 out.add_column("xmax".into(), f(&|r| spans[r.1].1));
122 out.add_column("ymin".into(), f(&|r| r.2));
123 out.add_column("ymax".into(), f(&|r| r.3));
124 out.add_column("x".into(), f(&|r| (spans[r.1].0 + spans[r.1].1) / 2.0));
125 out.add_column(
126 "y".into(),
127 f(&|r| if self.mosaic { r.5 } else { r.2 + r.3 }),
129 );
130 out.add_column(
131 "column".into(),
132 rows.iter()
133 .map(|r| Value::Str(totals[r.1].0.clone()))
134 .collect(),
135 );
136 out.add_column("value".into(), f(&|r| r.4));
137 out.add_column("share".into(), f(&|r| r.5));
138 for name in data.column_names() {
139 if out.has_column(name) || name == "weight" {
140 continue;
141 }
142 let col = data.column(name).unwrap_or_default();
143 out.add_column(
144 name.to_string(),
145 rows.iter().map(|r| col[r.0].clone()).collect(),
146 );
147 }
148 out
149 }
150
151 fn required_aes(&self) -> Vec<Aesthetic> {
152 vec![Aesthetic::X]
153 }
154
155 fn panelwise(&self) -> bool {
157 true
158 }
159
160 fn name(&self) -> &str {
161 "marimekko"
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 fn s(v: &[&str]) -> Vec<Value> {
170 v.iter().map(|x| Value::Str(x.to_string())).collect()
171 }
172 fn fl(v: &[f64]) -> Vec<Value> {
173 v.iter().map(|&x| Value::Float(x)).collect()
174 }
175 fn col(d: &DataFrame, c: &str) -> Vec<f64> {
176 d.column(c)
177 .unwrap()
178 .iter()
179 .map(|v| v.as_f64().unwrap())
180 .collect()
181 }
182
183 #[test]
184 fn spans_are_proportional_and_gapped() {
185 assert_eq!(
186 column_spans(&[1.0, 3.0], 0.0),
187 vec![(0.0, 0.25), (0.25, 1.0)]
188 );
189 let g = column_spans(&[1.0, 1.0], 0.1);
190 assert!((g[0].1 - 0.45).abs() < 1e-12 && (g[1].0 - 0.55).abs() < 1e-12);
191 }
192
193 #[test]
194 fn variwide_and_mosaic() {
195 let mut d = DataFrame::new();
196 d.add_column("x".into(), s(&["A", "B"]));
197 d.add_column("y".into(), fl(&[10.0, 4.0]));
198 d.add_column("weight".into(), fl(&[1.0, 3.0]));
199 let out = StatMarimekko::default().compute_group(&d, &ScaleSet::new());
200 assert_eq!(col(&out, "xmax"), vec![0.25, 1.0]);
201 assert_eq!(col(&out, "ymax"), vec![10.0, 4.0]);
202
203 let mut m = DataFrame::new();
204 m.add_column("x".into(), s(&["A", "A", "B", "B", "B"]));
205 m.add_column("fill".into(), s(&["p", "q", "p", "q", "p"]));
206 m.add_column("weight".into(), fl(&[1.0, 3.0, 2.0, 4.0, 2.0]));
207 let out = StatMarimekko {
208 mosaic: true,
209 gap: 0.0,
210 }
211 .compute_group(&m, &ScaleSet::new());
212 assert_eq!(out.nrows(), 4);
214 let (x1, y0, y1) = (col(&out, "xmax"), col(&out, "ymin"), col(&out, "ymax"));
215 assert!((x1[0] - 1.0 / 3.0).abs() < 1e-12);
216 assert_eq!((y0[1], y1[1]), (0.25, 1.0));
217 assert_eq!((y0[3], y1[3]), (0.5, 1.0));
218 }
219}