tui_piechart/lib.rs
1//! # tui-piechart
2//!
3//! A customizable pie chart widget for [Ratatui](https://github.com/ratatui/ratatui) TUI applications.
4//!
5//! ## Features
6//!
7//! - 🥧 Simple pie chart with customizable slices
8//! - 🎨 Customizable colors for each slice
9//! - 🔤 Labels and percentages
10//! - 📊 Legend support
11//! - 📦 Optional block wrapper
12//! - ✨ Custom symbols for pie chart and legend
13//! - ⚡ Zero-cost abstractions
14//!
15//! ## Examples
16//!
17//! Basic usage:
18//!
19//! ```no_run
20//! use ratatui::style::Color;
21//! use tui_piechart::{PieChart, PieSlice};
22//!
23//! let slices = vec![
24//! PieSlice::new("Rust", 45.0, Color::Red),
25//! PieSlice::new("Go", 30.0, Color::Blue),
26//! PieSlice::new("Python", 25.0, Color::Green),
27//! ];
28//! let piechart = PieChart::new(slices);
29//! ```
30//!
31//! With custom styling:
32//!
33//! ```no_run
34//! use ratatui::style::{Color, Style};
35//! use tui_piechart::{PieChart, PieSlice};
36//!
37//! let slices = vec![
38//! PieSlice::new("Rust", 45.0, Color::Red),
39//! PieSlice::new("Go", 30.0, Color::Blue),
40//! ];
41//! let piechart = PieChart::new(slices)
42//! .style(Style::default())
43//! .show_legend(true)
44//! .show_percentages(true);
45//! ```
46//!
47//! With custom symbols:
48//!
49//! ```no_run
50//! use ratatui::style::Color;
51//! use tui_piechart::{PieChart, PieSlice, symbols};
52//!
53//! let slices = vec![
54//! PieSlice::new("Rust", 45.0, Color::Red),
55//! PieSlice::new("Go", 30.0, Color::Blue),
56//! ];
57//!
58//! // Use predefined symbols
59//! let piechart = PieChart::new(slices.clone())
60//! .pie_char(symbols::PIE_CHAR_BLOCK)
61//! .legend_marker(symbols::LEGEND_MARKER_CIRCLE);
62//!
63//! // Or use any custom characters
64//! let piechart = PieChart::new(slices)
65//! .pie_char('█')
66//! .legend_marker("→");
67//! ```
68
69#![warn(missing_docs)]
70#![warn(clippy::pedantic)]
71#![allow(clippy::module_name_repetitions)]
72
73use std::f64::consts::PI;
74
75use ratatui::buffer::Buffer;
76use ratatui::layout::Rect;
77use ratatui::style::{Color, Style, Styled};
78use ratatui::text::{Line, Span};
79use ratatui::widgets::{Block, Widget};
80
81pub mod symbols;
82
83/// Rendering resolution mode for pie charts.
84///
85/// Different resolution modes provide varying levels of detail by using
86/// different Unicode block drawing characters with different dot densities.
87///
88/// # Examples
89///
90/// ```
91/// use tui_piechart::{PieChart, PieSlice, Resolution};
92/// use ratatui::style::Color;
93///
94/// let slices = vec![PieSlice::new("Rust", 45.0, Color::Red)];
95///
96/// // Standard resolution (1 dot per character)
97/// let standard = PieChart::new(slices.clone())
98/// .resolution(Resolution::Standard);
99///
100/// // High resolution with braille patterns (8 dots per character)
101/// let braille = PieChart::new(slices)
102/// .resolution(Resolution::Braille);
103/// ```
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
105pub enum Resolution {
106 /// Standard resolution using full characters (1 dot per cell).
107 ///
108 /// Uses regular Unicode characters like `●`. This is the default mode.
109 #[default]
110 Standard,
111
112 /// Braille resolution using 2×4 dot patterns (8 dots per cell).
113 ///
114 /// Uses Unicode braille patterns (U+2800-U+28FF) providing 8x resolution.
115 /// This provides the highest resolution available for terminal rendering.
116 Braille,
117}
118
119/// A slice of the pie chart representing a portion of data.
120///
121/// Each slice has a label, a value, and a color.
122///
123/// # Examples
124///
125/// ```
126/// use ratatui::style::Color;
127/// use tui_piechart::PieSlice;
128///
129/// let slice = PieSlice::new("Rust", 45.0, Color::Red);
130/// ```
131#[derive(Debug, Clone, PartialEq)]
132pub struct PieSlice<'a> {
133 /// The label for this slice
134 label: &'a str,
135 /// The value of this slice (will be converted to percentage)
136 value: f64,
137 /// The color of this slice
138 color: Color,
139}
140
141impl<'a> PieSlice<'a> {
142 /// Creates a new pie slice with the given label, value, and color.
143 ///
144 /// # Examples
145 ///
146 /// ```
147 /// use ratatui::style::Color;
148 /// use tui_piechart::PieSlice;
149 ///
150 /// let slice = PieSlice::new("Rust", 45.0, Color::Red);
151 /// ```
152 #[must_use]
153 pub const fn new(label: &'a str, value: f64, color: Color) -> Self {
154 Self {
155 label,
156 value,
157 color,
158 }
159 }
160
161 /// Returns the label of this slice.
162 #[must_use]
163 pub const fn label(&self) -> &'a str {
164 self.label
165 }
166
167 /// Returns the value of this slice.
168 #[must_use]
169 pub const fn value(&self) -> f64 {
170 self.value
171 }
172
173 /// Returns the color of this slice.
174 #[must_use]
175 pub const fn color(&self) -> Color {
176 self.color
177 }
178}
179
180/// A widget that displays a pie chart.
181///
182/// A `PieChart` displays data as slices of a circle, where each slice represents
183/// a proportion of the total.
184///
185/// # Examples
186///
187/// ```
188/// use ratatui::style::Color;
189/// use tui_piechart::{PieChart, PieSlice};
190///
191/// let slices = vec![
192/// PieSlice::new("Rust", 45.0, Color::Red),
193/// PieSlice::new("Go", 30.0, Color::Blue),
194/// PieSlice::new("Python", 25.0, Color::Green),
195/// ];
196/// let piechart = PieChart::new(slices);
197/// ```
198#[derive(Debug, Clone, PartialEq)]
199pub struct PieChart<'a> {
200 /// The slices of the pie chart
201 slices: Vec<PieSlice<'a>>,
202 /// Optional block to wrap the pie chart
203 block: Option<Block<'a>>,
204 /// Base style for the entire widget
205 style: Style,
206 /// Whether to show the legend
207 show_legend: bool,
208 /// Whether to show percentages on slices
209 show_percentages: bool,
210 /// The character to use for drawing the pie chart
211 pie_char: char,
212 /// The marker to use for legend items
213 legend_marker: &'a str,
214 /// Resolution mode for rendering
215 resolution: Resolution,
216}
217
218impl Default for PieChart<'_> {
219 /// Returns a default `PieChart` widget.
220 ///
221 /// The default widget has:
222 /// - No slices
223 /// - No block
224 /// - Default style
225 /// - Legend shown
226 /// - Percentages shown
227 /// - Default pie character (●)
228 /// - Default legend marker (■)
229 fn default() -> Self {
230 Self {
231 slices: Vec::new(),
232 block: None,
233 style: Style::default(),
234 show_legend: true,
235 show_percentages: true,
236 pie_char: symbols::PIE_CHAR,
237 legend_marker: symbols::LEGEND_MARKER,
238 resolution: Resolution::default(),
239 }
240 }
241}
242
243impl<'a> PieChart<'a> {
244 /// Creates a new `PieChart` with the given slices.
245 ///
246 /// # Examples
247 ///
248 /// ```
249 /// use ratatui::style::Color;
250 /// use tui_piechart::{PieChart, PieSlice};
251 ///
252 /// let slices = vec![
253 /// PieSlice::new("Rust", 45.0, Color::Red),
254 /// PieSlice::new("Go", 30.0, Color::Blue),
255 /// ];
256 /// let piechart = PieChart::new(slices);
257 /// ```
258 #[must_use]
259 pub fn new(slices: Vec<PieSlice<'a>>) -> Self {
260 Self {
261 slices,
262 ..Default::default()
263 }
264 }
265
266 /// Sets the slices of the pie chart.
267 ///
268 /// # Examples
269 ///
270 /// ```
271 /// use ratatui::style::Color;
272 /// use tui_piechart::{PieChart, PieSlice};
273 ///
274 /// let slices = vec![
275 /// PieSlice::new("Rust", 45.0, Color::Red),
276 /// ];
277 /// let piechart = PieChart::default().slices(slices);
278 /// ```
279 #[must_use]
280 pub fn slices(mut self, slices: Vec<PieSlice<'a>>) -> Self {
281 self.slices = slices;
282 self
283 }
284
285 /// Wraps the pie chart with the given block.
286 ///
287 /// # Examples
288 ///
289 /// ```
290 /// use ratatui::style::Color;
291 /// use ratatui::widgets::Block;
292 /// use tui_piechart::{PieChart, PieSlice};
293 ///
294 /// let slices = vec![PieSlice::new("Rust", 45.0, Color::Red)];
295 /// let piechart = PieChart::new(slices)
296 /// .block(Block::bordered().title("Statistics"));
297 /// ```
298 #[must_use]
299 pub fn block(mut self, block: Block<'a>) -> Self {
300 self.block = Some(block);
301 self
302 }
303
304 /// Sets the base style of the widget.
305 ///
306 /// # Examples
307 ///
308 /// ```
309 /// use ratatui::style::{Color, Style};
310 /// use tui_piechart::PieChart;
311 ///
312 /// let piechart = PieChart::default()
313 /// .style(Style::default().fg(Color::White));
314 /// ```
315 #[must_use]
316 pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
317 self.style = style.into();
318 self
319 }
320
321 /// Sets whether to show the legend.
322 ///
323 /// # Examples
324 ///
325 /// ```
326 /// use tui_piechart::PieChart;
327 ///
328 /// let piechart = PieChart::default().show_legend(true);
329 /// ```
330 #[must_use]
331 pub const fn show_legend(mut self, show: bool) -> Self {
332 self.show_legend = show;
333 self
334 }
335
336 /// Sets whether to show percentages on slices.
337 ///
338 /// # Examples
339 ///
340 /// ```
341 /// use tui_piechart::PieChart;
342 ///
343 /// let piechart = PieChart::default().show_percentages(true);
344 /// ```
345 #[must_use]
346 pub const fn show_percentages(mut self, show: bool) -> Self {
347 self.show_percentages = show;
348 self
349 }
350
351 /// Sets the character used to draw the pie chart.
352 ///
353 /// You can use any Unicode character for custom visualization.
354 ///
355 /// # Examples
356 ///
357 /// Using a predefined symbol:
358 ///
359 /// ```
360 /// use tui_piechart::{PieChart, symbols};
361 ///
362 /// let piechart = PieChart::default()
363 /// .pie_char(symbols::PIE_CHAR_BLOCK);
364 /// ```
365 ///
366 /// Using a custom character:
367 ///
368 /// ```
369 /// use tui_piechart::PieChart;
370 ///
371 /// let piechart = PieChart::default().pie_char('█');
372 /// ```
373 #[must_use]
374 pub const fn pie_char(mut self, c: char) -> Self {
375 self.pie_char = c;
376 self
377 }
378
379 /// Sets the marker used for legend items.
380 ///
381 /// You can use any string (including Unicode characters) for custom markers.
382 ///
383 /// # Examples
384 ///
385 /// Using a predefined symbol:
386 ///
387 /// ```
388 /// use tui_piechart::{PieChart, symbols};
389 ///
390 /// let piechart = PieChart::default()
391 /// .legend_marker(symbols::LEGEND_MARKER_CIRCLE);
392 /// ```
393 ///
394 /// Using custom markers:
395 ///
396 /// ```
397 /// use tui_piechart::PieChart;
398 ///
399 /// // Simple arrow
400 /// let piechart = PieChart::default().legend_marker("→");
401 ///
402 /// // Or any Unicode character
403 /// let piechart = PieChart::default().legend_marker("★");
404 ///
405 /// // Or even multi-character strings
406 /// let piechart = PieChart::default().legend_marker("-->");
407 /// ```
408 #[must_use]
409 pub const fn legend_marker(mut self, marker: &'a str) -> Self {
410 self.legend_marker = marker;
411 self
412 }
413
414 /// Sets the rendering resolution mode.
415 ///
416 /// Different resolution modes provide varying levels of detail:
417 /// - `Standard`: Regular characters (1 dot per cell)
418 /// - `Braille`: 2×4 patterns (8 dots per cell, 8x resolution)
419 ///
420 /// # Examples
421 ///
422 /// ```
423 /// use tui_piechart::{PieChart, Resolution};
424 ///
425 /// let standard = PieChart::default().resolution(Resolution::Standard);
426 /// let braille = PieChart::default().resolution(Resolution::Braille);
427 /// ```
428 #[must_use]
429 pub const fn resolution(mut self, resolution: Resolution) -> Self {
430 self.resolution = resolution;
431 self
432 }
433
434 /// Sets whether to use high resolution rendering with braille patterns.
435 ///
436 /// This is a convenience method that sets the resolution to `Braille` when enabled,
437 /// or `Standard` when disabled. For more control, use [`resolution`](Self::resolution).
438 ///
439 /// # Examples
440 ///
441 /// ```
442 /// use tui_piechart::PieChart;
443 ///
444 /// let piechart = PieChart::default().high_resolution(true);
445 /// ```
446 #[must_use]
447 pub const fn high_resolution(mut self, enabled: bool) -> Self {
448 self.resolution = if enabled {
449 Resolution::Braille
450 } else {
451 Resolution::Standard
452 };
453 self
454 }
455
456 /// Calculates the total value of all slices.
457 fn total_value(&self) -> f64 {
458 self.slices.iter().map(|s| s.value).sum()
459 }
460
461 /// Calculates the percentage for a given slice.
462 fn percentage(&self, slice: &PieSlice) -> f64 {
463 let total = self.total_value();
464 if total > 0.0 {
465 (slice.value / total) * 100.0
466 } else {
467 0.0
468 }
469 }
470}
471
472impl Styled for PieChart<'_> {
473 type Item = Self;
474
475 fn style(&self) -> Style {
476 self.style
477 }
478
479 fn set_style<S: Into<Style>>(mut self, style: S) -> Self::Item {
480 self.style = style.into();
481 self
482 }
483}
484
485impl Widget for PieChart<'_> {
486 fn render(self, area: Rect, buf: &mut Buffer) {
487 Widget::render(&self, area, buf);
488 }
489}
490
491impl Widget for &PieChart<'_> {
492 fn render(self, area: Rect, buf: &mut Buffer) {
493 buf.set_style(area, self.style);
494 let inner = if let Some(ref block) = self.block {
495 let inner_area = block.inner(area);
496 block.render(area, buf);
497 inner_area
498 } else {
499 area
500 };
501 self.render_piechart(inner, buf);
502 }
503}
504
505impl PieChart<'_> {
506 fn render_piechart(&self, area: Rect, buf: &mut Buffer) {
507 if area.is_empty() || self.slices.is_empty() {
508 return;
509 }
510
511 let total = self.total_value();
512 if total <= 0.0 {
513 return;
514 }
515
516 match self.resolution {
517 Resolution::Standard => {
518 // Continue with standard rendering below
519 }
520 Resolution::Braille => {
521 self.render_piechart_braille(area, buf);
522 return;
523 }
524 }
525
526 // If we need to show legend, reserve space on the right
527 let (pie_area, legend_x) = if self.show_legend && area.width > 35 {
528 let legend_width = 20;
529 let pie_width = area.width.saturating_sub(legend_width);
530 (
531 Rect {
532 x: area.x,
533 y: area.y,
534 width: pie_width,
535 height: area.height,
536 },
537 area.x + pie_width + 1, // Add 1 space padding
538 )
539 } else {
540 (area, 0)
541 };
542
543 // Calculate the center and radius of the pie chart
544 // Account for terminal character aspect ratio (typically 1:2, chars are twice as tall as wide)
545 let center_x = pie_area.width / 2;
546 let center_y = pie_area.height / 2;
547
548 // Adjust radius for aspect ratio - use width as limiting factor
549 let radius = center_x.min(center_y * 2).saturating_sub(1);
550
551 // Draw the pie chart
552 let mut cumulative_percent = 0.0;
553 for slice in &self.slices {
554 let percent = self.percentage(slice);
555 self.render_slice(
556 pie_area,
557 buf,
558 center_x,
559 center_y,
560 radius,
561 cumulative_percent,
562 percent,
563 slice.color,
564 );
565 cumulative_percent += percent;
566 }
567
568 // Draw legend if enabled
569 if self.show_legend && area.width > 35 {
570 self.render_legend(area, buf, legend_x);
571 }
572 }
573
574 #[allow(clippy::too_many_arguments, clippy::similar_names)]
575 fn render_slice(
576 &self,
577 area: Rect,
578 buf: &mut Buffer,
579 center_x: u16,
580 center_y: u16,
581 radius: u16,
582 start_percent: f64,
583 percent: f64,
584 color: Color,
585 ) {
586 if radius == 0 || percent <= 0.0 {
587 return;
588 }
589
590 // Start angle at top (90 degrees) and go clockwise
591 let start_angle = (start_percent / 100.0) * 2.0 * PI - PI / 2.0;
592 let end_angle = ((start_percent + percent) / 100.0) * 2.0 * PI - PI / 2.0;
593
594 // Scan the entire area around the center
595 let scan_width = i32::from(radius + 1);
596 let scan_height = i32::from((radius / 2) + 1); // Account for aspect ratio
597
598 for dy in -scan_height..=scan_height {
599 for dx in -scan_width..=scan_width {
600 // Calculate actual position in buffer
601 let x = i32::from(area.x) + i32::from(center_x) + dx;
602 let y = i32::from(area.y) + i32::from(center_y) + dy;
603
604 // Check bounds
605 if x < i32::from(area.x)
606 || x >= i32::from(area.x + area.width)
607 || y < i32::from(area.y)
608 || y >= i32::from(area.y + area.height)
609 {
610 continue;
611 }
612
613 // Adjust for aspect ratio: multiply y distance by 2
614 #[allow(clippy::cast_precision_loss)]
615 let adjusted_dx = f64::from(dx);
616 #[allow(clippy::cast_precision_loss)]
617 let adjusted_dy = f64::from(dy * 2);
618
619 // Calculate distance from center
620 let distance = (adjusted_dx * adjusted_dx + adjusted_dy * adjusted_dy).sqrt();
621
622 // Check if point is within radius
623 #[allow(clippy::cast_precision_loss)]
624 if distance <= f64::from(radius) {
625 // Calculate angle from center (0 = right, PI/2 = up, PI = left, 3PI/2 = down)
626 let angle = adjusted_dy.atan2(adjusted_dx);
627
628 // Check if angle is within slice
629 if Self::is_angle_in_slice(angle, start_angle, end_angle) {
630 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
631 {
632 let cell = &mut buf[(x as u16, y as u16)];
633 cell.set_char(self.pie_char).set_fg(color);
634 }
635 }
636 }
637 }
638 }
639 }
640
641 fn is_angle_in_slice(angle: f64, start: f64, end: f64) -> bool {
642 // Normalize angles to [0, 2π]
643 let normalize = |a: f64| {
644 let mut normalized = a % (2.0 * PI);
645 if normalized < 0.0 {
646 normalized += 2.0 * PI;
647 }
648 normalized
649 };
650
651 let norm_angle = normalize(angle);
652 let norm_start = normalize(start);
653 let norm_end = normalize(end);
654
655 if norm_start <= norm_end {
656 norm_angle >= norm_start && norm_angle <= norm_end
657 } else {
658 // Handle wrap around at 2π/0
659 norm_angle >= norm_start || norm_angle <= norm_end
660 }
661 }
662
663 fn render_legend(&self, area: Rect, buf: &mut Buffer, legend_x: u16) {
664 let total = self.total_value();
665
666 for (y_offset, slice) in self.slices.iter().enumerate() {
667 #[allow(clippy::cast_possible_truncation)]
668 let y_offset_u16 = y_offset as u16;
669
670 // Add spacing between legend items and start a bit lower
671 let actual_y_offset = y_offset_u16 * 2 + 1;
672
673 if actual_y_offset >= area.height {
674 break;
675 }
676
677 let legend_text = if self.show_percentages {
678 let percent = if total > 0.0 {
679 (slice.value / total) * 100.0
680 } else {
681 0.0
682 };
683 format!("{} {} {:.1}%", self.legend_marker, slice.label, percent)
684 } else {
685 format!("{} {}", self.legend_marker, slice.label)
686 };
687
688 let spans = vec![Span::styled(legend_text, Style::default().fg(slice.color))];
689 let line = Line::from(spans);
690
691 let legend_area = Rect {
692 x: legend_x,
693 y: area.y + actual_y_offset,
694 width: area.width.saturating_sub(legend_x - area.x),
695 height: 1,
696 };
697
698 line.render(legend_area, buf);
699 }
700 }
701
702 #[allow(clippy::similar_names)]
703 fn render_piechart_braille(&self, area: Rect, buf: &mut Buffer) {
704 // If we need to show legend, reserve space on the right
705 let (pie_area, legend_x) = if self.show_legend && area.width > 35 {
706 let legend_width = 20;
707 let pie_width = area.width.saturating_sub(legend_width);
708 (
709 Rect {
710 x: area.x,
711 y: area.y,
712 width: pie_width,
713 height: area.height,
714 },
715 area.x + pie_width + 1,
716 )
717 } else {
718 (area, 0)
719 };
720
721 // Calculate the center and radius of the pie chart
722 let center_x_chars = pie_area.width / 2;
723 let center_y_chars = pie_area.height / 2;
724
725 // Each character cell has 2x4 braille dots
726 let center_x_dots = center_x_chars * 2;
727 let center_y_dots = center_y_chars * 4;
728
729 // Calculate radius in dots
730 // Braille dots are equally spaced in physical screen space because:
731 // - Character cells are ~2:1 (height:width)
732 // - But braille has 2 horizontal dots and 4 vertical dots per character
733 // - So: horizontal spacing = W/2, vertical spacing = 2W/4 = W/2 (equal!)
734 let radius = (center_x_dots).min(center_y_dots).saturating_sub(2);
735
736 // Create a 2D array to store which slice each braille dot belongs to
737 let width_dots = pie_area.width * 2;
738 let height_dots = pie_area.height * 4;
739
740 let mut dot_slices: Vec<Vec<Option<usize>>> =
741 vec![vec![None; width_dots as usize]; height_dots as usize];
742
743 // Calculate slice assignments for each dot
744 let mut cumulative_percent = 0.0;
745 for (slice_idx, slice) in self.slices.iter().enumerate() {
746 let percent = self.percentage(slice);
747 let start_angle = (cumulative_percent / 100.0) * 2.0 * PI - PI / 2.0;
748 let end_angle = ((cumulative_percent + percent) / 100.0) * 2.0 * PI - PI / 2.0;
749
750 for dy in 0..height_dots {
751 for dx in 0..width_dots {
752 let rel_x = f64::from(dx) - f64::from(center_x_dots);
753 let rel_y = f64::from(dy) - f64::from(center_y_dots);
754
755 // No aspect ratio compensation needed for braille dots
756 // They're already equally spaced in physical screen space
757 let distance = (rel_x * rel_x + rel_y * rel_y).sqrt();
758
759 if distance <= f64::from(radius) {
760 let angle = rel_y.atan2(rel_x);
761 if Self::is_angle_in_slice(angle, start_angle, end_angle) {
762 dot_slices[dy as usize][dx as usize] = Some(slice_idx);
763 }
764 }
765 }
766 }
767
768 cumulative_percent += percent;
769 }
770
771 // Convert dot assignments to braille characters
772 for char_y in 0..pie_area.height {
773 for char_x in 0..pie_area.width {
774 let base_dot_x = char_x * 2;
775 let base_dot_y = char_y * 4;
776
777 // Braille pattern mapping (dots are numbered 1-8)
778 // Dot positions in a 2x4 grid:
779 // 1 4
780 // 2 5
781 // 3 6
782 // 7 8
783 let dot_positions = [
784 (0, 0, 0x01), // dot 1
785 (0, 1, 0x02), // dot 2
786 (0, 2, 0x04), // dot 3
787 (1, 0, 0x08), // dot 4
788 (1, 1, 0x10), // dot 5
789 (1, 2, 0x20), // dot 6
790 (0, 3, 0x40), // dot 7
791 (1, 3, 0x80), // dot 8
792 ];
793
794 let mut pattern = 0u32;
795 let mut slice_colors: Vec<(usize, u32)> = Vec::new();
796
797 for (dx, dy, bit) in dot_positions {
798 let dot_x = base_dot_x + dx;
799 let dot_y = base_dot_y + dy;
800
801 if dot_y < height_dots && dot_x < width_dots {
802 if let Some(slice_idx) = dot_slices[dot_y as usize][dot_x as usize] {
803 pattern |= bit;
804 // Track which slice and how many dots
805 if let Some(entry) =
806 slice_colors.iter_mut().find(|(idx, _)| *idx == slice_idx)
807 {
808 entry.1 += 1;
809 } else {
810 slice_colors.push((slice_idx, 1));
811 }
812 }
813 }
814 }
815
816 if pattern > 0 {
817 // Use the color of the slice with the most dots in this character
818 if let Some((slice_idx, _)) = slice_colors.iter().max_by_key(|(_, count)| count)
819 {
820 let braille_char = char::from_u32(0x2800 + pattern).unwrap_or('⠀');
821 let color = self.slices[*slice_idx].color;
822
823 let cell = &mut buf[(pie_area.x + char_x, pie_area.y + char_y)];
824 cell.set_char(braille_char).set_fg(color);
825 }
826 }
827 }
828 }
829
830 // Draw legend if enabled
831 if self.show_legend && area.width > 35 {
832 self.render_legend(area, buf, legend_x);
833 }
834 }
835}
836
837#[cfg(test)]
838#[allow(clippy::float_cmp)]
839mod tests {
840 use super::*;
841
842 #[test]
843 fn pie_slice_new() {
844 let slice = PieSlice::new("Test", 50.0, Color::Red);
845 assert_eq!(slice.label(), "Test");
846 assert_eq!(slice.value(), 50.0);
847 assert_eq!(slice.color(), Color::Red);
848 }
849
850 #[test]
851 fn piechart_new() {
852 let slices = vec![
853 PieSlice::new("A", 30.0, Color::Red),
854 PieSlice::new("B", 70.0, Color::Blue),
855 ];
856 let piechart = PieChart::new(slices.clone());
857 assert_eq!(piechart.slices, slices);
858 }
859
860 #[test]
861 fn piechart_default() {
862 let piechart = PieChart::default();
863 assert!(piechart.slices.is_empty());
864 assert!(piechart.show_legend);
865 assert!(piechart.show_percentages);
866 }
867
868 #[test]
869 fn piechart_slices() {
870 let slices = vec![PieSlice::new("Test", 100.0, Color::Green)];
871 let piechart = PieChart::default().slices(slices.clone());
872 assert_eq!(piechart.slices, slices);
873 }
874
875 #[test]
876 fn piechart_style() {
877 let style = Style::default().fg(Color::Red);
878 let piechart = PieChart::default().style(style);
879 assert_eq!(piechart.style, style);
880 }
881
882 #[test]
883 fn piechart_show_legend() {
884 let piechart = PieChart::default().show_legend(false);
885 assert!(!piechart.show_legend);
886 }
887
888 #[test]
889 fn piechart_show_percentages() {
890 let piechart = PieChart::default().show_percentages(false);
891 assert!(!piechart.show_percentages);
892 }
893
894 #[test]
895 fn piechart_pie_char() {
896 let piechart = PieChart::default().pie_char('█');
897 assert_eq!(piechart.pie_char, '█');
898 }
899
900 #[test]
901 fn piechart_total_value() {
902 let slices = vec![
903 PieSlice::new("A", 30.0, Color::Red),
904 PieSlice::new("B", 70.0, Color::Blue),
905 ];
906 let piechart = PieChart::new(slices);
907 assert_eq!(piechart.total_value(), 100.0);
908 }
909
910 #[test]
911 fn piechart_percentage() {
912 let slices = vec![
913 PieSlice::new("A", 30.0, Color::Red),
914 PieSlice::new("B", 70.0, Color::Blue),
915 ];
916 let piechart = PieChart::new(slices);
917 assert_eq!(
918 piechart.percentage(&PieSlice::new("A", 30.0, Color::Red)),
919 30.0
920 );
921 }
922
923 #[test]
924 fn piechart_render_empty_area() {
925 let piechart = PieChart::default();
926 let mut buffer = Buffer::empty(Rect::new(0, 0, 0, 0));
927 piechart.render(buffer.area, &mut buffer);
928 }
929
930 #[test]
931 fn piechart_render_with_block() {
932 let slices = vec![PieSlice::new("Test", 100.0, Color::Red)];
933 let piechart = PieChart::new(slices).block(Block::bordered());
934 let mut buffer = Buffer::empty(Rect::new(0, 0, 20, 10));
935 piechart.render(buffer.area, &mut buffer);
936 }
937
938 #[test]
939 fn piechart_render_basic() {
940 let slices = vec![
941 PieSlice::new("Rust", 45.0, Color::Red),
942 PieSlice::new("Go", 30.0, Color::Blue),
943 PieSlice::new("Python", 25.0, Color::Green),
944 ];
945 let piechart = PieChart::new(slices);
946 let mut buffer = Buffer::empty(Rect::new(0, 0, 40, 20));
947 piechart.render(buffer.area, &mut buffer);
948 }
949
950 #[test]
951 fn piechart_styled_trait() {
952 use ratatui::style::Stylize;
953 let piechart = PieChart::default().red();
954 assert_eq!(piechart.style.fg, Some(Color::Red));
955 }
956
957 #[test]
958 fn piechart_with_multiple_slices() {
959 let slices = vec![
960 PieSlice::new("A", 25.0, Color::Red),
961 PieSlice::new("B", 25.0, Color::Blue),
962 PieSlice::new("C", 25.0, Color::Green),
963 PieSlice::new("D", 25.0, Color::Yellow),
964 ];
965 let piechart = PieChart::new(slices);
966 assert_eq!(piechart.total_value(), 100.0);
967
968 let mut buffer = Buffer::empty(Rect::new(0, 0, 50, 30));
969 piechart.render(buffer.area, &mut buffer);
970 }
971
972 #[test]
973 fn piechart_zero_values() {
974 let slices = vec![
975 PieSlice::new("A", 0.0, Color::Red),
976 PieSlice::new("B", 0.0, Color::Blue),
977 ];
978 let piechart = PieChart::new(slices);
979 assert_eq!(piechart.total_value(), 0.0);
980 }
981
982 #[test]
983 fn piechart_method_chaining() {
984 use ratatui::widgets::Block;
985
986 let slices = vec![PieSlice::new("Test", 100.0, Color::Red)];
987 let piechart = PieChart::new(slices)
988 .show_legend(true)
989 .show_percentages(true)
990 .pie_char('█')
991 .block(Block::bordered().title("Test"))
992 .style(Style::default().fg(Color::White));
993
994 assert!(piechart.show_legend);
995 assert!(piechart.show_percentages);
996 assert_eq!(piechart.pie_char, '█');
997 assert!(piechart.block.is_some());
998 assert_eq!(piechart.style.fg, Some(Color::White));
999 }
1000
1001 #[test]
1002 fn piechart_custom_symbols() {
1003 use crate::symbols;
1004
1005 let piechart = PieChart::default().pie_char(symbols::PIE_CHAR_BLOCK);
1006 assert_eq!(piechart.pie_char, '█');
1007
1008 let piechart = PieChart::default().pie_char(symbols::PIE_CHAR_CIRCLE);
1009 assert_eq!(piechart.pie_char, '◉');
1010
1011 let piechart = PieChart::default().pie_char(symbols::PIE_CHAR_SQUARE);
1012 assert_eq!(piechart.pie_char, '■');
1013 }
1014
1015 #[test]
1016 fn piechart_is_angle_in_slice() {
1017 use std::f64::consts::PI;
1018
1019 // Test angle in range
1020 assert!(PieChart::is_angle_in_slice(PI / 4.0, 0.0, PI / 2.0));
1021
1022 // Test angle outside range
1023 assert!(!PieChart::is_angle_in_slice(PI, 0.0, PI / 2.0));
1024
1025 // Test wrap around
1026 assert!(PieChart::is_angle_in_slice(0.1, 1.5 * PI, 0.5));
1027 }
1028}