charts_rs/charts/
scatter_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 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    // 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    // x axis
66    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    // y axis
80    pub y_axis_hidden: bool,
81    pub y_axis_configs: Vec<YAxisConfig>,
82
83    // grid
84    pub grid_stroke_color: Color,
85    pub grid_stroke_width: f32,
86
87    // series
88    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    // symbol
99    pub series_symbol_sizes: Vec<f32>,
100}
101
102impl ScatterChart {
103    /// Creates a scatter chart from json.
104    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    /// Creates a scatter chart with  theme.
127    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    /// Creates a scatter chart with default theme.
148    pub fn new(series_list: Vec<Series>) -> ScatterChart {
149        ScatterChart::new_with_theme(series_list, &get_default_theme_name())
150    }
151    /// Converts scatter chart to svg.
152    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        // get the max height of title and legend
166        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        // minus the height of top text area
212        if axis_top > 0.0 {
213            c = c.child(Box {
214                top: axis_top,
215                ..Default::default()
216            });
217        }
218
219        // grid
220        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        // y axis
244        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        // x axis
255        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        // render dot
286        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}