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::KeyDown {
830 key: presentar_core::Key::Enter,
831 };
832 assert!(graph.event(&event).is_none());
833 }
834
835 #[test]
836 fn test_graph_children() {
837 let graph = BrailleGraph::new(vec![1.0]);
838 assert!(graph.children().is_empty());
839 }
840
841 #[test]
842 fn test_graph_children_mut() {
843 let mut graph = BrailleGraph::new(vec![1.0]);
844 assert!(graph.children_mut().is_empty());
845 }
846
847 #[test]
848 fn test_graph_type_id() {
849 let graph = BrailleGraph::new(vec![1.0]);
850 assert_eq!(Widget::type_id(&graph), TypeId::of::<BrailleGraph>());
851 }
852
853 #[test]
854 fn test_graph_brick_name() {
855 let graph = BrailleGraph::new(vec![1.0]);
856 assert_eq!(graph.brick_name(), "braille_graph");
857 }
858
859 #[test]
860 fn test_graph_budget() {
861 let graph = BrailleGraph::new(vec![1.0]);
862 let budget = graph.budget();
863 assert!(budget.measure_ms > 0);
864 }
865
866 #[test]
867 fn test_graph_to_html() {
868 let graph = BrailleGraph::new(vec![1.0]);
869 assert!(graph.to_html().is_empty());
870 }
871
872 #[test]
873 fn test_graph_to_css() {
874 let graph = BrailleGraph::new(vec![1.0]);
875 assert!(graph.to_css().is_empty());
876 }
877
878 #[test]
879 fn test_graph_mode_default() {
880 assert_eq!(GraphMode::default(), GraphMode::Braille);
881 }
882
883 #[test]
884 fn test_graph_large_dataset() {
885 let data: Vec<f64> = (0..1000).map(|i| (i as f64).sin()).collect();
886 let mut graph = BrailleGraph::new(data);
887 graph.bounds = Rect::new(0.0, 0.0, 50.0, 10.0);
888 let mut canvas = MockCanvas::new();
889 graph.paint(&mut canvas);
890 assert!(!canvas.texts.is_empty());
891 }
892
893 #[test]
894 fn test_graph_block_mode_various_values() {
895 let mut graph =
896 BrailleGraph::new(vec![0.0, 25.0, 50.0, 75.0, 100.0]).with_mode(GraphMode::Block);
897 graph.bounds = Rect::new(0.0, 0.0, 5.0, 4.0);
898 let mut canvas = MockCanvas::new();
899 graph.paint(&mut canvas);
900 assert!(!canvas.texts.is_empty());
901 }
902
903 #[test]
904 fn test_graph_tty_mode_various_values() {
905 let mut graph =
906 BrailleGraph::new(vec![0.0, 25.0, 50.0, 75.0, 100.0]).with_mode(GraphMode::Tty);
907 graph.bounds = Rect::new(0.0, 0.0, 5.0, 4.0);
908 let mut canvas = MockCanvas::new();
909 graph.paint(&mut canvas);
910 assert!(!canvas.texts.is_empty());
911 }
912
913 #[test]
918 fn test_graph_with_margins() {
919 let graph = BrailleGraph::new(vec![1.0, 2.0]).with_margins(AxisMargins::WIDE);
920 assert_eq!(graph.margins.y_axis_width, 10);
921 assert_eq!(graph.margins.x_axis_height, 2);
922 }
923
924 #[test]
925 fn test_graph_with_margins_none() {
926 let graph = BrailleGraph::new(vec![1.0, 2.0]).with_margins(AxisMargins::NONE);
927 assert_eq!(graph.margins.y_axis_width, 0);
928 assert_eq!(graph.margins.x_axis_height, 0);
929 }
930
931 #[test]
932 fn test_graph_with_margins_compact() {
933 let graph = BrailleGraph::new(vec![1.0, 2.0]).with_margins(AxisMargins::COMPACT);
934 assert_eq!(graph.margins.y_axis_width, 4);
935 assert_eq!(graph.margins.x_axis_height, 1);
936 }
937
938 #[test]
939 fn test_graph_with_margins_standard() {
940 let graph = BrailleGraph::new(vec![1.0, 2.0]).with_margins(AxisMargins::STANDARD);
941 assert_eq!(graph.margins.y_axis_width, 6);
942 assert_eq!(graph.margins.x_axis_height, 1);
943 }
944
945 #[test]
946 fn test_axis_margins_default() {
947 let margins = AxisMargins::default();
948 assert_eq!(margins.y_axis_width, 6);
949 assert_eq!(margins.x_axis_height, 1);
950 }
951
952 #[test]
953 fn test_graph_with_time_axis_indices() {
954 let graph = BrailleGraph::new(vec![1.0, 2.0]).with_time_axis(TimeAxisMode::Indices);
955 assert_eq!(graph.time_axis, TimeAxisMode::Indices);
956 }
957
958 #[test]
959 fn test_graph_with_time_axis_relative() {
960 let graph = BrailleGraph::new(vec![1.0, 2.0])
961 .with_time_axis(TimeAxisMode::Relative { interval_secs: 5 });
962 match graph.time_axis {
963 TimeAxisMode::Relative { interval_secs } => assert_eq!(interval_secs, 5),
964 _ => panic!("Expected Relative time axis mode"),
965 }
966 }
967
968 #[test]
969 fn test_graph_with_time_axis_absolute() {
970 let graph = BrailleGraph::new(vec![1.0, 2.0]).with_time_axis(TimeAxisMode::Absolute);
971 assert_eq!(graph.time_axis, TimeAxisMode::Absolute);
972 }
973
974 #[test]
975 fn test_graph_with_time_axis_hidden() {
976 let graph = BrailleGraph::new(vec![1.0, 2.0]).with_time_axis(TimeAxisMode::Hidden);
977 assert_eq!(graph.time_axis, TimeAxisMode::Hidden);
978 }
979
980 #[test]
981 fn test_time_axis_mode_default() {
982 assert_eq!(TimeAxisMode::default(), TimeAxisMode::Indices);
983 }
984
985 #[test]
986 fn test_time_axis_format_label_indices() {
987 let mode = TimeAxisMode::Indices;
988 assert_eq!(mode.format_label(0, 10), Some("0".to_string()));
989 assert_eq!(mode.format_label(5, 10), Some("5".to_string()));
990 assert_eq!(mode.format_label(9, 10), Some("9".to_string()));
991 }
992
993 #[test]
994 fn test_time_axis_format_label_relative_seconds() {
995 let mode = TimeAxisMode::Relative { interval_secs: 1 };
996 assert_eq!(mode.format_label(0, 60), Some("1m".to_string()));
998 assert_eq!(mode.format_label(59, 60), Some("1s".to_string()));
1000 assert_eq!(mode.format_label(30, 60), Some("30s".to_string()));
1002 }
1003
1004 #[test]
1005 fn test_time_axis_format_label_relative_minutes() {
1006 let mode = TimeAxisMode::Relative { interval_secs: 60 };
1007 assert_eq!(mode.format_label(0, 10), Some("10m".to_string()));
1009 assert_eq!(mode.format_label(5, 10), Some("5m".to_string()));
1011 }
1012
1013 #[test]
1014 fn test_time_axis_format_label_relative_hours() {
1015 let mode = TimeAxisMode::Relative {
1016 interval_secs: 3600,
1017 };
1018 assert_eq!(mode.format_label(0, 5), Some("5h".to_string()));
1020 assert_eq!(mode.format_label(3, 5), Some("2h".to_string()));
1022 }
1023
1024 #[test]
1025 fn test_time_axis_format_label_absolute() {
1026 let mode = TimeAxisMode::Absolute;
1027 assert_eq!(mode.format_label(0, 10), None);
1028 }
1029
1030 #[test]
1031 fn test_time_axis_format_label_hidden() {
1032 let mode = TimeAxisMode::Hidden;
1033 assert_eq!(mode.format_label(0, 10), None);
1034 }
1035
1036 #[test]
1037 fn test_graph_with_legend() {
1038 let graph = BrailleGraph::new(vec![1.0, 2.0]).with_legend(true);
1039 assert!(graph.show_legend);
1040 }
1041
1042 #[test]
1043 fn test_graph_with_legend_disabled() {
1044 let graph = BrailleGraph::new(vec![1.0, 2.0]).with_legend(false);
1045 assert!(!graph.show_legend);
1046 }
1047
1048 #[test]
1049 fn test_graph_with_gradient() {
1050 let gradient = Gradient::two(Color::BLUE, Color::RED);
1051 let graph = BrailleGraph::new(vec![1.0, 2.0]).with_gradient(gradient);
1052 assert!(graph.gradient.is_some());
1053 }
1054
1055 #[test]
1056 fn test_graph_color_for_value_without_gradient() {
1057 let graph = BrailleGraph::new(vec![0.0, 100.0]).with_color(Color::GREEN);
1058 let color = graph.color_for_value(0.5);
1059 assert_eq!(color, Color::GREEN);
1060 }
1061
1062 #[test]
1063 fn test_graph_color_for_value_with_gradient() {
1064 let gradient = Gradient::two(Color::BLUE, Color::RED);
1065 let graph = BrailleGraph::new(vec![0.0, 100.0]).with_gradient(gradient);
1066 let color_low = graph.color_for_value(0.0);
1068 let color_high = graph.color_for_value(1.0);
1069 assert_ne!(color_low, color_high);
1071 }
1072
1073 #[test]
1074 fn test_graph_area_with_margins() {
1075 let mut graph = BrailleGraph::new(vec![1.0, 2.0]).with_margins(AxisMargins::STANDARD);
1076 graph.bounds = Rect::new(0.0, 0.0, 80.0, 24.0);
1077 let area = graph.graph_area();
1078 assert_eq!(area.x, 6.0);
1080 assert_eq!(area.height, 23.0);
1082 assert_eq!(area.width, 74.0);
1083 }
1084
1085 #[test]
1086 fn test_graph_area_with_no_margins() {
1087 let mut graph = BrailleGraph::new(vec![1.0, 2.0]).with_margins(AxisMargins::NONE);
1088 graph.bounds = Rect::new(0.0, 0.0, 80.0, 24.0);
1089 let area = graph.graph_area();
1090 assert_eq!(area.x, 0.0);
1091 assert_eq!(area.y, 0.0);
1092 assert_eq!(area.width, 80.0);
1093 assert_eq!(area.height, 24.0);
1094 }
1095
1096 #[test]
1097 fn test_graph_paint_with_y_axis() {
1098 let mut graph =
1099 BrailleGraph::new(vec![0.0, 50.0, 100.0]).with_margins(AxisMargins::STANDARD);
1100 graph.bounds = Rect::new(0.0, 0.0, 80.0, 10.0);
1101 let mut canvas = MockCanvas::new();
1102 graph.paint(&mut canvas);
1103 let has_max_label = canvas.texts.iter().any(|(t, _)| t.contains("100"));
1105 let has_min_label = canvas.texts.iter().any(|(t, _)| t.contains("0"));
1106 assert!(has_max_label || has_min_label);
1107 }
1108
1109 #[test]
1110 fn test_graph_paint_with_x_axis_indices() {
1111 let mut graph = BrailleGraph::new(vec![0.0, 50.0, 100.0])
1112 .with_margins(AxisMargins::STANDARD)
1113 .with_time_axis(TimeAxisMode::Indices);
1114 graph.bounds = Rect::new(0.0, 0.0, 80.0, 10.0);
1115 let mut canvas = MockCanvas::new();
1116 graph.paint(&mut canvas);
1117 assert!(!canvas.texts.is_empty());
1119 }
1120
1121 #[test]
1122 fn test_graph_paint_with_x_axis_hidden() {
1123 let mut graph = BrailleGraph::new(vec![0.0, 50.0, 100.0])
1124 .with_margins(AxisMargins::STANDARD)
1125 .with_time_axis(TimeAxisMode::Hidden);
1126 graph.bounds = Rect::new(0.0, 0.0, 80.0, 10.0);
1127 let mut canvas = MockCanvas::new();
1128 graph.paint(&mut canvas);
1129 assert!(!canvas.texts.is_empty());
1131 }
1132
1133 #[test]
1134 fn test_graph_paint_with_legend() {
1135 let mut graph = BrailleGraph::new(vec![0.0, 100.0])
1136 .with_legend(true)
1137 .with_margins(AxisMargins::STANDARD);
1138 graph.bounds = Rect::new(0.0, 0.0, 80.0, 10.0);
1139 let mut canvas = MockCanvas::new();
1140 graph.paint(&mut canvas);
1141 let has_legend = canvas
1143 .texts
1144 .iter()
1145 .any(|(t, _)| t.contains("⣿") || t.contains("⣀"));
1146 assert!(has_legend);
1147 }
1148
1149 #[test]
1150 fn test_graph_paint_without_legend() {
1151 let mut graph = BrailleGraph::new(vec![0.0, 100.0])
1152 .with_legend(false)
1153 .with_margins(AxisMargins::NONE);
1154 graph.bounds = Rect::new(0.0, 0.0, 80.0, 10.0);
1155 let mut canvas = MockCanvas::new();
1156 graph.paint(&mut canvas);
1157 let has_legend = canvas
1159 .texts
1160 .iter()
1161 .any(|(t, _)| t.contains("⣿=") || t.contains("⣀="));
1162 assert!(!has_legend);
1163 }
1164
1165 #[test]
1166 fn test_graph_paint_with_no_y_axis_margin() {
1167 let mut graph = BrailleGraph::new(vec![0.0, 100.0]).with_margins(AxisMargins::NONE);
1168 graph.bounds = Rect::new(0.0, 0.0, 80.0, 10.0);
1169 let mut canvas = MockCanvas::new();
1170 graph.paint(&mut canvas);
1171 assert!(!canvas.texts.is_empty());
1173 }
1174
1175 #[test]
1176 fn test_graph_paint_with_gradient_braille() {
1177 let gradient = Gradient::two(Color::BLUE, Color::RED);
1178 let mut graph = BrailleGraph::new(vec![0.0, 50.0, 100.0])
1179 .with_gradient(gradient)
1180 .with_mode(GraphMode::Braille)
1181 .with_margins(AxisMargins::NONE);
1182 graph.bounds = Rect::new(0.0, 0.0, 10.0, 5.0);
1183 let mut canvas = MockCanvas::new();
1184 graph.paint(&mut canvas);
1185 assert!(!canvas.texts.is_empty());
1186 }
1187
1188 #[test]
1189 fn test_graph_paint_with_gradient_block() {
1190 let gradient = Gradient::two(Color::BLUE, Color::RED);
1191 let mut graph = BrailleGraph::new(vec![0.0, 50.0, 100.0])
1192 .with_gradient(gradient)
1193 .with_mode(GraphMode::Block)
1194 .with_margins(AxisMargins::NONE);
1195 graph.bounds = Rect::new(0.0, 0.0, 10.0, 5.0);
1196 let mut canvas = MockCanvas::new();
1197 graph.paint(&mut canvas);
1198 assert!(!canvas.texts.is_empty());
1199 }
1200
1201 #[test]
1202 fn test_graph_paint_with_gradient_tty() {
1203 let gradient = Gradient::two(Color::BLUE, Color::RED);
1204 let mut graph = BrailleGraph::new(vec![0.0, 50.0, 100.0])
1205 .with_gradient(gradient)
1206 .with_mode(GraphMode::Tty)
1207 .with_margins(AxisMargins::NONE);
1208 graph.bounds = Rect::new(0.0, 0.0, 10.0, 5.0);
1209 let mut canvas = MockCanvas::new();
1210 graph.paint(&mut canvas);
1211 assert!(!canvas.texts.is_empty());
1212 }
1213
1214 #[test]
1215 fn test_graph_block_mode_single_point() {
1216 let mut graph = BrailleGraph::new(vec![50.0]).with_mode(GraphMode::Block);
1217 graph.bounds = Rect::new(0.0, 0.0, 5.0, 4.0);
1218 let mut canvas = MockCanvas::new();
1219 graph.paint(&mut canvas);
1220 assert!(!canvas.texts.is_empty());
1221 }
1222
1223 #[test]
1224 fn test_graph_tty_mode_single_point() {
1225 let mut graph = BrailleGraph::new(vec![50.0]).with_mode(GraphMode::Tty);
1226 graph.bounds = Rect::new(0.0, 0.0, 5.0, 4.0);
1227 let mut canvas = MockCanvas::new();
1228 graph.paint(&mut canvas);
1229 assert!(!canvas.texts.is_empty());
1230 }
1231
1232 #[test]
1233 fn test_graph_braille_more_data_than_width() {
1234 let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
1235 let mut graph = BrailleGraph::new(data)
1236 .with_mode(GraphMode::Braille)
1237 .with_margins(AxisMargins::NONE);
1238 graph.bounds = Rect::new(0.0, 0.0, 10.0, 5.0);
1239 let mut canvas = MockCanvas::new();
1240 graph.paint(&mut canvas);
1241 assert!(!canvas.texts.is_empty());
1242 }
1243
1244 #[test]
1245 fn test_graph_block_more_data_than_width() {
1246 let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
1247 let mut graph = BrailleGraph::new(data)
1248 .with_mode(GraphMode::Block)
1249 .with_margins(AxisMargins::NONE);
1250 graph.bounds = Rect::new(0.0, 0.0, 10.0, 5.0);
1251 let mut canvas = MockCanvas::new();
1252 graph.paint(&mut canvas);
1253 assert!(!canvas.texts.is_empty());
1254 }
1255
1256 #[test]
1257 fn test_graph_tty_more_data_than_width() {
1258 let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
1259 let mut graph = BrailleGraph::new(data)
1260 .with_mode(GraphMode::Tty)
1261 .with_margins(AxisMargins::NONE);
1262 graph.bounds = Rect::new(0.0, 0.0, 10.0, 5.0);
1263 let mut canvas = MockCanvas::new();
1264 graph.paint(&mut canvas);
1265 assert!(!canvas.texts.is_empty());
1266 }
1267
1268 #[test]
1269 fn test_graph_small_bounds_clipping() {
1270 let mut graph = BrailleGraph::new(vec![0.0, 100.0]).with_margins(AxisMargins::WIDE);
1272 graph.bounds = Rect::new(0.0, 0.0, 5.0, 2.0);
1273 let area = graph.graph_area();
1274 assert!(area.width >= 0.0);
1276 assert!(area.height >= 0.0);
1277 }
1278
1279 #[test]
1280 fn test_graph_x_axis_single_data_point() {
1281 let mut graph = BrailleGraph::new(vec![50.0])
1282 .with_margins(AxisMargins::STANDARD)
1283 .with_time_axis(TimeAxisMode::Indices);
1284 graph.bounds = Rect::new(0.0, 0.0, 80.0, 10.0);
1285 let mut canvas = MockCanvas::new();
1286 graph.paint(&mut canvas);
1287 assert!(!canvas.texts.is_empty());
1289 }
1290
1291 #[test]
1292 fn test_graph_mode_debug() {
1293 let mode = GraphMode::Braille;
1295 let debug_str = format!("{:?}", mode);
1296 assert!(debug_str.contains("Braille"));
1297 }
1298
1299 #[test]
1300 fn test_time_axis_mode_debug() {
1301 let mode = TimeAxisMode::Relative { interval_secs: 60 };
1303 let debug_str = format!("{:?}", mode);
1304 assert!(debug_str.contains("Relative"));
1305 assert!(debug_str.contains("60"));
1306 }
1307
1308 #[test]
1309 fn test_axis_margins_debug() {
1310 let margins = AxisMargins::WIDE;
1312 let debug_str = format!("{:?}", margins);
1313 assert!(debug_str.contains("10")); assert!(debug_str.contains("2")); }
1316
1317 #[test]
1318 fn test_graph_clone() {
1319 let graph = BrailleGraph::new(vec![1.0, 2.0, 3.0])
1320 .with_color(Color::RED)
1321 .with_label("Test")
1322 .with_range(0.0, 100.0);
1323 let cloned = graph.clone();
1324 assert_eq!(cloned.data, graph.data);
1325 assert_eq!(cloned.color, graph.color);
1326 assert_eq!(cloned.label, graph.label);
1327 assert_eq!(cloned.min, graph.min);
1328 assert_eq!(cloned.max, graph.max);
1329 }
1330
1331 #[test]
1332 fn test_graph_debug() {
1333 let graph = BrailleGraph::new(vec![1.0, 2.0]);
1334 let debug_str = format!("{:?}", graph);
1335 assert!(debug_str.contains("BrailleGraph"));
1336 }
1337}