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 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 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 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 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 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 pub y_axis_configs: Vec<YAxisConfig>,
109
110 pub grid_stroke_color: Color,
112 pub grid_stroke_width: f32,
113
114 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 pub indicators: Vec<RadarIndicator>,
127}
128
129impl RadarChart {
130 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 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 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 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 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}