charts_rs/charts/
bar_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 BarChart {
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_hidden: bool,
76    pub x_boundary_gap: Option<bool>,
77
78    // y axis
79    pub y_axis_hidden: bool,
80    pub y_axis_configs: Vec<YAxisConfig>,
81
82    // grid
83    pub grid_stroke_color: Color,
84    pub grid_stroke_width: f32,
85
86    // series
87    pub series_stroke_width: f32,
88    pub series_label_font_color: Color,
89    pub series_label_font_size: f32,
90    pub series_label_font_weight: Option<String>,
91    pub series_label_formatter: String,
92    pub series_colors: Vec<Color>,
93    pub series_symbol: Option<Symbol>,
94    pub series_smooth: bool,
95    pub series_fill: bool,
96
97    pub radius: Option<f32>,
98}
99
100impl BarChart {
101    /// Creates a bar chart from json.
102    pub fn from_json(data: &str) -> canvas::Result<BarChart> {
103        let mut b = BarChart {
104            ..Default::default()
105        };
106        let value = b.fill_option(data)?;
107        if let Some(x_axis_hidden) = get_bool_from_value(&value, "x_axis_hidden") {
108            b.x_axis_hidden = x_axis_hidden;
109        }
110        if let Some(y_axis_hidden) = get_bool_from_value(&value, "y_axis_hidden") {
111            b.y_axis_hidden = y_axis_hidden;
112        }
113        if let Some(radius) = get_f32_from_value(&value, "radius") {
114            b.radius = Some(radius);
115        }
116        Ok(b)
117    }
118    /// Creates a bar chart with custom theme.
119    pub fn new_with_theme(
120        mut series_list: Vec<Series>,
121        x_axis_data: Vec<String>,
122        theme: &str,
123    ) -> BarChart {
124        // bar chart supports line and bar,
125        // so sets the index first
126        series_list
127            .iter_mut()
128            .enumerate()
129            .for_each(|(index, item)| {
130                item.index = Some(index);
131            });
132        let mut b = BarChart {
133            series_list,
134            x_axis_data,
135            ..Default::default()
136        };
137        let theme = get_theme(theme);
138        b.fill_theme(theme);
139        b
140    }
141    /// Creates a bar chart with default theme.
142    pub fn new(series_list: Vec<Series>, x_axis_data: Vec<String>) -> BarChart {
143        BarChart::new_with_theme(series_list, x_axis_data, &get_default_theme_name())
144    }
145    /// Converts bar chart to svg.
146    pub fn svg(&self) -> canvas::Result<String> {
147        let mut c = Canvas::new_width_xy(self.width, self.height, self.x, self.y);
148
149        self.render_background(c.child(Box::default()));
150        let mut x_axis_height = self.x_axis_height;
151        if self.x_axis_hidden {
152            x_axis_height = 0.0;
153        }
154        c.margin = self.margin.clone();
155
156        let title_height = self.render_title(c.child(Box::default()));
157
158        let legend_height = self.render_legend(c.child(Box::default()));
159        // get the max height of title and legend
160        let axis_top = if legend_height > title_height {
161            legend_height
162        } else {
163            title_height
164        };
165
166        let (left_y_axis_values, mut left_y_axis_width) = self.get_y_axis_values(0);
167        if self.y_axis_hidden {
168            left_y_axis_width = 0.0;
169        }
170        let mut exist_right_y_axis = false;
171        // check the right y axis
172        for series in self.series_list.iter() {
173            if series.y_axis_index != 0 {
174                exist_right_y_axis = true;
175            }
176        }
177        let mut right_y_axis_values = AxisValues::default();
178        let mut right_y_axis_width = 0.0_f32;
179        if !self.y_axis_hidden && exist_right_y_axis {
180            (right_y_axis_values, right_y_axis_width) = self.get_y_axis_values(1);
181        }
182
183        let axis_height = c.height() - x_axis_height - axis_top;
184        let axis_width = c.width() - left_y_axis_width - right_y_axis_width;
185        // minus the height of top text area
186        if axis_top > 0.0 {
187            c = c.child(Box {
188                top: axis_top,
189                ..Default::default()
190            });
191        }
192
193        self.render_grid(
194            c.child(Box {
195                left: left_y_axis_width,
196                ..Default::default()
197            }),
198            axis_width,
199            axis_height,
200        );
201
202        // y axis
203        if left_y_axis_width > 0.0 {
204            self.render_y_axis(
205                c.child(Box::default()),
206                left_y_axis_values.data.clone(),
207                axis_height,
208                left_y_axis_width,
209                0,
210            );
211        }
212        // render right y axis
213        if right_y_axis_width > 0.0 {
214            self.render_y_axis(
215                c.child(Box {
216                    left: c.width() - right_y_axis_width,
217                    ..Default::default()
218                }),
219                right_y_axis_values.data.clone(),
220                axis_height,
221                right_y_axis_width,
222                1,
223            );
224        }
225
226        // x axis
227        if !self.x_axis_hidden {
228            self.render_x_axis(
229                c.child(Box {
230                    top: c.height() - x_axis_height,
231                    left: left_y_axis_width,
232                    right: right_y_axis_width,
233                    ..Default::default()
234                }),
235                self.x_axis_data.clone(),
236                axis_width,
237            );
238        }
239
240        // bar point
241        let max_height = c.height() - x_axis_height;
242        let mut bar_series_list = vec![];
243        let mut line_series_list = vec![];
244        // filter line and bar series points
245        self.series_list.iter().for_each(|item| {
246            if let Some(ref cat) = item.category {
247                if *cat == SeriesCategory::Line {
248                    line_series_list.push(item);
249                    return;
250                }
251            }
252            bar_series_list.push(item);
253        });
254
255        let y_axis_values_list = vec![&left_y_axis_values, &right_y_axis_values];
256        let mut bar_series_labels_list = self.render_bar(
257            c.child(Box {
258                left: left_y_axis_width,
259                right: right_y_axis_width,
260                ..Default::default()
261            }),
262            &bar_series_list,
263            &y_axis_values_list,
264            max_height,
265            self.x_axis_data.len(),
266            self.radius,
267        );
268
269        let mut line_series_labels_list = self.render_line(
270            c.child(Box {
271                left: left_y_axis_width,
272                right: right_y_axis_width,
273                ..Default::default()
274            }),
275            &line_series_list,
276            &y_axis_values_list,
277            max_height,
278            axis_height,
279            self.x_axis_data.len(),
280        );
281
282        bar_series_labels_list.append(&mut line_series_labels_list);
283
284        self.render_series_label(
285            c.child(Box {
286                left: left_y_axis_width,
287                right: right_y_axis_width,
288                ..Default::default()
289            }),
290            bar_series_labels_list,
291        );
292
293        c.svg()
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::BarChart;
300    use crate::{
301        Box, LegendCategory, SeriesCategory, NIL_VALUE, THEME_ANT, THEME_DARK, THEME_GRAFANA,
302    };
303    use pretty_assertions::assert_eq;
304    #[test]
305    fn bar_chart_basic() {
306        let mut bar_chart = BarChart::new(
307            vec![
308                (
309                    "Email",
310                    vec![120.0, 132.0, 101.0, 134.0, 90.0, 230.0, 210.0],
311                )
312                    .into(),
313                (
314                    "Union Ads",
315                    vec![220.0, 182.0, 191.0, 234.0, 290.0, 330.0, 310.0],
316                )
317                    .into(),
318                (
319                    "Direct",
320                    vec![320.0, 332.0, 301.0, 334.0, 390.0, 330.0, 320.0],
321                )
322                    .into(),
323                (
324                    "Search Engine",
325                    vec![820.0, 932.0, 901.0, 934.0, 1290.0, 1330.0, 1320.0],
326                )
327                    .into(),
328            ],
329            vec![
330                "Mon".to_string(),
331                "Tue".to_string(),
332                "Wed".to_string(),
333                "Thu".to_string(),
334                "Fri".to_string(),
335                "Sat".to_string(),
336                "Sun".to_string(),
337            ],
338        );
339        bar_chart.y_axis_configs[0].axis_width = Some(55.0);
340        bar_chart.title_text = "Bar Chart".to_string();
341        bar_chart.legend_margin = Some(Box {
342            top: 35.0,
343            bottom: 10.0,
344            ..Default::default()
345        });
346        bar_chart.legend_category = LegendCategory::RoundRect;
347        bar_chart.y_axis_configs[0].axis_formatter = Some("{c} ml".to_string());
348        bar_chart.series_list[0].label_show = true;
349        bar_chart.series_list[0].colors = Some(vec![None, Some("#a90000".into())]);
350        assert_eq!(
351            include_str!("../../asset/bar_chart/basic.svg"),
352            bar_chart.svg().unwrap()
353        );
354    }
355    #[test]
356    fn bar_chart_basic_dark() {
357        let mut bar_chart = BarChart::new_with_theme(
358            vec![
359                (
360                    "Email",
361                    vec![120.0, 132.0, 101.0, 134.0, 90.0, 230.0, 210.0],
362                )
363                    .into(),
364                (
365                    "Union Ads",
366                    vec![220.0, 182.0, 191.0, 234.0, 290.0, 330.0, 310.0],
367                )
368                    .into(),
369                (
370                    "Direct",
371                    vec![320.0, 332.0, 301.0, 334.0, 390.0, 330.0, 320.0],
372                )
373                    .into(),
374                (
375                    "Search Engine",
376                    vec![820.0, 932.0, 901.0, 934.0, 1290.0, 1330.0, 1320.0],
377                )
378                    .into(),
379            ],
380            vec![
381                "Mon".to_string(),
382                "Tue".to_string(),
383                "Wed".to_string(),
384                "Thu".to_string(),
385                "Fri".to_string(),
386                "Sat".to_string(),
387                "Sun".to_string(),
388            ],
389            THEME_DARK,
390        );
391        bar_chart.y_axis_configs[0].axis_width = Some(55.0);
392        bar_chart.title_text = "Bar Chart".to_string();
393        bar_chart.legend_margin = Some(Box {
394            top: 35.0,
395            bottom: 10.0,
396            ..Default::default()
397        });
398        bar_chart.radius = Some(5.0);
399        bar_chart.y_axis_configs[0].axis_formatter = Some("{c} ml".to_string());
400        bar_chart.series_list[0].label_show = true;
401        bar_chart.legend_category = LegendCategory::Circle;
402        assert_eq!(
403            include_str!("../../asset/bar_chart/basic_dark.svg"),
404            bar_chart.svg().unwrap()
405        );
406    }
407
408    #[test]
409    fn bar_chart_basic_ant() {
410        let mut bar_chart = BarChart::new_with_theme(
411            vec![
412                (
413                    "Email",
414                    vec![120.0, 132.0, 101.0, 134.0, 90.0, 230.0, 210.0],
415                )
416                    .into(),
417                (
418                    "Union Ads",
419                    vec![220.0, 182.0, 191.0, 234.0, 290.0, 330.0, 310.0],
420                )
421                    .into(),
422                (
423                    "Direct",
424                    vec![320.0, 332.0, 301.0, 334.0, 390.0, 330.0, 320.0],
425                )
426                    .into(),
427                (
428                    "Search Engine",
429                    vec![820.0, 932.0, 901.0, 934.0, 1290.0, 1330.0, 1320.0],
430                )
431                    .into(),
432            ],
433            vec![
434                "Mon".to_string(),
435                "Tue".to_string(),
436                "Wed".to_string(),
437                "Thu".to_string(),
438                "Fri".to_string(),
439                "Sat".to_string(),
440                "Sun".to_string(),
441            ],
442            THEME_ANT,
443        );
444        bar_chart.y_axis_configs[0].axis_width = Some(55.0);
445        bar_chart.title_text = "Bar Chart".to_string();
446        bar_chart.legend_margin = Some(Box {
447            top: 35.0,
448            bottom: 10.0,
449            ..Default::default()
450        });
451        bar_chart.y_axis_configs[0].axis_formatter = Some("{c} ml".to_string());
452        bar_chart.series_list[0].label_show = true;
453        assert_eq!(
454            include_str!("../../asset/bar_chart/basic_ant.svg"),
455            bar_chart.svg().unwrap()
456        );
457    }
458
459    #[test]
460    fn bar_chart_basic_grafana() {
461        let mut bar_chart = BarChart::new_with_theme(
462            vec![
463                (
464                    "Email",
465                    vec![120.0, 132.0, 101.0, 134.0, 90.0, 230.0, 210.0],
466                )
467                    .into(),
468                (
469                    "Union Ads",
470                    vec![220.0, 182.0, 191.0, 234.0, 290.0, 330.0, 310.0],
471                )
472                    .into(),
473                (
474                    "Direct",
475                    vec![320.0, 332.0, 301.0, 334.0, 390.0, 330.0, 320.0],
476                )
477                    .into(),
478                (
479                    "Search Engine",
480                    vec![820.0, 932.0, 901.0, 934.0, 1290.0, 1330.0, 1320.0],
481                )
482                    .into(),
483            ],
484            vec![
485                "Mon".to_string(),
486                "Tue".to_string(),
487                "Wed".to_string(),
488                "Thu".to_string(),
489                "Fri".to_string(),
490                "Sat".to_string(),
491                "Sun".to_string(),
492            ],
493            THEME_GRAFANA,
494        );
495        bar_chart.y_axis_configs[0].axis_width = Some(55.0);
496        bar_chart.title_text = "Bar Chart".to_string();
497        bar_chart.legend_margin = Some(Box {
498            top: 35.0,
499            bottom: 10.0,
500            ..Default::default()
501        });
502        bar_chart.y_axis_configs[0].axis_formatter = Some("{c} ml".to_string());
503        bar_chart.series_list[0].label_show = true;
504        assert_eq!(
505            include_str!("../../asset/bar_chart/basic_grafana.svg"),
506            bar_chart.svg().unwrap()
507        );
508    }
509
510    #[test]
511    fn bar_chart_y_axis_min_max() {
512        let mut bar_chart = BarChart::new_with_theme(
513            vec![
514                (
515                    "Email",
516                    vec![120.0, 132.0, 101.0, 134.0, 90.0, 230.0, 210.0],
517                )
518                    .into(),
519                (
520                    "Union Ads",
521                    vec![220.0, 182.0, 191.0, 234.0, 290.0, 330.0, 310.0],
522                )
523                    .into(),
524                (
525                    "Direct",
526                    vec![320.0, 332.0, 301.0, 334.0, 390.0, 330.0, 320.0],
527                )
528                    .into(),
529                (
530                    "Search Engine",
531                    vec![820.0, 932.0, 901.0, 934.0, 1290.0, 1330.0, 1320.0],
532                )
533                    .into(),
534            ],
535            vec![
536                "Mon".to_string(),
537                "Tue".to_string(),
538                "Wed".to_string(),
539                "Thu".to_string(),
540                "Fri".to_string(),
541                "Sat".to_string(),
542                "Sun".to_string(),
543            ],
544            THEME_GRAFANA,
545        );
546        bar_chart.y_axis_configs[0].axis_width = Some(55.0);
547        bar_chart.y_axis_configs[0].axis_max = Some(1500.0);
548        bar_chart.title_text = "Bar Chart".to_string();
549        bar_chart.legend_margin = Some(Box {
550            top: 35.0,
551            bottom: 10.0,
552            ..Default::default()
553        });
554        bar_chart.y_axis_configs[0].axis_formatter = Some("{c} ml".to_string());
555        bar_chart.series_list[0].label_show = true;
556        assert_eq!(
557            include_str!("../../asset/bar_chart/y_axis_min_max.svg"),
558            bar_chart.svg().unwrap()
559        );
560    }
561
562    #[test]
563    fn bar_chart_line_mixin() {
564        let mut bar_chart = BarChart::new(
565            vec![
566                (
567                    "Email",
568                    vec![120.0, 132.0, 101.0, 134.0, 90.0, 230.0, 210.0],
569                )
570                    .into(),
571                (
572                    "Union Ads",
573                    vec![220.0, 182.0, 191.0, 234.0, 290.0, 330.0, 310.0],
574                )
575                    .into(),
576                (
577                    "Direct",
578                    vec![320.0, 332.0, 301.0, 334.0, 390.0, 330.0, 320.0],
579                )
580                    .into(),
581                (
582                    "Search Engine",
583                    vec![820.0, 932.0, 901.0, 934.0, 1290.0, 1330.0, 1320.0],
584                )
585                    .into(),
586            ],
587            vec![
588                "Mon".to_string(),
589                "Tue".to_string(),
590                "Wed".to_string(),
591                "Thu".to_string(),
592                "Fri".to_string(),
593                "Sat".to_string(),
594                "Sun".to_string(),
595            ],
596        );
597        bar_chart.series_list[0].category = Some(SeriesCategory::Line);
598        bar_chart.y_axis_configs[0].axis_width = Some(55.0);
599        bar_chart.title_text = "Bar Chart".to_string();
600        bar_chart.legend_margin = Some(Box {
601            top: 35.0,
602            bottom: 10.0,
603            ..Default::default()
604        });
605        bar_chart.legend_category = LegendCategory::Rect;
606        bar_chart.y_axis_configs[0].axis_formatter = Some("{c} ml".to_string());
607        bar_chart.series_list[0].label_show = true;
608        bar_chart.series_list[3].label_show = true;
609
610        assert_eq!(
611            include_str!("../../asset/bar_chart/line_mixin.svg"),
612            bar_chart.svg().unwrap()
613        );
614
615        #[cfg(feature = "image-encoder")]
616        {
617            use crate::svg_to_jpeg;
618            let buf = svg_to_jpeg(&bar_chart.svg().unwrap()).unwrap();
619            std::fs::write("./asset/image/line_mixin.jpeg", buf).unwrap();
620        }
621    }
622
623    #[test]
624    fn bar_chart_two_y_axis() {
625        let mut bar_chart = BarChart::new(
626            vec![
627                ("Evaporation", vec![2.0, 4.9, 7.0, 23.2, 25.6, 76.7, 135.6]).into(),
628                (
629                    "Precipitation",
630                    vec![2.6, 5.9, 9.0, 26.4, 28.7, 70.7, 175.6],
631                )
632                    .into(),
633                ("Temperature", vec![2.0, 2.2, 3.3, 4.5, 6.3, 10.2, 20.3]).into(),
634            ],
635            vec![
636                "Mon".to_string(),
637                "Tue".to_string(),
638                "Wed".to_string(),
639                "Thu".to_string(),
640                "Fri".to_string(),
641                "Sat".to_string(),
642                "Sun".to_string(),
643            ],
644        );
645        bar_chart.series_list[2].category = Some(SeriesCategory::Line);
646        bar_chart.series_list[2].y_axis_index = 1;
647
648        bar_chart.y_axis_configs[0].axis_width = Some(55.0);
649        bar_chart.title_text = "Bar Chart".to_string();
650        bar_chart.legend_margin = Some(Box {
651            top: 35.0,
652            bottom: 10.0,
653            ..Default::default()
654        });
655        bar_chart.legend_category = LegendCategory::Rect;
656        bar_chart.y_axis_configs[0].axis_formatter = Some("{c} ml".to_string());
657        bar_chart
658            .y_axis_configs
659            .push(bar_chart.y_axis_configs[0].clone());
660        bar_chart.y_axis_configs[1].axis_formatter = Some("{c} °C".to_string());
661        assert_eq!(
662            include_str!("../../asset/bar_chart/two_y_axis.svg"),
663            bar_chart.svg().unwrap()
664        );
665    }
666
667    #[test]
668    fn bar_chart_value_count_unequal() {
669        let mut bar_chart = BarChart::new(
670            vec![
671                ("Email", vec![120.0, 132.0, 101.0, 134.0, 90.0, 230.0]).into(),
672                (
673                    "Union Ads",
674                    vec![220.0, 182.0, 191.0, 234.0, 290.0, 330.0, 310.0],
675                )
676                    .into(),
677                (
678                    "Direct",
679                    vec![320.0, 332.0, 301.0, 334.0, 390.0, 330.0, 320.0],
680                )
681                    .into(),
682                (
683                    "Search Engine",
684                    vec![820.0, 932.0, 901.0, 934.0, 1290.0, 1330.0, 1320.0],
685                )
686                    .into(),
687            ],
688            vec![
689                "Mon".to_string(),
690                "Tue".to_string(),
691                "Wed".to_string(),
692                "Thu".to_string(),
693                "Fri".to_string(),
694                "Sat".to_string(),
695                "Sun".to_string(),
696            ],
697        );
698        bar_chart.y_axis_configs[0].axis_width = Some(55.0);
699        bar_chart.title_text = "Bar Chart".to_string();
700        bar_chart.legend_margin = Some(Box {
701            top: 35.0,
702            bottom: 10.0,
703            ..Default::default()
704        });
705        bar_chart.y_axis_configs[0].axis_formatter = Some("{c} ml".to_string());
706        bar_chart.series_list[0].label_show = true;
707        bar_chart.series_list[0].start_index = 1;
708        assert_eq!(
709            include_str!("../../asset/bar_chart/value_count_unequal.svg"),
710            bar_chart.svg().unwrap()
711        );
712    }
713
714    #[test]
715    fn bar_chart_nil_value() {
716        let mut bar_chart = BarChart::new(
717            vec![
718                (
719                    "Email",
720                    vec![120.0, NIL_VALUE, 132.0, 101.0, 134.0, 90.0, 230.0],
721                )
722                    .into(),
723                (
724                    "Union Ads",
725                    vec![220.0, 182.0, 191.0, NIL_VALUE, 290.0, 330.0, 310.0],
726                )
727                    .into(),
728                (
729                    "Direct",
730                    vec![320.0, 332.0, 301.0, 334.0, 390.0, NIL_VALUE, 320.0],
731                )
732                    .into(),
733                (
734                    "Search Engine",
735                    vec![NIL_VALUE, 932.0, 901.0, 934.0, 1290.0, 1330.0, 1320.0],
736                )
737                    .into(),
738            ],
739            vec![
740                "Mon".to_string(),
741                "Tue".to_string(),
742                "Wed".to_string(),
743                "Thu".to_string(),
744                "Fri".to_string(),
745                "Sat".to_string(),
746                "Sun".to_string(),
747            ],
748        );
749        bar_chart.y_axis_configs[0].axis_width = Some(55.0);
750        bar_chart.title_text = "Bar Chart".to_string();
751        bar_chart.legend_margin = Some(Box {
752            top: 35.0,
753            bottom: 10.0,
754            ..Default::default()
755        });
756        bar_chart.y_axis_configs[0].axis_formatter = Some("{c} ml".to_string());
757        bar_chart.series_list[0].label_show = true;
758        assert_eq!(
759            include_str!("../../asset/bar_chart/nil_value.svg"),
760            bar_chart.svg().unwrap()
761        );
762    }
763
764    #[test]
765    fn bar_chart_no_axis() {
766        let mut bar_chart = BarChart::new(
767            vec![
768                (
769                    "Email",
770                    vec![120.0, 132.0, 101.0, 134.0, 90.0, 230.0, 210.0],
771                )
772                    .into(),
773                (
774                    "Union Ads",
775                    vec![220.0, 182.0, 191.0, 234.0, 290.0, 330.0, 310.0],
776                )
777                    .into(),
778                (
779                    "Direct",
780                    vec![320.0, 332.0, 301.0, 334.0, 390.0, 330.0, 320.0],
781                )
782                    .into(),
783                (
784                    "Search Engine",
785                    vec![820.0, 932.0, 901.0, 934.0, 1290.0, 1330.0, 1320.0],
786                )
787                    .into(),
788            ],
789            vec![
790                "Mon".to_string(),
791                "Tue".to_string(),
792                "Wed".to_string(),
793                "Thu".to_string(),
794                "Fri".to_string(),
795                "Sat".to_string(),
796                "Sun".to_string(),
797            ],
798        );
799        bar_chart.x_axis_hidden = true;
800        bar_chart.y_axis_hidden = true;
801        bar_chart.title_text = "Bar Chart".to_string();
802        bar_chart.legend_margin = Some(Box {
803            top: 35.0,
804            bottom: 10.0,
805            ..Default::default()
806        });
807
808        assert_eq!(
809            include_str!("../../asset/bar_chart/no_axis.svg"),
810            bar_chart.svg().unwrap()
811        );
812    }
813
814    #[test]
815    fn bar_chart_custom_label_formatter() {
816        let mut bar_chart = BarChart::new(
817            vec![("values", vec![0.0, 0.25, 0.5, 0.75, 1.0]).into()],
818            vec!["1".into(), "2".into(), "3".into(), "4".into(), "5".into()],
819        );
820
821        bar_chart.series_list[0].label_show = true;
822        bar_chart.series_label_formatter = "{:.1}".to_string();
823
824        assert_eq!(
825            include_str!("../../asset/bar_chart/custom_label_formatter.svg").trim(),
826            bar_chart.svg().unwrap()
827        );
828    }
829
830    #[test]
831    fn test_render_legend_center() {
832        let bar_chart = BarChart::from_json(
833            r###"{
834  "legend_align": "center",
835  "series_list": [
836    {
837      "name": "None",
838      "label_show": true,
839      "data": [
840        44
841      ]
842    },
843        {
844      "name": "M1 - End of Inception",
845      "label_show": true,
846      "data": [
847        78
848      ]
849    },
850        {
851      "name": "M2 - End of Elaboration",
852      "label_show": true,
853      "data": [
854        26
855      ]
856    },
857        {
858      "name": "M3 - End of Construction",
859      "label_show": true,
860      "data": [
861        3
862      ]
863    }
864  ],
865  "type": "bar",
866  "x_axis_data": [
867    "Milestones"
868  ]
869}"###,
870        )
871        .unwrap();
872        assert_eq!(
873            include_str!("../../asset/bar_chart/legend_center.svg").trim(),
874            bar_chart.svg().unwrap()
875        );
876    }
877}