]> git.proxmox.com Git - rustc.git/blob - vendor/plotters/src/element/pie.rs
New upstream version 1.71.1+dfsg1
[rustc.git] / vendor / plotters / src / element / pie.rs
1 use crate::{
2 element::{Drawable, PointCollection},
3 style::{IntoFont, RGBColor, TextStyle, BLACK},
4 };
5 use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
6 use std::{error::Error, f64::consts::PI, fmt::Display};
7
8 #[derive(Debug)]
9 enum PieError {
10 LengthMismatch,
11 }
12 impl 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
20 impl Error for PieError {}
21
22 /// A Pie Graph
23 pub struct Pie<'a, Coord, Label: Display> {
24 center: &'a Coord, // cartesian coord
25 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 }
35
36 impl<'a, Label: Display> Pie<'a, (i32, i32), Label> {
37 /// Build a Pie object.
38 /// Assumes a start angle at 0.0, which is aligned to the horizontal axis.
39 pub fn new(
40 center: &'a (i32, i32),
41 radius: &'a f64,
42 sizes: &'a [f64],
43 colors: &'a [RGBColor],
44 labels: &'a [Label],
45 ) -> Self {
46 // fold iterator to pre-calculate total from given slice sizes
47 let total = sizes.iter().sum();
48
49 // default label style and offset as 5% of the radius
50 let radius_5pct = radius * 0.05;
51
52 // strong assumption that the background is white for legibility.
53 let label_style = TextStyle::from(("sans-serif", radius_5pct).into_font()).color(&BLACK);
54 Self {
55 center,
56 radius,
57 sizes,
58 colors,
59 labels,
60 total,
61 start_radian: 0.0,
62 label_style,
63 label_offset: radius_5pct,
64 percentage_style: None,
65 }
66 }
67
68 /// Pass an angle in degrees to change the default.
69 /// Default is set to start at 0, which is aligned on the x axis.
70 /// ```
71 /// use plotters::prelude::*;
72 /// let mut pie = Pie::new(&(50,50), &10.0, &[50.0, 25.25, 20.0, 5.5], &[RED, BLUE, GREEN, WHITE], &["Red", "Blue", "Green", "White"]);
73 /// pie.start_angle(-90.0); // retract to a right angle, so it starts aligned to a vertical Y axis.
74 /// ```
75 pub fn start_angle(&mut self, start_angle: f64) {
76 // angle is more intuitive in degrees as an API, but we use it as radian offset internally.
77 self.start_radian = start_angle.to_radians();
78 }
79
80 ///
81 pub fn label_style<T: Into<TextStyle<'a>>>(&mut self, label_style: T) {
82 self.label_style = label_style.into();
83 }
84
85 /// Sets the offset to labels, to distanciate them further/closer from the center.
86 pub fn label_offset(&mut self, offset_to_radius: f64) {
87 self.label_offset = offset_to_radius
88 }
89
90 /// enables drawing the wedge's percentage in the middle of the wedge, with the given style
91 pub fn percentages<T: Into<TextStyle<'a>>>(&mut self, label_style: T) {
92 self.percentage_style = Some(label_style.into());
93 }
94 }
95
96 impl<'a, DB: DrawingBackend, Label: Display> Drawable<DB> for Pie<'a, (i32, i32), Label> {
97 fn draw<I: Iterator<Item = BackendCoord>>(
98 &self,
99 _pos: I,
100 backend: &mut DB,
101 _parent_dim: (u32, u32),
102 ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
103 let mut offset_theta = self.start_radian;
104
105 // const reused for every radian calculation
106 // the bigger the radius, the more fine-grained it should calculate
107 // to avoid being aliasing from being too noticeable.
108 // this all could be avoided if backend could draw a curve/bezier line as part of a polygon.
109 let radian_increment = PI / 180.0 / self.radius.sqrt() * 2.0;
110 let mut perc_labels = Vec::new();
111 for (index, slice) in self.sizes.iter().enumerate() {
112 let slice_style =
113 self.colors
114 .get(index)
115 .ok_or_else(|| DrawingErrorKind::FontError(Box::new(
116 PieError::LengthMismatch,
117 )))?;
118 let label = self
119 .labels
120 .get(index)
121 .ok_or_else(|| DrawingErrorKind::FontError(Box::new(
122 PieError::LengthMismatch,
123 )))?;
124 // start building wedge line against the previous edge
125 let mut points = vec![*self.center];
126 let ratio = slice / self.total;
127 let theta_final = ratio * 2.0 * PI + offset_theta; // end radian for the wedge
128
129 // calculate middle for labels before mutating offset
130 let middle_theta = ratio * PI + offset_theta;
131
132 // calculate every fraction of radian for the wedge, offsetting for every iteration, clockwise
133 //
134 // a custom Range such as `for theta in offset_theta..=theta_final` would be more elegant
135 // but f64 doesn't implement the Range trait, and it would requires the Step trait (increment by 1.0 or 0.0001?)
136 // which is unstable therefore cannot be implemented outside of std, even as a newtype for radians.
137 while offset_theta <= theta_final {
138 let coord = theta_to_ordinal_coord(*self.radius, offset_theta, self.center);
139 points.push(coord);
140 offset_theta += radian_increment;
141 }
142 // final point of the wedge may not fall exactly on a radian, so add it extra
143 let final_coord = theta_to_ordinal_coord(*self.radius, theta_final, self.center);
144 points.push(final_coord);
145 // next wedge calculation will start from previous wedges's last radian
146 offset_theta = theta_final;
147
148 // draw wedge
149 // TODO: Currently the backend doesn't have API to draw an arc. We need add that in the
150 // future
151 backend.fill_polygon(points, slice_style)?;
152
153 // label coords from the middle
154 let mut mid_coord =
155 theta_to_ordinal_coord(self.radius + self.label_offset, middle_theta, self.center);
156
157 // ensure label's doesn't fall in the circle
158 let label_size = backend.estimate_text_size(&label.to_string(), &self.label_style)?;
159 // if on the left hand side of the pie, offset whole label to the left
160 if mid_coord.0 <= self.center.0 {
161 mid_coord.0 -= label_size.0 as i32;
162 }
163 // put label
164 backend.draw_text(&label.to_string(), &self.label_style, mid_coord)?;
165 if let Some(percentage_style) = &self.percentage_style {
166 let perc_label = format!("{:.1}%", (ratio * 100.0));
167 let label_size = backend.estimate_text_size(&perc_label, percentage_style)?;
168 let text_x_mid = (label_size.0 as f64 / 2.0).round() as i32;
169 let text_y_mid = (label_size.1 as f64 / 2.0).round() as i32;
170 let perc_coord = theta_to_ordinal_coord(
171 self.radius / 2.0,
172 middle_theta,
173 &(self.center.0 - text_x_mid, self.center.1 - text_y_mid),
174 );
175 // perc_coord.0 -= middle_label_size.0.round() as i32;
176 perc_labels.push((perc_label, perc_coord));
177 }
178 }
179 // while percentages are generated during the first main iterations,
180 // they have to go on top of the already drawn wedges, so require a new iteration.
181 for (label, coord) in perc_labels {
182 let style = self.percentage_style.as_ref().unwrap();
183 backend.draw_text(&label, style, coord)?;
184 }
185 Ok(())
186 }
187 }
188
189 impl<'a, Label: Display> PointCollection<'a, (i32, i32)> for &'a Pie<'a, (i32, i32), Label> {
190 type Point = &'a (i32, i32);
191 type IntoIter = std::iter::Once<&'a (i32, i32)>;
192 fn point_iter(self) -> std::iter::Once<&'a (i32, i32)> {
193 std::iter::once(self.center)
194 }
195 }
196
197 fn theta_to_ordinal_coord(radius: f64, theta: f64, ordinal_offset: &(i32, i32)) -> (i32, i32) {
198 // polar coordinates are (r, theta)
199 // convert to (x, y) coord, with center as offset
200
201 let (sin, cos) = theta.sin_cos();
202 (
203 // casting f64 to discrete i32 pixels coordinates is inevitably going to lose precision
204 // if plotters can support float coordinates, this place would surely benefit, especially for small sizes.
205 // so far, the result isn't so bad though
206 (radius * cos + ordinal_offset.0 as f64).round() as i32, // x
207 (radius * sin + ordinal_offset.1 as f64).round() as i32, // y
208 )
209 }
210 #[cfg(test)]
211 mod test {
212 use super::*;
213 // use crate::prelude::*;
214
215 #[test]
216 fn polar_coord_to_cartestian_coord() {
217 let coord = theta_to_ordinal_coord(800.0, 1.5_f64.to_radians(), &(5, 5));
218 // rounded tends to be more accurate. this gets truncated to (804, 25) without rounding.
219 assert_eq!(coord, (805, 26)); //coord calculated from theta
220 }
221 #[test]
222 fn pie_calculations() {
223 let mut center = (5, 5);
224 let mut radius = 800.0;
225
226 let sizes = vec![50.0, 25.0];
227 // length isn't validated in new()
228 let colors = vec![];
229 let labels: Vec<&str> = vec![];
230 let pie = Pie::new(&center, &radius, &sizes, &colors, &labels);
231 assert_eq!(pie.total, 75.0); // total calculated from sizes
232
233 // not ownership greedy
234 center.1 += 1;
235 radius += 1.0;
236 assert!(colors.get(0).is_none());
237 assert!(labels.get(0).is_none());
238 assert_eq!(radius, 801.0);
239 }
240 }