1use crate::artist::PolarArtist;
28use crate::primitives::Color;
29use crate::theme::Marker;
30
31impl PolarArtist {
32 pub fn color(&mut self, color: Color) -> &mut Self {
36 self.color = color;
37 self
38 }
39
40 pub fn label(&mut self, label: &str) -> &mut Self {
45 self.label = Some(label.to_string());
46 self
47 }
48
49 pub fn alpha(&mut self, alpha: f64) -> &mut Self {
53 self.alpha = alpha.clamp(0.0, 1.0);
54 self
55 }
56
57 pub fn linewidth(&mut self, width: f64) -> &mut Self {
62 self.linewidth = width;
63 self
64 }
65
66 pub fn filled(&mut self, filled: bool) -> &mut Self {
72 self.filled = filled;
73 self
74 }
75
76 pub fn marker(&mut self, marker: Marker) -> &mut Self {
81 self.marker = Some(marker);
82 self
83 }
84
85 pub fn data_bounds(&self) -> (f64, f64, f64, f64) {
92 if self.r.is_empty() || self.theta.is_empty() {
93 return (-1.0, 1.0, -1.0, 1.0);
94 }
95
96 let r_max = self.max_finite_r();
97 if r_max <= 0.0 || !r_max.is_finite() {
98 return (-1.0, 1.0, -1.0, 1.0);
99 }
100
101 let extent = r_max * 1.1;
104 (-extent, extent, -extent, extent)
105 }
106
107 pub fn max_finite_r(&self) -> f64 {
111 self.r
112 .iter()
113 .copied()
114 .filter(|v| v.is_finite() && *v >= 0.0)
115 .fold(0.0_f64, f64::max)
116 }
117
118 pub fn polar_to_cartesian(r: f64, theta: f64) -> (f64, f64) {
123 (r * theta.cos(), r * theta.sin())
124 }
125
126 pub fn cartesian_points(&self) -> Vec<(f64, f64)> {
130 let n = self.r.len().min(self.theta.len());
131 (0..n)
132 .filter(|&i| self.r[i].is_finite() && self.theta[i].is_finite() && self.r[i] >= 0.0)
133 .map(|i| Self::polar_to_cartesian(self.r[i], self.theta[i]))
134 .collect()
135 }
136}
137
138#[cfg(test)]
143mod tests {
144 use super::*;
145 use std::f64::consts::{FRAC_PI_2, PI, TAU};
146
147 fn sample_polar() -> PolarArtist {
149 PolarArtist {
150 theta: vec![0.0, FRAC_PI_2, PI, 3.0 * FRAC_PI_2],
151 r: vec![1.0, 2.0, 1.5, 0.5],
152 color: Color::TAB_BLUE,
153 label: None,
154 alpha: 1.0,
155 linewidth: 1.5,
156 filled: false,
157 marker: None,
158 }
159 }
160
161 #[test]
164 fn builder_color() {
165 let mut a = sample_polar();
166 a.color(Color::TAB_RED);
167 assert_eq!(a.color, Color::TAB_RED);
168 }
169
170 #[test]
171 fn builder_label() {
172 let mut a = sample_polar();
173 a.label("wind");
174 assert_eq!(a.label, Some("wind".to_string()));
175 }
176
177 #[test]
178 fn builder_alpha() {
179 let mut a = sample_polar();
180 a.alpha(0.5);
181 assert!((a.alpha - 0.5).abs() < f64::EPSILON);
182 }
183
184 #[test]
185 fn builder_alpha_clamped() {
186 let mut a = sample_polar();
187 a.alpha(1.5);
188 assert!((a.alpha - 1.0).abs() < f64::EPSILON);
189 a.alpha(-0.5);
190 assert!(a.alpha.abs() < f64::EPSILON);
191 }
192
193 #[test]
194 fn builder_linewidth() {
195 let mut a = sample_polar();
196 a.linewidth(3.0);
197 assert!((a.linewidth - 3.0).abs() < f64::EPSILON);
198 }
199
200 #[test]
201 fn builder_filled() {
202 let mut a = sample_polar();
203 assert!(!a.filled);
204 a.filled(true);
205 assert!(a.filled);
206 }
207
208 #[test]
209 fn builder_marker() {
210 let mut a = sample_polar();
211 assert!(a.marker.is_none());
212 a.marker(Marker::Circle);
213 assert_eq!(a.marker, Some(Marker::Circle));
214 }
215
216 #[test]
219 fn data_bounds_basic() {
220 let a = sample_polar();
221 let (xmin, xmax, ymin, ymax) = a.data_bounds();
222 assert!((xmin - (-2.2)).abs() < 1e-10);
224 assert!((xmax - 2.2).abs() < 1e-10);
225 assert!((ymin - (-2.2)).abs() < 1e-10);
226 assert!((ymax - 2.2).abs() < 1e-10);
227 }
228
229 #[test]
230 fn data_bounds_empty() {
231 let a = PolarArtist {
232 theta: vec![],
233 r: vec![],
234 color: Color::TAB_BLUE,
235 label: None,
236 alpha: 1.0,
237 linewidth: 1.5,
238 filled: false,
239 marker: None,
240 };
241 assert_eq!(a.data_bounds(), (-1.0, 1.0, -1.0, 1.0));
242 }
243
244 #[test]
245 fn data_bounds_all_nan() {
246 let a = PolarArtist {
247 theta: vec![0.0, 1.0],
248 r: vec![f64::NAN, f64::NAN],
249 color: Color::TAB_BLUE,
250 label: None,
251 alpha: 1.0,
252 linewidth: 1.5,
253 filled: false,
254 marker: None,
255 };
256 assert_eq!(a.data_bounds(), (-1.0, 1.0, -1.0, 1.0));
257 }
258
259 #[test]
262 fn polar_to_cartesian_at_zero() {
263 let (x, y) = PolarArtist::polar_to_cartesian(1.0, 0.0);
264 assert!((x - 1.0).abs() < 1e-10);
265 assert!(y.abs() < 1e-10);
266 }
267
268 #[test]
269 fn polar_to_cartesian_at_90_deg() {
270 let (x, y) = PolarArtist::polar_to_cartesian(2.0, FRAC_PI_2);
271 assert!(x.abs() < 1e-10);
272 assert!((y - 2.0).abs() < 1e-10);
273 }
274
275 #[test]
276 fn polar_to_cartesian_at_pi() {
277 let (x, y) = PolarArtist::polar_to_cartesian(1.0, PI);
278 assert!((x - (-1.0)).abs() < 1e-10);
279 assert!(y.abs() < 1e-10);
280 }
281
282 #[test]
283 fn polar_to_cartesian_at_270_deg() {
284 let (x, y) = PolarArtist::polar_to_cartesian(3.0, 3.0 * FRAC_PI_2);
285 assert!(x.abs() < 1e-10);
286 assert!((y - (-3.0)).abs() < 1e-10);
287 }
288
289 #[test]
292 fn cartesian_points_basic() {
293 let a = PolarArtist {
294 theta: vec![0.0, FRAC_PI_2],
295 r: vec![1.0, 2.0],
296 color: Color::TAB_BLUE,
297 label: None,
298 alpha: 1.0,
299 linewidth: 1.5,
300 filled: false,
301 marker: None,
302 };
303 let pts = a.cartesian_points();
304 assert_eq!(pts.len(), 2);
305 assert!((pts[0].0 - 1.0).abs() < 1e-10);
306 assert!(pts[0].1.abs() < 1e-10);
307 assert!(pts[1].0.abs() < 1e-10);
308 assert!((pts[1].1 - 2.0).abs() < 1e-10);
309 }
310
311 #[test]
312 fn cartesian_points_nan_filtered() {
313 let a = PolarArtist {
314 theta: vec![0.0, f64::NAN, PI],
315 r: vec![1.0, 2.0, f64::NAN],
316 color: Color::TAB_BLUE,
317 label: None,
318 alpha: 1.0,
319 linewidth: 1.5,
320 filled: false,
321 marker: None,
322 };
323 let pts = a.cartesian_points();
324 assert_eq!(pts.len(), 1);
326 assert!((pts[0].0 - 1.0).abs() < 1e-10);
327 }
328
329 #[test]
332 fn negative_angles() {
333 let (x, y) = PolarArtist::polar_to_cartesian(1.0, -FRAC_PI_2);
334 assert!(x.abs() < 1e-10);
335 assert!((y - (-1.0)).abs() < 1e-10);
336 }
337
338 #[test]
339 fn angles_greater_than_two_pi() {
340 let (x1, y1) = PolarArtist::polar_to_cartesian(1.0, TAU + FRAC_PI_2);
342 let (x2, y2) = PolarArtist::polar_to_cartesian(1.0, FRAC_PI_2);
343 assert!((x1 - x2).abs() < 1e-10);
344 assert!((y1 - y2).abs() < 1e-10);
345 }
346
347 #[test]
348 fn max_finite_r_ignores_nan_and_negative() {
349 let a = PolarArtist {
350 theta: vec![0.0, 1.0, 2.0, 3.0],
351 r: vec![f64::NAN, -1.0, 5.0, 3.0],
352 color: Color::TAB_BLUE,
353 label: None,
354 alpha: 1.0,
355 linewidth: 1.5,
356 filled: false,
357 marker: None,
358 };
359 assert!((a.max_finite_r() - 5.0).abs() < 1e-10);
360 }
361
362 #[test]
363 fn data_bounds_with_single_point() {
364 let a = PolarArtist {
365 theta: vec![0.0],
366 r: vec![3.0],
367 color: Color::TAB_BLUE,
368 label: None,
369 alpha: 1.0,
370 linewidth: 1.5,
371 filled: false,
372 marker: None,
373 };
374 let (xmin, xmax, ymin, ymax) = a.data_bounds();
375 assert!((xmin - (-3.3)).abs() < 1e-10);
377 assert!((xmax - 3.3).abs() < 1e-10);
378 assert!((ymin - (-3.3)).abs() < 1e-10);
379 assert!((ymax - 3.3).abs() < 1e-10);
380 }
381
382 #[test]
383 fn builder_chaining_returns_self() {
384 let mut a = sample_polar();
385 let _ = a
386 .color(Color::TAB_GREEN)
387 .label("test")
388 .alpha(0.7)
389 .linewidth(2.5)
390 .filled(true)
391 .marker(Marker::Diamond);
392 assert_eq!(a.color, Color::TAB_GREEN);
393 assert_eq!(a.label, Some("test".to_string()));
394 assert!((a.alpha - 0.7).abs() < f64::EPSILON);
395 assert!((a.linewidth - 2.5).abs() < f64::EPSILON);
396 assert!(a.filled);
397 assert_eq!(a.marker, Some(Marker::Diamond));
398 }
399}