ggplot_rs/stat/
ydensity.rs1use crate::aes::Aesthetic;
2use crate::data::{DataFrame, Value};
3use crate::scale::ScaleSet;
4
5use super::Stat;
6
7pub struct StatYDensity {
11 pub n_points: usize,
12}
13
14impl Default for StatYDensity {
15 fn default() -> Self {
16 StatYDensity { n_points: 512 }
17 }
18}
19
20impl Stat for StatYDensity {
21 fn compute_group(&self, data: &DataFrame, _scales: &ScaleSet) -> DataFrame {
22 let x_col = data.column("x");
23 let y_col = match data.column("y") {
24 Some(c) => c,
25 None => return DataFrame::new(),
26 };
27
28 let values: Vec<f64> = y_col.iter().filter_map(|v| v.as_f64()).collect();
29 if values.len() < 2 {
30 return DataFrame::new();
31 }
32
33 let group_x = x_col
37 .and_then(|c| c.first())
38 .cloned()
39 .unwrap_or(Value::Float(0.0));
40
41 let n = values.len() as f64;
42 let mean = values.iter().sum::<f64>() / n;
43 let var = values.iter().map(|y| (y - mean).powi(2)).sum::<f64>() / (n - 1.0);
44 let sd = var.sqrt();
45
46 let iqr_val = iqr(&values);
48 let bandwidth = 0.9 * sd.min(iqr_val / 1.34) * n.powf(-0.2);
49 let bandwidth = if bandwidth > 0.0 { bandwidth } else { sd * 0.5 };
50
51 let y_min = values.iter().cloned().fold(f64::INFINITY, f64::min);
54 let y_max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
55 let step = (y_max - y_min) / (self.n_points - 1) as f64;
56
57 let mut x_vals = Vec::with_capacity(self.n_points);
58 let mut y_vals = Vec::with_capacity(self.n_points);
59
60 let mut densities = Vec::with_capacity(self.n_points);
62 let mut max_density: f64 = 0.0;
63 for i in 0..self.n_points {
64 let y = y_min + i as f64 * step;
65 let density: f64 = values
66 .iter()
67 .map(|yi| gaussian_kernel((y - yi) / bandwidth))
68 .sum::<f64>()
69 / (n * bandwidth);
70 densities.push((y, density));
71 if density > max_density {
72 max_density = density;
73 }
74 }
75
76 let scale = if max_density > 0.0 {
79 1.0 / max_density
80 } else {
81 1.0
82 };
83
84 let mut width_vals = Vec::with_capacity(self.n_points);
85 for (y, density) in &densities {
86 x_vals.push(group_x.clone());
87 y_vals.push(Value::Float(*y));
88 width_vals.push(Value::Float(density * scale));
89 }
90
91 let mut result = DataFrame::new();
92 result.add_column("x".to_string(), x_vals);
93 result.add_column("y".to_string(), y_vals);
94 result.add_column("violinwidth".to_string(), width_vals);
95
96 for col_name in &["color", "fill", "group"] {
98 if let Some(col) = data.column(col_name) {
99 if let Some(first) = col.first() {
100 result.add_column(col_name.to_string(), vec![first.clone(); self.n_points]);
101 }
102 }
103 }
104
105 result
106 }
107
108 fn required_aes(&self) -> Vec<Aesthetic> {
109 vec![Aesthetic::X, Aesthetic::Y]
110 }
111
112 fn name(&self) -> &str {
113 "ydensity"
114 }
115}
116
117fn gaussian_kernel(x: f64) -> f64 {
118 (-(x * x) / 2.0).exp() / (2.0 * std::f64::consts::PI).sqrt()
119}
120
121fn iqr(values: &[f64]) -> f64 {
122 let mut sorted = values.to_vec();
123 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
124 quantile_type7(&sorted, 0.75) - quantile_type7(&sorted, 0.25)
125}
126
127fn quantile_type7(sorted: &[f64], p: f64) -> f64 {
129 let n = sorted.len();
130 if n == 0 {
131 return 0.0;
132 }
133 if n == 1 {
134 return sorted[0];
135 }
136 let h = (n - 1) as f64 * p;
137 let lo = h.floor() as usize;
138 let hi = (lo + 1).min(n - 1);
139 let frac = h - lo as f64;
140 sorted[lo] + frac * (sorted[hi] - sorted[lo])
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn test_ydensity_basic() {
149 let mut data = DataFrame::new();
150 data.add_column("x".to_string(), vec![Value::Float(1.0); 50]);
151 let y_vals: Vec<Value> = (0..50).map(|i| Value::Float(i as f64)).collect();
152 data.add_column("y".to_string(), y_vals);
153
154 let stat = StatYDensity::default();
155 let scales = ScaleSet::new();
156 let result = stat.compute_group(&data, &scales);
157
158 assert!(result.nrows() > 0);
159 assert!(result.column("x").is_some());
160 assert!(result.column("y").is_some());
161 assert!(result.column("violinwidth").is_some());
162 let max_w = result
164 .column("violinwidth")
165 .unwrap()
166 .iter()
167 .filter_map(|v| v.as_f64())
168 .fold(0.0_f64, f64::max);
169 assert!(
170 (max_w - 1.0).abs() < 1e-9,
171 "peak width should be 1.0, got {max_w}"
172 );
173 }
174}