1use super::Canvas;
14use super::canvas;
15use super::color::*;
16use super::common::*;
17use super::component::*;
18use super::params::*;
19use super::theme::{DEFAULT_Y_AXIS_WIDTH, Theme, get_default_theme_name, get_theme};
20use super::util::*;
21use crate::charts::measure_text_width_family;
22use charts_rs_derive::Chart;
23use std::sync::Arc;
24
25#[derive(Clone, Debug, Default)]
29pub struct BoxPlotSeries {
30 pub name: String,
31 pub data: Vec<[f32; 5]>,
33 pub index: Option<usize>,
34}
35
36#[charts_rs_derive::chart_common_fields]
37#[derive(Clone, Debug, Default, Chart)]
38pub struct BoxPlotChart {
39 pub x_axis_data: Vec<String>,
41 pub x_axis_height: f32,
42 pub x_axis_stroke_color: Color,
43 pub x_axis_font_size: f32,
44 pub x_axis_font_color: Color,
45 pub x_axis_font_weight: Option<String>,
46 pub x_axis_name_gap: f32,
47 pub x_axis_name_rotate: f32,
48 pub x_axis_margin: Option<Box>,
49 pub x_axis_hidden: bool,
50 pub x_boundary_gap: Option<bool>,
51
52 pub y_axis_hidden: bool,
54 pub y_axis_configs: Vec<YAxisConfig>,
55
56 pub grid_stroke_color: Color,
58 pub grid_stroke_width: f32,
59
60 pub series_stroke_width: f32,
62 pub series_label_font_color: Color,
63 pub series_label_font_size: f32,
64 pub series_label_font_weight: Option<String>,
65 pub series_label_formatter: String,
66 pub series_colors: Vec<Color>,
67 pub series_symbol: Option<Symbol>,
68 pub series_smooth: bool,
69 pub series_fill: bool,
70
71 pub box_series: Vec<BoxPlotSeries>,
73}
74
75impl BoxPlotChart {
76 fn fill_default(&mut self) {
77 if self.series_list.is_empty() {
79 for (i, bs) in self.box_series.iter().enumerate() {
80 let mut s = Series::new(bs.name.clone(), vec![]);
81 s.index = Some(bs.index.unwrap_or(i));
82 self.series_list.push(s);
83 }
84 }
85 if self.y_axis_configs[0].axis_stroke_color.is_zero() {
86 self.y_axis_configs[0].axis_stroke_color = self.x_axis_stroke_color;
87 }
88 }
89
90 pub fn new_with_theme(
91 box_series: Vec<BoxPlotSeries>,
92 x_axis_data: Vec<String>,
93 theme: &str,
94 ) -> BoxPlotChart {
95 let mut c = BoxPlotChart {
96 box_series,
97 x_axis_data,
98 ..Default::default()
99 };
100 c.fill_theme(get_theme(theme));
101 c.fill_default();
102 c
103 }
104
105 pub fn new(box_series: Vec<BoxPlotSeries>, x_axis_data: Vec<String>) -> BoxPlotChart {
106 BoxPlotChart::new_with_theme(box_series, x_axis_data, &get_default_theme_name())
107 }
108
109 pub fn from_json(json: &str) -> canvas::Result<BoxPlotChart> {
110 let mut c = BoxPlotChart {
111 ..Default::default()
112 };
113 let value = c.fill_option(json)?;
114 if let Some(arr) = value.get("box_series").and_then(|v| v.as_array()) {
116 for (i, item) in arr.iter().enumerate() {
117 let name = get_string_from_value(item, "name").unwrap_or_default();
118 let index = get_f32_from_value(item, "index").map(|v| v as usize);
119 let mut data: Vec<[f32; 5]> = vec![];
120 if let Some(rows) = item.get("data").and_then(|v| v.as_array()) {
121 for row in rows {
122 if let Some(vals) = row.as_array()
123 && vals.len() >= 5
124 {
125 let f = |i: usize| vals[i].as_f64().unwrap_or(0.0) as f32;
126 data.push([f(0), f(1), f(2), f(3), f(4)]);
127 }
128 }
129 }
130 c.box_series.push(BoxPlotSeries {
131 name,
132 data,
133 index: index.or(Some(i)),
134 });
135 }
136 }
137 c.fill_default();
138 Ok(c)
139 }
140
141 pub fn svg(&self) -> canvas::Result<String> {
142 let mut c = Canvas::new_width_xy(self.width, self.height, self.x, self.y);
143 self.render_background(c.child(Box::default()));
144 let mut x_axis_height = self.x_axis_height;
145 if self.x_axis_hidden {
146 x_axis_height = 0.0;
147 }
148 c.margin = self.margin.clone();
149
150 let title_height = self.render_title(c.child(Box::default()));
151 let legend_height = self.render_legend(c.child(Box::default()));
152 let axis_top = title_height.max(legend_height);
153
154 let mut all_values: Vec<f32> = vec![];
156 for bs in &self.box_series {
157 for entry in &bs.data {
158 all_values.push(entry[0]); all_values.push(entry[4]); }
161 }
162 if all_values.is_empty() {
163 return c.svg();
164 }
165
166 let y_axis_config = self.get_y_axis_config(0);
167 let y_axis_values = get_axis_values(AxisValueParams {
168 data_list: all_values,
169 split_number: y_axis_config.axis_split_number,
170 reverse: Some(true),
171 min: y_axis_config.axis_min,
172 max: y_axis_config.axis_max,
173 ..Default::default()
174 });
175
176 let y_axis_width = if self.y_axis_hidden {
177 0.0
178 } else if let Some(w) = y_axis_config.axis_width {
179 w
180 } else {
181 let formatter = y_axis_config.axis_formatter.clone().unwrap_or_default();
182 let label = format_string(&y_axis_values.data[0], &formatter);
183 measure_text_width_family(&self.font_family, y_axis_config.axis_font_size, &label)
184 .map(|b| b.width() + 5.0)
185 .unwrap_or(DEFAULT_Y_AXIS_WIDTH)
186 };
187
188 let axis_height = c.height() - x_axis_height - axis_top;
189 let axis_width = c.width() - y_axis_width;
190
191 if axis_top > 0.0 {
192 c = c.child(Box {
193 top: axis_top,
194 ..Default::default()
195 });
196 }
197
198 self.render_grid(
200 c.child(Box {
201 left: y_axis_width,
202 ..Default::default()
203 }),
204 axis_width,
205 axis_height,
206 );
207
208 if !self.y_axis_hidden {
210 self.render_y_axis(
211 c.child(Box::default()),
212 y_axis_values.data.clone(),
213 axis_height,
214 y_axis_width,
215 0,
216 );
217 }
218
219 if !self.x_axis_hidden {
221 self.render_x_axis(
222 c.child(Box {
223 top: c.height() - x_axis_height,
224 left: y_axis_width,
225 ..Default::default()
226 }),
227 self.x_axis_data.clone(),
228 axis_width,
229 );
230 }
231
232 let num_cats = self.x_axis_data.len().max(
233 self.box_series
234 .iter()
235 .map(|bs| bs.data.len())
236 .max()
237 .unwrap_or(0),
238 );
239 if num_cats == 0 {
240 return c.svg();
241 }
242
243 let num_series = self.box_series.len();
244 let col_w = axis_width / num_cats as f32;
245 let total_boxes_w = col_w * 0.6_f32;
247 let box_step = if num_series > 0 {
248 total_boxes_w / num_series as f32
249 } else {
250 total_boxes_w
251 };
252 let box_w = box_step * 0.8;
253 let cap_half = box_w * 0.3;
255 let stroke_w = self.series_stroke_width.max(1.0);
256
257 let mut data_c = c.child(Box {
258 left: y_axis_width,
259 ..Default::default()
260 });
261
262 for (si, bs) in self.box_series.iter().enumerate() {
263 let color = get_color(&self.series_colors, bs.index.unwrap_or(si));
264 let fill_color = color.with_alpha(80);
265
266 for (ci, entry) in bs.data.iter().enumerate() {
267 if ci >= num_cats {
268 break;
269 }
270 let [v_min, v_q1, v_med, v_q3, v_max] = *entry;
271
272 let cat_cx = col_w * (ci as f32 + 0.5);
274 let series_offset = (si as f32 - (num_series as f32 - 1.0) / 2.0) * box_step;
275 let cx = cat_cx + series_offset;
276
277 let y_min = y_axis_values.get_offset_height(v_min, axis_height);
278 let y_q1 = y_axis_values.get_offset_height(v_q1, axis_height);
279 let y_med = y_axis_values.get_offset_height(v_med, axis_height);
280 let y_q3 = y_axis_values.get_offset_height(v_q3, axis_height);
281 let y_max = y_axis_values.get_offset_height(v_max, axis_height);
282
283 let box_left = cx - box_w / 2.0;
284 let box_top = y_q3; let box_height = (y_q1 - y_q3).abs();
286
287 data_c.rect(Rect {
289 fill: Some(Fill::Solid(fill_color)),
290 color: Some(color),
291 left: box_left,
292 top: box_top,
293 width: box_w,
294 height: box_height,
295 ..Default::default()
296 });
297
298 data_c.line(Line {
300 color: Some(color),
301 stroke_width: stroke_w + 1.0,
302 left: box_left,
303 right: box_left + box_w,
304 top: y_med,
305 bottom: y_med,
306 ..Default::default()
307 });
308
309 data_c.line(Line {
311 color: Some(color),
312 stroke_width: stroke_w,
313 left: cx,
314 right: cx,
315 top: y_max,
316 bottom: y_q3,
317 ..Default::default()
318 });
319
320 data_c.line(Line {
322 color: Some(color),
323 stroke_width: stroke_w,
324 left: cx,
325 right: cx,
326 top: y_q1,
327 bottom: y_min,
328 ..Default::default()
329 });
330
331 data_c.line(Line {
333 color: Some(color),
334 stroke_width: stroke_w,
335 left: cx - cap_half,
336 right: cx + cap_half,
337 top: y_max,
338 bottom: y_max,
339 ..Default::default()
340 });
341
342 data_c.line(Line {
344 color: Some(color),
345 stroke_width: stroke_w,
346 left: cx - cap_half,
347 right: cx + cap_half,
348 top: y_min,
349 bottom: y_min,
350 ..Default::default()
351 });
352 }
353 }
354
355 c.svg()
356 }
357}
358
359#[cfg(test)]
360mod tests {
361 use super::{BoxPlotChart, BoxPlotSeries};
362 use pretty_assertions::assert_eq;
363
364 fn make_box_plot() -> BoxPlotChart {
365 BoxPlotChart::new(
366 vec![
367 BoxPlotSeries {
368 name: "Series A".to_string(),
369 data: vec![
370 [3.0, 10.0, 18.0, 28.0, 40.0],
371 [5.0, 14.0, 22.0, 32.0, 45.0],
372 [1.0, 8.0, 15.0, 24.0, 35.0],
373 [6.0, 12.0, 20.0, 30.0, 42.0],
374 ],
375 index: None,
376 },
377 BoxPlotSeries {
378 name: "Series B".to_string(),
379 data: vec![
380 [5.0, 13.0, 21.0, 31.0, 43.0],
381 [2.0, 9.0, 17.0, 26.0, 38.0],
382 [4.0, 11.0, 19.0, 29.0, 41.0],
383 [7.0, 15.0, 23.0, 33.0, 46.0],
384 ],
385 index: None,
386 },
387 ],
388 vec![
389 "Category A".to_string(),
390 "Category B".to_string(),
391 "Category C".to_string(),
392 "Category D".to_string(),
393 ],
394 )
395 }
396
397 #[test]
398 fn box_plot_chart_basic() {
399 assert_eq!(
400 include_str!("../../asset/box_plot_chart/basic.svg"),
401 make_box_plot().svg().unwrap()
402 );
403 }
404
405 #[test]
406 fn box_plot_chart_basic_json() {
407 let chart = BoxPlotChart::from_json(
408 r##"{
409 "title_text": "Box Plot",
410 "x_axis_data": ["Cat A", "Cat B", "Cat C"],
411 "box_series": [
412 {
413 "name": "Group 1",
414 "data": [
415 [3, 10, 18, 28, 40],
416 [5, 14, 22, 32, 45],
417 [1, 8, 15, 24, 35]
418 ]
419 },
420 {
421 "name": "Group 2",
422 "data": [
423 [5, 13, 21, 31, 43],
424 [2, 9, 17, 26, 38],
425 [4, 11, 19, 29, 41]
426 ]
427 }
428 ]
429 }"##,
430 )
431 .unwrap();
432 assert_eq!(
433 include_str!("../../asset/box_plot_chart/basic_json.svg"),
434 chart.svg().unwrap()
435 );
436 }
437}