use crate::aes::Aesthetic;
use crate::data::{DataFrame, Value};
use crate::scale::ScaleSet;
use super::Stat;
pub(crate) fn column_totals(x: &[Value], weight: Option<&[Value]>) -> Vec<(String, f64)> {
let mut cols: indexmap::IndexMap<String, f64> = indexmap::IndexMap::new();
for (i, v) in x.iter().enumerate() {
if v.is_na() {
continue;
}
let w = weight.map(|c| c[i].as_f64().unwrap_or(0.0)).unwrap_or(1.0);
*cols.entry(v.key_str().into_owned()).or_insert(0.0) += w.max(0.0);
}
cols.into_iter().collect()
}
pub(crate) fn column_spans(widths: &[f64], gap: f64) -> Vec<(f64, f64)> {
let k = widths.len();
let total: f64 = widths.iter().sum();
if k == 0 || total <= 0.0 {
return vec![(0.0, 0.0); k];
}
let gap = gap.clamp(0.0, 0.5 / k.max(1) as f64);
let avail = 1.0 - gap * (k - 1) as f64;
let mut x = 0.0;
widths
.iter()
.map(|w| {
let span = (x, x + avail * w / total);
x = span.1 + gap;
span
})
.collect()
}
type Cell = (usize, usize, f64, f64, f64, f64);
#[derive(Clone, Debug, Default)]
pub struct StatMarimekko {
pub mosaic: bool,
pub gap: f64,
}
impl Stat for StatMarimekko {
fn compute_group(&self, data: &DataFrame, _scales: &ScaleSet) -> DataFrame {
let Some(x) = data.column("x") else {
return DataFrame::new();
};
let weight = data.column("weight");
let totals = column_totals(x, weight);
let widths: Vec<f64> = totals.iter().map(|c| c.1).collect();
let spans = column_spans(&widths, self.gap);
let grand: f64 = widths.iter().sum();
let w = |i: usize| {
weight
.map(|c| c[i].as_f64().unwrap_or(0.0))
.unwrap_or(1.0)
.max(0.0)
};
let mut rows: Vec<Cell> = Vec::new();
for (k, (key, total)) in totals.iter().enumerate() {
let members: Vec<usize> = (0..data.nrows())
.filter(|&i| !x[i].is_na() && x[i].key_str() == key.as_str())
.collect();
if self.mosaic {
let fill = data.column("fill");
let mut segs: indexmap::IndexMap<String, (usize, f64)> = indexmap::IndexMap::new();
for &i in &members {
let f = fill
.map(|c| c[i].key_str().into_owned())
.unwrap_or_default();
segs.entry(f).or_insert((i, 0.0)).1 += w(i);
}
let mut y = 0.0;
for (first, v) in segs.into_values() {
let share = if *total > 0.0 { v / total } else { 0.0 };
rows.push((first, k, y, y + share, v, share));
y += share;
}
} else if let Some(&first) = members.first() {
let h = data
.column("y")
.and_then(|c| c[first].as_f64())
.unwrap_or(f64::NAN);
let share = if grand > 0.0 { total / grand } else { 0.0 };
rows.push((first, k, 0.0_f64.min(h), 0.0_f64.max(h), *total, share));
}
}
let f =
|g: &dyn Fn(&Cell) -> f64| rows.iter().map(|r| Value::Float(g(r))).collect::<Vec<_>>();
let mut out = DataFrame::new();
out.add_column("xmin".into(), f(&|r| spans[r.1].0));
out.add_column("xmax".into(), f(&|r| spans[r.1].1));
out.add_column("ymin".into(), f(&|r| r.2));
out.add_column("ymax".into(), f(&|r| r.3));
out.add_column("x".into(), f(&|r| (spans[r.1].0 + spans[r.1].1) / 2.0));
out.add_column(
"y".into(),
f(&|r| if self.mosaic { r.5 } else { r.2 + r.3 }),
);
out.add_column(
"column".into(),
rows.iter()
.map(|r| Value::Str(totals[r.1].0.clone()))
.collect(),
);
out.add_column("value".into(), f(&|r| r.4));
out.add_column("share".into(), f(&|r| r.5));
for name in data.column_names() {
if out.has_column(name) || name == "weight" {
continue;
}
let col = data.column(name).unwrap_or_default();
out.add_column(
name.to_string(),
rows.iter().map(|r| col[r.0].clone()).collect(),
);
}
out
}
fn required_aes(&self) -> Vec<Aesthetic> {
vec![Aesthetic::X]
}
fn panelwise(&self) -> bool {
true
}
fn name(&self) -> &str {
"marimekko"
}
}
#[cfg(test)]
mod tests {
use super::*;
fn s(v: &[&str]) -> Vec<Value> {
v.iter().map(|x| Value::Str(x.to_string())).collect()
}
fn fl(v: &[f64]) -> Vec<Value> {
v.iter().map(|&x| Value::Float(x)).collect()
}
fn col(d: &DataFrame, c: &str) -> Vec<f64> {
d.column(c)
.unwrap()
.iter()
.map(|v| v.as_f64().unwrap())
.collect()
}
#[test]
fn spans_are_proportional_and_gapped() {
assert_eq!(
column_spans(&[1.0, 3.0], 0.0),
vec![(0.0, 0.25), (0.25, 1.0)]
);
let g = column_spans(&[1.0, 1.0], 0.1);
assert!((g[0].1 - 0.45).abs() < 1e-12 && (g[1].0 - 0.55).abs() < 1e-12);
}
#[test]
fn variwide_and_mosaic() {
let mut d = DataFrame::new();
d.add_column("x".into(), s(&["A", "B"]));
d.add_column("y".into(), fl(&[10.0, 4.0]));
d.add_column("weight".into(), fl(&[1.0, 3.0]));
let out = StatMarimekko::default().compute_group(&d, &ScaleSet::new());
assert_eq!(col(&out, "xmax"), vec![0.25, 1.0]);
assert_eq!(col(&out, "ymax"), vec![10.0, 4.0]);
let mut m = DataFrame::new();
m.add_column("x".into(), s(&["A", "A", "B", "B", "B"]));
m.add_column("fill".into(), s(&["p", "q", "p", "q", "p"]));
m.add_column("weight".into(), fl(&[1.0, 3.0, 2.0, 4.0, 2.0]));
let out = StatMarimekko {
mosaic: true,
gap: 0.0,
}
.compute_group(&m, &ScaleSet::new());
assert_eq!(out.nrows(), 4);
let (x1, y0, y1) = (col(&out, "xmax"), col(&out, "ymin"), col(&out, "ymax"));
assert!((x1[0] - 1.0 / 3.0).abs() < 1e-12);
assert_eq!((y0[1], y1[1]), (0.25, 1.0));
assert_eq!((y0[3], y1[3]), (0.5, 1.0));
}
}