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]
355 pub fn slice_list(&self) -> &[PieSlice<'a>] {
356 &self.slices
357 }
358
359 #[must_use]
373 pub fn slices(mut self, slices: Vec<PieSlice<'a>>) -> Self {
374 self.slices = slices;
375 self
376 }
377
378 #[must_use]
392 pub fn block(mut self, block: Block<'a>) -> Self {
393 self.block = Some(block);
394 self
395 }
396
397 #[must_use]
409 pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
410 self.style = style.into();
411 self
412 }
413
414 #[cfg(feature = "theme")]
424 #[must_use]
425 pub fn theme(mut self, theme: &Theme) -> Self {
426 self.theme_style = theme.style();
427 self
428 }
429
430 #[must_use]
440 pub const fn show_legend(mut self, show: bool) -> Self {
441 self.show_legend = show;
442 self
443 }
444
445 #[must_use]
455 pub const fn show_percentages(mut self, show: bool) -> Self {
456 self.show_percentages = show;
457 self
458 }
459
460 #[must_use]
483 pub const fn pie_char(mut self, c: char) -> Self {
484 self.pie_char = c;
485 self
486 }
487
488 #[must_use]
518 pub const fn legend_marker(mut self, marker: &'a str) -> Self {
519 self.legend_marker = marker;
520 self
521 }
522
523 #[must_use]
538 pub const fn resolution(mut self, resolution: Resolution) -> Self {
539 self.resolution = resolution;
540 self
541 }
542
543 #[must_use]
556 pub const fn high_resolution(mut self, enabled: bool) -> Self {
557 self.resolution = if enabled {
558 Resolution::Braille
559 } else {
560 Resolution::Standard
561 };
562 self
563 }
564
565 #[must_use]
576 pub const fn legend_position(mut self, position: LegendPosition) -> Self {
577 self.legend_position = position;
578 self
579 }
580
581 #[must_use]
597 pub const fn legend_layout(mut self, layout: LegendLayout) -> Self {
598 self.legend_layout = layout;
599 self
600 }
601
602 #[must_use]
618 pub const fn legend_alignment(mut self, alignment: LegendAlignment) -> Self {
619 self.legend_alignment = alignment;
620 self
621 }
622
623 #[must_use]
633 pub fn hole(mut self, hole: f64) -> Self {
634 self.hole = if hole.is_finite() {
635 hole.clamp(0.0, 0.95)
636 } else {
637 0.0
638 };
639 self
640 }
641
642 #[must_use]
652 pub fn start_angle(mut self, degrees: f64) -> Self {
653 self.start = if degrees.is_finite() {
654 (degrees / 360.0).rem_euclid(1.0)
655 } else {
656 0.0
657 };
658 self
659 }
660
661 #[must_use]
666 pub fn gap(mut self, dots: f64) -> Self {
667 self.gap = if dots.is_finite() {
668 dots.clamp(0.0, 8.0)
669 } else {
670 0.0
671 };
672 self
673 }
674
675 #[must_use]
677 pub fn explode(mut self, dots: f64) -> Self {
678 self.explode = if dots.is_finite() {
679 dots.clamp(0.0, 8.0)
680 } else {
681 0.0
682 };
683 self
684 }
685
686 #[must_use]
688 pub const fn pie_style(mut self, style: PieStyle) -> Self {
689 self.pie_style = style;
690 self
691 }
692
693 #[must_use]
695 pub const fn center(mut self, text: &'a str) -> Self {
696 self.center = Some(text);
697 self
698 }
699
700 fn total_value(&self) -> f64 {
701 self.slices.iter().map(|s| s.value).sum()
702 }
703
704 fn value_percent(value: f64, total: f64) -> f64 {
709 if total > 0.0 {
710 (value / total) * 100.0
711 } else {
712 0.0
713 }
714 }
715
716 fn percentage(&self, slice: &PieSlice) -> f64 {
718 Self::value_percent(slice.value, self.total_value())
719 }
720}
721
722impl Styled for PieChart<'_> {
723 type Item = Self;
724
725 fn style(&self) -> Style {
726 self.style
727 }
728
729 fn set_style<S: Into<Style>>(mut self, style: S) -> Self::Item {
730 self.style = style.into();
731 self
732 }
733}
734
735impl Widget for PieChart<'_> {
736 fn render(self, area: Rect, buf: &mut Buffer) {
737 Widget::render(&self, area, buf);
738 }
739}
740
741impl Widget for &PieChart<'_> {
742 fn render(self, area: Rect, buf: &mut Buffer) {
743 buf.set_style(area, self.theme_style.patch(self.style));
744 let inner = if let Some(ref block) = self.block {
745 let inner_area = block.inner(area);
746 block.render(area, buf);
747 inner_area
748 } else {
749 area
750 };
751 self.render_piechart(inner, buf);
752 }
753}
754
755impl PieChart<'_> {
756 const LEGEND_VERTICAL_MAX_RATIO: u16 = 3;
758
759 const LEGEND_VERTICAL_MIN_WIDTH: u16 = 20;
761
762 const LEGEND_HORIZONTAL_MAX_RATIO: u16 = 5;
765
766 const LEGEND_HORIZONTAL_MAX_WIDTH: u16 = 60;
768
769 const LEGEND_VERTICAL_MAX_HEIGHT: u16 = 9;
772
773 const LEGEND_HORIZONTAL_HEIGHT: u16 = 3;
775
776 const LEGEND_SPACING: u16 = 1;
778
779 const LEGEND_PADDING: u16 = 1;
781
782 fn render_piechart(&self, area: Rect, buf: &mut Buffer) {
783 if area.is_empty() || self.slices.is_empty() {
784 return;
785 }
786
787 let total = self.total_value();
788 if total <= 0.0 {
789 return;
790 }
791
792 if self.needs_canvas() {
793 self.render_piechart_canvas(area, buf);
794 return;
795 }
796
797 match self.resolution {
798 Resolution::Standard => {
799 }
801 Resolution::Braille => {
802 self.render_piechart_braille(area, buf);
803 return;
804 }
805 }
806
807 let (pie_area, legend_area_opt) = self.calculate_layout(area);
809
810 let center_x = pie_area.width / 2;
813 let center_y = pie_area.height / 2;
814
815 let radius = center_x.min(center_y * 2).saturating_sub(1);
817
818 let mut cumulative_percent = self.start * 100.0;
820 for slice in &self.slices {
821 let percent = self.percentage(slice);
822 self.render_slice(
823 pie_area,
824 buf,
825 center_x,
826 center_y,
827 radius,
828 cumulative_percent,
829 percent,
830 slice.color,
831 );
832 cumulative_percent += percent;
833 }
834
835 self.draw_center(pie_area, buf);
836
837 if let Some(legend_area) = legend_area_opt {
839 self.render_legend(buf, legend_area);
840 }
841 }
842
843 fn slice_angles(start_percent: f64, percent: f64) -> (f64, f64, bool) {
849 let start_angle = (start_percent / 100.0) * 2.0 * PI - PI / 2.0;
850 let end_angle = ((start_percent + percent) / 100.0) * 2.0 * PI - PI / 2.0;
851 let is_full_circle = percent >= 100.0 - f64::EPSILON;
852 (start_angle, end_angle, is_full_circle)
853 }
854
855 #[allow(clippy::too_many_arguments, clippy::similar_names)]
856 fn render_slice(
857 &self,
858 area: Rect,
859 buf: &mut Buffer,
860 center_x: u16,
861 center_y: u16,
862 radius: u16,
863 start_percent: f64,
864 percent: f64,
865 color: Color,
866 ) {
867 if radius == 0 || percent <= 0.0 {
868 return;
869 }
870
871 let (start_angle, end_angle, is_full_circle) = Self::slice_angles(start_percent, percent);
875
876 let scan_width = i32::from(radius + 1);
878 let scan_height = i32::from((radius / 2) + 1); for dy in -scan_height..=scan_height {
881 for dx in -scan_width..=scan_width {
882 let x = i32::from(area.x) + i32::from(center_x) + dx;
884 let y = i32::from(area.y) + i32::from(center_y) + dy;
885
886 if x < i32::from(area.x)
888 || x >= i32::from(area.x + area.width)
889 || y < i32::from(area.y)
890 || y >= i32::from(area.y + area.height)
891 {
892 continue;
893 }
894
895 #[allow(clippy::cast_precision_loss)]
897 let adjusted_dx = f64::from(dx);
898 #[allow(clippy::cast_precision_loss)]
899 let adjusted_dy = f64::from(dy * 2);
900
901 let distance = (adjusted_dx * adjusted_dx + adjusted_dy * adjusted_dy).sqrt();
903
904 #[allow(clippy::cast_precision_loss)]
906 if distance <= f64::from(radius) && distance >= self.hole * f64::from(radius) {
907 let angle = adjusted_dy.atan2(adjusted_dx);
909
910 if is_full_circle || Self::is_angle_in_slice(angle, start_angle, end_angle) {
912 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
913 {
914 let cell = &mut buf[(x as u16, y as u16)];
915 cell.set_char(self.pie_char).set_fg(color);
916 }
917 }
918 }
919 }
920 }
921 }
922
923 fn is_angle_in_slice(angle: f64, start: f64, end: f64) -> bool {
924 let normalize = |a: f64| {
926 let mut normalized = a % (2.0 * PI);
927 if normalized < 0.0 {
928 normalized += 2.0 * PI;
929 }
930 normalized
931 };
932
933 let norm_angle = normalize(angle);
934 let norm_start = normalize(start);
935 let norm_end = normalize(end);
936
937 if norm_start <= norm_end {
938 norm_angle >= norm_start && norm_angle <= norm_end
939 } else {
940 norm_angle >= norm_start || norm_angle <= norm_end
942 }
943 }
944
945 fn format_legend_text(&self, slice: &PieSlice, total: f64, spacing: &str) -> String {
946 if self.show_percentages {
947 let percent = Self::value_percent(slice.value, total);
948 format!(
949 "{} {} {:.1}%{}",
950 self.legend_marker, slice.label, percent, spacing
951 )
952 } else {
953 format!("{} {}{}", self.legend_marker, slice.label, spacing)
954 }
955 }
956
957 fn legend_item_width(&self, slice: &PieSlice, total: f64) -> u16 {
960 u16::try_from(self.format_legend_text(slice, total, " ").chars().count())
961 .unwrap_or(u16::MAX)
962 }
963
964 fn calculate_aligned_x(&self, legend_area: Rect, content_width: u16) -> u16 {
965 match self.legend_alignment {
966 LegendAlignment::Left => legend_area.x,
967 LegendAlignment::Center => {
968 legend_area.x + (legend_area.width.saturating_sub(content_width)) / 2
969 }
970 LegendAlignment::Right => {
971 legend_area.x + legend_area.width.saturating_sub(content_width)
972 }
973 }
974 }
975
976 fn render_legend(&self, buf: &mut Buffer, legend_area: Rect) {
977 let total = self.total_value();
978
979 match self.legend_layout {
980 LegendLayout::Vertical => {
981 self.render_vertical_legend(buf, legend_area, total);
982 }
983 LegendLayout::Horizontal => {
984 self.render_horizontal_legend(buf, legend_area, total);
985 }
986 }
987 }
988
989 fn render_vertical_legend(&self, buf: &mut Buffer, legend_area: Rect, total: f64) {
990 for (idx, slice) in self.slices.iter().enumerate() {
991 #[allow(clippy::cast_possible_truncation)]
992 let y_offset = (idx as u16) * 2;
993
994 if y_offset >= legend_area.height {
995 break;
996 }
997
998 let legend_text = self.format_legend_text(slice, total, "");
999 #[allow(clippy::cast_possible_truncation)]
1000 let text_width = u16::try_from(legend_text.chars().count()).unwrap_or(u16::MAX);
1001 let x_pos = self.calculate_aligned_x(legend_area, text_width);
1002
1003 let line = Line::from(vec![Span::styled(
1004 legend_text,
1005 Style::default().fg(slice.color),
1006 )]);
1007 let item_area = Rect {
1008 x: x_pos,
1009 y: legend_area.y + y_offset,
1010 width: text_width.min(legend_area.width),
1011 height: 1,
1012 };
1013
1014 line.render(item_area, buf);
1015 }
1016 }
1017
1018 fn render_horizontal_legend(&self, buf: &mut Buffer, legend_area: Rect, total: f64) {
1019 let mut total_width = 0u16;
1020 let mut item_widths = Vec::new();
1021
1022 for slice in &self.slices {
1023 let legend_text = self.format_legend_text(slice, total, " ");
1024 #[allow(clippy::cast_possible_truncation)]
1025 let text_width = u16::try_from(legend_text.chars().count()).unwrap_or(u16::MAX);
1026 item_widths.push(text_width);
1027 total_width = total_width.saturating_add(text_width);
1028 }
1029
1030 let start_x = self.calculate_aligned_x(legend_area, total_width.min(legend_area.width));
1031 let mut x_offset = 0u16;
1032
1033 for (idx, slice) in self.slices.iter().enumerate() {
1034 if x_offset >= legend_area.width {
1035 break;
1036 }
1037
1038 let legend_text = self.format_legend_text(slice, total, " ");
1039 let text_width = item_widths[idx];
1040
1041 let line = Line::from(vec![Span::styled(
1042 legend_text,
1043 Style::default().fg(slice.color),
1044 )]);
1045 let item_area = Rect {
1046 x: start_x + x_offset,
1047 y: legend_area.y,
1048 width: text_width.min(legend_area.width.saturating_sub(x_offset)),
1049 height: 1,
1050 };
1051
1052 line.render(item_area, buf);
1053 x_offset = x_offset.saturating_add(text_width);
1054 }
1055 }
1056
1057 #[allow(clippy::too_many_lines)]
1058 #[must_use]
1072 pub fn areas(&self, area: Rect) -> (Rect, Option<Rect>) {
1073 let inner = self.block.as_ref().map_or(area, |b| b.inner(area));
1074 self.calculate_layout(inner)
1075 }
1076
1077 fn calculate_layout(&self, area: Rect) -> (Rect, Option<Rect>) {
1078 if !self.show_legend || area.width < 20 || area.height < 10 {
1079 return (area, None);
1080 }
1081
1082 match (self.legend_position, self.legend_layout) {
1084 (LegendPosition::Left | LegendPosition::Right, LegendLayout::Vertical) => {
1086 let legend_width = self
1087 .calculate_legend_width()
1088 .min(area.width / Self::LEGEND_VERTICAL_MAX_RATIO)
1089 .max(Self::LEGEND_VERTICAL_MIN_WIDTH);
1090 let is_left = matches!(self.legend_position, LegendPosition::Left);
1091 Self::layout_horizontal_split(area, legend_width, is_left)
1092 }
1093 (LegendPosition::Top | LegendPosition::Bottom, LegendLayout::Horizontal) => {
1095 let is_top = matches!(self.legend_position, LegendPosition::Top);
1096 Self::layout_vertical_split(area, Self::LEGEND_HORIZONTAL_HEIGHT, is_top)
1097 }
1098 (LegendPosition::Left | LegendPosition::Right, LegendLayout::Horizontal) => {
1100 let legend_width = self
1102 .calculate_legend_horizontal_width()
1103 .min(
1104 (area.width * (Self::LEGEND_HORIZONTAL_MAX_RATIO - 1))
1105 / Self::LEGEND_HORIZONTAL_MAX_RATIO,
1106 )
1107 .min(Self::LEGEND_HORIZONTAL_MAX_WIDTH);
1108 let is_left = matches!(self.legend_position, LegendPosition::Left);
1109 Self::layout_horizontal_split(area, legend_width, is_left)
1110 }
1111 (LegendPosition::Top | LegendPosition::Bottom, LegendLayout::Vertical) => {
1112 let legend_height = self.calculate_vertical_grid_height(area.width);
1114 let is_top = matches!(self.legend_position, LegendPosition::Top);
1115 Self::layout_vertical_split(area, legend_height, is_top)
1116 }
1117 }
1118 }
1119
1120 fn calculate_vertical_grid_height(&self, available_width: u16) -> u16 {
1121 let max_item_width = self.calculate_legend_width();
1123 let columns = (available_width.saturating_sub(Self::LEGEND_PADDING * 2)
1124 / max_item_width.max(1))
1125 .clamp(1, 2);
1126
1127 #[allow(clippy::cast_possible_truncation)]
1128 let num_items = self.slices.len() as u16;
1129
1130 let rows = num_items.div_ceil(columns);
1132 (rows * 2 + Self::LEGEND_PADDING).clamp(4, Self::LEGEND_VERTICAL_MAX_HEIGHT)
1134 }
1135
1136 fn layout_horizontal_split(
1137 area: Rect,
1138 legend_width: u16,
1139 legend_on_left: bool,
1140 ) -> (Rect, Option<Rect>) {
1141 if area.width <= legend_width {
1142 return (area, None);
1143 }
1144
1145 let pie_width = area
1146 .width
1147 .saturating_sub(legend_width + Self::LEGEND_SPACING);
1148
1149 if legend_on_left {
1150 (
1151 Rect {
1152 x: area.x + legend_width + Self::LEGEND_SPACING,
1153 y: area.y,
1154 width: pie_width,
1155 height: area.height,
1156 },
1157 Some(Rect {
1158 x: area.x,
1159 y: area.y + Self::LEGEND_PADDING,
1160 width: legend_width,
1161 height: area.height.saturating_sub(Self::LEGEND_PADDING * 2),
1162 }),
1163 )
1164 } else {
1165 (
1166 Rect {
1167 x: area.x,
1168 y: area.y,
1169 width: pie_width,
1170 height: area.height,
1171 },
1172 Some(Rect {
1173 x: area.x + pie_width + Self::LEGEND_SPACING,
1174 y: area.y + Self::LEGEND_PADDING,
1175 width: legend_width,
1176 height: area.height.saturating_sub(Self::LEGEND_PADDING * 2),
1177 }),
1178 )
1179 }
1180 }
1181
1182 fn layout_vertical_split(
1183 area: Rect,
1184 legend_height: u16,
1185 legend_on_top: bool,
1186 ) -> (Rect, Option<Rect>) {
1187 if area.height <= legend_height {
1188 return (area, None);
1189 }
1190
1191 let pie_height = area
1192 .height
1193 .saturating_sub(legend_height + Self::LEGEND_SPACING);
1194
1195 if legend_on_top {
1196 (
1197 Rect {
1198 x: area.x,
1199 y: area.y + legend_height + Self::LEGEND_SPACING,
1200 width: area.width,
1201 height: pie_height,
1202 },
1203 Some(Rect {
1204 x: area.x + Self::LEGEND_PADDING,
1205 y: area.y + Self::LEGEND_PADDING,
1206 width: area.width.saturating_sub(Self::LEGEND_PADDING * 2),
1207 height: legend_height.saturating_sub(Self::LEGEND_PADDING),
1208 }),
1209 )
1210 } else {
1211 (
1212 Rect {
1213 x: area.x,
1214 y: area.y,
1215 width: area.width,
1216 height: pie_height,
1217 },
1218 Some(Rect {
1219 x: area.x + Self::LEGEND_PADDING,
1220 y: area.y + pie_height + Self::LEGEND_SPACING,
1221 width: area.width.saturating_sub(Self::LEGEND_PADDING * 2),
1222 height: legend_height.saturating_sub(Self::LEGEND_PADDING),
1223 }),
1224 )
1225 }
1226 }
1227
1228 fn calculate_legend_width(&self) -> u16 {
1229 let total = self.total_value();
1230 let widths = self.slices.iter().map(|s| self.legend_item_width(s, total));
1231
1232 let base = match self.legend_layout {
1233 LegendLayout::Vertical => widths.max().unwrap_or(0),
1235 LegendLayout::Horizontal => widths.fold(0u16, u16::saturating_add),
1237 };
1238
1239 base.saturating_add(2)
1240 }
1241
1242 fn calculate_legend_horizontal_width(&self) -> u16 {
1243 let total = self.total_value();
1244 self.slices
1245 .iter()
1246 .map(|s| self.legend_item_width(s, total))
1247 .fold(0u16, u16::saturating_add)
1248 .saturating_add(2)
1249 }
1250
1251 #[allow(clippy::similar_names)]
1252 fn render_piechart_braille(&self, area: Rect, buf: &mut Buffer) {
1253 let (pie_area, legend_area_opt) = self.calculate_layout(area);
1255
1256 let center_x_chars = pie_area.width / 2;
1258 let center_y_chars = pie_area.height / 2;
1259
1260 let center_x_dots = center_x_chars * 2;
1262 let center_y_dots = center_y_chars * 4;
1263
1264 let radius = (center_x_dots).min(center_y_dots).saturating_sub(2);
1270
1271 let width_dots = pie_area.width * 2;
1273 let height_dots = pie_area.height * 4;
1274
1275 let mut dot_slices: Vec<Vec<Option<usize>>> =
1276 vec![vec![None; width_dots as usize]; height_dots as usize];
1277
1278 let mut cumulative_percent = self.start * 100.0;
1280 for (slice_idx, slice) in self.slices.iter().enumerate() {
1281 let percent = self.percentage(slice);
1282 let (start_angle, end_angle, is_full_circle) =
1283 Self::slice_angles(cumulative_percent, percent);
1284
1285 for dy in 0..height_dots {
1286 for dx in 0..width_dots {
1287 let rel_x = f64::from(dx) - f64::from(center_x_dots);
1288 let rel_y = f64::from(dy) - f64::from(center_y_dots);
1289
1290 let distance = (rel_x * rel_x + rel_y * rel_y).sqrt();
1293
1294 if distance <= f64::from(radius) && distance >= self.hole * f64::from(radius) {
1295 let angle = rel_y.atan2(rel_x);
1296 if is_full_circle || Self::is_angle_in_slice(angle, start_angle, end_angle)
1297 {
1298 dot_slices[dy as usize][dx as usize] = Some(slice_idx);
1299 }
1300 }
1301 }
1302 }
1303
1304 cumulative_percent += percent;
1305 }
1306
1307 for char_y in 0..pie_area.height {
1309 for char_x in 0..pie_area.width {
1310 let base_dot_x = char_x * 2;
1311 let base_dot_y = char_y * 4;
1312
1313 let dot_positions = [
1320 (0, 0, 0x01), (0, 1, 0x02), (0, 2, 0x04), (1, 0, 0x08), (1, 1, 0x10), (1, 2, 0x20), (0, 3, 0x40), (1, 3, 0x80), ];
1329
1330 let mut pattern = 0u32;
1331 let mut slice_colors: Vec<(usize, u32)> = Vec::new();
1332
1333 for (dx, dy, bit) in dot_positions {
1334 let dot_x = base_dot_x + dx;
1335 let dot_y = base_dot_y + dy;
1336
1337 if dot_y < height_dots && dot_x < width_dots {
1338 if let Some(slice_idx) = dot_slices[dot_y as usize][dot_x as usize] {
1339 pattern |= bit;
1340 if let Some(entry) =
1342 slice_colors.iter_mut().find(|(idx, _)| *idx == slice_idx)
1343 {
1344 entry.1 += 1;
1345 } else {
1346 slice_colors.push((slice_idx, 1));
1347 }
1348 }
1349 }
1350 }
1351
1352 if pattern > 0 {
1353 if let Some((slice_idx, _)) = slice_colors.iter().max_by_key(|(_, count)| count)
1355 {
1356 let braille_char = char::from_u32(0x2800 + pattern).unwrap_or('⠀');
1357 let color = self.slices[*slice_idx].color;
1358
1359 let cell = &mut buf[(pie_area.x + char_x, pie_area.y + char_y)];
1360 cell.set_char(braille_char).set_fg(color);
1361 }
1362 }
1363 }
1364 }
1365
1366 self.draw_center(pie_area, buf);
1367
1368 if let Some(legend_area) = legend_area_opt {
1370 self.render_legend(buf, legend_area);
1371 }
1372 }
1373}
1374
1375#[cfg(test)]
1376#[allow(clippy::float_cmp)]
1377#[allow(unnameable_test_items)]
1378mod tests {
1379 use super::*;
1380
1381 #[test]
1382 fn pie_slice_new() {
1383 let slice = PieSlice::new("Test", 50.0, Color::Red);
1384 assert_eq!(slice.label(), "Test");
1385 assert_eq!(slice.value(), 50.0);
1386 assert_eq!(slice.color(), Color::Red);
1387 }
1388
1389 #[test]
1390 fn piechart_new() {
1391 let slices = vec![
1392 PieSlice::new("A", 30.0, Color::Red),
1393 PieSlice::new("B", 70.0, Color::Blue),
1394 ];
1395 let piechart = PieChart::new(slices.clone());
1396 assert_eq!(piechart.slices, slices);
1397 }
1398
1399 #[test]
1400 fn piechart_default() {
1401 let piechart = PieChart::default();
1402 assert_eq!(piechart.slices, [] as [PieSlice<'_>; 0]);
1403 assert!(piechart.show_legend);
1404 assert!(piechart.show_percentages);
1405 }
1406
1407 #[test]
1408 fn piechart_slices() {
1409 let slices = vec![PieSlice::new("Test", 100.0, Color::Green)];
1410 let piechart = PieChart::default().slices(slices.clone());
1411 assert_eq!(piechart.slices, slices);
1412 }
1413
1414 #[test]
1415 fn piechart_style() {
1416 let style = Style::default().fg(Color::Red);
1417 let piechart = PieChart::default().style(style);
1418 assert_eq!(piechart.style, style);
1419 }
1420
1421 #[test]
1422 fn piechart_show_legend() {
1423 let piechart = PieChart::default().show_legend(false);
1424 assert!(!piechart.show_legend);
1425 }
1426
1427 #[test]
1428 fn piechart_show_percentages() {
1429 let piechart = PieChart::default().show_percentages(false);
1430 assert!(!piechart.show_percentages);
1431 }
1432
1433 #[test]
1434 fn piechart_pie_char() {
1435 let piechart = PieChart::default().pie_char('█');
1436 assert_eq!(piechart.pie_char, '█');
1437 }
1438
1439 #[test]
1440 fn piechart_total_value() {
1441 let slices = vec![
1442 PieSlice::new("A", 30.0, Color::Red),
1443 PieSlice::new("B", 70.0, Color::Blue),
1444 ];
1445 let piechart = PieChart::new(slices);
1446 assert_eq!(piechart.total_value(), 100.0);
1447 }
1448
1449 #[test]
1450 fn piechart_percentage() {
1451 let slices = vec![
1452 PieSlice::new("A", 30.0, Color::Red),
1453 PieSlice::new("B", 70.0, Color::Blue),
1454 ];
1455 let piechart = PieChart::new(slices);
1456 assert_eq!(
1457 piechart.percentage(&PieSlice::new("A", 30.0, Color::Red)),
1458 30.0
1459 );
1460 }
1461
1462 render_empty_test!(piechart_render_empty_area, PieChart::default());
1464
1465 render_with_size_test!(
1466 piechart_render_with_block,
1467 {
1468 let slices = vec![PieSlice::new("Test", 100.0, Color::Red)];
1469 PieChart::new(slices).block(Block::bordered())
1470 },
1471 width: 20,
1472 height: 10
1473 );
1474
1475 render_test!(
1476 piechart_render_basic,
1477 {
1478 let slices = vec![
1479 PieSlice::new("Rust", 45.0, Color::Red),
1480 PieSlice::new("Go", 30.0, Color::Blue),
1481 PieSlice::new("Python", 25.0, Color::Green),
1482 ];
1483 PieChart::new(slices)
1484 },
1485 Rect::new(0, 0, 40, 20)
1486 );
1487
1488 #[test]
1489 fn piechart_styled_trait() {
1490 use ratatui::style::Stylize;
1491 let piechart = PieChart::default().red();
1492 assert_eq!(piechart.style.fg, Some(Color::Red));
1493 }
1494
1495 #[test]
1496 fn piechart_with_multiple_slices() {
1497 let slices = vec![
1498 PieSlice::new("A", 25.0, Color::Red),
1499 PieSlice::new("B", 25.0, Color::Blue),
1500 PieSlice::new("C", 25.0, Color::Green),
1501 PieSlice::new("D", 25.0, Color::Yellow),
1502 ];
1503 let piechart = PieChart::new(slices);
1504 assert_eq!(piechart.total_value(), 100.0);
1505 }
1506
1507 render_with_size_test!(
1509 piechart_multi_slice_render,
1510 {
1511 let slices = vec![
1512 PieSlice::new("A", 25.0, Color::Red),
1513 PieSlice::new("B", 25.0, Color::Blue),
1514 PieSlice::new("C", 25.0, Color::Green),
1515 PieSlice::new("D", 25.0, Color::Yellow),
1516 ];
1517 PieChart::new(slices)
1518 },
1519 width: 50,
1520 height: 30
1521 );
1522
1523 #[test]
1524 fn piechart_zero_values() {
1525 let slices = vec![
1526 PieSlice::new("A", 0.0, Color::Red),
1527 PieSlice::new("B", 0.0, Color::Blue),
1528 ];
1529 let piechart = PieChart::new(slices);
1530 assert_eq!(piechart.total_value(), 0.0);
1531 }
1532
1533 #[test]
1534 fn piechart_method_chaining() {
1535 use ratatui::widgets::Block;
1536
1537 let slices = vec![PieSlice::new("Test", 100.0, Color::Red)];
1538 let piechart = PieChart::new(slices)
1539 .show_legend(true)
1540 .show_percentages(true)
1541 .pie_char('█')
1542 .block(Block::bordered().title("Test"))
1543 .style(Style::default().fg(Color::White));
1544
1545 assert!(piechart.show_legend);
1546 assert!(piechart.show_percentages);
1547 assert_eq!(piechart.pie_char, '█');
1548 assert!(piechart.block.is_some());
1549 assert_eq!(piechart.style.fg, Some(Color::White));
1550 }
1551
1552 #[test]
1553 fn piechart_custom_symbols() {
1554 use crate::symbols;
1555
1556 let piechart = PieChart::default().pie_char(symbols::PIE_CHAR_BLOCK);
1557 assert_eq!(piechart.pie_char, '█');
1558
1559 let piechart = PieChart::default().pie_char(symbols::PIE_CHAR_CIRCLE);
1560 assert_eq!(piechart.pie_char, '◉');
1561
1562 let piechart = PieChart::default().pie_char(symbols::PIE_CHAR_SQUARE);
1563 assert_eq!(piechart.pie_char, '■');
1564 }
1565
1566 #[test]
1567 fn piechart_is_angle_in_slice() {
1568 use std::f64::consts::PI;
1569
1570 assert!(PieChart::is_angle_in_slice(PI / 4.0, 0.0, PI / 2.0));
1572
1573 assert!(!PieChart::is_angle_in_slice(PI, 0.0, PI / 2.0));
1575
1576 assert!(PieChart::is_angle_in_slice(0.1, 1.5 * PI, 0.5));
1578 }
1579
1580 matches_test!(
1583 piechart_resolution_standard,
1584 PieChart::default()
1585 .resolution(Resolution::Standard)
1586 .resolution,
1587 Resolution::Standard
1588 );
1589 matches_test!(
1590 piechart_resolution_braille,
1591 PieChart::default()
1592 .resolution(Resolution::Braille)
1593 .resolution,
1594 Resolution::Braille
1595 );
1596 matches_test!(
1597 piechart_high_resolution_true,
1598 PieChart::default().high_resolution(true).resolution,
1599 Resolution::Braille
1600 );
1601 matches_test!(
1602 piechart_high_resolution_false,
1603 PieChart::default().high_resolution(false).resolution,
1604 Resolution::Standard
1605 );
1606
1607 matches_test!(
1610 piechart_legend_position_left,
1611 PieChart::default()
1612 .legend_position(LegendPosition::Left)
1613 .legend_position,
1614 LegendPosition::Left
1615 );
1616 matches_test!(
1617 piechart_legend_position_right,
1618 PieChart::default()
1619 .legend_position(LegendPosition::Right)
1620 .legend_position,
1621 LegendPosition::Right
1622 );
1623 matches_test!(
1624 piechart_legend_position_top,
1625 PieChart::default()
1626 .legend_position(LegendPosition::Top)
1627 .legend_position,
1628 LegendPosition::Top
1629 );
1630 matches_test!(
1631 piechart_legend_position_bottom,
1632 PieChart::default()
1633 .legend_position(LegendPosition::Bottom)
1634 .legend_position,
1635 LegendPosition::Bottom
1636 );
1637 matches_test!(
1638 piechart_legend_layout_horizontal,
1639 PieChart::default()
1640 .legend_layout(LegendLayout::Horizontal)
1641 .legend_layout,
1642 LegendLayout::Horizontal
1643 );
1644 matches_test!(
1645 piechart_legend_layout_vertical,
1646 PieChart::default()
1647 .legend_layout(LegendLayout::Vertical)
1648 .legend_layout,
1649 LegendLayout::Vertical
1650 );
1651 matches_test!(
1652 piechart_legend_alignment_left,
1653 PieChart::default()
1654 .legend_alignment(LegendAlignment::Left)
1655 .legend_alignment,
1656 LegendAlignment::Left
1657 );
1658 matches_test!(
1659 piechart_legend_alignment_center,
1660 PieChart::default()
1661 .legend_alignment(LegendAlignment::Center)
1662 .legend_alignment,
1663 LegendAlignment::Center
1664 );
1665 matches_test!(
1666 piechart_legend_alignment_right,
1667 PieChart::default()
1668 .legend_alignment(LegendAlignment::Right)
1669 .legend_alignment,
1670 LegendAlignment::Right
1671 );
1672
1673 #[test]
1676 fn piechart_legend_marker_custom() {
1677 use crate::symbols::LEGEND_MARKER;
1678 let piechart = PieChart::default().legend_marker(LEGEND_MARKER);
1679 assert_eq!(piechart.legend_marker, LEGEND_MARKER);
1680 }
1681
1682 #[test]
1685 fn piechart_format_legend_text_with_percentage() {
1686 let slices = vec![
1687 PieSlice::new("Rust", 50.0, Color::Red),
1688 PieSlice::new("Go", 50.0, Color::Blue),
1689 ];
1690 let piechart = PieChart::new(slices.clone()).show_percentages(true);
1691 let text = piechart.format_legend_text(&slices[0], 100.0, "");
1692 assert!(text.contains("Rust"));
1693 assert!(text.contains("50.0%"));
1694 }
1695
1696 #[test]
1697 fn piechart_format_legend_text_without_percentage() {
1698 let slices = vec![PieSlice::new("Rust", 50.0, Color::Red)];
1699 let piechart = PieChart::new(slices.clone()).show_percentages(false);
1700 let text = piechart.format_legend_text(&slices[0], 100.0, "");
1701 assert!(text.contains("Rust"));
1702 assert!(!text.contains('%'));
1703 }
1704
1705 #[test]
1706 fn piechart_format_legend_text_zero_total() {
1707 let slices = vec![PieSlice::new("X", 0.0, Color::Red)];
1708 let piechart = PieChart::new(slices.clone()).show_percentages(true);
1709 let text = piechart.format_legend_text(&slices[0], 0.0, "");
1710 assert!(text.contains("0.0%"));
1711 }
1712
1713 #[test]
1716 fn piechart_calculate_aligned_x_left() {
1717 let slices = vec![PieSlice::new("A", 100.0, Color::Red)];
1718 let piechart = PieChart::new(slices).legend_alignment(LegendAlignment::Left);
1719 let area = Rect::new(5, 0, 20, 10);
1720 assert_eq!(piechart.calculate_aligned_x(area, 10), 5);
1721 }
1722
1723 #[test]
1724 fn piechart_calculate_aligned_x_center() {
1725 let slices = vec![PieSlice::new("A", 100.0, Color::Red)];
1726 let piechart = PieChart::new(slices).legend_alignment(LegendAlignment::Center);
1727 let area = Rect::new(0, 0, 20, 10);
1728 assert_eq!(piechart.calculate_aligned_x(area, 10), 5);
1730 }
1731
1732 #[test]
1733 fn piechart_calculate_aligned_x_right() {
1734 let slices = vec![PieSlice::new("A", 100.0, Color::Red)];
1735 let piechart = PieChart::new(slices).legend_alignment(LegendAlignment::Right);
1736 let area = Rect::new(0, 0, 20, 10);
1737 assert_eq!(piechart.calculate_aligned_x(area, 10), 10);
1739 }
1740
1741 render_test!(
1744 piechart_render_braille,
1745 {
1746 let slices = vec![
1747 PieSlice::new("Rust", 60.0, Color::Red),
1748 PieSlice::new("Go", 40.0, Color::Blue),
1749 ];
1750 PieChart::new(slices).resolution(Resolution::Braille)
1751 },
1752 Rect::new(0, 0, 40, 20)
1753 );
1754
1755 render_test!(
1756 piechart_render_braille_with_legend,
1757 {
1758 let slices = vec![
1759 PieSlice::new("Rust", 60.0, Color::Red),
1760 PieSlice::new("Go", 40.0, Color::Blue),
1761 ];
1762 PieChart::new(slices)
1763 .resolution(Resolution::Braille)
1764 .show_legend(true)
1765 },
1766 Rect::new(0, 0, 40, 20)
1767 );
1768
1769 render_test!(
1772 piechart_render_legend_left,
1773 {
1774 let slices = vec![
1775 PieSlice::new("Alpha", 50.0, Color::Red),
1776 PieSlice::new("Beta", 50.0, Color::Blue),
1777 ];
1778 PieChart::new(slices)
1779 .show_legend(true)
1780 .legend_position(LegendPosition::Left)
1781 .legend_layout(LegendLayout::Vertical)
1782 },
1783 Rect::new(0, 0, 60, 20)
1784 );
1785
1786 render_test!(
1787 piechart_render_legend_right,
1788 {
1789 let slices = vec![
1790 PieSlice::new("Alpha", 50.0, Color::Red),
1791 PieSlice::new("Beta", 50.0, Color::Blue),
1792 ];
1793 PieChart::new(slices)
1794 .show_legend(true)
1795 .legend_position(LegendPosition::Right)
1796 .legend_layout(LegendLayout::Vertical)
1797 },
1798 Rect::new(0, 0, 60, 20)
1799 );
1800
1801 render_test!(
1802 piechart_render_legend_top_horizontal,
1803 {
1804 let slices = vec![
1805 PieSlice::new("Alpha", 50.0, Color::Red),
1806 PieSlice::new("Beta", 50.0, Color::Blue),
1807 ];
1808 PieChart::new(slices)
1809 .show_legend(true)
1810 .legend_position(LegendPosition::Top)
1811 .legend_layout(LegendLayout::Horizontal)
1812 },
1813 Rect::new(0, 0, 60, 20)
1814 );
1815
1816 render_test!(
1817 piechart_render_legend_bottom_horizontal,
1818 {
1819 let slices = vec![
1820 PieSlice::new("Alpha", 50.0, Color::Red),
1821 PieSlice::new("Beta", 50.0, Color::Blue),
1822 ];
1823 PieChart::new(slices)
1824 .show_legend(true)
1825 .legend_position(LegendPosition::Bottom)
1826 .legend_layout(LegendLayout::Horizontal)
1827 },
1828 Rect::new(0, 0, 60, 20)
1829 );
1830
1831 render_test!(
1832 piechart_render_legend_top_vertical,
1833 {
1834 let slices = vec![
1835 PieSlice::new("Alpha", 50.0, Color::Red),
1836 PieSlice::new("Beta", 50.0, Color::Blue),
1837 ];
1838 PieChart::new(slices)
1839 .show_legend(true)
1840 .legend_position(LegendPosition::Top)
1841 .legend_layout(LegendLayout::Vertical)
1842 },
1843 Rect::new(0, 0, 60, 20)
1844 );
1845
1846 render_test!(
1847 piechart_render_legend_bottom_vertical,
1848 {
1849 let slices = vec![
1850 PieSlice::new("Alpha", 50.0, Color::Red),
1851 PieSlice::new("Beta", 50.0, Color::Blue),
1852 ];
1853 PieChart::new(slices)
1854 .show_legend(true)
1855 .legend_position(LegendPosition::Bottom)
1856 .legend_layout(LegendLayout::Vertical)
1857 },
1858 Rect::new(0, 0, 60, 20)
1859 );
1860
1861 render_test!(
1862 piechart_render_legend_left_horizontal,
1863 {
1864 let slices = vec![
1865 PieSlice::new("Alpha", 50.0, Color::Red),
1866 PieSlice::new("Beta", 50.0, Color::Blue),
1867 ];
1868 PieChart::new(slices)
1869 .show_legend(true)
1870 .legend_position(LegendPosition::Left)
1871 .legend_layout(LegendLayout::Horizontal)
1872 },
1873 Rect::new(0, 0, 60, 20)
1874 );
1875
1876 render_test!(
1877 piechart_render_legend_right_horizontal,
1878 {
1879 let slices = vec![
1880 PieSlice::new("Alpha", 50.0, Color::Red),
1881 PieSlice::new("Beta", 50.0, Color::Blue),
1882 ];
1883 PieChart::new(slices)
1884 .show_legend(true)
1885 .legend_position(LegendPosition::Right)
1886 .legend_layout(LegendLayout::Horizontal)
1887 },
1888 Rect::new(0, 0, 60, 20)
1889 );
1890
1891 render_test!(
1894 piechart_render_legend_with_percentages,
1895 {
1896 let slices = vec![
1897 PieSlice::new("Rust", 45.0, Color::Red),
1898 PieSlice::new("Go", 30.0, Color::Blue),
1899 PieSlice::new("Python", 25.0, Color::Green),
1900 ];
1901 PieChart::new(slices)
1902 .show_legend(true)
1903 .show_percentages(true)
1904 },
1905 Rect::new(0, 0, 60, 20)
1906 );
1907
1908 render_test!(
1910 piechart_render_legend_alignment_center,
1911 {
1912 let slices = vec![
1913 PieSlice::new("A", 50.0, Color::Red),
1914 PieSlice::new("B", 50.0, Color::Blue),
1915 ];
1916 PieChart::new(slices)
1917 .show_legend(true)
1918 .legend_alignment(LegendAlignment::Center)
1919 },
1920 Rect::new(0, 0, 60, 20)
1921 );
1922
1923 render_test!(
1924 piechart_render_legend_alignment_right,
1925 {
1926 let slices = vec![
1927 PieSlice::new("A", 50.0, Color::Red),
1928 PieSlice::new("B", 50.0, Color::Blue),
1929 ];
1930 PieChart::new(slices)
1931 .show_legend(true)
1932 .legend_alignment(LegendAlignment::Right)
1933 },
1934 Rect::new(0, 0, 60, 20)
1935 );
1936
1937 #[test]
1940 fn piechart_layout_too_small_no_legend() {
1941 let slices = vec![PieSlice::new("A", 100.0, Color::Red)];
1942 let piechart = PieChart::new(slices).show_legend(true);
1943 let area = Rect::new(0, 0, 10, 5);
1945 let (pie_area, legend_opt) = piechart.calculate_layout(area);
1946 assert_eq!(pie_area, area);
1947 assert!(legend_opt.is_none());
1948 }
1949
1950 #[test]
1951 fn piechart_layout_show_legend_false_no_legend() {
1952 let slices = vec![PieSlice::new("A", 100.0, Color::Red)];
1953 let piechart = PieChart::new(slices).show_legend(false);
1954 let area = Rect::new(0, 0, 60, 20);
1955 let (pie_area, legend_opt) = piechart.calculate_layout(area);
1956 assert_eq!(pie_area, area);
1957 assert!(legend_opt.is_none());
1958 }
1959
1960 render_empty_test!(piechart_render_empty_slices, PieChart::default());
1963
1964 render_test!(
1967 piechart_render_single_slice,
1968 PieChart::new(vec![PieSlice::new("Only", 100.0, Color::Cyan)]),
1969 Rect::new(0, 0, 30, 15)
1970 );
1971
1972 #[test]
1975 fn piechart_percentage_zero_total() {
1976 let slices = vec![PieSlice::new("A", 0.0, Color::Red)];
1977 let piechart = PieChart::new(slices.clone());
1978 assert_eq!(piechart.percentage(&slices[0]), 0.0);
1979 }
1980
1981 #[test]
1986 fn piechart_calculate_legend_width_vertical_zero_total_with_percentages() {
1987 let slices = vec![
1988 PieSlice::new("A", 0.0, Color::Red),
1989 PieSlice::new("B", 0.0, Color::Blue),
1990 ];
1991 let piechart = PieChart::new(slices)
1992 .legend_layout(LegendLayout::Vertical)
1993 .show_percentages(true);
1994 let width = piechart.calculate_legend_width();
1995 assert!(width > 0);
1997 }
1998
1999 render_test!(
2000 piechart_render_legend_vertical_zero_total_with_percentages,
2001 PieChart::new(vec![
2002 PieSlice::new("A", 0.0, Color::Red),
2003 PieSlice::new("B", 0.0, Color::Blue),
2004 ])
2005 .show_legend(true)
2006 .show_percentages(true)
2007 .legend_position(LegendPosition::Right)
2008 .legend_layout(LegendLayout::Vertical),
2009 Rect::new(0, 0, 60, 20)
2010 );
2011
2012 render_test!(
2015 piechart_render_legend_vertical_no_percentages,
2016 PieChart::new(vec![
2017 PieSlice::new("Rust", 60.0, Color::Red),
2018 PieSlice::new("Go", 40.0, Color::Blue),
2019 ])
2020 .show_legend(true)
2021 .show_percentages(false)
2022 .legend_position(LegendPosition::Right)
2023 .legend_layout(LegendLayout::Vertical),
2024 Rect::new(0, 0, 60, 20)
2025 );
2026
2027 render_test!(
2030 piechart_render_all_zero_values,
2031 PieChart::new(vec![
2032 PieSlice::new("A", 0.0, Color::Red),
2033 PieSlice::new("B", 0.0, Color::Blue),
2034 ]),
2035 Rect::new(0, 0, 40, 20)
2036 );
2037
2038 render_test!(
2041 piechart_render_zero_value_slice_in_mix,
2042 PieChart::new(vec![
2043 PieSlice::new("A", 100.0, Color::Red),
2044 PieSlice::new("B", 0.0, Color::Blue),
2045 ]),
2046 Rect::new(0, 0, 40, 20)
2047 );
2048
2049 render_test!(
2052 piechart_render_tiny_area_radius_zero,
2053 PieChart::new(vec![PieSlice::new("A", 100.0, Color::Red)]),
2054 Rect::new(0, 0, 1, 1)
2055 );
2056
2057 render_test!(
2061 piechart_render_vertical_legend_overflow,
2062 PieChart::new(vec![
2063 PieSlice::new("Slice1", 10.0, Color::Red),
2064 PieSlice::new("Slice2", 10.0, Color::Blue),
2065 PieSlice::new("Slice3", 10.0, Color::Green),
2066 PieSlice::new("Slice4", 10.0, Color::Yellow),
2067 PieSlice::new("Slice5", 10.0, Color::Cyan),
2068 PieSlice::new("Slice6", 10.0, Color::Magenta),
2069 PieSlice::new("Slice7", 10.0, Color::White),
2070 PieSlice::new("Slice8", 10.0, Color::Red),
2071 PieSlice::new("Slice9", 10.0, Color::Blue),
2072 PieSlice::new("Slice10", 10.0, Color::Green),
2073 ])
2074 .show_legend(true)
2075 .legend_position(LegendPosition::Right)
2076 .legend_layout(LegendLayout::Vertical),
2077 Rect::new(0, 0, 60, 12)
2079 );
2080
2081 render_test!(
2085 piechart_render_horizontal_legend_overflow,
2086 PieChart::new(vec![
2087 PieSlice::new("LongLabelA", 20.0, Color::Red),
2088 PieSlice::new("LongLabelB", 20.0, Color::Blue),
2089 PieSlice::new("LongLabelC", 20.0, Color::Green),
2090 PieSlice::new("LongLabelD", 20.0, Color::Yellow),
2091 PieSlice::new("LongLabelE", 20.0, Color::Cyan),
2092 ])
2093 .show_legend(true)
2094 .legend_position(LegendPosition::Bottom)
2095 .legend_layout(LegendLayout::Horizontal),
2096 Rect::new(0, 0, 22, 15)
2098 );
2099
2100 #[test]
2103 fn piechart_layout_horizontal_split_too_narrow() {
2104 let area = Rect::new(0, 0, 5, 20);
2105 let (pie_area, legend_opt) = PieChart::<'_>::layout_horizontal_split(area, 100, true);
2107 assert_eq!(pie_area, area);
2108 assert!(legend_opt.is_none());
2109 }
2110
2111 #[test]
2114 fn piechart_layout_vertical_split_too_short() {
2115 let area = Rect::new(0, 0, 60, 3);
2116 let (pie_area, legend_opt) = PieChart::<'_>::layout_vertical_split(area, 100, true);
2118 assert_eq!(pie_area, area);
2119 assert!(legend_opt.is_none());
2120 }
2121
2122 #[test]
2126 fn piechart_calculate_legend_width_horizontal_with_percentages() {
2127 let slices = vec![
2128 PieSlice::new("Rust", 60.0, Color::Red),
2129 PieSlice::new("Go", 40.0, Color::Blue),
2130 ];
2131 let piechart = PieChart::new(slices)
2132 .legend_layout(LegendLayout::Horizontal)
2133 .show_percentages(true);
2134 let width = piechart.calculate_legend_width();
2135 assert!(width > 0);
2136 }
2137
2138 #[test]
2139 fn piechart_calculate_legend_width_horizontal_without_percentages() {
2140 let slices = vec![
2141 PieSlice::new("Alpha", 50.0, Color::Red),
2142 PieSlice::new("Beta", 50.0, Color::Blue),
2143 ];
2144 let piechart = PieChart::new(slices)
2145 .legend_layout(LegendLayout::Horizontal)
2146 .show_percentages(false);
2147 let width = piechart.calculate_legend_width();
2148 assert!(width > 0);
2149 }
2150
2151 #[test]
2152 fn piechart_calculate_legend_width_horizontal_zero_total() {
2153 let slices = vec![
2154 PieSlice::new("A", 0.0, Color::Red),
2155 PieSlice::new("B", 0.0, Color::Blue),
2156 ];
2157 let piechart = PieChart::new(slices)
2158 .legend_layout(LegendLayout::Horizontal)
2159 .show_percentages(true);
2160 let width = piechart.calculate_legend_width();
2161 assert!(width > 0);
2163 }
2164
2165 #[test]
2168 fn piechart_calculate_legend_horizontal_width_zero_total() {
2169 let slices = vec![
2170 PieSlice::new("X", 0.0, Color::Red),
2171 PieSlice::new("Y", 0.0, Color::Blue),
2172 ];
2173 let piechart = PieChart::new(slices).show_percentages(true);
2174 let width = piechart.calculate_legend_horizontal_width();
2175 assert!(width > 0);
2176 }
2177
2178 #[test]
2179 fn piechart_calculate_legend_horizontal_width_without_percentages() {
2180 let slices = vec![PieSlice::new("Item", 100.0, Color::Green)];
2181 let piechart = PieChart::new(slices).show_percentages(false);
2182 let width = piechart.calculate_legend_horizontal_width();
2183 assert!(width > 0);
2184 }
2185
2186 #[test]
2189 fn piechart_calculate_vertical_grid_height_narrow() {
2190 let slices = vec![
2191 PieSlice::new("A", 50.0, Color::Red),
2192 PieSlice::new("B", 50.0, Color::Blue),
2193 ];
2194 let piechart = PieChart::new(slices);
2195 let height = piechart.calculate_vertical_grid_height(1);
2197 assert!(height >= 4);
2198 }
2199
2200 #[test]
2203 fn piechart_value_percent_positive_total() {
2204 assert_eq!(PieChart::value_percent(25.0, 200.0), 12.5);
2205 }
2206
2207 #[test]
2208 fn piechart_value_percent_zero_total() {
2209 assert_eq!(PieChart::value_percent(25.0, 0.0), 0.0);
2210 }
2211
2212 #[test]
2213 fn piechart_value_percent_negative_total() {
2214 assert_eq!(PieChart::value_percent(25.0, -5.0), 0.0);
2216 }
2217
2218 #[test]
2219 fn piechart_slice_angles_full_circle_flag() {
2220 let (_, _, is_full) = PieChart::slice_angles(0.0, 100.0);
2221 assert!(is_full);
2222 }
2223
2224 #[test]
2225 fn piechart_slice_angles_partial_not_full() {
2226 let (start, end, is_full) = PieChart::slice_angles(0.0, 50.0);
2227 assert!(!is_full);
2228 assert!((end - start - PI).abs() < 1e-9);
2229 }
2230
2231 #[test]
2232 fn piechart_legend_item_width_matches_text() {
2233 let slices = vec![PieSlice::new("Rust", 50.0, Color::Red)];
2234 let chart = PieChart::new(slices.clone()).show_percentages(true);
2235 let expected = chart
2236 .format_legend_text(&slices[0], 100.0, " ")
2237 .chars()
2238 .count();
2239 assert_eq!(
2240 usize::from(chart.legend_item_width(&slices[0], 100.0)),
2241 expected
2242 );
2243 }
2244
2245 #[test]
2246 fn piechart_legend_item_width_unicode_label() {
2247 let slices = vec![PieSlice::new("日本語", 100.0, Color::Red)];
2249 let chart = PieChart::new(slices.clone()).show_percentages(false);
2250 assert_eq!(chart.legend_item_width(&slices[0], 100.0), 7);
2252 }
2253
2254 #[test]
2257 fn piechart_full_circle_fills_more_than_a_line() {
2258 fn filled_cells(chart: &PieChart) -> usize {
2259 let mut buffer = Buffer::empty(Rect::new(0, 0, 30, 15));
2260 Widget::render(chart, buffer.area, &mut buffer);
2261 buffer
2262 .content
2263 .iter()
2264 .filter(|c| !c.symbol().trim().is_empty())
2265 .count()
2266 }
2267
2268 let standard = PieChart::new(vec![PieSlice::new("Only", 100.0, Color::Green)])
2269 .show_legend(false)
2270 .show_percentages(false);
2271 assert!(filled_cells(&standard) > 30);
2272
2273 let braille = PieChart::new(vec![PieSlice::new("Only", 100.0, Color::Green)])
2274 .resolution(Resolution::Braille)
2275 .show_legend(false)
2276 .show_percentages(false);
2277 assert!(filled_cells(&braille) > 30);
2278 }
2279}