Skip to main content

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}