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 serde::{Deserialize, Serialize};
24use std::sync::Arc;
25
26#[derive(Serialize, Deserialize, Clone, Debug, Default, Chart)]
27pub struct ScatterChart {
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 x_axis_data: Vec<String>,
67 pub x_axis_height: f32,
68 pub x_axis_stroke_color: Color,
69 pub x_axis_font_size: f32,
70 pub x_axis_font_color: Color,
71 pub x_axis_font_weight: Option<String>,
72 pub x_axis_name_gap: f32,
73 pub x_axis_name_rotate: f32,
74 pub x_axis_margin: Option<Box>,
75 pub x_axis_config: YAxisConfig,
76 pub x_axis_hidden: bool,
77 pub x_boundary_gap: Option<bool>,
78
79 pub y_axis_hidden: bool,
81 pub y_axis_configs: Vec<YAxisConfig>,
82
83 pub grid_stroke_color: Color,
85 pub grid_stroke_width: f32,
86
87 pub series_stroke_width: f32,
89 pub series_label_font_color: Color,
90 pub series_label_font_size: f32,
91 pub series_label_font_weight: Option<String>,
92 pub series_label_formatter: String,
93 pub series_colors: Vec<Color>,
94 pub series_symbol: Option<Symbol>,
95 pub series_smooth: bool,
96 pub series_fill: bool,
97
98 pub series_symbol_sizes: Vec<f32>,
100}
101
102impl ScatterChart {
103 pub fn from_json(data: &str) -> canvas::Result<ScatterChart> {
105 let mut s = ScatterChart {
106 ..Default::default()
107 };
108 let value = s.fill_option(data)?;
109 s.fill_default();
110
111 if let Some(series_symbol_sizes) = get_f32_slice_from_value(&value, "series_symbol_sizes") {
112 s.series_symbol_sizes = series_symbol_sizes;
113 }
114 if let Some(x_axis_hidden) = get_bool_from_value(&value, "x_axis_hidden") {
115 s.x_axis_hidden = x_axis_hidden;
116 }
117 if let Some(y_axis_hidden) = get_bool_from_value(&value, "y_axis_hidden") {
118 s.y_axis_hidden = y_axis_hidden;
119 }
120 let theme = get_string_from_value(&value, "theme").unwrap_or_default();
121 if let Some(x_axis_config) = value.get("x_axis_config") {
122 s.x_axis_config = get_y_axis_config_from_value(get_theme(&theme), x_axis_config);
123 }
124 Ok(s)
125 }
126 pub fn new_with_theme(series_list: Vec<Series>, theme: &str) -> ScatterChart {
128 let mut s = ScatterChart {
129 series_list,
130 ..Default::default()
131 };
132 let theme = get_theme(theme);
133 s.fill_theme(theme);
134 s.fill_default();
135
136 s
137 }
138 fn fill_default(&mut self) {
139 if self.y_axis_configs[0].axis_stroke_color.is_zero() {
140 self.y_axis_configs[0].axis_stroke_color = self.x_axis_stroke_color;
141 }
142 if self.x_axis_config.axis_split_number == 0 {
143 self.x_axis_config = self.y_axis_configs[0].clone();
144 }
145 self.x_boundary_gap = Some(false);
146 }
147 pub fn new(series_list: Vec<Series>) -> ScatterChart {
149 ScatterChart::new_with_theme(series_list, &get_default_theme_name())
150 }
151 pub fn svg(&self) -> canvas::Result<String> {
153 let mut c = Canvas::new_width_xy(self.width, self.height, self.x, self.y);
154
155 self.render_background(c.child(Box::default()));
156 let mut x_axis_height = self.x_axis_height;
157 if self.x_axis_hidden {
158 x_axis_height = 0.0;
159 }
160 c.margin = self.margin.clone();
161
162 let title_height = self.render_title(c.child(Box::default()));
163
164 let legend_height = self.render_legend(c.child(Box::default()));
165 let axis_top = if legend_height > title_height {
167 legend_height
168 } else {
169 title_height
170 };
171
172 let y_axis_config = self.get_y_axis_config(0);
173
174 let mut y_axis_data_list = vec![];
175 let mut x_axis_data_list = vec![];
176 for series in self.series_list.iter() {
177 for (index, data) in series.data.iter().enumerate() {
178 if index % 2 == 0 {
179 x_axis_data_list.push(*data);
180 } else {
181 y_axis_data_list.push(*data);
182 }
183 }
184 }
185 let y_axis_values = get_axis_values(AxisValueParams {
186 data_list: y_axis_data_list,
187 split_number: y_axis_config.axis_split_number,
188 reverse: Some(true),
189 min: y_axis_config.axis_min,
190 max: y_axis_config.axis_max,
191 thousands_format: false,
192 });
193 let y_axis_width = if self.y_axis_hidden {
194 0.0
195 } else if let Some(value) = y_axis_config.axis_width {
196 value
197 } else {
198 let y_axis_formatter = &y_axis_config.axis_formatter.clone().unwrap_or_default();
199 let str = format_string(&y_axis_values.data[0], y_axis_formatter);
200 if let Ok(b) =
201 measure_text_width_family(&self.font_family, y_axis_config.axis_font_size, &str)
202 {
203 b.width() + 5.0
204 } else {
205 DEFAULT_Y_AXIS_WIDTH
206 }
207 };
208
209 let axis_height = c.height() - x_axis_height - axis_top;
210 let axis_width = c.width() - y_axis_width;
211 if axis_top > 0.0 {
213 c = c.child(Box {
214 top: axis_top,
215 ..Default::default()
216 });
217 }
218
219 self.render_grid(
221 c.child(Box {
222 left: y_axis_width,
223 ..Default::default()
224 }),
225 axis_width,
226 axis_height,
227 );
228 let x_axis_width = c.width() - y_axis_width;
229 c.child(Box {
230 left: y_axis_width,
231 ..Default::default()
232 })
233 .grid(Grid {
234 right: x_axis_width,
235 bottom: axis_height,
236 color: Some(self.grid_stroke_color),
237 stroke_width: self.grid_stroke_width,
238 verticals: y_axis_config.axis_split_number,
239 hidden_verticals: vec![0],
240 ..Default::default()
241 });
242
243 if !self.y_axis_hidden {
245 self.render_y_axis(
246 c.child(Box::default()),
247 y_axis_values.data.clone(),
248 axis_height,
249 y_axis_width,
250 0,
251 );
252 }
253
254 let x_axis_values = get_axis_values(AxisValueParams {
256 data_list: x_axis_data_list,
257 split_number: self.x_axis_config.axis_split_number,
258 min: self.x_axis_config.axis_min,
259 max: self.x_axis_config.axis_max,
260 ..Default::default()
261 });
262 let x_axis_formatter = &self
263 .x_axis_config
264 .axis_formatter
265 .clone()
266 .unwrap_or_default();
267 let content_width = c.width() - y_axis_width;
268 let content_height = axis_height;
269 if !self.x_axis_hidden {
270 self.render_x_axis(
271 c.child(Box {
272 top: c.height() - x_axis_height,
273 left: y_axis_width,
274 ..Default::default()
275 }),
276 x_axis_values
277 .data
278 .iter()
279 .map(|item| format_string(item, x_axis_formatter))
280 .collect(),
281 axis_width,
282 );
283 }
284
285 let mut content_canvas = c.child(Box {
287 left: y_axis_width,
288 ..Default::default()
289 });
290 let default_symbol_size = 10.0_f32;
291 for (index, series) in self.series_list.iter().enumerate() {
292 let mut color = get_color(&self.series_colors, series.index.unwrap_or(index));
293 let symbol_size = self
294 .series_symbol_sizes
295 .get(series.index.unwrap_or(index))
296 .unwrap_or(&default_symbol_size);
297 color = color.with_alpha(210);
298 for chunk in series.data.chunks(2) {
299 if chunk.len() != 2 {
300 continue;
301 }
302 let x = content_width - x_axis_values.get_offset_height(chunk[0], content_width);
303 let y = y_axis_values.get_offset_height(chunk[1], content_height);
304 content_canvas.circle(Circle {
305 fill: Some(color),
306 cx: x,
307 cy: y,
308 r: *symbol_size,
309 ..Default::default()
310 });
311 }
312 }
313
314 c.svg()
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use super::ScatterChart;
321 use crate::Align;
322 use pretty_assertions::assert_eq;
323 #[test]
324 fn scatter_chart_basic() {
325 let mut scatter_chart = ScatterChart::new(vec![
326 (
327 "Female",
328 vec![
329 161.2, 51.6, 167.5, 59.0, 159.5, 49.2, 157.0, 63.0, 155.8, 53.6, 170.0, 59.0,
330 159.1, 47.6, 166.0, 69.8, 176.2, 66.8, 160.2, 75.2, 172.5, 55.2, 170.9, 54.2,
331 172.9, 62.5, 153.4, 42.0, 160.0, 50.0, 147.2, 49.8, 168.2, 49.2, 175.0, 73.2,
332 157.0, 47.8, 167.6, 68.8, 159.5, 50.6, 175.0, 82.5, 166.8, 57.2, 176.5, 87.8,
333 170.2, 72.8,
334 ],
335 )
336 .into(),
337 (
338 "Male",
339 vec![
340 174.0, 65.6, 175.3, 71.8, 193.5, 80.7, 186.5, 72.6, 187.2, 78.8, 181.5, 74.8,
341 184.0, 86.4, 184.5, 78.4, 175.0, 62.0, 184.0, 81.6, 180.0, 76.6, 177.8, 83.6,
342 192.0, 90.0, 176.0, 74.6, 174.0, 71.0, 184.0, 79.6, 192.7, 93.8, 171.5, 70.0,
343 173.0, 72.4, 176.0, 85.9, 176.0, 78.8, 180.5, 77.8, 172.7, 66.2, 176.0, 86.4,
344 173.5, 81.8,
345 ],
346 )
347 .into(),
348 ]);
349
350 scatter_chart.title_text = "Male and female height and weight distribution".to_string();
351 scatter_chart.margin.right = 20.0;
352 scatter_chart.title_align = Align::Left;
353 scatter_chart.sub_title_text = "Data from: Heinz 2003".to_string();
354 scatter_chart.sub_title_align = Align::Left;
355 scatter_chart.legend_align = Align::Right;
356 scatter_chart.y_axis_configs[0].axis_min = Some(40.0);
357 scatter_chart.y_axis_configs[0].axis_max = Some(130.0);
358 scatter_chart.y_axis_configs[0].axis_formatter = Some("{c} kg".to_string());
359
360 scatter_chart.x_axis_config.axis_min = Some(140.0);
361 scatter_chart.x_axis_config.axis_max = Some(230.0);
362 scatter_chart.x_axis_config.axis_formatter = Some("{c} cm".to_string());
363
364 scatter_chart.series_symbol_sizes = vec![6.0, 6.0];
365
366 assert_eq!(
367 include_str!("../../asset/scatter_chart/basic.svg"),
368 scatter_chart.svg().unwrap()
369 );
370 }
371
372 #[test]
373 fn scatter_chart_no_axis() {
374 let mut scatter_chart = ScatterChart::new(vec![
375 (
376 "Female",
377 vec![
378 161.2, 51.6, 167.5, 59.0, 159.5, 49.2, 157.0, 63.0, 155.8, 53.6, 170.0, 59.0,
379 159.1, 47.6, 166.0, 69.8, 176.2, 66.8, 160.2, 75.2, 172.5, 55.2, 170.9, 54.2,
380 172.9, 62.5, 153.4, 42.0, 160.0, 50.0, 147.2, 49.8, 168.2, 49.2, 175.0, 73.2,
381 157.0, 47.8, 167.6, 68.8, 159.5, 50.6, 175.0, 82.5, 166.8, 57.2, 176.5, 87.8,
382 170.2, 72.8,
383 ],
384 )
385 .into(),
386 (
387 "Male",
388 vec![
389 174.0, 65.6, 175.3, 71.8, 193.5, 80.7, 186.5, 72.6, 187.2, 78.8, 181.5, 74.8,
390 184.0, 86.4, 184.5, 78.4, 175.0, 62.0, 184.0, 81.6, 180.0, 76.6, 177.8, 83.6,
391 192.0, 90.0, 176.0, 74.6, 174.0, 71.0, 184.0, 79.6, 192.7, 93.8, 171.5, 70.0,
392 173.0, 72.4, 176.0, 85.9, 176.0, 78.8, 180.5, 77.8, 172.7, 66.2, 176.0, 86.4,
393 173.5, 81.8,
394 ],
395 )
396 .into(),
397 ]);
398
399 scatter_chart.title_text = "Male and female height and weight distribution".to_string();
400 scatter_chart.margin.right = 20.0;
401 scatter_chart.title_align = Align::Left;
402 scatter_chart.sub_title_text = "Data from: Heinz 2003".to_string();
403 scatter_chart.sub_title_align = Align::Left;
404 scatter_chart.legend_align = Align::Right;
405 scatter_chart.y_axis_configs[0].axis_min = Some(40.0);
406 scatter_chart.y_axis_configs[0].axis_max = Some(130.0);
407 scatter_chart.y_axis_configs[0].axis_formatter = Some("{c} kg".to_string());
408
409 scatter_chart.x_axis_config.axis_min = Some(140.0);
410 scatter_chart.x_axis_config.axis_max = Some(230.0);
411 scatter_chart.x_axis_config.axis_formatter = Some("{c} cm".to_string());
412
413 scatter_chart.series_symbol_sizes = vec![6.0, 6.0];
414 scatter_chart.x_axis_hidden = true;
415 scatter_chart.y_axis_hidden = true;
416
417 assert_eq!(
418 include_str!("../../asset/scatter_chart/no_axis.svg"),
419 scatter_chart.svg().unwrap()
420 );
421 }
422}