1#![warn(missing_docs)]
90#![warn(clippy::pedantic)]
91#![allow(clippy::module_name_repetitions)]
92
93use std::f64::consts::PI;
94
95use ratatui::buffer::Buffer;
96use ratatui::layout::Rect;
97use ratatui::style::{Color, Style, Styled};
98use ratatui::text::{Line, Span};
99use ratatui::widgets::{Block, Widget};
100
101pub mod border_style;
102mod braille;
103pub mod legend;
104pub mod pie_style;
105#[macro_use]
106pub mod macros;
107pub mod symbols;
108pub mod title;
109
110pub use legend::{LegendAlignment, LegendLayout, LegendPosition};
112pub use pie_style::PieStyle;
113pub use title::{BlockExt, TitleAlignment, TitlePosition, TitleStyle};
114#[cfg(feature = "theme")]
115pub use tui_nuances::{Theme, ThemeProvider, ThemeSwitcher, Themed};
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
139pub enum Resolution {
140 #[default]
144 Standard,
145
146 Braille,
151}
152
153#[derive(Debug, Clone, PartialEq)]
166pub struct PieSlice<'a> {
167 label: &'a str,
169 value: f64,
171 color: Color,
173}
174
175impl<'a> PieSlice<'a> {
176 #[must_use]
187 pub const fn new(label: &'a str, value: f64, color: Color) -> Self {
188 Self {
189 label,
190 value,
191 color,
192 }
193 }
194
195 #[cfg(feature = "theme")]
203 #[must_use]
204 pub fn themed(label: &'a str, value: f64, theme: &Theme, index: usize) -> Self {
205 Self::new(label, value, theme.color(index))
206 }
207
208 #[must_use]
210 pub const fn label(&self) -> &'a str {
211 self.label
212 }
213
214 #[must_use]
216 pub const fn value(&self) -> f64 {
217 self.value
218 }
219
220 #[must_use]
222 pub const fn color(&self) -> Color {
223 self.color
224 }
225}
226
227#[cfg(feature = "theme")]
228impl Themed for PieChart<'_> {
229 fn apply_theme(self, theme: &Theme) -> Self {
230 self.theme(theme)
231 }
232}
233
234#[derive(Debug, Clone, PartialEq)]
253pub struct PieChart<'a> {
254 slices: Vec<PieSlice<'a>>,
256 block: Option<Block<'a>>,
258 style: Style,
260 theme_style: Style,
262 show_legend: bool,
264 show_percentages: bool,
266 pie_char: char,
268 legend_marker: &'a str,
270 resolution: Resolution,
272 legend_position: LegendPosition,
274 legend_layout: LegendLayout,
276 legend_alignment: LegendAlignment,
278 pie_style: PieStyle,
280 hole: f64,
282 start: f64,
284 gap: f64,
286 explode: f64,
288 center: Option<&'a str>,
290}
291
292impl Default for PieChart<'_> {
293 fn default() -> Self {
304 Self {
305 slices: Vec::new(),
306 block: None,
307 style: Style::default(),
308 theme_style: Style::default(),
309 show_legend: true,
310 show_percentages: true,
311 pie_char: symbols::PIE_CHAR,
312 legend_marker: symbols::LEGEND_MARKER,
313 resolution: Resolution::default(),
314 legend_position: LegendPosition::default(),
315 legend_layout: LegendLayout::default(),
316 legend_alignment: LegendAlignment::default(),
317 pie_style: PieStyle::default(),
318 hole: 0.0,
319 start: 0.0,
320 gap: 0.0,
321 explode: 0.0,
322 center: None,
323 }
324 }
325}
326
327impl<'a> PieChart<'a> {
328 #[must_use]
343 pub fn new(slices: Vec<PieSlice<'a>>) -> Self {
344 Self {
345 slices,
346 ..Default::default()
347 }
348 }
349
350 #[must_use]
364 pub fn slices(mut self, slices: Vec<PieSlice<'a>>) -> Self {
365 self.slices = slices;
366 self
367 }
368
369 #[must_use]
383 pub fn block(mut self, block: Block<'a>) -> Self {
384 self.block = Some(block);
385 self
386 }
387
388 #[must_use]
400 pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
401 self.style = style.into();
402 self
403 }
404
405 #[cfg(feature = "theme")]
415 #[must_use]
416 pub fn theme(mut self, theme: &Theme) -> Self {
417 self.theme_style = theme.style();
418 self
419 }
420
421 #[must_use]
431 pub const fn show_legend(mut self, show: bool) -> Self {
432 self.show_legend = show;
433 self
434 }
435
436 #[must_use]
446 pub const fn show_percentages(mut self, show: bool) -> Self {
447 self.show_percentages = show;
448 self
449 }
450
451 #[must_use]
474 pub const fn pie_char(mut self, c: char) -> Self {
475 self.pie_char = c;
476 self
477 }
478
479 #[must_use]
509 pub const fn legend_marker(mut self, marker: &'a str) -> Self {
510 self.legend_marker = marker;
511 self
512 }
513
514 #[must_use]
529 pub const fn resolution(mut self, resolution: Resolution) -> Self {
530 self.resolution = resolution;
531 self
532 }
533
534 #[must_use]
547 pub const fn high_resolution(mut self, enabled: bool) -> Self {
548 self.resolution = if enabled {
549 Resolution::Braille
550 } else {
551 Resolution::Standard
552 };
553 self
554 }
555
556 #[must_use]
567 pub const fn legend_position(mut self, position: LegendPosition) -> Self {
568 self.legend_position = position;
569 self
570 }
571
572 #[must_use]
588 pub const fn legend_layout(mut self, layout: LegendLayout) -> Self {
589 self.legend_layout = layout;
590 self
591 }
592
593 #[must_use]
609 pub const fn legend_alignment(mut self, alignment: LegendAlignment) -> Self {
610 self.legend_alignment = alignment;
611 self
612 }
613
614 #[must_use]
624 pub fn hole(mut self, hole: f64) -> Self {
625 self.hole = if hole.is_finite() {
626 hole.clamp(0.0, 0.95)
627 } else {
628 0.0
629 };
630 self
631 }
632
633 #[must_use]
643 pub fn start_angle(mut self, degrees: f64) -> Self {
644 self.start = if degrees.is_finite() {
645 (degrees / 360.0).rem_euclid(1.0)
646 } else {
647 0.0
648 };
649 self
650 }
651
652 #[must_use]
657 pub fn gap(mut self, dots: f64) -> Self {
658 self.gap = if dots.is_finite() {
659 dots.clamp(0.0, 8.0)
660 } else {
661 0.0
662 };
663 self
664 }
665
666 #[must_use]
668 pub fn explode(mut self, dots: f64) -> Self {
669 self.explode = if dots.is_finite() {
670 dots.clamp(0.0, 8.0)
671 } else {
672 0.0
673 };
674 self
675 }
676
677 #[must_use]
679 pub const fn pie_style(mut self, style: PieStyle) -> Self {
680 self.pie_style = style;
681 self
682 }
683
684 #[must_use]
686 pub const fn center(mut self, text: &'a str) -> Self {
687 self.center = Some(text);
688 self
689 }
690
691 fn total_value(&self) -> f64 {
692 self.slices.iter().map(|s| s.value).sum()
693 }
694
695 fn value_percent(value: f64, total: f64) -> f64 {
700 if total > 0.0 {
701 (value / total) * 100.0
702 } else {
703 0.0
704 }
705 }
706
707 fn percentage(&self, slice: &PieSlice) -> f64 {
709 Self::value_percent(slice.value, self.total_value())
710 }
711}
712
713impl Styled for PieChart<'_> {
714 type Item = Self;
715
716 fn style(&self) -> Style {
717 self.style
718 }
719
720 fn set_style<S: Into<Style>>(mut self, style: S) -> Self::Item {
721 self.style = style.into();
722 self
723 }
724}
725
726impl Widget for PieChart<'_> {
727 fn render(self, area: Rect, buf: &mut Buffer) {
728 Widget::render(&self, area, buf);
729 }
730}
731
732impl Widget for &PieChart<'_> {
733 fn render(self, area: Rect, buf: &mut Buffer) {
734 buf.set_style(area, self.theme_style.patch(self.style));
735 let inner = if let Some(ref block) = self.block {
736 let inner_area = block.inner(area);
737 block.render(area, buf);
738 inner_area
739 } else {
740 area
741 };
742 self.render_piechart(inner, buf);
743 }
744}
745
746impl PieChart<'_> {
747 const LEGEND_VERTICAL_MAX_RATIO: u16 = 3;
749
750 const LEGEND_VERTICAL_MIN_WIDTH: u16 = 20;
752
753 const LEGEND_HORIZONTAL_MAX_RATIO: u16 = 5;
756
757 const LEGEND_HORIZONTAL_MAX_WIDTH: u16 = 60;
759
760 const LEGEND_VERTICAL_MAX_HEIGHT: u16 = 9;
763
764 const LEGEND_HORIZONTAL_HEIGHT: u16 = 3;
766
767 const LEGEND_SPACING: u16 = 1;
769
770 const LEGEND_PADDING: u16 = 1;
772
773 fn render_piechart(&self, area: Rect, buf: &mut Buffer) {
774 if area.is_empty() || self.slices.is_empty() {
775 return;
776 }
777
778 let total = self.total_value();
779 if total <= 0.0 {
780 return;
781 }
782
783 if self.needs_canvas() {
784 self.render_piechart_canvas(area, buf);
785 return;
786 }
787
788 match self.resolution {
789 Resolution::Standard => {
790 }
792 Resolution::Braille => {
793 self.render_piechart_braille(area, buf);
794 return;
795 }
796 }
797
798 let (pie_area, legend_area_opt) = self.calculate_layout(area);
800
801 let center_x = pie_area.width / 2;
804 let center_y = pie_area.height / 2;
805
806 let radius = center_x.min(center_y * 2).saturating_sub(1);
808
809 let mut cumulative_percent = self.start * 100.0;
811 for slice in &self.slices {
812 let percent = self.percentage(slice);
813 self.render_slice(
814 pie_area,
815 buf,
816 center_x,
817 center_y,
818 radius,
819 cumulative_percent,
820 percent,
821 slice.color,
822 );
823 cumulative_percent += percent;
824 }
825
826 self.draw_center(pie_area, buf);
827
828 if let Some(legend_area) = legend_area_opt {
830 self.render_legend(buf, legend_area);
831 }
832 }
833
834 fn slice_angles(start_percent: f64, percent: f64) -> (f64, f64, bool) {
840 let start_angle = (start_percent / 100.0) * 2.0 * PI - PI / 2.0;
841 let end_angle = ((start_percent + percent) / 100.0) * 2.0 * PI - PI / 2.0;
842 let is_full_circle = percent >= 100.0 - f64::EPSILON;
843 (start_angle, end_angle, is_full_circle)
844 }
845
846 #[allow(clippy::too_many_arguments, clippy::similar_names)]
847 fn render_slice(
848 &self,
849 area: Rect,
850 buf: &mut Buffer,
851 center_x: u16,
852 center_y: u16,
853 radius: u16,
854 start_percent: f64,
855 percent: f64,
856 color: Color,
857 ) {
858 if radius == 0 || percent <= 0.0 {
859 return;
860 }
861
862 let (start_angle, end_angle, is_full_circle) = Self::slice_angles(start_percent, percent);
866
867 let scan_width = i32::from(radius + 1);
869 let scan_height = i32::from((radius / 2) + 1); for dy in -scan_height..=scan_height {
872 for dx in -scan_width..=scan_width {
873 let x = i32::from(area.x) + i32::from(center_x) + dx;
875 let y = i32::from(area.y) + i32::from(center_y) + dy;
876
877 if x < i32::from(area.x)
879 || x >= i32::from(area.x + area.width)
880 || y < i32::from(area.y)
881 || y >= i32::from(area.y + area.height)
882 {
883 continue;
884 }
885
886 #[allow(clippy::cast_precision_loss)]
888 let adjusted_dx = f64::from(dx);
889 #[allow(clippy::cast_precision_loss)]
890 let adjusted_dy = f64::from(dy * 2);
891
892 let distance = (adjusted_dx * adjusted_dx + adjusted_dy * adjusted_dy).sqrt();
894
895 #[allow(clippy::cast_precision_loss)]
897 if distance <= f64::from(radius) && distance >= self.hole * f64::from(radius) {
898 let angle = adjusted_dy.atan2(adjusted_dx);
900
901 if is_full_circle || Self::is_angle_in_slice(angle, start_angle, end_angle) {
903 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
904 {
905 let cell = &mut buf[(x as u16, y as u16)];
906 cell.set_char(self.pie_char).set_fg(color);
907 }
908 }
909 }
910 }
911 }
912 }
913
914 fn is_angle_in_slice(angle: f64, start: f64, end: f64) -> bool {
915 let normalize = |a: f64| {
917 let mut normalized = a % (2.0 * PI);
918 if normalized < 0.0 {
919 normalized += 2.0 * PI;
920 }
921 normalized
922 };
923
924 let norm_angle = normalize(angle);
925 let norm_start = normalize(start);
926 let norm_end = normalize(end);
927
928 if norm_start <= norm_end {
929 norm_angle >= norm_start && norm_angle <= norm_end
930 } else {
931 norm_angle >= norm_start || norm_angle <= norm_end
933 }
934 }
935
936 fn format_legend_text(&self, slice: &PieSlice, total: f64, spacing: &str) -> String {
937 if self.show_percentages {
938 let percent = Self::value_percent(slice.value, total);
939 format!(
940 "{} {} {:.1}%{}",
941 self.legend_marker, slice.label, percent, spacing
942 )
943 } else {
944 format!("{} {}{}", self.legend_marker, slice.label, spacing)
945 }
946 }
947
948 fn legend_item_width(&self, slice: &PieSlice, total: f64) -> u16 {
951 u16::try_from(self.format_legend_text(slice, total, " ").chars().count())
952 .unwrap_or(u16::MAX)
953 }
954
955 fn calculate_aligned_x(&self, legend_area: Rect, content_width: u16) -> u16 {
956 match self.legend_alignment {
957 LegendAlignment::Left => legend_area.x,
958 LegendAlignment::Center => {
959 legend_area.x + (legend_area.width.saturating_sub(content_width)) / 2
960 }
961 LegendAlignment::Right => {
962 legend_area.x + legend_area.width.saturating_sub(content_width)
963 }
964 }
965 }
966
967 fn render_legend(&self, buf: &mut Buffer, legend_area: Rect) {
968 let total = self.total_value();
969
970 match self.legend_layout {
971 LegendLayout::Vertical => {
972 self.render_vertical_legend(buf, legend_area, total);
973 }
974 LegendLayout::Horizontal => {
975 self.render_horizontal_legend(buf, legend_area, total);
976 }
977 }
978 }
979
980 fn render_vertical_legend(&self, buf: &mut Buffer, legend_area: Rect, total: f64) {
981 for (idx, slice) in self.slices.iter().enumerate() {
982 #[allow(clippy::cast_possible_truncation)]
983 let y_offset = (idx as u16) * 2;
984
985 if y_offset >= legend_area.height {
986 break;
987 }
988
989 let legend_text = self.format_legend_text(slice, total, "");
990 #[allow(clippy::cast_possible_truncation)]
991 let text_width = u16::try_from(legend_text.chars().count()).unwrap_or(u16::MAX);
992 let x_pos = self.calculate_aligned_x(legend_area, text_width);
993
994 let line = Line::from(vec![Span::styled(
995 legend_text,
996 Style::default().fg(slice.color),
997 )]);
998 let item_area = Rect {
999 x: x_pos,
1000 y: legend_area.y + y_offset,
1001 width: text_width.min(legend_area.width),
1002 height: 1,
1003 };
1004
1005 line.render(item_area, buf);
1006 }
1007 }
1008
1009 fn render_horizontal_legend(&self, buf: &mut Buffer, legend_area: Rect, total: f64) {
1010 let mut total_width = 0u16;
1011 let mut item_widths = Vec::new();
1012
1013 for slice in &self.slices {
1014 let legend_text = self.format_legend_text(slice, total, " ");
1015 #[allow(clippy::cast_possible_truncation)]
1016 let text_width = u16::try_from(legend_text.chars().count()).unwrap_or(u16::MAX);
1017 item_widths.push(text_width);
1018 total_width = total_width.saturating_add(text_width);
1019 }
1020
1021 let start_x = self.calculate_aligned_x(legend_area, total_width.min(legend_area.width));
1022 let mut x_offset = 0u16;
1023
1024 for (idx, slice) in self.slices.iter().enumerate() {
1025 if x_offset >= legend_area.width {
1026 break;
1027 }
1028
1029 let legend_text = self.format_legend_text(slice, total, " ");
1030 let text_width = item_widths[idx];
1031
1032 let line = Line::from(vec![Span::styled(
1033 legend_text,
1034 Style::default().fg(slice.color),
1035 )]);
1036 let item_area = Rect {
1037 x: start_x + x_offset,
1038 y: legend_area.y,
1039 width: text_width.min(legend_area.width.saturating_sub(x_offset)),
1040 height: 1,
1041 };
1042
1043 line.render(item_area, buf);
1044 x_offset = x_offset.saturating_add(text_width);
1045 }
1046 }
1047
1048 #[allow(clippy::too_many_lines)]
1049 fn calculate_layout(&self, area: Rect) -> (Rect, Option<Rect>) {
1050 if !self.show_legend || area.width < 20 || area.height < 10 {
1051 return (area, None);
1052 }
1053
1054 match (self.legend_position, self.legend_layout) {
1056 (LegendPosition::Left | LegendPosition::Right, LegendLayout::Vertical) => {
1058 let legend_width = self
1059 .calculate_legend_width()
1060 .min(area.width / Self::LEGEND_VERTICAL_MAX_RATIO)
1061 .max(Self::LEGEND_VERTICAL_MIN_WIDTH);
1062 let is_left = matches!(self.legend_position, LegendPosition::Left);
1063 Self::layout_horizontal_split(area, legend_width, is_left)
1064 }
1065 (LegendPosition::Top | LegendPosition::Bottom, LegendLayout::Horizontal) => {
1067 let is_top = matches!(self.legend_position, LegendPosition::Top);
1068 Self::layout_vertical_split(area, Self::LEGEND_HORIZONTAL_HEIGHT, is_top)
1069 }
1070 (LegendPosition::Left | LegendPosition::Right, LegendLayout::Horizontal) => {
1072 let legend_width = self
1074 .calculate_legend_horizontal_width()
1075 .min(
1076 (area.width * (Self::LEGEND_HORIZONTAL_MAX_RATIO - 1))
1077 / Self::LEGEND_HORIZONTAL_MAX_RATIO,
1078 )
1079 .min(Self::LEGEND_HORIZONTAL_MAX_WIDTH);
1080 let is_left = matches!(self.legend_position, LegendPosition::Left);
1081 Self::layout_horizontal_split(area, legend_width, is_left)
1082 }
1083 (LegendPosition::Top | LegendPosition::Bottom, LegendLayout::Vertical) => {
1084 let legend_height = self.calculate_vertical_grid_height(area.width);
1086 let is_top = matches!(self.legend_position, LegendPosition::Top);
1087 Self::layout_vertical_split(area, legend_height, is_top)
1088 }
1089 }
1090 }
1091
1092 fn calculate_vertical_grid_height(&self, available_width: u16) -> u16 {
1093 let max_item_width = self.calculate_legend_width();
1095 let columns = (available_width.saturating_sub(Self::LEGEND_PADDING * 2)
1096 / max_item_width.max(1))
1097 .clamp(1, 2);
1098
1099 #[allow(clippy::cast_possible_truncation)]
1100 let num_items = self.slices.len() as u16;
1101
1102 let rows = num_items.div_ceil(columns);
1104 (rows * 2 + Self::LEGEND_PADDING).clamp(4, Self::LEGEND_VERTICAL_MAX_HEIGHT)
1106 }
1107
1108 fn layout_horizontal_split(
1109 area: Rect,
1110 legend_width: u16,
1111 legend_on_left: bool,
1112 ) -> (Rect, Option<Rect>) {
1113 if area.width <= legend_width {
1114 return (area, None);
1115 }
1116
1117 let pie_width = area
1118 .width
1119 .saturating_sub(legend_width + Self::LEGEND_SPACING);
1120
1121 if legend_on_left {
1122 (
1123 Rect {
1124 x: area.x + legend_width + Self::LEGEND_SPACING,
1125 y: area.y,
1126 width: pie_width,
1127 height: area.height,
1128 },
1129 Some(Rect {
1130 x: area.x,
1131 y: area.y + Self::LEGEND_PADDING,
1132 width: legend_width,
1133 height: area.height.saturating_sub(Self::LEGEND_PADDING * 2),
1134 }),
1135 )
1136 } else {
1137 (
1138 Rect {
1139 x: area.x,
1140 y: area.y,
1141 width: pie_width,
1142 height: area.height,
1143 },
1144 Some(Rect {
1145 x: area.x + pie_width + Self::LEGEND_SPACING,
1146 y: area.y + Self::LEGEND_PADDING,
1147 width: legend_width,
1148 height: area.height.saturating_sub(Self::LEGEND_PADDING * 2),
1149 }),
1150 )
1151 }
1152 }
1153
1154 fn layout_vertical_split(
1155 area: Rect,
1156 legend_height: u16,
1157 legend_on_top: bool,
1158 ) -> (Rect, Option<Rect>) {
1159 if area.height <= legend_height {
1160 return (area, None);
1161 }
1162
1163 let pie_height = area
1164 .height
1165 .saturating_sub(legend_height + Self::LEGEND_SPACING);
1166
1167 if legend_on_top {
1168 (
1169 Rect {
1170 x: area.x,
1171 y: area.y + legend_height + Self::LEGEND_SPACING,
1172 width: area.width,
1173 height: pie_height,
1174 },
1175 Some(Rect {
1176 x: area.x + Self::LEGEND_PADDING,
1177 y: area.y + Self::LEGEND_PADDING,
1178 width: area.width.saturating_sub(Self::LEGEND_PADDING * 2),
1179 height: legend_height.saturating_sub(Self::LEGEND_PADDING),
1180 }),
1181 )
1182 } else {
1183 (
1184 Rect {
1185 x: area.x,
1186 y: area.y,
1187 width: area.width,
1188 height: pie_height,
1189 },
1190 Some(Rect {
1191 x: area.x + Self::LEGEND_PADDING,
1192 y: area.y + pie_height + Self::LEGEND_SPACING,
1193 width: area.width.saturating_sub(Self::LEGEND_PADDING * 2),
1194 height: legend_height.saturating_sub(Self::LEGEND_PADDING),
1195 }),
1196 )
1197 }
1198 }
1199
1200 fn calculate_legend_width(&self) -> u16 {
1201 let total = self.total_value();
1202 let widths = self.slices.iter().map(|s| self.legend_item_width(s, total));
1203
1204 let base = match self.legend_layout {
1205 LegendLayout::Vertical => widths.max().unwrap_or(0),
1207 LegendLayout::Horizontal => widths.fold(0u16, u16::saturating_add),
1209 };
1210
1211 base.saturating_add(2)
1212 }
1213
1214 fn calculate_legend_horizontal_width(&self) -> u16 {
1215 let total = self.total_value();
1216 self.slices
1217 .iter()
1218 .map(|s| self.legend_item_width(s, total))
1219 .fold(0u16, u16::saturating_add)
1220 .saturating_add(2)
1221 }
1222
1223 #[allow(clippy::similar_names)]
1224 fn render_piechart_braille(&self, area: Rect, buf: &mut Buffer) {
1225 let (pie_area, legend_area_opt) = self.calculate_layout(area);
1227
1228 let center_x_chars = pie_area.width / 2;
1230 let center_y_chars = pie_area.height / 2;
1231
1232 let center_x_dots = center_x_chars * 2;
1234 let center_y_dots = center_y_chars * 4;
1235
1236 let radius = (center_x_dots).min(center_y_dots).saturating_sub(2);
1242
1243 let width_dots = pie_area.width * 2;
1245 let height_dots = pie_area.height * 4;
1246
1247 let mut dot_slices: Vec<Vec<Option<usize>>> =
1248 vec![vec![None; width_dots as usize]; height_dots as usize];
1249
1250 let mut cumulative_percent = self.start * 100.0;
1252 for (slice_idx, slice) in self.slices.iter().enumerate() {
1253 let percent = self.percentage(slice);
1254 let (start_angle, end_angle, is_full_circle) =
1255 Self::slice_angles(cumulative_percent, percent);
1256
1257 for dy in 0..height_dots {
1258 for dx in 0..width_dots {
1259 let rel_x = f64::from(dx) - f64::from(center_x_dots);
1260 let rel_y = f64::from(dy) - f64::from(center_y_dots);
1261
1262 let distance = (rel_x * rel_x + rel_y * rel_y).sqrt();
1265
1266 if distance <= f64::from(radius) && distance >= self.hole * f64::from(radius) {
1267 let angle = rel_y.atan2(rel_x);
1268 if is_full_circle || Self::is_angle_in_slice(angle, start_angle, end_angle)
1269 {
1270 dot_slices[dy as usize][dx as usize] = Some(slice_idx);
1271 }
1272 }
1273 }
1274 }
1275
1276 cumulative_percent += percent;
1277 }
1278
1279 for char_y in 0..pie_area.height {
1281 for char_x in 0..pie_area.width {
1282 let base_dot_x = char_x * 2;
1283 let base_dot_y = char_y * 4;
1284
1285 let dot_positions = [
1292 (0, 0, 0x01), (0, 1, 0x02), (0, 2, 0x04), (1, 0, 0x08), (1, 1, 0x10), (1, 2, 0x20), (0, 3, 0x40), (1, 3, 0x80), ];
1301
1302 let mut pattern = 0u32;
1303 let mut slice_colors: Vec<(usize, u32)> = Vec::new();
1304
1305 for (dx, dy, bit) in dot_positions {
1306 let dot_x = base_dot_x + dx;
1307 let dot_y = base_dot_y + dy;
1308
1309 if dot_y < height_dots && dot_x < width_dots {
1310 if let Some(slice_idx) = dot_slices[dot_y as usize][dot_x as usize] {
1311 pattern |= bit;
1312 if let Some(entry) =
1314 slice_colors.iter_mut().find(|(idx, _)| *idx == slice_idx)
1315 {
1316 entry.1 += 1;
1317 } else {
1318 slice_colors.push((slice_idx, 1));
1319 }
1320 }
1321 }
1322 }
1323
1324 if pattern > 0 {
1325 if let Some((slice_idx, _)) = slice_colors.iter().max_by_key(|(_, count)| count)
1327 {
1328 let braille_char = char::from_u32(0x2800 + pattern).unwrap_or('⠀');
1329 let color = self.slices[*slice_idx].color;
1330
1331 let cell = &mut buf[(pie_area.x + char_x, pie_area.y + char_y)];
1332 cell.set_char(braille_char).set_fg(color);
1333 }
1334 }
1335 }
1336 }
1337
1338 self.draw_center(pie_area, buf);
1339
1340 if let Some(legend_area) = legend_area_opt {
1342 self.render_legend(buf, legend_area);
1343 }
1344 }
1345}
1346
1347#[cfg(test)]
1348#[allow(clippy::float_cmp)]
1349#[allow(unnameable_test_items)]
1350mod tests {
1351 use super::*;
1352
1353 #[test]
1354 fn pie_slice_new() {
1355 let slice = PieSlice::new("Test", 50.0, Color::Red);
1356 assert_eq!(slice.label(), "Test");
1357 assert_eq!(slice.value(), 50.0);
1358 assert_eq!(slice.color(), Color::Red);
1359 }
1360
1361 #[test]
1362 fn piechart_new() {
1363 let slices = vec![
1364 PieSlice::new("A", 30.0, Color::Red),
1365 PieSlice::new("B", 70.0, Color::Blue),
1366 ];
1367 let piechart = PieChart::new(slices.clone());
1368 assert_eq!(piechart.slices, slices);
1369 }
1370
1371 #[test]
1372 fn piechart_default() {
1373 let piechart = PieChart::default();
1374 assert_eq!(piechart.slices, [] as [PieSlice<'_>; 0]);
1375 assert!(piechart.show_legend);
1376 assert!(piechart.show_percentages);
1377 }
1378
1379 #[test]
1380 fn piechart_slices() {
1381 let slices = vec![PieSlice::new("Test", 100.0, Color::Green)];
1382 let piechart = PieChart::default().slices(slices.clone());
1383 assert_eq!(piechart.slices, slices);
1384 }
1385
1386 #[test]
1387 fn piechart_style() {
1388 let style = Style::default().fg(Color::Red);
1389 let piechart = PieChart::default().style(style);
1390 assert_eq!(piechart.style, style);
1391 }
1392
1393 #[test]
1394 fn piechart_show_legend() {
1395 let piechart = PieChart::default().show_legend(false);
1396 assert!(!piechart.show_legend);
1397 }
1398
1399 #[test]
1400 fn piechart_show_percentages() {
1401 let piechart = PieChart::default().show_percentages(false);
1402 assert!(!piechart.show_percentages);
1403 }
1404
1405 #[test]
1406 fn piechart_pie_char() {
1407 let piechart = PieChart::default().pie_char('█');
1408 assert_eq!(piechart.pie_char, '█');
1409 }
1410
1411 #[test]
1412 fn piechart_total_value() {
1413 let slices = vec![
1414 PieSlice::new("A", 30.0, Color::Red),
1415 PieSlice::new("B", 70.0, Color::Blue),
1416 ];
1417 let piechart = PieChart::new(slices);
1418 assert_eq!(piechart.total_value(), 100.0);
1419 }
1420
1421 #[test]
1422 fn piechart_percentage() {
1423 let slices = vec![
1424 PieSlice::new("A", 30.0, Color::Red),
1425 PieSlice::new("B", 70.0, Color::Blue),
1426 ];
1427 let piechart = PieChart::new(slices);
1428 assert_eq!(
1429 piechart.percentage(&PieSlice::new("A", 30.0, Color::Red)),
1430 30.0
1431 );
1432 }
1433
1434 render_empty_test!(piechart_render_empty_area, PieChart::default());
1436
1437 render_with_size_test!(
1438 piechart_render_with_block,
1439 {
1440 let slices = vec![PieSlice::new("Test", 100.0, Color::Red)];
1441 PieChart::new(slices).block(Block::bordered())
1442 },
1443 width: 20,
1444 height: 10
1445 );
1446
1447 render_test!(
1448 piechart_render_basic,
1449 {
1450 let slices = vec![
1451 PieSlice::new("Rust", 45.0, Color::Red),
1452 PieSlice::new("Go", 30.0, Color::Blue),
1453 PieSlice::new("Python", 25.0, Color::Green),
1454 ];
1455 PieChart::new(slices)
1456 },
1457 Rect::new(0, 0, 40, 20)
1458 );
1459
1460 #[test]
1461 fn piechart_styled_trait() {
1462 use ratatui::style::Stylize;
1463 let piechart = PieChart::default().red();
1464 assert_eq!(piechart.style.fg, Some(Color::Red));
1465 }
1466
1467 #[test]
1468 fn piechart_with_multiple_slices() {
1469 let slices = vec![
1470 PieSlice::new("A", 25.0, Color::Red),
1471 PieSlice::new("B", 25.0, Color::Blue),
1472 PieSlice::new("C", 25.0, Color::Green),
1473 PieSlice::new("D", 25.0, Color::Yellow),
1474 ];
1475 let piechart = PieChart::new(slices);
1476 assert_eq!(piechart.total_value(), 100.0);
1477 }
1478
1479 render_with_size_test!(
1481 piechart_multi_slice_render,
1482 {
1483 let slices = vec![
1484 PieSlice::new("A", 25.0, Color::Red),
1485 PieSlice::new("B", 25.0, Color::Blue),
1486 PieSlice::new("C", 25.0, Color::Green),
1487 PieSlice::new("D", 25.0, Color::Yellow),
1488 ];
1489 PieChart::new(slices)
1490 },
1491 width: 50,
1492 height: 30
1493 );
1494
1495 #[test]
1496 fn piechart_zero_values() {
1497 let slices = vec![
1498 PieSlice::new("A", 0.0, Color::Red),
1499 PieSlice::new("B", 0.0, Color::Blue),
1500 ];
1501 let piechart = PieChart::new(slices);
1502 assert_eq!(piechart.total_value(), 0.0);
1503 }
1504
1505 #[test]
1506 fn piechart_method_chaining() {
1507 use ratatui::widgets::Block;
1508
1509 let slices = vec![PieSlice::new("Test", 100.0, Color::Red)];
1510 let piechart = PieChart::new(slices)
1511 .show_legend(true)
1512 .show_percentages(true)
1513 .pie_char('█')
1514 .block(Block::bordered().title("Test"))
1515 .style(Style::default().fg(Color::White));
1516
1517 assert!(piechart.show_legend);
1518 assert!(piechart.show_percentages);
1519 assert_eq!(piechart.pie_char, '█');
1520 assert!(piechart.block.is_some());
1521 assert_eq!(piechart.style.fg, Some(Color::White));
1522 }
1523
1524 #[test]
1525 fn piechart_custom_symbols() {
1526 use crate::symbols;
1527
1528 let piechart = PieChart::default().pie_char(symbols::PIE_CHAR_BLOCK);
1529 assert_eq!(piechart.pie_char, '█');
1530
1531 let piechart = PieChart::default().pie_char(symbols::PIE_CHAR_CIRCLE);
1532 assert_eq!(piechart.pie_char, '◉');
1533
1534 let piechart = PieChart::default().pie_char(symbols::PIE_CHAR_SQUARE);
1535 assert_eq!(piechart.pie_char, '■');
1536 }
1537
1538 #[test]
1539 fn piechart_is_angle_in_slice() {
1540 use std::f64::consts::PI;
1541
1542 assert!(PieChart::is_angle_in_slice(PI / 4.0, 0.0, PI / 2.0));
1544
1545 assert!(!PieChart::is_angle_in_slice(PI, 0.0, PI / 2.0));
1547
1548 assert!(PieChart::is_angle_in_slice(0.1, 1.5 * PI, 0.5));
1550 }
1551
1552 matches_test!(
1555 piechart_resolution_standard,
1556 PieChart::default()
1557 .resolution(Resolution::Standard)
1558 .resolution,
1559 Resolution::Standard
1560 );
1561 matches_test!(
1562 piechart_resolution_braille,
1563 PieChart::default()
1564 .resolution(Resolution::Braille)
1565 .resolution,
1566 Resolution::Braille
1567 );
1568 matches_test!(
1569 piechart_high_resolution_true,
1570 PieChart::default().high_resolution(true).resolution,
1571 Resolution::Braille
1572 );
1573 matches_test!(
1574 piechart_high_resolution_false,
1575 PieChart::default().high_resolution(false).resolution,
1576 Resolution::Standard
1577 );
1578
1579 matches_test!(
1582 piechart_legend_position_left,
1583 PieChart::default()
1584 .legend_position(LegendPosition::Left)
1585 .legend_position,
1586 LegendPosition::Left
1587 );
1588 matches_test!(
1589 piechart_legend_position_right,
1590 PieChart::default()
1591 .legend_position(LegendPosition::Right)
1592 .legend_position,
1593 LegendPosition::Right
1594 );
1595 matches_test!(
1596 piechart_legend_position_top,
1597 PieChart::default()
1598 .legend_position(LegendPosition::Top)
1599 .legend_position,
1600 LegendPosition::Top
1601 );
1602 matches_test!(
1603 piechart_legend_position_bottom,
1604 PieChart::default()
1605 .legend_position(LegendPosition::Bottom)
1606 .legend_position,
1607 LegendPosition::Bottom
1608 );
1609 matches_test!(
1610 piechart_legend_layout_horizontal,
1611 PieChart::default()
1612 .legend_layout(LegendLayout::Horizontal)
1613 .legend_layout,
1614 LegendLayout::Horizontal
1615 );
1616 matches_test!(
1617 piechart_legend_layout_vertical,
1618 PieChart::default()
1619 .legend_layout(LegendLayout::Vertical)
1620 .legend_layout,
1621 LegendLayout::Vertical
1622 );
1623 matches_test!(
1624 piechart_legend_alignment_left,
1625 PieChart::default()
1626 .legend_alignment(LegendAlignment::Left)
1627 .legend_alignment,
1628 LegendAlignment::Left
1629 );
1630 matches_test!(
1631 piechart_legend_alignment_center,
1632 PieChart::default()
1633 .legend_alignment(LegendAlignment::Center)
1634 .legend_alignment,
1635 LegendAlignment::Center
1636 );
1637 matches_test!(
1638 piechart_legend_alignment_right,
1639 PieChart::default()
1640 .legend_alignment(LegendAlignment::Right)
1641 .legend_alignment,
1642 LegendAlignment::Right
1643 );
1644
1645 #[test]
1648 fn piechart_legend_marker_custom() {
1649 use crate::symbols::LEGEND_MARKER;
1650 let piechart = PieChart::default().legend_marker(LEGEND_MARKER);
1651 assert_eq!(piechart.legend_marker, LEGEND_MARKER);
1652 }
1653
1654 #[test]
1657 fn piechart_format_legend_text_with_percentage() {
1658 let slices = vec![
1659 PieSlice::new("Rust", 50.0, Color::Red),
1660 PieSlice::new("Go", 50.0, Color::Blue),
1661 ];
1662 let piechart = PieChart::new(slices.clone()).show_percentages(true);
1663 let text = piechart.format_legend_text(&slices[0], 100.0, "");
1664 assert!(text.contains("Rust"));
1665 assert!(text.contains("50.0%"));
1666 }
1667
1668 #[test]
1669 fn piechart_format_legend_text_without_percentage() {
1670 let slices = vec![PieSlice::new("Rust", 50.0, Color::Red)];
1671 let piechart = PieChart::new(slices.clone()).show_percentages(false);
1672 let text = piechart.format_legend_text(&slices[0], 100.0, "");
1673 assert!(text.contains("Rust"));
1674 assert!(!text.contains('%'));
1675 }
1676
1677 #[test]
1678 fn piechart_format_legend_text_zero_total() {
1679 let slices = vec![PieSlice::new("X", 0.0, Color::Red)];
1680 let piechart = PieChart::new(slices.clone()).show_percentages(true);
1681 let text = piechart.format_legend_text(&slices[0], 0.0, "");
1682 assert!(text.contains("0.0%"));
1683 }
1684
1685 #[test]
1688 fn piechart_calculate_aligned_x_left() {
1689 let slices = vec![PieSlice::new("A", 100.0, Color::Red)];
1690 let piechart = PieChart::new(slices).legend_alignment(LegendAlignment::Left);
1691 let area = Rect::new(5, 0, 20, 10);
1692 assert_eq!(piechart.calculate_aligned_x(area, 10), 5);
1693 }
1694
1695 #[test]
1696 fn piechart_calculate_aligned_x_center() {
1697 let slices = vec![PieSlice::new("A", 100.0, Color::Red)];
1698 let piechart = PieChart::new(slices).legend_alignment(LegendAlignment::Center);
1699 let area = Rect::new(0, 0, 20, 10);
1700 assert_eq!(piechart.calculate_aligned_x(area, 10), 5);
1702 }
1703
1704 #[test]
1705 fn piechart_calculate_aligned_x_right() {
1706 let slices = vec![PieSlice::new("A", 100.0, Color::Red)];
1707 let piechart = PieChart::new(slices).legend_alignment(LegendAlignment::Right);
1708 let area = Rect::new(0, 0, 20, 10);
1709 assert_eq!(piechart.calculate_aligned_x(area, 10), 10);
1711 }
1712
1713 render_test!(
1716 piechart_render_braille,
1717 {
1718 let slices = vec![
1719 PieSlice::new("Rust", 60.0, Color::Red),
1720 PieSlice::new("Go", 40.0, Color::Blue),
1721 ];
1722 PieChart::new(slices).resolution(Resolution::Braille)
1723 },
1724 Rect::new(0, 0, 40, 20)
1725 );
1726
1727 render_test!(
1728 piechart_render_braille_with_legend,
1729 {
1730 let slices = vec![
1731 PieSlice::new("Rust", 60.0, Color::Red),
1732 PieSlice::new("Go", 40.0, Color::Blue),
1733 ];
1734 PieChart::new(slices)
1735 .resolution(Resolution::Braille)
1736 .show_legend(true)
1737 },
1738 Rect::new(0, 0, 40, 20)
1739 );
1740
1741 render_test!(
1744 piechart_render_legend_left,
1745 {
1746 let slices = vec![
1747 PieSlice::new("Alpha", 50.0, Color::Red),
1748 PieSlice::new("Beta", 50.0, Color::Blue),
1749 ];
1750 PieChart::new(slices)
1751 .show_legend(true)
1752 .legend_position(LegendPosition::Left)
1753 .legend_layout(LegendLayout::Vertical)
1754 },
1755 Rect::new(0, 0, 60, 20)
1756 );
1757
1758 render_test!(
1759 piechart_render_legend_right,
1760 {
1761 let slices = vec![
1762 PieSlice::new("Alpha", 50.0, Color::Red),
1763 PieSlice::new("Beta", 50.0, Color::Blue),
1764 ];
1765 PieChart::new(slices)
1766 .show_legend(true)
1767 .legend_position(LegendPosition::Right)
1768 .legend_layout(LegendLayout::Vertical)
1769 },
1770 Rect::new(0, 0, 60, 20)
1771 );
1772
1773 render_test!(
1774 piechart_render_legend_top_horizontal,
1775 {
1776 let slices = vec![
1777 PieSlice::new("Alpha", 50.0, Color::Red),
1778 PieSlice::new("Beta", 50.0, Color::Blue),
1779 ];
1780 PieChart::new(slices)
1781 .show_legend(true)
1782 .legend_position(LegendPosition::Top)
1783 .legend_layout(LegendLayout::Horizontal)
1784 },
1785 Rect::new(0, 0, 60, 20)
1786 );
1787
1788 render_test!(
1789 piechart_render_legend_bottom_horizontal,
1790 {
1791 let slices = vec![
1792 PieSlice::new("Alpha", 50.0, Color::Red),
1793 PieSlice::new("Beta", 50.0, Color::Blue),
1794 ];
1795 PieChart::new(slices)
1796 .show_legend(true)
1797 .legend_position(LegendPosition::Bottom)
1798 .legend_layout(LegendLayout::Horizontal)
1799 },
1800 Rect::new(0, 0, 60, 20)
1801 );
1802
1803 render_test!(
1804 piechart_render_legend_top_vertical,
1805 {
1806 let slices = vec![
1807 PieSlice::new("Alpha", 50.0, Color::Red),
1808 PieSlice::new("Beta", 50.0, Color::Blue),
1809 ];
1810 PieChart::new(slices)
1811 .show_legend(true)
1812 .legend_position(LegendPosition::Top)
1813 .legend_layout(LegendLayout::Vertical)
1814 },
1815 Rect::new(0, 0, 60, 20)
1816 );
1817
1818 render_test!(
1819 piechart_render_legend_bottom_vertical,
1820 {
1821 let slices = vec![
1822 PieSlice::new("Alpha", 50.0, Color::Red),
1823 PieSlice::new("Beta", 50.0, Color::Blue),
1824 ];
1825 PieChart::new(slices)
1826 .show_legend(true)
1827 .legend_position(LegendPosition::Bottom)
1828 .legend_layout(LegendLayout::Vertical)
1829 },
1830 Rect::new(0, 0, 60, 20)
1831 );
1832
1833 render_test!(
1834 piechart_render_legend_left_horizontal,
1835 {
1836 let slices = vec![
1837 PieSlice::new("Alpha", 50.0, Color::Red),
1838 PieSlice::new("Beta", 50.0, Color::Blue),
1839 ];
1840 PieChart::new(slices)
1841 .show_legend(true)
1842 .legend_position(LegendPosition::Left)
1843 .legend_layout(LegendLayout::Horizontal)
1844 },
1845 Rect::new(0, 0, 60, 20)
1846 );
1847
1848 render_test!(
1849 piechart_render_legend_right_horizontal,
1850 {
1851 let slices = vec![
1852 PieSlice::new("Alpha", 50.0, Color::Red),
1853 PieSlice::new("Beta", 50.0, Color::Blue),
1854 ];
1855 PieChart::new(slices)
1856 .show_legend(true)
1857 .legend_position(LegendPosition::Right)
1858 .legend_layout(LegendLayout::Horizontal)
1859 },
1860 Rect::new(0, 0, 60, 20)
1861 );
1862
1863 render_test!(
1866 piechart_render_legend_with_percentages,
1867 {
1868 let slices = vec![
1869 PieSlice::new("Rust", 45.0, Color::Red),
1870 PieSlice::new("Go", 30.0, Color::Blue),
1871 PieSlice::new("Python", 25.0, Color::Green),
1872 ];
1873 PieChart::new(slices)
1874 .show_legend(true)
1875 .show_percentages(true)
1876 },
1877 Rect::new(0, 0, 60, 20)
1878 );
1879
1880 render_test!(
1882 piechart_render_legend_alignment_center,
1883 {
1884 let slices = vec![
1885 PieSlice::new("A", 50.0, Color::Red),
1886 PieSlice::new("B", 50.0, Color::Blue),
1887 ];
1888 PieChart::new(slices)
1889 .show_legend(true)
1890 .legend_alignment(LegendAlignment::Center)
1891 },
1892 Rect::new(0, 0, 60, 20)
1893 );
1894
1895 render_test!(
1896 piechart_render_legend_alignment_right,
1897 {
1898 let slices = vec![
1899 PieSlice::new("A", 50.0, Color::Red),
1900 PieSlice::new("B", 50.0, Color::Blue),
1901 ];
1902 PieChart::new(slices)
1903 .show_legend(true)
1904 .legend_alignment(LegendAlignment::Right)
1905 },
1906 Rect::new(0, 0, 60, 20)
1907 );
1908
1909 #[test]
1912 fn piechart_layout_too_small_no_legend() {
1913 let slices = vec![PieSlice::new("A", 100.0, Color::Red)];
1914 let piechart = PieChart::new(slices).show_legend(true);
1915 let area = Rect::new(0, 0, 10, 5);
1917 let (pie_area, legend_opt) = piechart.calculate_layout(area);
1918 assert_eq!(pie_area, area);
1919 assert!(legend_opt.is_none());
1920 }
1921
1922 #[test]
1923 fn piechart_layout_show_legend_false_no_legend() {
1924 let slices = vec![PieSlice::new("A", 100.0, Color::Red)];
1925 let piechart = PieChart::new(slices).show_legend(false);
1926 let area = Rect::new(0, 0, 60, 20);
1927 let (pie_area, legend_opt) = piechart.calculate_layout(area);
1928 assert_eq!(pie_area, area);
1929 assert!(legend_opt.is_none());
1930 }
1931
1932 render_empty_test!(piechart_render_empty_slices, PieChart::default());
1935
1936 render_test!(
1939 piechart_render_single_slice,
1940 PieChart::new(vec![PieSlice::new("Only", 100.0, Color::Cyan)]),
1941 Rect::new(0, 0, 30, 15)
1942 );
1943
1944 #[test]
1947 fn piechart_percentage_zero_total() {
1948 let slices = vec![PieSlice::new("A", 0.0, Color::Red)];
1949 let piechart = PieChart::new(slices.clone());
1950 assert_eq!(piechart.percentage(&slices[0]), 0.0);
1951 }
1952
1953 #[test]
1958 fn piechart_calculate_legend_width_vertical_zero_total_with_percentages() {
1959 let slices = vec![
1960 PieSlice::new("A", 0.0, Color::Red),
1961 PieSlice::new("B", 0.0, Color::Blue),
1962 ];
1963 let piechart = PieChart::new(slices)
1964 .legend_layout(LegendLayout::Vertical)
1965 .show_percentages(true);
1966 let width = piechart.calculate_legend_width();
1967 assert!(width > 0);
1969 }
1970
1971 render_test!(
1972 piechart_render_legend_vertical_zero_total_with_percentages,
1973 PieChart::new(vec![
1974 PieSlice::new("A", 0.0, Color::Red),
1975 PieSlice::new("B", 0.0, Color::Blue),
1976 ])
1977 .show_legend(true)
1978 .show_percentages(true)
1979 .legend_position(LegendPosition::Right)
1980 .legend_layout(LegendLayout::Vertical),
1981 Rect::new(0, 0, 60, 20)
1982 );
1983
1984 render_test!(
1987 piechart_render_legend_vertical_no_percentages,
1988 PieChart::new(vec![
1989 PieSlice::new("Rust", 60.0, Color::Red),
1990 PieSlice::new("Go", 40.0, Color::Blue),
1991 ])
1992 .show_legend(true)
1993 .show_percentages(false)
1994 .legend_position(LegendPosition::Right)
1995 .legend_layout(LegendLayout::Vertical),
1996 Rect::new(0, 0, 60, 20)
1997 );
1998
1999 render_test!(
2002 piechart_render_all_zero_values,
2003 PieChart::new(vec![
2004 PieSlice::new("A", 0.0, Color::Red),
2005 PieSlice::new("B", 0.0, Color::Blue),
2006 ]),
2007 Rect::new(0, 0, 40, 20)
2008 );
2009
2010 render_test!(
2013 piechart_render_zero_value_slice_in_mix,
2014 PieChart::new(vec![
2015 PieSlice::new("A", 100.0, Color::Red),
2016 PieSlice::new("B", 0.0, Color::Blue),
2017 ]),
2018 Rect::new(0, 0, 40, 20)
2019 );
2020
2021 render_test!(
2024 piechart_render_tiny_area_radius_zero,
2025 PieChart::new(vec![PieSlice::new("A", 100.0, Color::Red)]),
2026 Rect::new(0, 0, 1, 1)
2027 );
2028
2029 render_test!(
2033 piechart_render_vertical_legend_overflow,
2034 PieChart::new(vec![
2035 PieSlice::new("Slice1", 10.0, Color::Red),
2036 PieSlice::new("Slice2", 10.0, Color::Blue),
2037 PieSlice::new("Slice3", 10.0, Color::Green),
2038 PieSlice::new("Slice4", 10.0, Color::Yellow),
2039 PieSlice::new("Slice5", 10.0, Color::Cyan),
2040 PieSlice::new("Slice6", 10.0, Color::Magenta),
2041 PieSlice::new("Slice7", 10.0, Color::White),
2042 PieSlice::new("Slice8", 10.0, Color::Red),
2043 PieSlice::new("Slice9", 10.0, Color::Blue),
2044 PieSlice::new("Slice10", 10.0, Color::Green),
2045 ])
2046 .show_legend(true)
2047 .legend_position(LegendPosition::Right)
2048 .legend_layout(LegendLayout::Vertical),
2049 Rect::new(0, 0, 60, 12)
2051 );
2052
2053 render_test!(
2057 piechart_render_horizontal_legend_overflow,
2058 PieChart::new(vec![
2059 PieSlice::new("LongLabelA", 20.0, Color::Red),
2060 PieSlice::new("LongLabelB", 20.0, Color::Blue),
2061 PieSlice::new("LongLabelC", 20.0, Color::Green),
2062 PieSlice::new("LongLabelD", 20.0, Color::Yellow),
2063 PieSlice::new("LongLabelE", 20.0, Color::Cyan),
2064 ])
2065 .show_legend(true)
2066 .legend_position(LegendPosition::Bottom)
2067 .legend_layout(LegendLayout::Horizontal),
2068 Rect::new(0, 0, 22, 15)
2070 );
2071
2072 #[test]
2075 fn piechart_layout_horizontal_split_too_narrow() {
2076 let area = Rect::new(0, 0, 5, 20);
2077 let (pie_area, legend_opt) = PieChart::<'_>::layout_horizontal_split(area, 100, true);
2079 assert_eq!(pie_area, area);
2080 assert!(legend_opt.is_none());
2081 }
2082
2083 #[test]
2086 fn piechart_layout_vertical_split_too_short() {
2087 let area = Rect::new(0, 0, 60, 3);
2088 let (pie_area, legend_opt) = PieChart::<'_>::layout_vertical_split(area, 100, true);
2090 assert_eq!(pie_area, area);
2091 assert!(legend_opt.is_none());
2092 }
2093
2094 #[test]
2098 fn piechart_calculate_legend_width_horizontal_with_percentages() {
2099 let slices = vec![
2100 PieSlice::new("Rust", 60.0, Color::Red),
2101 PieSlice::new("Go", 40.0, Color::Blue),
2102 ];
2103 let piechart = PieChart::new(slices)
2104 .legend_layout(LegendLayout::Horizontal)
2105 .show_percentages(true);
2106 let width = piechart.calculate_legend_width();
2107 assert!(width > 0);
2108 }
2109
2110 #[test]
2111 fn piechart_calculate_legend_width_horizontal_without_percentages() {
2112 let slices = vec![
2113 PieSlice::new("Alpha", 50.0, Color::Red),
2114 PieSlice::new("Beta", 50.0, Color::Blue),
2115 ];
2116 let piechart = PieChart::new(slices)
2117 .legend_layout(LegendLayout::Horizontal)
2118 .show_percentages(false);
2119 let width = piechart.calculate_legend_width();
2120 assert!(width > 0);
2121 }
2122
2123 #[test]
2124 fn piechart_calculate_legend_width_horizontal_zero_total() {
2125 let slices = vec![
2126 PieSlice::new("A", 0.0, Color::Red),
2127 PieSlice::new("B", 0.0, Color::Blue),
2128 ];
2129 let piechart = PieChart::new(slices)
2130 .legend_layout(LegendLayout::Horizontal)
2131 .show_percentages(true);
2132 let width = piechart.calculate_legend_width();
2133 assert!(width > 0);
2135 }
2136
2137 #[test]
2140 fn piechart_calculate_legend_horizontal_width_zero_total() {
2141 let slices = vec![
2142 PieSlice::new("X", 0.0, Color::Red),
2143 PieSlice::new("Y", 0.0, Color::Blue),
2144 ];
2145 let piechart = PieChart::new(slices).show_percentages(true);
2146 let width = piechart.calculate_legend_horizontal_width();
2147 assert!(width > 0);
2148 }
2149
2150 #[test]
2151 fn piechart_calculate_legend_horizontal_width_without_percentages() {
2152 let slices = vec![PieSlice::new("Item", 100.0, Color::Green)];
2153 let piechart = PieChart::new(slices).show_percentages(false);
2154 let width = piechart.calculate_legend_horizontal_width();
2155 assert!(width > 0);
2156 }
2157
2158 #[test]
2161 fn piechart_calculate_vertical_grid_height_narrow() {
2162 let slices = vec![
2163 PieSlice::new("A", 50.0, Color::Red),
2164 PieSlice::new("B", 50.0, Color::Blue),
2165 ];
2166 let piechart = PieChart::new(slices);
2167 let height = piechart.calculate_vertical_grid_height(1);
2169 assert!(height >= 4);
2170 }
2171
2172 #[test]
2175 fn piechart_value_percent_positive_total() {
2176 assert_eq!(PieChart::value_percent(25.0, 200.0), 12.5);
2177 }
2178
2179 #[test]
2180 fn piechart_value_percent_zero_total() {
2181 assert_eq!(PieChart::value_percent(25.0, 0.0), 0.0);
2182 }
2183
2184 #[test]
2185 fn piechart_value_percent_negative_total() {
2186 assert_eq!(PieChart::value_percent(25.0, -5.0), 0.0);
2188 }
2189
2190 #[test]
2191 fn piechart_slice_angles_full_circle_flag() {
2192 let (_, _, is_full) = PieChart::slice_angles(0.0, 100.0);
2193 assert!(is_full);
2194 }
2195
2196 #[test]
2197 fn piechart_slice_angles_partial_not_full() {
2198 let (start, end, is_full) = PieChart::slice_angles(0.0, 50.0);
2199 assert!(!is_full);
2200 assert!((end - start - PI).abs() < 1e-9);
2201 }
2202
2203 #[test]
2204 fn piechart_legend_item_width_matches_text() {
2205 let slices = vec![PieSlice::new("Rust", 50.0, Color::Red)];
2206 let chart = PieChart::new(slices.clone()).show_percentages(true);
2207 let expected = chart
2208 .format_legend_text(&slices[0], 100.0, " ")
2209 .chars()
2210 .count();
2211 assert_eq!(
2212 usize::from(chart.legend_item_width(&slices[0], 100.0)),
2213 expected
2214 );
2215 }
2216
2217 #[test]
2218 fn piechart_legend_item_width_unicode_label() {
2219 let slices = vec![PieSlice::new("日本語", 100.0, Color::Red)];
2221 let chart = PieChart::new(slices.clone()).show_percentages(false);
2222 assert_eq!(chart.legend_item_width(&slices[0], 100.0), 7);
2224 }
2225
2226 #[test]
2229 fn piechart_full_circle_fills_more_than_a_line() {
2230 fn filled_cells(chart: &PieChart) -> usize {
2231 let mut buffer = Buffer::empty(Rect::new(0, 0, 30, 15));
2232 Widget::render(chart, buffer.area, &mut buffer);
2233 buffer
2234 .content
2235 .iter()
2236 .filter(|c| !c.symbol().trim().is_empty())
2237 .count()
2238 }
2239
2240 let standard = PieChart::new(vec![PieSlice::new("Only", 100.0, Color::Green)])
2241 .show_legend(false)
2242 .show_percentages(false);
2243 assert!(filled_cells(&standard) > 30);
2244
2245 let braille = PieChart::new(vec![PieSlice::new("Only", 100.0, Color::Green)])
2246 .resolution(Resolution::Braille)
2247 .show_legend(false)
2248 .show_percentages(false);
2249 assert!(filled_cells(&braille) > 30);
2250 }
2251}