charts_rs/charts/
multi_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::component::generate_svg;
15use super::component::Rect;
16use super::params::{get_color_from_value, get_f32_from_value, get_margin_from_value};
17use super::{
18    BarChart, CandlestickChart, CanvasResult, HorizontalBarChart, LineChart, PieChart, RadarChart,
19    ScatterChart, TableChart,
20};
21use super::{Box, Color};
22use substring::Substring;
23
24pub enum ChildChart {
25    Bar(BarChart, Option<(f32, f32)>),
26    Candlestick(CandlestickChart, Option<(f32, f32)>),
27    HorizontalBar(HorizontalBarChart, Option<(f32, f32)>),
28    Line(LineChart, Option<(f32, f32)>),
29    Pie(PieChart, Option<(f32, f32)>),
30    Radar(RadarChart, Option<(f32, f32)>),
31    Scatter(ScatterChart, Option<(f32, f32)>),
32    Table(TableChart, Option<(f32, f32)>),
33}
34#[derive(Default)]
35pub struct MultiChart {
36    pub charts: Vec<ChildChart>,
37    pub gap: f32,
38    pub margin: Box,
39    pub background_color: Option<Color>,
40}
41struct ChildChartResult {
42    svg: String,
43    right: f32,
44    bottom: f32,
45}
46
47impl MultiChart {
48    /// Creates a multi chart from json.
49    pub fn from_json(data: &str) -> canvas::Result<MultiChart> {
50        let value: serde_json::Value = serde_json::from_str(data)?;
51        let mut theme = "".to_string();
52        if let Some(value) = value.get("theme") {
53            theme = value.to_string();
54        }
55        let mut multi_chart = MultiChart::new();
56        if let Some(margin) = get_margin_from_value(&value, "margin") {
57            multi_chart.margin = margin;
58        }
59        if let Some(gap) = get_f32_from_value(&value, "gap") {
60            multi_chart.gap = gap;
61        }
62        if let Some(background_color) = get_color_from_value(&value, "background_color") {
63            multi_chart.background_color = Some(background_color);
64        }
65        if let Some(child_charts) = value.get("child_charts") {
66            if let Some(values) = child_charts.as_array() {
67                for item in values.iter() {
68                    let chart_type = if let Some(value) = item.get("type") {
69                        value.as_str().unwrap_or_default()
70                    } else {
71                        ""
72                    };
73                    let mut x = 0.0;
74                    let mut y = 0.0;
75                    let mut exists_position = false;
76                    if let Some(v) = get_f32_from_value(item, "x") {
77                        x = v;
78                        exists_position = true;
79                    }
80                    if let Some(v) = get_f32_from_value(item, "y") {
81                        y = v;
82                        exists_position = true;
83                    }
84                    let mut position = None;
85                    if exists_position {
86                        position = Some((x, y));
87                    }
88
89                    // 由json转换,因此不会出错
90                    let mut str = serde_json::to_string(item).unwrap();
91                    if item.get("theme").is_none() {
92                        str = format!(
93                            r###"{},"theme":{theme}}}"###,
94                            str.substring(0, str.len() - 1)
95                        );
96                    }
97                    match chart_type {
98                        "line" => {
99                            let chart = LineChart::from_json(&str)?;
100                            multi_chart.add(ChildChart::Line(chart, position));
101                        }
102                        "horizontal_bar" => {
103                            let chart = HorizontalBarChart::from_json(&str)?;
104                            multi_chart.add(ChildChart::HorizontalBar(chart, position));
105                        }
106                        "pie" => {
107                            let chart = PieChart::from_json(&str)?;
108                            multi_chart.add(ChildChart::Pie(chart, position));
109                        }
110                        "radar" => {
111                            let chart = RadarChart::from_json(&str)?;
112                            multi_chart.add(ChildChart::Radar(chart, position));
113                        }
114                        "table" => {
115                            let chart = TableChart::from_json(&str)?;
116                            multi_chart.add(ChildChart::Table(chart, position));
117                        }
118                        "scatter" => {
119                            let chart = ScatterChart::from_json(&str)?;
120                            multi_chart.add(ChildChart::Scatter(chart, position));
121                        }
122                        "candlestick" => {
123                            let chart = CandlestickChart::from_json(&str)?;
124                            multi_chart.add(ChildChart::Candlestick(chart, position));
125                        }
126                        _ => {
127                            let chart = BarChart::from_json(&str)?;
128                            multi_chart.add(ChildChart::Bar(chart, position));
129                        }
130                    };
131                }
132            }
133        }
134        Ok(multi_chart)
135    }
136    /// Creates a multi chart.
137    pub fn new() -> MultiChart {
138        MultiChart {
139            charts: vec![],
140            gap: 10.0,
141            margin: (10.0).into(),
142            ..Default::default()
143        }
144    }
145    /// Adds a child chart to multi chart.
146    pub fn add(&mut self, c: ChildChart) {
147        self.charts.push(c);
148    }
149    /// Converts the chart to svg.
150    pub fn svg(&mut self) -> CanvasResult<String> {
151        let mut arr = vec![];
152        let mut y = 0.0;
153        let mut x = 0.0;
154        let margin_top = self.margin.top;
155        let margin_left = self.margin.left;
156        for item in self.charts.iter_mut() {
157            let result = match item {
158                ChildChart::Bar(c, position) => {
159                    c.y = y;
160                    // fix postion, no need  gap
161                    if let Some((x, y)) = position {
162                        y.clone_into(&mut c.y);
163                        x.clone_into(&mut c.x);
164                    } else if y == 0.0 {
165                        c.y = margin_top;
166                    } else {
167                        // not the first chart and not set position
168                        y += self.gap;
169                        c.y = y;
170                    }
171                    if position.is_none() {
172                        c.x = c.x.max(margin_left);
173                    }
174
175                    ChildChartResult {
176                        svg: c.svg()?,
177                        right: c.x + c.width,
178                        bottom: c.y + c.height,
179                    }
180                }
181                ChildChart::Candlestick(c, position) => {
182                    c.y = y;
183                    if let Some((x, y)) = position {
184                        y.clone_into(&mut c.y);
185                        x.clone_into(&mut c.x);
186                    } else if y == 0.0 {
187                        c.y = margin_top;
188                    } else {
189                        // not the first chart and not set position
190                        y += self.gap;
191                        c.y = y;
192                    }
193                    if position.is_none() {
194                        c.x = c.x.max(margin_left);
195                    }
196
197                    ChildChartResult {
198                        svg: c.svg()?,
199                        right: c.x + c.width,
200                        bottom: c.y + c.height,
201                    }
202                }
203                ChildChart::HorizontalBar(c, position) => {
204                    c.y = y;
205                    if let Some((x, y)) = position {
206                        y.clone_into(&mut c.y);
207                        x.clone_into(&mut c.x);
208                    } else if y == 0.0 {
209                        c.y = margin_top;
210                    } else {
211                        y += self.gap;
212                        c.y = y;
213                    }
214                    if position.is_none() {
215                        c.x = c.x.max(margin_left);
216                    }
217
218                    ChildChartResult {
219                        svg: c.svg()?,
220                        right: c.x + c.width,
221                        bottom: c.y + c.height,
222                    }
223                }
224                ChildChart::Line(c, position) => {
225                    c.y = y;
226                    if let Some((x, y)) = position {
227                        y.clone_into(&mut c.y);
228                        x.clone_into(&mut c.x);
229                    } else if y == 0.0 {
230                        c.y = margin_top;
231                    } else {
232                        y += self.gap;
233                        c.y = y;
234                    }
235                    if position.is_none() {
236                        c.x = c.x.max(margin_left);
237                    }
238
239                    ChildChartResult {
240                        svg: c.svg()?,
241                        right: c.x + c.width,
242                        bottom: c.y + c.height,
243                    }
244                }
245                ChildChart::Pie(c, position) => {
246                    c.y = y;
247                    if let Some((x, y)) = position {
248                        y.clone_into(&mut c.y);
249                        x.clone_into(&mut c.x);
250                    } else if y == 0.0 {
251                        c.y = margin_top;
252                    } else {
253                        y += self.gap;
254                        c.y = y;
255                    }
256                    if position.is_none() {
257                        c.x = c.x.max(margin_left);
258                    }
259
260                    ChildChartResult {
261                        svg: c.svg()?,
262                        right: c.x + c.width,
263                        bottom: c.y + c.height,
264                    }
265                }
266                ChildChart::Radar(c, position) => {
267                    c.y = y;
268                    if let Some((x, y)) = position {
269                        y.clone_into(&mut c.y);
270                        x.clone_into(&mut c.x);
271                    } else if y == 0.0 {
272                        c.y = margin_top;
273                    } else {
274                        y += self.gap;
275                        c.y = y;
276                    }
277                    if position.is_none() {
278                        c.x = c.x.max(margin_left);
279                    }
280
281                    ChildChartResult {
282                        svg: c.svg()?,
283                        right: c.x + c.width,
284                        bottom: c.y + c.height,
285                    }
286                }
287                ChildChart::Scatter(c, position) => {
288                    c.y = y;
289                    if let Some((x, y)) = position {
290                        y.clone_into(&mut c.y);
291                        x.clone_into(&mut c.x);
292                    } else if y == 0.0 {
293                        c.y = margin_top;
294                    } else {
295                        y += self.gap;
296                        c.y = y;
297                    }
298                    if position.is_none() {
299                        c.x = c.x.max(margin_left);
300                    }
301
302                    ChildChartResult {
303                        svg: c.svg()?,
304                        right: c.x + c.width,
305                        bottom: c.y + c.height,
306                    }
307                }
308                ChildChart::Table(c, position) => {
309                    c.y = y;
310                    if let Some((x, y)) = position {
311                        y.clone_into(&mut c.y);
312                        x.clone_into(&mut c.x);
313                    } else if y == 0.0 {
314                        c.y = margin_top;
315                    } else {
316                        y += self.gap;
317                        c.y = y;
318                    }
319                    if position.is_none() {
320                        c.x = c.x.max(margin_left);
321                    }
322                    // the height will be recount
323                    let svg = c.svg()?;
324                    ChildChartResult {
325                        svg,
326                        right: c.x + c.width,
327                        bottom: c.y + c.height,
328                    }
329                }
330            };
331            if result.bottom > y {
332                y = result.bottom;
333            }
334            if result.right > x {
335                x = result.right;
336            }
337            arr.push(result.svg);
338        }
339        x += self.margin.right;
340        y += self.margin.bottom;
341
342        if let Some(background_color) = self.background_color {
343            arr.insert(
344                0,
345                Rect {
346                    fill: Some(background_color),
347                    left: 0.0,
348                    top: 0.0,
349                    width: x,
350                    height: y,
351                    ..Default::default()
352                }
353                .svg(),
354            );
355        }
356
357        Ok(generate_svg(x, y, 0.0, 0.0, arr.join("\n")))
358    }
359}
360
361#[cfg(test)]
362mod tests {
363    use super::{ChildChart, MultiChart};
364    use crate::{
365        BarChart, CandlestickChart, HorizontalBarChart, LineChart, PieChart, RadarChart,
366        ScatterChart, TableChart,
367    };
368    use pretty_assertions::assert_eq;
369    #[test]
370    fn multi_chart() {
371        let mut charts = MultiChart::new();
372        charts.margin = (10.0).into();
373        charts.background_color = Some((31, 29, 29, 150).into());
374
375        let bar_chart = BarChart::new(
376            vec![
377                (
378                    "Email",
379                    vec![120.0, 132.0, 101.0, 134.0, 90.0, 230.0, 210.0],
380                )
381                    .into(),
382                (
383                    "Union Ads",
384                    vec![220.0, 182.0, 191.0, 234.0, 290.0, 330.0, 310.0],
385                )
386                    .into(),
387                (
388                    "Direct",
389                    vec![320.0, 332.0, 301.0, 334.0, 390.0, 330.0, 320.0],
390                )
391                    .into(),
392                (
393                    "Search Engine",
394                    vec![820.0, 932.0, 901.0, 934.0, 1290.0, 1330.0, 1320.0],
395                )
396                    .into(),
397            ],
398            vec![
399                "Mon".to_string(),
400                "Tue".to_string(),
401                "Wed".to_string(),
402                "Thu".to_string(),
403                "Fri".to_string(),
404                "Sat".to_string(),
405                "Sun".to_string(),
406            ],
407        );
408        charts.add(ChildChart::Bar(bar_chart, None));
409
410        let candlestick_chart = CandlestickChart::new(
411            vec![(
412                "",
413                vec![
414                    20.0, 34.0, 10.0, 38.0, 40.0, 35.0, 30.0, 50.0, 31.0, 38.0, 33.0, 44.0, 38.0,
415                    15.0, 5.0, 42.0,
416                ],
417            )
418                .into()],
419            vec![
420                "2017-10-24".to_string(),
421                "2017-10-25".to_string(),
422                "2017-10-26".to_string(),
423                "2017-10-27".to_string(),
424            ],
425        );
426        charts.add(ChildChart::Candlestick(candlestick_chart, None));
427
428        let horizontal_bar_chart = HorizontalBarChart::new(
429            vec![
430                (
431                    "2011",
432                    vec![18203.0, 23489.0, 29034.0, 104970.0, 131744.0, 630230.0],
433                )
434                    .into(),
435                (
436                    "2012",
437                    vec![19325.0, 23438.0, 31000.0, 121594.0, 134141.0, 681807.0],
438                )
439                    .into(),
440            ],
441            vec![
442                "Brazil".to_string(),
443                "Indonesia".to_string(),
444                "USA".to_string(),
445                "India".to_string(),
446                "China".to_string(),
447                "World".to_string(),
448            ],
449        );
450        charts.add(ChildChart::HorizontalBar(horizontal_bar_chart, None));
451
452        let line_chart = LineChart::new(
453            vec![
454                (
455                    "Email",
456                    vec![120.0, 132.0, 101.0, 134.0, 90.0, 230.0, 210.0],
457                )
458                    .into(),
459                (
460                    "Union Ads",
461                    vec![220.0, 182.0, 191.0, 234.0, 290.0, 330.0, 310.0],
462                )
463                    .into(),
464                (
465                    "Direct",
466                    vec![320.0, 332.0, 301.0, 334.0, 390.0, 330.0, 320.0],
467                )
468                    .into(),
469                (
470                    "Search Engine",
471                    vec![820.0, 932.0, 901.0, 934.0, 1290.0, 1330.0, 1320.0],
472                )
473                    .into(),
474            ],
475            vec![
476                "Mon".to_string(),
477                "Tue".to_string(),
478                "Wed".to_string(),
479                "Thu".to_string(),
480                "Fri".to_string(),
481                "Sat".to_string(),
482                "Sun".to_string(),
483            ],
484        );
485        charts.add(ChildChart::Line(line_chart, None));
486
487        let pie_chart = PieChart::new(vec![
488            ("rose 1", vec![40.0]).into(),
489            ("rose 2", vec![38.0]).into(),
490            ("rose 3", vec![32.0]).into(),
491            ("rose 4", vec![30.0]).into(),
492            ("rose 5", vec![28.0]).into(),
493            ("rose 6", vec![26.0]).into(),
494            ("rose 7", vec![22.0]).into(),
495            ("rose 8", vec![18.0]).into(),
496        ]);
497
498        charts.add(ChildChart::Pie(pie_chart, None));
499
500        let radar_chart = RadarChart::new(
501            vec![
502                (
503                    "Allocated Budget",
504                    vec![4200.0, 3000.0, 20000.0, 35000.0, 50000.0, 18000.0],
505                )
506                    .into(),
507                (
508                    "Actual Spending",
509                    vec![5000.0, 14000.0, 28000.0, 26000.0, 42000.0, 21000.0],
510                )
511                    .into(),
512            ],
513            vec![
514                ("Sales", 6500.0).into(),
515                ("Administration", 16000.0).into(),
516                ("Information Technology", 30000.0).into(),
517                ("Customer Support", 38000.0).into(),
518                ("Development", 52000.0).into(),
519                ("Marketing", 25000.0).into(),
520            ],
521        );
522        charts.add(ChildChart::Radar(radar_chart, None));
523
524        let scatter_chart = ScatterChart::new(vec![
525            (
526                "Female",
527                vec![
528                    161.2, 51.6, 167.5, 59.0, 159.5, 49.2, 157.0, 63.0, 155.8, 53.6, 170.0, 59.0,
529                    159.1, 47.6, 166.0, 69.8, 176.2, 66.8, 160.2, 75.2, 172.5, 55.2, 170.9, 54.2,
530                    172.9, 62.5, 153.4, 42.0, 160.0, 50.0, 147.2, 49.8, 168.2, 49.2, 175.0, 73.2,
531                    157.0, 47.8, 167.6, 68.8, 159.5, 50.6, 175.0, 82.5, 166.8, 57.2, 176.5, 87.8,
532                    170.2, 72.8,
533                ],
534            )
535                .into(),
536            (
537                "Male",
538                vec![
539                    174.0, 65.6, 175.3, 71.8, 193.5, 80.7, 186.5, 72.6, 187.2, 78.8, 181.5, 74.8,
540                    184.0, 86.4, 184.5, 78.4, 175.0, 62.0, 184.0, 81.6, 180.0, 76.6, 177.8, 83.6,
541                    192.0, 90.0, 176.0, 74.6, 174.0, 71.0, 184.0, 79.6, 192.7, 93.8, 171.5, 70.0,
542                    173.0, 72.4, 176.0, 85.9, 176.0, 78.8, 180.5, 77.8, 172.7, 66.2, 176.0, 86.4,
543                    173.5, 81.8,
544                ],
545            )
546                .into(),
547        ]);
548        charts.add(ChildChart::Scatter(scatter_chart, None));
549
550        let table_chart = TableChart::new(vec![
551            vec![
552                "Name".to_string(),
553                "Price".to_string(),
554                "Change".to_string(),
555            ],
556            vec![
557                "Datadog Inc".to_string(),
558                "97.32".to_string(),
559                "-7.49%".to_string(),
560            ],
561            vec![
562                "Hashicorp Inc".to_string(),
563                "28.66".to_string(),
564                "-9.25%".to_string(),
565            ],
566            vec![
567                "Gitlab Inc".to_string(),
568                "51.63".to_string(),
569                "+4.32%".to_string(),
570            ],
571        ]);
572        charts.add(ChildChart::Table(table_chart, None));
573
574        assert_eq!(
575            include_str!("../../asset/multi_chart/basic.svg"),
576            charts.svg().unwrap()
577        );
578    }
579
580    #[test]
581    fn multi_chart_override() {
582        let mut charts = MultiChart::new();
583        let bar_chart = BarChart::new(
584            vec![
585                (
586                    "Email",
587                    vec![120.0, 132.0, 101.0, 134.0, 90.0, 230.0, 210.0],
588                )
589                    .into(),
590                (
591                    "Union Ads",
592                    vec![220.0, 182.0, 191.0, 234.0, 290.0, 330.0, 310.0],
593                )
594                    .into(),
595                (
596                    "Direct",
597                    vec![320.0, 332.0, 301.0, 334.0, 390.0, 330.0, 320.0],
598                )
599                    .into(),
600                (
601                    "Search Engine",
602                    vec![820.0, 932.0, 901.0, 934.0, 1290.0, 1330.0, 1320.0],
603                )
604                    .into(),
605            ],
606            vec![
607                "Mon".to_string(),
608                "Tue".to_string(),
609                "Wed".to_string(),
610                "Thu".to_string(),
611                "Fri".to_string(),
612                "Sat".to_string(),
613                "Sun".to_string(),
614            ],
615        );
616        charts.add(ChildChart::Bar(bar_chart, None));
617
618        let mut pie_chart = PieChart::new(vec![
619            ("rose 1", vec![40.0]).into(),
620            ("rose 2", vec![38.0]).into(),
621            ("rose 3", vec![32.0]).into(),
622            ("rose 4", vec![30.0]).into(),
623            ("rose 5", vec![28.0]).into(),
624            ("rose 6", vec![26.0]).into(),
625            ("rose 7", vec![22.0]).into(),
626            ("rose 8", vec![18.0]).into(),
627        ]);
628        pie_chart.width = 400.0;
629        pie_chart.height = 200.0;
630        pie_chart.background_color = (0, 0, 0, 0).into();
631
632        charts.add(ChildChart::Pie(pie_chart, Some((200.0, 0.0))));
633
634        assert_eq!(
635            include_str!("../../asset/multi_chart/override.svg"),
636            charts.svg().unwrap()
637        );
638    }
639}