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#[cfg(feature = "theme")]
109pub use tui_nuances::{Theme, ThemeProvider, ThemeSwitcher, Themed};
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
133pub enum Resolution {
134 #[default]
138 Standard,
139
140 Braille,
145}
146
147#[derive(Debug, Clone, PartialEq)]
160pub struct PieSlice<'a> {
161 label: &'a str,
163 value: f64,
165 color: Color,
167}
168
169impl<'a> PieSlice<'a> {
170 #[must_use]
181 pub const fn new(label: &'a str, value: f64, color: Color) -> Self {
182 Self {
183 label,
184 value,
185 color,
186 }
187 }
188
189 #[cfg(feature = "theme")]
197 #[must_use]
198 pub fn themed(label: &'a str, value: f64, theme: &Theme, index: usize) -> Self {
199 Self::new(label, value, theme.color(index))
200 }
201
202 #[must_use]
204 pub const fn label(&self) -> &'a str {
205 self.label
206 }
207
208 #[must_use]
210 pub const fn value(&self) -> f64 {
211 self.value
212 }
213
214 #[must_use]
216 pub const fn color(&self) -> Color {
217 self.color
218 }
219}
220
221#[cfg(feature = "theme")]
222impl Themed for PieChart<'_> {
223 fn apply_theme(self, theme: &Theme) -> Self {
224 self.theme(theme)
225 }
226}
227
228#[derive(Debug, Clone, PartialEq)]
247pub struct PieChart<'a> {
248 slices: Vec<PieSlice<'a>>,
250 block: Option<Block<'a>>,
252 style: Style,
254 theme_style: Style,
256 show_legend: bool,
258 show_percentages: bool,
260 pie_char: char,
262 legend_marker: &'a str,
264 resolution: Resolution,
266 legend_position: LegendPosition,
268 legend_layout: LegendLayout,
270 legend_alignment: LegendAlignment,
272}
273
274impl Default for PieChart<'_> {
275 fn default() -> Self {
286 Self {
287 slices: Vec::new(),
288 block: None,
289 style: Style::default(),
290 theme_style: Style::default(),
291 show_legend: true,
292 show_percentages: true,
293 pie_char: symbols::PIE_CHAR,
294 legend_marker: symbols::LEGEND_MARKER,
295 resolution: Resolution::default(),
296 legend_position: LegendPosition::default(),
297 legend_layout: LegendLayout::default(),
298 legend_alignment: LegendAlignment::default(),
299 }
300 }
301}
302
303impl<'a> PieChart<'a> {
304 #[must_use]
319 pub fn new(slices: Vec<PieSlice<'a>>) -> Self {
320 Self {
321 slices,
322 ..Default::default()
323 }
324 }
325
326 #[must_use]
340 pub fn slices(mut self, slices: Vec<PieSlice<'a>>) -> Self {
341 self.slices = slices;
342 self
343 }
344
345 #[must_use]
359 pub fn block(mut self, block: Block<'a>) -> Self {
360 self.block = Some(block);
361 self
362 }
363
364 #[must_use]
376 pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
377 self.style = style.into();
378 self
379 }
380
381 #[cfg(feature = "theme")]
391 #[must_use]
392 pub fn theme(mut self, theme: &Theme) -> Self {
393 self.theme_style = theme.style();
394 self
395 }
396
397 #[must_use]
407 pub const fn show_legend(mut self, show: bool) -> Self {
408 self.show_legend = show;
409 self
410 }
411
412 #[must_use]
422 pub const fn show_percentages(mut self, show: bool) -> Self {
423 self.show_percentages = show;
424 self
425 }
426
427 #[must_use]
450 pub const fn pie_char(mut self, c: char) -> Self {
451 self.pie_char = c;
452 self
453 }
454
455 #[must_use]
485 pub const fn legend_marker(mut self, marker: &'a str) -> Self {
486 self.legend_marker = marker;
487 self
488 }
489
490 #[must_use]
505 pub const fn resolution(mut self, resolution: Resolution) -> Self {
506 self.resolution = resolution;
507 self
508 }
509
510 #[must_use]
523 pub const fn high_resolution(mut self, enabled: bool) -> Self {
524 self.resolution = if enabled {
525 Resolution::Braille
526 } else {
527 Resolution::Standard
528 };
529 self
530 }
531
532 #[must_use]
543 pub const fn legend_position(mut self, position: LegendPosition) -> Self {
544 self.legend_position = position;
545 self
546 }
547
548 #[must_use]
564 pub const fn legend_layout(mut self, layout: LegendLayout) -> Self {
565 self.legend_layout = layout;
566 self
567 }
568
569 #[must_use]
585 pub const fn legend_alignment(mut self, alignment: LegendAlignment) -> Self {
586 self.legend_alignment = alignment;
587 self
588 }
589
590 fn total_value(&self) -> f64 {
591 self.slices.iter().map(|s| s.value).sum()
592 }
593
594 fn value_percent(value: f64, total: f64) -> f64 {
599 if total > 0.0 {
600 (value / total) * 100.0
601 } else {
602 0.0
603 }
604 }
605
606 fn percentage(&self, slice: &PieSlice) -> f64 {
608 Self::value_percent(slice.value, self.total_value())
609 }
610}
611
612impl Styled for PieChart<'_> {
613 type Item = Self;
614
615 fn style(&self) -> Style {
616 self.style
617 }
618
619 fn set_style<S: Into<Style>>(mut self, style: S) -> Self::Item {
620 self.style = style.into();
621 self
622 }
623}
624
625impl Widget for PieChart<'_> {
626 fn render(self, area: Rect, buf: &mut Buffer) {
627 Widget::render(&self, area, buf);
628 }
629}
630
631impl Widget for &PieChart<'_> {
632 fn render(self, area: Rect, buf: &mut Buffer) {
633 buf.set_style(area, self.theme_style.patch(self.style));
634 let inner = if let Some(ref block) = self.block {
635 let inner_area = block.inner(area);
636 block.render(area, buf);
637 inner_area
638 } else {
639 area
640 };
641 self.render_piechart(inner, buf);
642 }
643}
644
645impl PieChart<'_> {
646 const LEGEND_VERTICAL_MAX_RATIO: u16 = 3;
648
649 const LEGEND_VERTICAL_MIN_WIDTH: u16 = 20;
651
652 const LEGEND_HORIZONTAL_MAX_RATIO: u16 = 5;
655
656 const LEGEND_HORIZONTAL_MAX_WIDTH: u16 = 60;
658
659 const LEGEND_VERTICAL_MAX_HEIGHT: u16 = 9;
662
663 const LEGEND_HORIZONTAL_HEIGHT: u16 = 3;
665
666 const LEGEND_SPACING: u16 = 1;
668
669 const LEGEND_PADDING: u16 = 1;
671
672 fn render_piechart(&self, area: Rect, buf: &mut Buffer) {
673 if area.is_empty() || self.slices.is_empty() {
674 return;
675 }
676
677 let total = self.total_value();
678 if total <= 0.0 {
679 return;
680 }
681
682 match self.resolution {
683 Resolution::Standard => {
684 }
686 Resolution::Braille => {
687 self.render_piechart_braille(area, buf);
688 return;
689 }
690 }
691
692 let (pie_area, legend_area_opt) = self.calculate_layout(area);
694
695 let center_x = pie_area.width / 2;
698 let center_y = pie_area.height / 2;
699
700 let radius = center_x.min(center_y * 2).saturating_sub(1);
702
703 let mut cumulative_percent = 0.0;
705 for slice in &self.slices {
706 let percent = self.percentage(slice);
707 self.render_slice(
708 pie_area,
709 buf,
710 center_x,
711 center_y,
712 radius,
713 cumulative_percent,
714 percent,
715 slice.color,
716 );
717 cumulative_percent += percent;
718 }
719
720 if let Some(legend_area) = legend_area_opt {
722 self.render_legend(buf, legend_area);
723 }
724 }
725
726 fn slice_angles(start_percent: f64, percent: f64) -> (f64, f64, bool) {
732 let start_angle = (start_percent / 100.0) * 2.0 * PI - PI / 2.0;
733 let end_angle = ((start_percent + percent) / 100.0) * 2.0 * PI - PI / 2.0;
734 let is_full_circle = percent >= 100.0 - f64::EPSILON;
735 (start_angle, end_angle, is_full_circle)
736 }
737
738 #[allow(clippy::too_many_arguments, clippy::similar_names)]
739 fn render_slice(
740 &self,
741 area: Rect,
742 buf: &mut Buffer,
743 center_x: u16,
744 center_y: u16,
745 radius: u16,
746 start_percent: f64,
747 percent: f64,
748 color: Color,
749 ) {
750 if radius == 0 || percent <= 0.0 {
751 return;
752 }
753
754 let (start_angle, end_angle, is_full_circle) = Self::slice_angles(start_percent, percent);
758
759 let scan_width = i32::from(radius + 1);
761 let scan_height = i32::from((radius / 2) + 1); for dy in -scan_height..=scan_height {
764 for dx in -scan_width..=scan_width {
765 let x = i32::from(area.x) + i32::from(center_x) + dx;
767 let y = i32::from(area.y) + i32::from(center_y) + dy;
768
769 if x < i32::from(area.x)
771 || x >= i32::from(area.x + area.width)
772 || y < i32::from(area.y)
773 || y >= i32::from(area.y + area.height)
774 {
775 continue;
776 }
777
778 #[allow(clippy::cast_precision_loss)]
780 let adjusted_dx = f64::from(dx);
781 #[allow(clippy::cast_precision_loss)]
782 let adjusted_dy = f64::from(dy * 2);
783
784 let distance = (adjusted_dx * adjusted_dx + adjusted_dy * adjusted_dy).sqrt();
786
787 #[allow(clippy::cast_precision_loss)]
789 if distance <= f64::from(radius) {
790 let angle = adjusted_dy.atan2(adjusted_dx);
792
793 if is_full_circle || Self::is_angle_in_slice(angle, start_angle, end_angle) {
795 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
796 {
797 let cell = &mut buf[(x as u16, y as u16)];
798 cell.set_char(self.pie_char).set_fg(color);
799 }
800 }
801 }
802 }
803 }
804 }
805
806 fn is_angle_in_slice(angle: f64, start: f64, end: f64) -> bool {
807 let normalize = |a: f64| {
809 let mut normalized = a % (2.0 * PI);
810 if normalized < 0.0 {
811 normalized += 2.0 * PI;
812 }
813 normalized
814 };
815
816 let norm_angle = normalize(angle);
817 let norm_start = normalize(start);
818 let norm_end = normalize(end);
819
820 if norm_start <= norm_end {
821 norm_angle >= norm_start && norm_angle <= norm_end
822 } else {
823 norm_angle >= norm_start || norm_angle <= norm_end
825 }
826 }
827
828 fn format_legend_text(&self, slice: &PieSlice, total: f64, spacing: &str) -> String {
829 if self.show_percentages {
830 let percent = Self::value_percent(slice.value, total);
831 format!(
832 "{} {} {:.1}%{}",
833 self.legend_marker, slice.label, percent, spacing
834 )
835 } else {
836 format!("{} {}{}", self.legend_marker, slice.label, spacing)
837 }
838 }
839
840 fn legend_item_width(&self, slice: &PieSlice, total: f64) -> u16 {
843 u16::try_from(self.format_legend_text(slice, total, " ").chars().count())
844 .unwrap_or(u16::MAX)
845 }
846
847 fn calculate_aligned_x(&self, legend_area: Rect, content_width: u16) -> u16 {
848 match self.legend_alignment {
849 LegendAlignment::Left => legend_area.x,
850 LegendAlignment::Center => {
851 legend_area.x + (legend_area.width.saturating_sub(content_width)) / 2
852 }
853 LegendAlignment::Right => {
854 legend_area.x + legend_area.width.saturating_sub(content_width)
855 }
856 }
857 }
858
859 fn render_legend(&self, buf: &mut Buffer, legend_area: Rect) {
860 let total = self.total_value();
861
862 match self.legend_layout {
863 LegendLayout::Vertical => {
864 self.render_vertical_legend(buf, legend_area, total);
865 }
866 LegendLayout::Horizontal => {
867 self.render_horizontal_legend(buf, legend_area, total);
868 }
869 }
870 }
871
872 fn render_vertical_legend(&self, buf: &mut Buffer, legend_area: Rect, total: f64) {
873 for (idx, slice) in self.slices.iter().enumerate() {
874 #[allow(clippy::cast_possible_truncation)]
875 let y_offset = (idx as u16) * 2;
876
877 if y_offset >= legend_area.height {
878 break;
879 }
880
881 let legend_text = self.format_legend_text(slice, total, "");
882 #[allow(clippy::cast_possible_truncation)]
883 let text_width = u16::try_from(legend_text.chars().count()).unwrap_or(u16::MAX);
884 let x_pos = self.calculate_aligned_x(legend_area, text_width);
885
886 let line = Line::from(vec![Span::styled(
887 legend_text,
888 Style::default().fg(slice.color),
889 )]);
890 let item_area = Rect {
891 x: x_pos,
892 y: legend_area.y + y_offset,
893 width: text_width.min(legend_area.width),
894 height: 1,
895 };
896
897 line.render(item_area, buf);
898 }
899 }
900
901 fn render_horizontal_legend(&self, buf: &mut Buffer, legend_area: Rect, total: f64) {
902 let mut total_width = 0u16;
903 let mut item_widths = Vec::new();
904
905 for slice in &self.slices {
906 let legend_text = self.format_legend_text(slice, total, " ");
907 #[allow(clippy::cast_possible_truncation)]
908 let text_width = u16::try_from(legend_text.chars().count()).unwrap_or(u16::MAX);
909 item_widths.push(text_width);
910 total_width = total_width.saturating_add(text_width);
911 }
912
913 let start_x = self.calculate_aligned_x(legend_area, total_width.min(legend_area.width));
914 let mut x_offset = 0u16;
915
916 for (idx, slice) in self.slices.iter().enumerate() {
917 if x_offset >= legend_area.width {
918 break;
919 }
920
921 let legend_text = self.format_legend_text(slice, total, " ");
922 let text_width = item_widths[idx];
923
924 let line = Line::from(vec![Span::styled(
925 legend_text,
926 Style::default().fg(slice.color),
927 )]);
928 let item_area = Rect {
929 x: start_x + x_offset,
930 y: legend_area.y,
931 width: text_width.min(legend_area.width.saturating_sub(x_offset)),
932 height: 1,
933 };
934
935 line.render(item_area, buf);
936 x_offset = x_offset.saturating_add(text_width);
937 }
938 }
939
940 #[allow(clippy::too_many_lines)]
941 fn calculate_layout(&self, area: Rect) -> (Rect, Option<Rect>) {
942 if !self.show_legend || area.width < 20 || area.height < 10 {
943 return (area, None);
944 }
945
946 match (self.legend_position, self.legend_layout) {
948 (LegendPosition::Left | LegendPosition::Right, LegendLayout::Vertical) => {
950 let legend_width = self
951 .calculate_legend_width()
952 .min(area.width / Self::LEGEND_VERTICAL_MAX_RATIO)
953 .max(Self::LEGEND_VERTICAL_MIN_WIDTH);
954 let is_left = matches!(self.legend_position, LegendPosition::Left);
955 Self::layout_horizontal_split(area, legend_width, is_left)
956 }
957 (LegendPosition::Top | LegendPosition::Bottom, LegendLayout::Horizontal) => {
959 let is_top = matches!(self.legend_position, LegendPosition::Top);
960 Self::layout_vertical_split(area, Self::LEGEND_HORIZONTAL_HEIGHT, is_top)
961 }
962 (LegendPosition::Left | LegendPosition::Right, LegendLayout::Horizontal) => {
964 let legend_width = self
966 .calculate_legend_horizontal_width()
967 .min(
968 (area.width * (Self::LEGEND_HORIZONTAL_MAX_RATIO - 1))
969 / Self::LEGEND_HORIZONTAL_MAX_RATIO,
970 )
971 .min(Self::LEGEND_HORIZONTAL_MAX_WIDTH);
972 let is_left = matches!(self.legend_position, LegendPosition::Left);
973 Self::layout_horizontal_split(area, legend_width, is_left)
974 }
975 (LegendPosition::Top | LegendPosition::Bottom, LegendLayout::Vertical) => {
976 let legend_height = self.calculate_vertical_grid_height(area.width);
978 let is_top = matches!(self.legend_position, LegendPosition::Top);
979 Self::layout_vertical_split(area, legend_height, is_top)
980 }
981 }
982 }
983
984 fn calculate_vertical_grid_height(&self, available_width: u16) -> u16 {
985 let max_item_width = self.calculate_legend_width();
987 let columns = (available_width.saturating_sub(Self::LEGEND_PADDING * 2)
988 / max_item_width.max(1))
989 .clamp(1, 2);
990
991 #[allow(clippy::cast_possible_truncation)]
992 let num_items = self.slices.len() as u16;
993
994 let rows = num_items.div_ceil(columns);
996 (rows * 2 + Self::LEGEND_PADDING).clamp(4, Self::LEGEND_VERTICAL_MAX_HEIGHT)
998 }
999
1000 fn layout_horizontal_split(
1001 area: Rect,
1002 legend_width: u16,
1003 legend_on_left: bool,
1004 ) -> (Rect, Option<Rect>) {
1005 if area.width <= legend_width {
1006 return (area, None);
1007 }
1008
1009 let pie_width = area
1010 .width
1011 .saturating_sub(legend_width + Self::LEGEND_SPACING);
1012
1013 if legend_on_left {
1014 (
1015 Rect {
1016 x: area.x + legend_width + Self::LEGEND_SPACING,
1017 y: area.y,
1018 width: pie_width,
1019 height: area.height,
1020 },
1021 Some(Rect {
1022 x: area.x,
1023 y: area.y + Self::LEGEND_PADDING,
1024 width: legend_width,
1025 height: area.height.saturating_sub(Self::LEGEND_PADDING * 2),
1026 }),
1027 )
1028 } else {
1029 (
1030 Rect {
1031 x: area.x,
1032 y: area.y,
1033 width: pie_width,
1034 height: area.height,
1035 },
1036 Some(Rect {
1037 x: area.x + pie_width + Self::LEGEND_SPACING,
1038 y: area.y + Self::LEGEND_PADDING,
1039 width: legend_width,
1040 height: area.height.saturating_sub(Self::LEGEND_PADDING * 2),
1041 }),
1042 )
1043 }
1044 }
1045
1046 fn layout_vertical_split(
1047 area: Rect,
1048 legend_height: u16,
1049 legend_on_top: bool,
1050 ) -> (Rect, Option<Rect>) {
1051 if area.height <= legend_height {
1052 return (area, None);
1053 }
1054
1055 let pie_height = area
1056 .height
1057 .saturating_sub(legend_height + Self::LEGEND_SPACING);
1058
1059 if legend_on_top {
1060 (
1061 Rect {
1062 x: area.x,
1063 y: area.y + legend_height + Self::LEGEND_SPACING,
1064 width: area.width,
1065 height: pie_height,
1066 },
1067 Some(Rect {
1068 x: area.x + Self::LEGEND_PADDING,
1069 y: area.y + Self::LEGEND_PADDING,
1070 width: area.width.saturating_sub(Self::LEGEND_PADDING * 2),
1071 height: legend_height.saturating_sub(Self::LEGEND_PADDING),
1072 }),
1073 )
1074 } else {
1075 (
1076 Rect {
1077 x: area.x,
1078 y: area.y,
1079 width: area.width,
1080 height: pie_height,
1081 },
1082 Some(Rect {
1083 x: area.x + Self::LEGEND_PADDING,
1084 y: area.y + pie_height + Self::LEGEND_SPACING,
1085 width: area.width.saturating_sub(Self::LEGEND_PADDING * 2),
1086 height: legend_height.saturating_sub(Self::LEGEND_PADDING),
1087 }),
1088 )
1089 }
1090 }
1091
1092 fn calculate_legend_width(&self) -> u16 {
1093 let total = self.total_value();
1094 let widths = self.slices.iter().map(|s| self.legend_item_width(s, total));
1095
1096 let base = match self.legend_layout {
1097 LegendLayout::Vertical => widths.max().unwrap_or(0),
1099 LegendLayout::Horizontal => widths.fold(0u16, u16::saturating_add),
1101 };
1102
1103 base.saturating_add(2)
1104 }
1105
1106 fn calculate_legend_horizontal_width(&self) -> u16 {
1107 let total = self.total_value();
1108 self.slices
1109 .iter()
1110 .map(|s| self.legend_item_width(s, total))
1111 .fold(0u16, u16::saturating_add)
1112 .saturating_add(2)
1113 }
1114
1115 #[allow(clippy::similar_names)]
1116 fn render_piechart_braille(&self, area: Rect, buf: &mut Buffer) {
1117 let (pie_area, legend_area_opt) = self.calculate_layout(area);
1119
1120 let center_x_chars = pie_area.width / 2;
1122 let center_y_chars = pie_area.height / 2;
1123
1124 let center_x_dots = center_x_chars * 2;
1126 let center_y_dots = center_y_chars * 4;
1127
1128 let radius = (center_x_dots).min(center_y_dots).saturating_sub(2);
1134
1135 let width_dots = pie_area.width * 2;
1137 let height_dots = pie_area.height * 4;
1138
1139 let mut dot_slices: Vec<Vec<Option<usize>>> =
1140 vec![vec![None; width_dots as usize]; height_dots as usize];
1141
1142 let mut cumulative_percent = 0.0;
1144 for (slice_idx, slice) in self.slices.iter().enumerate() {
1145 let percent = self.percentage(slice);
1146 let (start_angle, end_angle, is_full_circle) =
1147 Self::slice_angles(cumulative_percent, percent);
1148
1149 for dy in 0..height_dots {
1150 for dx in 0..width_dots {
1151 let rel_x = f64::from(dx) - f64::from(center_x_dots);
1152 let rel_y = f64::from(dy) - f64::from(center_y_dots);
1153
1154 let distance = (rel_x * rel_x + rel_y * rel_y).sqrt();
1157
1158 if distance <= f64::from(radius) {
1159 let angle = rel_y.atan2(rel_x);
1160 if is_full_circle || Self::is_angle_in_slice(angle, start_angle, end_angle)
1161 {
1162 dot_slices[dy as usize][dx as usize] = Some(slice_idx);
1163 }
1164 }
1165 }
1166 }
1167
1168 cumulative_percent += percent;
1169 }
1170
1171 for char_y in 0..pie_area.height {
1173 for char_x in 0..pie_area.width {
1174 let base_dot_x = char_x * 2;
1175 let base_dot_y = char_y * 4;
1176
1177 let dot_positions = [
1184 (0, 0, 0x01), (0, 1, 0x02), (0, 2, 0x04), (1, 0, 0x08), (1, 1, 0x10), (1, 2, 0x20), (0, 3, 0x40), (1, 3, 0x80), ];
1193
1194 let mut pattern = 0u32;
1195 let mut slice_colors: Vec<(usize, u32)> = Vec::new();
1196
1197 for (dx, dy, bit) in dot_positions {
1198 let dot_x = base_dot_x + dx;
1199 let dot_y = base_dot_y + dy;
1200
1201 if dot_y < height_dots && dot_x < width_dots {
1202 if let Some(slice_idx) = dot_slices[dot_y as usize][dot_x as usize] {
1203 pattern |= bit;
1204 if let Some(entry) =
1206 slice_colors.iter_mut().find(|(idx, _)| *idx == slice_idx)
1207 {
1208 entry.1 += 1;
1209 } else {
1210 slice_colors.push((slice_idx, 1));
1211 }
1212 }
1213 }
1214 }
1215
1216 if pattern > 0 {
1217 if let Some((slice_idx, _)) = slice_colors.iter().max_by_key(|(_, count)| count)
1219 {
1220 let braille_char = char::from_u32(0x2800 + pattern).unwrap_or('⠀');
1221 let color = self.slices[*slice_idx].color;
1222
1223 let cell = &mut buf[(pie_area.x + char_x, pie_area.y + char_y)];
1224 cell.set_char(braille_char).set_fg(color);
1225 }
1226 }
1227 }
1228 }
1229
1230 if let Some(legend_area) = legend_area_opt {
1232 self.render_legend(buf, legend_area);
1233 }
1234 }
1235}
1236
1237#[cfg(test)]
1238#[allow(clippy::float_cmp)]
1239#[allow(unnameable_test_items)]
1240mod tests {
1241 use super::*;
1242
1243 #[test]
1244 fn pie_slice_new() {
1245 let slice = PieSlice::new("Test", 50.0, Color::Red);
1246 assert_eq!(slice.label(), "Test");
1247 assert_eq!(slice.value(), 50.0);
1248 assert_eq!(slice.color(), Color::Red);
1249 }
1250
1251 #[test]
1252 fn piechart_new() {
1253 let slices = vec![
1254 PieSlice::new("A", 30.0, Color::Red),
1255 PieSlice::new("B", 70.0, Color::Blue),
1256 ];
1257 let piechart = PieChart::new(slices.clone());
1258 assert_eq!(piechart.slices, slices);
1259 }
1260
1261 #[test]
1262 fn piechart_default() {
1263 let piechart = PieChart::default();
1264 assert!(piechart.slices.is_empty());
1265 assert!(piechart.show_legend);
1266 assert!(piechart.show_percentages);
1267 }
1268
1269 #[test]
1270 fn piechart_slices() {
1271 let slices = vec![PieSlice::new("Test", 100.0, Color::Green)];
1272 let piechart = PieChart::default().slices(slices.clone());
1273 assert_eq!(piechart.slices, slices);
1274 }
1275
1276 #[test]
1277 fn piechart_style() {
1278 let style = Style::default().fg(Color::Red);
1279 let piechart = PieChart::default().style(style);
1280 assert_eq!(piechart.style, style);
1281 }
1282
1283 #[test]
1284 fn piechart_show_legend() {
1285 let piechart = PieChart::default().show_legend(false);
1286 assert!(!piechart.show_legend);
1287 }
1288
1289 #[test]
1290 fn piechart_show_percentages() {
1291 let piechart = PieChart::default().show_percentages(false);
1292 assert!(!piechart.show_percentages);
1293 }
1294
1295 #[test]
1296 fn piechart_pie_char() {
1297 let piechart = PieChart::default().pie_char('█');
1298 assert_eq!(piechart.pie_char, '█');
1299 }
1300
1301 #[test]
1302 fn piechart_total_value() {
1303 let slices = vec![
1304 PieSlice::new("A", 30.0, Color::Red),
1305 PieSlice::new("B", 70.0, Color::Blue),
1306 ];
1307 let piechart = PieChart::new(slices);
1308 assert_eq!(piechart.total_value(), 100.0);
1309 }
1310
1311 #[test]
1312 fn piechart_percentage() {
1313 let slices = vec![
1314 PieSlice::new("A", 30.0, Color::Red),
1315 PieSlice::new("B", 70.0, Color::Blue),
1316 ];
1317 let piechart = PieChart::new(slices);
1318 assert_eq!(
1319 piechart.percentage(&PieSlice::new("A", 30.0, Color::Red)),
1320 30.0
1321 );
1322 }
1323
1324 render_empty_test!(piechart_render_empty_area, PieChart::default());
1326
1327 render_with_size_test!(
1328 piechart_render_with_block,
1329 {
1330 let slices = vec![PieSlice::new("Test", 100.0, Color::Red)];
1331 PieChart::new(slices).block(Block::bordered())
1332 },
1333 width: 20,
1334 height: 10
1335 );
1336
1337 render_test!(
1338 piechart_render_basic,
1339 {
1340 let slices = vec![
1341 PieSlice::new("Rust", 45.0, Color::Red),
1342 PieSlice::new("Go", 30.0, Color::Blue),
1343 PieSlice::new("Python", 25.0, Color::Green),
1344 ];
1345 PieChart::new(slices)
1346 },
1347 Rect::new(0, 0, 40, 20)
1348 );
1349
1350 #[test]
1351 fn piechart_styled_trait() {
1352 use ratatui::style::Stylize;
1353 let piechart = PieChart::default().red();
1354 assert_eq!(piechart.style.fg, Some(Color::Red));
1355 }
1356
1357 #[test]
1358 fn piechart_with_multiple_slices() {
1359 let slices = vec![
1360 PieSlice::new("A", 25.0, Color::Red),
1361 PieSlice::new("B", 25.0, Color::Blue),
1362 PieSlice::new("C", 25.0, Color::Green),
1363 PieSlice::new("D", 25.0, Color::Yellow),
1364 ];
1365 let piechart = PieChart::new(slices);
1366 assert_eq!(piechart.total_value(), 100.0);
1367 }
1368
1369 render_with_size_test!(
1371 piechart_multi_slice_render,
1372 {
1373 let slices = vec![
1374 PieSlice::new("A", 25.0, Color::Red),
1375 PieSlice::new("B", 25.0, Color::Blue),
1376 PieSlice::new("C", 25.0, Color::Green),
1377 PieSlice::new("D", 25.0, Color::Yellow),
1378 ];
1379 PieChart::new(slices)
1380 },
1381 width: 50,
1382 height: 30
1383 );
1384
1385 #[test]
1386 fn piechart_zero_values() {
1387 let slices = vec![
1388 PieSlice::new("A", 0.0, Color::Red),
1389 PieSlice::new("B", 0.0, Color::Blue),
1390 ];
1391 let piechart = PieChart::new(slices);
1392 assert_eq!(piechart.total_value(), 0.0);
1393 }
1394
1395 #[test]
1396 fn piechart_method_chaining() {
1397 use ratatui::widgets::Block;
1398
1399 let slices = vec![PieSlice::new("Test", 100.0, Color::Red)];
1400 let piechart = PieChart::new(slices)
1401 .show_legend(true)
1402 .show_percentages(true)
1403 .pie_char('█')
1404 .block(Block::bordered().title("Test"))
1405 .style(Style::default().fg(Color::White));
1406
1407 assert!(piechart.show_legend);
1408 assert!(piechart.show_percentages);
1409 assert_eq!(piechart.pie_char, '█');
1410 assert!(piechart.block.is_some());
1411 assert_eq!(piechart.style.fg, Some(Color::White));
1412 }
1413
1414 #[test]
1415 fn piechart_custom_symbols() {
1416 use crate::symbols;
1417
1418 let piechart = PieChart::default().pie_char(symbols::PIE_CHAR_BLOCK);
1419 assert_eq!(piechart.pie_char, '█');
1420
1421 let piechart = PieChart::default().pie_char(symbols::PIE_CHAR_CIRCLE);
1422 assert_eq!(piechart.pie_char, '◉');
1423
1424 let piechart = PieChart::default().pie_char(symbols::PIE_CHAR_SQUARE);
1425 assert_eq!(piechart.pie_char, '■');
1426 }
1427
1428 #[test]
1429 fn piechart_is_angle_in_slice() {
1430 use std::f64::consts::PI;
1431
1432 assert!(PieChart::is_angle_in_slice(PI / 4.0, 0.0, PI / 2.0));
1434
1435 assert!(!PieChart::is_angle_in_slice(PI, 0.0, PI / 2.0));
1437
1438 assert!(PieChart::is_angle_in_slice(0.1, 1.5 * PI, 0.5));
1440 }
1441
1442 matches_test!(
1445 piechart_resolution_standard,
1446 PieChart::default()
1447 .resolution(Resolution::Standard)
1448 .resolution,
1449 Resolution::Standard
1450 );
1451 matches_test!(
1452 piechart_resolution_braille,
1453 PieChart::default()
1454 .resolution(Resolution::Braille)
1455 .resolution,
1456 Resolution::Braille
1457 );
1458 matches_test!(
1459 piechart_high_resolution_true,
1460 PieChart::default().high_resolution(true).resolution,
1461 Resolution::Braille
1462 );
1463 matches_test!(
1464 piechart_high_resolution_false,
1465 PieChart::default().high_resolution(false).resolution,
1466 Resolution::Standard
1467 );
1468
1469 matches_test!(
1472 piechart_legend_position_left,
1473 PieChart::default()
1474 .legend_position(LegendPosition::Left)
1475 .legend_position,
1476 LegendPosition::Left
1477 );
1478 matches_test!(
1479 piechart_legend_position_right,
1480 PieChart::default()
1481 .legend_position(LegendPosition::Right)
1482 .legend_position,
1483 LegendPosition::Right
1484 );
1485 matches_test!(
1486 piechart_legend_position_top,
1487 PieChart::default()
1488 .legend_position(LegendPosition::Top)
1489 .legend_position,
1490 LegendPosition::Top
1491 );
1492 matches_test!(
1493 piechart_legend_position_bottom,
1494 PieChart::default()
1495 .legend_position(LegendPosition::Bottom)
1496 .legend_position,
1497 LegendPosition::Bottom
1498 );
1499 matches_test!(
1500 piechart_legend_layout_horizontal,
1501 PieChart::default()
1502 .legend_layout(LegendLayout::Horizontal)
1503 .legend_layout,
1504 LegendLayout::Horizontal
1505 );
1506 matches_test!(
1507 piechart_legend_layout_vertical,
1508 PieChart::default()
1509 .legend_layout(LegendLayout::Vertical)
1510 .legend_layout,
1511 LegendLayout::Vertical
1512 );
1513 matches_test!(
1514 piechart_legend_alignment_left,
1515 PieChart::default()
1516 .legend_alignment(LegendAlignment::Left)
1517 .legend_alignment,
1518 LegendAlignment::Left
1519 );
1520 matches_test!(
1521 piechart_legend_alignment_center,
1522 PieChart::default()
1523 .legend_alignment(LegendAlignment::Center)
1524 .legend_alignment,
1525 LegendAlignment::Center
1526 );
1527 matches_test!(
1528 piechart_legend_alignment_right,
1529 PieChart::default()
1530 .legend_alignment(LegendAlignment::Right)
1531 .legend_alignment,
1532 LegendAlignment::Right
1533 );
1534
1535 #[test]
1538 fn piechart_legend_marker_custom() {
1539 use crate::symbols::LEGEND_MARKER;
1540 let piechart = PieChart::default().legend_marker(LEGEND_MARKER);
1541 assert_eq!(piechart.legend_marker, LEGEND_MARKER);
1542 }
1543
1544 #[test]
1547 fn piechart_format_legend_text_with_percentage() {
1548 let slices = vec![
1549 PieSlice::new("Rust", 50.0, Color::Red),
1550 PieSlice::new("Go", 50.0, Color::Blue),
1551 ];
1552 let piechart = PieChart::new(slices.clone()).show_percentages(true);
1553 let text = piechart.format_legend_text(&slices[0], 100.0, "");
1554 assert!(text.contains("Rust"));
1555 assert!(text.contains("50.0%"));
1556 }
1557
1558 #[test]
1559 fn piechart_format_legend_text_without_percentage() {
1560 let slices = vec![PieSlice::new("Rust", 50.0, Color::Red)];
1561 let piechart = PieChart::new(slices.clone()).show_percentages(false);
1562 let text = piechart.format_legend_text(&slices[0], 100.0, "");
1563 assert!(text.contains("Rust"));
1564 assert!(!text.contains('%'));
1565 }
1566
1567 #[test]
1568 fn piechart_format_legend_text_zero_total() {
1569 let slices = vec![PieSlice::new("X", 0.0, Color::Red)];
1570 let piechart = PieChart::new(slices.clone()).show_percentages(true);
1571 let text = piechart.format_legend_text(&slices[0], 0.0, "");
1572 assert!(text.contains("0.0%"));
1573 }
1574
1575 #[test]
1578 fn piechart_calculate_aligned_x_left() {
1579 let slices = vec![PieSlice::new("A", 100.0, Color::Red)];
1580 let piechart = PieChart::new(slices).legend_alignment(LegendAlignment::Left);
1581 let area = Rect::new(5, 0, 20, 10);
1582 assert_eq!(piechart.calculate_aligned_x(area, 10), 5);
1583 }
1584
1585 #[test]
1586 fn piechart_calculate_aligned_x_center() {
1587 let slices = vec![PieSlice::new("A", 100.0, Color::Red)];
1588 let piechart = PieChart::new(slices).legend_alignment(LegendAlignment::Center);
1589 let area = Rect::new(0, 0, 20, 10);
1590 assert_eq!(piechart.calculate_aligned_x(area, 10), 5);
1592 }
1593
1594 #[test]
1595 fn piechart_calculate_aligned_x_right() {
1596 let slices = vec![PieSlice::new("A", 100.0, Color::Red)];
1597 let piechart = PieChart::new(slices).legend_alignment(LegendAlignment::Right);
1598 let area = Rect::new(0, 0, 20, 10);
1599 assert_eq!(piechart.calculate_aligned_x(area, 10), 10);
1601 }
1602
1603 render_test!(
1606 piechart_render_braille,
1607 {
1608 let slices = vec![
1609 PieSlice::new("Rust", 60.0, Color::Red),
1610 PieSlice::new("Go", 40.0, Color::Blue),
1611 ];
1612 PieChart::new(slices).resolution(Resolution::Braille)
1613 },
1614 Rect::new(0, 0, 40, 20)
1615 );
1616
1617 render_test!(
1618 piechart_render_braille_with_legend,
1619 {
1620 let slices = vec![
1621 PieSlice::new("Rust", 60.0, Color::Red),
1622 PieSlice::new("Go", 40.0, Color::Blue),
1623 ];
1624 PieChart::new(slices)
1625 .resolution(Resolution::Braille)
1626 .show_legend(true)
1627 },
1628 Rect::new(0, 0, 40, 20)
1629 );
1630
1631 render_test!(
1634 piechart_render_legend_left,
1635 {
1636 let slices = vec![
1637 PieSlice::new("Alpha", 50.0, Color::Red),
1638 PieSlice::new("Beta", 50.0, Color::Blue),
1639 ];
1640 PieChart::new(slices)
1641 .show_legend(true)
1642 .legend_position(LegendPosition::Left)
1643 .legend_layout(LegendLayout::Vertical)
1644 },
1645 Rect::new(0, 0, 60, 20)
1646 );
1647
1648 render_test!(
1649 piechart_render_legend_right,
1650 {
1651 let slices = vec![
1652 PieSlice::new("Alpha", 50.0, Color::Red),
1653 PieSlice::new("Beta", 50.0, Color::Blue),
1654 ];
1655 PieChart::new(slices)
1656 .show_legend(true)
1657 .legend_position(LegendPosition::Right)
1658 .legend_layout(LegendLayout::Vertical)
1659 },
1660 Rect::new(0, 0, 60, 20)
1661 );
1662
1663 render_test!(
1664 piechart_render_legend_top_horizontal,
1665 {
1666 let slices = vec![
1667 PieSlice::new("Alpha", 50.0, Color::Red),
1668 PieSlice::new("Beta", 50.0, Color::Blue),
1669 ];
1670 PieChart::new(slices)
1671 .show_legend(true)
1672 .legend_position(LegendPosition::Top)
1673 .legend_layout(LegendLayout::Horizontal)
1674 },
1675 Rect::new(0, 0, 60, 20)
1676 );
1677
1678 render_test!(
1679 piechart_render_legend_bottom_horizontal,
1680 {
1681 let slices = vec![
1682 PieSlice::new("Alpha", 50.0, Color::Red),
1683 PieSlice::new("Beta", 50.0, Color::Blue),
1684 ];
1685 PieChart::new(slices)
1686 .show_legend(true)
1687 .legend_position(LegendPosition::Bottom)
1688 .legend_layout(LegendLayout::Horizontal)
1689 },
1690 Rect::new(0, 0, 60, 20)
1691 );
1692
1693 render_test!(
1694 piechart_render_legend_top_vertical,
1695 {
1696 let slices = vec![
1697 PieSlice::new("Alpha", 50.0, Color::Red),
1698 PieSlice::new("Beta", 50.0, Color::Blue),
1699 ];
1700 PieChart::new(slices)
1701 .show_legend(true)
1702 .legend_position(LegendPosition::Top)
1703 .legend_layout(LegendLayout::Vertical)
1704 },
1705 Rect::new(0, 0, 60, 20)
1706 );
1707
1708 render_test!(
1709 piechart_render_legend_bottom_vertical,
1710 {
1711 let slices = vec![
1712 PieSlice::new("Alpha", 50.0, Color::Red),
1713 PieSlice::new("Beta", 50.0, Color::Blue),
1714 ];
1715 PieChart::new(slices)
1716 .show_legend(true)
1717 .legend_position(LegendPosition::Bottom)
1718 .legend_layout(LegendLayout::Vertical)
1719 },
1720 Rect::new(0, 0, 60, 20)
1721 );
1722
1723 render_test!(
1724 piechart_render_legend_left_horizontal,
1725 {
1726 let slices = vec![
1727 PieSlice::new("Alpha", 50.0, Color::Red),
1728 PieSlice::new("Beta", 50.0, Color::Blue),
1729 ];
1730 PieChart::new(slices)
1731 .show_legend(true)
1732 .legend_position(LegendPosition::Left)
1733 .legend_layout(LegendLayout::Horizontal)
1734 },
1735 Rect::new(0, 0, 60, 20)
1736 );
1737
1738 render_test!(
1739 piechart_render_legend_right_horizontal,
1740 {
1741 let slices = vec![
1742 PieSlice::new("Alpha", 50.0, Color::Red),
1743 PieSlice::new("Beta", 50.0, Color::Blue),
1744 ];
1745 PieChart::new(slices)
1746 .show_legend(true)
1747 .legend_position(LegendPosition::Right)
1748 .legend_layout(LegendLayout::Horizontal)
1749 },
1750 Rect::new(0, 0, 60, 20)
1751 );
1752
1753 render_test!(
1756 piechart_render_legend_with_percentages,
1757 {
1758 let slices = vec![
1759 PieSlice::new("Rust", 45.0, Color::Red),
1760 PieSlice::new("Go", 30.0, Color::Blue),
1761 PieSlice::new("Python", 25.0, Color::Green),
1762 ];
1763 PieChart::new(slices)
1764 .show_legend(true)
1765 .show_percentages(true)
1766 },
1767 Rect::new(0, 0, 60, 20)
1768 );
1769
1770 render_test!(
1772 piechart_render_legend_alignment_center,
1773 {
1774 let slices = vec![
1775 PieSlice::new("A", 50.0, Color::Red),
1776 PieSlice::new("B", 50.0, Color::Blue),
1777 ];
1778 PieChart::new(slices)
1779 .show_legend(true)
1780 .legend_alignment(LegendAlignment::Center)
1781 },
1782 Rect::new(0, 0, 60, 20)
1783 );
1784
1785 render_test!(
1786 piechart_render_legend_alignment_right,
1787 {
1788 let slices = vec![
1789 PieSlice::new("A", 50.0, Color::Red),
1790 PieSlice::new("B", 50.0, Color::Blue),
1791 ];
1792 PieChart::new(slices)
1793 .show_legend(true)
1794 .legend_alignment(LegendAlignment::Right)
1795 },
1796 Rect::new(0, 0, 60, 20)
1797 );
1798
1799 #[test]
1802 fn piechart_layout_too_small_no_legend() {
1803 let slices = vec![PieSlice::new("A", 100.0, Color::Red)];
1804 let piechart = PieChart::new(slices).show_legend(true);
1805 let area = Rect::new(0, 0, 10, 5);
1807 let (pie_area, legend_opt) = piechart.calculate_layout(area);
1808 assert_eq!(pie_area, area);
1809 assert!(legend_opt.is_none());
1810 }
1811
1812 #[test]
1813 fn piechart_layout_show_legend_false_no_legend() {
1814 let slices = vec![PieSlice::new("A", 100.0, Color::Red)];
1815 let piechart = PieChart::new(slices).show_legend(false);
1816 let area = Rect::new(0, 0, 60, 20);
1817 let (pie_area, legend_opt) = piechart.calculate_layout(area);
1818 assert_eq!(pie_area, area);
1819 assert!(legend_opt.is_none());
1820 }
1821
1822 render_empty_test!(piechart_render_empty_slices, PieChart::default());
1825
1826 render_test!(
1829 piechart_render_single_slice,
1830 PieChart::new(vec![PieSlice::new("Only", 100.0, Color::Cyan)]),
1831 Rect::new(0, 0, 30, 15)
1832 );
1833
1834 #[test]
1837 fn piechart_percentage_zero_total() {
1838 let slices = vec![PieSlice::new("A", 0.0, Color::Red)];
1839 let piechart = PieChart::new(slices.clone());
1840 assert_eq!(piechart.percentage(&slices[0]), 0.0);
1841 }
1842
1843 #[test]
1848 fn piechart_calculate_legend_width_vertical_zero_total_with_percentages() {
1849 let slices = vec![
1850 PieSlice::new("A", 0.0, Color::Red),
1851 PieSlice::new("B", 0.0, Color::Blue),
1852 ];
1853 let piechart = PieChart::new(slices)
1854 .legend_layout(LegendLayout::Vertical)
1855 .show_percentages(true);
1856 let width = piechart.calculate_legend_width();
1857 assert!(width > 0);
1859 }
1860
1861 render_test!(
1862 piechart_render_legend_vertical_zero_total_with_percentages,
1863 PieChart::new(vec![
1864 PieSlice::new("A", 0.0, Color::Red),
1865 PieSlice::new("B", 0.0, Color::Blue),
1866 ])
1867 .show_legend(true)
1868 .show_percentages(true)
1869 .legend_position(LegendPosition::Right)
1870 .legend_layout(LegendLayout::Vertical),
1871 Rect::new(0, 0, 60, 20)
1872 );
1873
1874 render_test!(
1877 piechart_render_legend_vertical_no_percentages,
1878 PieChart::new(vec![
1879 PieSlice::new("Rust", 60.0, Color::Red),
1880 PieSlice::new("Go", 40.0, Color::Blue),
1881 ])
1882 .show_legend(true)
1883 .show_percentages(false)
1884 .legend_position(LegendPosition::Right)
1885 .legend_layout(LegendLayout::Vertical),
1886 Rect::new(0, 0, 60, 20)
1887 );
1888
1889 render_test!(
1892 piechart_render_all_zero_values,
1893 PieChart::new(vec![
1894 PieSlice::new("A", 0.0, Color::Red),
1895 PieSlice::new("B", 0.0, Color::Blue),
1896 ]),
1897 Rect::new(0, 0, 40, 20)
1898 );
1899
1900 render_test!(
1903 piechart_render_zero_value_slice_in_mix,
1904 PieChart::new(vec![
1905 PieSlice::new("A", 100.0, Color::Red),
1906 PieSlice::new("B", 0.0, Color::Blue),
1907 ]),
1908 Rect::new(0, 0, 40, 20)
1909 );
1910
1911 render_test!(
1914 piechart_render_tiny_area_radius_zero,
1915 PieChart::new(vec![PieSlice::new("A", 100.0, Color::Red)]),
1916 Rect::new(0, 0, 1, 1)
1917 );
1918
1919 render_test!(
1923 piechart_render_vertical_legend_overflow,
1924 PieChart::new(vec![
1925 PieSlice::new("Slice1", 10.0, Color::Red),
1926 PieSlice::new("Slice2", 10.0, Color::Blue),
1927 PieSlice::new("Slice3", 10.0, Color::Green),
1928 PieSlice::new("Slice4", 10.0, Color::Yellow),
1929 PieSlice::new("Slice5", 10.0, Color::Cyan),
1930 PieSlice::new("Slice6", 10.0, Color::Magenta),
1931 PieSlice::new("Slice7", 10.0, Color::White),
1932 PieSlice::new("Slice8", 10.0, Color::Red),
1933 PieSlice::new("Slice9", 10.0, Color::Blue),
1934 PieSlice::new("Slice10", 10.0, Color::Green),
1935 ])
1936 .show_legend(true)
1937 .legend_position(LegendPosition::Right)
1938 .legend_layout(LegendLayout::Vertical),
1939 Rect::new(0, 0, 60, 12)
1941 );
1942
1943 render_test!(
1947 piechart_render_horizontal_legend_overflow,
1948 PieChart::new(vec![
1949 PieSlice::new("LongLabelA", 20.0, Color::Red),
1950 PieSlice::new("LongLabelB", 20.0, Color::Blue),
1951 PieSlice::new("LongLabelC", 20.0, Color::Green),
1952 PieSlice::new("LongLabelD", 20.0, Color::Yellow),
1953 PieSlice::new("LongLabelE", 20.0, Color::Cyan),
1954 ])
1955 .show_legend(true)
1956 .legend_position(LegendPosition::Bottom)
1957 .legend_layout(LegendLayout::Horizontal),
1958 Rect::new(0, 0, 22, 15)
1960 );
1961
1962 #[test]
1965 fn piechart_layout_horizontal_split_too_narrow() {
1966 let area = Rect::new(0, 0, 5, 20);
1967 let (pie_area, legend_opt) = PieChart::<'_>::layout_horizontal_split(area, 100, true);
1969 assert_eq!(pie_area, area);
1970 assert!(legend_opt.is_none());
1971 }
1972
1973 #[test]
1976 fn piechart_layout_vertical_split_too_short() {
1977 let area = Rect::new(0, 0, 60, 3);
1978 let (pie_area, legend_opt) = PieChart::<'_>::layout_vertical_split(area, 100, true);
1980 assert_eq!(pie_area, area);
1981 assert!(legend_opt.is_none());
1982 }
1983
1984 #[test]
1988 fn piechart_calculate_legend_width_horizontal_with_percentages() {
1989 let slices = vec![
1990 PieSlice::new("Rust", 60.0, Color::Red),
1991 PieSlice::new("Go", 40.0, Color::Blue),
1992 ];
1993 let piechart = PieChart::new(slices)
1994 .legend_layout(LegendLayout::Horizontal)
1995 .show_percentages(true);
1996 let width = piechart.calculate_legend_width();
1997 assert!(width > 0);
1998 }
1999
2000 #[test]
2001 fn piechart_calculate_legend_width_horizontal_without_percentages() {
2002 let slices = vec![
2003 PieSlice::new("Alpha", 50.0, Color::Red),
2004 PieSlice::new("Beta", 50.0, Color::Blue),
2005 ];
2006 let piechart = PieChart::new(slices)
2007 .legend_layout(LegendLayout::Horizontal)
2008 .show_percentages(false);
2009 let width = piechart.calculate_legend_width();
2010 assert!(width > 0);
2011 }
2012
2013 #[test]
2014 fn piechart_calculate_legend_width_horizontal_zero_total() {
2015 let slices = vec![
2016 PieSlice::new("A", 0.0, Color::Red),
2017 PieSlice::new("B", 0.0, Color::Blue),
2018 ];
2019 let piechart = PieChart::new(slices)
2020 .legend_layout(LegendLayout::Horizontal)
2021 .show_percentages(true);
2022 let width = piechart.calculate_legend_width();
2023 assert!(width > 0);
2025 }
2026
2027 #[test]
2030 fn piechart_calculate_legend_horizontal_width_zero_total() {
2031 let slices = vec![
2032 PieSlice::new("X", 0.0, Color::Red),
2033 PieSlice::new("Y", 0.0, Color::Blue),
2034 ];
2035 let piechart = PieChart::new(slices).show_percentages(true);
2036 let width = piechart.calculate_legend_horizontal_width();
2037 assert!(width > 0);
2038 }
2039
2040 #[test]
2041 fn piechart_calculate_legend_horizontal_width_without_percentages() {
2042 let slices = vec![PieSlice::new("Item", 100.0, Color::Green)];
2043 let piechart = PieChart::new(slices).show_percentages(false);
2044 let width = piechart.calculate_legend_horizontal_width();
2045 assert!(width > 0);
2046 }
2047
2048 #[test]
2051 fn piechart_calculate_vertical_grid_height_narrow() {
2052 let slices = vec![
2053 PieSlice::new("A", 50.0, Color::Red),
2054 PieSlice::new("B", 50.0, Color::Blue),
2055 ];
2056 let piechart = PieChart::new(slices);
2057 let height = piechart.calculate_vertical_grid_height(1);
2059 assert!(height >= 4);
2060 }
2061
2062 #[test]
2065 fn piechart_value_percent_positive_total() {
2066 assert_eq!(PieChart::value_percent(25.0, 200.0), 12.5);
2067 }
2068
2069 #[test]
2070 fn piechart_value_percent_zero_total() {
2071 assert_eq!(PieChart::value_percent(25.0, 0.0), 0.0);
2072 }
2073
2074 #[test]
2075 fn piechart_value_percent_negative_total() {
2076 assert_eq!(PieChart::value_percent(25.0, -5.0), 0.0);
2078 }
2079
2080 #[test]
2081 fn piechart_slice_angles_full_circle_flag() {
2082 let (_, _, is_full) = PieChart::slice_angles(0.0, 100.0);
2083 assert!(is_full);
2084 }
2085
2086 #[test]
2087 fn piechart_slice_angles_partial_not_full() {
2088 let (start, end, is_full) = PieChart::slice_angles(0.0, 50.0);
2089 assert!(!is_full);
2090 assert!((end - start - PI).abs() < 1e-9);
2091 }
2092
2093 #[test]
2094 fn piechart_legend_item_width_matches_text() {
2095 let slices = vec![PieSlice::new("Rust", 50.0, Color::Red)];
2096 let chart = PieChart::new(slices.clone()).show_percentages(true);
2097 let expected = chart
2098 .format_legend_text(&slices[0], 100.0, " ")
2099 .chars()
2100 .count();
2101 assert_eq!(
2102 usize::from(chart.legend_item_width(&slices[0], 100.0)),
2103 expected
2104 );
2105 }
2106
2107 #[test]
2108 fn piechart_legend_item_width_unicode_label() {
2109 let slices = vec![PieSlice::new("日本語", 100.0, Color::Red)];
2111 let chart = PieChart::new(slices.clone()).show_percentages(false);
2112 assert_eq!(chart.legend_item_width(&slices[0], 100.0), 7);
2114 }
2115
2116 #[test]
2119 fn piechart_full_circle_fills_more_than_a_line() {
2120 fn filled_cells(chart: &PieChart) -> usize {
2121 let mut buffer = Buffer::empty(Rect::new(0, 0, 30, 15));
2122 Widget::render(chart, buffer.area, &mut buffer);
2123 buffer
2124 .content
2125 .iter()
2126 .filter(|c| !c.symbol().trim().is_empty())
2127 .count()
2128 }
2129
2130 let standard = PieChart::new(vec![PieSlice::new("Only", 100.0, Color::Green)])
2131 .show_legend(false)
2132 .show_percentages(false);
2133 assert!(filled_cells(&standard) > 30);
2134
2135 let braille = PieChart::new(vec![PieSlice::new("Only", 100.0, Color::Green)])
2136 .resolution(Resolution::Braille)
2137 .show_legend(false)
2138 .show_percentages(false);
2139 assert!(filled_cells(&braille) > 30);
2140 }
2141}