1use crate::{
2 element::{Drawable, PointCollection},
3 style::{IntoFont, RGBColor, TextStyle, BLACK},
4};
5use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
6use std::{error::Error, f64::consts::PI, fmt::Display};
7
8#[derive(Debug)]
9enum PieError {
10 LengthMismatch,
11}
12impl Display for PieError {
13 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
14 match self {
15 &PieError::LengthMismatch => write!(f, "Length Mismatch"),
16 }
17 }
18}
19
20impl Error for PieError {}
21
22pub struct Pie<'a, Coord, Label: Display> {
24 center: &'a Coord, radius: &'a f64,
26 sizes: &'a [f64],
27 colors: &'a [RGBColor],
28 labels: &'a [Label],
29 total: f64,
30 start_radian: f64,
31 label_style: TextStyle<'a>,
32 label_offset: f64,
33 percentage_style: Option<TextStyle<'a>>,
34 donut_hole: f64, }
36
37impl<'a, Label: Display> Pie<'a, (i32, i32), Label> {
38 pub fn new(
41 center: &'a (i32, i32),
42 radius: &'a f64,
43 sizes: &'a [f64],
44 colors: &'a [RGBColor],
45 labels: &'a [Label],
46 ) -> Self {
47 let total = sizes.iter().sum();
49
50 let radius_5pct = radius * 0.05;
52
53 let label_style = TextStyle::from(("sans-serif", radius_5pct).into_font()).color(&BLACK);
55 Self {
56 center,
57 radius,
58 sizes,
59 colors,
60 labels,
61 total,
62 start_radian: 0.0,
63 label_style,
64 label_offset: radius_5pct,
65 percentage_style: None,
66 donut_hole: 0.0,
67 }
68 }
69
70 pub fn start_angle(&mut self, start_angle: f64) {
78 self.start_radian = start_angle.to_radians();
80 }
81
82 pub fn label_style<T: Into<TextStyle<'a>>>(&mut self, label_style: T) {
84 self.label_style = label_style.into();
85 }
86
87 pub fn label_offset(&mut self, offset_to_radius: f64) {
89 self.label_offset = offset_to_radius
90 }
91
92 pub fn percentages<T: Into<TextStyle<'a>>>(&mut self, label_style: T) {
94 self.percentage_style = Some(label_style.into());
95 }
96
97 pub fn donut_hole(&mut self, hole_radius: f64) {
101 if hole_radius > 0.0 && hole_radius < *self.radius {
102 self.donut_hole = hole_radius;
103 }
104 }
105}
106
107impl<'a, DB: DrawingBackend, Label: Display> Drawable<DB> for Pie<'a, (i32, i32), Label> {
108 fn draw<I: Iterator<Item = BackendCoord>>(
109 &self,
110 _pos: I,
111 backend: &mut DB,
112 _parent_dim: (u32, u32),
113 ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
114 let mut offset_theta = self.start_radian;
115
116 let radian_increment = PI / 180.0 / self.radius.sqrt() * 2.0;
121 let mut perc_labels = Vec::new();
122 for (index, slice) in self.sizes.iter().enumerate() {
123 let slice_style = self
124 .colors
125 .get(index)
126 .ok_or_else(|| DrawingErrorKind::FontError(Box::new(PieError::LengthMismatch)))?;
127 let label = self
128 .labels
129 .get(index)
130 .ok_or_else(|| DrawingErrorKind::FontError(Box::new(PieError::LengthMismatch)))?;
131 let mut points = if self.donut_hole == 0.0 {
133 vec![*self.center]
134 } else {
135 vec![]
136 };
137 let ratio = slice / self.total;
138 let theta_final = ratio * 2.0 * PI + offset_theta; let middle_theta = ratio * PI + offset_theta;
142
143 let slice_start = offset_theta;
144
145 while offset_theta <= theta_final {
151 let coord = theta_to_ordinal_coord(*self.radius, offset_theta, self.center);
152 points.push(coord);
153 offset_theta += radian_increment;
154 }
155 let final_coord = theta_to_ordinal_coord(*self.radius, theta_final, self.center);
157 points.push(final_coord);
158
159 if self.donut_hole > 0.0 {
160 while offset_theta >= slice_start {
161 let coord = theta_to_ordinal_coord(self.donut_hole, offset_theta, self.center);
162 points.push(coord);
163 offset_theta -= radian_increment;
164 }
165 let final_coord_inner =
167 theta_to_ordinal_coord(self.donut_hole, slice_start, self.center);
168 points.push(final_coord_inner);
169 }
170
171 offset_theta = theta_final;
173
174 backend.fill_polygon(points, slice_style)?;
178
179 let mut mid_coord =
181 theta_to_ordinal_coord(self.radius + self.label_offset, middle_theta, self.center);
182
183 let label_size = backend.estimate_text_size(&label.to_string(), &self.label_style)?;
185 if mid_coord.0 <= self.center.0 {
187 mid_coord.0 -= label_size.0 as i32;
188 }
189 backend.draw_text(&label.to_string(), &self.label_style, mid_coord)?;
191 if let Some(percentage_style) = &self.percentage_style {
192 let perc_label = format!("{:.1}%", (ratio * 100.0));
193 let label_size = backend.estimate_text_size(&perc_label, percentage_style)?;
194 let text_x_mid = (label_size.0 as f64 / 2.0).round() as i32;
195 let text_y_mid = (label_size.1 as f64 / 2.0).round() as i32;
196 let perc_radius = (self.radius + self.donut_hole) / 2.0;
197 let perc_coord = theta_to_ordinal_coord(
198 perc_radius,
199 middle_theta,
200 &(self.center.0 - text_x_mid, self.center.1 - text_y_mid),
201 );
202 perc_labels.push((perc_label, perc_coord));
204 }
205 }
206 for (label, coord) in perc_labels {
209 let style = self.percentage_style.as_ref().unwrap();
210 backend.draw_text(&label, style, coord)?;
211 }
212 Ok(())
213 }
214}
215
216impl<'a, Label: Display> PointCollection<'a, (i32, i32)> for &'a Pie<'a, (i32, i32), Label> {
217 type Point = &'a (i32, i32);
218 type IntoIter = std::iter::Once<&'a (i32, i32)>;
219 fn point_iter(self) -> std::iter::Once<&'a (i32, i32)> {
220 std::iter::once(self.center)
221 }
222}
223
224fn theta_to_ordinal_coord(radius: f64, theta: f64, ordinal_offset: &(i32, i32)) -> (i32, i32) {
225 let (sin, cos) = theta.sin_cos();
229 (
230 (radius * cos + ordinal_offset.0 as f64).round() as i32, (radius * sin + ordinal_offset.1 as f64).round() as i32, )
236}
237#[cfg(test)]
238mod test {
239 use super::*;
240 #[test]
243 fn polar_coord_to_cartestian_coord() {
244 let coord = theta_to_ordinal_coord(800.0, 1.5_f64.to_radians(), &(5, 5));
245 assert_eq!(coord, (805, 26)); }
248 #[test]
249 fn pie_calculations() {
250 let mut center = (5, 5);
251 let mut radius = 800.0;
252
253 let sizes = vec![50.0, 25.0];
254 let colors = vec![];
256 let labels: Vec<&str> = vec![];
257 let pie = Pie::new(¢er, &radius, &sizes, &colors, &labels);
258 assert_eq!(pie.total, 75.0); center.1 += 1;
262 radius += 1.0;
263 assert!(colors.get(0).is_none());
264 assert!(labels.first().is_none());
265 assert_eq!(radius, 801.0);
266 }
267}