1use crate::aes::Aesthetic;
11use crate::coord::Coord;
12use crate::data::{DataFrame, Value};
13use crate::position::identity::PositionIdentity;
14use crate::position::Position;
15use crate::render::backend::{DrawBackend, LineStyle, Linetype, TextAnchor, TextStyle};
16use crate::render::RenderError;
17use crate::scale::ScaleSet;
18use crate::stat::identity::StatIdentity;
19use crate::stat::Stat;
20use crate::theme::Theme;
21
22use super::support::clip_polyline_unit;
23use super::{Geom, GeomParams};
24
25pub fn cooks_contour_y(level: f64, p: f64, h: f64) -> f64 {
29 if !(h > 0.0 && h <= 1.0 && level > 0.0 && p > 0.0) {
30 return f64::NAN;
31 }
32 (level * p * (1.0 - h) / h).sqrt()
33}
34
35pub fn cooks_distance(r: f64, h: f64, p: f64) -> f64 {
38 r * r * h / (p * (1.0 - h))
39}
40
41pub struct GeomCooksContour {
43 pub p: f64,
45 pub levels: Vec<f64>,
47 pub h_range: Option<(f64, f64)>,
49 pub color: (u8, u8, u8),
50 pub width: f64,
51 pub linetype: Linetype,
52 pub alpha: f64,
53 pub label: bool,
55 pub label_size: f64,
56}
57
58impl GeomCooksContour {
59 pub fn new(p: usize, levels: &[f64]) -> Self {
60 GeomCooksContour {
61 p: p as f64,
62 levels: levels.to_vec(),
63 h_range: None,
64 color: (205, 0, 0),
65 width: 0.8,
66 linetype: Linetype::Dashed,
67 alpha: 1.0,
68 label: true,
69 label_size: 9.0,
70 }
71 }
72
73 pub fn with_h_range(mut self, lo: f64, hi: f64) -> Self {
75 self.h_range = Some((lo, hi));
76 self
77 }
78}
79
80impl Default for GeomCooksContour {
81 fn default() -> Self {
82 GeomCooksContour::new(2, &[0.5, 1.0])
83 }
84}
85
86impl Geom for GeomCooksContour {
87 fn draw(
88 &self,
89 _data: &DataFrame,
90 coord: &dyn Coord,
91 scales: &ScaleSet,
92 _theme: &Theme,
93 backend: &mut dyn DrawBackend,
94 ) -> Result<(), RenderError> {
95 let (Some(xs), Some(ys)) = (scales.get(&Aesthetic::X), scales.get(&Aesthetic::Y)) else {
96 return Ok(());
97 };
98 if xs.is_discrete() || ys.is_discrete() {
99 return Ok(());
100 }
101 let Some((x0, x1)) = xs.expanded_domain() else {
102 return Ok(());
103 };
104 let (lo, hi) = self.h_range.unwrap_or((x0, x1));
105 let lo = lo.max(x0).max(1e-9);
106 let hi = hi.min(x1).min(1.0);
107 if !(lo.is_finite() && hi.is_finite() && hi > lo) {
108 return Ok(());
109 }
110 let plot_area = backend.plot_area();
111 const N: usize = 240;
113 let hs: Vec<f64> = (0..=N)
114 .map(|k| lo * (hi / lo).powf(k as f64 / N as f64))
115 .collect();
116 let style = LineStyle {
117 color: self.color,
118 alpha: self.alpha,
119 width: self.width,
120 linetype: self.linetype,
121 };
122 for &level in self.levels.iter().filter(|l| l.is_finite() && **l > 0.0) {
123 let label = super::tip_value(&Value::Float(level));
124 super::set_mark(
125 backend,
126 Some(format!("Cook's distance = {label}")),
127 None,
128 None,
129 super::raw_value(&Value::Float(level)),
130 );
131 for sign in [1.0, -1.0] {
132 let pts: Vec<(f64, f64)> = hs
133 .iter()
134 .map(|&h| {
135 let r = sign * cooks_contour_y(level, self.p, h);
136 (xs.map(&Value::Float(h)), ys.map(&Value::Float(r)))
137 })
138 .collect();
139 let runs = clip_polyline_unit(&pts);
140 for run in &runs {
141 let px: Vec<(f64, f64)> = run
142 .iter()
143 .map(|&q| coord.transform(q, &plot_area))
144 .collect();
145 backend.draw_line(&px, &style)?;
146 }
147 if self.label {
149 if let Some(&end) = runs
150 .iter()
151 .filter_map(|r| r.last())
152 .max_by(|a, b| a.0.total_cmp(&b.0))
153 {
154 let (px, py) = coord.transform(end, &plot_area);
155 let dy = if sign > 0.0 { -6.0 } else { 6.0 };
156 backend.draw_text(
157 &label,
158 (px - 2.0, py + dy),
159 &TextStyle {
160 color: self.color,
161 size: self.label_size,
162 anchor: TextAnchor::End,
163 ..TextStyle::default()
164 },
165 )?;
166 }
167 }
168 }
169 }
170 super::clear_mark(backend);
171 Ok(())
172 }
173
174 fn required_aes(&self) -> Vec<Aesthetic> {
175 vec![]
176 }
177 fn default_stat(&self) -> Box<dyn Stat> {
178 Box::new(StatIdentity)
179 }
180 fn default_position(&self) -> Box<dyn Position> {
181 Box::new(PositionIdentity)
182 }
183 fn default_params(&self) -> GeomParams {
184 GeomParams::default()
185 }
186 fn name(&self) -> &str {
187 "cooks_contour"
188 }
189 fn inherit_aes(&self) -> bool {
190 false
191 }
192 fn set_series_color(&mut self, _color: (u8, u8, u8)) {}
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn contour_inverts_cooks_distance() {
201 for &(level, p, h) in &[(0.5, 3.0, 0.1), (1.0, 2.0, 0.4), (4.0, 5.0, 0.9)] {
202 let r = cooks_contour_y(level, p, h);
203 assert!((cooks_distance(r, h, p) - level).abs() < 1e-12);
204 }
205 assert!(cooks_contour_y(0.5, 3.0, 0.0).is_nan());
206 assert!(cooks_contour_y(0.5, 3.0, 1.5).is_nan());
207 assert_eq!(cooks_contour_y(0.5, 3.0, 1.0), 0.0);
208 }
209}