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 funnel;
104pub mod gauge;
105pub mod legend;
106pub mod pie_style;
107pub mod radar;
108pub mod radial;
109pub mod treemap;
110mod util;
111pub mod waterfall;
112#[macro_use]
113pub mod macros;
114pub mod symbols;
115pub mod title;
116
117pub use funnel::{Funnel, FunnelStage, FunnelStyle};
119pub use gauge::{GaugeChart, GaugeStyle};
120pub use legend::{LegendAlignment, LegendLayout, LegendPosition};
121pub use pie_style::PieStyle;
122pub use radar::{RadarChart, RadarSeries};
123pub use radial::{RadialBar, RadialRing, RadialSweep, RingStack};
124pub use title::{BlockExt, TitleAlignment, TitlePosition, TitleStyle};
125pub use treemap::{Treemap, TreemapItem};
126#[cfg(feature = "theme")]
127pub use tui_nuances::{Theme, ThemeProvider, ThemeSwitcher, Themed};
128pub use waterfall::{Waterfall, WaterfallStep};
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
152pub enum Resolution {
153 #[default]
157 Standard,
158
159 Braille,
164}
165
166#[derive(Debug, Clone, PartialEq)]
179pub struct PieSlice<'a> {
180 label: &'a str,
182 value: f64,
184 color: Color,
186}
187
188impl<'a> PieSlice<'a> {
189 #[must_use]
200 pub const fn new(label: &'a str, value: f64, color: Color) -> Self {
201 Self {
202 label,
203 value,
204 color,
205 }
206 }
207
208 #[cfg(feature = "theme")]
216 #[must_use]
217 pub fn themed(label: &'a str, value: f64, theme: &Theme, index: usize) -> Self {
218 Self::new(label, value, theme.color(index))
219 }
220
221 #[must_use]
223 pub const fn label(&self) -> &'a str {
224 self.label
225 }
226
227 #[must_use]
229 pub const fn value(&self) -> f64 {
230 self.value
231 }
232
233 #[must_use]
235 pub const fn color(&self) -> Color {
236 self.color
237 }
238}
239
240#[cfg(feature = "theme")]
241impl Themed for PieChart<'_> {
242 fn apply_theme(self, theme: &Theme) -> Self {
243 self.theme(theme)
244 }
245}
246
247#[derive(Debug, Clone, PartialEq)]
266pub struct PieChart<'a> {
267 slices: Vec<PieSlice<'a>>,
269 block: Option<Block<'a>>,
271 style: Style,
273 theme_style: Style,
275 show_legend: bool,
277 show_percentages: bool,
279 pie_char: char,
281 legend_marker: &'a str,
283 resolution: Resolution,
285 legend_position: LegendPosition,
287 legend_layout: LegendLayout,
289 legend_alignment: LegendAlignment,
291 pie_style: PieStyle,
293 hole: f64,
295 start: f64,
297 gap: f64,
299 explode: f64,
301 center: Option<&'a str>,
303}
304
305impl Default for PieChart<'_> {
306 fn default() -> Self {
317 Self {
318 slices: Vec::new(),
319 block: None,
320 style: Style::default(),
321 theme_style: Style::default(),
322 show_legend: true,
323 show_percentages: true,
324 pie_char: symbols::PIE_CHAR,
325 legend_marker: symbols::LEGEND_MARKER,
326 resolution: Resolution::default(),
327 legend_position: LegendPosition::default(),
328 legend_layout: LegendLayout::default(),
329 legend_alignment: LegendAlignment::default(),
330 pie_style: PieStyle::default(),
331 hole: 0.0,
332 start: 0.0,
333 gap: 0.0,
334 explode: 0.0,
335 center: None,
336 }
337 }
338}
339
340impl<'a> PieChart<'a> {
341 #[must_use]
356 pub fn new(slices: Vec<PieSlice<'a>>) -> Self {
357 Self {
358 slices,
359 ..Default::default()
360 }
361 }
362
363 #[must_use]
377 pub fn slices(mut self, slices: Vec<PieSlice<'a>>) -> Self {
378 self.slices = slices;
379 self
380 }
381
382 #[must_use]
396 pub fn block(mut self, block: Block<'a>) -> Self {
397 self.block = Some(block);
398 self
399 }
400
401 #[must_use]
413 pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
414 self.style = style.into();
415 self
416 }
417
418 #[cfg(feature = "theme")]
428 #[must_use]
429 pub fn theme(mut self, theme: &Theme) -> Self {
430 self.theme_style = theme.style();
431 self
432 }
433
434 #[must_use]
444 pub const fn show_legend(mut self, show: bool) -> Self {
445 self.show_legend = show;
446 self
447 }
448
449 #[must_use]
459 pub const fn show_percentages(mut self, show: bool) -> Self {
460 self.show_percentages = show;
461 self
462 }
463
464 #[must_use]
487 pub const fn pie_char(mut self, c: char) -> Self {
488 self.pie_char = c;
489 self
490 }
491
492 #[must_use]
522 pub const fn legend_marker(mut self, marker: &'a str) -> Self {
523 self.legend_marker = marker;
524 self
525 }
526
527 #[must_use]
542 pub const fn resolution(mut self, resolution: Resolution) -> Self {
543 self.resolution = resolution;
544 self
545 }
546
547 #[must_use]
560 pub const fn high_resolution(mut self, enabled: bool) -> Self {
561 self.resolution = if enabled {
562 Resolution::Braille
563 } else {
564 Resolution::Standard
565 };
566 self
567 }
568
569 #[must_use]
580 pub const fn legend_position(mut self, position: LegendPosition) -> Self {
581 self.legend_position = position;
582 self
583 }
584
585 #[must_use]
601 pub const fn legend_layout(mut self, layout: LegendLayout) -> Self {
602 self.legend_layout = layout;
603 self
604 }
605
606 #[must_use]
622 pub const fn legend_alignment(mut self, alignment: LegendAlignment) -> Self {
623 self.legend_alignment = alignment;
624 self
625 }
626
627 #[must_use]
637 pub fn hole(mut self, hole: f64) -> Self {
638 self.hole = if hole.is_finite() {
639 hole.clamp(0.0, 0.95)
640 } else {
641 0.0
642 };
643 self
644 }
645
646 #[must_use]
656 pub fn start_angle(mut self, degrees: f64) -> Self {
657 self.start = if degrees.is_finite() {
658 (degrees / 360.0).rem_euclid(1.0)
659 } else {
660 0.0
661 };
662 self
663 }
664
665 #[must_use]
670 pub fn gap(mut self, dots: f64) -> Self {
671 self.gap = if dots.is_finite() {
672 dots.clamp(0.0, 8.0)
673 } else {
674 0.0
675 };
676 self
677 }
678
679 #[must_use]
681 pub fn explode(mut self, dots: f64) -> Self {
682 self.explode = if dots.is_finite() {
683 dots.clamp(0.0, 8.0)
684 } else {
685 0.0
686 };
687 self
688 }
689
690 #[must_use]
692 pub const fn pie_style(mut self, style: PieStyle) -> Self {
693 self.pie_style = style;
694 self
695 }
696
697 #[must_use]
699 pub const fn center(mut self, text: &'a str) -> Self {
700 self.center = Some(text);
701 self
702 }
703
704 fn total_value(&self) -> f64 {
705 self.slices.iter().map(|s| s.value).sum()
706 }
707
708 fn value_percent(value: f64, total: f64) -> f64 {
713 if total > 0.0 {
714 (value / total) * 100.0
715 } else {
716 0.0
717 }
718 }
719
720 fn percentage(&self, slice: &PieSlice) -> f64 {
722 Self::value_percent(slice.value, self.total_value())
723 }
724}
725
726impl Styled for PieChart<'_> {
727 type Item = Self;
728
729 fn style(&self) -> Style {
730 self.style
731 }
732
733 fn set_style<S: Into<Style>>(mut self, style: S) -> Self::Item {
734 self.style = style.into();
735 self
736 }
737}
738
739impl Widget for PieChart<'_> {
740 fn render(self, area: Rect, buf: &mut Buffer) {
741 Widget::render(&self, area, buf);
742 }
743}
744
745impl Widget for &PieChart<'_> {
746 fn render(self, area: Rect, buf: &mut Buffer) {
747 buf.set_style(area, self.theme_style.patch(self.style));
748 let inner = if let Some(ref block) = self.block {
749 let inner_area = block.inner(area);
750 block.render(area, buf);
751 inner_area
752 } else {
753 area
754 };
755 self.render_piechart(inner, buf);
756 }
757}
758
759impl PieChart<'_> {
760 const LEGEND_VERTICAL_MAX_RATIO: u16 = 3;
762
763 const LEGEND_VERTICAL_MIN_WIDTH: u16 = 20;
765
766 const LEGEND_HORIZONTAL_MAX_RATIO: u16 = 5;
769
770 const LEGEND_HORIZONTAL_MAX_WIDTH: u16 = 60;
772
773 const LEGEND_VERTICAL_MAX_HEIGHT: u16 = 9;
776
777 const LEGEND_HORIZONTAL_HEIGHT: u16 = 3;
779
780 const LEGEND_SPACING: u16 = 1;
782
783 const LEGEND_PADDING: u16 = 1;
785
786 fn render_piechart(&self, area: Rect, buf: &mut Buffer) {
787 if area.is_empty() || self.slices.is_empty() {
788 return;
789 }
790
791 let total = self.total_value();
792 if total <= 0.0 {
793 return;
794 }
795
796 if self.needs_canvas() {
797 self.render_piechart_canvas(area, buf);
798 return;
799 }
800
801 match self.resolution {
802 Resolution::Standard => {
803 }
805 Resolution::Braille => {
806 self.render_piechart_braille(area, buf);
807 return;
808 }
809 }
810
811 let (pie_area, legend_area_opt) = self.calculate_layout(area);
813
814 let center_x = pie_area.width / 2;
817 let center_y = pie_area.height / 2;
818
819 let radius = center_x.min(center_y * 2).saturating_sub(1);
821
822 let mut cumulative_percent = self.start * 100.0;
824 for slice in &self.slices {
825 let percent = self.percentage(slice);
826 self.render_slice(
827 pie_area,
828 buf,
829 center_x,
830 center_y,
831 radius,
832 cumulative_percent,
833 percent,
834 slice.color,
835 );
836 cumulative_percent += percent;
837 }
838
839 self.draw_center(pie_area, buf);
840
841 if let Some(legend_area) = legend_area_opt {
843 self.render_legend(buf, legend_area);
844 }
845 }
846
847 fn slice_angles(start_percent: f64, percent: f64) -> (f64, f64, bool) {
853 let start_angle = (start_percent / 100.0) * 2.0 * PI - PI / 2.0;
854 let end_angle = ((start_percent + percent) / 100.0) * 2.0 * PI - PI / 2.0;
855 let is_full_circle = percent >= 100.0 - f64::EPSILON;
856 (start_angle, end_angle, is_full_circle)
857 }
858
859 #[allow(clippy::too_many_arguments, clippy::similar_names)]
860 fn render_slice(
861 &self,
862 area: Rect,
863 buf: &mut Buffer,
864 center_x: u16,
865 center_y: u16,
866 radius: u16,
867 start_percent: f64,
868 percent: f64,
869 color: Color,
870 ) {
871 if radius == 0 || percent <= 0.0 {
872 return;
873 }
874
875 let (start_angle, end_angle, is_full_circle) = Self::slice_angles(start_percent, percent);
879
880 let scan_width = i32::from(radius + 1);
882 let scan_height = i32::from((radius / 2) + 1); for dy in -scan_height..=scan_height {
885 for dx in -scan_width..=scan_width {
886 let x = i32::from(area.x) + i32::from(center_x) + dx;
888 let y = i32::from(area.y) + i32::from(center_y) + dy;
889
890 if x < i32::from(area.x)
892 || x >= i32::from(area.x + area.width)
893 || y < i32::from(area.y)
894 || y >= i32::from(area.y + area.height)
895 {
896 continue;
897 }
898
899 #[allow(clippy::cast_precision_loss)]
901 let adjusted_dx = f64::from(dx);
902 #[allow(clippy::cast_precision_loss)]
903 let adjusted_dy = f64::from(dy * 2);
904
905 let distance = (adjusted_dx * adjusted_dx + adjusted_dy * adjusted_dy).sqrt();
907
908 #[allow(clippy::cast_precision_loss)]
910 if distance <= f64::from(radius) && distance >= self.hole * f64::from(radius) {
911 let angle = adjusted_dy.atan2(adjusted_dx);
913
914 if is_full_circle || Self::is_angle_in_slice(angle, start_angle, end_angle) {
916 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
917 {
918 let cell = &mut buf[(x as u16, y as u16)];
919 cell.set_char(self.pie_char).set_fg(color);
920 }
921 }
922 }
923 }
924 }
925 }
926
927 fn is_angle_in_slice(angle: f64, start: f64, end: f64) -> bool {
928 let normalize = |a: f64| {
930 let mut normalized = a % (2.0 * PI);
931 if normalized < 0.0 {
932 normalized += 2.0 * PI;
933 }
934 normalized
935 };
936
937 let norm_angle = normalize(angle);
938 let norm_start = normalize(start);
939 let norm_end = normalize(end);
940
941 if norm_start <= norm_end {
942 norm_angle >= norm_start && norm_angle <= norm_end
943 } else {
944 norm_angle >= norm_start || norm_angle <= norm_end
946 }
947 }
948
949 fn format_legend_text(&self, slice: &PieSlice, total: f64, spacing: &str) -> String {
950 if self.show_percentages {
951 let percent = Self::value_percent(slice.value, total);
952 format!(
953 "{} {} {:.1}%{}",
954 self.legend_marker, slice.label, percent, spacing
955 )
956 } else {
957 format!("{} {}{}", self.legend_marker, slice.label, spacing)
958 }
959 }
960
961 fn legend_item_width(&self, slice: &PieSlice, total: f64) -> u16 {
964 u16::try_from(self.format_legend_text(slice, total, " ").chars().count())
965 .unwrap_or(u16::MAX)
966 }
967
968 fn calculate_aligned_x(&self, legend_area: Rect, content_width: u16) -> u16 {
969 match self.legend_alignment {
970 LegendAlignment::Left => legend_area.x,
971 LegendAlignment::Center => {
972 legend_area.x + (legend_area.width.saturating_sub(content_width)) / 2
973 }
974 LegendAlignment::Right => {
975 legend_area.x + legend_area.width.saturating_sub(content_width)
976 }
977 }
978 }
979
980 fn render_legend(&self, buf: &mut Buffer, legend_area: Rect) {
981 let total = self.total_value();
982
983 match self.legend_layout {
984 LegendLayout::Vertical => {
985 self.render_vertical_legend(buf, legend_area, total);
986 }
987 LegendLayout::Horizontal => {
988 self.render_horizontal_legend(buf, legend_area, total);
989 }
990 }
991 }
992
993 fn render_vertical_legend(&self, buf: &mut Buffer, legend_area: Rect, total: f64) {
994 for (idx, slice) in self.slices.iter().enumerate() {
995 #[allow(clippy::cast_possible_truncation)]
996 let y_offset = (idx as u16) * 2;
997
998 if y_offset >= legend_area.height {
999 break;
1000 }
1001
1002 let legend_text = self.format_legend_text(slice, total, "");
1003 #[allow(clippy::cast_possible_truncation)]
1004 let text_width = u16::try_from(legend_text.chars().count()).unwrap_or(u16::MAX);
1005 let x_pos = self.calculate_aligned_x(legend_area, text_width);
1006
1007 let line = Line::from(vec![Span::styled(
1008 legend_text,
1009 Style::default().fg(slice.color),
1010 )]);
1011 let item_area = Rect {
1012 x: x_pos,
1013 y: legend_area.y + y_offset,
1014 width: text_width.min(legend_area.width),
1015 height: 1,
1016 };
1017
1018 line.render(item_area, buf);
1019 }
1020 }
1021
1022 fn render_horizontal_legend(&self, buf: &mut Buffer, legend_area: Rect, total: f64) {
1023 let mut total_width = 0u16;
1024 let mut item_widths = Vec::new();
1025
1026 for slice in &self.slices {
1027 let legend_text = self.format_legend_text(slice, total, " ");
1028 #[allow(clippy::cast_possible_truncation)]
1029 let text_width = u16::try_from(legend_text.chars().count()).unwrap_or(u16::MAX);
1030 item_widths.push(text_width);
1031 total_width = total_width.saturating_add(text_width);
1032 }
1033
1034 let start_x = self.calculate_aligned_x(legend_area, total_width.min(legend_area.width));
1035 let mut x_offset = 0u16;
1036
1037 for (idx, slice) in self.slices.iter().enumerate() {
1038 if x_offset >= legend_area.width {
1039 break;
1040 }
1041
1042 let legend_text = self.format_legend_text(slice, total, " ");
1043 let text_width = item_widths[idx];
1044
1045 let line = Line::from(vec![Span::styled(
1046 legend_text,
1047 Style::default().fg(slice.color),
1048 )]);
1049 let item_area = Rect {
1050 x: start_x + x_offset,
1051 y: legend_area.y,
1052 width: text_width.min(legend_area.width.saturating_sub(x_offset)),
1053 height: 1,
1054 };
1055
1056 line.render(item_area, buf);
1057 x_offset = x_offset.saturating_add(text_width);
1058 }
1059 }
1060
1061 #[allow(clippy::too_many_lines)]
1062 fn calculate_layout(&self, area: Rect) -> (Rect, Option<Rect>) {
1063 if !self.show_legend || area.width < 20 || area.height < 10 {
1064 return (area, None);
1065 }
1066
1067 match (self.legend_position, self.legend_layout) {
1069 (LegendPosition::Left | LegendPosition::Right, LegendLayout::Vertical) => {
1071 let legend_width = self
1072 .calculate_legend_width()
1073 .min(area.width / Self::LEGEND_VERTICAL_MAX_RATIO)
1074 .max(Self::LEGEND_VERTICAL_MIN_WIDTH);
1075 let is_left = matches!(self.legend_position, LegendPosition::Left);
1076 Self::layout_horizontal_split(area, legend_width, is_left)
1077 }
1078 (LegendPosition::Top | LegendPosition::Bottom, LegendLayout::Horizontal) => {
1080 let is_top = matches!(self.legend_position, LegendPosition::Top);
1081 Self::layout_vertical_split(area, Self::LEGEND_HORIZONTAL_HEIGHT, is_top)
1082 }
1083 (LegendPosition::Left | LegendPosition::Right, LegendLayout::Horizontal) => {
1085 let legend_width = self
1087 .calculate_legend_horizontal_width()
1088 .min(
1089 (area.width * (Self::LEGEND_HORIZONTAL_MAX_RATIO - 1))
1090 / Self::LEGEND_HORIZONTAL_MAX_RATIO,
1091 )
1092 .min(Self::LEGEND_HORIZONTAL_MAX_WIDTH);
1093 let is_left = matches!(self.legend_position, LegendPosition::Left);
1094 Self::layout_horizontal_split(area, legend_width, is_left)
1095 }
1096 (LegendPosition::Top | LegendPosition::Bottom, LegendLayout::Vertical) => {
1097 let legend_height = self.calculate_vertical_grid_height(area.width);
1099 let is_top = matches!(self.legend_position, LegendPosition::Top);
1100 Self::layout_vertical_split(area, legend_height, is_top)
1101 }
1102 }
1103 }
1104
1105 fn calculate_vertical_grid_height(&self, available_width: u16) -> u16 {
1106 let max_item_width = self.calculate_legend_width();
1108 let columns = (available_width.saturating_sub(Self::LEGEND_PADDING * 2)
1109 / max_item_width.max(1))
1110 .clamp(1, 2);
1111
1112 #[allow(clippy::cast_possible_truncation)]
1113 let num_items = self.slices.len() as u16;
1114
1115 let rows = num_items.div_ceil(columns);
1117 (rows * 2 + Self::LEGEND_PADDING).clamp(4, Self::LEGEND_VERTICAL_MAX_HEIGHT)
1119 }
1120
1121 fn layout_horizontal_split(
1122 area: Rect,
1123 legend_width: u16,
1124 legend_on_left: bool,
1125 ) -> (Rect, Option<Rect>) {
1126 if area.width <= legend_width {
1127 return (area, None);
1128 }
1129
1130 let pie_width = area
1131 .width
1132 .saturating_sub(legend_width + Self::LEGEND_SPACING);
1133
1134 if legend_on_left {
1135 (
1136 Rect {
1137 x: area.x + legend_width + Self::LEGEND_SPACING,
1138 y: area.y,
1139 width: pie_width,
1140 height: area.height,
1141 },
1142 Some(Rect {
1143 x: area.x,
1144 y: area.y + Self::LEGEND_PADDING,
1145 width: legend_width,
1146 height: area.height.saturating_sub(Self::LEGEND_PADDING * 2),
1147 }),
1148 )
1149 } else {
1150 (
1151 Rect {
1152 x: area.x,
1153 y: area.y,
1154 width: pie_width,
1155 height: area.height,
1156 },
1157 Some(Rect {
1158 x: area.x + pie_width + Self::LEGEND_SPACING,
1159 y: area.y + Self::LEGEND_PADDING,
1160 width: legend_width,
1161 height: area.height.saturating_sub(Self::LEGEND_PADDING * 2),
1162 }),
1163 )
1164 }
1165 }
1166
1167 fn layout_vertical_split(
1168 area: Rect,
1169 legend_height: u16,
1170 legend_on_top: bool,
1171 ) -> (Rect, Option<Rect>) {
1172 if area.height <= legend_height {
1173 return (area, None);
1174 }
1175
1176 let pie_height = area
1177 .height
1178 .saturating_sub(legend_height + Self::LEGEND_SPACING);
1179
1180 if legend_on_top {
1181 (
1182 Rect {
1183 x: area.x,
1184 y: area.y + legend_height + Self::LEGEND_SPACING,
1185 width: area.width,
1186 height: pie_height,
1187 },
1188 Some(Rect {
1189 x: area.x + Self::LEGEND_PADDING,
1190 y: area.y + Self::LEGEND_PADDING,
1191 width: area.width.saturating_sub(Self::LEGEND_PADDING * 2),
1192 height: legend_height.saturating_sub(Self::LEGEND_PADDING),
1193 }),
1194 )
1195 } else {
1196 (
1197 Rect {
1198 x: area.x,
1199 y: area.y,
1200 width: area.width,
1201 height: pie_height,
1202 },
1203 Some(Rect {
1204 x: area.x + Self::LEGEND_PADDING,
1205 y: area.y + pie_height + Self::LEGEND_SPACING,
1206 width: area.width.saturating_sub(Self::LEGEND_PADDING * 2),
1207 height: legend_height.saturating_sub(Self::LEGEND_PADDING),
1208 }),
1209 )
1210 }
1211 }
1212
1213 fn calculate_legend_width(&self) -> u16 {
1214 let total = self.total_value();
1215 let widths = self.slices.iter().map(|s| self.legend_item_width(s, total));
1216
1217 let base = match self.legend_layout {
1218 LegendLayout::Vertical => widths.max().unwrap_or(0),
1220 LegendLayout::Horizontal => widths.fold(0u16, u16::saturating_add),
1222 };
1223
1224 base.saturating_add(2)
1225 }
1226
1227 fn calculate_legend_horizontal_width(&self) -> u16 {
1228 let total = self.total_value();
1229 self.slices
1230 .iter()
1231 .map(|s| self.legend_item_width(s, total))
1232 .fold(0u16, u16::saturating_add)
1233 .saturating_add(2)
1234 }
1235
1236 #[allow(clippy::similar_names)]
1237 fn render_piechart_braille(&self, area: Rect, buf: &mut Buffer) {
1238 let (pie_area, legend_area_opt) = self.calculate_layout(area);
1240
1241 let center_x_chars = pie_area.width / 2;
1243 let center_y_chars = pie_area.height / 2;
1244
1245 let center_x_dots = center_x_chars * 2;
1247 let center_y_dots = center_y_chars * 4;
1248
1249 let radius = (center_x_dots).min(center_y_dots).saturating_sub(2);
1255
1256 let width_dots = pie_area.width * 2;
1258 let height_dots = pie_area.height * 4;
1259
1260 let mut dot_slices: Vec<Vec<Option<usize>>> =
1261 vec![vec![None; width_dots as usize]; height_dots as usize];
1262
1263 let mut cumulative_percent = self.start * 100.0;
1265 for (slice_idx, slice) in self.slices.iter().enumerate() {
1266 let percent = self.percentage(slice);
1267 let (start_angle, end_angle, is_full_circle) =
1268 Self::slice_angles(cumulative_percent, percent);
1269
1270 for dy in 0..height_dots {
1271 for dx in 0..width_dots {
1272 let rel_x = f64::from(dx) - f64::from(center_x_dots);
1273 let rel_y = f64::from(dy) - f64::from(center_y_dots);
1274
1275 let distance = (rel_x * rel_x + rel_y * rel_y).sqrt();
1278
1279 if distance <= f64::from(radius) && distance >= self.hole * f64::from(radius) {
1280 let angle = rel_y.atan2(rel_x);
1281 if is_full_circle || Self::is_angle_in_slice(angle, start_angle, end_angle)
1282 {
1283 dot_slices[dy as usize][dx as usize] = Some(slice_idx);
1284 }
1285 }
1286 }
1287 }
1288
1289 cumulative_percent += percent;
1290 }
1291
1292 for char_y in 0..pie_area.height {
1294 for char_x in 0..pie_area.width {
1295 let base_dot_x = char_x * 2;
1296 let base_dot_y = char_y * 4;
1297
1298 let dot_positions = [
1305 (0, 0, 0x01), (0, 1, 0x02), (0, 2, 0x04), (1, 0, 0x08), (1, 1, 0x10), (1, 2, 0x20), (0, 3, 0x40), (1, 3, 0x80), ];
1314
1315 let mut pattern = 0u32;
1316 let mut slice_colors: Vec<(usize, u32)> = Vec::new();
1317
1318 for (dx, dy, bit) in dot_positions {
1319 let dot_x = base_dot_x + dx;
1320 let dot_y = base_dot_y + dy;
1321
1322 if dot_y < height_dots && dot_x < width_dots {
1323 if let Some(slice_idx) = dot_slices[dot_y as usize][dot_x as usize] {
1324 pattern |= bit;
1325 if let Some(entry) =
1327 slice_colors.iter_mut().find(|(idx, _)| *idx == slice_idx)
1328 {
1329 entry.1 += 1;
1330 } else {
1331 slice_colors.push((slice_idx, 1));
1332 }
1333 }
1334 }
1335 }
1336
1337 if pattern > 0 {
1338 if let Some((slice_idx, _)) = slice_colors.iter().max_by_key(|(_, count)| count)
1340 {
1341 let braille_char = char::from_u32(0x2800 + pattern).unwrap_or('⠀');
1342 let color = self.slices[*slice_idx].color;
1343
1344 let cell = &mut buf[(pie_area.x + char_x, pie_area.y + char_y)];
1345 cell.set_char(braille_char).set_fg(color);
1346 }
1347 }
1348 }
1349 }
1350
1351 self.draw_center(pie_area, buf);
1352
1353 if let Some(legend_area) = legend_area_opt {
1355 self.render_legend(buf, legend_area);
1356 }
1357 }
1358}
1359
1360#[cfg(test)]
1361#[allow(clippy::float_cmp)]
1362#[allow(unnameable_test_items)]
1363mod tests {
1364 use super::*;
1365
1366 #[test]
1367 fn pie_slice_new() {
1368 let slice = PieSlice::new("Test", 50.0, Color::Red);
1369 assert_eq!(slice.label(), "Test");
1370 assert_eq!(slice.value(), 50.0);
1371 assert_eq!(slice.color(), Color::Red);
1372 }
1373
1374 #[test]
1375 fn piechart_new() {
1376 let slices = vec![
1377 PieSlice::new("A", 30.0, Color::Red),
1378 PieSlice::new("B", 70.0, Color::Blue),
1379 ];
1380 let piechart = PieChart::new(slices.clone());
1381 assert_eq!(piechart.slices, slices);
1382 }
1383
1384 #[test]
1385 fn piechart_default() {
1386 let piechart = PieChart::default();
1387 assert_eq!(piechart.slices, [] as [PieSlice<'_>; 0]);
1388 assert!(piechart.show_legend);
1389 assert!(piechart.show_percentages);
1390 }
1391
1392 #[test]
1393 fn piechart_slices() {
1394 let slices = vec![PieSlice::new("Test", 100.0, Color::Green)];
1395 let piechart = PieChart::default().slices(slices.clone());
1396 assert_eq!(piechart.slices, slices);
1397 }
1398
1399 #[test]
1400 fn piechart_style() {
1401 let style = Style::default().fg(Color::Red);
1402 let piechart = PieChart::default().style(style);
1403 assert_eq!(piechart.style, style);
1404 }
1405
1406 #[test]
1407 fn piechart_show_legend() {
1408 let piechart = PieChart::default().show_legend(false);
1409 assert!(!piechart.show_legend);
1410 }
1411
1412 #[test]
1413 fn piechart_show_percentages() {
1414 let piechart = PieChart::default().show_percentages(false);
1415 assert!(!piechart.show_percentages);
1416 }
1417
1418 #[test]
1419 fn piechart_pie_char() {
1420 let piechart = PieChart::default().pie_char('█');
1421 assert_eq!(piechart.pie_char, '█');
1422 }
1423
1424 #[test]
1425 fn piechart_total_value() {
1426 let slices = vec![
1427 PieSlice::new("A", 30.0, Color::Red),
1428 PieSlice::new("B", 70.0, Color::Blue),
1429 ];
1430 let piechart = PieChart::new(slices);
1431 assert_eq!(piechart.total_value(), 100.0);
1432 }
1433
1434 #[test]
1435 fn piechart_percentage() {
1436 let slices = vec![
1437 PieSlice::new("A", 30.0, Color::Red),
1438 PieSlice::new("B", 70.0, Color::Blue),
1439 ];
1440 let piechart = PieChart::new(slices);
1441 assert_eq!(
1442 piechart.percentage(&PieSlice::new("A", 30.0, Color::Red)),
1443 30.0
1444 );
1445 }
1446
1447 render_empty_test!(piechart_render_empty_area, PieChart::default());
1449
1450 render_with_size_test!(
1451 piechart_render_with_block,
1452 {
1453 let slices = vec![PieSlice::new("Test", 100.0, Color::Red)];
1454 PieChart::new(slices).block(Block::bordered())
1455 },
1456 width: 20,
1457 height: 10
1458 );
1459
1460 render_test!(
1461 piechart_render_basic,
1462 {
1463 let slices = vec![
1464 PieSlice::new("Rust", 45.0, Color::Red),
1465 PieSlice::new("Go", 30.0, Color::Blue),
1466 PieSlice::new("Python", 25.0, Color::Green),
1467 ];
1468 PieChart::new(slices)
1469 },
1470 Rect::new(0, 0, 40, 20)
1471 );
1472
1473 #[test]
1474 fn piechart_styled_trait() {
1475 use ratatui::style::Stylize;
1476 let piechart = PieChart::default().red();
1477 assert_eq!(piechart.style.fg, Some(Color::Red));
1478 }
1479
1480 #[test]
1481 fn piechart_with_multiple_slices() {
1482 let slices = vec![
1483 PieSlice::new("A", 25.0, Color::Red),
1484 PieSlice::new("B", 25.0, Color::Blue),
1485 PieSlice::new("C", 25.0, Color::Green),
1486 PieSlice::new("D", 25.0, Color::Yellow),
1487 ];
1488 let piechart = PieChart::new(slices);
1489 assert_eq!(piechart.total_value(), 100.0);
1490 }
1491
1492 render_with_size_test!(
1494 piechart_multi_slice_render,
1495 {
1496 let slices = vec![
1497 PieSlice::new("A", 25.0, Color::Red),
1498 PieSlice::new("B", 25.0, Color::Blue),
1499 PieSlice::new("C", 25.0, Color::Green),
1500 PieSlice::new("D", 25.0, Color::Yellow),
1501 ];
1502 PieChart::new(slices)
1503 },
1504 width: 50,
1505 height: 30
1506 );
1507
1508 #[test]
1509 fn piechart_zero_values() {
1510 let slices = vec![
1511 PieSlice::new("A", 0.0, Color::Red),
1512 PieSlice::new("B", 0.0, Color::Blue),
1513 ];
1514 let piechart = PieChart::new(slices);
1515 assert_eq!(piechart.total_value(), 0.0);
1516 }
1517
1518 #[test]
1519 fn piechart_method_chaining() {
1520 use ratatui::widgets::Block;
1521
1522 let slices = vec![PieSlice::new("Test", 100.0, Color::Red)];
1523 let piechart = PieChart::new(slices)
1524 .show_legend(true)
1525 .show_percentages(true)
1526 .pie_char('█')
1527 .block(Block::bordered().title("Test"))
1528 .style(Style::default().fg(Color::White));
1529
1530 assert!(piechart.show_legend);
1531 assert!(piechart.show_percentages);
1532 assert_eq!(piechart.pie_char, '█');
1533 assert!(piechart.block.is_some());
1534 assert_eq!(piechart.style.fg, Some(Color::White));
1535 }
1536
1537 #[test]
1538 fn piechart_custom_symbols() {
1539 use crate::symbols;
1540
1541 let piechart = PieChart::default().pie_char(symbols::PIE_CHAR_BLOCK);
1542 assert_eq!(piechart.pie_char, '█');
1543
1544 let piechart = PieChart::default().pie_char(symbols::PIE_CHAR_CIRCLE);
1545 assert_eq!(piechart.pie_char, '◉');
1546
1547 let piechart = PieChart::default().pie_char(symbols::PIE_CHAR_SQUARE);
1548 assert_eq!(piechart.pie_char, '■');
1549 }
1550
1551 #[test]
1552 fn piechart_is_angle_in_slice() {
1553 use std::f64::consts::PI;
1554
1555 assert!(PieChart::is_angle_in_slice(PI / 4.0, 0.0, PI / 2.0));
1557
1558 assert!(!PieChart::is_angle_in_slice(PI, 0.0, PI / 2.0));
1560
1561 assert!(PieChart::is_angle_in_slice(0.1, 1.5 * PI, 0.5));
1563 }
1564
1565 matches_test!(
1568 piechart_resolution_standard,
1569 PieChart::default()
1570 .resolution(Resolution::Standard)
1571 .resolution,
1572 Resolution::Standard
1573 );
1574 matches_test!(
1575 piechart_resolution_braille,
1576 PieChart::default()
1577 .resolution(Resolution::Braille)
1578 .resolution,
1579 Resolution::Braille
1580 );
1581 matches_test!(
1582 piechart_high_resolution_true,
1583 PieChart::default().high_resolution(true).resolution,
1584 Resolution::Braille
1585 );
1586 matches_test!(
1587 piechart_high_resolution_false,
1588 PieChart::default().high_resolution(false).resolution,
1589 Resolution::Standard
1590 );
1591
1592 matches_test!(
1595 piechart_legend_position_left,
1596 PieChart::default()
1597 .legend_position(LegendPosition::Left)
1598 .legend_position,
1599 LegendPosition::Left
1600 );
1601 matches_test!(
1602 piechart_legend_position_right,
1603 PieChart::default()
1604 .legend_position(LegendPosition::Right)
1605 .legend_position,
1606 LegendPosition::Right
1607 );
1608 matches_test!(
1609 piechart_legend_position_top,
1610 PieChart::default()
1611 .legend_position(LegendPosition::Top)
1612 .legend_position,
1613 LegendPosition::Top
1614 );
1615 matches_test!(
1616 piechart_legend_position_bottom,
1617 PieChart::default()
1618 .legend_position(LegendPosition::Bottom)
1619 .legend_position,
1620 LegendPosition::Bottom
1621 );
1622 matches_test!(
1623 piechart_legend_layout_horizontal,
1624 PieChart::default()
1625 .legend_layout(LegendLayout::Horizontal)
1626 .legend_layout,
1627 LegendLayout::Horizontal
1628 );
1629 matches_test!(
1630 piechart_legend_layout_vertical,
1631 PieChart::default()
1632 .legend_layout(LegendLayout::Vertical)
1633 .legend_layout,
1634 LegendLayout::Vertical
1635 );
1636 matches_test!(
1637 piechart_legend_alignment_left,
1638 PieChart::default()
1639 .legend_alignment(LegendAlignment::Left)
1640 .legend_alignment,
1641 LegendAlignment::Left
1642 );
1643 matches_test!(
1644 piechart_legend_alignment_center,
1645 PieChart::default()
1646 .legend_alignment(LegendAlignment::Center)
1647 .legend_alignment,
1648 LegendAlignment::Center
1649 );
1650 matches_test!(
1651 piechart_legend_alignment_right,
1652 PieChart::default()
1653 .legend_alignment(LegendAlignment::Right)
1654 .legend_alignment,
1655 LegendAlignment::Right
1656 );
1657
1658 #[test]
1661 fn piechart_legend_marker_custom() {
1662 use crate::symbols::LEGEND_MARKER;
1663 let piechart = PieChart::default().legend_marker(LEGEND_MARKER);
1664 assert_eq!(piechart.legend_marker, LEGEND_MARKER);
1665 }
1666
1667 #[test]
1670 fn piechart_format_legend_text_with_percentage() {
1671 let slices = vec![
1672 PieSlice::new("Rust", 50.0, Color::Red),
1673 PieSlice::new("Go", 50.0, Color::Blue),
1674 ];
1675 let piechart = PieChart::new(slices.clone()).show_percentages(true);
1676 let text = piechart.format_legend_text(&slices[0], 100.0, "");
1677 assert!(text.contains("Rust"));
1678 assert!(text.contains("50.0%"));
1679 }
1680
1681 #[test]
1682 fn piechart_format_legend_text_without_percentage() {
1683 let slices = vec![PieSlice::new("Rust", 50.0, Color::Red)];
1684 let piechart = PieChart::new(slices.clone()).show_percentages(false);
1685 let text = piechart.format_legend_text(&slices[0], 100.0, "");
1686 assert!(text.contains("Rust"));
1687 assert!(!text.contains('%'));
1688 }
1689
1690 #[test]
1691 fn piechart_format_legend_text_zero_total() {
1692 let slices = vec![PieSlice::new("X", 0.0, Color::Red)];
1693 let piechart = PieChart::new(slices.clone()).show_percentages(true);
1694 let text = piechart.format_legend_text(&slices[0], 0.0, "");
1695 assert!(text.contains("0.0%"));
1696 }
1697
1698 #[test]
1701 fn piechart_calculate_aligned_x_left() {
1702 let slices = vec![PieSlice::new("A", 100.0, Color::Red)];
1703 let piechart = PieChart::new(slices).legend_alignment(LegendAlignment::Left);
1704 let area = Rect::new(5, 0, 20, 10);
1705 assert_eq!(piechart.calculate_aligned_x(area, 10), 5);
1706 }
1707
1708 #[test]
1709 fn piechart_calculate_aligned_x_center() {
1710 let slices = vec![PieSlice::new("A", 100.0, Color::Red)];
1711 let piechart = PieChart::new(slices).legend_alignment(LegendAlignment::Center);
1712 let area = Rect::new(0, 0, 20, 10);
1713 assert_eq!(piechart.calculate_aligned_x(area, 10), 5);
1715 }
1716
1717 #[test]
1718 fn piechart_calculate_aligned_x_right() {
1719 let slices = vec![PieSlice::new("A", 100.0, Color::Red)];
1720 let piechart = PieChart::new(slices).legend_alignment(LegendAlignment::Right);
1721 let area = Rect::new(0, 0, 20, 10);
1722 assert_eq!(piechart.calculate_aligned_x(area, 10), 10);
1724 }
1725
1726 render_test!(
1729 piechart_render_braille,
1730 {
1731 let slices = vec![
1732 PieSlice::new("Rust", 60.0, Color::Red),
1733 PieSlice::new("Go", 40.0, Color::Blue),
1734 ];
1735 PieChart::new(slices).resolution(Resolution::Braille)
1736 },
1737 Rect::new(0, 0, 40, 20)
1738 );
1739
1740 render_test!(
1741 piechart_render_braille_with_legend,
1742 {
1743 let slices = vec![
1744 PieSlice::new("Rust", 60.0, Color::Red),
1745 PieSlice::new("Go", 40.0, Color::Blue),
1746 ];
1747 PieChart::new(slices)
1748 .resolution(Resolution::Braille)
1749 .show_legend(true)
1750 },
1751 Rect::new(0, 0, 40, 20)
1752 );
1753
1754 render_test!(
1757 piechart_render_legend_left,
1758 {
1759 let slices = vec![
1760 PieSlice::new("Alpha", 50.0, Color::Red),
1761 PieSlice::new("Beta", 50.0, Color::Blue),
1762 ];
1763 PieChart::new(slices)
1764 .show_legend(true)
1765 .legend_position(LegendPosition::Left)
1766 .legend_layout(LegendLayout::Vertical)
1767 },
1768 Rect::new(0, 0, 60, 20)
1769 );
1770
1771 render_test!(
1772 piechart_render_legend_right,
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::Right)
1781 .legend_layout(LegendLayout::Vertical)
1782 },
1783 Rect::new(0, 0, 60, 20)
1784 );
1785
1786 render_test!(
1787 piechart_render_legend_top_horizontal,
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::Top)
1796 .legend_layout(LegendLayout::Horizontal)
1797 },
1798 Rect::new(0, 0, 60, 20)
1799 );
1800
1801 render_test!(
1802 piechart_render_legend_bottom_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::Bottom)
1811 .legend_layout(LegendLayout::Horizontal)
1812 },
1813 Rect::new(0, 0, 60, 20)
1814 );
1815
1816 render_test!(
1817 piechart_render_legend_top_vertical,
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::Top)
1826 .legend_layout(LegendLayout::Vertical)
1827 },
1828 Rect::new(0, 0, 60, 20)
1829 );
1830
1831 render_test!(
1832 piechart_render_legend_bottom_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::Bottom)
1841 .legend_layout(LegendLayout::Vertical)
1842 },
1843 Rect::new(0, 0, 60, 20)
1844 );
1845
1846 render_test!(
1847 piechart_render_legend_left_horizontal,
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::Left)
1856 .legend_layout(LegendLayout::Horizontal)
1857 },
1858 Rect::new(0, 0, 60, 20)
1859 );
1860
1861 render_test!(
1862 piechart_render_legend_right_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::Right)
1871 .legend_layout(LegendLayout::Horizontal)
1872 },
1873 Rect::new(0, 0, 60, 20)
1874 );
1875
1876 render_test!(
1879 piechart_render_legend_with_percentages,
1880 {
1881 let slices = vec![
1882 PieSlice::new("Rust", 45.0, Color::Red),
1883 PieSlice::new("Go", 30.0, Color::Blue),
1884 PieSlice::new("Python", 25.0, Color::Green),
1885 ];
1886 PieChart::new(slices)
1887 .show_legend(true)
1888 .show_percentages(true)
1889 },
1890 Rect::new(0, 0, 60, 20)
1891 );
1892
1893 render_test!(
1895 piechart_render_legend_alignment_center,
1896 {
1897 let slices = vec![
1898 PieSlice::new("A", 50.0, Color::Red),
1899 PieSlice::new("B", 50.0, Color::Blue),
1900 ];
1901 PieChart::new(slices)
1902 .show_legend(true)
1903 .legend_alignment(LegendAlignment::Center)
1904 },
1905 Rect::new(0, 0, 60, 20)
1906 );
1907
1908 render_test!(
1909 piechart_render_legend_alignment_right,
1910 {
1911 let slices = vec![
1912 PieSlice::new("A", 50.0, Color::Red),
1913 PieSlice::new("B", 50.0, Color::Blue),
1914 ];
1915 PieChart::new(slices)
1916 .show_legend(true)
1917 .legend_alignment(LegendAlignment::Right)
1918 },
1919 Rect::new(0, 0, 60, 20)
1920 );
1921
1922 #[test]
1925 fn piechart_layout_too_small_no_legend() {
1926 let slices = vec![PieSlice::new("A", 100.0, Color::Red)];
1927 let piechart = PieChart::new(slices).show_legend(true);
1928 let area = Rect::new(0, 0, 10, 5);
1930 let (pie_area, legend_opt) = piechart.calculate_layout(area);
1931 assert_eq!(pie_area, area);
1932 assert!(legend_opt.is_none());
1933 }
1934
1935 #[test]
1936 fn piechart_layout_show_legend_false_no_legend() {
1937 let slices = vec![PieSlice::new("A", 100.0, Color::Red)];
1938 let piechart = PieChart::new(slices).show_legend(false);
1939 let area = Rect::new(0, 0, 60, 20);
1940 let (pie_area, legend_opt) = piechart.calculate_layout(area);
1941 assert_eq!(pie_area, area);
1942 assert!(legend_opt.is_none());
1943 }
1944
1945 render_empty_test!(piechart_render_empty_slices, PieChart::default());
1948
1949 render_test!(
1952 piechart_render_single_slice,
1953 PieChart::new(vec![PieSlice::new("Only", 100.0, Color::Cyan)]),
1954 Rect::new(0, 0, 30, 15)
1955 );
1956
1957 #[test]
1960 fn piechart_percentage_zero_total() {
1961 let slices = vec![PieSlice::new("A", 0.0, Color::Red)];
1962 let piechart = PieChart::new(slices.clone());
1963 assert_eq!(piechart.percentage(&slices[0]), 0.0);
1964 }
1965
1966 #[test]
1971 fn piechart_calculate_legend_width_vertical_zero_total_with_percentages() {
1972 let slices = vec![
1973 PieSlice::new("A", 0.0, Color::Red),
1974 PieSlice::new("B", 0.0, Color::Blue),
1975 ];
1976 let piechart = PieChart::new(slices)
1977 .legend_layout(LegendLayout::Vertical)
1978 .show_percentages(true);
1979 let width = piechart.calculate_legend_width();
1980 assert!(width > 0);
1982 }
1983
1984 render_test!(
1985 piechart_render_legend_vertical_zero_total_with_percentages,
1986 PieChart::new(vec![
1987 PieSlice::new("A", 0.0, Color::Red),
1988 PieSlice::new("B", 0.0, Color::Blue),
1989 ])
1990 .show_legend(true)
1991 .show_percentages(true)
1992 .legend_position(LegendPosition::Right)
1993 .legend_layout(LegendLayout::Vertical),
1994 Rect::new(0, 0, 60, 20)
1995 );
1996
1997 render_test!(
2000 piechart_render_legend_vertical_no_percentages,
2001 PieChart::new(vec![
2002 PieSlice::new("Rust", 60.0, Color::Red),
2003 PieSlice::new("Go", 40.0, Color::Blue),
2004 ])
2005 .show_legend(true)
2006 .show_percentages(false)
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_all_zero_values,
2016 PieChart::new(vec![
2017 PieSlice::new("A", 0.0, Color::Red),
2018 PieSlice::new("B", 0.0, Color::Blue),
2019 ]),
2020 Rect::new(0, 0, 40, 20)
2021 );
2022
2023 render_test!(
2026 piechart_render_zero_value_slice_in_mix,
2027 PieChart::new(vec![
2028 PieSlice::new("A", 100.0, Color::Red),
2029 PieSlice::new("B", 0.0, Color::Blue),
2030 ]),
2031 Rect::new(0, 0, 40, 20)
2032 );
2033
2034 render_test!(
2037 piechart_render_tiny_area_radius_zero,
2038 PieChart::new(vec![PieSlice::new("A", 100.0, Color::Red)]),
2039 Rect::new(0, 0, 1, 1)
2040 );
2041
2042 render_test!(
2046 piechart_render_vertical_legend_overflow,
2047 PieChart::new(vec![
2048 PieSlice::new("Slice1", 10.0, Color::Red),
2049 PieSlice::new("Slice2", 10.0, Color::Blue),
2050 PieSlice::new("Slice3", 10.0, Color::Green),
2051 PieSlice::new("Slice4", 10.0, Color::Yellow),
2052 PieSlice::new("Slice5", 10.0, Color::Cyan),
2053 PieSlice::new("Slice6", 10.0, Color::Magenta),
2054 PieSlice::new("Slice7", 10.0, Color::White),
2055 PieSlice::new("Slice8", 10.0, Color::Red),
2056 PieSlice::new("Slice9", 10.0, Color::Blue),
2057 PieSlice::new("Slice10", 10.0, Color::Green),
2058 ])
2059 .show_legend(true)
2060 .legend_position(LegendPosition::Right)
2061 .legend_layout(LegendLayout::Vertical),
2062 Rect::new(0, 0, 60, 12)
2064 );
2065
2066 render_test!(
2070 piechart_render_horizontal_legend_overflow,
2071 PieChart::new(vec![
2072 PieSlice::new("LongLabelA", 20.0, Color::Red),
2073 PieSlice::new("LongLabelB", 20.0, Color::Blue),
2074 PieSlice::new("LongLabelC", 20.0, Color::Green),
2075 PieSlice::new("LongLabelD", 20.0, Color::Yellow),
2076 PieSlice::new("LongLabelE", 20.0, Color::Cyan),
2077 ])
2078 .show_legend(true)
2079 .legend_position(LegendPosition::Bottom)
2080 .legend_layout(LegendLayout::Horizontal),
2081 Rect::new(0, 0, 22, 15)
2083 );
2084
2085 #[test]
2088 fn piechart_layout_horizontal_split_too_narrow() {
2089 let area = Rect::new(0, 0, 5, 20);
2090 let (pie_area, legend_opt) = PieChart::<'_>::layout_horizontal_split(area, 100, true);
2092 assert_eq!(pie_area, area);
2093 assert!(legend_opt.is_none());
2094 }
2095
2096 #[test]
2099 fn piechart_layout_vertical_split_too_short() {
2100 let area = Rect::new(0, 0, 60, 3);
2101 let (pie_area, legend_opt) = PieChart::<'_>::layout_vertical_split(area, 100, true);
2103 assert_eq!(pie_area, area);
2104 assert!(legend_opt.is_none());
2105 }
2106
2107 #[test]
2111 fn piechart_calculate_legend_width_horizontal_with_percentages() {
2112 let slices = vec![
2113 PieSlice::new("Rust", 60.0, Color::Red),
2114 PieSlice::new("Go", 40.0, Color::Blue),
2115 ];
2116 let piechart = PieChart::new(slices)
2117 .legend_layout(LegendLayout::Horizontal)
2118 .show_percentages(true);
2119 let width = piechart.calculate_legend_width();
2120 assert!(width > 0);
2121 }
2122
2123 #[test]
2124 fn piechart_calculate_legend_width_horizontal_without_percentages() {
2125 let slices = vec![
2126 PieSlice::new("Alpha", 50.0, Color::Red),
2127 PieSlice::new("Beta", 50.0, Color::Blue),
2128 ];
2129 let piechart = PieChart::new(slices)
2130 .legend_layout(LegendLayout::Horizontal)
2131 .show_percentages(false);
2132 let width = piechart.calculate_legend_width();
2133 assert!(width > 0);
2134 }
2135
2136 #[test]
2137 fn piechart_calculate_legend_width_horizontal_zero_total() {
2138 let slices = vec![
2139 PieSlice::new("A", 0.0, Color::Red),
2140 PieSlice::new("B", 0.0, Color::Blue),
2141 ];
2142 let piechart = PieChart::new(slices)
2143 .legend_layout(LegendLayout::Horizontal)
2144 .show_percentages(true);
2145 let width = piechart.calculate_legend_width();
2146 assert!(width > 0);
2148 }
2149
2150 #[test]
2153 fn piechart_calculate_legend_horizontal_width_zero_total() {
2154 let slices = vec![
2155 PieSlice::new("X", 0.0, Color::Red),
2156 PieSlice::new("Y", 0.0, Color::Blue),
2157 ];
2158 let piechart = PieChart::new(slices).show_percentages(true);
2159 let width = piechart.calculate_legend_horizontal_width();
2160 assert!(width > 0);
2161 }
2162
2163 #[test]
2164 fn piechart_calculate_legend_horizontal_width_without_percentages() {
2165 let slices = vec![PieSlice::new("Item", 100.0, Color::Green)];
2166 let piechart = PieChart::new(slices).show_percentages(false);
2167 let width = piechart.calculate_legend_horizontal_width();
2168 assert!(width > 0);
2169 }
2170
2171 #[test]
2174 fn piechart_calculate_vertical_grid_height_narrow() {
2175 let slices = vec![
2176 PieSlice::new("A", 50.0, Color::Red),
2177 PieSlice::new("B", 50.0, Color::Blue),
2178 ];
2179 let piechart = PieChart::new(slices);
2180 let height = piechart.calculate_vertical_grid_height(1);
2182 assert!(height >= 4);
2183 }
2184
2185 #[test]
2188 fn piechart_value_percent_positive_total() {
2189 assert_eq!(PieChart::value_percent(25.0, 200.0), 12.5);
2190 }
2191
2192 #[test]
2193 fn piechart_value_percent_zero_total() {
2194 assert_eq!(PieChart::value_percent(25.0, 0.0), 0.0);
2195 }
2196
2197 #[test]
2198 fn piechart_value_percent_negative_total() {
2199 assert_eq!(PieChart::value_percent(25.0, -5.0), 0.0);
2201 }
2202
2203 #[test]
2204 fn piechart_slice_angles_full_circle_flag() {
2205 let (_, _, is_full) = PieChart::slice_angles(0.0, 100.0);
2206 assert!(is_full);
2207 }
2208
2209 #[test]
2210 fn piechart_slice_angles_partial_not_full() {
2211 let (start, end, is_full) = PieChart::slice_angles(0.0, 50.0);
2212 assert!(!is_full);
2213 assert!((end - start - PI).abs() < 1e-9);
2214 }
2215
2216 #[test]
2217 fn piechart_legend_item_width_matches_text() {
2218 let slices = vec![PieSlice::new("Rust", 50.0, Color::Red)];
2219 let chart = PieChart::new(slices.clone()).show_percentages(true);
2220 let expected = chart
2221 .format_legend_text(&slices[0], 100.0, " ")
2222 .chars()
2223 .count();
2224 assert_eq!(
2225 usize::from(chart.legend_item_width(&slices[0], 100.0)),
2226 expected
2227 );
2228 }
2229
2230 #[test]
2231 fn piechart_legend_item_width_unicode_label() {
2232 let slices = vec![PieSlice::new("日本語", 100.0, Color::Red)];
2234 let chart = PieChart::new(slices.clone()).show_percentages(false);
2235 assert_eq!(chart.legend_item_width(&slices[0], 100.0), 7);
2237 }
2238
2239 #[test]
2242 fn piechart_full_circle_fills_more_than_a_line() {
2243 fn filled_cells(chart: &PieChart) -> usize {
2244 let mut buffer = Buffer::empty(Rect::new(0, 0, 30, 15));
2245 Widget::render(chart, buffer.area, &mut buffer);
2246 buffer
2247 .content
2248 .iter()
2249 .filter(|c| !c.symbol().trim().is_empty())
2250 .count()
2251 }
2252
2253 let standard = PieChart::new(vec![PieSlice::new("Only", 100.0, Color::Green)])
2254 .show_legend(false)
2255 .show_percentages(false);
2256 assert!(filled_cells(&standard) > 30);
2257
2258 let braille = PieChart::new(vec![PieSlice::new("Only", 100.0, Color::Green)])
2259 .resolution(Resolution::Braille)
2260 .show_legend(false)
2261 .show_percentages(false);
2262 assert!(filled_cells(&braille) > 30);
2263 }
2264}