1use super::canvas;
14use super::color::*;
15use super::common::*;
16use super::component::*;
17use super::params::*;
18use super::theme::{get_default_theme_name, get_theme, Theme, DEFAULT_Y_AXIS_WIDTH};
19use super::util::*;
20use super::Canvas;
21use crate::charts::measure_text_width_family;
22use charts_rs_derive::Chart;
23use core::f32;
24use std::sync::Arc;
25
26#[derive(Clone, Debug, Default, Chart)]
27pub struct PieChart {
28 pub width: f32,
29 pub height: f32,
30 pub x: f32,
31 pub y: f32,
32 pub margin: Box,
33 pub series_list: Vec<Series>,
34 pub font_family: String,
35 pub background_color: Color,
36 pub is_light: bool,
37
38 pub title_text: String,
40 pub title_font_size: f32,
41 pub title_font_color: Color,
42 pub title_font_weight: Option<String>,
43 pub title_margin: Option<Box>,
44 pub title_align: Align,
45 pub title_height: f32,
46
47 pub sub_title_text: String,
49 pub sub_title_font_size: f32,
50 pub sub_title_font_color: Color,
51 pub sub_title_font_weight: Option<String>,
52 pub sub_title_margin: Option<Box>,
53 pub sub_title_align: Align,
54 pub sub_title_height: f32,
55
56 pub legend_font_size: f32,
58 pub legend_font_color: Color,
59 pub legend_font_weight: Option<String>,
60 pub legend_align: Align,
61 pub legend_margin: Option<Box>,
62 pub legend_category: LegendCategory,
63 pub legend_show: Option<bool>,
64
65 pub radius: f32,
66 pub inner_radius: f32,
67 pub rose_type: Option<bool>,
68 pub border_radius: Option<f32>,
69
70 pub x_axis_data: Vec<String>,
72 pub x_axis_height: f32,
73 pub x_axis_stroke_color: Color,
74 pub x_axis_font_size: f32,
75 pub x_axis_font_color: Color,
76 pub x_axis_font_weight: Option<String>,
77 pub x_axis_name_gap: f32,
78 pub x_axis_name_rotate: f32,
79 pub x_axis_margin: Option<Box>,
80 pub x_boundary_gap: Option<bool>,
81
82 pub y_axis_configs: Vec<YAxisConfig>,
84
85 pub grid_stroke_color: Color,
87 pub grid_stroke_width: f32,
88
89 pub series_stroke_width: f32,
91 pub series_label_font_color: Color,
92 pub series_label_font_size: f32,
93 pub series_label_font_weight: Option<String>,
94 pub series_label_formatter: String,
95 pub series_colors: Vec<Color>,
96 pub series_symbol: Option<Symbol>,
97 pub series_smooth: bool,
98 pub series_fill: bool,
99}
100
101impl PieChart {
102 fn fill_default(&mut self) {
103 self.radius = 150.0;
104 self.inner_radius = 40.0;
105 self.legend_show = Some(false);
106 self.rose_type = Some(true);
107 }
108 pub fn from_json(data: &str) -> canvas::Result<PieChart> {
110 let mut p = PieChart {
111 ..Default::default()
112 };
113 p.fill_default();
114 let value = p.fill_option(data)?;
115 if let Some(radius) = get_f32_from_value(&value, "radius") {
116 p.radius = radius;
117 }
118 if let Some(inner_radius) = get_f32_from_value(&value, "inner_radius") {
119 p.inner_radius = inner_radius;
120 }
121 if let Some(rose_type) = get_bool_from_value(&value, "rose_type") {
122 p.rose_type = Some(rose_type);
123 }
124 if let Some(border_radius) = get_f32_from_value(&value, "border_radius") {
125 p.border_radius = Some(border_radius);
126 }
127 Ok(p)
128 }
129 pub fn new_with_theme(series_list: Vec<Series>, theme: &str) -> PieChart {
131 let mut p = PieChart {
132 series_list,
133 ..Default::default()
134 };
135 p.fill_default();
136 p.fill_theme(get_theme(theme));
137 p
138 }
139 pub fn new(series_list: Vec<Series>) -> PieChart {
141 PieChart::new_with_theme(series_list, &get_default_theme_name())
142 }
143 pub fn svg(&self) -> canvas::Result<String> {
145 let mut c = Canvas::new_width_xy(self.width, self.height, self.x, self.y);
146
147 self.render_background(c.child(Box::default()));
148 c.margin = self.margin.clone();
149
150 let title_height = self.render_title(c.child(Box::default()));
151
152 let legend_height = self.render_legend(c.child(Box::default()));
153 let axis_top = if legend_height > title_height {
155 legend_height
156 } else {
157 title_height
158 };
159 if axis_top > 0.0 {
160 c = c.child(Box {
161 top: axis_top,
162 ..Default::default()
163 });
164 }
165
166 let values: Vec<f32> = self
167 .series_list
168 .iter()
169 .map(|item| item.data.iter().sum())
170 .collect();
171 let mut max = 0.0;
172 let mut sum = 0.0;
173 for item in values.iter() {
174 sum += *item;
175 if *item > max {
176 max = *item;
177 }
178 }
179 let mut delta = 360.0 / values.len() as f32;
180 let mut half_delta = delta / 2.0;
181 let mut start_angle = 0.0_f32;
182 let mut radius_double = c.height();
183
184 if c.width() < radius_double {
185 radius_double = c.width();
186 }
187 radius_double *= 0.8;
188 let mut r = radius_double / 2.0;
189 if r > self.radius {
190 r = self.radius;
191 }
192
193 let cx = (c.width() - radius_double) / 2.0 + r;
194 let cy = (c.height() - radius_double) / 2.0 + r;
195 let label_offset = 20.0;
196 let mut series_label_formatter = self.series_label_formatter.clone();
197 if series_label_formatter.is_empty() {
198 series_label_formatter = "{a}: {d}".to_string();
199 }
200 let rose_type = self.rose_type.unwrap_or_default();
201
202 let mut prev_quadrant = u8::MAX;
203 let mut prev_end_y = f32::MAX;
204 for (index, series) in self.series_list.iter().enumerate() {
205 let value = values[index];
206 let mut cr = value / max * (r - self.inner_radius) + self.inner_radius;
207 let color = get_color(&self.series_colors, series.index.unwrap_or(index));
208 if !rose_type {
210 cr = r;
211 delta = value / sum * 360.0;
212 half_delta = delta / 2.0;
213 }
214 if cr - self.inner_radius < 1.0 {
215 cr = self.inner_radius + 1.0;
216 }
217 let mut pie = Pie {
218 fill: color,
219 cx,
220 cy,
221 r: cr,
222 ir: self.inner_radius,
223 start_angle,
224 delta,
225 ..Default::default()
226 };
227 if let Some(border_radius) = self.border_radius {
228 pie.border_radius = border_radius;
229 }
230
231 c.pie(pie);
232
233 let angle = start_angle + half_delta;
234 let mut points = vec![];
235 points.push(get_pie_point(cx, cy, cr, angle));
236 let mut end = get_pie_point(cx, cy, r + label_offset, angle);
237
238 let quadrant = get_quadrant(cx, cy, &end);
239 if quadrant != prev_quadrant {
241 prev_end_y = f32::MAX;
242 prev_quadrant = quadrant;
243 }
244 if (end.y - prev_end_y).abs() < self.series_label_font_size {
246 if quadrant == 1 || quadrant == 4 {
247 end.y = prev_end_y + self.series_label_font_size;
248 } else {
249 end.y = prev_end_y - self.series_label_font_size;
250 }
251 }
252 prev_end_y = end.y;
253
254 points.push(end);
255
256 let is_left = angle > 180.0;
257 if is_left {
258 end.x -= label_offset;
259 } else {
260 end.x += label_offset;
261 }
262 let mut label_margin = Box {
263 left: end.x,
264 top: end.y + 5.0,
265 ..Default::default()
266 };
267 let label_option = LabelOption {
268 series_name: series.name.clone(),
269 value,
270 percentage: value / sum,
271 formatter: series_label_formatter.clone(),
272 ..Default::default()
273 };
274 let label_text = label_option.format();
275
276 if is_left {
277 if let Ok(b) = measure_text_width_family(
278 &self.font_family,
279 self.series_label_font_size,
280 &label_text,
281 ) {
282 label_margin.left -= b.width();
283 }
284 } else {
285 label_margin.left += 3.0;
286 }
287
288 points.push(end);
289 c.smooth_line(SmoothLine {
290 color: Some(color),
291 points,
292 symbol: None,
293 ..Default::default()
294 });
295
296 c.child(label_margin).text(Text {
297 text: label_text,
298 font_family: Some(self.font_family.clone()),
299 font_size: Some(self.series_label_font_size),
300 font_color: Some(self.series_label_font_color),
301 ..Default::default()
302 });
303
304 start_angle += delta;
305 }
306
307 c.svg()
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::PieChart;
314 use pretty_assertions::assert_eq;
315
316 #[test]
317 fn pie_basic() {
318 let mut pie_chart = PieChart::new(vec![
319 ("rose 1", vec![40.0]).into(),
320 ("rose 2", vec![38.0]).into(),
321 ("rose 3", vec![32.0]).into(),
322 ("rose 4", vec![30.0]).into(),
323 ("rose 5", vec![28.0]).into(),
324 ("rose 6", vec![26.0]).into(),
325 ("rose 7", vec![22.0]).into(),
326 ("rose 8", vec![18.0]).into(),
327 ]);
328 pie_chart.title_text = "Nightingale Chart".to_string();
329 pie_chart.sub_title_text = "Fake Data".to_string();
330 assert_eq!(
331 include_str!("../../asset/pie_chart/basic.svg"),
332 pie_chart.svg().unwrap()
333 );
334 }
335
336 #[test]
337 fn small_pie_basic() {
338 let mut pie_chart = PieChart::new(vec![
339 ("rose 1", vec![400.0]).into(),
340 ("rose 2", vec![38.0]).into(),
341 ("rose 3", vec![32.0]).into(),
342 ("rose 4", vec![30.0]).into(),
343 ("rose 5", vec![28.0]).into(),
344 ("rose 6", vec![26.0]).into(),
345 ("rose 7", vec![22.0]).into(),
346 ("rose 8", vec![18.0]).into(),
347 ]);
348 pie_chart.width = 400.0;
349 pie_chart.height = 300.0;
350 pie_chart.title_text = "Nightingale Chart".to_string();
351 pie_chart.sub_title_text = "Fake Data".to_string();
352 assert_eq!(
353 include_str!("../../asset/pie_chart/small_basic.svg"),
354 pie_chart.svg().unwrap()
355 );
356 }
357
358 #[test]
359 fn not_rose_pie() {
360 let mut pie_chart = PieChart::new(vec![
361 ("rose 1", vec![400.0]).into(),
362 ("rose 2", vec![38.0]).into(),
363 ("rose 3", vec![32.0]).into(),
364 ("rose 4", vec![30.0]).into(),
365 ("rose 5", vec![28.0]).into(),
366 ("rose 6", vec![26.0]).into(),
367 ("rose 7", vec![22.0]).into(),
368 ("rose 8", vec![18.0]).into(),
369 ]);
370 pie_chart.rose_type = Some(false);
371 pie_chart.title_text = "Pie Chart".to_string();
372 pie_chart.sub_title_text = "Fake Data".to_string();
373 assert_eq!(
374 include_str!("../../asset/pie_chart/not_rose.svg").trim(),
375 pie_chart.svg().unwrap()
376 );
377 }
378
379 #[test]
380 fn not_rose_radius_pie() {
381 let mut pie_chart = PieChart::new(vec![
382 ("rose 1", vec![400.0]).into(),
383 ("rose 2", vec![38.0]).into(),
384 ("rose 3", vec![32.0]).into(),
385 ("rose 4", vec![30.0]).into(),
386 ("rose 5", vec![28.0]).into(),
387 ("rose 6", vec![26.0]).into(),
388 ("rose 7", vec![22.0]).into(),
389 ("rose 8", vec![18.0]).into(),
390 ]);
391 pie_chart.rose_type = Some(false);
392 pie_chart.inner_radius = 0.0;
393 pie_chart.border_radius = Some(0.0);
394 pie_chart.title_text = "Pie Chart".to_string();
395 pie_chart.sub_title_text = "Fake Data".to_string();
396 assert_eq!(
397 include_str!("../../asset/pie_chart/not_rose_radius.svg").trim(),
398 pie_chart.svg().unwrap()
399 );
400 }
401
402 #[test]
403 fn pie_rose_small_piece() {
404 let mut pie_chart = PieChart::new(vec![
405 ("rose 1", vec![40000.0]).into(),
406 ("rose 2", vec![38.0]).into(),
407 ("rose 3", vec![32.0]).into(),
408 ("rose 4", vec![30.0]).into(),
409 ("rose 5", vec![28.0]).into(),
410 ("rose 6", vec![26.0]).into(),
411 ("rose 7", vec![22.0]).into(),
412 ("rose 8", vec![18.0]).into(),
413 ]);
414 pie_chart.title_text = "Nightingale Chart".to_string();
415 pie_chart.sub_title_text = "Fake Data".to_string();
416 assert_eq!(
417 include_str!("../../asset/pie_chart/rose_small_piece.svg"),
418 pie_chart.svg().unwrap()
419 );
420 }
421}