Skip to main content

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//! - 🔭 More chart types (gauges, radial bars, radar, treemap, funnel, waterfall, instruments)
14//!   live in [`tui-chart`](https://crates.io/crates/tui-chart)
15//! - 🍩 Donut / segmented / HUD / rose pies via [`PieStyle`]
16//! - ⚡ Zero-cost abstractions
17//!
18//! ## Examples
19//!
20//! Basic usage:
21//!
22//! ```no_run
23//! use ratatui::style::Color;
24//! use tui_piechart::{PieChart, PieSlice};
25//!
26//! let slices = vec![
27//!     PieSlice::new("Rust", 45.0, Color::Red),
28//!     PieSlice::new("Go", 30.0, Color::Blue),
29//!     PieSlice::new("Python", 25.0, Color::Green),
30//! ];
31//! let piechart = PieChart::new(slices);
32//! ```
33//!
34//! With custom styling:
35//!
36//! ```no_run
37//! use ratatui::style::{Color, Style};
38//! use tui_piechart::{PieChart, PieSlice};
39//!
40//! let slices = vec![
41//!     PieSlice::new("Rust", 45.0, Color::Red),
42//!     PieSlice::new("Go", 30.0, Color::Blue),
43//! ];
44//! let piechart = PieChart::new(slices)
45//!     .style(Style::default())
46//!     .show_legend(true)
47//!     .show_percentages(true);
48//! ```
49//!
50//! With custom symbols:
51//!
52//! ```no_run
53//! use ratatui::style::Color;
54//! use tui_piechart::{PieChart, PieSlice, symbols};
55//!
56//! let slices = vec![
57//!     PieSlice::new("Rust", 45.0, Color::Red),
58//!     PieSlice::new("Go", 30.0, Color::Blue),
59//! ];
60//!
61//! // Use predefined symbols
62//! let piechart = PieChart::new(slices.clone())
63//!     .pie_char(symbols::PIE_CHAR_BLOCK)
64//!     .legend_marker(symbols::LEGEND_MARKER_CIRCLE);
65//!
66//! // Or use any custom characters
67//! let piechart = PieChart::new(slices)
68//!     .pie_char('█')
69//!     .legend_marker("→");
70//! ```
71//!
72//! With custom border styles:
73//!
74//! ```no_run
75//! use ratatui::style::Color;
76//! use tui_piechart::{PieChart, PieSlice, border_style::BorderStyle};
77//! // Or use backwards-compatible path: use tui_piechart::symbols::BorderStyle;
78//!
79//! let slices = vec![
80//!     PieSlice::new("Rust", 45.0, Color::Red),
81//!     PieSlice::new("Go", 30.0, Color::Blue),
82//! ];
83//!
84//! // Use predefined border styles
85//! let piechart = PieChart::new(slices)
86//!     .block(BorderStyle::Rounded.block().title("My Chart"));
87//! ```
88
89#![warn(missing_docs)]
90#![warn(clippy::pedantic)]
91#![allow(clippy::module_name_repetitions)]
92
93use std::f64::consts::PI;
94
95use ratatui::buffer::Buffer;
96use ratatui::layout::Rect;
97use ratatui::style::{Color, Style, Styled};
98use ratatui::text::{Line, Span};
99use ratatui::widgets::{Block, Widget};
100
101pub mod border_style;
102mod braille;
103pub mod legend;
104pub mod pie_style;
105#[macro_use]
106pub mod macros;
107pub mod symbols;
108pub mod title;
109
110// Re-export commonly used types from submodules for convenience
111pub use legend::{LegendAlignment, LegendLayout, LegendPosition};
112pub use pie_style::PieStyle;
113pub use title::{BlockExt, TitleAlignment, TitlePosition, TitleStyle};
114#[cfg(feature = "theme")]
115pub use tui_nuances::{Theme, ThemeProvider, ThemeSwitcher, Themed};
116
117/// Rendering resolution mode for pie charts.
118///
119/// Different resolution modes provide varying levels of detail by using
120/// different Unicode block drawing characters with different dot densities.
121///
122/// # Examples
123///
124/// ```
125/// use tui_piechart::{PieChart, PieSlice, Resolution};
126/// use ratatui::style::Color;
127///
128/// let slices = vec![PieSlice::new("Rust", 45.0, Color::Red)];
129///
130/// // Standard resolution (1 dot per character)
131/// let standard = PieChart::new(slices.clone())
132///     .resolution(Resolution::Standard);
133///
134/// // High resolution with braille patterns (8 dots per character)
135/// let braille = PieChart::new(slices)
136///     .resolution(Resolution::Braille);
137/// ```
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
139pub enum Resolution {
140    /// Standard resolution using full characters (1 dot per cell).
141    ///
142    /// Uses regular Unicode characters like `●`. This is the default mode.
143    #[default]
144    Standard,
145
146    /// Braille resolution using 2×4 dot patterns (8 dots per cell).
147    ///
148    /// Uses Unicode braille patterns (U+2800-U+28FF) providing 8x resolution.
149    /// This provides the highest resolution available for terminal rendering.
150    Braille,
151}
152
153/// A slice of the pie chart representing a portion of data.
154///
155/// Each slice has a label, a value, and a color.
156///
157/// # Examples
158///
159/// ```
160/// use ratatui::style::Color;
161/// use tui_piechart::PieSlice;
162///
163/// let slice = PieSlice::new("Rust", 45.0, Color::Red);
164/// ```
165#[derive(Debug, Clone, PartialEq)]
166pub struct PieSlice<'a> {
167    /// The label for this slice
168    label: &'a str,
169    /// The value of this slice (will be converted to percentage)
170    value: f64,
171    /// The color of this slice
172    color: Color,
173}
174
175impl<'a> PieSlice<'a> {
176    /// Creates a new pie slice with the given label, value, and color.
177    ///
178    /// # Examples
179    ///
180    /// ```
181    /// use ratatui::style::Color;
182    /// use tui_piechart::PieSlice;
183    ///
184    /// let slice = PieSlice::new("Rust", 45.0, Color::Red);
185    /// ```
186    #[must_use]
187    pub const fn new(label: &'a str, value: f64, color: Color) -> Self {
188        Self {
189            label,
190            value,
191            color,
192        }
193    }
194
195    /// Creates a slice colored from the theme palette (`theme.color(index)`).
196    ///
197    /// ```
198    /// use tui_piechart::{PieSlice, Theme};
199    /// let s = PieSlice::themed("Rust", 45.0, &Theme::nord(), 0);
200    /// assert_eq!(s.color(), Theme::nord().color(0));
201    /// ```
202    #[cfg(feature = "theme")]
203    #[must_use]
204    pub fn themed(label: &'a str, value: f64, theme: &Theme, index: usize) -> Self {
205        Self::new(label, value, theme.color(index))
206    }
207
208    /// Returns the label of this slice.
209    #[must_use]
210    pub const fn label(&self) -> &'a str {
211        self.label
212    }
213
214    /// Returns the value of this slice.
215    #[must_use]
216    pub const fn value(&self) -> f64 {
217        self.value
218    }
219
220    /// Returns the color of this slice.
221    #[must_use]
222    pub const fn color(&self) -> Color {
223        self.color
224    }
225}
226
227#[cfg(feature = "theme")]
228impl Themed for PieChart<'_> {
229    fn apply_theme(self, theme: &Theme) -> Self {
230        self.theme(theme)
231    }
232}
233
234/// A widget that displays a pie chart.
235///
236/// A `PieChart` displays data as slices of a circle, where each slice represents
237/// a proportion of the total.
238///
239/// # Examples
240///
241/// ```
242/// use ratatui::style::Color;
243/// use tui_piechart::{PieChart, PieSlice};
244///
245/// let slices = vec![
246///     PieSlice::new("Rust", 45.0, Color::Red),
247///     PieSlice::new("Go", 30.0, Color::Blue),
248///     PieSlice::new("Python", 25.0, Color::Green),
249/// ];
250/// let piechart = PieChart::new(slices);
251/// ```
252#[derive(Debug, Clone, PartialEq)]
253pub struct PieChart<'a> {
254    /// The slices of the pie chart
255    slices: Vec<PieSlice<'a>>,
256    /// Optional block to wrap the pie chart
257    block: Option<Block<'a>>,
258    /// Base style for the entire widget
259    style: Style,
260    /// Theme-derived base style; lower precedence than `style`
261    theme_style: Style,
262    /// Whether to show the legend
263    show_legend: bool,
264    /// Whether to show percentages on slices
265    show_percentages: bool,
266    /// The character to use for drawing the pie chart
267    pie_char: char,
268    /// The marker to use for legend items
269    legend_marker: &'a str,
270    /// Resolution mode for rendering
271    resolution: Resolution,
272    /// Position of the legend
273    legend_position: LegendPosition,
274    /// Layout of the legend
275    legend_layout: LegendLayout,
276    /// Alignment of legend items
277    legend_alignment: LegendAlignment,
278    /// Look of the pie (solid, segmented ring, HUD, rose)
279    pie_style: PieStyle,
280    /// Hole radius as a fraction of the radius (`0` = pie, `>0` = donut)
281    hole: f64,
282    /// Start angle in turns (`0` = 12 o'clock, clockwise)
283    start: f64,
284    /// Gap between slices in braille dots (canvas styles only)
285    gap: f64,
286    /// Outward shift of every slice in braille dots (canvas styles only)
287    explode: f64,
288    /// Text shown in the donut hole
289    center: Option<&'a str>,
290}
291
292impl Default for PieChart<'_> {
293    /// Returns a default `PieChart` widget.
294    ///
295    /// The default widget has:
296    /// - No slices
297    /// - No block
298    /// - Default style
299    /// - Legend shown
300    /// - Percentages shown
301    /// - Default pie character (●)
302    /// - Default legend marker (■)
303    fn default() -> Self {
304        Self {
305            slices: Vec::new(),
306            block: None,
307            style: Style::default(),
308            theme_style: Style::default(),
309            show_legend: true,
310            show_percentages: true,
311            pie_char: symbols::PIE_CHAR,
312            legend_marker: symbols::LEGEND_MARKER,
313            resolution: Resolution::default(),
314            legend_position: LegendPosition::default(),
315            legend_layout: LegendLayout::default(),
316            legend_alignment: LegendAlignment::default(),
317            pie_style: PieStyle::default(),
318            hole: 0.0,
319            start: 0.0,
320            gap: 0.0,
321            explode: 0.0,
322            center: None,
323        }
324    }
325}
326
327impl<'a> PieChart<'a> {
328    /// Creates a new `PieChart` with the given slices.
329    ///
330    /// # Examples
331    ///
332    /// ```
333    /// use ratatui::style::Color;
334    /// use tui_piechart::{PieChart, PieSlice};
335    ///
336    /// let slices = vec![
337    ///     PieSlice::new("Rust", 45.0, Color::Red),
338    ///     PieSlice::new("Go", 30.0, Color::Blue),
339    /// ];
340    /// let piechart = PieChart::new(slices);
341    /// ```
342    #[must_use]
343    pub fn new(slices: Vec<PieSlice<'a>>) -> Self {
344        Self {
345            slices,
346            ..Default::default()
347        }
348    }
349
350    /// The slices, in drawing order.
351    ///
352    /// Together with [`areas`](Self::areas) this lets other crates build hit
353    /// testing (hover / click) on top of a rendered chart.
354    #[must_use]
355    pub fn slice_list(&self) -> &[PieSlice<'a>] {
356        &self.slices
357    }
358
359    /// Sets the slices of the pie chart.
360    ///
361    /// # Examples
362    ///
363    /// ```
364    /// use ratatui::style::Color;
365    /// use tui_piechart::{PieChart, PieSlice};
366    ///
367    /// let slices = vec![
368    ///     PieSlice::new("Rust", 45.0, Color::Red),
369    /// ];
370    /// let piechart = PieChart::default().slices(slices);
371    /// ```
372    #[must_use]
373    pub fn slices(mut self, slices: Vec<PieSlice<'a>>) -> Self {
374        self.slices = slices;
375        self
376    }
377
378    /// Wraps the pie chart with the given block.
379    ///
380    /// # Examples
381    ///
382    /// ```
383    /// use ratatui::style::Color;
384    /// use ratatui::widgets::Block;
385    /// use tui_piechart::{PieChart, PieSlice};
386    ///
387    /// let slices = vec![PieSlice::new("Rust", 45.0, Color::Red)];
388    /// let piechart = PieChart::new(slices)
389    ///     .block(Block::bordered().title("Statistics"));
390    /// ```
391    #[must_use]
392    pub fn block(mut self, block: Block<'a>) -> Self {
393        self.block = Some(block);
394        self
395    }
396
397    /// Sets the base style of the widget.
398    ///
399    /// # Examples
400    ///
401    /// ```
402    /// use ratatui::style::{Color, Style};
403    /// use tui_piechart::PieChart;
404    ///
405    /// let piechart = PieChart::default()
406    ///     .style(Style::default().fg(Color::White));
407    /// ```
408    #[must_use]
409    pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
410        self.style = style.into();
411        self
412    }
413
414    /// Applies a shared [`Theme`] (foreground/background of the widget).
415    ///
416    /// Precedence: fields set explicitly with [`style`](Self::style) always win
417    /// over the theme, regardless of call order. Without a theme the look is unchanged.
418    ///
419    /// ```
420    /// use tui_piechart::{PieChart, Theme};
421    /// let chart = PieChart::default().theme(&Theme::nord());
422    /// ```
423    #[cfg(feature = "theme")]
424    #[must_use]
425    pub fn theme(mut self, theme: &Theme) -> Self {
426        self.theme_style = theme.style();
427        self
428    }
429
430    /// Sets whether to show the legend.
431    ///
432    /// # Examples
433    ///
434    /// ```
435    /// use tui_piechart::PieChart;
436    ///
437    /// let piechart = PieChart::default().show_legend(true);
438    /// ```
439    #[must_use]
440    pub const fn show_legend(mut self, show: bool) -> Self {
441        self.show_legend = show;
442        self
443    }
444
445    /// Sets whether to show percentages on slices.
446    ///
447    /// # Examples
448    ///
449    /// ```
450    /// use tui_piechart::PieChart;
451    ///
452    /// let piechart = PieChart::default().show_percentages(true);
453    /// ```
454    #[must_use]
455    pub const fn show_percentages(mut self, show: bool) -> Self {
456        self.show_percentages = show;
457        self
458    }
459
460    /// Sets the character used to draw the pie chart.
461    ///
462    /// You can use any Unicode character for custom visualization.
463    ///
464    /// # Examples
465    ///
466    /// Using a predefined symbol:
467    ///
468    /// ```
469    /// use tui_piechart::{PieChart, symbols};
470    ///
471    /// let piechart = PieChart::default()
472    ///     .pie_char(symbols::PIE_CHAR_BLOCK);
473    /// ```
474    ///
475    /// Using a custom character:
476    ///
477    /// ```
478    /// use tui_piechart::PieChart;
479    ///
480    /// let piechart = PieChart::default().pie_char('█');
481    /// ```
482    #[must_use]
483    pub const fn pie_char(mut self, c: char) -> Self {
484        self.pie_char = c;
485        self
486    }
487
488    /// Sets the marker used for legend items.
489    ///
490    /// You can use any string (including Unicode characters) for custom markers.
491    ///
492    /// # Examples
493    ///
494    /// Using a predefined symbol:
495    ///
496    /// ```
497    /// use tui_piechart::{PieChart, symbols};
498    ///
499    /// let piechart = PieChart::default()
500    ///     .legend_marker(symbols::LEGEND_MARKER_CIRCLE);
501    /// ```
502    ///
503    /// Using custom markers:
504    ///
505    /// ```
506    /// use tui_piechart::PieChart;
507    ///
508    /// // Simple arrow
509    /// let piechart = PieChart::default().legend_marker("→");
510    ///
511    /// // Or any Unicode character
512    /// let piechart = PieChart::default().legend_marker("★");
513    ///
514    /// // Or even multi-character strings
515    /// let piechart = PieChart::default().legend_marker("-->");
516    /// ```
517    #[must_use]
518    pub const fn legend_marker(mut self, marker: &'a str) -> Self {
519        self.legend_marker = marker;
520        self
521    }
522
523    /// Sets the rendering resolution mode.
524    ///
525    /// Different resolution modes provide varying levels of detail:
526    /// - `Standard`: Regular characters (1 dot per cell)
527    /// - `Braille`: 2×4 patterns (8 dots per cell, 8x resolution)
528    ///
529    /// # Examples
530    ///
531    /// ```
532    /// use tui_piechart::{PieChart, Resolution};
533    ///
534    /// let standard = PieChart::default().resolution(Resolution::Standard);
535    /// let braille = PieChart::default().resolution(Resolution::Braille);
536    /// ```
537    #[must_use]
538    pub const fn resolution(mut self, resolution: Resolution) -> Self {
539        self.resolution = resolution;
540        self
541    }
542
543    /// Sets whether to use high resolution rendering with braille patterns.
544    ///
545    /// This is a convenience method that sets the resolution to `Braille` when enabled,
546    /// or `Standard` when disabled. For more control, use [`resolution`](Self::resolution).
547    ///
548    /// # Examples
549    ///
550    /// ```
551    /// use tui_piechart::PieChart;
552    ///
553    /// let piechart = PieChart::default().high_resolution(true);
554    /// ```
555    #[must_use]
556    pub const fn high_resolution(mut self, enabled: bool) -> Self {
557        self.resolution = if enabled {
558            Resolution::Braille
559        } else {
560            Resolution::Standard
561        };
562        self
563    }
564
565    /// Sets the position of the legend relative to the pie chart.
566    ///
567    /// # Examples
568    ///
569    /// ```
570    /// use tui_piechart::{PieChart, LegendPosition};
571    ///
572    /// let piechart = PieChart::default()
573    ///     .legend_position(LegendPosition::Right);
574    /// ```
575    #[must_use]
576    pub const fn legend_position(mut self, position: LegendPosition) -> Self {
577        self.legend_position = position;
578        self
579    }
580
581    /// Sets the layout mode for the legend.
582    ///
583    /// # Examples
584    ///
585    /// ```
586    /// use tui_piechart::{PieChart, LegendLayout};
587    ///
588    /// // Single horizontal row
589    /// let piechart = PieChart::default()
590    ///     .legend_layout(LegendLayout::Horizontal);
591    ///
592    /// // Vertical stacking (default)
593    /// let piechart = PieChart::default()
594    ///     .legend_layout(LegendLayout::Vertical);
595    /// ```
596    #[must_use]
597    pub const fn legend_layout(mut self, layout: LegendLayout) -> Self {
598        self.legend_layout = layout;
599        self
600    }
601
602    /// Sets the alignment of legend items within the legend area.
603    ///
604    /// # Examples
605    ///
606    /// ```
607    /// use tui_piechart::{PieChart, LegendAlignment};
608    ///
609    /// // Center-align legend items
610    /// let piechart = PieChart::default()
611    ///     .legend_alignment(LegendAlignment::Center);
612    ///
613    /// // Right-align legend items
614    /// let piechart = PieChart::default()
615    ///     .legend_alignment(LegendAlignment::Right);
616    /// ```
617    #[must_use]
618    pub const fn legend_alignment(mut self, alignment: LegendAlignment) -> Self {
619        self.legend_alignment = alignment;
620        self
621    }
622
623    /// Draws a donut: the inner `hole` fraction (`0.0..=0.95`) of the radius stays empty.
624    ///
625    /// # Examples
626    ///
627    /// ```
628    /// use tui_piechart::PieChart;
629    ///
630    /// let donut = PieChart::default().hole(0.55).center("42 items");
631    /// ```
632    #[must_use]
633    pub fn hole(mut self, hole: f64) -> Self {
634        self.hole = if hole.is_finite() {
635            hole.clamp(0.0, 0.95)
636        } else {
637            0.0
638        };
639        self
640    }
641
642    /// Sets where the first slice starts, in degrees clockwise from 12 o'clock.
643    ///
644    /// # Examples
645    ///
646    /// ```
647    /// use tui_piechart::PieChart;
648    ///
649    /// let chart = PieChart::default().start_angle(90.0);
650    /// ```
651    #[must_use]
652    pub fn start_angle(mut self, degrees: f64) -> Self {
653        self.start = if degrees.is_finite() {
654            (degrees / 360.0).rem_euclid(1.0)
655        } else {
656            0.0
657        };
658        self
659    }
660
661    /// Leaves a gap of `dots` braille dots between slices (`0` = flush).
662    ///
663    /// Gaps, [`Self::explode`] and the non-solid [`PieStyle`]s are drawn with
664    /// braille dots, whatever the [`Resolution`].
665    #[must_use]
666    pub fn gap(mut self, dots: f64) -> Self {
667        self.gap = if dots.is_finite() {
668            dots.clamp(0.0, 8.0)
669        } else {
670            0.0
671        };
672        self
673    }
674
675    /// Pushes every slice outward by `dots` braille dots (an exploded pie).
676    #[must_use]
677    pub fn explode(mut self, dots: f64) -> Self {
678        self.explode = if dots.is_finite() {
679            dots.clamp(0.0, 8.0)
680        } else {
681            0.0
682        };
683        self
684    }
685
686    /// Chooses how slices are drawn: solid, segmented ring, HUD band or rose.
687    #[must_use]
688    pub const fn pie_style(mut self, style: PieStyle) -> Self {
689        self.pie_style = style;
690        self
691    }
692
693    /// Text shown in the middle of a donut (needs a [`Self::hole`]).
694    #[must_use]
695    pub const fn center(mut self, text: &'a str) -> Self {
696        self.center = Some(text);
697        self
698    }
699
700    fn total_value(&self) -> f64 {
701        self.slices.iter().map(|s| s.value).sum()
702    }
703
704    /// Computes the percentage a value represents of a total.
705    ///
706    /// Returns `0.0` when `total` is not strictly positive, which keeps the
707    /// arithmetic safe for empty charts and all-zero data sets.
708    fn value_percent(value: f64, total: f64) -> f64 {
709        if total > 0.0 {
710            (value / total) * 100.0
711        } else {
712            0.0
713        }
714    }
715
716    /// Calculates the percentage for a given slice.
717    fn percentage(&self, slice: &PieSlice) -> f64 {
718        Self::value_percent(slice.value, self.total_value())
719    }
720}
721
722impl Styled for PieChart<'_> {
723    type Item = Self;
724
725    fn style(&self) -> Style {
726        self.style
727    }
728
729    fn set_style<S: Into<Style>>(mut self, style: S) -> Self::Item {
730        self.style = style.into();
731        self
732    }
733}
734
735impl Widget for PieChart<'_> {
736    fn render(self, area: Rect, buf: &mut Buffer) {
737        Widget::render(&self, area, buf);
738    }
739}
740
741impl Widget for &PieChart<'_> {
742    fn render(self, area: Rect, buf: &mut Buffer) {
743        buf.set_style(area, self.theme_style.patch(self.style));
744        let inner = if let Some(ref block) = self.block {
745            let inner_area = block.inner(area);
746            block.render(area, buf);
747            inner_area
748        } else {
749            area
750        };
751        self.render_piechart(inner, buf);
752    }
753}
754
755impl PieChart<'_> {
756    /// Maximum ratio for vertical legend width (1/3 of available width).
757    const LEGEND_VERTICAL_MAX_RATIO: u16 = 3;
758
759    /// Minimum width for vertical legend to ensure readability.
760    const LEGEND_VERTICAL_MIN_WIDTH: u16 = 20;
761
762    /// Maximum ratio for horizontal legend width (2/5 = 40% of available width).
763    /// This keeps the pie chart proportional and prevents legend from dominating.
764    const LEGEND_HORIZONTAL_MAX_RATIO: u16 = 5;
765
766    /// Absolute maximum width for horizontal legends to prevent excessive space usage.
767    const LEGEND_HORIZONTAL_MAX_WIDTH: u16 = 60;
768
769    /// Absolute maximum height for vertical legends to prevent pie chart from being too small.
770    /// This allows 4 items with spacing (4 items * 2 lines = 8 lines, +1 for padding = 9).
771    const LEGEND_VERTICAL_MAX_HEIGHT: u16 = 9;
772
773    /// Height required for horizontal legend layout (single row with padding).
774    const LEGEND_HORIZONTAL_HEIGHT: u16 = 3;
775
776    /// Space between pie chart and legend areas.
777    const LEGEND_SPACING: u16 = 1;
778
779    /// Inner padding for legend area.
780    const LEGEND_PADDING: u16 = 1;
781
782    fn render_piechart(&self, area: Rect, buf: &mut Buffer) {
783        if area.is_empty() || self.slices.is_empty() {
784            return;
785        }
786
787        let total = self.total_value();
788        if total <= 0.0 {
789            return;
790        }
791
792        if self.needs_canvas() {
793            self.render_piechart_canvas(area, buf);
794            return;
795        }
796
797        match self.resolution {
798            Resolution::Standard => {
799                // Continue with standard rendering below
800            }
801            Resolution::Braille => {
802                self.render_piechart_braille(area, buf);
803                return;
804            }
805        }
806
807        // Calculate layout with legend positioning
808        let (pie_area, legend_area_opt) = self.calculate_layout(area);
809
810        // Calculate the center and radius of the pie chart
811        // Account for terminal character aspect ratio (typically 1:2, chars are twice as tall as wide)
812        let center_x = pie_area.width / 2;
813        let center_y = pie_area.height / 2;
814
815        // Adjust radius for aspect ratio - use width as limiting factor
816        let radius = center_x.min(center_y * 2).saturating_sub(1);
817
818        // Draw the pie chart
819        let mut cumulative_percent = self.start * 100.0;
820        for slice in &self.slices {
821            let percent = self.percentage(slice);
822            self.render_slice(
823                pie_area,
824                buf,
825                center_x,
826                center_y,
827                radius,
828                cumulative_percent,
829                percent,
830                slice.color,
831            );
832            cumulative_percent += percent;
833        }
834
835        self.draw_center(pie_area, buf);
836
837        // Draw legend if enabled
838        if let Some(legend_area) = legend_area_opt {
839            self.render_legend(buf, legend_area);
840        }
841    }
842
843    /// Returns the `(start_angle, end_angle, is_full_circle)` for a slice.
844    ///
845    /// Angles start at the top of the circle (12 o'clock) and increase
846    /// clockwise. A slice covering the whole circle is flagged so callers can
847    /// fill the entire disc instead of collapsing to a single boundary line.
848    fn slice_angles(start_percent: f64, percent: f64) -> (f64, f64, bool) {
849        let start_angle = (start_percent / 100.0) * 2.0 * PI - PI / 2.0;
850        let end_angle = ((start_percent + percent) / 100.0) * 2.0 * PI - PI / 2.0;
851        let is_full_circle = percent >= 100.0 - f64::EPSILON;
852        (start_angle, end_angle, is_full_circle)
853    }
854
855    #[allow(clippy::too_many_arguments, clippy::similar_names)]
856    fn render_slice(
857        &self,
858        area: Rect,
859        buf: &mut Buffer,
860        center_x: u16,
861        center_y: u16,
862        radius: u16,
863        start_percent: f64,
864        percent: f64,
865        color: Color,
866    ) {
867        if radius == 0 || percent <= 0.0 {
868            return;
869        }
870
871        // A slice covering the whole (or effectively the whole) circle must be
872        // rendered as a full disc. Otherwise the start and end angles collapse
873        // to the same value and only the boundary line gets drawn.
874        let (start_angle, end_angle, is_full_circle) = Self::slice_angles(start_percent, percent);
875
876        // Scan the entire area around the center
877        let scan_width = i32::from(radius + 1);
878        let scan_height = i32::from((radius / 2) + 1); // Account for aspect ratio
879
880        for dy in -scan_height..=scan_height {
881            for dx in -scan_width..=scan_width {
882                // Calculate actual position in buffer
883                let x = i32::from(area.x) + i32::from(center_x) + dx;
884                let y = i32::from(area.y) + i32::from(center_y) + dy;
885
886                // Check bounds
887                if x < i32::from(area.x)
888                    || x >= i32::from(area.x + area.width)
889                    || y < i32::from(area.y)
890                    || y >= i32::from(area.y + area.height)
891                {
892                    continue;
893                }
894
895                // Adjust for aspect ratio: multiply y distance by 2
896                #[allow(clippy::cast_precision_loss)]
897                let adjusted_dx = f64::from(dx);
898                #[allow(clippy::cast_precision_loss)]
899                let adjusted_dy = f64::from(dy * 2);
900
901                // Calculate distance from center
902                let distance = (adjusted_dx * adjusted_dx + adjusted_dy * adjusted_dy).sqrt();
903
904                // Check if point is within radius
905                #[allow(clippy::cast_precision_loss)]
906                if distance <= f64::from(radius) && distance >= self.hole * f64::from(radius) {
907                    // Calculate angle from center (0 = right, PI/2 = up, PI = left, 3PI/2 = down)
908                    let angle = adjusted_dy.atan2(adjusted_dx);
909
910                    // Check if angle is within slice
911                    if is_full_circle || Self::is_angle_in_slice(angle, start_angle, end_angle) {
912                        #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
913                        {
914                            let cell = &mut buf[(x as u16, y as u16)];
915                            cell.set_char(self.pie_char).set_fg(color);
916                        }
917                    }
918                }
919            }
920        }
921    }
922
923    fn is_angle_in_slice(angle: f64, start: f64, end: f64) -> bool {
924        // Normalize angles to [0, 2π]
925        let normalize = |a: f64| {
926            let mut normalized = a % (2.0 * PI);
927            if normalized < 0.0 {
928                normalized += 2.0 * PI;
929            }
930            normalized
931        };
932
933        let norm_angle = normalize(angle);
934        let norm_start = normalize(start);
935        let norm_end = normalize(end);
936
937        if norm_start <= norm_end {
938            norm_angle >= norm_start && norm_angle <= norm_end
939        } else {
940            // Handle wrap around at 2π/0
941            norm_angle >= norm_start || norm_angle <= norm_end
942        }
943    }
944
945    fn format_legend_text(&self, slice: &PieSlice, total: f64, spacing: &str) -> String {
946        if self.show_percentages {
947            let percent = Self::value_percent(slice.value, total);
948            format!(
949                "{} {} {:.1}%{}",
950                self.legend_marker, slice.label, percent, spacing
951            )
952        } else {
953            format!("{} {}{}", self.legend_marker, slice.label, spacing)
954        }
955    }
956
957    /// Rendered display width of a single legend entry (marker, label, and an
958    /// optional percentage) followed by two spaces of trailing padding.
959    fn legend_item_width(&self, slice: &PieSlice, total: f64) -> u16 {
960        u16::try_from(self.format_legend_text(slice, total, "  ").chars().count())
961            .unwrap_or(u16::MAX)
962    }
963
964    fn calculate_aligned_x(&self, legend_area: Rect, content_width: u16) -> u16 {
965        match self.legend_alignment {
966            LegendAlignment::Left => legend_area.x,
967            LegendAlignment::Center => {
968                legend_area.x + (legend_area.width.saturating_sub(content_width)) / 2
969            }
970            LegendAlignment::Right => {
971                legend_area.x + legend_area.width.saturating_sub(content_width)
972            }
973        }
974    }
975
976    fn render_legend(&self, buf: &mut Buffer, legend_area: Rect) {
977        let total = self.total_value();
978
979        match self.legend_layout {
980            LegendLayout::Vertical => {
981                self.render_vertical_legend(buf, legend_area, total);
982            }
983            LegendLayout::Horizontal => {
984                self.render_horizontal_legend(buf, legend_area, total);
985            }
986        }
987    }
988
989    fn render_vertical_legend(&self, buf: &mut Buffer, legend_area: Rect, total: f64) {
990        for (idx, slice) in self.slices.iter().enumerate() {
991            #[allow(clippy::cast_possible_truncation)]
992            let y_offset = (idx as u16) * 2;
993
994            if y_offset >= legend_area.height {
995                break;
996            }
997
998            let legend_text = self.format_legend_text(slice, total, "");
999            #[allow(clippy::cast_possible_truncation)]
1000            let text_width = u16::try_from(legend_text.chars().count()).unwrap_or(u16::MAX);
1001            let x_pos = self.calculate_aligned_x(legend_area, text_width);
1002
1003            let line = Line::from(vec![Span::styled(
1004                legend_text,
1005                Style::default().fg(slice.color),
1006            )]);
1007            let item_area = Rect {
1008                x: x_pos,
1009                y: legend_area.y + y_offset,
1010                width: text_width.min(legend_area.width),
1011                height: 1,
1012            };
1013
1014            line.render(item_area, buf);
1015        }
1016    }
1017
1018    fn render_horizontal_legend(&self, buf: &mut Buffer, legend_area: Rect, total: f64) {
1019        let mut total_width = 0u16;
1020        let mut item_widths = Vec::new();
1021
1022        for slice in &self.slices {
1023            let legend_text = self.format_legend_text(slice, total, "  ");
1024            #[allow(clippy::cast_possible_truncation)]
1025            let text_width = u16::try_from(legend_text.chars().count()).unwrap_or(u16::MAX);
1026            item_widths.push(text_width);
1027            total_width = total_width.saturating_add(text_width);
1028        }
1029
1030        let start_x = self.calculate_aligned_x(legend_area, total_width.min(legend_area.width));
1031        let mut x_offset = 0u16;
1032
1033        for (idx, slice) in self.slices.iter().enumerate() {
1034            if x_offset >= legend_area.width {
1035                break;
1036            }
1037
1038            let legend_text = self.format_legend_text(slice, total, "  ");
1039            let text_width = item_widths[idx];
1040
1041            let line = Line::from(vec![Span::styled(
1042                legend_text,
1043                Style::default().fg(slice.color),
1044            )]);
1045            let item_area = Rect {
1046                x: start_x + x_offset,
1047                y: legend_area.y,
1048                width: text_width.min(legend_area.width.saturating_sub(x_offset)),
1049                height: 1,
1050            };
1051
1052            line.render(item_area, buf);
1053            x_offset = x_offset.saturating_add(text_width);
1054        }
1055    }
1056
1057    #[allow(clippy::too_many_lines)]
1058    /// Where the pie and the legend are drawn inside `area` (inside the block,
1059    /// if there is one): `(pie_area, legend_area)`.
1060    ///
1061    /// # Example
1062    ///
1063    /// ```
1064    /// use ratatui::{layout::Rect, style::Color};
1065    /// use tui_piechart::{PieChart, PieSlice};
1066    ///
1067    /// let chart = PieChart::new(vec![PieSlice::new("a", 1.0, Color::Red)]);
1068    /// let (pie, legend) = chart.areas(Rect::new(0, 0, 60, 20));
1069    /// assert!(!pie.is_empty() && legend.is_some());
1070    /// ```
1071    #[must_use]
1072    pub fn areas(&self, area: Rect) -> (Rect, Option<Rect>) {
1073        let inner = self.block.as_ref().map_or(area, |b| b.inner(area));
1074        self.calculate_layout(inner)
1075    }
1076
1077    fn calculate_layout(&self, area: Rect) -> (Rect, Option<Rect>) {
1078        if !self.show_legend || area.width < 20 || area.height < 10 {
1079            return (area, None);
1080        }
1081
1082        // Vertical layout uses Left/Right positions, Horizontal layout uses Top/Bottom
1083        match (self.legend_position, self.legend_layout) {
1084            // Left/Right with Vertical layout - proper vertical stacking on sides
1085            (LegendPosition::Left | LegendPosition::Right, LegendLayout::Vertical) => {
1086                let legend_width = self
1087                    .calculate_legend_width()
1088                    .min(area.width / Self::LEGEND_VERTICAL_MAX_RATIO)
1089                    .max(Self::LEGEND_VERTICAL_MIN_WIDTH);
1090                let is_left = matches!(self.legend_position, LegendPosition::Left);
1091                Self::layout_horizontal_split(area, legend_width, is_left)
1092            }
1093            // Top/Bottom with Horizontal layout - single row at top/bottom
1094            (LegendPosition::Top | LegendPosition::Bottom, LegendLayout::Horizontal) => {
1095                let is_top = matches!(self.legend_position, LegendPosition::Top);
1096                Self::layout_vertical_split(area, Self::LEGEND_HORIZONTAL_HEIGHT, is_top)
1097            }
1098            // Fallback: use horizontal layout for incompatible combinations
1099            (LegendPosition::Left | LegendPosition::Right, LegendLayout::Horizontal) => {
1100                // Horizontal layout on sides - allocate limited width
1101                let legend_width = self
1102                    .calculate_legend_horizontal_width()
1103                    .min(
1104                        (area.width * (Self::LEGEND_HORIZONTAL_MAX_RATIO - 1))
1105                            / Self::LEGEND_HORIZONTAL_MAX_RATIO,
1106                    )
1107                    .min(Self::LEGEND_HORIZONTAL_MAX_WIDTH);
1108                let is_left = matches!(self.legend_position, LegendPosition::Left);
1109                Self::layout_horizontal_split(area, legend_width, is_left)
1110            }
1111            (LegendPosition::Top | LegendPosition::Bottom, LegendLayout::Vertical) => {
1112                // Vertical layout at top/bottom - use 2-column grid with minimal height
1113                let legend_height = self.calculate_vertical_grid_height(area.width);
1114                let is_top = matches!(self.legend_position, LegendPosition::Top);
1115                Self::layout_vertical_split(area, legend_height, is_top)
1116            }
1117        }
1118    }
1119
1120    fn calculate_vertical_grid_height(&self, available_width: u16) -> u16 {
1121        // For vertical layout at top/bottom, use 2-column grid
1122        let max_item_width = self.calculate_legend_width();
1123        let columns = (available_width.saturating_sub(Self::LEGEND_PADDING * 2)
1124            / max_item_width.max(1))
1125        .clamp(1, 2);
1126
1127        #[allow(clippy::cast_possible_truncation)]
1128        let num_items = self.slices.len() as u16;
1129
1130        // Calculate rows: ceil(items / columns)
1131        let rows = num_items.div_ceil(columns);
1132        // Each row needs 2 lines (item + spacing), plus account for padding that will be subtracted
1133        (rows * 2 + Self::LEGEND_PADDING).clamp(4, Self::LEGEND_VERTICAL_MAX_HEIGHT)
1134    }
1135
1136    fn layout_horizontal_split(
1137        area: Rect,
1138        legend_width: u16,
1139        legend_on_left: bool,
1140    ) -> (Rect, Option<Rect>) {
1141        if area.width <= legend_width {
1142            return (area, None);
1143        }
1144
1145        let pie_width = area
1146            .width
1147            .saturating_sub(legend_width + Self::LEGEND_SPACING);
1148
1149        if legend_on_left {
1150            (
1151                Rect {
1152                    x: area.x + legend_width + Self::LEGEND_SPACING,
1153                    y: area.y,
1154                    width: pie_width,
1155                    height: area.height,
1156                },
1157                Some(Rect {
1158                    x: area.x,
1159                    y: area.y + Self::LEGEND_PADDING,
1160                    width: legend_width,
1161                    height: area.height.saturating_sub(Self::LEGEND_PADDING * 2),
1162                }),
1163            )
1164        } else {
1165            (
1166                Rect {
1167                    x: area.x,
1168                    y: area.y,
1169                    width: pie_width,
1170                    height: area.height,
1171                },
1172                Some(Rect {
1173                    x: area.x + pie_width + Self::LEGEND_SPACING,
1174                    y: area.y + Self::LEGEND_PADDING,
1175                    width: legend_width,
1176                    height: area.height.saturating_sub(Self::LEGEND_PADDING * 2),
1177                }),
1178            )
1179        }
1180    }
1181
1182    fn layout_vertical_split(
1183        area: Rect,
1184        legend_height: u16,
1185        legend_on_top: bool,
1186    ) -> (Rect, Option<Rect>) {
1187        if area.height <= legend_height {
1188            return (area, None);
1189        }
1190
1191        let pie_height = area
1192            .height
1193            .saturating_sub(legend_height + Self::LEGEND_SPACING);
1194
1195        if legend_on_top {
1196            (
1197                Rect {
1198                    x: area.x,
1199                    y: area.y + legend_height + Self::LEGEND_SPACING,
1200                    width: area.width,
1201                    height: pie_height,
1202                },
1203                Some(Rect {
1204                    x: area.x + Self::LEGEND_PADDING,
1205                    y: area.y + Self::LEGEND_PADDING,
1206                    width: area.width.saturating_sub(Self::LEGEND_PADDING * 2),
1207                    height: legend_height.saturating_sub(Self::LEGEND_PADDING),
1208                }),
1209            )
1210        } else {
1211            (
1212                Rect {
1213                    x: area.x,
1214                    y: area.y,
1215                    width: area.width,
1216                    height: pie_height,
1217                },
1218                Some(Rect {
1219                    x: area.x + Self::LEGEND_PADDING,
1220                    y: area.y + pie_height + Self::LEGEND_SPACING,
1221                    width: area.width.saturating_sub(Self::LEGEND_PADDING * 2),
1222                    height: legend_height.saturating_sub(Self::LEGEND_PADDING),
1223                }),
1224            )
1225        }
1226    }
1227
1228    fn calculate_legend_width(&self) -> u16 {
1229        let total = self.total_value();
1230        let widths = self.slices.iter().map(|s| self.legend_item_width(s, total));
1231
1232        let base = match self.legend_layout {
1233            // For vertical layout, the column is as wide as the widest item.
1234            LegendLayout::Vertical => widths.max().unwrap_or(0),
1235            // For horizontal layout, all items sit on one row.
1236            LegendLayout::Horizontal => widths.fold(0u16, u16::saturating_add),
1237        };
1238
1239        base.saturating_add(2)
1240    }
1241
1242    fn calculate_legend_horizontal_width(&self) -> u16 {
1243        let total = self.total_value();
1244        self.slices
1245            .iter()
1246            .map(|s| self.legend_item_width(s, total))
1247            .fold(0u16, u16::saturating_add)
1248            .saturating_add(2)
1249    }
1250
1251    #[allow(clippy::similar_names)]
1252    fn render_piechart_braille(&self, area: Rect, buf: &mut Buffer) {
1253        // Calculate layout with legend positioning
1254        let (pie_area, legend_area_opt) = self.calculate_layout(area);
1255
1256        // Calculate the center and radius of the pie chart
1257        let center_x_chars = pie_area.width / 2;
1258        let center_y_chars = pie_area.height / 2;
1259
1260        // Each character cell has 2x4 braille dots
1261        let center_x_dots = center_x_chars * 2;
1262        let center_y_dots = center_y_chars * 4;
1263
1264        // Calculate radius in dots
1265        // Braille dots are equally spaced in physical screen space because:
1266        // - Character cells are ~2:1 (height:width)
1267        // - But braille has 2 horizontal dots and 4 vertical dots per character
1268        // - So: horizontal spacing = W/2, vertical spacing = 2W/4 = W/2 (equal!)
1269        let radius = (center_x_dots).min(center_y_dots).saturating_sub(2);
1270
1271        // Create a 2D array to store which slice each braille dot belongs to
1272        let width_dots = pie_area.width * 2;
1273        let height_dots = pie_area.height * 4;
1274
1275        let mut dot_slices: Vec<Vec<Option<usize>>> =
1276            vec![vec![None; width_dots as usize]; height_dots as usize];
1277
1278        // Calculate slice assignments for each dot
1279        let mut cumulative_percent = self.start * 100.0;
1280        for (slice_idx, slice) in self.slices.iter().enumerate() {
1281            let percent = self.percentage(slice);
1282            let (start_angle, end_angle, is_full_circle) =
1283                Self::slice_angles(cumulative_percent, percent);
1284
1285            for dy in 0..height_dots {
1286                for dx in 0..width_dots {
1287                    let rel_x = f64::from(dx) - f64::from(center_x_dots);
1288                    let rel_y = f64::from(dy) - f64::from(center_y_dots);
1289
1290                    // No aspect ratio compensation needed for braille dots
1291                    // They're already equally spaced in physical screen space
1292                    let distance = (rel_x * rel_x + rel_y * rel_y).sqrt();
1293
1294                    if distance <= f64::from(radius) && distance >= self.hole * f64::from(radius) {
1295                        let angle = rel_y.atan2(rel_x);
1296                        if is_full_circle || Self::is_angle_in_slice(angle, start_angle, end_angle)
1297                        {
1298                            dot_slices[dy as usize][dx as usize] = Some(slice_idx);
1299                        }
1300                    }
1301                }
1302            }
1303
1304            cumulative_percent += percent;
1305        }
1306
1307        // Convert dot assignments to braille characters
1308        for char_y in 0..pie_area.height {
1309            for char_x in 0..pie_area.width {
1310                let base_dot_x = char_x * 2;
1311                let base_dot_y = char_y * 4;
1312
1313                // Braille pattern mapping (dots are numbered 1-8)
1314                // Dot positions in a 2x4 grid:
1315                // 1 4
1316                // 2 5
1317                // 3 6
1318                // 7 8
1319                let dot_positions = [
1320                    (0, 0, 0x01), // dot 1
1321                    (0, 1, 0x02), // dot 2
1322                    (0, 2, 0x04), // dot 3
1323                    (1, 0, 0x08), // dot 4
1324                    (1, 1, 0x10), // dot 5
1325                    (1, 2, 0x20), // dot 6
1326                    (0, 3, 0x40), // dot 7
1327                    (1, 3, 0x80), // dot 8
1328                ];
1329
1330                let mut pattern = 0u32;
1331                let mut slice_colors: Vec<(usize, u32)> = Vec::new();
1332
1333                for (dx, dy, bit) in dot_positions {
1334                    let dot_x = base_dot_x + dx;
1335                    let dot_y = base_dot_y + dy;
1336
1337                    if dot_y < height_dots && dot_x < width_dots {
1338                        if let Some(slice_idx) = dot_slices[dot_y as usize][dot_x as usize] {
1339                            pattern |= bit;
1340                            // Track which slice and how many dots
1341                            if let Some(entry) =
1342                                slice_colors.iter_mut().find(|(idx, _)| *idx == slice_idx)
1343                            {
1344                                entry.1 += 1;
1345                            } else {
1346                                slice_colors.push((slice_idx, 1));
1347                            }
1348                        }
1349                    }
1350                }
1351
1352                if pattern > 0 {
1353                    // Use the color of the slice with the most dots in this character
1354                    if let Some((slice_idx, _)) = slice_colors.iter().max_by_key(|(_, count)| count)
1355                    {
1356                        let braille_char = char::from_u32(0x2800 + pattern).unwrap_or('⠀');
1357                        let color = self.slices[*slice_idx].color;
1358
1359                        let cell = &mut buf[(pie_area.x + char_x, pie_area.y + char_y)];
1360                        cell.set_char(braille_char).set_fg(color);
1361                    }
1362                }
1363            }
1364        }
1365
1366        self.draw_center(pie_area, buf);
1367
1368        // Draw legend if enabled
1369        if let Some(legend_area) = legend_area_opt {
1370            self.render_legend(buf, legend_area);
1371        }
1372    }
1373}
1374
1375#[cfg(test)]
1376#[allow(clippy::float_cmp)]
1377#[allow(unnameable_test_items)]
1378mod tests {
1379    use super::*;
1380
1381    #[test]
1382    fn pie_slice_new() {
1383        let slice = PieSlice::new("Test", 50.0, Color::Red);
1384        assert_eq!(slice.label(), "Test");
1385        assert_eq!(slice.value(), 50.0);
1386        assert_eq!(slice.color(), Color::Red);
1387    }
1388
1389    #[test]
1390    fn piechart_new() {
1391        let slices = vec![
1392            PieSlice::new("A", 30.0, Color::Red),
1393            PieSlice::new("B", 70.0, Color::Blue),
1394        ];
1395        let piechart = PieChart::new(slices.clone());
1396        assert_eq!(piechart.slices, slices);
1397    }
1398
1399    #[test]
1400    fn piechart_default() {
1401        let piechart = PieChart::default();
1402        assert_eq!(piechart.slices, [] as [PieSlice<'_>; 0]);
1403        assert!(piechart.show_legend);
1404        assert!(piechart.show_percentages);
1405    }
1406
1407    #[test]
1408    fn piechart_slices() {
1409        let slices = vec![PieSlice::new("Test", 100.0, Color::Green)];
1410        let piechart = PieChart::default().slices(slices.clone());
1411        assert_eq!(piechart.slices, slices);
1412    }
1413
1414    #[test]
1415    fn piechart_style() {
1416        let style = Style::default().fg(Color::Red);
1417        let piechart = PieChart::default().style(style);
1418        assert_eq!(piechart.style, style);
1419    }
1420
1421    #[test]
1422    fn piechart_show_legend() {
1423        let piechart = PieChart::default().show_legend(false);
1424        assert!(!piechart.show_legend);
1425    }
1426
1427    #[test]
1428    fn piechart_show_percentages() {
1429        let piechart = PieChart::default().show_percentages(false);
1430        assert!(!piechart.show_percentages);
1431    }
1432
1433    #[test]
1434    fn piechart_pie_char() {
1435        let piechart = PieChart::default().pie_char('█');
1436        assert_eq!(piechart.pie_char, '█');
1437    }
1438
1439    #[test]
1440    fn piechart_total_value() {
1441        let slices = vec![
1442            PieSlice::new("A", 30.0, Color::Red),
1443            PieSlice::new("B", 70.0, Color::Blue),
1444        ];
1445        let piechart = PieChart::new(slices);
1446        assert_eq!(piechart.total_value(), 100.0);
1447    }
1448
1449    #[test]
1450    fn piechart_percentage() {
1451        let slices = vec![
1452            PieSlice::new("A", 30.0, Color::Red),
1453            PieSlice::new("B", 70.0, Color::Blue),
1454        ];
1455        let piechart = PieChart::new(slices);
1456        assert_eq!(
1457            piechart.percentage(&PieSlice::new("A", 30.0, Color::Red)),
1458            30.0
1459        );
1460    }
1461
1462    // Render tests - using macros for common patterns
1463    render_empty_test!(piechart_render_empty_area, PieChart::default());
1464
1465    render_with_size_test!(
1466        piechart_render_with_block,
1467        {
1468            let slices = vec![PieSlice::new("Test", 100.0, Color::Red)];
1469            PieChart::new(slices).block(Block::bordered())
1470        },
1471        width: 20,
1472        height: 10
1473    );
1474
1475    render_test!(
1476        piechart_render_basic,
1477        {
1478            let slices = vec![
1479                PieSlice::new("Rust", 45.0, Color::Red),
1480                PieSlice::new("Go", 30.0, Color::Blue),
1481                PieSlice::new("Python", 25.0, Color::Green),
1482            ];
1483            PieChart::new(slices)
1484        },
1485        Rect::new(0, 0, 40, 20)
1486    );
1487
1488    #[test]
1489    fn piechart_styled_trait() {
1490        use ratatui::style::Stylize;
1491        let piechart = PieChart::default().red();
1492        assert_eq!(piechart.style.fg, Some(Color::Red));
1493    }
1494
1495    #[test]
1496    fn piechart_with_multiple_slices() {
1497        let slices = vec![
1498            PieSlice::new("A", 25.0, Color::Red),
1499            PieSlice::new("B", 25.0, Color::Blue),
1500            PieSlice::new("C", 25.0, Color::Green),
1501            PieSlice::new("D", 25.0, Color::Yellow),
1502        ];
1503        let piechart = PieChart::new(slices);
1504        assert_eq!(piechart.total_value(), 100.0);
1505    }
1506
1507    // Using render macro for the visual test
1508    render_with_size_test!(
1509        piechart_multi_slice_render,
1510        {
1511            let slices = vec![
1512                PieSlice::new("A", 25.0, Color::Red),
1513                PieSlice::new("B", 25.0, Color::Blue),
1514                PieSlice::new("C", 25.0, Color::Green),
1515                PieSlice::new("D", 25.0, Color::Yellow),
1516            ];
1517            PieChart::new(slices)
1518        },
1519        width: 50,
1520        height: 30
1521    );
1522
1523    #[test]
1524    fn piechart_zero_values() {
1525        let slices = vec![
1526            PieSlice::new("A", 0.0, Color::Red),
1527            PieSlice::new("B", 0.0, Color::Blue),
1528        ];
1529        let piechart = PieChart::new(slices);
1530        assert_eq!(piechart.total_value(), 0.0);
1531    }
1532
1533    #[test]
1534    fn piechart_method_chaining() {
1535        use ratatui::widgets::Block;
1536
1537        let slices = vec![PieSlice::new("Test", 100.0, Color::Red)];
1538        let piechart = PieChart::new(slices)
1539            .show_legend(true)
1540            .show_percentages(true)
1541            .pie_char('█')
1542            .block(Block::bordered().title("Test"))
1543            .style(Style::default().fg(Color::White));
1544
1545        assert!(piechart.show_legend);
1546        assert!(piechart.show_percentages);
1547        assert_eq!(piechart.pie_char, '█');
1548        assert!(piechart.block.is_some());
1549        assert_eq!(piechart.style.fg, Some(Color::White));
1550    }
1551
1552    #[test]
1553    fn piechart_custom_symbols() {
1554        use crate::symbols;
1555
1556        let piechart = PieChart::default().pie_char(symbols::PIE_CHAR_BLOCK);
1557        assert_eq!(piechart.pie_char, '█');
1558
1559        let piechart = PieChart::default().pie_char(symbols::PIE_CHAR_CIRCLE);
1560        assert_eq!(piechart.pie_char, '◉');
1561
1562        let piechart = PieChart::default().pie_char(symbols::PIE_CHAR_SQUARE);
1563        assert_eq!(piechart.pie_char, '■');
1564    }
1565
1566    #[test]
1567    fn piechart_is_angle_in_slice() {
1568        use std::f64::consts::PI;
1569
1570        // Test angle in range
1571        assert!(PieChart::is_angle_in_slice(PI / 4.0, 0.0, PI / 2.0));
1572
1573        // Test angle outside range
1574        assert!(!PieChart::is_angle_in_slice(PI, 0.0, PI / 2.0));
1575
1576        // Test wrap around
1577        assert!(PieChart::is_angle_in_slice(0.1, 1.5 * PI, 0.5));
1578    }
1579
1580    // --- Resolution ---
1581
1582    matches_test!(
1583        piechart_resolution_standard,
1584        PieChart::default()
1585            .resolution(Resolution::Standard)
1586            .resolution,
1587        Resolution::Standard
1588    );
1589    matches_test!(
1590        piechart_resolution_braille,
1591        PieChart::default()
1592            .resolution(Resolution::Braille)
1593            .resolution,
1594        Resolution::Braille
1595    );
1596    matches_test!(
1597        piechart_high_resolution_true,
1598        PieChart::default().high_resolution(true).resolution,
1599        Resolution::Braille
1600    );
1601    matches_test!(
1602        piechart_high_resolution_false,
1603        PieChart::default().high_resolution(false).resolution,
1604        Resolution::Standard
1605    );
1606
1607    // --- Legend position / layout / alignment setters ---
1608
1609    matches_test!(
1610        piechart_legend_position_left,
1611        PieChart::default()
1612            .legend_position(LegendPosition::Left)
1613            .legend_position,
1614        LegendPosition::Left
1615    );
1616    matches_test!(
1617        piechart_legend_position_right,
1618        PieChart::default()
1619            .legend_position(LegendPosition::Right)
1620            .legend_position,
1621        LegendPosition::Right
1622    );
1623    matches_test!(
1624        piechart_legend_position_top,
1625        PieChart::default()
1626            .legend_position(LegendPosition::Top)
1627            .legend_position,
1628        LegendPosition::Top
1629    );
1630    matches_test!(
1631        piechart_legend_position_bottom,
1632        PieChart::default()
1633            .legend_position(LegendPosition::Bottom)
1634            .legend_position,
1635        LegendPosition::Bottom
1636    );
1637    matches_test!(
1638        piechart_legend_layout_horizontal,
1639        PieChart::default()
1640            .legend_layout(LegendLayout::Horizontal)
1641            .legend_layout,
1642        LegendLayout::Horizontal
1643    );
1644    matches_test!(
1645        piechart_legend_layout_vertical,
1646        PieChart::default()
1647            .legend_layout(LegendLayout::Vertical)
1648            .legend_layout,
1649        LegendLayout::Vertical
1650    );
1651    matches_test!(
1652        piechart_legend_alignment_left,
1653        PieChart::default()
1654            .legend_alignment(LegendAlignment::Left)
1655            .legend_alignment,
1656        LegendAlignment::Left
1657    );
1658    matches_test!(
1659        piechart_legend_alignment_center,
1660        PieChart::default()
1661            .legend_alignment(LegendAlignment::Center)
1662            .legend_alignment,
1663        LegendAlignment::Center
1664    );
1665    matches_test!(
1666        piechart_legend_alignment_right,
1667        PieChart::default()
1668            .legend_alignment(LegendAlignment::Right)
1669            .legend_alignment,
1670        LegendAlignment::Right
1671    );
1672
1673    // --- legend_marker setter ---
1674
1675    #[test]
1676    fn piechart_legend_marker_custom() {
1677        use crate::symbols::LEGEND_MARKER;
1678        let piechart = PieChart::default().legend_marker(LEGEND_MARKER);
1679        assert_eq!(piechart.legend_marker, LEGEND_MARKER);
1680    }
1681
1682    // --- format_legend_text ---
1683
1684    #[test]
1685    fn piechart_format_legend_text_with_percentage() {
1686        let slices = vec![
1687            PieSlice::new("Rust", 50.0, Color::Red),
1688            PieSlice::new("Go", 50.0, Color::Blue),
1689        ];
1690        let piechart = PieChart::new(slices.clone()).show_percentages(true);
1691        let text = piechart.format_legend_text(&slices[0], 100.0, "");
1692        assert!(text.contains("Rust"));
1693        assert!(text.contains("50.0%"));
1694    }
1695
1696    #[test]
1697    fn piechart_format_legend_text_without_percentage() {
1698        let slices = vec![PieSlice::new("Rust", 50.0, Color::Red)];
1699        let piechart = PieChart::new(slices.clone()).show_percentages(false);
1700        let text = piechart.format_legend_text(&slices[0], 100.0, "");
1701        assert!(text.contains("Rust"));
1702        assert!(!text.contains('%'));
1703    }
1704
1705    #[test]
1706    fn piechart_format_legend_text_zero_total() {
1707        let slices = vec![PieSlice::new("X", 0.0, Color::Red)];
1708        let piechart = PieChart::new(slices.clone()).show_percentages(true);
1709        let text = piechart.format_legend_text(&slices[0], 0.0, "");
1710        assert!(text.contains("0.0%"));
1711    }
1712
1713    // --- calculate_aligned_x ---
1714
1715    #[test]
1716    fn piechart_calculate_aligned_x_left() {
1717        let slices = vec![PieSlice::new("A", 100.0, Color::Red)];
1718        let piechart = PieChart::new(slices).legend_alignment(LegendAlignment::Left);
1719        let area = Rect::new(5, 0, 20, 10);
1720        assert_eq!(piechart.calculate_aligned_x(area, 10), 5);
1721    }
1722
1723    #[test]
1724    fn piechart_calculate_aligned_x_center() {
1725        let slices = vec![PieSlice::new("A", 100.0, Color::Red)];
1726        let piechart = PieChart::new(slices).legend_alignment(LegendAlignment::Center);
1727        let area = Rect::new(0, 0, 20, 10);
1728        // (20 - 10) / 2 = 5
1729        assert_eq!(piechart.calculate_aligned_x(area, 10), 5);
1730    }
1731
1732    #[test]
1733    fn piechart_calculate_aligned_x_right() {
1734        let slices = vec![PieSlice::new("A", 100.0, Color::Red)];
1735        let piechart = PieChart::new(slices).legend_alignment(LegendAlignment::Right);
1736        let area = Rect::new(0, 0, 20, 10);
1737        // 20 - 10 = 10
1738        assert_eq!(piechart.calculate_aligned_x(area, 10), 10);
1739    }
1740
1741    // --- Braille render ---
1742
1743    render_test!(
1744        piechart_render_braille,
1745        {
1746            let slices = vec![
1747                PieSlice::new("Rust", 60.0, Color::Red),
1748                PieSlice::new("Go", 40.0, Color::Blue),
1749            ];
1750            PieChart::new(slices).resolution(Resolution::Braille)
1751        },
1752        Rect::new(0, 0, 40, 20)
1753    );
1754
1755    render_test!(
1756        piechart_render_braille_with_legend,
1757        {
1758            let slices = vec![
1759                PieSlice::new("Rust", 60.0, Color::Red),
1760                PieSlice::new("Go", 40.0, Color::Blue),
1761            ];
1762            PieChart::new(slices)
1763                .resolution(Resolution::Braille)
1764                .show_legend(true)
1765        },
1766        Rect::new(0, 0, 40, 20)
1767    );
1768
1769    // --- Legend layout render paths ---
1770
1771    render_test!(
1772        piechart_render_legend_left,
1773        {
1774            let slices = vec![
1775                PieSlice::new("Alpha", 50.0, Color::Red),
1776                PieSlice::new("Beta", 50.0, Color::Blue),
1777            ];
1778            PieChart::new(slices)
1779                .show_legend(true)
1780                .legend_position(LegendPosition::Left)
1781                .legend_layout(LegendLayout::Vertical)
1782        },
1783        Rect::new(0, 0, 60, 20)
1784    );
1785
1786    render_test!(
1787        piechart_render_legend_right,
1788        {
1789            let slices = vec![
1790                PieSlice::new("Alpha", 50.0, Color::Red),
1791                PieSlice::new("Beta", 50.0, Color::Blue),
1792            ];
1793            PieChart::new(slices)
1794                .show_legend(true)
1795                .legend_position(LegendPosition::Right)
1796                .legend_layout(LegendLayout::Vertical)
1797        },
1798        Rect::new(0, 0, 60, 20)
1799    );
1800
1801    render_test!(
1802        piechart_render_legend_top_horizontal,
1803        {
1804            let slices = vec![
1805                PieSlice::new("Alpha", 50.0, Color::Red),
1806                PieSlice::new("Beta", 50.0, Color::Blue),
1807            ];
1808            PieChart::new(slices)
1809                .show_legend(true)
1810                .legend_position(LegendPosition::Top)
1811                .legend_layout(LegendLayout::Horizontal)
1812        },
1813        Rect::new(0, 0, 60, 20)
1814    );
1815
1816    render_test!(
1817        piechart_render_legend_bottom_horizontal,
1818        {
1819            let slices = vec![
1820                PieSlice::new("Alpha", 50.0, Color::Red),
1821                PieSlice::new("Beta", 50.0, Color::Blue),
1822            ];
1823            PieChart::new(slices)
1824                .show_legend(true)
1825                .legend_position(LegendPosition::Bottom)
1826                .legend_layout(LegendLayout::Horizontal)
1827        },
1828        Rect::new(0, 0, 60, 20)
1829    );
1830
1831    render_test!(
1832        piechart_render_legend_top_vertical,
1833        {
1834            let slices = vec![
1835                PieSlice::new("Alpha", 50.0, Color::Red),
1836                PieSlice::new("Beta", 50.0, Color::Blue),
1837            ];
1838            PieChart::new(slices)
1839                .show_legend(true)
1840                .legend_position(LegendPosition::Top)
1841                .legend_layout(LegendLayout::Vertical)
1842        },
1843        Rect::new(0, 0, 60, 20)
1844    );
1845
1846    render_test!(
1847        piechart_render_legend_bottom_vertical,
1848        {
1849            let slices = vec![
1850                PieSlice::new("Alpha", 50.0, Color::Red),
1851                PieSlice::new("Beta", 50.0, Color::Blue),
1852            ];
1853            PieChart::new(slices)
1854                .show_legend(true)
1855                .legend_position(LegendPosition::Bottom)
1856                .legend_layout(LegendLayout::Vertical)
1857        },
1858        Rect::new(0, 0, 60, 20)
1859    );
1860
1861    render_test!(
1862        piechart_render_legend_left_horizontal,
1863        {
1864            let slices = vec![
1865                PieSlice::new("Alpha", 50.0, Color::Red),
1866                PieSlice::new("Beta", 50.0, Color::Blue),
1867            ];
1868            PieChart::new(slices)
1869                .show_legend(true)
1870                .legend_position(LegendPosition::Left)
1871                .legend_layout(LegendLayout::Horizontal)
1872        },
1873        Rect::new(0, 0, 60, 20)
1874    );
1875
1876    render_test!(
1877        piechart_render_legend_right_horizontal,
1878        {
1879            let slices = vec![
1880                PieSlice::new("Alpha", 50.0, Color::Red),
1881                PieSlice::new("Beta", 50.0, Color::Blue),
1882            ];
1883            PieChart::new(slices)
1884                .show_legend(true)
1885                .legend_position(LegendPosition::Right)
1886                .legend_layout(LegendLayout::Horizontal)
1887        },
1888        Rect::new(0, 0, 60, 20)
1889    );
1890
1891    // --- Legend with percentages ---
1892
1893    render_test!(
1894        piechart_render_legend_with_percentages,
1895        {
1896            let slices = vec![
1897                PieSlice::new("Rust", 45.0, Color::Red),
1898                PieSlice::new("Go", 30.0, Color::Blue),
1899                PieSlice::new("Python", 25.0, Color::Green),
1900            ];
1901            PieChart::new(slices)
1902                .show_legend(true)
1903                .show_percentages(true)
1904        },
1905        Rect::new(0, 0, 60, 20)
1906    );
1907
1908    // Legend alignment render paths
1909    render_test!(
1910        piechart_render_legend_alignment_center,
1911        {
1912            let slices = vec![
1913                PieSlice::new("A", 50.0, Color::Red),
1914                PieSlice::new("B", 50.0, Color::Blue),
1915            ];
1916            PieChart::new(slices)
1917                .show_legend(true)
1918                .legend_alignment(LegendAlignment::Center)
1919        },
1920        Rect::new(0, 0, 60, 20)
1921    );
1922
1923    render_test!(
1924        piechart_render_legend_alignment_right,
1925        {
1926            let slices = vec![
1927                PieSlice::new("A", 50.0, Color::Red),
1928                PieSlice::new("B", 50.0, Color::Blue),
1929            ];
1930            PieChart::new(slices)
1931                .show_legend(true)
1932                .legend_alignment(LegendAlignment::Right)
1933        },
1934        Rect::new(0, 0, 60, 20)
1935    );
1936
1937    // --- calculate_layout: small area returns no legend ---
1938
1939    #[test]
1940    fn piechart_layout_too_small_no_legend() {
1941        let slices = vec![PieSlice::new("A", 100.0, Color::Red)];
1942        let piechart = PieChart::new(slices).show_legend(true);
1943        // area too narrow (< 20)
1944        let area = Rect::new(0, 0, 10, 5);
1945        let (pie_area, legend_opt) = piechart.calculate_layout(area);
1946        assert_eq!(pie_area, area);
1947        assert!(legend_opt.is_none());
1948    }
1949
1950    #[test]
1951    fn piechart_layout_show_legend_false_no_legend() {
1952        let slices = vec![PieSlice::new("A", 100.0, Color::Red)];
1953        let piechart = PieChart::new(slices).show_legend(false);
1954        let area = Rect::new(0, 0, 60, 20);
1955        let (pie_area, legend_opt) = piechart.calculate_layout(area);
1956        assert_eq!(pie_area, area);
1957        assert!(legend_opt.is_none());
1958    }
1959
1960    // --- Render with no slices (empty) ---
1961
1962    render_empty_test!(piechart_render_empty_slices, PieChart::default());
1963
1964    // --- Render single slice ---
1965
1966    render_test!(
1967        piechart_render_single_slice,
1968        PieChart::new(vec![PieSlice::new("Only", 100.0, Color::Cyan)]),
1969        Rect::new(0, 0, 30, 15)
1970    );
1971
1972    // --- Percentage zero total ---
1973
1974    #[test]
1975    fn piechart_percentage_zero_total() {
1976        let slices = vec![PieSlice::new("A", 0.0, Color::Red)];
1977        let piechart = PieChart::new(slices.clone());
1978        assert_eq!(piechart.percentage(&slices[0]), 0.0);
1979    }
1980
1981    // --- calculate_legend_width Vertical: zero total with percentages (line 1041) ---
1982    // Must call the private method directly: render_piechart returns early when total == 0,
1983    // so the zero-total branch inside calculate_legend_width is only reachable this way.
1984
1985    #[test]
1986    fn piechart_calculate_legend_width_vertical_zero_total_with_percentages() {
1987        let slices = vec![
1988            PieSlice::new("A", 0.0, Color::Red),
1989            PieSlice::new("B", 0.0, Color::Blue),
1990        ];
1991        let piechart = PieChart::new(slices)
1992            .legend_layout(LegendLayout::Vertical)
1993            .show_percentages(true);
1994        let width = piechart.calculate_legend_width();
1995        // Each label shows "• A 0.0%  " — width must be > 0
1996        assert!(width > 0);
1997    }
1998
1999    render_test!(
2000        piechart_render_legend_vertical_zero_total_with_percentages,
2001        PieChart::new(vec![
2002            PieSlice::new("A", 0.0, Color::Red),
2003            PieSlice::new("B", 0.0, Color::Blue),
2004        ])
2005        .show_legend(true)
2006        .show_percentages(true)
2007        .legend_position(LegendPosition::Right)
2008        .legend_layout(LegendLayout::Vertical),
2009        Rect::new(0, 0, 60, 20)
2010    );
2011
2012    // --- calculate_legend_width Vertical: show_percentages=false (line 1045) ---
2013
2014    render_test!(
2015        piechart_render_legend_vertical_no_percentages,
2016        PieChart::new(vec![
2017            PieSlice::new("Rust", 60.0, Color::Red),
2018            PieSlice::new("Go", 40.0, Color::Blue),
2019        ])
2020        .show_legend(true)
2021        .show_percentages(false)
2022        .legend_position(LegendPosition::Right)
2023        .legend_layout(LegendLayout::Vertical),
2024        Rect::new(0, 0, 60, 20)
2025    );
2026
2027    // --- render_piechart: total <= 0.0 guard (line 631) ---
2028
2029    render_test!(
2030        piechart_render_all_zero_values,
2031        PieChart::new(vec![
2032            PieSlice::new("A", 0.0, Color::Red),
2033            PieSlice::new("B", 0.0, Color::Blue),
2034        ]),
2035        Rect::new(0, 0, 40, 20)
2036    );
2037
2038    // --- render_slice: percent <= 0.0 guard (line 691) ---
2039
2040    render_test!(
2041        piechart_render_zero_value_slice_in_mix,
2042        PieChart::new(vec![
2043            PieSlice::new("A", 100.0, Color::Red),
2044            PieSlice::new("B", 0.0, Color::Blue),
2045        ]),
2046        Rect::new(0, 0, 40, 20)
2047    );
2048
2049    // --- render_slice: radius == 0 guard — tiny 1×1 area (line 691) ---
2050
2051    render_test!(
2052        piechart_render_tiny_area_radius_zero,
2053        PieChart::new(vec![PieSlice::new("A", 100.0, Color::Red)]),
2054        Rect::new(0, 0, 1, 1)
2055    );
2056
2057    // --- render_vertical_legend: overflow break (line 814) ---
2058    // Many slices in a tall-enough outer area but tiny legend height forces the break.
2059
2060    render_test!(
2061        piechart_render_vertical_legend_overflow,
2062        PieChart::new(vec![
2063            PieSlice::new("Slice1", 10.0, Color::Red),
2064            PieSlice::new("Slice2", 10.0, Color::Blue),
2065            PieSlice::new("Slice3", 10.0, Color::Green),
2066            PieSlice::new("Slice4", 10.0, Color::Yellow),
2067            PieSlice::new("Slice5", 10.0, Color::Cyan),
2068            PieSlice::new("Slice6", 10.0, Color::Magenta),
2069            PieSlice::new("Slice7", 10.0, Color::White),
2070            PieSlice::new("Slice8", 10.0, Color::Red),
2071            PieSlice::new("Slice9", 10.0, Color::Blue),
2072            PieSlice::new("Slice10", 10.0, Color::Green),
2073        ])
2074        .show_legend(true)
2075        .legend_position(LegendPosition::Right)
2076        .legend_layout(LegendLayout::Vertical),
2077        // height=12 → legend area height ≈ 10 → only 5 items fit (each takes 2 rows)
2078        Rect::new(0, 0, 60, 12)
2079    );
2080
2081    // --- render_horizontal_legend: overflow break (line 854) ---
2082    // Many slices with a very narrow area forces the x_offset >= width break.
2083
2084    render_test!(
2085        piechart_render_horizontal_legend_overflow,
2086        PieChart::new(vec![
2087            PieSlice::new("LongLabelA", 20.0, Color::Red),
2088            PieSlice::new("LongLabelB", 20.0, Color::Blue),
2089            PieSlice::new("LongLabelC", 20.0, Color::Green),
2090            PieSlice::new("LongLabelD", 20.0, Color::Yellow),
2091            PieSlice::new("LongLabelE", 20.0, Color::Cyan),
2092        ])
2093        .show_legend(true)
2094        .legend_position(LegendPosition::Bottom)
2095        .legend_layout(LegendLayout::Horizontal),
2096        // width=22 is very narrow relative to the label sizes
2097        Rect::new(0, 0, 22, 15)
2098    );
2099
2100    // --- layout_horizontal_split: early return when area.width <= legend_width (line 988) ---
2101
2102    #[test]
2103    fn piechart_layout_horizontal_split_too_narrow() {
2104        let area = Rect::new(0, 0, 5, 20);
2105        // legend_width (100) > area.width (5) → early return
2106        let (pie_area, legend_opt) = PieChart::<'_>::layout_horizontal_split(area, 100, true);
2107        assert_eq!(pie_area, area);
2108        assert!(legend_opt.is_none());
2109    }
2110
2111    // --- layout_vertical_split: early return when area.height <= legend_height (line 1041) ---
2112
2113    #[test]
2114    fn piechart_layout_vertical_split_too_short() {
2115        let area = Rect::new(0, 0, 60, 3);
2116        // legend_height (100) > area.height (3) → early return
2117        let (pie_area, legend_opt) = PieChart::<'_>::layout_vertical_split(area, 100, true);
2118        assert_eq!(pie_area, area);
2119        assert!(legend_opt.is_none());
2120    }
2121
2122    // --- calculate_legend_width: LegendLayout::Horizontal branch (lines 1057-1076) ---
2123    // This branch is only reachable by calling the method directly in tests.
2124
2125    #[test]
2126    fn piechart_calculate_legend_width_horizontal_with_percentages() {
2127        let slices = vec![
2128            PieSlice::new("Rust", 60.0, Color::Red),
2129            PieSlice::new("Go", 40.0, Color::Blue),
2130        ];
2131        let piechart = PieChart::new(slices)
2132            .legend_layout(LegendLayout::Horizontal)
2133            .show_percentages(true);
2134        let width = piechart.calculate_legend_width();
2135        assert!(width > 0);
2136    }
2137
2138    #[test]
2139    fn piechart_calculate_legend_width_horizontal_without_percentages() {
2140        let slices = vec![
2141            PieSlice::new("Alpha", 50.0, Color::Red),
2142            PieSlice::new("Beta", 50.0, Color::Blue),
2143        ];
2144        let piechart = PieChart::new(slices)
2145            .legend_layout(LegendLayout::Horizontal)
2146            .show_percentages(false);
2147        let width = piechart.calculate_legend_width();
2148        assert!(width > 0);
2149    }
2150
2151    #[test]
2152    fn piechart_calculate_legend_width_horizontal_zero_total() {
2153        let slices = vec![
2154            PieSlice::new("A", 0.0, Color::Red),
2155            PieSlice::new("B", 0.0, Color::Blue),
2156        ];
2157        let piechart = PieChart::new(slices)
2158            .legend_layout(LegendLayout::Horizontal)
2159            .show_percentages(true);
2160        let width = piechart.calculate_legend_width();
2161        // Zero total → 0.0% for each item, but width is still > 0
2162        assert!(width > 0);
2163    }
2164
2165    // --- calculate_legend_horizontal_width: zero total branch (lines 1090, 1094) ---
2166
2167    #[test]
2168    fn piechart_calculate_legend_horizontal_width_zero_total() {
2169        let slices = vec![
2170            PieSlice::new("X", 0.0, Color::Red),
2171            PieSlice::new("Y", 0.0, Color::Blue),
2172        ];
2173        let piechart = PieChart::new(slices).show_percentages(true);
2174        let width = piechart.calculate_legend_horizontal_width();
2175        assert!(width > 0);
2176    }
2177
2178    #[test]
2179    fn piechart_calculate_legend_horizontal_width_without_percentages() {
2180        let slices = vec![PieSlice::new("Item", 100.0, Color::Green)];
2181        let piechart = PieChart::new(slices).show_percentages(false);
2182        let width = piechart.calculate_legend_horizontal_width();
2183        assert!(width > 0);
2184    }
2185
2186    // --- calculate_vertical_grid_height: very narrow width clamps columns to 1 ---
2187
2188    #[test]
2189    fn piechart_calculate_vertical_grid_height_narrow() {
2190        let slices = vec![
2191            PieSlice::new("A", 50.0, Color::Red),
2192            PieSlice::new("B", 50.0, Color::Blue),
2193        ];
2194        let piechart = PieChart::new(slices);
2195        // width=1 → columns clamped to 1 → all items stack in one column
2196        let height = piechart.calculate_vertical_grid_height(1);
2197        assert!(height >= 4);
2198    }
2199
2200    // --- value_percent / slice_angles helpers ---
2201
2202    #[test]
2203    fn piechart_value_percent_positive_total() {
2204        assert_eq!(PieChart::value_percent(25.0, 200.0), 12.5);
2205    }
2206
2207    #[test]
2208    fn piechart_value_percent_zero_total() {
2209        assert_eq!(PieChart::value_percent(25.0, 0.0), 0.0);
2210    }
2211
2212    #[test]
2213    fn piechart_value_percent_negative_total() {
2214        // Non-positive totals are treated as empty to avoid nonsensical output.
2215        assert_eq!(PieChart::value_percent(25.0, -5.0), 0.0);
2216    }
2217
2218    #[test]
2219    fn piechart_slice_angles_full_circle_flag() {
2220        let (_, _, is_full) = PieChart::slice_angles(0.0, 100.0);
2221        assert!(is_full);
2222    }
2223
2224    #[test]
2225    fn piechart_slice_angles_partial_not_full() {
2226        let (start, end, is_full) = PieChart::slice_angles(0.0, 50.0);
2227        assert!(!is_full);
2228        assert!((end - start - PI).abs() < 1e-9);
2229    }
2230
2231    #[test]
2232    fn piechart_legend_item_width_matches_text() {
2233        let slices = vec![PieSlice::new("Rust", 50.0, Color::Red)];
2234        let chart = PieChart::new(slices.clone()).show_percentages(true);
2235        let expected = chart
2236            .format_legend_text(&slices[0], 100.0, "  ")
2237            .chars()
2238            .count();
2239        assert_eq!(
2240            usize::from(chart.legend_item_width(&slices[0], 100.0)),
2241            expected
2242        );
2243    }
2244
2245    #[test]
2246    fn piechart_legend_item_width_unicode_label() {
2247        // Multi-byte labels must be measured by character count, not byte length.
2248        let slices = vec![PieSlice::new("日本語", 100.0, Color::Red)];
2249        let chart = PieChart::new(slices.clone()).show_percentages(false);
2250        // "■ 日本語  " => marker(1) + space(1) + 3 chars + 2 trailing spaces = 7
2251        assert_eq!(chart.legend_item_width(&slices[0], 100.0), 7);
2252    }
2253
2254    // --- Full circle rendering (regression for issue #2) ---
2255
2256    #[test]
2257    fn piechart_full_circle_fills_more_than_a_line() {
2258        fn filled_cells(chart: &PieChart) -> usize {
2259            let mut buffer = Buffer::empty(Rect::new(0, 0, 30, 15));
2260            Widget::render(chart, buffer.area, &mut buffer);
2261            buffer
2262                .content
2263                .iter()
2264                .filter(|c| !c.symbol().trim().is_empty())
2265                .count()
2266        }
2267
2268        let standard = PieChart::new(vec![PieSlice::new("Only", 100.0, Color::Green)])
2269            .show_legend(false)
2270            .show_percentages(false);
2271        assert!(filled_cells(&standard) > 30);
2272
2273        let braille = PieChart::new(vec![PieSlice::new("Only", 100.0, Color::Green)])
2274            .resolution(Resolution::Braille)
2275            .show_legend(false)
2276            .show_percentages(false);
2277        assert!(filled_cells(&braille) > 30);
2278    }
2279}