1use xml::writer::{EventWriter, XmlEvent};
28
29use crate::chart::colors::ColorIter;
30use crate::chart::legend::Legend;
31use crate::chart::tag_percent::TagPercent;
32use crate::chart::utils;
33use crate::emit_xml;
34
35pub struct PieChart<'a> {
37 r: f32,
38 legend: Legend<'a>
39}
40
41impl<'a> PieChart<'a> {
42 pub fn new(r: f32, legend: Legend<'a>) -> Self { Self { r, legend } }
44
45 pub fn radius(&self) -> f32 { self.r }
47
48 pub fn write_pie<W>(&self, w: &mut EventWriter<W>, percents: &[TagPercent]) -> crate::Result<()>
54 where
55 W: std::io::Write
56 {
57 emit_xml!(w, div, class: "piechart" => {
58 emit_xml!(w, div, class: "pie" => {
59 let size = format!("{}", 2.0 * (self.r + 1.0));
60 let view = format!("{0:} {0:} {1:} {1:}", -(self.r + 1.0), size);
61 emit_xml!(w, svg, viewbox: &view, width: &size, height: &size => {
62 emit_xml!(w, circle, r: &format!("{}", self.r), stroke: "black")?;
63 let colors = ColorIter::default();
64 let percents = colors.limit_percents(percents, "Other");
65 let mut alpha = -90.0;
66 for (p, clr) in percents.iter().zip(colors) {
67 let theta = p.percent_val() * 3.6f32;
68
69 emit_xml!(w, path, fill: clr, d: &self.pie_slice(alpha, theta))?;
70 alpha += theta;
71 }
72 Ok(())
73 })
74 })?;
75 self.legend.write(w, percents.iter())?;
76 Ok(())
77 })
78 }
79
80 #[rustfmt::skip]
82 fn pie_slice(&self, alpha: f32, theta: f32) -> String {
83 let alpha_rad = alpha.to_radians();
84 let sx = utils::format_coord(self.r * alpha_rad.cos());
85 let sy = utils::format_coord(self.r * alpha_rad.sin());
86
87 if (theta - 360.0).abs() < 0.01 {
88 let rend = (alpha + 180.0).to_radians();
89 let ex = utils::format_coord(self.r * rend.cos());
90 let ey = utils::format_coord(self.r * rend.sin());
91
92 format!(
93 "M0,0 L{sx},{sy} A{r},{r} 0 1,1 {ex},{ey} A{r},{r} 0 1,1 {sx},{sy} z",
94 sx=sx, sy=sy,
95 r=self.r,
96 ex=ex, ey=ey
97 )
98 }
99 else {
100 let rend = (alpha + theta).to_radians();
101 let ex = utils::format_coord(self.r * rend.cos());
102 let ey = utils::format_coord(self.r * rend.sin());
103
104 let large = i32::from(theta >= 180.0);
105 format!(
106 "M0,0 L{sx},{sy} A{r},{r} 0 {lg},1 {ex},{ey} z",
107 sx=sx, sy=sy,
108 r=self.r,
109 lg=large,
110 ex=ex, ey=ey
111 )
112 }
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use spectral::prelude::*;
119 use xml::EmitterConfig;
120
121 use super::*;
122 use crate::chart::ColorIter;
123 use crate::chart::Legend;
124
125 #[test]
126 fn test_new() {
127 let legend = Legend::new(14.0, ColorIter::default());
128 let pie = PieChart::new(100.0, legend);
129
130 assert_that!(pie.radius()).is_equal_to(&100.0);
131 }
132
133 #[test]
134 fn test_pie_one_slice() {
135 let legend = Legend::new(14.0, ColorIter::default());
136 let pie = PieChart::new(100.0, legend);
137
138 let percents = [TagPercent::new("foo", 100.0).unwrap()];
139
140 let mut output: Vec<u8> = Vec::new();
141 let mut w = EmitterConfig::new()
142 .perform_indent(true)
143 .write_document_declaration(false)
144 .create_writer(&mut output);
145 assert_that!(pie.write_pie(&mut w, &percents)).is_ok();
146 let actual = String::from_utf8(output).unwrap();
147 let expected = r##"<div class="piechart">
148 <div class="pie">
149 <svg viewbox="-101 -101 202 202" width="202" height="202">
150 <circle r="100" stroke="black" />
151 <path fill="#1f78b4" d="M0,0 L-0,-100 A100,100 0 1,1 -0,100 A100,100 0 1,1 -0,-100 z" />
152 </svg>
153 </div>
154 <table class="legend" style="font-size: 14px">
155 <tr>
156 <td>
157 <svg height="14" width="14">
158 <rect height="14" width="14" fill="#1f78b4" />
159 </svg>
160 <span>100% - foo</span>
161 </td>
162 </tr>
163 </table>
164</div>"##;
165 assert_that!(actual.as_str()).is_equal_to(expected);
166 }
167
168 #[test]
169 fn test_pie_multiple_slices() {
170 let legend = Legend::new(14.0, ColorIter::default());
171 let pie = PieChart::new(100.0, legend);
172
173 #[rustfmt::skip]
174 let percents = [
175 TagPercent::new("david", 40.0).unwrap(),
176 TagPercent::new("connie", 30.0).unwrap(),
177 TagPercent::new("mark", 20.0).unwrap(),
178 TagPercent::new("kirsten", 10.0).unwrap()
179 ];
180
181 let mut output: Vec<u8> = Vec::new();
182 let mut w = EmitterConfig::new()
183 .perform_indent(true)
184 .write_document_declaration(false)
185 .create_writer(&mut output);
186 assert_that!(pie.write_pie(&mut w, &percents)).is_ok();
187 let actual = String::from_utf8(output).unwrap();
188 let expected = r##"<div class="piechart">
189 <div class="pie">
190 <svg viewbox="-101 -101 202 202" width="202" height="202">
191 <circle r="100" stroke="black" />
192 <path fill="#1f78b4" d="M0,0 L-0,-100 A100,100 0 0,1 58.779,80.902 z" />
193 <path fill="#a6cee3" d="M0,0 L58.779,80.902 A100,100 0 0,1 -95.106,30.902 z" />
194 <path fill="#33a02c" d="M0,0 L-95.106,30.902 A100,100 0 0,1 -58.779,-80.902 z" />
195 <path fill="#b2df8a" d="M0,0 L-58.779,-80.902 A100,100 0 0,1 0,-100 z" />
196 </svg>
197 </div>
198 <table class="legend" style="font-size: 14px">
199 <tr>
200 <td>
201 <svg height="14" width="14">
202 <rect height="14" width="14" fill="#1f78b4" />
203 </svg>
204 <span>40% - david</span>
205 </td>
206 </tr>
207 <tr>
208 <td>
209 <svg height="14" width="14">
210 <rect height="14" width="14" fill="#a6cee3" />
211 </svg>
212 <span>30% - connie</span>
213 </td>
214 </tr>
215 <tr>
216 <td>
217 <svg height="14" width="14">
218 <rect height="14" width="14" fill="#33a02c" />
219 </svg>
220 <span>20% - mark</span>
221 </td>
222 </tr>
223 <tr>
224 <td>
225 <svg height="14" width="14">
226 <rect height="14" width="14" fill="#b2df8a" />
227 </svg>
228 <span>10% - kirsten</span>
229 </td>
230 </tr>
231 </table>
232</div>"##;
233 assert_that!(actual.as_str()).is_equal_to(expected);
234 }
235}