1use crate::widgets::symbols::BRAILLE_UP;
7use presentar_core::{
8 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
9 LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
10};
11use std::any::Any;
12use std::time::Duration;
13
14#[derive(Debug, Clone, Copy)]
16pub struct EmaConfig {
17 pub alpha: f64,
19}
20
21impl Default for EmaConfig {
22 fn default() -> Self {
23 Self { alpha: 0.6 }
24 }
25}
26
27#[derive(Debug, Clone)]
29pub struct LossSeries {
30 pub name: String,
32 pub values: Vec<f64>,
34 pub color: Color,
36 pub smoothed: bool,
38}
39
40#[derive(Debug, Clone)]
42pub struct LossCurve {
43 series: Vec<LossSeries>,
44 ema_config: EmaConfig,
45 show_raw: bool,
46 x_label: Option<String>,
47 y_label: Option<String>,
48 y_log_scale: bool,
49 bounds: Rect,
50 smoothed_cache: Vec<Vec<f64>>,
52}
53
54impl LossCurve {
55 #[must_use]
57 pub fn new() -> Self {
58 Self {
59 series: Vec::new(),
60 ema_config: EmaConfig::default(),
61 show_raw: true,
62 x_label: Some("Epoch".to_string()),
63 y_label: Some("Loss".to_string()),
64 y_log_scale: false,
65 bounds: Rect::default(),
66 smoothed_cache: Vec::new(),
67 }
68 }
69
70 #[must_use]
72 pub fn add_series(mut self, name: &str, values: Vec<f64>, color: Color) -> Self {
73 self.series.push(LossSeries {
74 name: name.to_string(),
75 values,
76 color,
77 smoothed: true,
78 });
79 self.invalidate_cache();
80 self
81 }
82
83 #[must_use]
85 pub fn with_ema(mut self, config: EmaConfig) -> Self {
86 self.ema_config = config;
87 self.invalidate_cache();
88 self
89 }
90
91 #[must_use]
93 pub fn with_raw_visible(mut self, show: bool) -> Self {
94 self.show_raw = show;
95 self
96 }
97
98 #[must_use]
100 pub fn with_log_scale(mut self, log: bool) -> Self {
101 self.y_log_scale = log;
102 self
103 }
104
105 #[must_use]
107 pub fn with_x_label(mut self, label: &str) -> Self {
108 self.x_label = Some(label.to_string());
109 self
110 }
111
112 #[must_use]
114 pub fn with_y_label(mut self, label: &str) -> Self {
115 self.y_label = Some(label.to_string());
116 self
117 }
118
119 pub fn update_series(&mut self, index: usize, values: Vec<f64>) {
121 if let Some(series) = self.series.get_mut(index) {
122 series.values = values;
123 self.invalidate_cache();
124 }
125 }
126
127 fn invalidate_cache(&mut self) {
129 self.smoothed_cache.clear();
130 }
131
132 fn compute_ema(&self, values: &[f64]) -> Vec<f64> {
135 if values.is_empty() {
136 return Vec::new();
137 }
138
139 let alpha = self.ema_config.alpha;
140 let mut smoothed = Vec::with_capacity(values.len());
141
142 let mut prev = values[0];
146 smoothed.push(prev);
147
148 for &val in values.iter().skip(1) {
149 if val.is_finite() {
150 prev = alpha * val + (1.0 - alpha) * prev;
151 }
152 smoothed.push(prev);
154 }
155
156 smoothed
157 }
158
159 fn ensure_cache(&mut self) {
161 if self.smoothed_cache.len() != self.series.len() {
162 self.smoothed_cache = self
163 .series
164 .iter()
165 .map(|s| {
166 if s.smoothed {
167 self.compute_ema(&s.values)
168 } else {
169 s.values.clone()
170 }
171 })
172 .collect();
173 }
174 }
175
176 fn y_range(&self) -> (f64, f64) {
178 let mut y_min = f64::INFINITY;
179 let mut y_max = f64::NEG_INFINITY;
180
181 for series in &self.series {
182 for &v in &series.values {
183 if v.is_finite() && v > 0.0 {
184 y_min = y_min.min(v);
186 y_max = y_max.max(v);
187 }
188 }
189 }
190
191 if y_min == f64::INFINITY {
192 (0.001, 1.0)
193 } else {
194 let padding = (y_max - y_min) * 0.1;
196 (
197 (y_min - padding).max(if self.y_log_scale { 1e-10 } else { 0.0 }),
198 y_max + padding,
199 )
200 }
201 }
202
203 fn x_range(&self) -> (f64, f64) {
205 let max_len = self
206 .series
207 .iter()
208 .map(|s| s.values.len())
209 .max()
210 .unwrap_or(0);
211 (0.0, max_len.saturating_sub(1) as f64)
212 }
213
214 fn transform_y(&self, y: f64, y_min: f64, y_max: f64) -> f64 {
216 if self.y_log_scale {
217 let log_min = y_min.max(1e-10).ln();
218 let log_max = y_max.ln();
219 let log_y = y.max(1e-10).ln();
220 (log_y - log_min) / (log_max - log_min)
221 } else {
222 (y - y_min) / (y_max - y_min)
223 }
224 }
225}
226
227impl Default for LossCurve {
228 fn default() -> Self {
229 Self::new()
230 }
231}
232
233impl Widget for LossCurve {
234 fn type_id(&self) -> TypeId {
235 TypeId::of::<Self>()
236 }
237
238 fn measure(&self, constraints: Constraints) -> Size {
239 Size::new(
240 constraints.max_width.min(80.0),
241 constraints.max_height.min(20.0),
242 )
243 }
244
245 fn layout(&mut self, bounds: Rect) -> LayoutResult {
246 self.bounds = bounds;
247 self.ensure_cache();
248 LayoutResult {
249 size: Size::new(bounds.width, bounds.height),
250 }
251 }
252
253 #[allow(clippy::too_many_lines)]
254 fn paint(&self, canvas: &mut dyn Canvas) {
255 if self.bounds.width < 15.0 || self.bounds.height < 5.0 {
256 return;
257 }
258
259 let margin_left = 8.0;
260 let margin_bottom = 2.0;
261 let margin_right = 2.0;
262
263 let plot_x = self.bounds.x + margin_left;
264 let plot_y = self.bounds.y;
265 let plot_width = self.bounds.width - margin_left - margin_right;
266 let plot_height = self.bounds.height - margin_bottom;
267
268 if plot_width <= 0.0 || plot_height <= 0.0 {
269 return;
270 }
271
272 let (y_min, y_max) = self.y_range();
273 let (x_min, x_max) = self.x_range();
274
275 let label_style = TextStyle {
276 color: Color::new(0.6, 0.6, 0.6, 1.0),
277 ..Default::default()
278 };
279
280 for i in 0..=4 {
282 let t = i as f64 / 4.0;
283 let y_val = if self.y_log_scale {
284 let log_min = y_min.max(1e-10).ln();
285 let log_max = y_max.ln();
286 (log_min + (log_max - log_min) * (1.0 - t)).exp()
287 } else {
288 y_min + (y_max - y_min) * (1.0 - t)
289 };
290 let y_pos = plot_y + plot_height * t as f32;
291
292 if y_pos >= plot_y && y_pos < plot_y + plot_height {
293 let label = if y_val < 0.01 {
294 format!("{y_val:.1e}")
295 } else if y_val < 1.0 {
296 format!("{y_val:.3}")
297 } else {
298 format!("{y_val:.2}")
299 };
300 canvas.draw_text(
301 &format!("{label:>7}"),
302 Point::new(self.bounds.x, y_pos),
303 &label_style,
304 );
305 }
306 }
307
308 let x_ticks = 5.min(x_max as usize);
310 for i in 0..=x_ticks {
311 let t = i as f64 / x_ticks as f64;
312 let x_val = x_min + (x_max - x_min) * t;
313 let x_pos = plot_x + plot_width * t as f32;
314
315 if x_pos >= plot_x && x_pos < plot_x + plot_width - 3.0 {
316 let label = format!("{x_val:.0}");
317 canvas.draw_text(
318 &label,
319 Point::new(x_pos, plot_y + plot_height),
320 &label_style,
321 );
322 }
323 }
324
325 for (series_idx, series) in self.series.iter().enumerate() {
327 if series.values.is_empty() {
328 continue;
329 }
330
331 let smoothed = if series_idx < self.smoothed_cache.len() && series.smoothed {
333 &self.smoothed_cache[series_idx]
334 } else {
335 &series.values
336 };
337
338 if self.show_raw && series.smoothed {
340 let raw_style = TextStyle {
341 color: Color::new(
342 series.color.r * 0.4,
343 series.color.g * 0.4,
344 series.color.b * 0.4,
345 0.5,
346 ),
347 ..Default::default()
348 };
349
350 self.draw_line_braille(
351 canvas,
352 &series.values,
353 &raw_style,
354 plot_x,
355 plot_y,
356 plot_width,
357 plot_height,
358 y_min,
359 y_max,
360 x_max,
361 );
362 }
363
364 let style = TextStyle {
366 color: series.color,
367 ..Default::default()
368 };
369
370 self.draw_line_braille(
371 canvas,
372 smoothed,
373 &style,
374 plot_x,
375 plot_y,
376 plot_width,
377 plot_height,
378 y_min,
379 y_max,
380 x_max,
381 );
382 }
383
384 if !self.series.is_empty() {
386 for (i, series) in self.series.iter().enumerate() {
387 let style = TextStyle {
388 color: series.color,
389 ..Default::default()
390 };
391 let legend_y = plot_y + i as f32;
392 canvas.draw_text(
393 &format!("─ {}", series.name),
394 Point::new(plot_x + plot_width - 15.0, legend_y),
395 &style,
396 );
397 }
398 }
399 }
400
401 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
402 None
403 }
404
405 fn children(&self) -> &[Box<dyn Widget>] {
406 &[]
407 }
408
409 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
410 &mut []
411 }
412}
413
414impl LossCurve {
415 #[allow(clippy::too_many_arguments)]
417 fn draw_line_braille(
418 &self,
419 canvas: &mut dyn Canvas,
420 values: &[f64],
421 style: &TextStyle,
422 plot_x: f32,
423 plot_y: f32,
424 plot_width: f32,
425 plot_height: f32,
426 y_min: f64,
427 y_max: f64,
428 x_max: f64,
429 ) {
430 if values.is_empty() || x_max <= 0.0 {
431 return;
432 }
433
434 let cols = plot_width as usize;
435 let rows = (plot_height * 4.0) as usize; if cols == 0 || rows == 0 {
438 return;
439 }
440
441 let mut grid = vec![vec![false; rows]; cols];
442
443 for (i, &y) in values.iter().enumerate() {
445 if !y.is_finite() {
446 continue;
447 }
448
449 let x_norm = i as f64 / x_max;
450 let y_norm = self.transform_y(y, y_min, y_max);
451
452 if !(0.0..=1.0).contains(&x_norm) || !(0.0..=1.0).contains(&y_norm) {
453 continue;
454 }
455
456 let gx = ((x_norm * (cols - 1) as f64).round() as usize).min(cols.saturating_sub(1));
457 let gy =
458 (((1.0 - y_norm) * (rows - 1) as f64).round() as usize).min(rows.saturating_sub(1));
459
460 grid[gx][gy] = true;
461
462 if i > 0 {
464 let prev_y = values[i - 1];
465 if prev_y.is_finite() {
466 let prev_x_norm = (i - 1) as f64 / x_max;
467 let prev_y_norm = self.transform_y(prev_y, y_min, y_max);
468
469 if (0.0..=1.0).contains(&prev_x_norm) && (0.0..=1.0).contains(&prev_y_norm) {
470 let prev_gx = ((prev_x_norm * (cols - 1) as f64).round() as usize)
471 .min(cols.saturating_sub(1));
472 let prev_gy = (((1.0 - prev_y_norm) * (rows - 1) as f64).round() as usize)
473 .min(rows.saturating_sub(1));
474
475 Self::draw_line_bresenham(&mut grid, prev_gx, prev_gy, gx, gy);
476 }
477 }
478 }
479 }
480
481 let char_rows = plot_height as usize;
483 for cy in 0..char_rows {
484 for (cx, column) in grid.iter().enumerate() {
485 let mut dots = 0u8;
486 for dy in 0..4 {
487 let gy = cy * 4 + dy;
488 if gy < rows && column[gy] {
489 dots |= 1 << dy;
490 }
491 }
492
493 if dots > 0 {
494 let braille_idx = dots as usize;
495 let ch = if braille_idx < BRAILLE_UP.len() {
496 BRAILLE_UP[braille_idx]
497 } else {
498 '⣿'
499 };
500 canvas.draw_text(
501 &ch.to_string(),
502 Point::new(plot_x + cx as f32, plot_y + cy as f32),
503 style,
504 );
505 }
506 }
507 }
508 }
509
510 #[allow(clippy::cast_possible_wrap)]
512 fn draw_line_bresenham(grid: &mut [Vec<bool>], x0: usize, y0: usize, x1: usize, y1: usize) {
513 let dx = (x1 as isize - x0 as isize).abs();
514 let dy = -(y1 as isize - y0 as isize).abs();
515 let sx: isize = if x0 < x1 { 1 } else { -1 };
516 let sy: isize = if y0 < y1 { 1 } else { -1 };
517 let mut err = dx + dy;
518
519 let mut x = x0 as isize;
520 let mut y = y0 as isize;
521
522 let cols = grid.len() as isize;
523 let rows = if cols > 0 { grid[0].len() as isize } else { 0 };
524
525 loop {
526 if x >= 0 && x < cols && y >= 0 && y < rows {
527 grid[x as usize][y as usize] = true;
528 }
529
530 if x == x1 as isize && y == y1 as isize {
531 break;
532 }
533
534 let e2 = 2 * err;
535 if e2 >= dy {
536 err += dy;
537 x += sx;
538 }
539 if e2 <= dx {
540 err += dx;
541 y += sy;
542 }
543 }
544 }
545}
546
547impl Brick for LossCurve {
548 fn brick_name(&self) -> &'static str {
549 "LossCurve"
550 }
551
552 fn assertions(&self) -> &[BrickAssertion] {
553 static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
554 ASSERTIONS
555 }
556
557 fn budget(&self) -> BrickBudget {
558 BrickBudget::uniform(16)
559 }
560
561 fn verify(&self) -> BrickVerification {
562 let mut passed = Vec::new();
563 let mut failed = Vec::new();
564
565 if self.bounds.width >= 15.0 && self.bounds.height >= 5.0 {
566 passed.push(BrickAssertion::max_latency_ms(16));
567 } else {
568 failed.push((
569 BrickAssertion::max_latency_ms(16),
570 "Size too small".to_string(),
571 ));
572 }
573
574 BrickVerification {
575 passed,
576 failed,
577 verification_time: Duration::from_micros(5),
578 }
579 }
580
581 fn to_html(&self) -> String {
582 String::new()
583 }
584
585 fn to_css(&self) -> String {
586 String::new()
587 }
588}
589
590#[cfg(test)]
591mod tests {
592 use super::*;
593 use crate::{CellBuffer, DirectTerminalCanvas};
594
595 #[test]
596 fn test_loss_curve_creation() {
597 let curve =
598 LossCurve::new().add_series("train", vec![1.0, 0.8, 0.6, 0.4, 0.3, 0.25], Color::BLUE);
599 assert_eq!(curve.series.len(), 1);
600 }
601
602 #[test]
603 fn test_ema_smoothing() {
604 let curve = LossCurve::new().with_ema(EmaConfig { alpha: 0.5 });
605 let values = vec![1.0, 0.0, 1.0, 0.0, 1.0];
606 let smoothed = curve.compute_ema(&values);
607
608 assert_eq!(smoothed.len(), values.len());
609 assert!((smoothed[0] - 1.0).abs() < 0.001);
611 assert!((smoothed[1] - 0.5).abs() < 0.001);
613 }
614
615 #[test]
616 fn test_ema_empty_values() {
617 let curve = LossCurve::new();
618 let smoothed = curve.compute_ema(&[]);
619 assert!(smoothed.is_empty());
620 }
621
622 #[test]
623 fn test_ema_with_nan_values() {
624 let curve = LossCurve::new().with_ema(EmaConfig { alpha: 0.5 });
625 let values = vec![1.0, f64::NAN, 0.5];
626 let smoothed = curve.compute_ema(&values);
627 assert_eq!(smoothed.len(), 3);
628 assert!((smoothed[0] - 1.0).abs() < 0.001);
629 assert!((smoothed[1] - 1.0).abs() < 0.001);
631 }
632
633 #[test]
634 fn test_ema_config_default() {
635 let config = EmaConfig::default();
636 assert!((config.alpha - 0.6).abs() < 0.001);
637 }
638
639 #[test]
640 fn test_log_scale() {
641 let curve = LossCurve::new().with_log_scale(true);
642 let y_norm = curve.transform_y(1.0, 0.1, 10.0);
643 assert!(y_norm > 0.0 && y_norm < 1.0);
644 }
645
646 #[test]
647 fn test_linear_scale() {
648 let curve = LossCurve::new().with_log_scale(false);
649 let y_norm = curve.transform_y(5.0, 0.0, 10.0);
650 assert!((y_norm - 0.5).abs() < 0.001);
651 }
652
653 #[test]
654 fn test_multi_series() {
655 let curve = LossCurve::new()
656 .add_series("train", vec![1.0, 0.5, 0.3], Color::BLUE)
657 .add_series("val", vec![1.1, 0.6, 0.4], Color::RED);
658 assert_eq!(curve.series.len(), 2);
659 }
660
661 #[test]
662 fn test_with_x_label() {
663 let curve = LossCurve::new().with_x_label("Steps");
664 assert_eq!(curve.x_label, Some("Steps".to_string()));
665 }
666
667 #[test]
668 fn test_with_y_label() {
669 let curve = LossCurve::new().with_y_label("MSE");
670 assert_eq!(curve.y_label, Some("MSE".to_string()));
671 }
672
673 #[test]
674 fn test_with_raw_visible() {
675 let curve = LossCurve::new().with_raw_visible(false);
676 assert!(!curve.show_raw);
677
678 let curve2 = LossCurve::new().with_raw_visible(true);
679 assert!(curve2.show_raw);
680 }
681
682 #[test]
683 fn test_update_series() {
684 let mut curve = LossCurve::new().add_series("train", vec![1.0, 0.8], Color::BLUE);
685 curve.update_series(0, vec![0.5, 0.3, 0.1]);
686 assert_eq!(curve.series[0].values.len(), 3);
687 }
688
689 #[test]
690 fn test_update_series_invalid_index() {
691 let mut curve = LossCurve::new().add_series("train", vec![1.0], Color::BLUE);
692 curve.update_series(5, vec![0.5]); assert_eq!(curve.series[0].values.len(), 1);
694 }
695
696 #[test]
697 fn test_y_range_empty() {
698 let curve = LossCurve::new();
699 let (y_min, y_max) = curve.y_range();
700 assert!(y_min < y_max);
701 }
702
703 #[test]
704 fn test_y_range_with_data() {
705 let curve = LossCurve::new().add_series("train", vec![1.0, 2.0, 3.0], Color::BLUE);
706 let (y_min, y_max) = curve.y_range();
707 assert!(y_min <= 1.0);
708 assert!(y_max >= 3.0);
709 }
710
711 #[test]
712 fn test_x_range() {
713 let curve =
714 LossCurve::new().add_series("train", vec![1.0, 2.0, 3.0, 4.0, 5.0], Color::BLUE);
715 let (x_min, x_max) = curve.x_range();
716 assert!((x_min - 0.0).abs() < 0.001);
717 assert!((x_max - 4.0).abs() < 0.001);
718 }
719
720 #[test]
721 fn test_x_range_empty() {
722 let curve = LossCurve::new();
723 let (x_min, x_max) = curve.x_range();
724 assert!((x_min - 0.0).abs() < 0.001);
725 assert!((x_max - 0.0).abs() < 0.001);
726 }
727
728 #[test]
729 fn test_loss_curve_measure() {
730 let curve = LossCurve::new();
731 let constraints = Constraints::new(0.0, 100.0, 0.0, 50.0);
732 let size = curve.measure(constraints);
733 assert_eq!(size.width, 80.0);
734 assert_eq!(size.height, 20.0);
735 }
736
737 #[test]
738 fn test_loss_curve_layout_and_paint() {
739 let mut curve = LossCurve::new()
740 .add_series("train", vec![2.0, 1.5, 1.0, 0.8, 0.6], Color::BLUE)
741 .add_series("val", vec![2.2, 1.8, 1.2, 1.0, 0.9], Color::RED);
742
743 let mut buffer = CellBuffer::new(60, 20);
744 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
745
746 let result = curve.layout(Rect::new(0.0, 0.0, 60.0, 20.0));
747 assert_eq!(result.size.width, 60.0);
748 assert_eq!(result.size.height, 20.0);
749
750 curve.paint(&mut canvas);
751
752 let cells = buffer.cells();
754 let non_empty = cells.iter().filter(|c| !c.symbol.is_empty()).count();
755 assert!(non_empty > 0, "Loss curve should render some content");
756 }
757
758 #[test]
759 fn test_loss_curve_paint_with_log_scale() {
760 let mut curve = LossCurve::new().with_log_scale(true).add_series(
761 "train",
762 vec![1.0, 0.1, 0.01, 0.001],
763 Color::BLUE,
764 );
765
766 let mut buffer = CellBuffer::new(60, 20);
767 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
768
769 curve.layout(Rect::new(0.0, 0.0, 60.0, 20.0));
770 curve.paint(&mut canvas);
771 }
772
773 #[test]
774 fn test_loss_curve_paint_small_bounds() {
775 let mut curve = LossCurve::new().add_series("train", vec![1.0, 0.5], Color::BLUE);
776
777 let mut buffer = CellBuffer::new(10, 3);
778 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
779
780 curve.layout(Rect::new(0.0, 0.0, 10.0, 3.0));
781 curve.paint(&mut canvas);
782 }
784
785 #[test]
786 fn test_loss_curve_paint_with_raw_hidden() {
787 let mut curve = LossCurve::new().with_raw_visible(false).add_series(
788 "train",
789 vec![1.0, 0.8, 0.6, 0.4],
790 Color::BLUE,
791 );
792
793 let mut buffer = CellBuffer::new(60, 20);
794 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
795
796 curve.layout(Rect::new(0.0, 0.0, 60.0, 20.0));
797 curve.paint(&mut canvas);
798 }
799
800 #[test]
801 fn test_loss_curve_paint_noisy_data() {
802 let values: Vec<f64> = (0..100)
803 .map(|i| 2.0 * (-i as f64 / 30.0).exp() + 0.1 * (i as f64 * 0.5).sin())
804 .collect();
805
806 let mut curve = LossCurve::new()
807 .with_ema(EmaConfig { alpha: 0.1 })
808 .add_series("train", values, Color::BLUE);
809
810 let mut buffer = CellBuffer::new(80, 25);
811 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
812
813 curve.layout(Rect::new(0.0, 0.0, 80.0, 25.0));
814 curve.paint(&mut canvas);
815 }
816
817 #[test]
818 fn test_loss_curve_paint_with_nan() {
819 let values = vec![1.0, f64::NAN, 0.5, f64::INFINITY, 0.3];
820 let mut curve = LossCurve::new().add_series("train", values, Color::BLUE);
821
822 let mut buffer = CellBuffer::new(60, 20);
823 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
824
825 curve.layout(Rect::new(0.0, 0.0, 60.0, 20.0));
826 curve.paint(&mut canvas);
827 }
828
829 #[test]
830 fn test_loss_curve_ensure_cache() {
831 let mut curve = LossCurve::new().add_series("train", vec![1.0, 0.5, 0.3], Color::BLUE);
832 curve.ensure_cache();
833 assert_eq!(curve.smoothed_cache.len(), 1);
834 assert_eq!(curve.smoothed_cache[0].len(), 3);
835 }
836
837 #[test]
838 fn test_loss_curve_assertions() {
839 let curve = LossCurve::default();
840 assert!(!curve.assertions().is_empty());
841 }
842
843 #[test]
844 fn test_loss_curve_verify_valid() {
845 let mut curve = LossCurve::default();
846 curve.bounds = Rect::new(0.0, 0.0, 80.0, 20.0);
847 assert!(curve.verify().is_valid());
848 }
849
850 #[test]
851 fn test_loss_curve_verify_invalid() {
852 let mut curve = LossCurve::default();
853 curve.bounds = Rect::new(0.0, 0.0, 10.0, 3.0);
854 assert!(!curve.verify().is_valid());
855 }
856
857 #[test]
858 fn test_loss_curve_children() {
859 let curve = LossCurve::default();
860 assert!(curve.children().is_empty());
861 }
862
863 #[test]
864 fn test_loss_curve_children_mut() {
865 let mut curve = LossCurve::default();
866 assert!(curve.children_mut().is_empty());
867 }
868
869 #[test]
870 fn test_loss_curve_brick_name() {
871 let curve = LossCurve::new();
872 assert_eq!(curve.brick_name(), "LossCurve");
873 }
874
875 #[test]
876 fn test_loss_curve_budget() {
877 let curve = LossCurve::new();
878 let budget = curve.budget();
879 assert!(budget.layout_ms > 0);
880 }
881
882 #[test]
883 fn test_loss_curve_to_html() {
884 let curve = LossCurve::new();
885 assert!(curve.to_html().is_empty());
886 }
887
888 #[test]
889 fn test_loss_curve_to_css() {
890 let curve = LossCurve::new();
891 assert!(curve.to_css().is_empty());
892 }
893
894 #[test]
895 fn test_loss_curve_type_id() {
896 let curve = LossCurve::new();
897 let type_id = Widget::type_id(&curve);
898 assert_eq!(type_id, TypeId::of::<LossCurve>());
899 }
900
901 #[test]
902 fn test_loss_curve_event() {
903 let mut curve = LossCurve::new();
904 let event = Event::Resize {
905 width: 80.0,
906 height: 24.0,
907 };
908 assert!(curve.event(&event).is_none());
909 }
910
911 #[test]
912 fn test_empty_series() {
913 let curve = LossCurve::new().add_series("empty", vec![], Color::BLUE);
914 let (y_min, y_max) = curve.y_range();
915 assert!(y_min < y_max);
916 }
917
918 #[test]
919 fn test_bresenham_line() {
920 let mut grid = vec![vec![false; 10]; 10];
921 LossCurve::draw_line_bresenham(&mut grid, 0, 0, 9, 9);
922 assert!(grid[0][0]);
924 assert!(grid[9][9]);
925 }
926
927 #[test]
928 fn test_bresenham_line_reverse() {
929 let mut grid = vec![vec![false; 10]; 10];
930 LossCurve::draw_line_bresenham(&mut grid, 9, 9, 0, 0);
931 assert!(grid[0][0]);
932 assert!(grid[9][9]);
933 }
934
935 #[test]
936 fn test_bresenham_horizontal() {
937 let mut grid = vec![vec![false; 10]; 10];
938 LossCurve::draw_line_bresenham(&mut grid, 0, 5, 9, 5);
939 for x in 0..10 {
940 assert!(grid[x][5]);
941 }
942 }
943
944 #[test]
945 fn test_bresenham_vertical() {
946 let mut grid = vec![vec![false; 10]; 10];
947 LossCurve::draw_line_bresenham(&mut grid, 5, 0, 5, 9);
948 for y in 0..10 {
949 assert!(grid[5][y]);
950 }
951 }
952}