ggplot_rs/stat/
ydensity.rs1use crate::aes::Aesthetic;
2use crate::data::{DataFrame, Value};
3use crate::scale::ScaleSet;
4
5use super::Stat;
6
7#[derive(Clone, Debug, PartialEq)]
15pub struct StatYDensity {
16 pub n_points: usize,
17 pub trim: bool,
20 pub bw: Option<f64>,
22 pub adjust: f64,
24 pub quantiles: Vec<f64>,
26}
27
28impl Default for StatYDensity {
29 fn default() -> Self {
30 StatYDensity {
31 n_points: 512,
32 trim: true,
33 bw: None,
34 adjust: 1.0,
35 quantiles: Vec::new(),
36 }
37 }
38}
39
40fn quantile7(v: &[f64], p: f64) -> f64 {
42 let h = (v.len() - 1) as f64 * p.clamp(0.0, 1.0);
43 let (lo, hi) = (h.floor() as usize, h.ceil() as usize);
44 v[lo] + (h - lo as f64) * (v[hi] - v[lo])
45}
46
47impl Stat for StatYDensity {
48 fn compute_group(&self, data: &DataFrame, _scales: &ScaleSet) -> DataFrame {
49 let x_col = data.column("x");
50 let y_col = match data.column("y") {
51 Some(c) => c,
52 None => return DataFrame::new(),
53 };
54
55 let values: Vec<f64> = y_col.iter().filter_map(|v| v.as_f64()).collect();
56 if values.len() < 2 {
57 return DataFrame::new();
58 }
59
60 let group_x = x_col
64 .and_then(|c| c.first())
65 .cloned()
66 .unwrap_or(Value::Float(0.0));
67
68 let n = values.len() as f64;
69 let adjust = if self.adjust.is_finite() && self.adjust > 0.0 {
72 self.adjust
73 } else {
74 1.0
75 };
76 let bandwidth = self
77 .bw
78 .filter(|b| b.is_finite() && *b > 0.0)
79 .unwrap_or_else(|| super::bw_nrd0(&values))
80 * adjust;
81
82 let tail = if self.trim { 0.0 } else { 3.0 * bandwidth };
85 let y_min = values.iter().cloned().fold(f64::INFINITY, f64::min) - tail;
86 let y_max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max) + tail;
87 let step = (y_max - y_min) / (self.n_points - 1) as f64;
88
89 let mut x_vals = Vec::with_capacity(self.n_points);
90 let mut y_vals = Vec::with_capacity(self.n_points);
91
92 let mut densities = Vec::with_capacity(self.n_points);
94 let mut max_density: f64 = 0.0;
95 for i in 0..self.n_points {
96 let y = y_min + i as f64 * step;
97 let density: f64 = values
98 .iter()
99 .map(|yi| gaussian_kernel((y - yi) / bandwidth))
100 .sum::<f64>()
101 / (n * bandwidth);
102 densities.push((y, density));
103 if density > max_density {
104 max_density = density;
105 }
106 }
107
108 let scale = if max_density > 0.0 {
111 1.0 / max_density
112 } else {
113 1.0
114 };
115
116 let mut width_vals = Vec::with_capacity(self.n_points);
117 let mut density_vals = Vec::with_capacity(self.n_points);
118 for (y, density) in &densities {
119 x_vals.push(group_x.clone());
120 y_vals.push(Value::Float(*y));
121 width_vals.push(Value::Float(density * scale));
122 density_vals.push(Value::Float(*density));
123 }
124
125 let mut result = DataFrame::new();
126 result.add_column("x".to_string(), x_vals);
127 result.add_column("y".to_string(), y_vals);
128 result.add_column("violinwidth".to_string(), width_vals);
129 result.add_column("density".to_string(), density_vals);
130 result.add_column("n".to_string(), vec![Value::Float(n); self.n_points]);
131 if !self.quantiles.is_empty() {
132 let mut sorted = values.clone();
133 sorted.sort_by(f64::total_cmp);
134 let q = self
135 .quantiles
136 .iter()
137 .map(|&p| quantile7(&sorted, p).to_string())
138 .collect::<Vec<_>>()
139 .join(",");
140 result.add_column("quantiles".to_string(), vec![Value::Str(q); self.n_points]);
141 }
142
143 for col_name in &["color", "fill", "group"] {
145 if let Some(col) = data.column(col_name) {
146 if let Some(first) = col.first() {
147 result.add_column(col_name.to_string(), vec![first.clone(); self.n_points]);
148 }
149 }
150 }
151
152 result
153 }
154
155 fn required_aes(&self) -> Vec<Aesthetic> {
156 vec![Aesthetic::X, Aesthetic::Y]
157 }
158
159 fn name(&self) -> &str {
160 "ydensity"
161 }
162}
163
164fn gaussian_kernel(x: f64) -> f64 {
165 (-(x * x) / 2.0).exp() / (2.0 * std::f64::consts::PI).sqrt()
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn test_ydensity_basic() {
174 let mut data = DataFrame::new();
175 data.add_column("x".to_string(), vec![Value::Float(1.0); 50]);
176 let y_vals: Vec<Value> = (0..50).map(|i| Value::Float(i as f64)).collect();
177 data.add_column("y".to_string(), y_vals);
178
179 let stat = StatYDensity::default();
180 let scales = ScaleSet::new();
181 let result = stat.compute_group(&data, &scales);
182
183 assert!(result.nrows() > 0);
184 assert!(result.column("x").is_some());
185 assert!(result.column("y").is_some());
186 assert!(result.column("violinwidth").is_some());
187 let max_w = result
189 .column("violinwidth")
190 .unwrap()
191 .iter()
192 .filter_map(|v| v.as_f64())
193 .fold(0.0_f64, f64::max);
194 assert!(
195 (max_w - 1.0).abs() < 1e-9,
196 "peak width should be 1.0, got {max_w}"
197 );
198 }
199}