1use crate::theme::Gradient;
6use presentar_core::{
7 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
8 LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
9};
10use std::any::Any;
11use std::time::Duration;
12
13#[derive(Debug, Clone, Copy, Default)]
15pub enum MarkerStyle {
16 #[default]
18 Dot,
19 Cross,
21 Circle,
23 Square,
25 Diamond,
27 Triangle,
29 Star,
31}
32
33impl MarkerStyle {
34 #[must_use]
36 pub const fn char(self) -> char {
37 match self {
38 Self::Dot => '•',
39 Self::Cross => '+',
40 Self::Circle => '○',
41 Self::Square => '□',
42 Self::Diamond => '◇',
43 Self::Triangle => '△',
44 Self::Star => '★',
45 }
46 }
47}
48
49#[derive(Debug, Clone)]
51pub struct ScatterAxis {
52 pub label: Option<String>,
54 pub min: Option<f64>,
56 pub max: Option<f64>,
58 pub ticks: usize,
60}
61
62impl Default for ScatterAxis {
63 fn default() -> Self {
64 Self {
65 label: None,
66 min: None,
67 max: None,
68 ticks: 5,
69 }
70 }
71}
72
73#[derive(Debug, Clone)]
75pub struct ScatterPlot {
76 points: Vec<(f64, f64)>,
77 marker: MarkerStyle,
78 color: Color,
79 color_by: Option<Vec<f64>>,
81 gradient: Option<Gradient>,
82 x_axis: ScatterAxis,
83 y_axis: ScatterAxis,
84 show_axes: bool,
85 bounds: Rect,
86}
87
88impl ScatterPlot {
89 #[must_use]
91 pub fn new(points: Vec<(f64, f64)>) -> Self {
92 Self {
93 points,
94 marker: MarkerStyle::default(),
95 color: Color::new(0.3, 0.7, 1.0, 1.0),
96 color_by: None,
97 gradient: None,
98 x_axis: ScatterAxis::default(),
99 y_axis: ScatterAxis::default(),
100 show_axes: true,
101 bounds: Rect::default(),
102 }
103 }
104
105 #[must_use]
107 pub fn with_marker(mut self, marker: MarkerStyle) -> Self {
108 self.marker = marker;
109 self
110 }
111
112 #[must_use]
114 pub fn with_color(mut self, color: Color) -> Self {
115 self.color = color;
116 self
117 }
118
119 #[must_use]
121 pub fn with_color_by(mut self, values: Vec<f64>, gradient: Gradient) -> Self {
122 self.color_by = Some(values);
123 self.gradient = Some(gradient);
124 self
125 }
126
127 #[must_use]
129 pub fn with_x_axis(mut self, axis: ScatterAxis) -> Self {
130 self.x_axis = axis;
131 self
132 }
133
134 #[must_use]
136 pub fn with_y_axis(mut self, axis: ScatterAxis) -> Self {
137 self.y_axis = axis;
138 self
139 }
140
141 #[must_use]
143 pub fn with_axes(mut self, show: bool) -> Self {
144 self.show_axes = show;
145 self
146 }
147
148 pub fn set_points(&mut self, points: Vec<(f64, f64)>) {
150 self.points = points;
151 }
152
153 fn x_range(&self) -> (f64, f64) {
155 if let (Some(min), Some(max)) = (self.x_axis.min, self.x_axis.max) {
156 return (min, max);
157 }
158
159 let mut x_min = f64::INFINITY;
160 let mut x_max = f64::NEG_INFINITY;
161
162 for &(x, _) in &self.points {
163 if x.is_finite() {
164 x_min = x_min.min(x);
165 x_max = x_max.max(x);
166 }
167 }
168
169 if x_min == f64::INFINITY {
170 (0.0, 1.0)
171 } else {
172 let padding = (x_max - x_min) * 0.05;
173 (
174 self.x_axis.min.unwrap_or(x_min - padding),
175 self.x_axis.max.unwrap_or(x_max + padding),
176 )
177 }
178 }
179
180 fn y_range(&self) -> (f64, f64) {
182 if let (Some(min), Some(max)) = (self.y_axis.min, self.y_axis.max) {
183 return (min, max);
184 }
185
186 let mut y_min = f64::INFINITY;
187 let mut y_max = f64::NEG_INFINITY;
188
189 for &(_, y) in &self.points {
190 if y.is_finite() {
191 y_min = y_min.min(y);
192 y_max = y_max.max(y);
193 }
194 }
195
196 if y_min == f64::INFINITY {
197 (0.0, 1.0)
198 } else {
199 let padding = (y_max - y_min) * 0.05;
200 (
201 self.y_axis.min.unwrap_or(y_min - padding),
202 self.y_axis.max.unwrap_or(y_max + padding),
203 )
204 }
205 }
206
207 fn color_range(&self) -> (f64, f64) {
209 if let Some(ref values) = self.color_by {
210 let mut c_min = f64::INFINITY;
211 let mut c_max = f64::NEG_INFINITY;
212
213 for &v in values {
214 if v.is_finite() {
215 c_min = c_min.min(v);
216 c_max = c_max.max(v);
217 }
218 }
219
220 if c_min == f64::INFINITY {
221 (0.0, 1.0)
222 } else {
223 (c_min, c_max)
224 }
225 } else {
226 (0.0, 1.0)
227 }
228 }
229
230 fn draw_y_axis(
232 &self,
233 canvas: &mut dyn Canvas,
234 y_min: f64,
235 y_max: f64,
236 plot_y: f32,
237 plot_height: f32,
238 label_style: &TextStyle,
239 ) {
240 for i in 0..=self.y_axis.ticks {
241 let t = i as f64 / self.y_axis.ticks as f64;
242 let y_val = y_min + (y_max - y_min) * (1.0 - t);
243 let y_pos = plot_y + plot_height * t as f32;
244
245 if y_pos >= plot_y && y_pos < plot_y + plot_height {
246 let label = format!("{y_val:>5.0}");
247 canvas.draw_text(&label, Point::new(self.bounds.x, y_pos), label_style);
248 }
249 }
250 }
251
252 #[allow(clippy::too_many_arguments)]
254 fn draw_x_axis(
255 &self,
256 canvas: &mut dyn Canvas,
257 x_min: f64,
258 x_max: f64,
259 plot_x: f32,
260 plot_y: f32,
261 plot_width: f32,
262 plot_height: f32,
263 label_style: &TextStyle,
264 ) {
265 for i in 0..=self.x_axis.ticks.min(plot_width as usize / 8) {
266 let t = i as f64 / self.x_axis.ticks as f64;
267 let x_val = x_min + (x_max - x_min) * t;
268 let x_pos = plot_x + plot_width * t as f32;
269
270 if x_pos >= plot_x && x_pos < plot_x + plot_width - 4.0 {
271 let label = format!("{x_val:.0}");
272 canvas.draw_text(&label, Point::new(x_pos, plot_y + plot_height), label_style);
273 }
274 }
275 }
276
277 fn point_color(&self, i: usize, c_min: f64, c_max: f64) -> Color {
279 if let (Some(ref values), Some(ref gradient)) = (&self.color_by, &self.gradient) {
280 if i < values.len() {
281 let c_norm = if c_max > c_min {
282 (values[i] - c_min) / (c_max - c_min)
283 } else {
284 0.5
285 };
286 return gradient.sample(c_norm);
287 }
288 }
289 self.color
290 }
291}
292
293impl Default for ScatterPlot {
294 fn default() -> Self {
295 Self::new(Vec::new())
296 }
297}
298
299impl Widget for ScatterPlot {
300 fn type_id(&self) -> TypeId {
301 TypeId::of::<Self>()
302 }
303
304 fn measure(&self, constraints: Constraints) -> Size {
305 Size::new(
306 constraints.max_width.min(60.0),
307 constraints.max_height.min(20.0),
308 )
309 }
310
311 fn layout(&mut self, bounds: Rect) -> LayoutResult {
312 self.bounds = bounds;
313 LayoutResult {
314 size: Size::new(bounds.width, bounds.height),
315 }
316 }
317
318 #[allow(clippy::too_many_lines)]
319 fn paint(&self, canvas: &mut dyn Canvas) {
320 if self.bounds.width < 10.0 || self.bounds.height < 5.0 {
321 return;
322 }
323
324 let (x_min, x_max) = self.x_range();
325 let (y_min, y_max) = self.y_range();
326 let (c_min, c_max) = self.color_range();
327
328 let margin_left = if self.show_axes { 6.0 } else { 0.0 };
330 let margin_bottom = if self.show_axes { 2.0 } else { 0.0 };
331
332 let plot_x = self.bounds.x + margin_left;
333 let plot_y = self.bounds.y;
334 let plot_width = self.bounds.width - margin_left;
335 let plot_height = self.bounds.height - margin_bottom;
336
337 if plot_width <= 0.0 || plot_height <= 0.0 {
338 return;
339 }
340
341 let label_style = TextStyle {
342 color: Color::new(0.6, 0.6, 0.6, 1.0),
343 ..Default::default()
344 };
345
346 if self.show_axes {
348 self.draw_y_axis(canvas, y_min, y_max, plot_y, plot_height, &label_style);
349 self.draw_x_axis(
350 canvas,
351 x_min,
352 x_max,
353 plot_x,
354 plot_y,
355 plot_width,
356 plot_height,
357 &label_style,
358 );
359 }
360
361 let marker_char = self.marker.char();
363
364 for (i, &(x, y)) in self.points.iter().enumerate() {
365 if !x.is_finite() || !y.is_finite() {
366 continue;
367 }
368
369 let x_norm = if x_max > x_min {
371 (x - x_min) / (x_max - x_min)
372 } else {
373 0.5
374 };
375 let y_norm = if y_max > y_min {
376 (y - y_min) / (y_max - y_min)
377 } else {
378 0.5
379 };
380
381 let screen_x = plot_x + (x_norm * plot_width as f64) as f32;
383 let screen_y = plot_y + ((1.0 - y_norm) * plot_height as f64) as f32;
384
385 if screen_x < plot_x
387 || screen_x >= plot_x + plot_width
388 || screen_y < plot_y
389 || screen_y >= plot_y + plot_height
390 {
391 continue;
392 }
393
394 let style = TextStyle {
395 color: self.point_color(i, c_min, c_max),
396 ..Default::default()
397 };
398
399 canvas.draw_text(
400 &marker_char.to_string(),
401 Point::new(screen_x, screen_y),
402 &style,
403 );
404 }
405
406 if self.show_axes {
408 if let Some(ref label) = self.x_axis.label {
409 let x = plot_x + plot_width / 2.0 - label.len() as f32 / 2.0;
410 canvas.draw_text(
411 label,
412 Point::new(x, self.bounds.y + self.bounds.height - 1.0),
413 &label_style,
414 );
415 }
416
417 if let Some(ref label) = self.y_axis.label {
418 canvas.draw_text(
420 &label.chars().next().unwrap_or(' ').to_string(),
421 Point::new(self.bounds.x, plot_y + plot_height / 2.0),
422 &label_style,
423 );
424 }
425 }
426 }
427
428 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
429 None
430 }
431
432 fn children(&self) -> &[Box<dyn Widget>] {
433 &[]
434 }
435
436 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
437 &mut []
438 }
439}
440
441impl Brick for ScatterPlot {
442 fn brick_name(&self) -> &'static str {
443 "ScatterPlot"
444 }
445
446 fn assertions(&self) -> &[BrickAssertion] {
447 static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
448 ASSERTIONS
449 }
450
451 fn budget(&self) -> BrickBudget {
452 BrickBudget::uniform(16)
453 }
454
455 fn verify(&self) -> BrickVerification {
456 let mut passed = Vec::new();
457 let mut failed = Vec::new();
458
459 if self.bounds.width >= 10.0 && self.bounds.height >= 5.0 {
460 passed.push(BrickAssertion::max_latency_ms(16));
461 } else {
462 failed.push((
463 BrickAssertion::max_latency_ms(16),
464 "Size too small".to_string(),
465 ));
466 }
467
468 BrickVerification {
469 passed,
470 failed,
471 verification_time: Duration::from_micros(5),
472 }
473 }
474
475 fn to_html(&self) -> String {
476 String::new()
477 }
478
479 fn to_css(&self) -> String {
480 String::new()
481 }
482}
483
484#[cfg(test)]
485mod tests {
486 use super::*;
487 use crate::direct::{CellBuffer, DirectTerminalCanvas};
488
489 #[test]
490 fn test_scatter_creation() {
491 let points = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 4.0)];
492 let scatter = ScatterPlot::new(points);
493 assert_eq!(scatter.points.len(), 3);
494 }
495
496 #[test]
497 fn test_marker_chars() {
498 assert_eq!(MarkerStyle::Dot.char(), '•');
499 assert_eq!(MarkerStyle::Cross.char(), '+');
500 assert_eq!(MarkerStyle::Circle.char(), '○');
501 assert_eq!(MarkerStyle::Square.char(), '□');
502 assert_eq!(MarkerStyle::Diamond.char(), '◇');
503 }
504
505 #[test]
506 fn test_empty_scatter() {
507 let scatter = ScatterPlot::new(vec![]);
508 let (x_min, x_max) = scatter.x_range();
509 assert_eq!(x_min, 0.0);
510 assert_eq!(x_max, 1.0);
511 }
512
513 #[test]
514 fn test_auto_range() {
515 let points = vec![(10.0, 20.0), (30.0, 40.0)];
516 let scatter = ScatterPlot::new(points);
517 let (x_min, x_max) = scatter.x_range();
518 assert!(x_min < 10.0); assert!(x_max > 30.0);
520 }
521
522 #[test]
523 fn test_scatter_assertions() {
524 let scatter = ScatterPlot::default();
525 assert!(!scatter.assertions().is_empty());
526 }
527
528 #[test]
529 fn test_scatter_verify() {
530 let mut scatter = ScatterPlot::default();
531 scatter.bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
532 assert!(scatter.verify().is_valid());
533 }
534
535 #[test]
536 fn test_scatter_verify_small_bounds() {
537 let mut scatter = ScatterPlot::default();
538 scatter.bounds = Rect::new(0.0, 0.0, 5.0, 3.0);
539 let result = scatter.verify();
540 assert!(!result.is_valid());
541 }
542
543 #[test]
544 fn test_scatter_children() {
545 let scatter = ScatterPlot::default();
546 assert!(scatter.children().is_empty());
547 }
548
549 #[test]
550 fn test_scatter_layout() {
551 let mut scatter = ScatterPlot::new(vec![(0.0, 0.0), (1.0, 1.0), (2.0, 4.0)]);
552 let bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
553 let result = scatter.layout(bounds);
554 assert!(result.size.width > 0.0);
555 assert!(result.size.height > 0.0);
556 }
557
558 #[test]
559 fn test_scatter_paint() {
560 let mut scatter = ScatterPlot::new(vec![(0.0, 0.0), (1.0, 1.0), (2.0, 4.0)]);
561 let bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
562 scatter.layout(bounds);
563
564 let mut buffer = CellBuffer::new(60, 20);
565 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
566 scatter.paint(&mut canvas);
567 }
568
569 #[test]
570 fn test_scatter_with_all_markers() {
571 for marker in [
572 MarkerStyle::Dot,
573 MarkerStyle::Cross,
574 MarkerStyle::Circle,
575 MarkerStyle::Square,
576 MarkerStyle::Diamond,
577 ] {
578 let mut scatter = ScatterPlot::new(vec![(0.0, 0.0), (1.0, 1.0)]).with_marker(marker);
579 let bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
580 scatter.layout(bounds);
581 let mut buffer = CellBuffer::new(60, 20);
582 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
583 scatter.paint(&mut canvas);
584 }
585 }
586
587 #[test]
588 fn test_scatter_with_color() {
589 let mut scatter = ScatterPlot::new(vec![(0.0, 0.0), (1.0, 1.0)]).with_color(Color::RED);
590 let bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
591 scatter.layout(bounds);
592 let mut buffer = CellBuffer::new(60, 20);
593 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
594 scatter.paint(&mut canvas);
595 }
596
597 #[test]
598 fn test_scatter_with_color_gradient() {
599 let mut scatter = ScatterPlot::new(vec![(0.0, 0.0), (1.0, 1.0), (2.0, 4.0)])
600 .with_color_by(vec![0.0, 0.5, 1.0], Gradient::two(Color::BLUE, Color::RED));
601 let bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
602 scatter.layout(bounds);
603 let mut buffer = CellBuffer::new(60, 20);
604 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
605 scatter.paint(&mut canvas);
606 }
607
608 #[test]
609 fn test_scatter_with_axes() {
610 let mut scatter = ScatterPlot::new(vec![(0.0, 0.0), (10.0, 10.0)])
611 .with_axes(true)
612 .with_x_axis(ScatterAxis {
613 label: Some("X Axis".to_string()),
614 min: Some(0.0),
615 max: Some(10.0),
616 ticks: 5,
617 })
618 .with_y_axis(ScatterAxis {
619 label: Some("Y Axis".to_string()),
620 min: Some(0.0),
621 max: Some(10.0),
622 ticks: 5,
623 });
624 let bounds = Rect::new(0.0, 0.0, 80.0, 24.0);
625 scatter.layout(bounds);
626 let mut buffer = CellBuffer::new(80, 24);
627 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
628 scatter.paint(&mut canvas);
629 }
630
631 #[test]
632 fn test_scatter_y_range() {
633 let scatter = ScatterPlot::new(vec![(0.0, -5.0), (1.0, 10.0), (2.0, 3.0)]);
634 let (y_min, y_max) = scatter.y_range();
635 assert!(y_min <= -5.0);
636 assert!(y_max >= 10.0);
637 }
638
639 #[test]
640 fn test_scatter_y_range_empty() {
641 let scatter = ScatterPlot::new(vec![]);
642 let (y_min, y_max) = scatter.y_range();
643 assert_eq!(y_min, 0.0);
644 assert_eq!(y_max, 1.0);
645 }
646
647 #[test]
648 fn test_scatter_with_many_points() {
649 let points: Vec<(f64, f64)> = (0..100)
650 .map(|i| (i as f64, (i as f64 * 0.1).sin() * 10.0))
651 .collect();
652 let mut scatter = ScatterPlot::new(points);
653 let bounds = Rect::new(0.0, 0.0, 80.0, 24.0);
654 scatter.layout(bounds);
655 let mut buffer = CellBuffer::new(80, 24);
656 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
657 scatter.paint(&mut canvas);
658 }
659
660 #[test]
661 fn test_scatter_axis_default() {
662 let axis = ScatterAxis::default();
663 assert!(axis.label.is_none());
664 assert!(axis.min.is_none());
665 assert!(axis.max.is_none());
666 }
667
668 #[test]
669 fn test_marker_style_default() {
670 let marker = MarkerStyle::default();
671 assert!(matches!(marker, MarkerStyle::Dot));
672 }
673
674 #[test]
675 fn test_gradient_interpolate() {
676 let gradient = Gradient::two(Color::BLACK, Color::WHITE);
677 let mid = gradient.sample(0.5);
678 assert!(mid.r > 0.0);
681 assert!(mid.g > 0.0);
682 assert!(mid.b > 0.0);
683 }
684
685 #[test]
686 fn test_scatter_brick_name() {
687 let scatter = ScatterPlot::default();
688 assert_eq!(scatter.brick_name(), "ScatterPlot");
689 }
690
691 #[test]
692 fn test_scatter_budget() {
693 let scatter = ScatterPlot::default();
694 let budget = scatter.budget();
695 assert!(budget.layout_ms > 0);
696 }
697
698 #[test]
699 fn test_scatter_to_html_css() {
700 let scatter = ScatterPlot::default();
701 assert!(scatter.to_html().is_empty());
702 assert!(scatter.to_css().is_empty());
703 }
704
705 #[test]
706 fn test_marker_style_triangle() {
707 let marker = MarkerStyle::Triangle;
708 assert_eq!(marker.char(), '△');
709 }
710
711 #[test]
712 fn test_marker_style_star() {
713 let marker = MarkerStyle::Star;
714 assert_eq!(marker.char(), '★');
715 }
716
717 #[test]
718 fn test_scatter_plot_with_axis_labels() {
719 let scatter = ScatterPlot::new(vec![(1.0, 2.0), (3.0, 4.0)])
720 .with_x_axis(ScatterAxis {
721 label: Some("X-Axis".to_string()),
722 ..Default::default()
723 })
724 .with_y_axis(ScatterAxis {
725 label: Some("Y-Axis".to_string()),
726 ..Default::default()
727 });
728 assert!(scatter.x_axis.label.is_some());
729 assert!(scatter.y_axis.label.is_some());
730 }
731
732 #[test]
733 fn test_scatter_plot_with_diamond_marker() {
734 let scatter =
735 ScatterPlot::new(vec![(1.0, 2.0), (3.0, 4.0)]).with_marker(MarkerStyle::Diamond);
736 assert!(matches!(scatter.marker, MarkerStyle::Diamond));
737 }
738
739 #[test]
740 fn test_scatter_set_points() {
741 let mut scatter = ScatterPlot::new(vec![(0.0, 0.0)]);
742 assert_eq!(scatter.points.len(), 1);
743 scatter.set_points(vec![(1.0, 1.0), (2.0, 2.0), (3.0, 3.0)]);
744 assert_eq!(scatter.points.len(), 3);
745 }
746
747 #[test]
748 fn test_scatter_with_axes_false() {
749 let mut scatter = ScatterPlot::new(vec![(0.0, 0.0), (10.0, 10.0)]).with_axes(false);
750 assert!(!scatter.show_axes);
751 let bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
752 scatter.layout(bounds);
753 let mut buffer = CellBuffer::new(60, 20);
754 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
755 scatter.paint(&mut canvas);
756 }
757
758 #[test]
759 fn test_scatter_nan_values() {
760 let mut scatter = ScatterPlot::new(vec![(0.0, 0.0), (f64::NAN, f64::NAN), (2.0, 2.0)]);
761 let bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
762 scatter.layout(bounds);
763 let mut buffer = CellBuffer::new(60, 20);
764 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
765 scatter.paint(&mut canvas); }
767
768 #[test]
769 fn test_scatter_infinite_values() {
770 let mut scatter = ScatterPlot::new(vec![
771 (0.0, 0.0),
772 (f64::INFINITY, f64::NEG_INFINITY),
773 (2.0, 2.0),
774 ]);
775 let bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
776 scatter.layout(bounds);
777 let mut buffer = CellBuffer::new(60, 20);
778 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
779 scatter.paint(&mut canvas); }
781
782 #[test]
783 fn test_scatter_color_range_no_color_by() {
784 let scatter = ScatterPlot::new(vec![(0.0, 0.0), (1.0, 1.0)]);
785 let (c_min, c_max) = scatter.color_range();
786 assert_eq!(c_min, 0.0);
787 assert_eq!(c_max, 1.0);
788 }
789
790 #[test]
791 fn test_scatter_color_range_with_values() {
792 let scatter = ScatterPlot::new(vec![(0.0, 0.0), (1.0, 1.0), (2.0, 2.0)]).with_color_by(
793 vec![5.0, 10.0, 15.0],
794 Gradient::two(Color::BLUE, Color::RED),
795 );
796 let (c_min, c_max) = scatter.color_range();
797 assert_eq!(c_min, 5.0);
798 assert_eq!(c_max, 15.0);
799 }
800
801 #[test]
802 fn test_scatter_color_range_empty_values() {
803 let scatter = ScatterPlot::new(vec![(0.0, 0.0)])
804 .with_color_by(vec![], Gradient::two(Color::BLUE, Color::RED));
805 let (c_min, c_max) = scatter.color_range();
806 assert_eq!(c_min, 0.0);
808 assert_eq!(c_max, 1.0);
809 }
810
811 #[test]
812 fn test_scatter_color_by_fewer_values_than_points() {
813 let mut scatter = ScatterPlot::new(vec![(0.0, 0.0), (1.0, 1.0), (2.0, 2.0)])
815 .with_color_by(vec![0.0, 1.0], Gradient::two(Color::BLUE, Color::RED));
816 let bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
817 scatter.layout(bounds);
818 let mut buffer = CellBuffer::new(60, 20);
819 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
820 scatter.paint(&mut canvas);
821 }
822
823 #[test]
824 fn test_scatter_same_x_values() {
825 let mut scatter = ScatterPlot::new(vec![(5.0, 0.0), (5.0, 5.0), (5.0, 10.0)]);
827 let bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
828 scatter.layout(bounds);
829 let (x_min, x_max) = scatter.x_range();
830 let mut buffer = CellBuffer::new(60, 20);
832 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
833 scatter.paint(&mut canvas);
834 let _ = (x_min, x_max);
835 }
836
837 #[test]
838 fn test_scatter_same_y_values() {
839 let mut scatter = ScatterPlot::new(vec![(0.0, 5.0), (5.0, 5.0), (10.0, 5.0)]);
841 let bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
842 scatter.layout(bounds);
843 let mut buffer = CellBuffer::new(60, 20);
844 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
845 scatter.paint(&mut canvas);
846 }
847
848 #[test]
849 fn test_scatter_too_small_bounds() {
850 let mut scatter = ScatterPlot::new(vec![(0.0, 0.0), (1.0, 1.0)]);
851 let bounds = Rect::new(0.0, 0.0, 5.0, 3.0);
852 scatter.layout(bounds);
853 let mut buffer = CellBuffer::new(5, 3);
854 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
855 scatter.paint(&mut canvas); }
857
858 #[test]
859 fn test_scatter_children_mut() {
860 let mut scatter = ScatterPlot::default();
861 assert!(scatter.children_mut().is_empty());
862 }
863
864 #[test]
865 fn test_scatter_measure() {
866 let scatter = ScatterPlot::default();
867 let size = scatter.measure(Constraints {
868 min_width: 0.0,
869 min_height: 0.0,
870 max_width: 100.0,
871 max_height: 50.0,
872 });
873 assert_eq!(size.width, 60.0);
874 assert_eq!(size.height, 20.0);
875 }
876
877 #[test]
878 fn test_scatter_clone() {
879 let original = ScatterPlot::new(vec![(1.0, 2.0), (3.0, 4.0)])
880 .with_marker(MarkerStyle::Star)
881 .with_color(Color::GREEN);
882 let cloned = original.clone();
883 assert_eq!(cloned.points.len(), 2);
884 assert_eq!(cloned.color, Color::GREEN);
885 assert!(matches!(cloned.marker, MarkerStyle::Star));
886 }
887
888 #[test]
889 fn test_scatter_debug() {
890 let scatter = ScatterPlot::default();
891 let debug = format!("{:?}", scatter);
892 assert!(debug.contains("ScatterPlot"));
893 }
894}