1use crate::graphics::Color;
4use crate::text::Font;
5
6#[derive(Debug, Clone, Copy, PartialEq, Default)]
8pub enum LegendPosition {
9 None,
11 #[default]
13 Right,
14 Bottom,
16 Top,
18 Left,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq)]
24pub enum ChartType {
25 VerticalBar,
27 HorizontalBar,
29 Pie,
31 Line,
33 Area,
35}
36
37#[derive(Debug, Clone)]
39pub struct ChartData {
40 pub label: String,
42 pub value: f64,
44 pub color: Option<Color>,
46 pub highlighted: bool,
48}
49
50impl ChartData {
51 pub fn new<S: Into<String>>(label: S, value: f64) -> Self {
53 Self {
54 label: label.into(),
55 value,
56 color: None,
57 highlighted: false,
58 }
59 }
60
61 pub fn color(mut self, color: Color) -> Self {
63 self.color = Some(color);
64 self
65 }
66
67 pub fn highlighted(mut self) -> Self {
69 self.highlighted = true;
70 self
71 }
72}
73
74#[derive(Debug, Clone)]
76pub struct Chart {
77 pub title: String,
79 pub chart_type: ChartType,
81 pub data: Vec<ChartData>,
83 pub colors: Vec<Color>,
85 pub title_font: Font,
87 pub title_font_size: f64,
89 pub label_font: Font,
91 pub label_font_size: f64,
93 pub legend_position: LegendPosition,
95 pub background_color: Option<Color>,
97 pub show_values: bool,
99 pub show_grid: bool,
101 pub grid_color: Color,
103 pub border_color: Color,
105 pub border_width: f64,
107}
108
109impl Chart {
110 pub fn new(chart_type: ChartType) -> Self {
112 Self {
113 title: String::new(),
114 chart_type,
115 data: Vec::new(),
116 colors: default_colors(),
117 title_font: Font::HelveticaBold,
118 title_font_size: 14.0,
119 label_font: Font::Helvetica,
120 label_font_size: 10.0,
121 legend_position: LegendPosition::Right,
122 background_color: None,
123 show_values: true,
124 show_grid: true,
125 grid_color: Color::rgb(0.9, 0.9, 0.9),
126 border_color: Color::black(),
127 border_width: 1.0,
128 }
129 }
130
131 pub fn max_value(&self) -> f64 {
133 self.data.iter().map(|d| d.value).fold(0.0, f64::max)
134 }
135
136 pub fn total_value(&self) -> f64 {
138 self.data.iter().map(|d| d.value).sum()
139 }
140
141 pub fn color_for_index(&self, index: usize) -> Color {
143 if let Some(data_point) = self.data.get(index) {
144 if let Some(color) = data_point.color {
145 return color;
146 }
147 }
148
149 self.colors
150 .get(index % self.colors.len())
151 .copied()
152 .unwrap_or(Color::rgb(0.5, 0.5, 0.5))
153 }
154}
155
156pub struct ChartBuilder {
158 chart: Chart,
159}
160
161impl ChartBuilder {
162 pub fn new(chart_type: ChartType) -> Self {
164 Self {
165 chart: Chart::new(chart_type),
166 }
167 }
168
169 pub fn title<S: Into<String>>(mut self, title: S) -> Self {
171 self.chart.title = title.into();
172 self
173 }
174
175 pub fn add_data(mut self, data: ChartData) -> Self {
177 self.chart.data.push(data);
178 self
179 }
180
181 pub fn data(mut self, data: Vec<ChartData>) -> Self {
183 self.chart.data = data;
184 self
185 }
186
187 pub fn colors(mut self, colors: Vec<Color>) -> Self {
189 self.chart.colors = colors;
190 self
191 }
192
193 pub fn title_font(mut self, font: Font, size: f64) -> Self {
195 self.chart.title_font = font;
196 self.chart.title_font_size = size;
197 self
198 }
199
200 pub fn label_font(mut self, font: Font, size: f64) -> Self {
202 self.chart.label_font = font;
203 self.chart.label_font_size = size;
204 self
205 }
206
207 pub fn legend_position(mut self, position: LegendPosition) -> Self {
209 self.chart.legend_position = position;
210 self
211 }
212
213 pub fn background_color(mut self, color: Color) -> Self {
215 self.chart.background_color = Some(color);
216 self
217 }
218
219 pub fn show_values(mut self, show: bool) -> Self {
221 self.chart.show_values = show;
222 self
223 }
224
225 pub fn show_grid(mut self, show: bool) -> Self {
227 self.chart.show_grid = show;
228 self
229 }
230
231 pub fn grid_color(mut self, color: Color) -> Self {
233 self.chart.grid_color = color;
234 self
235 }
236
237 pub fn border(mut self, color: Color, width: f64) -> Self {
239 self.chart.border_color = color;
240 self.chart.border_width = width;
241 self
242 }
243
244 pub fn simple_data(mut self, values: Vec<f64>) -> Self {
246 for (i, value) in values.into_iter().enumerate() {
247 self.chart
248 .data
249 .push(ChartData::new(format!("Item {}", i + 1), value));
250 }
251 self
252 }
253
254 pub fn labeled_data(mut self, data: Vec<(&str, f64)>) -> Self {
256 for (label, value) in data {
257 self.chart.data.push(ChartData::new(label, value));
258 }
259 self
260 }
261
262 pub fn financial_style(mut self) -> Self {
264 self.chart.colors = vec![
265 Color::rgb(0.2, 0.6, 0.2), Color::rgb(0.8, 0.2, 0.2), Color::rgb(0.2, 0.4, 0.8), Color::rgb(0.9, 0.6, 0.1), Color::rgb(0.6, 0.2, 0.8), ];
271 self.chart.show_grid = true;
272 self.chart.grid_color = Color::rgb(0.95, 0.95, 0.95);
273 self.chart.title_font_size = 16.0;
274 self
275 }
276
277 pub fn minimal_style(mut self) -> Self {
279 self.chart.show_grid = false;
280 self.chart.border_width = 0.0;
281 self.chart.background_color = None;
282 self
283 }
284
285 pub fn build(self) -> Chart {
287 self.chart
288 }
289}
290
291fn default_colors() -> Vec<Color> {
293 vec![
294 Color::rgb(0.26, 0.45, 0.76), Color::rgb(0.85, 0.37, 0.0), Color::rgb(0.18, 0.55, 0.34), Color::rgb(0.84, 0.15, 0.16), Color::rgb(0.58, 0.4, 0.74), Color::rgb(0.55, 0.34, 0.29), Color::rgb(0.89, 0.47, 0.76), Color::rgb(0.5, 0.5, 0.5), Color::rgb(0.74, 0.74, 0.13), Color::rgb(0.09, 0.75, 0.81), ]
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 #[test]
314 fn test_legend_position_default() {
315 let pos: LegendPosition = Default::default();
316 assert_eq!(pos, LegendPosition::Right);
317 }
318
319 #[test]
320 fn test_legend_position_variants() {
321 assert_eq!(LegendPosition::None, LegendPosition::None);
322 assert_eq!(LegendPosition::Right, LegendPosition::Right);
323 assert_eq!(LegendPosition::Bottom, LegendPosition::Bottom);
324 assert_eq!(LegendPosition::Top, LegendPosition::Top);
325 assert_eq!(LegendPosition::Left, LegendPosition::Left);
326 }
327
328 #[test]
329 fn test_legend_position_clone() {
330 let pos = LegendPosition::Bottom;
331 let cloned = pos;
332 assert_eq!(pos, cloned);
333 }
334
335 #[test]
336 fn test_legend_position_debug() {
337 let debug_str = format!("{:?}", LegendPosition::Top);
338 assert!(debug_str.contains("Top"));
339 }
340
341 #[test]
344 fn test_chart_type_variants() {
345 assert_eq!(ChartType::VerticalBar, ChartType::VerticalBar);
346 assert_eq!(ChartType::HorizontalBar, ChartType::HorizontalBar);
347 assert_eq!(ChartType::Pie, ChartType::Pie);
348 assert_eq!(ChartType::Line, ChartType::Line);
349 assert_eq!(ChartType::Area, ChartType::Area);
350 }
351
352 #[test]
353 fn test_chart_type_not_equal() {
354 assert_ne!(ChartType::VerticalBar, ChartType::HorizontalBar);
355 assert_ne!(ChartType::Pie, ChartType::Line);
356 }
357
358 #[test]
359 fn test_chart_type_clone() {
360 let ct = ChartType::Area;
361 let cloned = ct;
362 assert_eq!(ct, cloned);
363 }
364
365 #[test]
366 fn test_chart_type_debug() {
367 let debug_str = format!("{:?}", ChartType::Line);
368 assert!(debug_str.contains("Line"));
369 }
370
371 #[test]
374 fn test_chart_data_creation() {
375 let data = ChartData::new("Test", 42.0);
376 assert_eq!(data.label, "Test");
377 assert_eq!(data.value, 42.0);
378 assert_eq!(data.color, None);
379 assert!(!data.highlighted);
380 }
381
382 #[test]
383 fn test_chart_data_with_color() {
384 let data = ChartData::new("Test", 42.0)
385 .color(Color::red())
386 .highlighted();
387
388 assert_eq!(data.color, Some(Color::red()));
389 assert!(data.highlighted);
390 }
391
392 #[test]
393 fn test_chart_data_from_string() {
394 let data = ChartData::new(String::from("Dynamic"), 99.9);
395 assert_eq!(data.label, "Dynamic");
396 assert_eq!(data.value, 99.9);
397 }
398
399 #[test]
400 fn test_chart_data_negative_value() {
401 let data = ChartData::new("Negative", -25.5);
402 assert_eq!(data.value, -25.5);
403 }
404
405 #[test]
406 fn test_chart_data_zero_value() {
407 let data = ChartData::new("Zero", 0.0);
408 assert_eq!(data.value, 0.0);
409 }
410
411 #[test]
412 fn test_chart_data_clone() {
413 let data = ChartData::new("Clone", 10.0).color(Color::blue());
414 let cloned = data.clone();
415 assert_eq!(cloned.label, "Clone");
416 assert_eq!(cloned.value, 10.0);
417 assert_eq!(cloned.color, Some(Color::blue()));
418 }
419
420 #[test]
421 fn test_chart_data_debug() {
422 let data = ChartData::new("Debug", 5.0);
423 let debug_str = format!("{:?}", data);
424 assert!(debug_str.contains("Debug"));
425 assert!(debug_str.contains("5"));
426 }
427
428 #[test]
431 fn test_chart_new_vertical_bar() {
432 let chart = Chart::new(ChartType::VerticalBar);
433 assert_eq!(chart.chart_type, ChartType::VerticalBar);
434 assert!(chart.title.is_empty());
435 assert!(chart.data.is_empty());
436 assert_eq!(chart.legend_position, LegendPosition::Right);
437 }
438
439 #[test]
440 fn test_chart_new_pie() {
441 let chart = Chart::new(ChartType::Pie);
442 assert_eq!(chart.chart_type, ChartType::Pie);
443 }
444
445 #[test]
446 fn test_chart_new_line() {
447 let chart = Chart::new(ChartType::Line);
448 assert_eq!(chart.chart_type, ChartType::Line);
449 }
450
451 #[test]
452 fn test_chart_new_defaults() {
453 let chart = Chart::new(ChartType::Area);
454 assert_eq!(chart.title_font, Font::HelveticaBold);
455 assert_eq!(chart.title_font_size, 14.0);
456 assert_eq!(chart.label_font, Font::Helvetica);
457 assert_eq!(chart.label_font_size, 10.0);
458 assert!(chart.show_values);
459 assert!(chart.show_grid);
460 assert_eq!(chart.border_width, 1.0);
461 assert_eq!(chart.background_color, None);
462 }
463
464 #[test]
465 fn test_chart_max_value_empty() {
466 let chart = Chart::new(ChartType::VerticalBar);
467 assert_eq!(chart.max_value(), 0.0);
468 }
469
470 #[test]
471 fn test_chart_total_value_empty() {
472 let chart = Chart::new(ChartType::Pie);
473 assert_eq!(chart.total_value(), 0.0);
474 }
475
476 #[test]
477 fn test_chart_max_value_with_negatives() {
478 let mut chart = Chart::new(ChartType::Line);
479 chart.data = vec![
480 ChartData::new("A", -10.0),
481 ChartData::new("B", -5.0),
482 ChartData::new("C", -20.0),
483 ];
484 assert_eq!(chart.max_value(), 0.0);
487 }
488
489 #[test]
490 fn test_chart_total_value_with_negatives() {
491 let mut chart = Chart::new(ChartType::VerticalBar);
492 chart.data = vec![ChartData::new("A", 10.0), ChartData::new("B", -5.0)];
493 assert_eq!(chart.total_value(), 5.0);
494 }
495
496 #[test]
497 fn test_chart_color_for_index_out_of_bounds() {
498 let chart = Chart::new(ChartType::Pie);
499 let color = chart.color_for_index(100);
501 assert!(color.r() >= 0.0 && color.r() <= 1.0);
503 }
504
505 #[test]
506 #[should_panic(expected = "attempt to calculate the remainder with a divisor of zero")]
507 fn test_chart_color_for_index_with_empty_colors_panics() {
508 let mut chart = Chart::new(ChartType::Pie);
509 chart.colors = vec![];
510 let _ = chart.color_for_index(0);
513 }
514
515 #[test]
516 fn test_chart_color_for_index_custom_overrides_default() {
517 let mut chart = Chart::new(ChartType::Pie);
518 chart.data = vec![ChartData::new("Custom", 10.0).color(Color::green())];
519 assert_eq!(chart.color_for_index(0), Color::green());
520 }
521
522 #[test]
525 fn test_chart_builder() {
526 let chart = ChartBuilder::new(ChartType::VerticalBar)
527 .title("Test Chart")
528 .simple_data(vec![10.0, 20.0, 30.0])
529 .build();
530
531 assert_eq!(chart.title, "Test Chart");
532 assert_eq!(chart.chart_type, ChartType::VerticalBar);
533 assert_eq!(chart.data.len(), 3);
534 assert_eq!(chart.max_value(), 30.0);
535 assert_eq!(chart.total_value(), 60.0);
536 }
537
538 #[test]
539 fn test_chart_builder_add_data() {
540 let chart = ChartBuilder::new(ChartType::Pie)
541 .add_data(ChartData::new("A", 10.0))
542 .add_data(ChartData::new("B", 20.0))
543 .build();
544
545 assert_eq!(chart.data.len(), 2);
546 assert_eq!(chart.data[0].label, "A");
547 assert_eq!(chart.data[1].label, "B");
548 }
549
550 #[test]
551 fn test_chart_builder_data_replaces() {
552 let chart = ChartBuilder::new(ChartType::Line)
553 .add_data(ChartData::new("Old", 1.0))
554 .data(vec![ChartData::new("New", 2.0)])
555 .build();
556
557 assert_eq!(chart.data.len(), 1);
558 assert_eq!(chart.data[0].label, "New");
559 }
560
561 #[test]
562 fn test_chart_builder_colors() {
563 let custom_colors = vec![Color::red(), Color::blue()];
564 let chart = ChartBuilder::new(ChartType::VerticalBar)
565 .colors(custom_colors.clone())
566 .build();
567
568 assert_eq!(chart.colors.len(), 2);
569 assert_eq!(chart.colors[0], Color::red());
570 assert_eq!(chart.colors[1], Color::blue());
571 }
572
573 #[test]
574 fn test_chart_builder_title_font() {
575 let chart = ChartBuilder::new(ChartType::Pie)
576 .title_font(Font::TimesBold, 24.0)
577 .build();
578
579 assert_eq!(chart.title_font, Font::TimesBold);
580 assert_eq!(chart.title_font_size, 24.0);
581 }
582
583 #[test]
584 fn test_chart_builder_label_font() {
585 let chart = ChartBuilder::new(ChartType::Line)
586 .label_font(Font::Courier, 8.0)
587 .build();
588
589 assert_eq!(chart.label_font, Font::Courier);
590 assert_eq!(chart.label_font_size, 8.0);
591 }
592
593 #[test]
594 fn test_chart_builder_legend_position() {
595 let chart = ChartBuilder::new(ChartType::Pie)
596 .legend_position(LegendPosition::Bottom)
597 .build();
598
599 assert_eq!(chart.legend_position, LegendPosition::Bottom);
600 }
601
602 #[test]
603 fn test_chart_builder_legend_none() {
604 let chart = ChartBuilder::new(ChartType::VerticalBar)
605 .legend_position(LegendPosition::None)
606 .build();
607
608 assert_eq!(chart.legend_position, LegendPosition::None);
609 }
610
611 #[test]
612 fn test_chart_builder_background_color() {
613 let chart = ChartBuilder::new(ChartType::Line)
614 .background_color(Color::white())
615 .build();
616
617 assert_eq!(chart.background_color, Some(Color::white()));
618 }
619
620 #[test]
621 fn test_chart_builder_show_values() {
622 let chart = ChartBuilder::new(ChartType::VerticalBar)
623 .show_values(false)
624 .build();
625
626 assert!(!chart.show_values);
627 }
628
629 #[test]
630 fn test_chart_builder_show_grid() {
631 let chart = ChartBuilder::new(ChartType::Line).show_grid(false).build();
632
633 assert!(!chart.show_grid);
634 }
635
636 #[test]
637 fn test_chart_builder_grid_color() {
638 let chart = ChartBuilder::new(ChartType::Area)
639 .grid_color(Color::gray(0.5))
640 .build();
641
642 assert_eq!(chart.grid_color, Color::gray(0.5));
643 }
644
645 #[test]
646 fn test_chart_builder_border() {
647 let chart = ChartBuilder::new(ChartType::VerticalBar)
648 .border(Color::black(), 2.5)
649 .build();
650
651 assert_eq!(chart.border_color, Color::black());
652 assert_eq!(chart.border_width, 2.5);
653 }
654
655 #[test]
656 fn test_chart_builder_simple_data_labels() {
657 let chart = ChartBuilder::new(ChartType::VerticalBar)
658 .simple_data(vec![1.0, 2.0, 3.0])
659 .build();
660
661 assert_eq!(chart.data[0].label, "Item 1");
662 assert_eq!(chart.data[1].label, "Item 2");
663 assert_eq!(chart.data[2].label, "Item 3");
664 }
665
666 #[test]
667 fn test_chart_builder_labeled_data() {
668 let chart = ChartBuilder::new(ChartType::Pie)
669 .labeled_data(vec![("Q1", 100.0), ("Q2", 200.0)])
670 .build();
671
672 assert_eq!(chart.data.len(), 2);
673 assert_eq!(chart.data[0].label, "Q1");
674 assert_eq!(chart.data[0].value, 100.0);
675 assert_eq!(chart.data[1].label, "Q2");
676 assert_eq!(chart.data[1].value, 200.0);
677 }
678
679 #[test]
680 fn test_chart_builder_financial_style() {
681 let chart = ChartBuilder::new(ChartType::VerticalBar)
682 .financial_style()
683 .build();
684
685 assert_eq!(chart.colors.len(), 5);
686 assert!(chart.show_grid);
687 assert_eq!(chart.title_font_size, 16.0);
688 }
689
690 #[test]
691 fn test_chart_builder_minimal_style() {
692 let chart = ChartBuilder::new(ChartType::Line).minimal_style().build();
693
694 assert!(!chart.show_grid);
695 assert_eq!(chart.border_width, 0.0);
696 assert_eq!(chart.background_color, None);
697 }
698
699 #[test]
700 fn test_chart_builder_chained_styles() {
701 let chart = ChartBuilder::new(ChartType::VerticalBar)
702 .title("Financial Report")
703 .financial_style()
704 .labeled_data(vec![("2023", 1000.0), ("2024", 1500.0)])
705 .legend_position(LegendPosition::Bottom)
706 .build();
707
708 assert_eq!(chart.title, "Financial Report");
709 assert_eq!(chart.data.len(), 2);
710 assert_eq!(chart.legend_position, LegendPosition::Bottom);
711 assert!(chart.show_grid);
712 }
713
714 #[test]
715 fn test_color_for_index() {
716 let chart = ChartBuilder::new(ChartType::Pie)
717 .data(vec![
718 ChartData::new("A", 10.0).color(Color::red()),
719 ChartData::new("B", 20.0), ])
721 .build();
722
723 assert_eq!(chart.color_for_index(0), Color::red());
724 assert_eq!(chart.color_for_index(1), chart.colors[1]); }
726
727 #[test]
730 fn test_default_colors_count() {
731 let colors = default_colors();
732 assert_eq!(colors.len(), 10);
733 }
734
735 #[test]
736 fn test_default_colors_valid_rgb() {
737 let colors = default_colors();
738 for color in colors {
739 assert!(color.r() >= 0.0 && color.r() <= 1.0);
740 assert!(color.g() >= 0.0 && color.g() <= 1.0);
741 assert!(color.b() >= 0.0 && color.b() <= 1.0);
742 }
743 }
744
745 #[test]
746 fn test_default_colors_unique() {
747 let colors = default_colors();
748 for i in 0..colors.len() {
749 for j in (i + 1)..colors.len() {
750 assert_ne!(
751 colors[i], colors[j],
752 "Colors at {} and {} should be different",
753 i, j
754 );
755 }
756 }
757 }
758
759 #[test]
762 fn test_empty_chart() {
763 let chart = ChartBuilder::new(ChartType::Pie).build();
764 assert!(chart.data.is_empty());
765 assert_eq!(chart.max_value(), 0.0);
766 assert_eq!(chart.total_value(), 0.0);
767 }
768
769 #[test]
770 fn test_single_data_point() {
771 let chart = ChartBuilder::new(ChartType::VerticalBar)
772 .add_data(ChartData::new("Only", 42.0))
773 .build();
774
775 assert_eq!(chart.data.len(), 1);
776 assert_eq!(chart.max_value(), 42.0);
777 assert_eq!(chart.total_value(), 42.0);
778 }
779
780 #[test]
781 fn test_large_dataset() {
782 let mut builder = ChartBuilder::new(ChartType::Line);
783 for i in 0..1000 {
784 builder = builder.add_data(ChartData::new(format!("Item {}", i), i as f64));
785 }
786 let chart = builder.build();
787
788 assert_eq!(chart.data.len(), 1000);
789 assert_eq!(chart.max_value(), 999.0);
790 assert_eq!(chart.total_value(), (0..1000).sum::<i32>() as f64);
791 }
792
793 #[test]
794 fn test_special_float_values() {
795 let chart = ChartBuilder::new(ChartType::VerticalBar)
796 .simple_data(vec![f64::MIN_POSITIVE, f64::MAX / 2.0])
797 .build();
798
799 assert_eq!(chart.data.len(), 2);
800 assert!(chart.max_value() > 0.0);
801 }
802}