chartml_core/shapes/
pie.rs1use std::f64::consts::PI;
4
5#[derive(Debug, Clone)]
7pub struct PieSlice {
8 pub index: usize,
10 pub value: f64,
12 pub start_angle: f64,
14 pub end_angle: f64,
16}
17
18pub struct PieLayout {
20 start_angle: f64,
21 end_angle: f64,
22 sort: bool,
23}
24
25impl PieLayout {
26 pub fn new() -> Self {
28 Self {
29 start_angle: 0.0,
30 end_angle: 2.0 * PI,
31 sort: false,
32 }
33 }
34
35 pub fn start_angle(mut self, angle: f64) -> Self {
37 self.start_angle = angle;
38 self
39 }
40
41 pub fn end_angle(mut self, angle: f64) -> Self {
43 self.end_angle = angle;
44 self
45 }
46
47 pub fn sort(mut self, sort: bool) -> Self {
49 self.sort = sort;
50 self
51 }
52
53 pub fn layout(&self, values: &[f64]) -> Vec<PieSlice> {
56 if values.is_empty() {
57 return Vec::new();
58 }
59
60 let total: f64 = values.iter().sum();
61 let angle_span = self.end_angle - self.start_angle;
62
63 let mut indexed: Vec<(usize, f64)> = values.iter().enumerate().map(|(i, &v)| (i, v)).collect();
65
66 if self.sort {
67 indexed.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
68 }
69
70 let mut slices = Vec::with_capacity(indexed.len());
71 let mut current_angle = self.start_angle;
72
73 for &(index, value) in &indexed {
74 let slice_angle = if total > 0.0 {
75 value / total * angle_span
76 } else {
77 0.0
78 };
79 let start = current_angle;
80 let end = current_angle + slice_angle;
81 slices.push(PieSlice {
82 index,
83 value,
84 start_angle: start,
85 end_angle: end,
86 });
87 current_angle = end;
88 }
89
90 slices
91 }
92}
93
94impl Default for PieLayout {
95 fn default() -> Self {
96 Self::new()
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 #![allow(clippy::unwrap_used)]
103 use super::*;
104
105 #[test]
106 fn pie_basic() {
107 let layout = PieLayout::new();
108 let slices = layout.layout(&[1.0, 2.0, 3.0]);
109 assert_eq!(slices.len(), 3);
110 let total_angle: f64 = slices.iter().map(|s| s.end_angle - s.start_angle).sum();
112 assert!((total_angle - 2.0 * PI).abs() < 1e-10, "Total angle should be 2*PI, got: {}", total_angle);
113 let first_angle = slices[0].end_angle - slices[0].start_angle;
115 assert!((first_angle - 2.0 * PI / 6.0).abs() < 1e-10);
116 }
117
118 #[test]
119 fn pie_single_value() {
120 let layout = PieLayout::new();
121 let slices = layout.layout(&[1.0]);
122 assert_eq!(slices.len(), 1);
123 assert!((slices[0].start_angle - 0.0).abs() < 1e-10);
124 assert!((slices[0].end_angle - 2.0 * PI).abs() < 1e-10);
125 }
126
127 #[test]
128 fn pie_equal_values() {
129 let layout = PieLayout::new();
130 let slices = layout.layout(&[1.0, 1.0, 1.0]);
131 assert_eq!(slices.len(), 3);
132 let expected_angle = 2.0 * PI / 3.0;
133 for slice in &slices {
134 let angle = slice.end_angle - slice.start_angle;
135 assert!((angle - expected_angle).abs() < 1e-10, "Each slice should be 2*PI/3, got: {}", angle);
136 }
137 }
138
139 #[test]
140 fn pie_sorted_descending() {
141 let layout = PieLayout::new().sort(true);
142 let slices = layout.layout(&[1.0, 3.0, 2.0]);
143 assert_eq!(slices.len(), 3);
144 assert_eq!(slices[0].index, 1);
146 assert!((slices[0].value - 3.0).abs() < 1e-10);
147 assert_eq!(slices[1].index, 2);
149 assert!((slices[1].value - 2.0).abs() < 1e-10);
150 assert_eq!(slices[2].index, 0);
152 }
153
154 #[test]
155 fn pie_custom_angle_range() {
156 let layout = PieLayout::new().start_angle(0.0).end_angle(PI);
158 let slices = layout.layout(&[1.0, 1.0]);
159 assert_eq!(slices.len(), 2);
160 let total_angle: f64 = slices.iter().map(|s| s.end_angle - s.start_angle).sum();
161 assert!((total_angle - PI).abs() < 1e-10, "Total angle should be PI, got: {}", total_angle);
162 let each = PI / 2.0;
164 assert!((slices[0].end_angle - slices[0].start_angle - each).abs() < 1e-10);
165 }
166
167 #[test]
168 fn pie_with_zero() {
169 let layout = PieLayout::new();
170 let slices = layout.layout(&[0.0, 1.0, 2.0]);
171 assert_eq!(slices.len(), 3);
172 assert!((slices[0].end_angle - slices[0].start_angle).abs() < 1e-10,
174 "Zero-value slice should have zero angle span");
175 }
176}