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}