charts_rs/charts/
radar_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 std::sync::Arc;
24
25#[derive(Clone, Debug, Default)]
26pub struct RadarIndicator {
27    pub name: String,
28    pub max: f32,
29}
30impl From<(&str, f32)> for RadarIndicator {
31    fn from(val: (&str, f32)) -> Self {
32        RadarIndicator {
33            name: val.0.to_string(),
34            max: val.1,
35        }
36    }
37}
38
39fn get_radar_indicator_list_from_value(value: &serde_json::Value) -> Option<Vec<RadarIndicator>> {
40    if let Some(data) = value.get("indicators") {
41        if let Some(arr) = data.as_array() {
42            let mut indicators = vec![];
43            for item in arr.iter() {
44                let name = get_string_from_value(item, "name").unwrap_or_default();
45                let max = get_f32_from_value(item, "max").unwrap_or_default();
46                if !name.is_empty() {
47                    indicators.push(RadarIndicator { name, max });
48                }
49            }
50            return Some(indicators);
51        }
52    }
53    None
54}
55
56#[derive(Clone, Debug, Default, Chart)]
57pub struct RadarChart {
58    pub width: f32,
59    pub height: f32,
60    pub x: f32,
61    pub y: f32,
62    pub margin: Box,
63    pub series_list: Vec<Series>,
64    pub font_family: String,
65    pub background_color: Color,
66    pub is_light: bool,
67
68    // title
69    pub title_text: String,
70    pub title_font_size: f32,
71    pub title_font_color: Color,
72    pub title_font_weight: Option<String>,
73    pub title_margin: Option<Box>,
74    pub title_align: Align,
75    pub title_height: f32,
76
77    // sub title
78    pub sub_title_text: String,
79    pub sub_title_font_size: f32,
80    pub sub_title_font_color: Color,
81    pub sub_title_font_weight: Option<String>,
82    pub sub_title_margin: Option<Box>,
83    pub sub_title_align: Align,
84    pub sub_title_height: f32,
85
86    // legend
87    pub legend_font_size: f32,
88    pub legend_font_color: Color,
89    pub legend_font_weight: Option<String>,
90    pub legend_align: Align,
91    pub legend_margin: Option<Box>,
92    pub legend_category: LegendCategory,
93    pub legend_show: Option<bool>,
94
95    // x axis
96    pub x_axis_data: Vec<String>,
97    pub x_axis_height: f32,
98    pub x_axis_stroke_color: Color,
99    pub x_axis_font_size: f32,
100    pub x_axis_font_color: Color,
101    pub x_axis_font_weight: Option<String>,
102    pub x_axis_name_gap: f32,
103    pub x_axis_name_rotate: f32,
104    pub x_axis_margin: Option<Box>,
105    pub x_boundary_gap: Option<bool>,
106
107    // y axis
108    pub y_axis_configs: Vec<YAxisConfig>,
109
110    // grid
111    pub grid_stroke_color: Color,
112    pub grid_stroke_width: f32,
113
114    // series
115    pub series_stroke_width: f32,
116    pub series_label_font_color: Color,
117    pub series_label_font_size: f32,
118    pub series_label_font_weight: Option<String>,
119    pub series_label_formatter: String,
120    pub series_colors: Vec<Color>,
121    pub series_symbol: Option<Symbol>,
122    pub series_smooth: bool,
123    pub series_fill: bool,
124
125    // indicators
126    pub indicators: Vec<RadarIndicator>,
127}
128
129impl RadarChart {
130    /// Creates a radar chart from json.
131    pub fn from_json(data: &str) -> canvas::Result<RadarChart> {
132        let mut r = RadarChart {
133            ..Default::default()
134        };
135        let data = r.fill_option(data)?;
136        if let Some(indicators) = get_radar_indicator_list_from_value(&data) {
137            r.indicators = indicators;
138        }
139        if data.get("series_fill").is_none() {
140            r.series_fill = true;
141        }
142        Ok(r)
143    }
144    /// Creates a radar chart with custom theme.
145    pub fn new_with_theme(
146        series_list: Vec<Series>,
147        indicators: Vec<RadarIndicator>,
148        theme: &str,
149    ) -> RadarChart {
150        let mut r = RadarChart {
151            series_list,
152            indicators,
153            series_fill: true,
154            ..Default::default()
155        };
156        let theme = get_theme(theme);
157        r.fill_theme(theme);
158        r
159    }
160    /// Creates a radar chart with default theme.
161    pub fn new(series_list: Vec<Series>, indicators: Vec<RadarIndicator>) -> RadarChart {
162        RadarChart::new_with_theme(series_list, indicators, &get_default_theme_name())
163    }
164    /// Converts bar chart to svg.
165    pub fn svg(&self) -> canvas::Result<String> {
166        if self.indicators.len() < 3 {
167            return Err(canvas::Error::Params {
168                message: "The count of indicator should be >= 3".to_string(),
169            });
170        }
171        let mut c = Canvas::new_width_xy(self.width, self.height, self.x, self.y);
172
173        self.render_background(c.child(Box::default()));
174        c.margin = self.margin.clone();
175
176        let title_height = self.render_title(c.child(Box::default()));
177
178        let legend_height = self.render_legend(c.child(Box::default()));
179        // get the max height of title and legend
180        let axis_top = if legend_height > title_height {
181            legend_height
182        } else {
183            title_height
184        };
185        if axis_top > 0.0 {
186            c = c.child(Box {
187                top: axis_top,
188                ..Default::default()
189            });
190        }
191
192        let mut max_values: Vec<f32> = vec![0.0; self.indicators.len()];
193        for series in self.series_list.iter() {
194            for (index, item) in series.data.iter().enumerate() {
195                if index < max_values.len() && *item > max_values[index] {
196                    max_values[index] = *item
197                }
198            }
199        }
200
201        let mut indicators = self.indicators.clone();
202        for (index, item) in indicators.iter_mut().enumerate() {
203            if item.max < max_values[index] {
204                item.max = max_values[index];
205            }
206        }
207
208        let offset = 40.0;
209        let r = c.height() / 2.0 - offset;
210        let angle = 360.0 / indicators.len() as f32;
211        let cx = c.width() / 2.0;
212        let cy = c.height() / 2.0;
213        let round_count = 5;
214        for i in 1..=round_count {
215            let ir = r / round_count as f32 * i as f32;
216            let mut points = vec![];
217            for index in 0..indicators.len() {
218                points.push(get_pie_point(cx, cy, ir, angle * index as f32));
219            }
220            c.straight_line(StraightLine {
221                color: Some(self.grid_stroke_color),
222                points,
223                stroke_width: self.grid_stroke_width,
224                symbol: None,
225                close: true,
226                ..Default::default()
227            });
228        }
229        for (index, item) in indicators.iter().enumerate() {
230            let current_angle = angle * index as f32;
231            let p = get_pie_point(cx, cy, r, current_angle);
232            let mut x = p.x;
233            let mut y = p.y;
234            let x_offset = 3.0;
235            if let Ok(measurement) = measure_text_width_family(
236                &self.font_family,
237                self.series_label_font_size,
238                &item.name,
239            ) {
240                if current_angle < 10.0 || (360.0 - current_angle) < 10.0 {
241                    y -= 5.0;
242                } else if (current_angle - 180.0).abs() < 10.0 {
243                    y += measurement.height();
244                } else if p.y > cy {
245                    let x_angle = if current_angle <= 180.0 {
246                        current_angle - 90.0
247                    } else {
248                        270.0 - current_angle
249                    };
250                    let y_offset = (x_angle / 180.0).cos() * (measurement.height() / 2.0);
251                    y += y_offset;
252                }
253
254                if current_angle == 0.0 || current_angle == 180.0 {
255                    x -= measurement.width() / 2.0;
256                } else if current_angle < 180.0 {
257                    x += x_offset;
258                } else {
259                    x -= measurement.width() + x_offset;
260                }
261            }
262            c.text(Text {
263                text: item.name.clone(),
264                font_size: Some(self.series_label_font_size),
265                font_family: Some(self.font_family.clone()),
266                font_color: Some(self.series_label_font_color),
267                x: Some(x),
268                y: Some(y),
269                ..Default::default()
270            });
271            c.child(Box::default()).line(Line {
272                color: Some(self.grid_stroke_color),
273                stroke_width: self.grid_stroke_width,
274                left: p.x,
275                top: p.y,
276                right: cx,
277                bottom: cy,
278                ..Default::default()
279            });
280        }
281
282        let mut label_positions = vec![];
283        for (index, series) in self.series_list.iter().enumerate() {
284            let color = get_color(&self.series_colors, series.index.unwrap_or(index));
285            let mut points = vec![];
286            for (i, item) in indicators.iter().enumerate() {
287                if let Some(value) = series.data.get(i) {
288                    let mut ir = if item.max <= 0.0 {
289                        0.0
290                    } else {
291                        *value / item.max * r
292                    };
293
294                    if ir > r {
295                        ir = r;
296                    }
297                    let p = get_pie_point(cx, cy, ir, angle * i as f32);
298                    if series.label_show {
299                        let label =
300                            format_series_value(value.to_owned(), &self.series_label_formatter);
301                        label_positions.push((p, label));
302                    }
303                    points.push(p);
304                }
305            }
306            let fill = if self.series_fill {
307                Some(color.with_alpha(50))
308            } else {
309                None
310            };
311            c.straight_line(StraightLine {
312                color: Some(color),
313                fill,
314                points: points.clone(),
315                stroke_width: self.series_stroke_width,
316                close: true,
317                ..Default::default()
318            });
319        }
320        for item in label_positions.iter() {
321            let mut dx = None;
322            let text = item.1.clone();
323            let point = item.0;
324            if let Ok(value) =
325                measure_text_width_family(&self.font_family, self.series_label_font_size, &text)
326            {
327                dx = Some(-value.width() / 2.0);
328            }
329            c.text(Text {
330                text: text.clone(),
331                dy: Some(-8.0),
332                dx,
333                font_family: Some(self.font_family.clone()),
334                font_color: Some(self.series_label_font_color),
335                font_size: Some(self.series_label_font_size),
336                font_weight: self.series_label_font_weight.clone(),
337                x: Some(point.x),
338                y: Some(point.y),
339                ..Default::default()
340            });
341        }
342
343        c.svg()
344    }
345}
346
347#[cfg(test)]
348mod tests {
349    use super::RadarChart;
350    use crate::Series;
351    use pretty_assertions::assert_eq;
352
353    #[test]
354    fn radar_basic() {
355        let radar_chart = RadarChart::new(
356            vec![
357                (
358                    "Allocated Budget",
359                    vec![4200.0, 3000.0, 20000.0, 35000.0, 50000.0, 18000.0],
360                )
361                    .into(),
362                (
363                    "Actual Spending",
364                    vec![5000.0, 14000.0, 28000.0, 26000.0, 42000.0, 21000.0],
365                )
366                    .into(),
367            ],
368            vec![
369                ("Sales", 6500.0).into(),
370                ("Administration", 16000.0).into(),
371                ("Information Technology", 30000.0).into(),
372                ("Customer Support", 38000.0).into(),
373                ("Development", 52000.0).into(),
374                ("Marketing", 25000.0).into(),
375            ],
376        );
377        assert_eq!(
378            include_str!("../../asset/radar_chart/basic.svg"),
379            radar_chart.svg().unwrap()
380        );
381    }
382
383    #[test]
384    fn radar_seven_basic() {
385        let radar_chart = RadarChart::new(
386            vec![
387                Series::new(
388                    "Allocated Budget".to_string(),
389                    vec![4200.0, 3000.0, 20000.0, 35000.0, 50000.0, 18000.0, 9000.0],
390                ),
391                Series::new(
392                    "Actual Spending".to_string(),
393                    vec![5000.0, 14000.0, 28000.0, 26000.0, 42000.0, 21000.0, 7000.0],
394                ),
395            ],
396            vec![
397                ("Sales", 6500.0).into(),
398                ("Administration", 16000.0).into(),
399                ("Information Technology", 30000.0).into(),
400                ("Customer Support", 38000.0).into(),
401                ("Development", 52000.0).into(),
402                ("Marketing", 25000.0).into(),
403                ("Online", 10000.0).into(),
404            ],
405        );
406
407        assert_eq!(
408            include_str!("../../asset/radar_chart/seven_points.svg"),
409            radar_chart.svg().unwrap()
410        );
411    }
412
413    #[test]
414    fn radar_five_points() {
415        let radar_chart = RadarChart::new(
416            vec![
417                Series::new(
418                    "Allocated Budget".to_string(),
419                    vec![4200.0, 3000.0, 20000.0, 35000.0, 50000.0],
420                ),
421                Series::new(
422                    "Actual Spending".to_string(),
423                    vec![5000.0, 14000.0, 28000.0, 26000.0, 42000.0],
424                ),
425            ],
426            vec![
427                ("Sales", 6500.0).into(),
428                ("Administration", 16000.0).into(),
429                ("Information Technology", 30000.0).into(),
430                ("Customer Support", 38000.0).into(),
431                ("Development", 52000.0).into(),
432            ],
433        );
434
435        assert_eq!(
436            include_str!("../../asset/radar_chart/five_points.svg"),
437            radar_chart.svg().unwrap()
438        );
439    }
440
441    #[test]
442    fn radar_four_points() {
443        let radar_chart = RadarChart::new(
444            vec![
445                Series::new(
446                    "Allocated Budget".to_string(),
447                    vec![4200.0, 3000.0, 20000.0, 35000.0],
448                ),
449                Series::new(
450                    "Actual Spending".to_string(),
451                    vec![5000.0, 14000.0, 28000.0, 26000.0],
452                ),
453            ],
454            vec![
455                ("Sales", 6500.0).into(),
456                ("Administration", 16000.0).into(),
457                ("Information Technology", 30000.0).into(),
458                ("Customer Support", 38000.0).into(),
459            ],
460        );
461
462        assert_eq!(
463            include_str!("../../asset/radar_chart/four_points.svg"),
464            radar_chart.svg().unwrap()
465        );
466    }
467
468    #[test]
469    fn radar_three_points() {
470        let mut radar_chart = RadarChart::new(
471            vec![
472                Series::new(
473                    "Allocated Budget".to_string(),
474                    vec![4200.0, 3000.0, 20000.0],
475                ),
476                Series::new(
477                    "Actual Spending".to_string(),
478                    vec![5000.0, 14000.0, 28000.0],
479                ),
480            ],
481            vec![
482                ("Sales", 6500.0).into(),
483                ("Administration", 16000.0).into(),
484                ("Information Technology", 30000.0).into(),
485            ],
486        );
487        radar_chart.series_list[0].label_show = true;
488
489        assert_eq!(
490            include_str!("../../asset/radar_chart/three_points.svg"),
491            radar_chart.svg().unwrap()
492        );
493    }
494}