1use crate::widgets::symbols::BRAILLE_UP;
6use presentar_core::{
7 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
8 LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
9};
10use std::any::Any;
11use std::time::Duration;
12
13#[derive(Debug, Clone, Copy, Default)]
15pub enum Simplification {
16 #[default]
18 None,
19 DouglasPeucker { epsilon: f64 },
21 VisvalingamWhyatt { threshold: f64 },
23}
24
25#[derive(Debug, Clone)]
27pub struct Series {
28 pub name: String,
30 pub data: Vec<(f64, f64)>,
32 pub color: Color,
34 pub style: LineStyle,
36}
37
38#[derive(Debug, Clone, Copy, Default)]
40pub enum LineStyle {
41 #[default]
43 Solid,
44 Dashed,
46 Dotted,
48 Markers,
50}
51
52#[derive(Debug, Clone)]
54pub struct Axis {
55 pub label: Option<String>,
57 pub min: Option<f64>,
59 pub max: Option<f64>,
61 pub ticks: usize,
63 pub grid: bool,
65}
66
67impl Default for Axis {
68 fn default() -> Self {
69 Self {
70 label: None,
71 min: None,
72 max: None,
73 ticks: 5,
74 grid: false,
75 }
76 }
77}
78
79#[derive(Debug, Clone, Copy, Default)]
81pub enum LegendPosition {
82 #[default]
84 TopRight,
85 TopLeft,
87 BottomRight,
89 BottomLeft,
91 None,
93}
94
95#[derive(Debug, Clone)]
97pub struct LineChart {
98 series: Vec<Series>,
99 x_axis: Axis,
100 y_axis: Axis,
101 legend: LegendPosition,
102 simplification: Simplification,
103 bounds: Rect,
104 margin_left: f32,
106 margin_bottom: f32,
107}
108
109impl LineChart {
110 #[must_use]
112 pub fn new() -> Self {
113 Self {
114 series: Vec::new(),
115 x_axis: Axis::default(),
116 y_axis: Axis::default(),
117 legend: LegendPosition::default(),
118 simplification: Simplification::default(),
119 bounds: Rect::default(),
120 margin_left: 6.0,
121 margin_bottom: 2.0,
122 }
123 }
124
125 #[must_use]
127 pub fn add_series(mut self, name: &str, data: Vec<(f64, f64)>, color: Color) -> Self {
128 self.series.push(Series {
129 name: name.to_string(),
130 data,
131 color,
132 style: LineStyle::default(),
133 });
134 self
135 }
136
137 #[must_use]
139 pub fn add_series_styled(
140 mut self,
141 name: &str,
142 data: Vec<(f64, f64)>,
143 color: Color,
144 style: LineStyle,
145 ) -> Self {
146 self.series.push(Series {
147 name: name.to_string(),
148 data,
149 color,
150 style,
151 });
152 self
153 }
154
155 #[must_use]
157 pub fn with_simplification(mut self, algorithm: Simplification) -> Self {
158 self.simplification = algorithm;
159 self
160 }
161
162 #[must_use]
164 pub fn with_x_axis(mut self, axis: Axis) -> Self {
165 self.x_axis = axis;
166 self
167 }
168
169 #[must_use]
171 pub fn with_y_axis(mut self, axis: Axis) -> Self {
172 self.y_axis = axis;
173 self
174 }
175
176 #[must_use]
180 pub fn compact(mut self) -> Self {
181 self.margin_left = 0.0;
182 self.margin_bottom = 0.0;
183 self.y_axis.ticks = 0;
184 self.x_axis.ticks = 0;
185 self.legend = LegendPosition::None;
186 self
187 }
188
189 #[must_use]
191 pub fn with_margins(mut self, left: f32, bottom: f32) -> Self {
192 debug_assert!(left >= 0.0, "left margin must be non-negative");
193 debug_assert!(bottom >= 0.0, "bottom margin must be non-negative");
194 self.margin_left = left;
195 self.margin_bottom = bottom;
196 self
197 }
198
199 #[must_use]
201 pub fn with_legend(mut self, position: LegendPosition) -> Self {
202 self.legend = position;
203 self
204 }
205
206 fn x_range(&self) -> (f64, f64) {
208 if let Some(min) = self.x_axis.min {
209 if let Some(max) = self.x_axis.max {
210 return (min, max);
211 }
212 }
213
214 let mut x_min = f64::INFINITY;
215 let mut x_max = f64::NEG_INFINITY;
216
217 for series in &self.series {
218 for &(x, _) in &series.data {
219 if x.is_finite() {
220 x_min = x_min.min(x);
221 x_max = x_max.max(x);
222 }
223 }
224 }
225
226 if x_min == f64::INFINITY {
227 (0.0, 1.0)
228 } else {
229 (
230 self.x_axis.min.unwrap_or(x_min),
231 self.x_axis.max.unwrap_or(x_max),
232 )
233 }
234 }
235
236 fn y_range(&self) -> (f64, f64) {
238 if let Some(min) = self.y_axis.min {
239 if let Some(max) = self.y_axis.max {
240 return (min, max);
241 }
242 }
243
244 let mut y_min = f64::INFINITY;
245 let mut y_max = f64::NEG_INFINITY;
246
247 for series in &self.series {
248 for &(_, y) in &series.data {
249 if y.is_finite() {
250 y_min = y_min.min(y);
251 y_max = y_max.max(y);
252 }
253 }
254 }
255
256 if y_min == f64::INFINITY {
257 (0.0, 1.0)
258 } else {
259 let padding = (y_max - y_min) * 0.1;
261 (
262 self.y_axis.min.unwrap_or(y_min - padding),
263 self.y_axis.max.unwrap_or(y_max + padding),
264 )
265 }
266 }
267
268 fn simplify(&self, data: &[(f64, f64)]) -> Vec<(f64, f64)> {
270 match self.simplification {
271 Simplification::None => data.to_vec(),
272 Simplification::DouglasPeucker { epsilon } => douglas_peucker(data, epsilon),
273 Simplification::VisvalingamWhyatt { threshold } => visvalingam_whyatt(data, threshold),
274 }
275 }
276
277 fn draw_y_axis(
279 &self,
280 canvas: &mut dyn Canvas,
281 y_min: f64,
282 y_max: f64,
283 plot_y: f32,
284 plot_height: f32,
285 ) {
286 let style = TextStyle {
287 color: Color::new(0.6, 0.6, 0.6, 1.0),
288 ..Default::default()
289 };
290 for i in 0..=self.y_axis.ticks {
291 let t = i as f64 / self.y_axis.ticks as f64;
292 let y_val = y_min + (y_max - y_min) * (1.0 - t);
293 let y_pos = plot_y + plot_height * t as f32;
294 if y_pos >= plot_y && y_pos < plot_y + plot_height {
295 canvas.draw_text(
296 &format!("{y_val:>5.0}"),
297 Point::new(self.bounds.x, y_pos),
298 &style,
299 );
300 }
301 }
302 }
303
304 #[allow(clippy::too_many_arguments)]
306 fn draw_x_axis(
307 &self,
308 canvas: &mut dyn Canvas,
309 x_min: f64,
310 x_max: f64,
311 plot_x: f32,
312 plot_y: f32,
313 plot_width: f32,
314 plot_height: f32,
315 ) {
316 let style = TextStyle {
317 color: Color::new(0.6, 0.6, 0.6, 1.0),
318 ..Default::default()
319 };
320 for i in 0..=self.x_axis.ticks.min(plot_width as usize / 8) {
321 let t = i as f64 / self.x_axis.ticks as f64;
322 let x_val = x_min + (x_max - x_min) * t;
323 let x_pos = plot_x + plot_width * t as f32;
324 if x_pos >= plot_x && x_pos < plot_x + plot_width - 4.0 {
325 canvas.draw_text(
326 &format!("{x_val:.0}"),
327 Point::new(x_pos, plot_y + plot_height),
328 &style,
329 );
330 }
331 }
332 }
333
334 fn draw_legend(
336 &self,
337 canvas: &mut dyn Canvas,
338 plot_x: f32,
339 plot_y: f32,
340 plot_width: f32,
341 plot_height: f32,
342 ) {
343 if matches!(self.legend, LegendPosition::None) || self.series.is_empty() {
344 return;
345 }
346 let legend_width = self
347 .series
348 .iter()
349 .map(|s| s.name.len() + 3)
350 .max()
351 .unwrap_or(10) as f32;
352 let (lx, ly) = match self.legend {
353 LegendPosition::TopRight => (plot_x + plot_width - legend_width, plot_y),
354 LegendPosition::TopLeft => (plot_x, plot_y),
355 LegendPosition::BottomRight => (
356 plot_x + plot_width - legend_width,
357 plot_y + plot_height - self.series.len() as f32,
358 ),
359 LegendPosition::BottomLeft => (plot_x, plot_y + plot_height - self.series.len() as f32),
360 LegendPosition::None => return,
361 };
362 for (i, series) in self.series.iter().enumerate() {
363 canvas.draw_text(
364 &format!("─ {}", series.name),
365 Point::new(lx, ly + i as f32),
366 &TextStyle {
367 color: series.color,
368 ..Default::default()
369 },
370 );
371 }
372 }
373}
374
375impl Default for LineChart {
376 fn default() -> Self {
377 Self::new()
378 }
379}
380
381impl Widget for LineChart {
382 fn type_id(&self) -> TypeId {
383 TypeId::of::<Self>()
384 }
385
386 fn measure(&self, constraints: Constraints) -> Size {
387 Size::new(
388 constraints.max_width.min(80.0),
389 constraints.max_height.min(20.0),
390 )
391 }
392
393 fn layout(&mut self, bounds: Rect) -> LayoutResult {
394 self.bounds = bounds;
395 LayoutResult {
396 size: Size::new(bounds.width, bounds.height),
397 }
398 }
399
400 fn paint(&self, canvas: &mut dyn Canvas) {
401 if self.bounds.width < 10.0 || self.bounds.height < 5.0 {
402 return;
403 }
404
405 let (x_min, x_max) = self.x_range();
406 let (y_min, y_max) = self.y_range();
407 let plot_x = self.bounds.x + self.margin_left;
408 let plot_y = self.bounds.y;
409 let plot_width = self.bounds.width - self.margin_left;
410 let plot_height = self.bounds.height - self.margin_bottom;
411 if plot_width <= 0.0 || plot_height <= 0.0 {
412 return;
413 }
414
415 self.draw_y_axis(canvas, y_min, y_max, plot_y, plot_height);
417 self.draw_x_axis(
418 canvas,
419 x_min,
420 x_max,
421 plot_x,
422 plot_y,
423 plot_width,
424 plot_height,
425 );
426
427 for series in &self.series {
429 let simplified = self.simplify(&series.data);
430 let style = TextStyle {
431 color: series.color,
432 ..Default::default()
433 };
434
435 let cols = plot_width as usize;
437 let rows = (plot_height * 4.0) as usize; if cols == 0 || rows == 0 {
440 continue;
441 }
442
443 let mut grid = vec![vec![false; rows]; cols];
444
445 for &(x, y) in &simplified {
447 if !x.is_finite() || !y.is_finite() {
448 continue;
449 }
450
451 let x_norm = if x_max > x_min {
453 (x - x_min) / (x_max - x_min)
454 } else {
455 0.5
456 };
457 let y_norm = if y_max > y_min {
458 (y - y_min) / (y_max - y_min)
459 } else {
460 0.5
461 };
462
463 let gx =
465 ((x_norm * (cols - 1) as f64).round() as usize).min(cols.saturating_sub(1));
466 let gy = (((1.0 - y_norm) * (rows - 1) as f64).round() as usize)
467 .min(rows.saturating_sub(1));
468
469 grid[gx][gy] = true;
470 }
471
472 let points: Vec<(usize, usize)> = simplified
474 .iter()
475 .filter_map(|&(x, y)| {
476 if !x.is_finite() || !y.is_finite() {
477 return None;
478 }
479 let x_norm = if x_max > x_min {
480 (x - x_min) / (x_max - x_min)
481 } else {
482 0.5
483 };
484 let y_norm = if y_max > y_min {
485 (y - y_min) / (y_max - y_min)
486 } else {
487 0.5
488 };
489 let gx =
490 ((x_norm * (cols - 1) as f64).round() as usize).min(cols.saturating_sub(1));
491 let gy = (((1.0 - y_norm) * (rows - 1) as f64).round() as usize)
492 .min(rows.saturating_sub(1));
493 Some((gx, gy))
494 })
495 .collect();
496
497 for window in points.windows(2) {
498 if let [p1, p2] = window {
499 draw_line(&mut grid, p1.0, p1.1, p2.0, p2.1);
500 }
501 }
502
503 let char_rows = plot_height as usize;
505 for cy in 0..char_rows {
506 #[allow(clippy::needless_range_loop)]
507 for cx in 0..cols {
508 let mut dots = 0u8;
511 for dy in 0..4 {
512 let gy = cy * 4 + dy;
513 if gy < rows && grid[cx][gy] {
514 dots |= 1 << dy;
515 }
516 }
517
518 if dots > 0 {
519 let braille_idx = dots as usize;
521 let ch = if braille_idx < BRAILLE_UP.len() {
522 BRAILLE_UP[braille_idx]
523 } else {
524 '⣿'
525 };
526 canvas.draw_text(
527 &ch.to_string(),
528 Point::new(plot_x + cx as f32, plot_y + cy as f32),
529 &style,
530 );
531 }
532 }
533 }
534 }
535
536 self.draw_legend(canvas, plot_x, plot_y, plot_width, plot_height);
538 }
539
540 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
541 None
542 }
543
544 fn children(&self) -> &[Box<dyn Widget>] {
545 &[]
546 }
547
548 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
549 &mut []
550 }
551}
552
553impl Brick for LineChart {
554 fn brick_name(&self) -> &'static str {
555 "LineChart"
556 }
557
558 fn assertions(&self) -> &[BrickAssertion] {
559 static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
560 ASSERTIONS
561 }
562
563 fn budget(&self) -> BrickBudget {
564 BrickBudget::uniform(16)
565 }
566
567 fn verify(&self) -> BrickVerification {
568 let mut passed = Vec::new();
569 let mut failed = Vec::new();
570
571 if self.bounds.width >= 10.0 {
572 passed.push(BrickAssertion::max_latency_ms(16));
573 } else {
574 failed.push((
575 BrickAssertion::max_latency_ms(16),
576 "Width too small".to_string(),
577 ));
578 }
579
580 BrickVerification {
581 passed,
582 failed,
583 verification_time: Duration::from_micros(5),
584 }
585 }
586
587 fn to_html(&self) -> String {
588 String::new()
589 }
590
591 fn to_css(&self) -> String {
592 String::new()
593 }
594}
595
596#[inline]
598fn is_in_grid_bounds(x: isize, y: isize, cols: isize, rows: isize) -> bool {
599 x >= 0 && x < cols && y >= 0 && y < rows
600}
601
602#[inline]
604fn line_step(from: usize, to: usize) -> isize {
605 if from < to {
606 1
607 } else {
608 -1
609 }
610}
611
612#[allow(clippy::cast_possible_wrap)]
614fn draw_line(grid: &mut [Vec<bool>], x0: usize, y0: usize, x1: usize, y1: usize) {
615 let dx = (x1 as isize - x0 as isize).abs();
616 let dy = -(y1 as isize - y0 as isize).abs();
617 let sx = line_step(x0, x1);
618 let sy = line_step(y0, y1);
619 let mut err = dx + dy;
620
621 let mut x = x0 as isize;
622 let mut y = y0 as isize;
623
624 let cols = grid.len() as isize;
625 let rows = if cols > 0 { grid[0].len() as isize } else { 0 };
626
627 loop {
628 if is_in_grid_bounds(x, y, cols, rows) {
629 grid[x as usize][y as usize] = true;
630 }
631
632 if x == x1 as isize && y == y1 as isize {
633 break;
634 }
635
636 let e2 = 2 * err;
637 if e2 >= dy {
638 err += dy;
639 x += sx;
640 }
641 if e2 <= dx {
642 err += dx;
643 y += sy;
644 }
645 }
646}
647
648fn douglas_peucker(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> {
650 if points.len() < 3 {
651 return points.to_vec();
652 }
653
654 let start = points[0];
656 let end = points[points.len() - 1];
657
658 let mut max_dist = 0.0;
659 let mut max_idx = 0;
660
661 for (i, &point) in points.iter().enumerate().skip(1).take(points.len() - 2) {
662 let dist = perpendicular_distance(point, start, end);
663 if dist > max_dist {
664 max_dist = dist;
665 max_idx = i;
666 }
667 }
668
669 if max_dist > epsilon {
671 let mut left = douglas_peucker(&points[..=max_idx], epsilon);
672 let right = douglas_peucker(&points[max_idx..], epsilon);
673
674 left.pop();
676 left.extend(right);
677 left
678 } else {
679 vec![start, end]
681 }
682}
683
684fn perpendicular_distance(point: (f64, f64), start: (f64, f64), end: (f64, f64)) -> f64 {
686 let dx = end.0 - start.0;
687 let dy = end.1 - start.1;
688
689 let mag = dx.hypot(dy);
690 if mag < 1e-10 {
691 return (point.0 - start.0).hypot(point.1 - start.1);
692 }
693
694 ((dy * point.0 - dx * point.1 + end.0 * start.1 - end.1 * start.0) / mag).abs()
695}
696
697fn visvalingam_whyatt(points: &[(f64, f64)], threshold: f64) -> Vec<(f64, f64)> {
699 if points.len() < 3 {
700 return points.to_vec();
701 }
702
703 let mut result: Vec<(f64, f64)> = points.to_vec();
704
705 while result.len() > 2 {
706 let mut min_area = f64::INFINITY;
708 let mut min_idx = 1;
709
710 for i in 1..result.len() - 1 {
711 let area = triangle_area(result[i - 1], result[i], result[i + 1]);
712 if area < min_area {
713 min_area = area;
714 min_idx = i;
715 }
716 }
717
718 if min_area >= threshold {
719 break;
720 }
721
722 result.remove(min_idx);
723 }
724
725 result
726}
727
728fn triangle_area(p1: (f64, f64), p2: (f64, f64), p3: (f64, f64)) -> f64 {
730 ((p2.0 - p1.0) * (p3.1 - p1.1) - (p3.0 - p1.0) * (p2.1 - p1.1)).abs() / 2.0
731}
732
733#[cfg(test)]
734mod tests {
735 use super::*;
736 use crate::direct::{CellBuffer, DirectTerminalCanvas};
737
738 #[test]
739 fn test_line_chart_creation() {
740 let chart = LineChart::new().add_series("test", vec![(0.0, 0.0), (1.0, 1.0)], Color::RED);
741 assert_eq!(chart.series.len(), 1);
742 assert_eq!(chart.series[0].name, "test");
743 }
744
745 #[test]
746 fn test_douglas_peucker() {
747 let points = vec![(0.0, 0.0), (1.0, 0.1), (2.0, 0.0), (3.0, 0.0)];
748 let simplified = douglas_peucker(&points, 0.5);
749 assert!(simplified.len() <= points.len());
750 }
751
752 #[test]
753 fn test_douglas_peucker_few_points() {
754 let points = vec![(0.0, 0.0), (1.0, 1.0)];
755 let simplified = douglas_peucker(&points, 0.5);
756 assert_eq!(simplified.len(), 2);
757 }
758
759 #[test]
760 fn test_visvalingam_whyatt() {
761 let points = vec![(0.0, 0.0), (1.0, 0.1), (2.0, 0.0), (3.0, 0.0)];
762 let simplified = visvalingam_whyatt(&points, 0.5);
763 assert!(simplified.len() <= points.len());
764 }
765
766 #[test]
767 fn test_visvalingam_whyatt_few_points() {
768 let points = vec![(0.0, 0.0), (1.0, 1.0)];
769 let simplified = visvalingam_whyatt(&points, 0.5);
770 assert_eq!(simplified.len(), 2);
771 }
772
773 #[test]
774 fn test_empty_chart() {
775 let chart = LineChart::new();
776 let (x_min, x_max) = chart.x_range();
777 assert_eq!(x_min, 0.0);
778 assert_eq!(x_max, 1.0);
779 }
780
781 #[test]
782 fn test_multi_series() {
783 let chart = LineChart::new()
784 .add_series("a", vec![(0.0, 0.0)], Color::RED)
785 .add_series("b", vec![(1.0, 1.0)], Color::BLUE)
786 .add_series("c", vec![(2.0, 2.0)], Color::GREEN);
787 assert_eq!(chart.series.len(), 3);
788 }
789
790 #[test]
791 fn test_line_chart_assertions() {
792 let chart = LineChart::default();
793 assert!(!chart.assertions().is_empty());
794 }
795
796 #[test]
797 fn test_line_chart_verify() {
798 let mut chart = LineChart::default();
799 chart.bounds = Rect::new(0.0, 0.0, 80.0, 20.0);
800 assert!(chart.verify().is_valid());
801 }
802
803 #[test]
804 fn test_line_chart_children() {
805 let chart = LineChart::default();
806 assert!(chart.children().is_empty());
807 }
808
809 #[test]
810 fn test_line_chart_layout() {
811 let mut chart = LineChart::new().add_series(
812 "test",
813 vec![(0.0, 0.0), (1.0, 1.0), (2.0, 0.5)],
814 Color::RED,
815 );
816 let bounds = Rect::new(0.0, 0.0, 80.0, 24.0);
817 let result = chart.layout(bounds);
818 assert!(result.size.width > 0.0);
819 assert!(result.size.height > 0.0);
820 }
821
822 #[test]
823 fn test_line_chart_paint() {
824 let mut chart = LineChart::new().add_series(
825 "test",
826 vec![(0.0, 0.0), (1.0, 1.0), (2.0, 0.5)],
827 Color::RED,
828 );
829 let bounds = Rect::new(0.0, 0.0, 80.0, 24.0);
830 chart.layout(bounds);
831
832 let mut buffer = CellBuffer::new(80, 24);
833 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
834 chart.paint(&mut canvas);
835 }
837
838 #[test]
839 fn test_line_chart_with_legend_positions() {
840 for pos in [
841 LegendPosition::TopRight,
842 LegendPosition::TopLeft,
843 LegendPosition::BottomRight,
844 LegendPosition::BottomLeft,
845 LegendPosition::None,
846 ] {
847 let mut chart = LineChart::new()
848 .add_series("s1", vec![(0.0, 0.0), (1.0, 1.0)], Color::RED)
849 .with_legend(pos);
850 let bounds = Rect::new(0.0, 0.0, 80.0, 24.0);
851 chart.layout(bounds);
852 let mut buffer = CellBuffer::new(80, 24);
853 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
854 chart.paint(&mut canvas);
855 }
856 }
857
858 #[test]
859 fn test_line_chart_with_axis_config() {
860 let mut chart = LineChart::new()
861 .add_series("test", vec![(0.0, 0.0), (10.0, 100.0)], Color::RED)
862 .with_x_axis(Axis {
863 label: Some("X Label".to_string()),
864 min: Some(0.0),
865 max: Some(10.0),
866 ticks: 5,
867 grid: true,
868 })
869 .with_y_axis(Axis {
870 label: Some("Y Label".to_string()),
871 min: Some(0.0),
872 max: Some(100.0),
873 ticks: 10,
874 grid: true,
875 });
876 let bounds = Rect::new(0.0, 0.0, 80.0, 24.0);
877 chart.layout(bounds);
878 let mut buffer = CellBuffer::new(80, 24);
879 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
880 chart.paint(&mut canvas);
881 }
882
883 #[test]
884 fn test_line_chart_with_simplification() {
885 let data: Vec<(f64, f64)> = (0..100)
886 .map(|i| (i as f64, (i as f64 * 0.1).sin()))
887 .collect();
888
889 let mut chart = LineChart::new()
891 .add_series("dp", data.clone(), Color::RED)
892 .with_simplification(Simplification::DouglasPeucker { epsilon: 0.1 });
893 let bounds = Rect::new(0.0, 0.0, 80.0, 24.0);
894 chart.layout(bounds);
895 let mut buffer = CellBuffer::new(80, 24);
896 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
897 chart.paint(&mut canvas);
898
899 let mut chart = LineChart::new()
901 .add_series("vw", data, Color::BLUE)
902 .with_simplification(Simplification::VisvalingamWhyatt { threshold: 0.1 });
903 chart.layout(bounds);
904 chart.paint(&mut canvas);
905 }
906
907 #[test]
908 fn test_line_chart_line_styles() {
909 for style in [
910 LineStyle::Solid,
911 LineStyle::Dashed,
912 LineStyle::Dotted,
913 LineStyle::Markers,
914 ] {
915 let mut chart = LineChart::new().add_series_styled(
916 "test",
917 vec![(0.0, 0.0), (1.0, 1.0), (2.0, 0.5)],
918 Color::RED,
919 style,
920 );
921 let bounds = Rect::new(0.0, 0.0, 80.0, 24.0);
922 chart.layout(bounds);
923 let mut buffer = CellBuffer::new(80, 24);
924 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
925 chart.paint(&mut canvas);
926 }
927 }
928
929 #[test]
930 fn test_line_chart_y_range() {
931 let chart = LineChart::new().add_series(
932 "test",
933 vec![(0.0, -5.0), (1.0, 10.0), (2.0, 3.0)],
934 Color::RED,
935 );
936 let (y_min, y_max) = chart.y_range();
937 assert!(y_min <= -5.0);
938 assert!(y_max >= 10.0);
939 }
940
941 #[test]
942 fn test_line_chart_x_range_with_data() {
943 let chart = LineChart::new().add_series("test", vec![(5.0, 0.0), (15.0, 1.0)], Color::RED);
944 let (x_min, x_max) = chart.x_range();
945 assert!(x_min <= 5.0);
946 assert!(x_max >= 15.0);
947 }
948
949 #[test]
950 fn test_triangle_area() {
951 let area = triangle_area((0.0, 0.0), (1.0, 0.0), (0.5, 1.0));
952 assert!((area - 0.5).abs() < 0.001);
953 }
954
955 #[test]
956 fn test_perpendicular_distance() {
957 let dist = perpendicular_distance((0.5, 0.5), (0.0, 0.0), (1.0, 1.0));
959 assert!(dist < 0.001);
960
961 let dist = perpendicular_distance((0.0, 1.0), (0.0, 0.0), (1.0, 0.0));
963 assert!((dist - 1.0).abs() < 0.001);
964 }
965
966 #[test]
967 fn test_axis_default() {
968 let axis = Axis::default();
969 assert!(axis.label.is_none());
970 assert!(axis.min.is_none());
971 assert!(axis.max.is_none());
972 assert_eq!(axis.ticks, 5);
973 assert!(!axis.grid);
974 }
975
976 #[test]
977 fn test_simplification_default() {
978 let simp = Simplification::default();
979 assert!(matches!(simp, Simplification::None));
980 }
981
982 #[test]
983 fn test_line_style_default() {
984 let style = LineStyle::default();
985 assert!(matches!(style, LineStyle::Solid));
986 }
987
988 #[test]
989 fn test_legend_position_default() {
990 let pos = LegendPosition::default();
991 assert!(matches!(pos, LegendPosition::TopRight));
992 }
993
994 #[test]
995 fn test_line_chart_compact() {
996 let chart = LineChart::new()
997 .add_series("test", vec![(0.0, 0.0), (1.0, 1.0)], Color::RED)
998 .compact();
999 assert_eq!(chart.margin_left, 0.0);
1000 assert_eq!(chart.margin_bottom, 0.0);
1001 assert_eq!(chart.y_axis.ticks, 0);
1002 assert_eq!(chart.x_axis.ticks, 0);
1003 assert!(matches!(chart.legend, LegendPosition::None));
1004 }
1005
1006 #[test]
1007 fn test_line_chart_with_margins() {
1008 let chart = LineChart::new()
1009 .add_series("test", vec![(0.0, 0.0), (1.0, 1.0)], Color::RED)
1010 .with_margins(10.0, 5.0);
1011 assert_eq!(chart.margin_left, 10.0);
1012 assert_eq!(chart.margin_bottom, 5.0);
1013 }
1014
1015 #[test]
1016 fn test_line_chart_explicit_x_range() {
1017 let chart = LineChart::new()
1018 .add_series("test", vec![(0.0, 0.0), (1.0, 1.0)], Color::RED)
1019 .with_x_axis(Axis {
1020 min: Some(0.0),
1021 max: Some(10.0),
1022 ..Default::default()
1023 });
1024 let (xmin, xmax) = chart.x_range();
1025 assert_eq!(xmin, 0.0);
1026 assert_eq!(xmax, 10.0);
1027 }
1028
1029 #[test]
1030 fn test_line_chart_explicit_y_range() {
1031 let chart = LineChart::new()
1032 .add_series("test", vec![(0.0, 0.0), (1.0, 1.0)], Color::RED)
1033 .with_y_axis(Axis {
1034 min: Some(-10.0),
1035 max: Some(10.0),
1036 ..Default::default()
1037 });
1038 let (ymin, ymax) = chart.y_range();
1039 assert_eq!(ymin, -10.0);
1040 assert_eq!(ymax, 10.0);
1041 }
1042
1043 #[test]
1044 fn test_line_chart_nan_values() {
1045 let mut chart = LineChart::new().add_series(
1046 "test",
1047 vec![(0.0, 0.0), (f64::NAN, f64::NAN), (2.0, 2.0)],
1048 Color::RED,
1049 );
1050 let bounds = Rect::new(0.0, 0.0, 80.0, 24.0);
1051 chart.layout(bounds);
1052 let mut buffer = CellBuffer::new(80, 24);
1053 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1054 chart.paint(&mut canvas);
1056 }
1057
1058 #[test]
1059 fn test_line_chart_infinite_values() {
1060 let mut chart = LineChart::new().add_series(
1061 "test",
1062 vec![(0.0, 0.0), (f64::INFINITY, f64::NEG_INFINITY), (2.0, 2.0)],
1063 Color::RED,
1064 );
1065 let bounds = Rect::new(0.0, 0.0, 80.0, 24.0);
1066 chart.layout(bounds);
1067 let mut buffer = CellBuffer::new(80, 24);
1068 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1069 chart.paint(&mut canvas);
1071 }
1072
1073 #[test]
1074 fn test_line_chart_too_small() {
1075 let mut chart =
1076 LineChart::new().add_series("test", vec![(0.0, 0.0), (1.0, 1.0)], Color::RED);
1077 let bounds = Rect::new(0.0, 0.0, 5.0, 2.0);
1079 chart.layout(bounds);
1080 let mut buffer = CellBuffer::new(5, 2);
1081 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1082 chart.paint(&mut canvas);
1083 }
1084
1085 #[test]
1086 fn test_line_chart_children_mut() {
1087 let mut chart = LineChart::default();
1088 assert!(chart.children_mut().is_empty());
1089 }
1090
1091 #[test]
1092 fn test_line_chart_to_html() {
1093 let chart = LineChart::default();
1094 assert!(chart.to_html().is_empty());
1095 }
1096
1097 #[test]
1098 fn test_line_chart_to_css() {
1099 let chart = LineChart::default();
1100 assert!(chart.to_css().is_empty());
1101 }
1102
1103 #[test]
1104 fn test_line_chart_verify_small_width() {
1105 let mut chart = LineChart::default();
1106 chart.bounds = Rect::new(0.0, 0.0, 5.0, 20.0); let verification = chart.verify();
1108 assert!(!verification.is_valid());
1109 }
1110
1111 #[test]
1112 fn test_line_chart_budget() {
1113 let chart = LineChart::default();
1114 let budget = chart.budget();
1115 let _ = budget;
1117 }
1118
1119 #[test]
1120 fn test_line_chart_measure() {
1121 let chart = LineChart::default();
1122 let size = chart.measure(Constraints {
1123 min_width: 0.0,
1124 min_height: 0.0,
1125 max_width: 100.0,
1126 max_height: 50.0,
1127 });
1128 assert_eq!(size.width, 80.0);
1129 assert_eq!(size.height, 20.0);
1130 }
1131
1132 #[test]
1133 fn test_line_chart_type_id() {
1134 let chart = LineChart::default();
1135 let tid = Widget::type_id(&chart);
1137 assert_eq!(tid, TypeId::of::<LineChart>());
1138 }
1139
1140 #[test]
1141 fn test_draw_line_horizontal() {
1142 let mut grid = vec![vec![false; 10]; 20];
1143 draw_line(&mut grid, 0, 5, 19, 5);
1144 assert!(grid[0][5]);
1146 assert!(grid[10][5]);
1147 assert!(grid[19][5]);
1148 }
1149
1150 #[test]
1151 fn test_draw_line_vertical() {
1152 let mut grid = vec![vec![false; 10]; 20];
1153 draw_line(&mut grid, 5, 0, 5, 9);
1154 assert!(grid[5][0]);
1155 assert!(grid[5][5]);
1156 assert!(grid[5][9]);
1157 }
1158
1159 #[test]
1160 fn test_draw_line_diagonal() {
1161 let mut grid = vec![vec![false; 10]; 10];
1162 draw_line(&mut grid, 0, 0, 9, 9);
1163 assert!(grid[0][0]);
1164 assert!(grid[9][9]);
1165 }
1166
1167 #[test]
1168 fn test_draw_line_reverse() {
1169 let mut grid = vec![vec![false; 10]; 10];
1170 draw_line(&mut grid, 9, 9, 0, 0);
1171 assert!(grid[0][0]);
1172 assert!(grid[9][9]);
1173 }
1174
1175 #[test]
1176 fn test_perpendicular_distance_coincident_points() {
1177 let dist = perpendicular_distance((1.0, 1.0), (0.0, 0.0), (0.0, 0.0));
1179 assert!((dist - std::f64::consts::SQRT_2).abs() < 0.001);
1180 }
1181
1182 #[test]
1183 fn test_series_struct() {
1184 let series = Series {
1185 name: "test".to_string(),
1186 data: vec![(0.0, 0.0), (1.0, 1.0)],
1187 color: Color::RED,
1188 style: LineStyle::Dashed,
1189 };
1190 assert_eq!(series.name, "test");
1191 assert_eq!(series.data.len(), 2);
1192 assert!(matches!(series.style, LineStyle::Dashed));
1193 }
1194
1195 #[test]
1196 fn test_line_chart_single_point_x_range() {
1197 let chart = LineChart::new().add_series("test", vec![(5.0, 0.0), (5.0, 1.0)], Color::RED);
1199 let (xmin, xmax) = chart.x_range();
1200 assert_eq!(xmin, 5.0);
1202 assert_eq!(xmax, 5.0);
1203 }
1204
1205 #[test]
1206 fn test_line_chart_single_point_y_range() {
1207 let chart = LineChart::new().add_series("test", vec![(0.0, 5.0), (1.0, 5.0)], Color::RED);
1209 let (ymin, ymax) = chart.y_range();
1210 assert_eq!(ymin, 5.0);
1212 assert_eq!(ymax, 5.0);
1213 }
1214}