1use crate::theme::Gradient;
4use presentar_core::{
5 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
6 LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
7};
8use std::any::Any;
9use std::time::Duration;
10
11#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
13pub enum GraphMode {
14 #[default]
16 Braille,
17 Block,
19 Tty,
21}
22
23#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
25pub enum TimeAxisMode {
26 #[default]
28 Indices,
29 Relative {
31 interval_secs: u64,
33 },
34 Absolute,
36 Hidden,
38}
39
40impl TimeAxisMode {
41 pub fn format_label(&self, index: usize, total: usize) -> Option<String> {
43 match self {
44 Self::Indices => Some(format!("{index}")),
45 Self::Relative { interval_secs } => {
46 let secs_ago = (total - index) as u64 * interval_secs;
47 if secs_ago < 60 {
48 Some(format!("{secs_ago}s"))
49 } else if secs_ago < 3600 {
50 Some(format!("{}m", secs_ago / 60))
51 } else {
52 Some(format!("{}h", secs_ago / 3600))
53 }
54 }
55 Self::Absolute | Self::Hidden => None, }
57 }
58}
59
60#[derive(Debug, Clone, Copy)]
62pub struct AxisMargins {
63 pub y_axis_width: u16,
65 pub x_axis_height: u16,
67}
68
69impl Default for AxisMargins {
70 fn default() -> Self {
71 Self {
72 y_axis_width: 6,
73 x_axis_height: 1,
74 }
75 }
76}
77
78impl AxisMargins {
79 pub const NONE: Self = Self {
81 y_axis_width: 0,
82 x_axis_height: 0,
83 };
84
85 pub const COMPACT: Self = Self {
87 y_axis_width: 4,
88 x_axis_height: 1,
89 };
90
91 pub const STANDARD: Self = Self {
93 y_axis_width: 6,
94 x_axis_height: 1,
95 };
96
97 pub const WIDE: Self = Self {
99 y_axis_width: 10,
100 x_axis_height: 2,
101 };
102}
103
104#[derive(Debug, Clone)]
106pub struct BrailleGraph {
107 data: Vec<f64>,
108 color: Color,
109 gradient: Option<Gradient>,
111 min: f64,
112 max: f64,
113 mode: GraphMode,
114 label: Option<String>,
115 margins: AxisMargins,
117 time_axis: TimeAxisMode,
119 show_legend: bool,
121 bounds: Rect,
122}
123
124impl BrailleGraph {
125 #[must_use]
127 pub fn new(data: Vec<f64>) -> Self {
128 let (min, max) = Self::compute_range(&data);
129 Self {
130 data,
131 color: Color::GREEN,
132 gradient: None,
133 min,
134 max,
135 mode: GraphMode::default(),
136 label: None,
137 margins: AxisMargins::default(),
138 time_axis: TimeAxisMode::default(),
139 show_legend: false,
140 bounds: Rect::new(0.0, 0.0, 0.0, 0.0),
141 }
142 }
143
144 #[must_use]
146 pub fn with_color(mut self, color: Color) -> Self {
147 self.color = color;
148 self
149 }
150
151 #[must_use]
154 pub fn with_gradient(mut self, gradient: Gradient) -> Self {
155 self.gradient = Some(gradient);
156 self
157 }
158
159 #[must_use]
161 pub fn with_range(mut self, min: f64, max: f64) -> Self {
162 debug_assert!(min.is_finite(), "min must be finite");
163 debug_assert!(max.is_finite(), "max must be finite");
164 self.min = min;
165 self.max = max;
166 self
167 }
168
169 #[must_use]
171 pub fn with_mode(mut self, mode: GraphMode) -> Self {
172 self.mode = mode;
173 self
174 }
175
176 #[must_use]
178 pub fn with_label(mut self, label: impl Into<String>) -> Self {
179 self.label = Some(label.into());
180 self
181 }
182
183 #[must_use]
185 pub fn with_margins(mut self, margins: AxisMargins) -> Self {
186 self.margins = margins;
187 self
188 }
189
190 #[must_use]
192 pub fn with_time_axis(mut self, mode: TimeAxisMode) -> Self {
193 self.time_axis = mode;
194 self
195 }
196
197 #[must_use]
199 pub fn with_legend(mut self, show: bool) -> Self {
200 self.show_legend = show;
201 self
202 }
203
204 fn graph_area(&self) -> Rect {
206 let y_offset = self.margins.y_axis_width as f32;
207 let x_height = self.margins.x_axis_height as f32;
208 Rect::new(
209 self.bounds.x + y_offset,
210 self.bounds.y,
211 (self.bounds.width - y_offset).max(0.0),
212 (self.bounds.height - x_height).max(0.0),
213 )
214 }
215
216 fn render_y_axis(&self, canvas: &mut dyn Canvas) {
218 if self.margins.y_axis_width == 0 {
219 return;
220 }
221
222 let style = TextStyle {
223 color: Color::WHITE,
224 ..Default::default()
225 };
226
227 let max_str = format!("{:.0}", self.max);
229 canvas.draw_text(&max_str, Point::new(self.bounds.x, self.bounds.y), &style);
230
231 let graph_height = (self.bounds.height - self.margins.x_axis_height as f32).max(1.0);
233 let min_str = format!("{:.0}", self.min);
234 canvas.draw_text(
235 &min_str,
236 Point::new(self.bounds.x, self.bounds.y + graph_height - 1.0),
237 &style,
238 );
239 }
240
241 fn render_x_axis(&self, canvas: &mut dyn Canvas) {
243 if self.margins.x_axis_height == 0 {
244 return;
245 }
246 if matches!(self.time_axis, TimeAxisMode::Hidden) {
247 return;
248 }
249
250 let graph = self.graph_area();
251 let y_pos = self.bounds.y + self.bounds.height - 1.0;
252 let total = self.data.len();
253
254 let style = TextStyle {
255 color: Color::WHITE,
256 ..Default::default()
257 };
258
259 let positions = [0, total / 2, total.saturating_sub(1)];
261 for &idx in &positions {
262 if let Some(label) = self.time_axis.format_label(idx, total) {
263 let x_frac = if total > 1 {
264 idx as f32 / (total - 1) as f32
265 } else {
266 0.5
267 };
268 let x_pos = graph.x + x_frac * (graph.width - 1.0).max(0.0);
269 canvas.draw_text(&label, Point::new(x_pos, y_pos), &style);
270 }
271 }
272 }
273
274 fn render_legend(&self, canvas: &mut dyn Canvas) {
276 if !self.show_legend {
277 return;
278 }
279
280 let style = TextStyle {
281 color: Color::WHITE,
282 ..Default::default()
283 };
284
285 let legend = format!("⣿={:.0} ⣀={:.0}", self.max, self.min);
287 let x = self.bounds.x + self.bounds.width - legend.len() as f32;
288 canvas.draw_text(&legend, Point::new(x.max(0.0), self.bounds.y), &style);
289 }
290
291 pub fn set_data(&mut self, data: Vec<f64>) {
293 let (min, max) = Self::compute_range(&data);
294 self.data = data;
295 self.min = min;
296 self.max = max;
297 }
298
299 pub fn push(&mut self, value: f64) {
301 self.data.push(value);
302 if value < self.min {
303 self.min = value;
304 }
305 if value > self.max {
306 self.max = value;
307 }
308 }
309
310 fn compute_range(data: &[f64]) -> (f64, f64) {
311 if data.is_empty() {
312 return (0.0, 1.0);
313 }
314 let min = data.iter().fold(f64::INFINITY, |a, &b| a.min(b));
315 let max = data.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
316 if (max - min).abs() < f64::EPSILON {
317 (min - 0.5, max + 0.5)
318 } else {
319 (min, max)
320 }
321 }
322
323 fn normalize(&self, value: f64) -> f64 {
324 if (self.max - self.min).abs() < f64::EPSILON {
325 0.5
326 } else {
327 (value - self.min) / (self.max - self.min)
328 }
329 }
330
331 fn color_for_value(&self, normalized: f64) -> Color {
334 match &self.gradient {
335 Some(gradient) => gradient.sample(normalized),
336 None => self.color,
337 }
338 }
339
340 fn render_braille(&self, canvas: &mut dyn Canvas) {
341 let width = self.bounds.width as usize;
342 let height = self.bounds.height as usize;
343 if width == 0 || height == 0 || self.data.is_empty() {
344 return;
345 }
346
347 let dots_per_col = 2;
348 let dots_per_row = 4;
349 let total_dots_x = width * dots_per_col;
350 let total_dots_y = height * dots_per_row;
351
352 let step = if self.data.len() > total_dots_x {
353 self.data.len() as f64 / total_dots_x as f64
354 } else {
355 1.0
356 };
357
358 let mut dots = vec![vec![false; total_dots_x]; total_dots_y];
360 let mut column_values: Vec<f64> = vec![0.0; width];
361
362 for (i, x) in (0..total_dots_x).enumerate() {
363 let data_idx = (i as f64 * step) as usize;
364 if data_idx >= self.data.len() {
365 break;
366 }
367 let value = self.normalize(self.data[data_idx]);
368 let y = ((1.0 - value) * (total_dots_y - 1) as f64).round() as usize;
369 if y < total_dots_y {
370 dots[y][x] = true;
371 }
372 let char_col = x / dots_per_col;
374 if char_col < width && value > column_values[char_col] {
375 column_values[char_col] = value;
376 }
377 }
378
379 for cy in 0..height {
380 for (cx, &col_value) in column_values.iter().enumerate().take(width) {
381 let mut code_point = 0x2800u32;
382 let dot_offsets = [
383 (0, 0, 0x01),
384 (0, 1, 0x02),
385 (0, 2, 0x04),
386 (1, 0, 0x08),
387 (1, 1, 0x10),
388 (1, 2, 0x20),
389 (0, 3, 0x40),
390 (1, 3, 0x80),
391 ];
392
393 for (dx, dy, bit) in dot_offsets {
394 let dot_x = cx * dots_per_col + dx;
395 let dot_y = cy * dots_per_row + dy;
396 if dot_y < total_dots_y && dot_x < total_dots_x && dots[dot_y][dot_x] {
397 code_point |= bit;
398 }
399 }
400
401 if let Some(c) = char::from_u32(code_point) {
402 let color = self.color_for_value(col_value);
404 let style = TextStyle {
405 color,
406 ..Default::default()
407 };
408 canvas.draw_text(
409 &c.to_string(),
410 Point::new(self.bounds.x + cx as f32, self.bounds.y + cy as f32),
411 &style,
412 );
413 }
414 }
415 }
416 }
417
418 fn render_block(&self, canvas: &mut dyn Canvas) {
419 let width = self.bounds.width as usize;
420 let height = self.bounds.height as usize;
421 if width == 0 || height == 0 || self.data.is_empty() {
422 return;
423 }
424
425 let total_rows = height * 2;
426
427 let step = if self.data.len() > width {
428 self.data.len() as f64 / width as f64
429 } else {
430 1.0
431 };
432
433 let mut column_data: Vec<(usize, f64)> = Vec::with_capacity(width);
435 for x in 0..width {
436 let data_idx = (x as f64 * step) as usize;
437 if data_idx >= self.data.len() {
438 column_data.push((total_rows, 0.0));
439 continue;
440 }
441 let value = self.normalize(self.data[data_idx]);
442 let row = ((1.0 - value) * (total_rows - 1) as f64).round() as usize;
443 column_data.push((row.min(total_rows - 1), value));
444 }
445
446 for cy in 0..height {
447 for cx in 0..width {
448 let (value_row, normalized) =
449 column_data.get(cx).copied().unwrap_or((total_rows, 0.0));
450 let top_row = cy * 2;
451 let bottom_row = cy * 2 + 1;
452
453 let top_filled = value_row <= top_row;
454 let bottom_filled = value_row <= bottom_row;
455
456 let ch = match (top_filled, bottom_filled) {
457 (true, true) => '█',
458 (true, false) => '▀',
459 (false, true) => '▄',
460 (false, false) => ' ',
461 };
462
463 let color = self.color_for_value(normalized);
465 let style = TextStyle {
466 color,
467 ..Default::default()
468 };
469 canvas.draw_text(
470 &ch.to_string(),
471 Point::new(self.bounds.x + cx as f32, self.bounds.y + cy as f32),
472 &style,
473 );
474 }
475 }
476 }
477
478 fn render_tty(&self, canvas: &mut dyn Canvas) {
479 let width = self.bounds.width as usize;
480 let height = self.bounds.height as usize;
481 if width == 0 || height == 0 || self.data.is_empty() {
482 return;
483 }
484
485 let step = if self.data.len() > width {
486 self.data.len() as f64 / width as f64
487 } else {
488 1.0
489 };
490
491 let mut column_data: Vec<(usize, f64)> = Vec::with_capacity(width);
493 for x in 0..width {
494 let data_idx = (x as f64 * step) as usize;
495 if data_idx >= self.data.len() {
496 column_data.push((height, 0.0));
497 continue;
498 }
499 let value = self.normalize(self.data[data_idx]);
500 let row = ((1.0 - value) * (height - 1) as f64).round() as usize;
501 column_data.push((row.min(height - 1), value));
502 }
503
504 for cy in 0..height {
505 for cx in 0..width {
506 let (value_row, normalized) = column_data.get(cx).copied().unwrap_or((height, 0.0));
507 let ch = if value_row == cy { '*' } else { ' ' };
508
509 let color = self.color_for_value(normalized);
511 let style = TextStyle {
512 color,
513 ..Default::default()
514 };
515 canvas.draw_text(
516 &ch.to_string(),
517 Point::new(self.bounds.x + cx as f32, self.bounds.y + cy as f32),
518 &style,
519 );
520 }
521 }
522 }
523}
524
525impl Brick for BrailleGraph {
526 fn brick_name(&self) -> &'static str {
527 "braille_graph"
528 }
529
530 fn assertions(&self) -> &[BrickAssertion] {
531 static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
532 ASSERTIONS
533 }
534
535 fn budget(&self) -> BrickBudget {
536 BrickBudget::uniform(16)
537 }
538
539 fn verify(&self) -> BrickVerification {
540 BrickVerification {
541 passed: vec![BrickAssertion::max_latency_ms(16)],
542 failed: vec![],
543 verification_time: Duration::from_micros(10),
544 }
545 }
546
547 fn to_html(&self) -> String {
548 String::new() }
550
551 fn to_css(&self) -> String {
552 String::new() }
554}
555
556impl Widget for BrailleGraph {
557 fn type_id(&self) -> TypeId {
558 TypeId::of::<Self>()
559 }
560
561 fn measure(&self, constraints: Constraints) -> Size {
562 let width = constraints.max_width.max(10.0);
563 let height = constraints.max_height.max(3.0);
564 constraints.constrain(Size::new(width, height))
565 }
566
567 fn layout(&mut self, bounds: Rect) -> LayoutResult {
568 self.bounds = bounds;
569 LayoutResult {
570 size: Size::new(bounds.width, bounds.height),
571 }
572 }
573
574 fn paint(&self, canvas: &mut dyn Canvas) {
575 if self.bounds.width < 1.0 || self.bounds.height < 1.0 || self.data.is_empty() {
577 return;
578 }
579
580 self.render_y_axis(canvas);
582
583 self.render_x_axis(canvas);
585
586 self.render_legend(canvas);
588
589 match self.mode {
591 GraphMode::Braille => self.render_braille(canvas),
592 GraphMode::Block => self.render_block(canvas),
593 GraphMode::Tty => self.render_tty(canvas),
594 }
595
596 if let Some(ref label) = self.label {
597 let style = TextStyle {
598 color: self.color,
599 ..Default::default()
600 };
601 canvas.draw_text(label, Point::new(self.bounds.x, self.bounds.y), &style);
602 }
603 }
604
605 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
606 None
607 }
608
609 fn children(&self) -> &[Box<dyn Widget>] {
610 &[]
611 }
612
613 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
614 &mut []
615 }
616}
617
618#[cfg(test)]
619mod tests {
620 use super::*;
621 use presentar_core::{Canvas, TextStyle};
622
623 struct MockCanvas {
624 texts: Vec<(String, Point)>,
625 }
626
627 impl MockCanvas {
628 fn new() -> Self {
629 Self { texts: vec![] }
630 }
631 }
632
633 impl Canvas for MockCanvas {
634 fn fill_rect(&mut self, _rect: Rect, _color: Color) {}
635 fn stroke_rect(&mut self, _rect: Rect, _color: Color, _width: f32) {}
636 fn draw_text(&mut self, text: &str, position: Point, _style: &TextStyle) {
637 self.texts.push((text.to_string(), position));
638 }
639 fn draw_line(&mut self, _from: Point, _to: Point, _color: Color, _width: f32) {}
640 fn fill_circle(&mut self, _center: Point, _radius: f32, _color: Color) {}
641 fn stroke_circle(&mut self, _center: Point, _radius: f32, _color: Color, _width: f32) {}
642 fn fill_arc(
643 &mut self,
644 _center: Point,
645 _radius: f32,
646 _start: f32,
647 _end: f32,
648 _color: Color,
649 ) {
650 }
651 fn draw_path(&mut self, _points: &[Point], _color: Color, _width: f32) {}
652 fn fill_polygon(&mut self, _points: &[Point], _color: Color) {}
653 fn push_clip(&mut self, _rect: Rect) {}
654 fn pop_clip(&mut self) {}
655 fn push_transform(&mut self, _transform: presentar_core::Transform2D) {}
656 fn pop_transform(&mut self) {}
657 }
658
659 #[test]
660 fn test_graph_creation() {
661 let graph = BrailleGraph::new(vec![1.0, 2.0, 3.0]);
662 assert_eq!(graph.data.len(), 3);
663 }
664
665 #[test]
666 fn test_graph_assertions_not_empty() {
667 let graph = BrailleGraph::new(vec![1.0, 2.0, 3.0]);
668 assert!(!graph.assertions().is_empty());
669 }
670
671 #[test]
672 fn test_graph_verify_pass() {
673 let graph = BrailleGraph::new(vec![1.0, 2.0, 3.0]);
674 assert!(graph.verify().is_valid());
675 }
676
677 #[test]
678 fn test_graph_with_color() {
679 let graph = BrailleGraph::new(vec![1.0, 2.0]).with_color(Color::RED);
680 assert_eq!(graph.color, Color::RED);
681 }
682
683 #[test]
684 fn test_graph_with_range() {
685 let graph = BrailleGraph::new(vec![1.0, 2.0]).with_range(0.0, 100.0);
686 assert_eq!(graph.min, 0.0);
687 assert_eq!(graph.max, 100.0);
688 }
689
690 #[test]
691 fn test_graph_with_mode() {
692 let graph = BrailleGraph::new(vec![1.0]).with_mode(GraphMode::Block);
693 assert_eq!(graph.mode, GraphMode::Block);
694
695 let graph2 = BrailleGraph::new(vec![1.0]).with_mode(GraphMode::Tty);
696 assert_eq!(graph2.mode, GraphMode::Tty);
697 }
698
699 #[test]
700 fn test_graph_with_label() {
701 let graph = BrailleGraph::new(vec![1.0]).with_label("CPU Usage");
702 assert_eq!(graph.label, Some("CPU Usage".to_string()));
703 }
704
705 #[test]
706 fn test_graph_set_data() {
707 let mut graph = BrailleGraph::new(vec![1.0, 2.0]);
708 graph.set_data(vec![10.0, 20.0, 30.0, 40.0]);
709 assert_eq!(graph.data.len(), 4);
710 assert_eq!(graph.min, 10.0);
711 assert_eq!(graph.max, 40.0);
712 }
713
714 #[test]
715 fn test_graph_push() {
716 let mut graph = BrailleGraph::new(vec![5.0, 10.0]);
717 graph.push(15.0);
718 assert_eq!(graph.data.len(), 3);
719 assert_eq!(graph.max, 15.0);
720
721 graph.push(2.0);
722 assert_eq!(graph.min, 2.0);
723 }
724
725 #[test]
726 fn test_graph_empty_data_range() {
727 let graph = BrailleGraph::new(vec![]);
728 assert_eq!(graph.min, 0.0);
729 assert_eq!(graph.max, 1.0);
730 }
731
732 #[test]
733 fn test_graph_constant_data_range() {
734 let graph = BrailleGraph::new(vec![5.0, 5.0, 5.0]);
735 assert_eq!(graph.min, 4.5);
736 assert_eq!(graph.max, 5.5);
737 }
738
739 #[test]
740 fn test_graph_normalize() {
741 let graph = BrailleGraph::new(vec![0.0, 100.0]);
742 assert!((graph.normalize(50.0) - 0.5).abs() < f64::EPSILON);
743 assert!((graph.normalize(0.0) - 0.0).abs() < f64::EPSILON);
744 assert!((graph.normalize(100.0) - 1.0).abs() < f64::EPSILON);
745 }
746
747 #[test]
748 fn test_graph_normalize_constant() {
749 let graph = BrailleGraph::new(vec![5.0, 5.0]);
750 assert!((graph.normalize(5.0) - 0.5).abs() < f64::EPSILON);
751 }
752
753 #[test]
754 fn test_graph_measure() {
755 let graph = BrailleGraph::new(vec![1.0, 2.0]);
756 let constraints = Constraints::new(0.0, 100.0, 0.0, 50.0);
757 let size = graph.measure(constraints);
758 assert!(size.width >= 10.0);
759 assert!(size.height >= 3.0);
760 }
761
762 #[test]
763 fn test_graph_layout() {
764 let mut graph = BrailleGraph::new(vec![1.0, 2.0]);
765 let bounds = Rect::new(10.0, 20.0, 80.0, 24.0);
766 let result = graph.layout(bounds);
767 assert_eq!(result.size.width, 80.0);
768 assert_eq!(result.size.height, 24.0);
769 assert_eq!(graph.bounds, bounds);
770 }
771
772 #[test]
773 fn test_graph_paint_braille() {
774 let mut graph = BrailleGraph::new(vec![0.0, 50.0, 100.0]).with_mode(GraphMode::Braille);
775 graph.bounds = Rect::new(0.0, 0.0, 10.0, 5.0);
776 let mut canvas = MockCanvas::new();
777 graph.paint(&mut canvas);
778 assert!(!canvas.texts.is_empty());
779 }
780
781 #[test]
782 fn test_graph_paint_block() {
783 let mut graph = BrailleGraph::new(vec![0.0, 50.0, 100.0]).with_mode(GraphMode::Block);
784 graph.bounds = Rect::new(0.0, 0.0, 10.0, 5.0);
785 let mut canvas = MockCanvas::new();
786 graph.paint(&mut canvas);
787 assert!(!canvas.texts.is_empty());
788 }
789
790 #[test]
791 fn test_graph_paint_tty() {
792 let mut graph = BrailleGraph::new(vec![0.0, 50.0, 100.0]).with_mode(GraphMode::Tty);
793 graph.bounds = Rect::new(0.0, 0.0, 10.0, 5.0);
794 let mut canvas = MockCanvas::new();
795 graph.paint(&mut canvas);
796 assert!(!canvas.texts.is_empty());
797 }
798
799 #[test]
800 fn test_graph_paint_with_label() {
801 let mut graph = BrailleGraph::new(vec![1.0, 2.0]).with_label("Test");
802 graph.bounds = Rect::new(0.0, 0.0, 20.0, 10.0);
803 let mut canvas = MockCanvas::new();
804 graph.paint(&mut canvas);
805 assert!(canvas.texts.iter().any(|(t, _)| t.contains("Test")));
806 }
807
808 #[test]
809 fn test_graph_paint_empty_bounds() {
810 let mut graph = BrailleGraph::new(vec![1.0, 2.0]);
811 graph.bounds = Rect::new(0.0, 0.0, 0.0, 0.0);
812 let mut canvas = MockCanvas::new();
813 graph.paint(&mut canvas);
814 assert!(canvas.texts.is_empty());
815 }
816
817 #[test]
818 fn test_graph_paint_empty_data() {
819 let mut graph = BrailleGraph::new(vec![]);
820 graph.bounds = Rect::new(0.0, 0.0, 10.0, 5.0);
821 let mut canvas = MockCanvas::new();
822 graph.paint(&mut canvas);
823 assert!(canvas.texts.is_empty());
824 }
825
826 #[test]
827 fn test_graph_event() {
828 let mut graph = BrailleGraph::new(vec![1.0]);
829 let event = Event::key_down(presentar_core::Key::Enter);
830 assert!(graph.event(&event).is_none());
831 }
832
833 #[test]
834 fn test_graph_children() {
835 let graph = BrailleGraph::new(vec![1.0]);
836 assert!(graph.children().is_empty());
837 }
838
839 #[test]
840 fn test_graph_children_mut() {
841 let mut graph = BrailleGraph::new(vec![1.0]);
842 assert!(graph.children_mut().is_empty());
843 }
844
845 #[test]
846 fn test_graph_type_id() {
847 let graph = BrailleGraph::new(vec![1.0]);
848 assert_eq!(Widget::type_id(&graph), TypeId::of::<BrailleGraph>());
849 }
850
851 #[test]
852 fn test_graph_brick_name() {
853 let graph = BrailleGraph::new(vec![1.0]);
854 assert_eq!(graph.brick_name(), "braille_graph");
855 }
856
857 #[test]
858 fn test_graph_budget() {
859 let graph = BrailleGraph::new(vec![1.0]);
860 let budget = graph.budget();
861 assert!(budget.measure_ms > 0);
862 }
863
864 #[test]
865 fn test_graph_to_html() {
866 let graph = BrailleGraph::new(vec![1.0]);
867 assert!(graph.to_html().is_empty());
868 }
869
870 #[test]
871 fn test_graph_to_css() {
872 let graph = BrailleGraph::new(vec![1.0]);
873 assert!(graph.to_css().is_empty());
874 }
875
876 #[test]
877 fn test_graph_mode_default() {
878 assert_eq!(GraphMode::default(), GraphMode::Braille);
879 }
880
881 #[test]
882 fn test_graph_large_dataset() {
883 let data: Vec<f64> = (0..1000).map(|i| (i as f64).sin()).collect();
884 let mut graph = BrailleGraph::new(data);
885 graph.bounds = Rect::new(0.0, 0.0, 50.0, 10.0);
886 let mut canvas = MockCanvas::new();
887 graph.paint(&mut canvas);
888 assert!(!canvas.texts.is_empty());
889 }
890
891 #[test]
892 fn test_graph_block_mode_various_values() {
893 let mut graph =
894 BrailleGraph::new(vec![0.0, 25.0, 50.0, 75.0, 100.0]).with_mode(GraphMode::Block);
895 graph.bounds = Rect::new(0.0, 0.0, 5.0, 4.0);
896 let mut canvas = MockCanvas::new();
897 graph.paint(&mut canvas);
898 assert!(!canvas.texts.is_empty());
899 }
900
901 #[test]
902 fn test_graph_tty_mode_various_values() {
903 let mut graph =
904 BrailleGraph::new(vec![0.0, 25.0, 50.0, 75.0, 100.0]).with_mode(GraphMode::Tty);
905 graph.bounds = Rect::new(0.0, 0.0, 5.0, 4.0);
906 let mut canvas = MockCanvas::new();
907 graph.paint(&mut canvas);
908 assert!(!canvas.texts.is_empty());
909 }
910
911 #[test]
916 fn test_graph_with_margins() {
917 let graph = BrailleGraph::new(vec![1.0, 2.0]).with_margins(AxisMargins::WIDE);
918 assert_eq!(graph.margins.y_axis_width, 10);
919 assert_eq!(graph.margins.x_axis_height, 2);
920 }
921
922 #[test]
923 fn test_graph_with_margins_none() {
924 let graph = BrailleGraph::new(vec![1.0, 2.0]).with_margins(AxisMargins::NONE);
925 assert_eq!(graph.margins.y_axis_width, 0);
926 assert_eq!(graph.margins.x_axis_height, 0);
927 }
928
929 #[test]
930 fn test_graph_with_margins_compact() {
931 let graph = BrailleGraph::new(vec![1.0, 2.0]).with_margins(AxisMargins::COMPACT);
932 assert_eq!(graph.margins.y_axis_width, 4);
933 assert_eq!(graph.margins.x_axis_height, 1);
934 }
935
936 #[test]
937 fn test_graph_with_margins_standard() {
938 let graph = BrailleGraph::new(vec![1.0, 2.0]).with_margins(AxisMargins::STANDARD);
939 assert_eq!(graph.margins.y_axis_width, 6);
940 assert_eq!(graph.margins.x_axis_height, 1);
941 }
942
943 #[test]
944 fn test_axis_margins_default() {
945 let margins = AxisMargins::default();
946 assert_eq!(margins.y_axis_width, 6);
947 assert_eq!(margins.x_axis_height, 1);
948 }
949
950 #[test]
951 fn test_graph_with_time_axis_indices() {
952 let graph = BrailleGraph::new(vec![1.0, 2.0]).with_time_axis(TimeAxisMode::Indices);
953 assert_eq!(graph.time_axis, TimeAxisMode::Indices);
954 }
955
956 #[test]
957 fn test_graph_with_time_axis_relative() {
958 let graph = BrailleGraph::new(vec![1.0, 2.0])
959 .with_time_axis(TimeAxisMode::Relative { interval_secs: 5 });
960 match graph.time_axis {
961 TimeAxisMode::Relative { interval_secs } => assert_eq!(interval_secs, 5),
962 _ => panic!("Expected Relative time axis mode"),
963 }
964 }
965
966 #[test]
967 fn test_graph_with_time_axis_absolute() {
968 let graph = BrailleGraph::new(vec![1.0, 2.0]).with_time_axis(TimeAxisMode::Absolute);
969 assert_eq!(graph.time_axis, TimeAxisMode::Absolute);
970 }
971
972 #[test]
973 fn test_graph_with_time_axis_hidden() {
974 let graph = BrailleGraph::new(vec![1.0, 2.0]).with_time_axis(TimeAxisMode::Hidden);
975 assert_eq!(graph.time_axis, TimeAxisMode::Hidden);
976 }
977
978 #[test]
979 fn test_time_axis_mode_default() {
980 assert_eq!(TimeAxisMode::default(), TimeAxisMode::Indices);
981 }
982
983 #[test]
984 fn test_time_axis_format_label_indices() {
985 let mode = TimeAxisMode::Indices;
986 assert_eq!(mode.format_label(0, 10), Some("0".to_string()));
987 assert_eq!(mode.format_label(5, 10), Some("5".to_string()));
988 assert_eq!(mode.format_label(9, 10), Some("9".to_string()));
989 }
990
991 #[test]
992 fn test_time_axis_format_label_relative_seconds() {
993 let mode = TimeAxisMode::Relative { interval_secs: 1 };
994 assert_eq!(mode.format_label(0, 60), Some("1m".to_string()));
996 assert_eq!(mode.format_label(59, 60), Some("1s".to_string()));
998 assert_eq!(mode.format_label(30, 60), Some("30s".to_string()));
1000 }
1001
1002 #[test]
1003 fn test_time_axis_format_label_relative_minutes() {
1004 let mode = TimeAxisMode::Relative { interval_secs: 60 };
1005 assert_eq!(mode.format_label(0, 10), Some("10m".to_string()));
1007 assert_eq!(mode.format_label(5, 10), Some("5m".to_string()));
1009 }
1010
1011 #[test]
1012 fn test_time_axis_format_label_relative_hours() {
1013 let mode = TimeAxisMode::Relative {
1014 interval_secs: 3600,
1015 };
1016 assert_eq!(mode.format_label(0, 5), Some("5h".to_string()));
1018 assert_eq!(mode.format_label(3, 5), Some("2h".to_string()));
1020 }
1021
1022 #[test]
1023 fn test_time_axis_format_label_absolute() {
1024 let mode = TimeAxisMode::Absolute;
1025 assert_eq!(mode.format_label(0, 10), None);
1026 }
1027
1028 #[test]
1029 fn test_time_axis_format_label_hidden() {
1030 let mode = TimeAxisMode::Hidden;
1031 assert_eq!(mode.format_label(0, 10), None);
1032 }
1033
1034 #[test]
1035 fn test_graph_with_legend() {
1036 let graph = BrailleGraph::new(vec![1.0, 2.0]).with_legend(true);
1037 assert!(graph.show_legend);
1038 }
1039
1040 #[test]
1041 fn test_graph_with_legend_disabled() {
1042 let graph = BrailleGraph::new(vec![1.0, 2.0]).with_legend(false);
1043 assert!(!graph.show_legend);
1044 }
1045
1046 #[test]
1047 fn test_graph_with_gradient() {
1048 let gradient = Gradient::two(Color::BLUE, Color::RED);
1049 let graph = BrailleGraph::new(vec![1.0, 2.0]).with_gradient(gradient);
1050 assert!(graph.gradient.is_some());
1051 }
1052
1053 #[test]
1054 fn test_graph_color_for_value_without_gradient() {
1055 let graph = BrailleGraph::new(vec![0.0, 100.0]).with_color(Color::GREEN);
1056 let color = graph.color_for_value(0.5);
1057 assert_eq!(color, Color::GREEN);
1058 }
1059
1060 #[test]
1061 fn test_graph_color_for_value_with_gradient() {
1062 let gradient = Gradient::two(Color::BLUE, Color::RED);
1063 let graph = BrailleGraph::new(vec![0.0, 100.0]).with_gradient(gradient);
1064 let color_low = graph.color_for_value(0.0);
1066 let color_high = graph.color_for_value(1.0);
1067 assert_ne!(color_low, color_high);
1069 }
1070
1071 #[test]
1072 fn test_graph_area_with_margins() {
1073 let mut graph = BrailleGraph::new(vec![1.0, 2.0]).with_margins(AxisMargins::STANDARD);
1074 graph.bounds = Rect::new(0.0, 0.0, 80.0, 24.0);
1075 let area = graph.graph_area();
1076 assert_eq!(area.x, 6.0);
1078 assert_eq!(area.height, 23.0);
1080 assert_eq!(area.width, 74.0);
1081 }
1082
1083 #[test]
1084 fn test_graph_area_with_no_margins() {
1085 let mut graph = BrailleGraph::new(vec![1.0, 2.0]).with_margins(AxisMargins::NONE);
1086 graph.bounds = Rect::new(0.0, 0.0, 80.0, 24.0);
1087 let area = graph.graph_area();
1088 assert_eq!(area.x, 0.0);
1089 assert_eq!(area.y, 0.0);
1090 assert_eq!(area.width, 80.0);
1091 assert_eq!(area.height, 24.0);
1092 }
1093
1094 #[test]
1095 fn test_graph_paint_with_y_axis() {
1096 let mut graph =
1097 BrailleGraph::new(vec![0.0, 50.0, 100.0]).with_margins(AxisMargins::STANDARD);
1098 graph.bounds = Rect::new(0.0, 0.0, 80.0, 10.0);
1099 let mut canvas = MockCanvas::new();
1100 graph.paint(&mut canvas);
1101 let has_max_label = canvas.texts.iter().any(|(t, _)| t.contains("100"));
1103 let has_min_label = canvas.texts.iter().any(|(t, _)| t.contains('0'));
1104 assert!(has_max_label || has_min_label);
1105 }
1106
1107 #[test]
1108 fn test_graph_paint_with_x_axis_indices() {
1109 let mut graph = BrailleGraph::new(vec![0.0, 50.0, 100.0])
1110 .with_margins(AxisMargins::STANDARD)
1111 .with_time_axis(TimeAxisMode::Indices);
1112 graph.bounds = Rect::new(0.0, 0.0, 80.0, 10.0);
1113 let mut canvas = MockCanvas::new();
1114 graph.paint(&mut canvas);
1115 assert!(!canvas.texts.is_empty());
1117 }
1118
1119 #[test]
1120 fn test_graph_paint_with_x_axis_hidden() {
1121 let mut graph = BrailleGraph::new(vec![0.0, 50.0, 100.0])
1122 .with_margins(AxisMargins::STANDARD)
1123 .with_time_axis(TimeAxisMode::Hidden);
1124 graph.bounds = Rect::new(0.0, 0.0, 80.0, 10.0);
1125 let mut canvas = MockCanvas::new();
1126 graph.paint(&mut canvas);
1127 assert!(!canvas.texts.is_empty());
1129 }
1130
1131 #[test]
1132 fn test_graph_paint_with_legend() {
1133 let mut graph = BrailleGraph::new(vec![0.0, 100.0])
1134 .with_legend(true)
1135 .with_margins(AxisMargins::STANDARD);
1136 graph.bounds = Rect::new(0.0, 0.0, 80.0, 10.0);
1137 let mut canvas = MockCanvas::new();
1138 graph.paint(&mut canvas);
1139 let has_legend = canvas
1141 .texts
1142 .iter()
1143 .any(|(t, _)| t.contains("⣿") || t.contains("⣀"));
1144 assert!(has_legend);
1145 }
1146
1147 #[test]
1148 fn test_graph_paint_without_legend() {
1149 let mut graph = BrailleGraph::new(vec![0.0, 100.0])
1150 .with_legend(false)
1151 .with_margins(AxisMargins::NONE);
1152 graph.bounds = Rect::new(0.0, 0.0, 80.0, 10.0);
1153 let mut canvas = MockCanvas::new();
1154 graph.paint(&mut canvas);
1155 let has_legend = canvas
1157 .texts
1158 .iter()
1159 .any(|(t, _)| t.contains("⣿=") || t.contains("⣀="));
1160 assert!(!has_legend);
1161 }
1162
1163 #[test]
1164 fn test_graph_paint_with_no_y_axis_margin() {
1165 let mut graph = BrailleGraph::new(vec![0.0, 100.0]).with_margins(AxisMargins::NONE);
1166 graph.bounds = Rect::new(0.0, 0.0, 80.0, 10.0);
1167 let mut canvas = MockCanvas::new();
1168 graph.paint(&mut canvas);
1169 assert!(!canvas.texts.is_empty());
1171 }
1172
1173 #[test]
1174 fn test_graph_paint_with_gradient_braille() {
1175 let gradient = Gradient::two(Color::BLUE, Color::RED);
1176 let mut graph = BrailleGraph::new(vec![0.0, 50.0, 100.0])
1177 .with_gradient(gradient)
1178 .with_mode(GraphMode::Braille)
1179 .with_margins(AxisMargins::NONE);
1180 graph.bounds = Rect::new(0.0, 0.0, 10.0, 5.0);
1181 let mut canvas = MockCanvas::new();
1182 graph.paint(&mut canvas);
1183 assert!(!canvas.texts.is_empty());
1184 }
1185
1186 #[test]
1187 fn test_graph_paint_with_gradient_block() {
1188 let gradient = Gradient::two(Color::BLUE, Color::RED);
1189 let mut graph = BrailleGraph::new(vec![0.0, 50.0, 100.0])
1190 .with_gradient(gradient)
1191 .with_mode(GraphMode::Block)
1192 .with_margins(AxisMargins::NONE);
1193 graph.bounds = Rect::new(0.0, 0.0, 10.0, 5.0);
1194 let mut canvas = MockCanvas::new();
1195 graph.paint(&mut canvas);
1196 assert!(!canvas.texts.is_empty());
1197 }
1198
1199 #[test]
1200 fn test_graph_paint_with_gradient_tty() {
1201 let gradient = Gradient::two(Color::BLUE, Color::RED);
1202 let mut graph = BrailleGraph::new(vec![0.0, 50.0, 100.0])
1203 .with_gradient(gradient)
1204 .with_mode(GraphMode::Tty)
1205 .with_margins(AxisMargins::NONE);
1206 graph.bounds = Rect::new(0.0, 0.0, 10.0, 5.0);
1207 let mut canvas = MockCanvas::new();
1208 graph.paint(&mut canvas);
1209 assert!(!canvas.texts.is_empty());
1210 }
1211
1212 #[test]
1213 fn test_graph_block_mode_single_point() {
1214 let mut graph = BrailleGraph::new(vec![50.0]).with_mode(GraphMode::Block);
1215 graph.bounds = Rect::new(0.0, 0.0, 5.0, 4.0);
1216 let mut canvas = MockCanvas::new();
1217 graph.paint(&mut canvas);
1218 assert!(!canvas.texts.is_empty());
1219 }
1220
1221 #[test]
1222 fn test_graph_tty_mode_single_point() {
1223 let mut graph = BrailleGraph::new(vec![50.0]).with_mode(GraphMode::Tty);
1224 graph.bounds = Rect::new(0.0, 0.0, 5.0, 4.0);
1225 let mut canvas = MockCanvas::new();
1226 graph.paint(&mut canvas);
1227 assert!(!canvas.texts.is_empty());
1228 }
1229
1230 #[test]
1231 fn test_graph_braille_more_data_than_width() {
1232 let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
1233 let mut graph = BrailleGraph::new(data)
1234 .with_mode(GraphMode::Braille)
1235 .with_margins(AxisMargins::NONE);
1236 graph.bounds = Rect::new(0.0, 0.0, 10.0, 5.0);
1237 let mut canvas = MockCanvas::new();
1238 graph.paint(&mut canvas);
1239 assert!(!canvas.texts.is_empty());
1240 }
1241
1242 #[test]
1243 fn test_graph_block_more_data_than_width() {
1244 let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
1245 let mut graph = BrailleGraph::new(data)
1246 .with_mode(GraphMode::Block)
1247 .with_margins(AxisMargins::NONE);
1248 graph.bounds = Rect::new(0.0, 0.0, 10.0, 5.0);
1249 let mut canvas = MockCanvas::new();
1250 graph.paint(&mut canvas);
1251 assert!(!canvas.texts.is_empty());
1252 }
1253
1254 #[test]
1255 fn test_graph_tty_more_data_than_width() {
1256 let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
1257 let mut graph = BrailleGraph::new(data)
1258 .with_mode(GraphMode::Tty)
1259 .with_margins(AxisMargins::NONE);
1260 graph.bounds = Rect::new(0.0, 0.0, 10.0, 5.0);
1261 let mut canvas = MockCanvas::new();
1262 graph.paint(&mut canvas);
1263 assert!(!canvas.texts.is_empty());
1264 }
1265
1266 #[test]
1267 fn test_graph_small_bounds_clipping() {
1268 let mut graph = BrailleGraph::new(vec![0.0, 100.0]).with_margins(AxisMargins::WIDE);
1270 graph.bounds = Rect::new(0.0, 0.0, 5.0, 2.0);
1271 let area = graph.graph_area();
1272 assert!(area.width >= 0.0);
1274 assert!(area.height >= 0.0);
1275 }
1276
1277 #[test]
1278 fn test_graph_x_axis_single_data_point() {
1279 let mut graph = BrailleGraph::new(vec![50.0])
1280 .with_margins(AxisMargins::STANDARD)
1281 .with_time_axis(TimeAxisMode::Indices);
1282 graph.bounds = Rect::new(0.0, 0.0, 80.0, 10.0);
1283 let mut canvas = MockCanvas::new();
1284 graph.paint(&mut canvas);
1285 assert!(!canvas.texts.is_empty());
1287 }
1288
1289 #[test]
1290 fn test_graph_mode_debug() {
1291 let mode = GraphMode::Braille;
1293 let debug_str = format!("{:?}", mode);
1294 assert!(debug_str.contains("Braille"));
1295 }
1296
1297 #[test]
1298 fn test_time_axis_mode_debug() {
1299 let mode = TimeAxisMode::Relative { interval_secs: 60 };
1301 let debug_str = format!("{:?}", mode);
1302 assert!(debug_str.contains("Relative"));
1303 assert!(debug_str.contains("60"));
1304 }
1305
1306 #[test]
1307 fn test_axis_margins_debug() {
1308 let margins = AxisMargins::WIDE;
1310 let debug_str = format!("{:?}", margins);
1311 assert!(debug_str.contains("10")); assert!(debug_str.contains('2')); }
1314
1315 #[test]
1316 fn test_graph_clone() {
1317 let graph = BrailleGraph::new(vec![1.0, 2.0, 3.0])
1318 .with_color(Color::RED)
1319 .with_label("Test")
1320 .with_range(0.0, 100.0);
1321 let cloned = graph.clone();
1322 assert_eq!(cloned.data, graph.data);
1323 assert_eq!(cloned.color, graph.color);
1324 assert_eq!(cloned.label, graph.label);
1325 assert_eq!(cloned.min, graph.min);
1326 assert_eq!(cloned.max, graph.max);
1327 }
1328
1329 #[test]
1330 fn test_graph_debug() {
1331 let graph = BrailleGraph::new(vec![1.0, 2.0]);
1332 let debug_str = format!("{:?}", graph);
1333 assert!(debug_str.contains("BrailleGraph"));
1334 }
1335}