1#![warn(missing_docs)]
87#![warn(clippy::pedantic)]
88#![allow(clippy::module_name_repetitions)]
89
90use std::f64::consts::PI;
91
92use ratatui::buffer::Buffer;
93use ratatui::layout::Rect;
94use ratatui::style::{Color, Style, Styled};
95use ratatui::text::{Line, Span};
96use ratatui::widgets::{Block, Widget};
97
98pub mod border_style;
99pub mod legend;
100#[macro_use]
101pub mod macros;
102pub mod symbols;
103pub mod title;
104
105pub use legend::{LegendAlignment, LegendLayout, LegendPosition};
107pub use title::{BlockExt, TitleAlignment, TitlePosition, TitleStyle};
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
131pub enum Resolution {
132 #[default]
136 Standard,
137
138 Braille,
143}
144
145#[derive(Debug, Clone, PartialEq)]
158pub struct PieSlice<'a> {
159 label: &'a str,
161 value: f64,
163 color: Color,
165}
166
167impl<'a> PieSlice<'a> {
168 #[must_use]
179 pub const fn new(label: &'a str, value: f64, color: Color) -> Self {
180 Self {
181 label,
182 value,
183 color,
184 }
185 }
186
187 #[must_use]
189 pub const fn label(&self) -> &'a str {
190 self.label
191 }
192
193 #[must_use]
195 pub const fn value(&self) -> f64 {
196 self.value
197 }
198
199 #[must_use]
201 pub const fn color(&self) -> Color {
202 self.color
203 }
204}
205
206#[derive(Debug, Clone, PartialEq)]
225pub struct PieChart<'a> {
226 slices: Vec<PieSlice<'a>>,
228 block: Option<Block<'a>>,
230 style: Style,
232 show_legend: bool,
234 show_percentages: bool,
236 pie_char: char,
238 legend_marker: &'a str,
240 resolution: Resolution,
242 legend_position: LegendPosition,
244 legend_layout: LegendLayout,
246 legend_alignment: LegendAlignment,
248}
249
250impl Default for PieChart<'_> {
251 fn default() -> Self {
262 Self {
263 slices: Vec::new(),
264 block: None,
265 style: Style::default(),
266 show_legend: true,
267 show_percentages: true,
268 pie_char: symbols::PIE_CHAR,
269 legend_marker: symbols::LEGEND_MARKER,
270 resolution: Resolution::default(),
271 legend_position: LegendPosition::default(),
272 legend_layout: LegendLayout::default(),
273 legend_alignment: LegendAlignment::default(),
274 }
275 }
276}
277
278impl<'a> PieChart<'a> {
279 #[must_use]
294 pub fn new(slices: Vec<PieSlice<'a>>) -> Self {
295 Self {
296 slices,
297 ..Default::default()
298 }
299 }
300
301 #[must_use]
315 pub fn slices(mut self, slices: Vec<PieSlice<'a>>) -> Self {
316 self.slices = slices;
317 self
318 }
319
320 #[must_use]
334 pub fn block(mut self, block: Block<'a>) -> Self {
335 self.block = Some(block);
336 self
337 }
338
339 #[must_use]
351 pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
352 self.style = style.into();
353 self
354 }
355
356 #[must_use]
366 pub const fn show_legend(mut self, show: bool) -> Self {
367 self.show_legend = show;
368 self
369 }
370
371 #[must_use]
381 pub const fn show_percentages(mut self, show: bool) -> Self {
382 self.show_percentages = show;
383 self
384 }
385
386 #[must_use]
409 pub const fn pie_char(mut self, c: char) -> Self {
410 self.pie_char = c;
411 self
412 }
413
414 #[must_use]
444 pub const fn legend_marker(mut self, marker: &'a str) -> Self {
445 self.legend_marker = marker;
446 self
447 }
448
449 #[must_use]
464 pub const fn resolution(mut self, resolution: Resolution) -> Self {
465 self.resolution = resolution;
466 self
467 }
468
469 #[must_use]
482 pub const fn high_resolution(mut self, enabled: bool) -> Self {
483 self.resolution = if enabled {
484 Resolution::Braille
485 } else {
486 Resolution::Standard
487 };
488 self
489 }
490
491 #[must_use]
502 pub const fn legend_position(mut self, position: LegendPosition) -> Self {
503 self.legend_position = position;
504 self
505 }
506
507 #[must_use]
523 pub const fn legend_layout(mut self, layout: LegendLayout) -> Self {
524 self.legend_layout = layout;
525 self
526 }
527
528 #[must_use]
544 pub const fn legend_alignment(mut self, alignment: LegendAlignment) -> Self {
545 self.legend_alignment = alignment;
546 self
547 }
548
549 fn total_value(&self) -> f64 {
550 self.slices.iter().map(|s| s.value).sum()
551 }
552
553 fn percentage(&self, slice: &PieSlice) -> f64 {
555 let total = self.total_value();
556 if total > 0.0 {
557 (slice.value / total) * 100.0
558 } else {
559 0.0
560 }
561 }
562}
563
564impl Styled for PieChart<'_> {
565 type Item = Self;
566
567 fn style(&self) -> Style {
568 self.style
569 }
570
571 fn set_style<S: Into<Style>>(mut self, style: S) -> Self::Item {
572 self.style = style.into();
573 self
574 }
575}
576
577impl Widget for PieChart<'_> {
578 fn render(self, area: Rect, buf: &mut Buffer) {
579 Widget::render(&self, area, buf);
580 }
581}
582
583impl Widget for &PieChart<'_> {
584 fn render(self, area: Rect, buf: &mut Buffer) {
585 buf.set_style(area, self.style);
586 let inner = if let Some(ref block) = self.block {
587 let inner_area = block.inner(area);
588 block.render(area, buf);
589 inner_area
590 } else {
591 area
592 };
593 self.render_piechart(inner, buf);
594 }
595}
596
597impl PieChart<'_> {
598 fn render_piechart(&self, area: Rect, buf: &mut Buffer) {
599 if area.is_empty() || self.slices.is_empty() {
600 return;
601 }
602
603 let total = self.total_value();
604 if total <= 0.0 {
605 return;
606 }
607
608 match self.resolution {
609 Resolution::Standard => {
610 }
612 Resolution::Braille => {
613 self.render_piechart_braille(area, buf);
614 return;
615 }
616 }
617
618 let (pie_area, legend_area_opt) = self.calculate_layout(area);
620
621 let center_x = pie_area.width / 2;
624 let center_y = pie_area.height / 2;
625
626 let radius = center_x.min(center_y * 2).saturating_sub(1);
628
629 let mut cumulative_percent = 0.0;
631 for slice in &self.slices {
632 let percent = self.percentage(slice);
633 self.render_slice(
634 pie_area,
635 buf,
636 center_x,
637 center_y,
638 radius,
639 cumulative_percent,
640 percent,
641 slice.color,
642 );
643 cumulative_percent += percent;
644 }
645
646 if let Some(legend_area) = legend_area_opt {
648 self.render_legend(buf, legend_area);
649 }
650 }
651
652 #[allow(clippy::too_many_arguments, clippy::similar_names)]
653 fn render_slice(
654 &self,
655 area: Rect,
656 buf: &mut Buffer,
657 center_x: u16,
658 center_y: u16,
659 radius: u16,
660 start_percent: f64,
661 percent: f64,
662 color: Color,
663 ) {
664 if radius == 0 || percent <= 0.0 {
665 return;
666 }
667
668 let start_angle = (start_percent / 100.0) * 2.0 * PI - PI / 2.0;
670 let end_angle = ((start_percent + percent) / 100.0) * 2.0 * PI - PI / 2.0;
671
672 let scan_width = i32::from(radius + 1);
674 let scan_height = i32::from((radius / 2) + 1); for dy in -scan_height..=scan_height {
677 for dx in -scan_width..=scan_width {
678 let x = i32::from(area.x) + i32::from(center_x) + dx;
680 let y = i32::from(area.y) + i32::from(center_y) + dy;
681
682 if x < i32::from(area.x)
684 || x >= i32::from(area.x + area.width)
685 || y < i32::from(area.y)
686 || y >= i32::from(area.y + area.height)
687 {
688 continue;
689 }
690
691 #[allow(clippy::cast_precision_loss)]
693 let adjusted_dx = f64::from(dx);
694 #[allow(clippy::cast_precision_loss)]
695 let adjusted_dy = f64::from(dy * 2);
696
697 let distance = (adjusted_dx * adjusted_dx + adjusted_dy * adjusted_dy).sqrt();
699
700 #[allow(clippy::cast_precision_loss)]
702 if distance <= f64::from(radius) {
703 let angle = adjusted_dy.atan2(adjusted_dx);
705
706 if Self::is_angle_in_slice(angle, start_angle, end_angle) {
708 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
709 {
710 let cell = &mut buf[(x as u16, y as u16)];
711 cell.set_char(self.pie_char).set_fg(color);
712 }
713 }
714 }
715 }
716 }
717 }
718
719 fn is_angle_in_slice(angle: f64, start: f64, end: f64) -> bool {
720 let normalize = |a: f64| {
722 let mut normalized = a % (2.0 * PI);
723 if normalized < 0.0 {
724 normalized += 2.0 * PI;
725 }
726 normalized
727 };
728
729 let norm_angle = normalize(angle);
730 let norm_start = normalize(start);
731 let norm_end = normalize(end);
732
733 if norm_start <= norm_end {
734 norm_angle >= norm_start && norm_angle <= norm_end
735 } else {
736 norm_angle >= norm_start || norm_angle <= norm_end
738 }
739 }
740
741 fn format_legend_text(&self, slice: &PieSlice, total: f64, spacing: &str) -> String {
742 if self.show_percentages {
743 let percent = if total > 0.0 {
744 (slice.value / total) * 100.0
745 } else {
746 0.0
747 };
748 format!(
749 "{} {} {:.1}%{}",
750 self.legend_marker, slice.label, percent, spacing
751 )
752 } else {
753 format!("{} {}{}", self.legend_marker, slice.label, spacing)
754 }
755 }
756
757 fn calculate_aligned_x(&self, legend_area: Rect, content_width: u16) -> u16 {
758 match self.legend_alignment {
759 LegendAlignment::Left => legend_area.x,
760 LegendAlignment::Center => {
761 legend_area.x + (legend_area.width.saturating_sub(content_width)) / 2
762 }
763 LegendAlignment::Right => {
764 legend_area.x + legend_area.width.saturating_sub(content_width)
765 }
766 }
767 }
768
769 fn render_legend(&self, buf: &mut Buffer, legend_area: Rect) {
770 let total = self.total_value();
771
772 match self.legend_layout {
773 LegendLayout::Vertical => {
774 self.render_vertical_legend(buf, legend_area, total);
775 }
776 LegendLayout::Horizontal => {
777 self.render_horizontal_legend(buf, legend_area, total);
778 }
779 }
780 }
781
782 fn render_vertical_legend(&self, buf: &mut Buffer, legend_area: Rect, total: f64) {
783 for (idx, slice) in self.slices.iter().enumerate() {
784 #[allow(clippy::cast_possible_truncation)]
785 let y_offset = (idx as u16) * 2;
786
787 if y_offset >= legend_area.height {
788 break;
789 }
790
791 let legend_text = self.format_legend_text(slice, total, "");
792 #[allow(clippy::cast_possible_truncation)]
793 let text_width = legend_text.len() as u16;
794 let x_pos = self.calculate_aligned_x(legend_area, text_width);
795
796 let line = Line::from(vec![Span::styled(
797 legend_text,
798 Style::default().fg(slice.color),
799 )]);
800 let item_area = Rect {
801 x: x_pos,
802 y: legend_area.y + y_offset,
803 width: text_width.min(legend_area.width),
804 height: 1,
805 };
806
807 line.render(item_area, buf);
808 }
809 }
810
811 fn render_horizontal_legend(&self, buf: &mut Buffer, legend_area: Rect, total: f64) {
812 let mut total_width = 0u16;
813 let mut item_widths = Vec::new();
814
815 for slice in &self.slices {
816 let legend_text = self.format_legend_text(slice, total, " ");
817 #[allow(clippy::cast_possible_truncation)]
818 let text_width = legend_text.len() as u16;
819 item_widths.push(text_width);
820 total_width = total_width.saturating_add(text_width);
821 }
822
823 let start_x = self.calculate_aligned_x(legend_area, total_width.min(legend_area.width));
824 let mut x_offset = 0u16;
825
826 for (idx, slice) in self.slices.iter().enumerate() {
827 if x_offset >= legend_area.width {
828 break;
829 }
830
831 let legend_text = self.format_legend_text(slice, total, " ");
832 let text_width = item_widths[idx];
833
834 let line = Line::from(vec![Span::styled(
835 legend_text,
836 Style::default().fg(slice.color),
837 )]);
838 let item_area = Rect {
839 x: start_x + x_offset,
840 y: legend_area.y,
841 width: text_width.min(legend_area.width.saturating_sub(x_offset)),
842 height: 1,
843 };
844
845 line.render(item_area, buf);
846 x_offset = x_offset.saturating_add(text_width);
847 }
848 }
849
850 #[allow(clippy::too_many_lines)]
851 fn calculate_layout(&self, area: Rect) -> (Rect, Option<Rect>) {
852 if !self.show_legend || area.width < 20 || area.height < 10 {
853 return (area, None);
854 }
855
856 match self.legend_position {
857 LegendPosition::Right => {
858 let legend_width = if self.legend_layout == LegendLayout::Horizontal {
859 self.calculate_legend_width().min(area.width / 2)
860 } else {
861 self.calculate_legend_width().min(area.width / 3).max(20)
862 };
863 if area.width <= legend_width {
864 return (area, None);
865 }
866 let pie_width = area.width.saturating_sub(legend_width + 1);
867 (
868 Rect {
869 x: area.x,
870 y: area.y,
871 width: pie_width,
872 height: area.height,
873 },
874 Some(Rect {
875 x: area.x + pie_width + 1,
876 y: area.y + 1,
877 width: legend_width,
878 height: area.height.saturating_sub(2),
879 }),
880 )
881 }
882 LegendPosition::Left => {
883 let legend_width = if self.legend_layout == LegendLayout::Horizontal {
884 self.calculate_legend_width().min(area.width / 2)
885 } else {
886 self.calculate_legend_width().min(area.width / 3).max(20)
887 };
888 if area.width <= legend_width {
889 return (area, None);
890 }
891 let pie_width = area.width.saturating_sub(legend_width + 1);
892 (
893 Rect {
894 x: area.x + legend_width + 1,
895 y: area.y,
896 width: pie_width,
897 height: area.height,
898 },
899 Some(Rect {
900 x: area.x,
901 y: area.y + 1,
902 width: legend_width,
903 height: area.height.saturating_sub(2),
904 }),
905 )
906 }
907 LegendPosition::Top => {
908 let legend_height = if self.legend_layout == LegendLayout::Horizontal {
909 3
910 } else {
911 #[allow(clippy::cast_possible_truncation)]
912 (self.slices.len() as u16 * 2).min(area.height / 3)
913 };
914 if area.height <= legend_height {
915 return (area, None);
916 }
917 let pie_height = area.height.saturating_sub(legend_height + 1);
918 (
919 Rect {
920 x: area.x,
921 y: area.y + legend_height + 1,
922 width: area.width,
923 height: pie_height,
924 },
925 Some(Rect {
926 x: area.x + 1,
927 y: area.y + 1,
928 width: area.width.saturating_sub(2),
929 height: legend_height.saturating_sub(1),
930 }),
931 )
932 }
933 LegendPosition::Bottom => {
934 let legend_height = if self.legend_layout == LegendLayout::Horizontal {
935 3
936 } else {
937 #[allow(clippy::cast_possible_truncation)]
938 (self.slices.len() as u16 * 2).min(area.height / 3)
939 };
940 if area.height <= legend_height {
941 return (area, None);
942 }
943 let pie_height = area.height.saturating_sub(legend_height + 1);
944 (
945 Rect {
946 x: area.x,
947 y: area.y,
948 width: area.width,
949 height: pie_height,
950 },
951 Some(Rect {
952 x: area.x + 1,
953 y: area.y + pie_height + 1,
954 width: area.width.saturating_sub(2),
955 height: legend_height.saturating_sub(1),
956 }),
957 )
958 }
959 }
960 }
961
962 fn calculate_legend_width(&self) -> u16 {
963 let total = self.total_value();
964
965 match self.legend_layout {
966 LegendLayout::Vertical => {
967 let mut max_width = 0u16;
969
970 for slice in &self.slices {
971 let text = if self.show_percentages {
972 let percent = if total > 0.0 {
973 (slice.value / total) * 100.0
974 } else {
975 0.0
976 };
977 format!("{} {} {:.1}% ", self.legend_marker, slice.label, percent)
978 } else {
979 format!("{} {} ", self.legend_marker, slice.label)
980 };
981
982 #[allow(clippy::cast_possible_truncation)]
983 let text_width = text.len() as u16;
984 max_width = max_width.max(text_width);
985 }
986
987 max_width.saturating_add(2)
988 }
989 LegendLayout::Horizontal => {
990 let mut total_width = 0u16;
992
993 for slice in &self.slices {
994 let text = if self.show_percentages {
995 let percent = if total > 0.0 {
996 (slice.value / total) * 100.0
997 } else {
998 0.0
999 };
1000 format!("{} {} {:.1}% ", self.legend_marker, slice.label, percent)
1001 } else {
1002 format!("{} {} ", self.legend_marker, slice.label)
1003 };
1004
1005 #[allow(clippy::cast_possible_truncation)]
1006 let text_width = text.len() as u16;
1007 total_width = total_width.saturating_add(text_width);
1008 }
1009
1010 total_width.saturating_add(2)
1011 }
1012 }
1013 }
1014
1015 #[allow(clippy::similar_names)]
1016 fn render_piechart_braille(&self, area: Rect, buf: &mut Buffer) {
1017 let (pie_area, legend_area_opt) = self.calculate_layout(area);
1019
1020 let center_x_chars = pie_area.width / 2;
1022 let center_y_chars = pie_area.height / 2;
1023
1024 let center_x_dots = center_x_chars * 2;
1026 let center_y_dots = center_y_chars * 4;
1027
1028 let radius = (center_x_dots).min(center_y_dots).saturating_sub(2);
1034
1035 let width_dots = pie_area.width * 2;
1037 let height_dots = pie_area.height * 4;
1038
1039 let mut dot_slices: Vec<Vec<Option<usize>>> =
1040 vec![vec![None; width_dots as usize]; height_dots as usize];
1041
1042 let mut cumulative_percent = 0.0;
1044 for (slice_idx, slice) in self.slices.iter().enumerate() {
1045 let percent = self.percentage(slice);
1046 let start_angle = (cumulative_percent / 100.0) * 2.0 * PI - PI / 2.0;
1047 let end_angle = ((cumulative_percent + percent) / 100.0) * 2.0 * PI - PI / 2.0;
1048
1049 for dy in 0..height_dots {
1050 for dx in 0..width_dots {
1051 let rel_x = f64::from(dx) - f64::from(center_x_dots);
1052 let rel_y = f64::from(dy) - f64::from(center_y_dots);
1053
1054 let distance = (rel_x * rel_x + rel_y * rel_y).sqrt();
1057
1058 if distance <= f64::from(radius) {
1059 let angle = rel_y.atan2(rel_x);
1060 if Self::is_angle_in_slice(angle, start_angle, end_angle) {
1061 dot_slices[dy as usize][dx as usize] = Some(slice_idx);
1062 }
1063 }
1064 }
1065 }
1066
1067 cumulative_percent += percent;
1068 }
1069
1070 for char_y in 0..pie_area.height {
1072 for char_x in 0..pie_area.width {
1073 let base_dot_x = char_x * 2;
1074 let base_dot_y = char_y * 4;
1075
1076 let dot_positions = [
1083 (0, 0, 0x01), (0, 1, 0x02), (0, 2, 0x04), (1, 0, 0x08), (1, 1, 0x10), (1, 2, 0x20), (0, 3, 0x40), (1, 3, 0x80), ];
1092
1093 let mut pattern = 0u32;
1094 let mut slice_colors: Vec<(usize, u32)> = Vec::new();
1095
1096 for (dx, dy, bit) in dot_positions {
1097 let dot_x = base_dot_x + dx;
1098 let dot_y = base_dot_y + dy;
1099
1100 if dot_y < height_dots && dot_x < width_dots {
1101 if let Some(slice_idx) = dot_slices[dot_y as usize][dot_x as usize] {
1102 pattern |= bit;
1103 if let Some(entry) =
1105 slice_colors.iter_mut().find(|(idx, _)| *idx == slice_idx)
1106 {
1107 entry.1 += 1;
1108 } else {
1109 slice_colors.push((slice_idx, 1));
1110 }
1111 }
1112 }
1113 }
1114
1115 if pattern > 0 {
1116 if let Some((slice_idx, _)) = slice_colors.iter().max_by_key(|(_, count)| count)
1118 {
1119 let braille_char = char::from_u32(0x2800 + pattern).unwrap_or('⠀');
1120 let color = self.slices[*slice_idx].color;
1121
1122 let cell = &mut buf[(pie_area.x + char_x, pie_area.y + char_y)];
1123 cell.set_char(braille_char).set_fg(color);
1124 }
1125 }
1126 }
1127 }
1128
1129 if let Some(legend_area) = legend_area_opt {
1131 self.render_legend(buf, legend_area);
1132 }
1133 }
1134}
1135
1136#[cfg(test)]
1137#[allow(clippy::float_cmp)]
1138mod tests {
1139 use super::*;
1140
1141 #[test]
1142 fn pie_slice_new() {
1143 let slice = PieSlice::new("Test", 50.0, Color::Red);
1144 assert_eq!(slice.label(), "Test");
1145 assert_eq!(slice.value(), 50.0);
1146 assert_eq!(slice.color(), Color::Red);
1147 }
1148
1149 #[test]
1150 fn piechart_new() {
1151 let slices = vec![
1152 PieSlice::new("A", 30.0, Color::Red),
1153 PieSlice::new("B", 70.0, Color::Blue),
1154 ];
1155 let piechart = PieChart::new(slices.clone());
1156 assert_eq!(piechart.slices, slices);
1157 }
1158
1159 #[test]
1160 fn piechart_default() {
1161 let piechart = PieChart::default();
1162 assert!(piechart.slices.is_empty());
1163 assert!(piechart.show_legend);
1164 assert!(piechart.show_percentages);
1165 }
1166
1167 #[test]
1168 fn piechart_slices() {
1169 let slices = vec![PieSlice::new("Test", 100.0, Color::Green)];
1170 let piechart = PieChart::default().slices(slices.clone());
1171 assert_eq!(piechart.slices, slices);
1172 }
1173
1174 #[test]
1175 fn piechart_style() {
1176 let style = Style::default().fg(Color::Red);
1177 let piechart = PieChart::default().style(style);
1178 assert_eq!(piechart.style, style);
1179 }
1180
1181 #[test]
1182 fn piechart_show_legend() {
1183 let piechart = PieChart::default().show_legend(false);
1184 assert!(!piechart.show_legend);
1185 }
1186
1187 #[test]
1188 fn piechart_show_percentages() {
1189 let piechart = PieChart::default().show_percentages(false);
1190 assert!(!piechart.show_percentages);
1191 }
1192
1193 #[test]
1194 fn piechart_pie_char() {
1195 let piechart = PieChart::default().pie_char('█');
1196 assert_eq!(piechart.pie_char, '█');
1197 }
1198
1199 #[test]
1200 fn piechart_total_value() {
1201 let slices = vec![
1202 PieSlice::new("A", 30.0, Color::Red),
1203 PieSlice::new("B", 70.0, Color::Blue),
1204 ];
1205 let piechart = PieChart::new(slices);
1206 assert_eq!(piechart.total_value(), 100.0);
1207 }
1208
1209 #[test]
1210 fn piechart_percentage() {
1211 let slices = vec![
1212 PieSlice::new("A", 30.0, Color::Red),
1213 PieSlice::new("B", 70.0, Color::Blue),
1214 ];
1215 let piechart = PieChart::new(slices);
1216 assert_eq!(
1217 piechart.percentage(&PieSlice::new("A", 30.0, Color::Red)),
1218 30.0
1219 );
1220 }
1221
1222 render_empty_test!(piechart_render_empty_area, PieChart::default());
1224
1225 render_with_size_test!(
1226 piechart_render_with_block,
1227 {
1228 let slices = vec![PieSlice::new("Test", 100.0, Color::Red)];
1229 PieChart::new(slices).block(Block::bordered())
1230 },
1231 width: 20,
1232 height: 10
1233 );
1234
1235 render_test!(
1236 piechart_render_basic,
1237 {
1238 let slices = vec![
1239 PieSlice::new("Rust", 45.0, Color::Red),
1240 PieSlice::new("Go", 30.0, Color::Blue),
1241 PieSlice::new("Python", 25.0, Color::Green),
1242 ];
1243 PieChart::new(slices)
1244 },
1245 Rect::new(0, 0, 40, 20)
1246 );
1247
1248 #[test]
1249 fn piechart_styled_trait() {
1250 use ratatui::style::Stylize;
1251 let piechart = PieChart::default().red();
1252 assert_eq!(piechart.style.fg, Some(Color::Red));
1253 }
1254
1255 #[test]
1256 fn piechart_with_multiple_slices() {
1257 let slices = vec![
1258 PieSlice::new("A", 25.0, Color::Red),
1259 PieSlice::new("B", 25.0, Color::Blue),
1260 PieSlice::new("C", 25.0, Color::Green),
1261 PieSlice::new("D", 25.0, Color::Yellow),
1262 ];
1263 let piechart = PieChart::new(slices);
1264 assert_eq!(piechart.total_value(), 100.0);
1265 }
1266
1267 render_with_size_test!(
1269 piechart_multi_slice_render,
1270 {
1271 let slices = vec![
1272 PieSlice::new("A", 25.0, Color::Red),
1273 PieSlice::new("B", 25.0, Color::Blue),
1274 PieSlice::new("C", 25.0, Color::Green),
1275 PieSlice::new("D", 25.0, Color::Yellow),
1276 ];
1277 PieChart::new(slices)
1278 },
1279 width: 50,
1280 height: 30
1281 );
1282
1283 #[test]
1284 fn piechart_zero_values() {
1285 let slices = vec![
1286 PieSlice::new("A", 0.0, Color::Red),
1287 PieSlice::new("B", 0.0, Color::Blue),
1288 ];
1289 let piechart = PieChart::new(slices);
1290 assert_eq!(piechart.total_value(), 0.0);
1291 }
1292
1293 #[test]
1294 fn piechart_method_chaining() {
1295 use ratatui::widgets::Block;
1296
1297 let slices = vec![PieSlice::new("Test", 100.0, Color::Red)];
1298 let piechart = PieChart::new(slices)
1299 .show_legend(true)
1300 .show_percentages(true)
1301 .pie_char('█')
1302 .block(Block::bordered().title("Test"))
1303 .style(Style::default().fg(Color::White));
1304
1305 assert!(piechart.show_legend);
1306 assert!(piechart.show_percentages);
1307 assert_eq!(piechart.pie_char, '█');
1308 assert!(piechart.block.is_some());
1309 assert_eq!(piechart.style.fg, Some(Color::White));
1310 }
1311
1312 #[test]
1313 fn piechart_custom_symbols() {
1314 use crate::symbols;
1315
1316 let piechart = PieChart::default().pie_char(symbols::PIE_CHAR_BLOCK);
1317 assert_eq!(piechart.pie_char, '█');
1318
1319 let piechart = PieChart::default().pie_char(symbols::PIE_CHAR_CIRCLE);
1320 assert_eq!(piechart.pie_char, '◉');
1321
1322 let piechart = PieChart::default().pie_char(symbols::PIE_CHAR_SQUARE);
1323 assert_eq!(piechart.pie_char, '■');
1324 }
1325
1326 #[test]
1327 fn piechart_is_angle_in_slice() {
1328 use std::f64::consts::PI;
1329
1330 assert!(PieChart::is_angle_in_slice(PI / 4.0, 0.0, PI / 2.0));
1332
1333 assert!(!PieChart::is_angle_in_slice(PI, 0.0, PI / 2.0));
1335
1336 assert!(PieChart::is_angle_in_slice(0.1, 1.5 * PI, 0.5));
1338 }
1339}