plotkit_core/charts/pie.rs
1//! Pie chart builder methods.
2//!
3//! Provides a fluent builder API for configuring [`PieArtist`] instances.
4//! Each method returns `&mut Self`, allowing calls to be chained together
5//! for concise, readable chart construction.
6
7use crate::artist::PieArtist;
8use crate::primitives::Color;
9
10impl PieArtist {
11 /// Sets the wedge labels displayed next to each slice.
12 ///
13 /// The number of labels should match the number of wedge sizes. If
14 /// fewer labels are provided, extra wedges will have no label.
15 ///
16 /// # Arguments
17 ///
18 /// * `labels` - A vector of label strings, one per wedge.
19 ///
20 /// # Examples
21 ///
22 /// ```
23 /// # use plotkit_core::artist::PieArtist;
24 /// let mut pie = PieArtist {
25 /// sizes: vec![1.0, 2.0, 3.0],
26 /// labels: None,
27 /// colors: None,
28 /// explode: None,
29 /// autopct: false,
30 /// start_angle: 90.0,
31 /// radius: 1.0,
32 /// label: None,
33 /// color: plotkit_core::primitives::Color::TAB_BLUE,
34 /// };
35 /// pie.labels(vec!["A", "B", "C"]);
36 /// assert_eq!(pie.labels.as_ref().unwrap().len(), 3);
37 /// ```
38 pub fn labels(&mut self, labels: Vec<&str>) -> &mut Self {
39 self.labels = Some(labels.into_iter().map(String::from).collect());
40 self
41 }
42
43 /// Sets custom colors for each wedge.
44 ///
45 /// When not set, the theme color cycle is used automatically.
46 ///
47 /// # Arguments
48 ///
49 /// * `colors` - A vector of [`Color`] values, one per wedge.
50 ///
51 /// # Examples
52 ///
53 /// ```
54 /// # use plotkit_core::artist::PieArtist;
55 /// # use plotkit_core::primitives::Color;
56 /// let mut pie = PieArtist {
57 /// sizes: vec![1.0, 2.0],
58 /// labels: None,
59 /// colors: None,
60 /// explode: None,
61 /// autopct: false,
62 /// start_angle: 90.0,
63 /// radius: 1.0,
64 /// label: None,
65 /// color: Color::TAB_BLUE,
66 /// };
67 /// pie.colors(vec![Color::TAB_RED, Color::TAB_GREEN]);
68 /// assert_eq!(pie.colors.as_ref().unwrap().len(), 2);
69 /// ```
70 pub fn colors(&mut self, colors: Vec<Color>) -> &mut Self {
71 self.colors = Some(colors);
72 self
73 }
74
75 /// Sets the explode offsets for each wedge.
76 ///
77 /// Each value represents the fraction of the radius by which the
78 /// corresponding wedge is offset from the center. A value of `0.0`
79 /// means no offset; `0.1` pushes the wedge outward by 10% of the
80 /// radius.
81 ///
82 /// # Arguments
83 ///
84 /// * `explode` - A vector of offset fractions, one per wedge.
85 ///
86 /// # Examples
87 ///
88 /// ```
89 /// # use plotkit_core::artist::PieArtist;
90 /// # use plotkit_core::primitives::Color;
91 /// let mut pie = PieArtist {
92 /// sizes: vec![1.0, 2.0, 3.0],
93 /// labels: None,
94 /// colors: None,
95 /// explode: None,
96 /// autopct: false,
97 /// start_angle: 90.0,
98 /// radius: 1.0,
99 /// label: None,
100 /// color: Color::TAB_BLUE,
101 /// };
102 /// pie.explode(vec![0.1, 0.0, 0.0]);
103 /// assert_eq!(pie.explode.as_ref().unwrap()[0], 0.1);
104 /// ```
105 pub fn explode(&mut self, explode: Vec<f64>) -> &mut Self {
106 self.explode = Some(explode);
107 self
108 }
109
110 /// Enables or disables automatic percentage labels on each wedge.
111 ///
112 /// When enabled, each wedge displays its percentage of the total as
113 /// text positioned at the midpoint of the wedge arc.
114 ///
115 /// # Arguments
116 ///
117 /// * `show` - `true` to show percentage labels, `false` to hide them.
118 ///
119 /// # Examples
120 ///
121 /// ```
122 /// # use plotkit_core::artist::PieArtist;
123 /// # use plotkit_core::primitives::Color;
124 /// let mut pie = PieArtist {
125 /// sizes: vec![1.0, 2.0],
126 /// labels: None,
127 /// colors: None,
128 /// explode: None,
129 /// autopct: false,
130 /// start_angle: 90.0,
131 /// radius: 1.0,
132 /// label: None,
133 /// color: Color::TAB_BLUE,
134 /// };
135 /// pie.autopct(true);
136 /// assert!(pie.autopct);
137 /// ```
138 pub fn autopct(&mut self, show: bool) -> &mut Self {
139 self.autopct = show;
140 self
141 }
142
143 /// Sets the starting angle for the first wedge in degrees.
144 ///
145 /// The default is `90.0` (top of the circle). Angles increase
146 /// counter-clockwise.
147 ///
148 /// # Arguments
149 ///
150 /// * `angle` - The starting angle in degrees.
151 ///
152 /// # Examples
153 ///
154 /// ```
155 /// # use plotkit_core::artist::PieArtist;
156 /// # use plotkit_core::primitives::Color;
157 /// let mut pie = PieArtist {
158 /// sizes: vec![1.0, 2.0],
159 /// labels: None,
160 /// colors: None,
161 /// explode: None,
162 /// autopct: false,
163 /// start_angle: 90.0,
164 /// radius: 1.0,
165 /// label: None,
166 /// color: Color::TAB_BLUE,
167 /// };
168 /// pie.start_angle(0.0);
169 /// assert!((pie.start_angle - 0.0).abs() < f64::EPSILON);
170 /// ```
171 pub fn start_angle(&mut self, angle: f64) -> &mut Self {
172 self.start_angle = angle;
173 self
174 }
175
176 /// Sets the radius of the pie chart in data-space units.
177 ///
178 /// The default radius is `1.0`. The pie is drawn in a square
179 /// coordinate space that accommodates the radius plus any explode
180 /// offsets.
181 ///
182 /// # Arguments
183 ///
184 /// * `radius` - The radius of the pie.
185 ///
186 /// # Examples
187 ///
188 /// ```
189 /// # use plotkit_core::artist::PieArtist;
190 /// # use plotkit_core::primitives::Color;
191 /// let mut pie = PieArtist {
192 /// sizes: vec![1.0, 2.0],
193 /// labels: None,
194 /// colors: None,
195 /// explode: None,
196 /// autopct: false,
197 /// start_angle: 90.0,
198 /// radius: 1.0,
199 /// label: None,
200 /// color: Color::TAB_BLUE,
201 /// };
202 /// pie.radius(0.8);
203 /// assert!((pie.radius - 0.8).abs() < f64::EPSILON);
204 /// ```
205 pub fn radius(&mut self, radius: f64) -> &mut Self {
206 self.radius = radius;
207 self
208 }
209
210 /// Sets the legend label for the pie chart.
211 ///
212 /// When a legend is displayed on the figure, this label will appear
213 /// next to the color swatch for this pie chart.
214 ///
215 /// # Arguments
216 ///
217 /// * `label` - A string slice that will be stored as the legend entry.
218 ///
219 /// # Examples
220 ///
221 /// ```
222 /// # use plotkit_core::artist::PieArtist;
223 /// # use plotkit_core::primitives::Color;
224 /// let mut pie = PieArtist {
225 /// sizes: vec![1.0, 2.0],
226 /// labels: None,
227 /// colors: None,
228 /// explode: None,
229 /// autopct: false,
230 /// start_angle: 90.0,
231 /// radius: 1.0,
232 /// label: None,
233 /// color: Color::TAB_BLUE,
234 /// };
235 /// pie.label("pie chart");
236 /// assert_eq!(pie.label.as_deref(), Some("pie chart"));
237 /// ```
238 pub fn label(&mut self, label: &str) -> &mut Self {
239 self.label = Some(label.to_string());
240 self
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use crate::artist::PieArtist;
247 use crate::primitives::Color;
248
249 fn sample_pie() -> PieArtist {
250 PieArtist {
251 sizes: vec![35.0, 25.0, 20.0, 15.0, 5.0],
252 labels: None,
253 colors: None,
254 explode: None,
255 autopct: false,
256 start_angle: 90.0,
257 radius: 1.0,
258 label: None,
259 color: Color::TAB_BLUE,
260 }
261 }
262
263 #[test]
264 fn builder_labels() {
265 let mut p = sample_pie();
266 p.labels(vec!["A", "B", "C", "D", "E"]);
267 assert_eq!(p.labels.as_ref().unwrap().len(), 5);
268 assert_eq!(p.labels.as_ref().unwrap()[0], "A");
269 }
270
271 #[test]
272 fn builder_colors() {
273 let mut p = sample_pie();
274 p.colors(vec![Color::TAB_RED, Color::TAB_GREEN]);
275 assert_eq!(p.colors.as_ref().unwrap().len(), 2);
276 }
277
278 #[test]
279 fn builder_explode() {
280 let mut p = sample_pie();
281 p.explode(vec![0.1, 0.0, 0.0, 0.0, 0.0]);
282 assert!((p.explode.as_ref().unwrap()[0] - 0.1).abs() < f64::EPSILON);
283 assert!((p.explode.as_ref().unwrap()[1] - 0.0).abs() < f64::EPSILON);
284 }
285
286 #[test]
287 fn builder_autopct() {
288 let mut p = sample_pie();
289 assert!(!p.autopct);
290 p.autopct(true);
291 assert!(p.autopct);
292 }
293
294 #[test]
295 fn builder_start_angle() {
296 let mut p = sample_pie();
297 p.start_angle(45.0);
298 assert!((p.start_angle - 45.0).abs() < f64::EPSILON);
299 }
300
301 #[test]
302 fn builder_radius() {
303 let mut p = sample_pie();
304 p.radius(0.5);
305 assert!((p.radius - 0.5).abs() < f64::EPSILON);
306 }
307
308 #[test]
309 fn builder_label() {
310 let mut p = sample_pie();
311 p.label("my pie");
312 assert_eq!(p.label.as_deref(), Some("my pie"));
313 }
314
315 #[test]
316 fn builder_chaining() {
317 let mut p = sample_pie();
318 p.labels(vec!["A", "B", "C", "D", "E"])
319 .autopct(true)
320 .explode(vec![0.05, 0.0, 0.0, 0.0, 0.0])
321 .start_angle(0.0)
322 .radius(0.9)
323 .label("chained");
324 assert!(p.autopct);
325 assert!((p.start_angle - 0.0).abs() < f64::EPSILON);
326 assert!((p.radius - 0.9).abs() < f64::EPSILON);
327 assert_eq!(p.label.as_deref(), Some("chained"));
328 assert_eq!(p.labels.as_ref().unwrap()[0], "A");
329 }
330
331 #[test]
332 fn data_bounds_default() {
333 let p = sample_pie();
334 let (xmin, xmax, ymin, ymax) = p.data_bounds();
335 assert!((xmin - (-1.1)).abs() < f64::EPSILON);
336 assert!((xmax - 1.1).abs() < f64::EPSILON);
337 assert!((ymin - (-1.1)).abs() < f64::EPSILON);
338 assert!((ymax - 1.1).abs() < f64::EPSILON);
339 }
340
341 #[test]
342 fn data_bounds_custom_radius() {
343 let mut p = sample_pie();
344 p.radius(2.0);
345 let (xmin, xmax, ymin, ymax) = p.data_bounds();
346 assert!((xmin - (-2.2)).abs() < f64::EPSILON);
347 assert!((xmax - 2.2).abs() < f64::EPSILON);
348 assert!((ymin - (-2.2)).abs() < f64::EPSILON);
349 assert!((ymax - 2.2).abs() < f64::EPSILON);
350 }
351
352 #[test]
353 fn data_bounds_with_explode() {
354 let mut p = sample_pie();
355 p.explode(vec![0.2, 0.0, 0.0, 0.0, 0.0]);
356 let (xmin, xmax, ymin, ymax) = p.data_bounds();
357 // max_explode = 0.2, extent = 1.0 * (1.0 + 0.2) + 0.1 = 1.3
358 assert!((xmin - (-1.3)).abs() < f64::EPSILON);
359 assert!((xmax - 1.3).abs() < f64::EPSILON);
360 assert!((ymin - (-1.3)).abs() < f64::EPSILON);
361 assert!((ymax - 1.3).abs() < f64::EPSILON);
362 }
363
364 #[test]
365 fn data_bounds_empty_sizes() {
366 let p = PieArtist {
367 sizes: vec![],
368 labels: None,
369 colors: None,
370 explode: None,
371 autopct: false,
372 start_angle: 90.0,
373 radius: 1.0,
374 label: None,
375 color: Color::TAB_BLUE,
376 };
377 let (xmin, xmax, ymin, ymax) = p.data_bounds();
378 assert!((xmin - (-1.1)).abs() < f64::EPSILON);
379 assert!((xmax - 1.1).abs() < f64::EPSILON);
380 assert!((ymin - (-1.1)).abs() < f64::EPSILON);
381 assert!((ymax - 1.1).abs() < f64::EPSILON);
382 }
383}