2 element
::{Drawable, PointCollection}
,
3 style
::{IntoFont, RGBColor, TextStyle, BLACK}
,
5 use plotters_backend
::{BackendCoord, DrawingBackend, DrawingErrorKind}
;
6 use std
::{error::Error, f64::consts::PI, fmt::Display}
;
12 impl Display
for PieError
{
13 fn fmt(&self, f
: &mut std
::fmt
::Formatter
<'_
>) -> std
::fmt
::Result
{
15 &PieError
::LengthMismatch
=> write
!(f
, "Length Mismatch"),
20 impl Error
for PieError {}
23 pub struct Pie
<'a
, Coord
, Label
: Display
> {
24 center
: &'a Coord
, // cartesian coord
27 colors
: &'a
[RGBColor
],
31 label_style
: TextStyle
<'a
>,
33 percentage_style
: Option
<TextStyle
<'a
>>,
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.
40 center
: &'
a (i32, i32),
43 colors
: &'a
[RGBColor
],
46 // fold iterator to pre-calculate total from given slice sizes
47 let total
= sizes
.iter().sum();
49 // default label style and offset as 5% of the radius
50 let radius_5pct
= radius
* 0.05;
52 // strong assumption that the background is white for legibility.
53 let label_style
= TextStyle
::from(("sans-serif", radius_5pct
).into_font()).color(&BLACK
);
63 label_offset
: radius_5pct
,
64 percentage_style
: None
,
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.
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.
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();
81 pub fn label_style
<T
: Into
<TextStyle
<'a
>>>(&mut self, label_style
: T
) {
82 self.label_style
= label_style
.into();
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
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());
96 impl<'a
, DB
: DrawingBackend
, Label
: Display
> Drawable
<DB
> for Pie
<'a
, (i32, i32), Label
> {
97 fn draw
<I
: Iterator
<Item
= BackendCoord
>>(
101 _parent_dim
: (u32, u32),
102 ) -> Result
<(), DrawingErrorKind
<DB
::ErrorType
>> {
103 let mut offset_theta
= self.start_radian
;
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() {
115 .ok_or_else(|| DrawingErrorKind
::FontError(Box
::new(
116 PieError
::LengthMismatch
,
121 .ok_or_else(|| DrawingErrorKind
::FontError(Box
::new(
122 PieError
::LengthMismatch
,
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
129 // calculate middle for labels before mutating offset
130 let middle_theta
= ratio
* PI
+ offset_theta
;
132 // calculate every fraction of radian for the wedge, offsetting for every iteration, clockwise
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
);
140 offset_theta
+= radian_increment
;
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
;
149 // TODO: Currently the backend doesn't have API to draw an arc. We need add that in the
151 backend
.fill_polygon(points
, slice_style
)?
;
153 // label coords from the middle
155 theta_to_ordinal_coord(self.radius
+ self.label_offset
, middle_theta
, self.center
);
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;
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(
173 &(self.center
.0 - text_x_mid
, self.center
.1 - text_y_mid
),
175 // perc_coord.0 -= middle_label_size.0.round() as i32;
176 perc_labels
.push((perc_label
, perc_coord
));
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
)?
;
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
)
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
201 let (sin
, cos
) = theta
.sin_cos();
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
213 // use crate::prelude::*;
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
222 fn pie_calculations() {
223 let mut center
= (5, 5);
224 let mut radius
= 800.0;
226 let sizes
= vec
![50.0, 25.0];
227 // length isn't validated in new()
229 let labels
: Vec
<&str> = vec
![];
230 let pie
= Pie
::new(¢er
, &radius
, &sizes
, &colors
, &labels
);
231 assert_eq
!(pie
.total
, 75.0); // total calculated from sizes
233 // not ownership greedy
236 assert
!(colors
.get(0).is_none());
237 assert
!(labels
.get(0).is_none());
238 assert_eq
!(radius
, 801.0);