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 value_percent(value: f64, total: f64) -> f64 {
558 if total > 0.0 {
559 (value / total) * 100.0
560 } else {
561 0.0
562 }
563 }
564
565 fn percentage(&self, slice: &PieSlice) -> f64 {
567 Self::value_percent(slice.value, self.total_value())
568 }
569}
570
571impl Styled for PieChart<'_> {
572 type Item = Self;
573
574 fn style(&self) -> Style {
575 self.style
576 }
577
578 fn set_style<S: Into<Style>>(mut self, style: S) -> Self::Item {
579 self.style = style.into();
580 self
581 }
582}
583
584impl Widget for PieChart<'_> {
585 fn render(self, area: Rect, buf: &mut Buffer) {
586 Widget::render(&self, area, buf);
587 }
588}
589
590impl Widget for &PieChart<'_> {
591 fn render(self, area: Rect, buf: &mut Buffer) {
592 buf.set_style(area, self.style);
593 let inner = if let Some(ref block) = self.block {
594 let inner_area = block.inner(area);
595 block.render(area, buf);
596 inner_area
597 } else {
598 area
599 };
600 self.render_piechart(inner, buf);
601 }
602}
603
604impl PieChart<'_> {
605 const LEGEND_VERTICAL_MAX_RATIO: u16 = 3;
607
608 const LEGEND_VERTICAL_MIN_WIDTH: u16 = 20;
610
611 const LEGEND_HORIZONTAL_MAX_RATIO: u16 = 5;
614
615 const LEGEND_HORIZONTAL_MAX_WIDTH: u16 = 60;
617
618 const LEGEND_VERTICAL_MAX_HEIGHT: u16 = 9;
621
622 const LEGEND_HORIZONTAL_HEIGHT: u16 = 3;
624
625 const LEGEND_SPACING: u16 = 1;
627
628 const LEGEND_PADDING: u16 = 1;
630
631 fn render_piechart(&self, area: Rect, buf: &mut Buffer) {
632 if area.is_empty() || self.slices.is_empty() {
633 return;
634 }
635
636 let total = self.total_value();
637 if total <= 0.0 {
638 return;
639 }
640
641 match self.resolution {
642 Resolution::Standard => {
643 }
645 Resolution::Braille => {
646 self.render_piechart_braille(area, buf);
647 return;
648 }
649 }
650
651 let (pie_area, legend_area_opt) = self.calculate_layout(area);
653
654 let center_x = pie_area.width / 2;
657 let center_y = pie_area.height / 2;
658
659 let radius = center_x.min(center_y * 2).saturating_sub(1);
661
662 let mut cumulative_percent = 0.0;
664 for slice in &self.slices {
665 let percent = self.percentage(slice);
666 self.render_slice(
667 pie_area,
668 buf,
669 center_x,
670 center_y,
671 radius,
672 cumulative_percent,
673 percent,
674 slice.color,
675 );
676 cumulative_percent += percent;
677 }
678
679 if let Some(legend_area) = legend_area_opt {
681 self.render_legend(buf, legend_area);
682 }
683 }
684
685 fn slice_angles(start_percent: f64, percent: f64) -> (f64, f64, bool) {
691 let start_angle = (start_percent / 100.0) * 2.0 * PI - PI / 2.0;
692 let end_angle = ((start_percent + percent) / 100.0) * 2.0 * PI - PI / 2.0;
693 let is_full_circle = percent >= 100.0 - f64::EPSILON;
694 (start_angle, end_angle, is_full_circle)
695 }
696
697 #[allow(clippy::too_many_arguments, clippy::similar_names)]
698 fn render_slice(
699 &self,
700 area: Rect,
701 buf: &mut Buffer,
702 center_x: u16,
703 center_y: u16,
704 radius: u16,
705 start_percent: f64,
706 percent: f64,
707 color: Color,
708 ) {
709 if radius == 0 || percent <= 0.0 {
710 return;
711 }
712
713 let (start_angle, end_angle, is_full_circle) = Self::slice_angles(start_percent, percent);
717
718 let scan_width = i32::from(radius + 1);
720 let scan_height = i32::from((radius / 2) + 1); for dy in -scan_height..=scan_height {
723 for dx in -scan_width..=scan_width {
724 let x = i32::from(area.x) + i32::from(center_x) + dx;
726 let y = i32::from(area.y) + i32::from(center_y) + dy;
727
728 if x < i32::from(area.x)
730 || x >= i32::from(area.x + area.width)
731 || y < i32::from(area.y)
732 || y >= i32::from(area.y + area.height)
733 {
734 continue;
735 }
736
737 #[allow(clippy::cast_precision_loss)]
739 let adjusted_dx = f64::from(dx);
740 #[allow(clippy::cast_precision_loss)]
741 let adjusted_dy = f64::from(dy * 2);
742
743 let distance = (adjusted_dx * adjusted_dx + adjusted_dy * adjusted_dy).sqrt();
745
746 #[allow(clippy::cast_precision_loss)]
748 if distance <= f64::from(radius) {
749 let angle = adjusted_dy.atan2(adjusted_dx);
751
752 if is_full_circle || Self::is_angle_in_slice(angle, start_angle, end_angle) {
754 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
755 {
756 let cell = &mut buf[(x as u16, y as u16)];
757 cell.set_char(self.pie_char).set_fg(color);
758 }
759 }
760 }
761 }
762 }
763 }
764
765 fn is_angle_in_slice(angle: f64, start: f64, end: f64) -> bool {
766 let normalize = |a: f64| {
768 let mut normalized = a % (2.0 * PI);
769 if normalized < 0.0 {
770 normalized += 2.0 * PI;
771 }
772 normalized
773 };
774
775 let norm_angle = normalize(angle);
776 let norm_start = normalize(start);
777 let norm_end = normalize(end);
778
779 if norm_start <= norm_end {
780 norm_angle >= norm_start && norm_angle <= norm_end
781 } else {
782 norm_angle >= norm_start || norm_angle <= norm_end
784 }
785 }
786
787 fn format_legend_text(&self, slice: &PieSlice, total: f64, spacing: &str) -> String {
788 if self.show_percentages {
789 let percent = Self::value_percent(slice.value, total);
790 format!(
791 "{} {} {:.1}%{}",
792 self.legend_marker, slice.label, percent, spacing
793 )
794 } else {
795 format!("{} {}{}", self.legend_marker, slice.label, spacing)
796 }
797 }
798
799 fn legend_item_width(&self, slice: &PieSlice, total: f64) -> u16 {
802 u16::try_from(self.format_legend_text(slice, total, " ").chars().count())
803 .unwrap_or(u16::MAX)
804 }
805
806 fn calculate_aligned_x(&self, legend_area: Rect, content_width: u16) -> u16 {
807 match self.legend_alignment {
808 LegendAlignment::Left => legend_area.x,
809 LegendAlignment::Center => {
810 legend_area.x + (legend_area.width.saturating_sub(content_width)) / 2
811 }
812 LegendAlignment::Right => {
813 legend_area.x + legend_area.width.saturating_sub(content_width)
814 }
815 }
816 }
817
818 fn render_legend(&self, buf: &mut Buffer, legend_area: Rect) {
819 let total = self.total_value();
820
821 match self.legend_layout {
822 LegendLayout::Vertical => {
823 self.render_vertical_legend(buf, legend_area, total);
824 }
825 LegendLayout::Horizontal => {
826 self.render_horizontal_legend(buf, legend_area, total);
827 }
828 }
829 }
830
831 fn render_vertical_legend(&self, buf: &mut Buffer, legend_area: Rect, total: f64) {
832 for (idx, slice) in self.slices.iter().enumerate() {
833 #[allow(clippy::cast_possible_truncation)]
834 let y_offset = (idx as u16) * 2;
835
836 if y_offset >= legend_area.height {
837 break;
838 }
839
840 let legend_text = self.format_legend_text(slice, total, "");
841 #[allow(clippy::cast_possible_truncation)]
842 let text_width = u16::try_from(legend_text.chars().count()).unwrap_or(u16::MAX);
843 let x_pos = self.calculate_aligned_x(legend_area, text_width);
844
845 let line = Line::from(vec![Span::styled(
846 legend_text,
847 Style::default().fg(slice.color),
848 )]);
849 let item_area = Rect {
850 x: x_pos,
851 y: legend_area.y + y_offset,
852 width: text_width.min(legend_area.width),
853 height: 1,
854 };
855
856 line.render(item_area, buf);
857 }
858 }
859
860 fn render_horizontal_legend(&self, buf: &mut Buffer, legend_area: Rect, total: f64) {
861 let mut total_width = 0u16;
862 let mut item_widths = Vec::new();
863
864 for slice in &self.slices {
865 let legend_text = self.format_legend_text(slice, total, " ");
866 #[allow(clippy::cast_possible_truncation)]
867 let text_width = u16::try_from(legend_text.chars().count()).unwrap_or(u16::MAX);
868 item_widths.push(text_width);
869 total_width = total_width.saturating_add(text_width);
870 }
871
872 let start_x = self.calculate_aligned_x(legend_area, total_width.min(legend_area.width));
873 let mut x_offset = 0u16;
874
875 for (idx, slice) in self.slices.iter().enumerate() {
876 if x_offset >= legend_area.width {
877 break;
878 }
879
880 let legend_text = self.format_legend_text(slice, total, " ");
881 let text_width = item_widths[idx];
882
883 let line = Line::from(vec![Span::styled(
884 legend_text,
885 Style::default().fg(slice.color),
886 )]);
887 let item_area = Rect {
888 x: start_x + x_offset,
889 y: legend_area.y,
890 width: text_width.min(legend_area.width.saturating_sub(x_offset)),
891 height: 1,
892 };
893
894 line.render(item_area, buf);
895 x_offset = x_offset.saturating_add(text_width);
896 }
897 }
898
899 #[allow(clippy::too_many_lines)]
900 fn calculate_layout(&self, area: Rect) -> (Rect, Option<Rect>) {
901 if !self.show_legend || area.width < 20 || area.height < 10 {
902 return (area, None);
903 }
904
905 match (self.legend_position, self.legend_layout) {
907 (LegendPosition::Left | LegendPosition::Right, LegendLayout::Vertical) => {
909 let legend_width = self
910 .calculate_legend_width()
911 .min(area.width / Self::LEGEND_VERTICAL_MAX_RATIO)
912 .max(Self::LEGEND_VERTICAL_MIN_WIDTH);
913 let is_left = matches!(self.legend_position, LegendPosition::Left);
914 Self::layout_horizontal_split(area, legend_width, is_left)
915 }
916 (LegendPosition::Top | LegendPosition::Bottom, LegendLayout::Horizontal) => {
918 let is_top = matches!(self.legend_position, LegendPosition::Top);
919 Self::layout_vertical_split(area, Self::LEGEND_HORIZONTAL_HEIGHT, is_top)
920 }
921 (LegendPosition::Left | LegendPosition::Right, LegendLayout::Horizontal) => {
923 let legend_width = self
925 .calculate_legend_horizontal_width()
926 .min(
927 (area.width * (Self::LEGEND_HORIZONTAL_MAX_RATIO - 1))
928 / Self::LEGEND_HORIZONTAL_MAX_RATIO,
929 )
930 .min(Self::LEGEND_HORIZONTAL_MAX_WIDTH);
931 let is_left = matches!(self.legend_position, LegendPosition::Left);
932 Self::layout_horizontal_split(area, legend_width, is_left)
933 }
934 (LegendPosition::Top | LegendPosition::Bottom, LegendLayout::Vertical) => {
935 let legend_height = self.calculate_vertical_grid_height(area.width);
937 let is_top = matches!(self.legend_position, LegendPosition::Top);
938 Self::layout_vertical_split(area, legend_height, is_top)
939 }
940 }
941 }
942
943 fn calculate_vertical_grid_height(&self, available_width: u16) -> u16 {
944 let max_item_width = self.calculate_legend_width();
946 let columns = (available_width.saturating_sub(Self::LEGEND_PADDING * 2)
947 / max_item_width.max(1))
948 .clamp(1, 2);
949
950 #[allow(clippy::cast_possible_truncation)]
951 let num_items = self.slices.len() as u16;
952
953 let rows = num_items.div_ceil(columns);
955 (rows * 2 + Self::LEGEND_PADDING).clamp(4, Self::LEGEND_VERTICAL_MAX_HEIGHT)
957 }
958
959 fn layout_horizontal_split(
960 area: Rect,
961 legend_width: u16,
962 legend_on_left: bool,
963 ) -> (Rect, Option<Rect>) {
964 if area.width <= legend_width {
965 return (area, None);
966 }
967
968 let pie_width = area
969 .width
970 .saturating_sub(legend_width + Self::LEGEND_SPACING);
971
972 if legend_on_left {
973 (
974 Rect {
975 x: area.x + legend_width + Self::LEGEND_SPACING,
976 y: area.y,
977 width: pie_width,
978 height: area.height,
979 },
980 Some(Rect {
981 x: area.x,
982 y: area.y + Self::LEGEND_PADDING,
983 width: legend_width,
984 height: area.height.saturating_sub(Self::LEGEND_PADDING * 2),
985 }),
986 )
987 } else {
988 (
989 Rect {
990 x: area.x,
991 y: area.y,
992 width: pie_width,
993 height: area.height,
994 },
995 Some(Rect {
996 x: area.x + pie_width + Self::LEGEND_SPACING,
997 y: area.y + Self::LEGEND_PADDING,
998 width: legend_width,
999 height: area.height.saturating_sub(Self::LEGEND_PADDING * 2),
1000 }),
1001 )
1002 }
1003 }
1004
1005 fn layout_vertical_split(
1006 area: Rect,
1007 legend_height: u16,
1008 legend_on_top: bool,
1009 ) -> (Rect, Option<Rect>) {
1010 if area.height <= legend_height {
1011 return (area, None);
1012 }
1013
1014 let pie_height = area
1015 .height
1016 .saturating_sub(legend_height + Self::LEGEND_SPACING);
1017
1018 if legend_on_top {
1019 (
1020 Rect {
1021 x: area.x,
1022 y: area.y + legend_height + Self::LEGEND_SPACING,
1023 width: area.width,
1024 height: pie_height,
1025 },
1026 Some(Rect {
1027 x: area.x + Self::LEGEND_PADDING,
1028 y: area.y + Self::LEGEND_PADDING,
1029 width: area.width.saturating_sub(Self::LEGEND_PADDING * 2),
1030 height: legend_height.saturating_sub(Self::LEGEND_PADDING),
1031 }),
1032 )
1033 } else {
1034 (
1035 Rect {
1036 x: area.x,
1037 y: area.y,
1038 width: area.width,
1039 height: pie_height,
1040 },
1041 Some(Rect {
1042 x: area.x + Self::LEGEND_PADDING,
1043 y: area.y + pie_height + Self::LEGEND_SPACING,
1044 width: area.width.saturating_sub(Self::LEGEND_PADDING * 2),
1045 height: legend_height.saturating_sub(Self::LEGEND_PADDING),
1046 }),
1047 )
1048 }
1049 }
1050
1051 fn calculate_legend_width(&self) -> u16 {
1052 let total = self.total_value();
1053 let widths = self.slices.iter().map(|s| self.legend_item_width(s, total));
1054
1055 let base = match self.legend_layout {
1056 LegendLayout::Vertical => widths.max().unwrap_or(0),
1058 LegendLayout::Horizontal => widths.fold(0u16, u16::saturating_add),
1060 };
1061
1062 base.saturating_add(2)
1063 }
1064
1065 fn calculate_legend_horizontal_width(&self) -> u16 {
1066 let total = self.total_value();
1067 self.slices
1068 .iter()
1069 .map(|s| self.legend_item_width(s, total))
1070 .fold(0u16, u16::saturating_add)
1071 .saturating_add(2)
1072 }
1073
1074 #[allow(clippy::similar_names)]
1075 fn render_piechart_braille(&self, area: Rect, buf: &mut Buffer) {
1076 let (pie_area, legend_area_opt) = self.calculate_layout(area);
1078
1079 let center_x_chars = pie_area.width / 2;
1081 let center_y_chars = pie_area.height / 2;
1082
1083 let center_x_dots = center_x_chars * 2;
1085 let center_y_dots = center_y_chars * 4;
1086
1087 let radius = (center_x_dots).min(center_y_dots).saturating_sub(2);
1093
1094 let width_dots = pie_area.width * 2;
1096 let height_dots = pie_area.height * 4;
1097
1098 let mut dot_slices: Vec<Vec<Option<usize>>> =
1099 vec![vec![None; width_dots as usize]; height_dots as usize];
1100
1101 let mut cumulative_percent = 0.0;
1103 for (slice_idx, slice) in self.slices.iter().enumerate() {
1104 let percent = self.percentage(slice);
1105 let (start_angle, end_angle, is_full_circle) =
1106 Self::slice_angles(cumulative_percent, percent);
1107
1108 for dy in 0..height_dots {
1109 for dx in 0..width_dots {
1110 let rel_x = f64::from(dx) - f64::from(center_x_dots);
1111 let rel_y = f64::from(dy) - f64::from(center_y_dots);
1112
1113 let distance = (rel_x * rel_x + rel_y * rel_y).sqrt();
1116
1117 if distance <= f64::from(radius) {
1118 let angle = rel_y.atan2(rel_x);
1119 if is_full_circle || Self::is_angle_in_slice(angle, start_angle, end_angle)
1120 {
1121 dot_slices[dy as usize][dx as usize] = Some(slice_idx);
1122 }
1123 }
1124 }
1125 }
1126
1127 cumulative_percent += percent;
1128 }
1129
1130 for char_y in 0..pie_area.height {
1132 for char_x in 0..pie_area.width {
1133 let base_dot_x = char_x * 2;
1134 let base_dot_y = char_y * 4;
1135
1136 let dot_positions = [
1143 (0, 0, 0x01), (0, 1, 0x02), (0, 2, 0x04), (1, 0, 0x08), (1, 1, 0x10), (1, 2, 0x20), (0, 3, 0x40), (1, 3, 0x80), ];
1152
1153 let mut pattern = 0u32;
1154 let mut slice_colors: Vec<(usize, u32)> = Vec::new();
1155
1156 for (dx, dy, bit) in dot_positions {
1157 let dot_x = base_dot_x + dx;
1158 let dot_y = base_dot_y + dy;
1159
1160 if dot_y < height_dots && dot_x < width_dots {
1161 if let Some(slice_idx) = dot_slices[dot_y as usize][dot_x as usize] {
1162 pattern |= bit;
1163 if let Some(entry) =
1165 slice_colors.iter_mut().find(|(idx, _)| *idx == slice_idx)
1166 {
1167 entry.1 += 1;
1168 } else {
1169 slice_colors.push((slice_idx, 1));
1170 }
1171 }
1172 }
1173 }
1174
1175 if pattern > 0 {
1176 if let Some((slice_idx, _)) = slice_colors.iter().max_by_key(|(_, count)| count)
1178 {
1179 let braille_char = char::from_u32(0x2800 + pattern).unwrap_or('⠀');
1180 let color = self.slices[*slice_idx].color;
1181
1182 let cell = &mut buf[(pie_area.x + char_x, pie_area.y + char_y)];
1183 cell.set_char(braille_char).set_fg(color);
1184 }
1185 }
1186 }
1187 }
1188
1189 if let Some(legend_area) = legend_area_opt {
1191 self.render_legend(buf, legend_area);
1192 }
1193 }
1194}
1195
1196#[cfg(test)]
1197#[allow(clippy::float_cmp)]
1198#[allow(unnameable_test_items)]
1199mod tests {
1200 use super::*;
1201
1202 #[test]
1203 fn pie_slice_new() {
1204 let slice = PieSlice::new("Test", 50.0, Color::Red);
1205 assert_eq!(slice.label(), "Test");
1206 assert_eq!(slice.value(), 50.0);
1207 assert_eq!(slice.color(), Color::Red);
1208 }
1209
1210 #[test]
1211 fn piechart_new() {
1212 let slices = vec![
1213 PieSlice::new("A", 30.0, Color::Red),
1214 PieSlice::new("B", 70.0, Color::Blue),
1215 ];
1216 let piechart = PieChart::new(slices.clone());
1217 assert_eq!(piechart.slices, slices);
1218 }
1219
1220 #[test]
1221 fn piechart_default() {
1222 let piechart = PieChart::default();
1223 assert!(piechart.slices.is_empty());
1224 assert!(piechart.show_legend);
1225 assert!(piechart.show_percentages);
1226 }
1227
1228 #[test]
1229 fn piechart_slices() {
1230 let slices = vec![PieSlice::new("Test", 100.0, Color::Green)];
1231 let piechart = PieChart::default().slices(slices.clone());
1232 assert_eq!(piechart.slices, slices);
1233 }
1234
1235 #[test]
1236 fn piechart_style() {
1237 let style = Style::default().fg(Color::Red);
1238 let piechart = PieChart::default().style(style);
1239 assert_eq!(piechart.style, style);
1240 }
1241
1242 #[test]
1243 fn piechart_show_legend() {
1244 let piechart = PieChart::default().show_legend(false);
1245 assert!(!piechart.show_legend);
1246 }
1247
1248 #[test]
1249 fn piechart_show_percentages() {
1250 let piechart = PieChart::default().show_percentages(false);
1251 assert!(!piechart.show_percentages);
1252 }
1253
1254 #[test]
1255 fn piechart_pie_char() {
1256 let piechart = PieChart::default().pie_char('█');
1257 assert_eq!(piechart.pie_char, '█');
1258 }
1259
1260 #[test]
1261 fn piechart_total_value() {
1262 let slices = vec![
1263 PieSlice::new("A", 30.0, Color::Red),
1264 PieSlice::new("B", 70.0, Color::Blue),
1265 ];
1266 let piechart = PieChart::new(slices);
1267 assert_eq!(piechart.total_value(), 100.0);
1268 }
1269
1270 #[test]
1271 fn piechart_percentage() {
1272 let slices = vec![
1273 PieSlice::new("A", 30.0, Color::Red),
1274 PieSlice::new("B", 70.0, Color::Blue),
1275 ];
1276 let piechart = PieChart::new(slices);
1277 assert_eq!(
1278 piechart.percentage(&PieSlice::new("A", 30.0, Color::Red)),
1279 30.0
1280 );
1281 }
1282
1283 render_empty_test!(piechart_render_empty_area, PieChart::default());
1285
1286 render_with_size_test!(
1287 piechart_render_with_block,
1288 {
1289 let slices = vec![PieSlice::new("Test", 100.0, Color::Red)];
1290 PieChart::new(slices).block(Block::bordered())
1291 },
1292 width: 20,
1293 height: 10
1294 );
1295
1296 render_test!(
1297 piechart_render_basic,
1298 {
1299 let slices = vec![
1300 PieSlice::new("Rust", 45.0, Color::Red),
1301 PieSlice::new("Go", 30.0, Color::Blue),
1302 PieSlice::new("Python", 25.0, Color::Green),
1303 ];
1304 PieChart::new(slices)
1305 },
1306 Rect::new(0, 0, 40, 20)
1307 );
1308
1309 #[test]
1310 fn piechart_styled_trait() {
1311 use ratatui::style::Stylize;
1312 let piechart = PieChart::default().red();
1313 assert_eq!(piechart.style.fg, Some(Color::Red));
1314 }
1315
1316 #[test]
1317 fn piechart_with_multiple_slices() {
1318 let slices = vec![
1319 PieSlice::new("A", 25.0, Color::Red),
1320 PieSlice::new("B", 25.0, Color::Blue),
1321 PieSlice::new("C", 25.0, Color::Green),
1322 PieSlice::new("D", 25.0, Color::Yellow),
1323 ];
1324 let piechart = PieChart::new(slices);
1325 assert_eq!(piechart.total_value(), 100.0);
1326 }
1327
1328 render_with_size_test!(
1330 piechart_multi_slice_render,
1331 {
1332 let slices = vec![
1333 PieSlice::new("A", 25.0, Color::Red),
1334 PieSlice::new("B", 25.0, Color::Blue),
1335 PieSlice::new("C", 25.0, Color::Green),
1336 PieSlice::new("D", 25.0, Color::Yellow),
1337 ];
1338 PieChart::new(slices)
1339 },
1340 width: 50,
1341 height: 30
1342 );
1343
1344 #[test]
1345 fn piechart_zero_values() {
1346 let slices = vec![
1347 PieSlice::new("A", 0.0, Color::Red),
1348 PieSlice::new("B", 0.0, Color::Blue),
1349 ];
1350 let piechart = PieChart::new(slices);
1351 assert_eq!(piechart.total_value(), 0.0);
1352 }
1353
1354 #[test]
1355 fn piechart_method_chaining() {
1356 use ratatui::widgets::Block;
1357
1358 let slices = vec![PieSlice::new("Test", 100.0, Color::Red)];
1359 let piechart = PieChart::new(slices)
1360 .show_legend(true)
1361 .show_percentages(true)
1362 .pie_char('█')
1363 .block(Block::bordered().title("Test"))
1364 .style(Style::default().fg(Color::White));
1365
1366 assert!(piechart.show_legend);
1367 assert!(piechart.show_percentages);
1368 assert_eq!(piechart.pie_char, '█');
1369 assert!(piechart.block.is_some());
1370 assert_eq!(piechart.style.fg, Some(Color::White));
1371 }
1372
1373 #[test]
1374 fn piechart_custom_symbols() {
1375 use crate::symbols;
1376
1377 let piechart = PieChart::default().pie_char(symbols::PIE_CHAR_BLOCK);
1378 assert_eq!(piechart.pie_char, '█');
1379
1380 let piechart = PieChart::default().pie_char(symbols::PIE_CHAR_CIRCLE);
1381 assert_eq!(piechart.pie_char, '◉');
1382
1383 let piechart = PieChart::default().pie_char(symbols::PIE_CHAR_SQUARE);
1384 assert_eq!(piechart.pie_char, '■');
1385 }
1386
1387 #[test]
1388 fn piechart_is_angle_in_slice() {
1389 use std::f64::consts::PI;
1390
1391 assert!(PieChart::is_angle_in_slice(PI / 4.0, 0.0, PI / 2.0));
1393
1394 assert!(!PieChart::is_angle_in_slice(PI, 0.0, PI / 2.0));
1396
1397 assert!(PieChart::is_angle_in_slice(0.1, 1.5 * PI, 0.5));
1399 }
1400
1401 matches_test!(
1404 piechart_resolution_standard,
1405 PieChart::default()
1406 .resolution(Resolution::Standard)
1407 .resolution,
1408 Resolution::Standard
1409 );
1410 matches_test!(
1411 piechart_resolution_braille,
1412 PieChart::default()
1413 .resolution(Resolution::Braille)
1414 .resolution,
1415 Resolution::Braille
1416 );
1417 matches_test!(
1418 piechart_high_resolution_true,
1419 PieChart::default().high_resolution(true).resolution,
1420 Resolution::Braille
1421 );
1422 matches_test!(
1423 piechart_high_resolution_false,
1424 PieChart::default().high_resolution(false).resolution,
1425 Resolution::Standard
1426 );
1427
1428 matches_test!(
1431 piechart_legend_position_left,
1432 PieChart::default()
1433 .legend_position(LegendPosition::Left)
1434 .legend_position,
1435 LegendPosition::Left
1436 );
1437 matches_test!(
1438 piechart_legend_position_right,
1439 PieChart::default()
1440 .legend_position(LegendPosition::Right)
1441 .legend_position,
1442 LegendPosition::Right
1443 );
1444 matches_test!(
1445 piechart_legend_position_top,
1446 PieChart::default()
1447 .legend_position(LegendPosition::Top)
1448 .legend_position,
1449 LegendPosition::Top
1450 );
1451 matches_test!(
1452 piechart_legend_position_bottom,
1453 PieChart::default()
1454 .legend_position(LegendPosition::Bottom)
1455 .legend_position,
1456 LegendPosition::Bottom
1457 );
1458 matches_test!(
1459 piechart_legend_layout_horizontal,
1460 PieChart::default()
1461 .legend_layout(LegendLayout::Horizontal)
1462 .legend_layout,
1463 LegendLayout::Horizontal
1464 );
1465 matches_test!(
1466 piechart_legend_layout_vertical,
1467 PieChart::default()
1468 .legend_layout(LegendLayout::Vertical)
1469 .legend_layout,
1470 LegendLayout::Vertical
1471 );
1472 matches_test!(
1473 piechart_legend_alignment_left,
1474 PieChart::default()
1475 .legend_alignment(LegendAlignment::Left)
1476 .legend_alignment,
1477 LegendAlignment::Left
1478 );
1479 matches_test!(
1480 piechart_legend_alignment_center,
1481 PieChart::default()
1482 .legend_alignment(LegendAlignment::Center)
1483 .legend_alignment,
1484 LegendAlignment::Center
1485 );
1486 matches_test!(
1487 piechart_legend_alignment_right,
1488 PieChart::default()
1489 .legend_alignment(LegendAlignment::Right)
1490 .legend_alignment,
1491 LegendAlignment::Right
1492 );
1493
1494 #[test]
1497 fn piechart_legend_marker_custom() {
1498 use crate::symbols::LEGEND_MARKER;
1499 let piechart = PieChart::default().legend_marker(LEGEND_MARKER);
1500 assert_eq!(piechart.legend_marker, LEGEND_MARKER);
1501 }
1502
1503 #[test]
1506 fn piechart_format_legend_text_with_percentage() {
1507 let slices = vec![
1508 PieSlice::new("Rust", 50.0, Color::Red),
1509 PieSlice::new("Go", 50.0, Color::Blue),
1510 ];
1511 let piechart = PieChart::new(slices.clone()).show_percentages(true);
1512 let text = piechart.format_legend_text(&slices[0], 100.0, "");
1513 assert!(text.contains("Rust"));
1514 assert!(text.contains("50.0%"));
1515 }
1516
1517 #[test]
1518 fn piechart_format_legend_text_without_percentage() {
1519 let slices = vec![PieSlice::new("Rust", 50.0, Color::Red)];
1520 let piechart = PieChart::new(slices.clone()).show_percentages(false);
1521 let text = piechart.format_legend_text(&slices[0], 100.0, "");
1522 assert!(text.contains("Rust"));
1523 assert!(!text.contains('%'));
1524 }
1525
1526 #[test]
1527 fn piechart_format_legend_text_zero_total() {
1528 let slices = vec![PieSlice::new("X", 0.0, Color::Red)];
1529 let piechart = PieChart::new(slices.clone()).show_percentages(true);
1530 let text = piechart.format_legend_text(&slices[0], 0.0, "");
1531 assert!(text.contains("0.0%"));
1532 }
1533
1534 #[test]
1537 fn piechart_calculate_aligned_x_left() {
1538 let slices = vec![PieSlice::new("A", 100.0, Color::Red)];
1539 let piechart = PieChart::new(slices).legend_alignment(LegendAlignment::Left);
1540 let area = Rect::new(5, 0, 20, 10);
1541 assert_eq!(piechart.calculate_aligned_x(area, 10), 5);
1542 }
1543
1544 #[test]
1545 fn piechart_calculate_aligned_x_center() {
1546 let slices = vec![PieSlice::new("A", 100.0, Color::Red)];
1547 let piechart = PieChart::new(slices).legend_alignment(LegendAlignment::Center);
1548 let area = Rect::new(0, 0, 20, 10);
1549 assert_eq!(piechart.calculate_aligned_x(area, 10), 5);
1551 }
1552
1553 #[test]
1554 fn piechart_calculate_aligned_x_right() {
1555 let slices = vec![PieSlice::new("A", 100.0, Color::Red)];
1556 let piechart = PieChart::new(slices).legend_alignment(LegendAlignment::Right);
1557 let area = Rect::new(0, 0, 20, 10);
1558 assert_eq!(piechart.calculate_aligned_x(area, 10), 10);
1560 }
1561
1562 render_test!(
1565 piechart_render_braille,
1566 {
1567 let slices = vec![
1568 PieSlice::new("Rust", 60.0, Color::Red),
1569 PieSlice::new("Go", 40.0, Color::Blue),
1570 ];
1571 PieChart::new(slices).resolution(Resolution::Braille)
1572 },
1573 Rect::new(0, 0, 40, 20)
1574 );
1575
1576 render_test!(
1577 piechart_render_braille_with_legend,
1578 {
1579 let slices = vec![
1580 PieSlice::new("Rust", 60.0, Color::Red),
1581 PieSlice::new("Go", 40.0, Color::Blue),
1582 ];
1583 PieChart::new(slices)
1584 .resolution(Resolution::Braille)
1585 .show_legend(true)
1586 },
1587 Rect::new(0, 0, 40, 20)
1588 );
1589
1590 render_test!(
1593 piechart_render_legend_left,
1594 {
1595 let slices = vec![
1596 PieSlice::new("Alpha", 50.0, Color::Red),
1597 PieSlice::new("Beta", 50.0, Color::Blue),
1598 ];
1599 PieChart::new(slices)
1600 .show_legend(true)
1601 .legend_position(LegendPosition::Left)
1602 .legend_layout(LegendLayout::Vertical)
1603 },
1604 Rect::new(0, 0, 60, 20)
1605 );
1606
1607 render_test!(
1608 piechart_render_legend_right,
1609 {
1610 let slices = vec![
1611 PieSlice::new("Alpha", 50.0, Color::Red),
1612 PieSlice::new("Beta", 50.0, Color::Blue),
1613 ];
1614 PieChart::new(slices)
1615 .show_legend(true)
1616 .legend_position(LegendPosition::Right)
1617 .legend_layout(LegendLayout::Vertical)
1618 },
1619 Rect::new(0, 0, 60, 20)
1620 );
1621
1622 render_test!(
1623 piechart_render_legend_top_horizontal,
1624 {
1625 let slices = vec![
1626 PieSlice::new("Alpha", 50.0, Color::Red),
1627 PieSlice::new("Beta", 50.0, Color::Blue),
1628 ];
1629 PieChart::new(slices)
1630 .show_legend(true)
1631 .legend_position(LegendPosition::Top)
1632 .legend_layout(LegendLayout::Horizontal)
1633 },
1634 Rect::new(0, 0, 60, 20)
1635 );
1636
1637 render_test!(
1638 piechart_render_legend_bottom_horizontal,
1639 {
1640 let slices = vec![
1641 PieSlice::new("Alpha", 50.0, Color::Red),
1642 PieSlice::new("Beta", 50.0, Color::Blue),
1643 ];
1644 PieChart::new(slices)
1645 .show_legend(true)
1646 .legend_position(LegendPosition::Bottom)
1647 .legend_layout(LegendLayout::Horizontal)
1648 },
1649 Rect::new(0, 0, 60, 20)
1650 );
1651
1652 render_test!(
1653 piechart_render_legend_top_vertical,
1654 {
1655 let slices = vec![
1656 PieSlice::new("Alpha", 50.0, Color::Red),
1657 PieSlice::new("Beta", 50.0, Color::Blue),
1658 ];
1659 PieChart::new(slices)
1660 .show_legend(true)
1661 .legend_position(LegendPosition::Top)
1662 .legend_layout(LegendLayout::Vertical)
1663 },
1664 Rect::new(0, 0, 60, 20)
1665 );
1666
1667 render_test!(
1668 piechart_render_legend_bottom_vertical,
1669 {
1670 let slices = vec![
1671 PieSlice::new("Alpha", 50.0, Color::Red),
1672 PieSlice::new("Beta", 50.0, Color::Blue),
1673 ];
1674 PieChart::new(slices)
1675 .show_legend(true)
1676 .legend_position(LegendPosition::Bottom)
1677 .legend_layout(LegendLayout::Vertical)
1678 },
1679 Rect::new(0, 0, 60, 20)
1680 );
1681
1682 render_test!(
1683 piechart_render_legend_left_horizontal,
1684 {
1685 let slices = vec![
1686 PieSlice::new("Alpha", 50.0, Color::Red),
1687 PieSlice::new("Beta", 50.0, Color::Blue),
1688 ];
1689 PieChart::new(slices)
1690 .show_legend(true)
1691 .legend_position(LegendPosition::Left)
1692 .legend_layout(LegendLayout::Horizontal)
1693 },
1694 Rect::new(0, 0, 60, 20)
1695 );
1696
1697 render_test!(
1698 piechart_render_legend_right_horizontal,
1699 {
1700 let slices = vec![
1701 PieSlice::new("Alpha", 50.0, Color::Red),
1702 PieSlice::new("Beta", 50.0, Color::Blue),
1703 ];
1704 PieChart::new(slices)
1705 .show_legend(true)
1706 .legend_position(LegendPosition::Right)
1707 .legend_layout(LegendLayout::Horizontal)
1708 },
1709 Rect::new(0, 0, 60, 20)
1710 );
1711
1712 render_test!(
1715 piechart_render_legend_with_percentages,
1716 {
1717 let slices = vec![
1718 PieSlice::new("Rust", 45.0, Color::Red),
1719 PieSlice::new("Go", 30.0, Color::Blue),
1720 PieSlice::new("Python", 25.0, Color::Green),
1721 ];
1722 PieChart::new(slices)
1723 .show_legend(true)
1724 .show_percentages(true)
1725 },
1726 Rect::new(0, 0, 60, 20)
1727 );
1728
1729 render_test!(
1731 piechart_render_legend_alignment_center,
1732 {
1733 let slices = vec![
1734 PieSlice::new("A", 50.0, Color::Red),
1735 PieSlice::new("B", 50.0, Color::Blue),
1736 ];
1737 PieChart::new(slices)
1738 .show_legend(true)
1739 .legend_alignment(LegendAlignment::Center)
1740 },
1741 Rect::new(0, 0, 60, 20)
1742 );
1743
1744 render_test!(
1745 piechart_render_legend_alignment_right,
1746 {
1747 let slices = vec![
1748 PieSlice::new("A", 50.0, Color::Red),
1749 PieSlice::new("B", 50.0, Color::Blue),
1750 ];
1751 PieChart::new(slices)
1752 .show_legend(true)
1753 .legend_alignment(LegendAlignment::Right)
1754 },
1755 Rect::new(0, 0, 60, 20)
1756 );
1757
1758 #[test]
1761 fn piechart_layout_too_small_no_legend() {
1762 let slices = vec![PieSlice::new("A", 100.0, Color::Red)];
1763 let piechart = PieChart::new(slices).show_legend(true);
1764 let area = Rect::new(0, 0, 10, 5);
1766 let (pie_area, legend_opt) = piechart.calculate_layout(area);
1767 assert_eq!(pie_area, area);
1768 assert!(legend_opt.is_none());
1769 }
1770
1771 #[test]
1772 fn piechart_layout_show_legend_false_no_legend() {
1773 let slices = vec![PieSlice::new("A", 100.0, Color::Red)];
1774 let piechart = PieChart::new(slices).show_legend(false);
1775 let area = Rect::new(0, 0, 60, 20);
1776 let (pie_area, legend_opt) = piechart.calculate_layout(area);
1777 assert_eq!(pie_area, area);
1778 assert!(legend_opt.is_none());
1779 }
1780
1781 render_empty_test!(piechart_render_empty_slices, PieChart::default());
1784
1785 render_test!(
1788 piechart_render_single_slice,
1789 PieChart::new(vec![PieSlice::new("Only", 100.0, Color::Cyan)]),
1790 Rect::new(0, 0, 30, 15)
1791 );
1792
1793 #[test]
1796 fn piechart_percentage_zero_total() {
1797 let slices = vec![PieSlice::new("A", 0.0, Color::Red)];
1798 let piechart = PieChart::new(slices.clone());
1799 assert_eq!(piechart.percentage(&slices[0]), 0.0);
1800 }
1801
1802 #[test]
1807 fn piechart_calculate_legend_width_vertical_zero_total_with_percentages() {
1808 let slices = vec![
1809 PieSlice::new("A", 0.0, Color::Red),
1810 PieSlice::new("B", 0.0, Color::Blue),
1811 ];
1812 let piechart = PieChart::new(slices)
1813 .legend_layout(LegendLayout::Vertical)
1814 .show_percentages(true);
1815 let width = piechart.calculate_legend_width();
1816 assert!(width > 0);
1818 }
1819
1820 render_test!(
1821 piechart_render_legend_vertical_zero_total_with_percentages,
1822 PieChart::new(vec![
1823 PieSlice::new("A", 0.0, Color::Red),
1824 PieSlice::new("B", 0.0, Color::Blue),
1825 ])
1826 .show_legend(true)
1827 .show_percentages(true)
1828 .legend_position(LegendPosition::Right)
1829 .legend_layout(LegendLayout::Vertical),
1830 Rect::new(0, 0, 60, 20)
1831 );
1832
1833 render_test!(
1836 piechart_render_legend_vertical_no_percentages,
1837 PieChart::new(vec![
1838 PieSlice::new("Rust", 60.0, Color::Red),
1839 PieSlice::new("Go", 40.0, Color::Blue),
1840 ])
1841 .show_legend(true)
1842 .show_percentages(false)
1843 .legend_position(LegendPosition::Right)
1844 .legend_layout(LegendLayout::Vertical),
1845 Rect::new(0, 0, 60, 20)
1846 );
1847
1848 render_test!(
1851 piechart_render_all_zero_values,
1852 PieChart::new(vec![
1853 PieSlice::new("A", 0.0, Color::Red),
1854 PieSlice::new("B", 0.0, Color::Blue),
1855 ]),
1856 Rect::new(0, 0, 40, 20)
1857 );
1858
1859 render_test!(
1862 piechart_render_zero_value_slice_in_mix,
1863 PieChart::new(vec![
1864 PieSlice::new("A", 100.0, Color::Red),
1865 PieSlice::new("B", 0.0, Color::Blue),
1866 ]),
1867 Rect::new(0, 0, 40, 20)
1868 );
1869
1870 render_test!(
1873 piechart_render_tiny_area_radius_zero,
1874 PieChart::new(vec![PieSlice::new("A", 100.0, Color::Red)]),
1875 Rect::new(0, 0, 1, 1)
1876 );
1877
1878 render_test!(
1882 piechart_render_vertical_legend_overflow,
1883 PieChart::new(vec![
1884 PieSlice::new("Slice1", 10.0, Color::Red),
1885 PieSlice::new("Slice2", 10.0, Color::Blue),
1886 PieSlice::new("Slice3", 10.0, Color::Green),
1887 PieSlice::new("Slice4", 10.0, Color::Yellow),
1888 PieSlice::new("Slice5", 10.0, Color::Cyan),
1889 PieSlice::new("Slice6", 10.0, Color::Magenta),
1890 PieSlice::new("Slice7", 10.0, Color::White),
1891 PieSlice::new("Slice8", 10.0, Color::Red),
1892 PieSlice::new("Slice9", 10.0, Color::Blue),
1893 PieSlice::new("Slice10", 10.0, Color::Green),
1894 ])
1895 .show_legend(true)
1896 .legend_position(LegendPosition::Right)
1897 .legend_layout(LegendLayout::Vertical),
1898 Rect::new(0, 0, 60, 12)
1900 );
1901
1902 render_test!(
1906 piechart_render_horizontal_legend_overflow,
1907 PieChart::new(vec![
1908 PieSlice::new("LongLabelA", 20.0, Color::Red),
1909 PieSlice::new("LongLabelB", 20.0, Color::Blue),
1910 PieSlice::new("LongLabelC", 20.0, Color::Green),
1911 PieSlice::new("LongLabelD", 20.0, Color::Yellow),
1912 PieSlice::new("LongLabelE", 20.0, Color::Cyan),
1913 ])
1914 .show_legend(true)
1915 .legend_position(LegendPosition::Bottom)
1916 .legend_layout(LegendLayout::Horizontal),
1917 Rect::new(0, 0, 22, 15)
1919 );
1920
1921 #[test]
1924 fn piechart_layout_horizontal_split_too_narrow() {
1925 let area = Rect::new(0, 0, 5, 20);
1926 let (pie_area, legend_opt) = PieChart::<'_>::layout_horizontal_split(area, 100, true);
1928 assert_eq!(pie_area, area);
1929 assert!(legend_opt.is_none());
1930 }
1931
1932 #[test]
1935 fn piechart_layout_vertical_split_too_short() {
1936 let area = Rect::new(0, 0, 60, 3);
1937 let (pie_area, legend_opt) = PieChart::<'_>::layout_vertical_split(area, 100, true);
1939 assert_eq!(pie_area, area);
1940 assert!(legend_opt.is_none());
1941 }
1942
1943 #[test]
1947 fn piechart_calculate_legend_width_horizontal_with_percentages() {
1948 let slices = vec![
1949 PieSlice::new("Rust", 60.0, Color::Red),
1950 PieSlice::new("Go", 40.0, Color::Blue),
1951 ];
1952 let piechart = PieChart::new(slices)
1953 .legend_layout(LegendLayout::Horizontal)
1954 .show_percentages(true);
1955 let width = piechart.calculate_legend_width();
1956 assert!(width > 0);
1957 }
1958
1959 #[test]
1960 fn piechart_calculate_legend_width_horizontal_without_percentages() {
1961 let slices = vec![
1962 PieSlice::new("Alpha", 50.0, Color::Red),
1963 PieSlice::new("Beta", 50.0, Color::Blue),
1964 ];
1965 let piechart = PieChart::new(slices)
1966 .legend_layout(LegendLayout::Horizontal)
1967 .show_percentages(false);
1968 let width = piechart.calculate_legend_width();
1969 assert!(width > 0);
1970 }
1971
1972 #[test]
1973 fn piechart_calculate_legend_width_horizontal_zero_total() {
1974 let slices = vec![
1975 PieSlice::new("A", 0.0, Color::Red),
1976 PieSlice::new("B", 0.0, Color::Blue),
1977 ];
1978 let piechart = PieChart::new(slices)
1979 .legend_layout(LegendLayout::Horizontal)
1980 .show_percentages(true);
1981 let width = piechart.calculate_legend_width();
1982 assert!(width > 0);
1984 }
1985
1986 #[test]
1989 fn piechart_calculate_legend_horizontal_width_zero_total() {
1990 let slices = vec![
1991 PieSlice::new("X", 0.0, Color::Red),
1992 PieSlice::new("Y", 0.0, Color::Blue),
1993 ];
1994 let piechart = PieChart::new(slices).show_percentages(true);
1995 let width = piechart.calculate_legend_horizontal_width();
1996 assert!(width > 0);
1997 }
1998
1999 #[test]
2000 fn piechart_calculate_legend_horizontal_width_without_percentages() {
2001 let slices = vec![PieSlice::new("Item", 100.0, Color::Green)];
2002 let piechart = PieChart::new(slices).show_percentages(false);
2003 let width = piechart.calculate_legend_horizontal_width();
2004 assert!(width > 0);
2005 }
2006
2007 #[test]
2010 fn piechart_calculate_vertical_grid_height_narrow() {
2011 let slices = vec![
2012 PieSlice::new("A", 50.0, Color::Red),
2013 PieSlice::new("B", 50.0, Color::Blue),
2014 ];
2015 let piechart = PieChart::new(slices);
2016 let height = piechart.calculate_vertical_grid_height(1);
2018 assert!(height >= 4);
2019 }
2020
2021 #[test]
2024 fn piechart_value_percent_positive_total() {
2025 assert_eq!(PieChart::value_percent(25.0, 200.0), 12.5);
2026 }
2027
2028 #[test]
2029 fn piechart_value_percent_zero_total() {
2030 assert_eq!(PieChart::value_percent(25.0, 0.0), 0.0);
2031 }
2032
2033 #[test]
2034 fn piechart_value_percent_negative_total() {
2035 assert_eq!(PieChart::value_percent(25.0, -5.0), 0.0);
2037 }
2038
2039 #[test]
2040 fn piechart_slice_angles_full_circle_flag() {
2041 let (_, _, is_full) = PieChart::slice_angles(0.0, 100.0);
2042 assert!(is_full);
2043 }
2044
2045 #[test]
2046 fn piechart_slice_angles_partial_not_full() {
2047 let (start, end, is_full) = PieChart::slice_angles(0.0, 50.0);
2048 assert!(!is_full);
2049 assert!((end - start - PI).abs() < 1e-9);
2050 }
2051
2052 #[test]
2053 fn piechart_legend_item_width_matches_text() {
2054 let slices = vec![PieSlice::new("Rust", 50.0, Color::Red)];
2055 let chart = PieChart::new(slices.clone()).show_percentages(true);
2056 let expected = chart
2057 .format_legend_text(&slices[0], 100.0, " ")
2058 .chars()
2059 .count();
2060 assert_eq!(
2061 usize::from(chart.legend_item_width(&slices[0], 100.0)),
2062 expected
2063 );
2064 }
2065
2066 #[test]
2067 fn piechart_legend_item_width_unicode_label() {
2068 let slices = vec![PieSlice::new("日本語", 100.0, Color::Red)];
2070 let chart = PieChart::new(slices.clone()).show_percentages(false);
2071 assert_eq!(chart.legend_item_width(&slices[0], 100.0), 7);
2073 }
2074
2075 #[test]
2078 fn piechart_full_circle_fills_more_than_a_line() {
2079 fn filled_cells(chart: &PieChart) -> usize {
2080 let mut buffer = Buffer::empty(Rect::new(0, 0, 30, 15));
2081 Widget::render(chart, buffer.area, &mut buffer);
2082 buffer
2083 .content
2084 .iter()
2085 .filter(|c| !c.symbol().trim().is_empty())
2086 .count()
2087 }
2088
2089 let standard = PieChart::new(vec![PieSlice::new("Only", 100.0, Color::Green)])
2090 .show_legend(false)
2091 .show_percentages(false);
2092 assert!(filled_cells(&standard) > 30);
2093
2094 let braille = PieChart::new(vec![PieSlice::new("Only", 100.0, Color::Green)])
2095 .resolution(Resolution::Braille)
2096 .show_legend(false)
2097 .show_percentages(false);
2098 assert!(filled_cells(&braille) > 30);
2099 }
2100}