charts_rs/charts/
pie_chart.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License at
4//
5//     http://www.apache.org/licenses/LICENSE-2.0
6//
7// Unless required by applicable law or agreed to in writing, software
8// distributed under the License is distributed on an "AS IS" BASIS,
9// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10// See the License for the specific language governing permissions and
11// limitations under the License.
12
13use 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    // title
39    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    // sub title
48    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    // legend
57    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    // x axis
71    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    // y axis
83    pub y_axis_configs: Vec<YAxisConfig>,
84
85    // grid
86    pub grid_stroke_color: Color,
87    pub grid_stroke_width: f32,
88
89    // series
90    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    /// Creates a pie chart from json.
109    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    /// Creates a pie chart with custom theme.
130    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    /// Creates a pie chart with default theme.
140    pub fn new(series_list: Vec<Series>) -> PieChart {
141        PieChart::new_with_theme(series_list, &get_default_theme_name())
142    }
143    /// Converts pie chart to svg.
144    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        // get the max height of title and legend
154        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            // normal pie
209            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            // quadrant change
240            if quadrant != prev_quadrant {
241                prev_end_y = f32::MAX;
242                prev_quadrant = quadrant;
243            }
244            // label overlap
245            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}