charts_rs/charts/
horizontal_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 std::sync::Arc;
24
25#[derive(Clone, Debug, Default, Chart)]
26pub struct HorizontalBarChart {
27    pub width: f32,
28    pub height: f32,
29    pub x: f32,
30    pub y: f32,
31    pub margin: Box,
32    pub series_list: Vec<Series>,
33    pub font_family: String,
34    pub background_color: Color,
35    pub is_light: bool,
36
37    // title
38    pub title_text: String,
39    pub title_font_size: f32,
40    pub title_font_color: Color,
41    pub title_font_weight: Option<String>,
42    pub title_margin: Option<Box>,
43    pub title_align: Align,
44    pub title_height: f32,
45
46    // sub title
47    pub sub_title_text: String,
48    pub sub_title_font_size: f32,
49    pub sub_title_font_color: Color,
50    pub sub_title_font_weight: Option<String>,
51    pub sub_title_margin: Option<Box>,
52    pub sub_title_align: Align,
53    pub sub_title_height: f32,
54
55    // legend
56    pub legend_font_size: f32,
57    pub legend_font_color: Color,
58    pub legend_font_weight: Option<String>,
59    pub legend_align: Align,
60    pub legend_margin: Option<Box>,
61    pub legend_category: LegendCategory,
62    pub legend_show: Option<bool>,
63
64    // x axis
65    pub x_axis_data: Vec<String>,
66    pub x_axis_height: f32,
67    pub x_axis_stroke_color: Color,
68    pub x_axis_font_size: f32,
69    pub x_axis_font_color: Color,
70    pub x_axis_font_weight: Option<String>,
71    pub x_axis_name_gap: f32,
72    pub x_axis_name_rotate: f32,
73    pub x_axis_margin: Option<Box>,
74    pub x_boundary_gap: Option<bool>,
75
76    // y axis
77    pub y_axis_configs: Vec<YAxisConfig>,
78
79    // grid
80    pub grid_stroke_color: Color,
81    pub grid_stroke_width: f32,
82
83    // series
84    pub series_stroke_width: f32,
85    pub series_label_font_color: Color,
86    pub series_label_font_size: f32,
87    pub series_label_font_weight: Option<String>,
88    pub series_label_formatter: String,
89    pub series_label_position: Option<Position>,
90    pub series_colors: Vec<Color>,
91    pub series_symbol: Option<Symbol>,
92    pub series_smooth: bool,
93    pub series_fill: bool,
94}
95
96impl HorizontalBarChart {
97    /// Creates a horizontal bar from json.
98    pub fn from_json(data: &str) -> canvas::Result<HorizontalBarChart> {
99        let mut h = HorizontalBarChart {
100            ..Default::default()
101        };
102        let value = h.fill_option(data)?;
103        if let Some(series_label_position) =
104            get_position_from_value(&value, "series_label_position")
105        {
106            h.series_label_position = Some(series_label_position);
107        }
108        Ok(h)
109    }
110    /// Creates a horizontal bar with custom theme.
111    pub fn new_with_theme(
112        series_list: Vec<Series>,
113        x_axis_data: Vec<String>,
114        theme: &str,
115    ) -> HorizontalBarChart {
116        let mut h = HorizontalBarChart {
117            series_list,
118            x_axis_data,
119            ..Default::default()
120        };
121        let theme = get_theme(theme);
122        h.fill_theme(theme);
123        h
124    }
125    /// Creates a horizontal bar with default theme.
126    pub fn new(series_list: Vec<Series>, x_axis_data: Vec<String>) -> HorizontalBarChart {
127        HorizontalBarChart::new_with_theme(series_list, x_axis_data, &get_default_theme_name())
128    }
129    /// Converts horizontal bar chart to svg.
130    pub fn svg(&self) -> canvas::Result<String> {
131        let mut c = Canvas::new_width_xy(self.width, self.height, self.x, self.y);
132
133        self.render_background(c.child(Box::default()));
134        c.margin = self.margin.clone();
135
136        let title_height = self.render_title(c.child(Box::default()));
137
138        let legend_height = self.render_legend(c.child(Box::default()));
139        // get the max height of title and legend
140        let axis_top = if legend_height > title_height {
141            legend_height
142        } else {
143            title_height
144        };
145
146        let x_axis_height = 25.0_f32;
147        let axis_height = c.height() - axis_top - x_axis_height;
148        // minus the height of top text area
149        if axis_top > 0.0 {
150            c = c.child(Box {
151                top: axis_top,
152                ..Default::default()
153            });
154        }
155
156        let mut data = self.x_axis_data.clone();
157        data.reverse();
158        let mut max_width = 0.0;
159        for text in data.iter() {
160            if let Ok(b) = measure_text_width_family(&self.font_family, self.x_axis_font_size, text)
161            {
162                if b.width() > max_width {
163                    max_width = b.width();
164                }
165            }
166        }
167
168        let y_axis_width = max_width + 5.0;
169
170        c.axis(Axis {
171            position: Position::Left,
172            height: axis_height,
173            width: y_axis_width,
174            split_number: self.x_axis_data.len(),
175            font_family: self.font_family.clone(),
176            stroke_color: Some(self.x_axis_stroke_color),
177            name_align: Align::Center,
178            name_gap: self.x_axis_name_gap,
179            font_color: Some(self.x_axis_font_color),
180            font_size: self.x_axis_font_size,
181            data,
182            ..Default::default()
183        });
184
185        let mut data_list = vec![];
186        for series in self.series_list.iter() {
187            data_list.append(series.data.clone().as_mut());
188        }
189        let x_axis_config = self.get_y_axis_config(0);
190        let x_axis_values = get_axis_values(AxisValueParams {
191            data_list,
192            split_number: x_axis_config.axis_split_number,
193            ..Default::default()
194        });
195
196        let x_axis_width = c.width() - y_axis_width;
197        c.child(Box {
198            left: y_axis_width,
199            top: axis_height,
200            ..Default::default()
201        })
202        .axis(Axis {
203            position: Position::Bottom,
204            height: x_axis_height,
205            width: x_axis_width,
206            split_number: x_axis_config.axis_split_number,
207            font_family: self.font_family.clone(),
208            stroke_color: Some(x_axis_config.axis_stroke_color),
209            name_align: Align::Left,
210            name_gap: x_axis_config.axis_name_gap,
211            font_color: Some(x_axis_config.axis_font_color),
212            font_size: x_axis_config.axis_font_size,
213            data: x_axis_values.data.clone(),
214            ..Default::default()
215        });
216
217        c.child(Box {
218            left: y_axis_width,
219            ..Default::default()
220        })
221        .grid(Grid {
222            right: x_axis_width,
223            bottom: axis_height,
224            color: Some(self.grid_stroke_color),
225            stroke_width: self.grid_stroke_width,
226            verticals: x_axis_config.axis_split_number,
227            hidden_verticals: vec![0],
228            ..Default::default()
229        });
230
231        // horizontal bar
232        if !self.series_list.is_empty() {
233            let mut c1 = c.child(Box {
234                left: y_axis_width,
235                bottom: x_axis_height,
236                ..Default::default()
237            });
238            let max_width = c1.width();
239            let unit_height = c1.height() / self.series_list[0].data.len() as f32;
240            let bar_chart_margin = 5.0_f32;
241            let bar_chart_gap = 3.0_f32;
242
243            let bar_chart_margin_height = bar_chart_margin * 2.0;
244            let bar_chart_gap_height = bar_chart_gap * (self.series_list.len() - 1) as f32;
245            let bar_height = (unit_height - bar_chart_margin_height - bar_chart_gap_height)
246                / self.series_list.len() as f32;
247            let half_bar_height = bar_height / 2.0;
248
249            let mut series_labels_list = vec![];
250            for (index, series) in self.series_list.iter().enumerate() {
251                let color = get_color(&self.series_colors, series.index.unwrap_or(index));
252
253                let mut series_labels = vec![];
254                let series_data_count = series.data.len();
255                for (i, p) in series.data.iter().enumerate() {
256                    let value = p.to_owned();
257                    if value == NIL_VALUE {
258                        continue;
259                    }
260                    let mut top =
261                        unit_height * (series_data_count - i - 1) as f32 + bar_chart_margin;
262                    top += (bar_height + bar_chart_gap) * index as f32;
263
264                    let x = max_width - x_axis_values.get_offset_height(value, max_width);
265                    c1.rect(Rect {
266                        fill: Some(color),
267                        top,
268                        width: x,
269                        height: bar_height,
270                        ..Default::default()
271                    });
272                    series_labels.push(SeriesLabel {
273                        point: (x, top + half_bar_height).into(),
274                        text: format_series_value(value, &self.series_label_formatter),
275                    })
276                }
277                if series.label_show {
278                    series_labels_list.push(series_labels);
279                }
280            }
281
282            let series_label_position = self
283                .series_label_position
284                .clone()
285                .unwrap_or(Position::Right);
286            for series_labels in series_labels_list.iter() {
287                for series_label in series_labels.iter() {
288                    let mut dy = None;
289                    let mut dx = Some(3.0);
290                    let mut x = Some(series_label.point.x);
291                    if let Ok(value) = measure_text_width_family(
292                        &self.font_family,
293                        self.series_label_font_size,
294                        &series_label.text,
295                    ) {
296                        dy = Some(value.height() / 2.0 - 2.0);
297                        if series_label_position == Position::Inside {
298                            dx = None;
299                            let offset = series_label.point.x - value.width();
300                            if offset <= 0.0 {
301                                x = Some(1.0);
302                            } else {
303                                x = Some(offset / 2.0);
304                            }
305                        } else if series_label_position == Position::Left {
306                            x = Some(0.0);
307                            dx = Some(-value.width());
308                        }
309                    }
310                    c1.text(Text {
311                        text: series_label.text.clone(),
312                        dx,
313                        dy,
314                        font_family: Some(self.font_family.clone()),
315                        font_color: Some(self.series_label_font_color),
316                        font_size: Some(self.series_label_font_size),
317                        x,
318                        y: Some(series_label.point.y),
319                        ..Default::default()
320                    });
321                }
322            }
323        }
324
325        c.svg()
326    }
327}
328
329#[cfg(test)]
330mod tests {
331    use super::HorizontalBarChart;
332    use crate::{Align, Position, NIL_VALUE};
333    use pretty_assertions::assert_eq;
334    #[test]
335    fn horizontal_bar_chart_basic() {
336        let mut horizontal_bar_chart = HorizontalBarChart::new(
337            vec![
338                (
339                    "2011",
340                    vec![18203.0, 23489.0, 29034.0, 104970.0, 131744.0, 630230.0],
341                )
342                    .into(),
343                (
344                    "2012",
345                    vec![19325.0, 23438.0, 31000.0, 121594.0, 134141.0, 681807.0],
346                )
347                    .into(),
348            ],
349            vec![
350                "Brazil".to_string(),
351                "Indonesia".to_string(),
352                "USA".to_string(),
353                "India".to_string(),
354                "China".to_string(),
355                "World".to_string(),
356            ],
357        );
358        horizontal_bar_chart.title_text = "World Population".to_string();
359        horizontal_bar_chart.series_label_formatter = "{t}".to_string();
360        horizontal_bar_chart.margin.right = 15.0;
361        horizontal_bar_chart.series_list[0].label_show = true;
362        horizontal_bar_chart.title_align = Align::Left;
363        assert_eq!(
364            include_str!("../../asset/horizontal_bar_chart/basic.svg"),
365            horizontal_bar_chart.svg().unwrap()
366        );
367    }
368
369    #[test]
370    fn horizontal_bar_chart_inside() {
371        let mut horizontal_bar_chart = HorizontalBarChart::new(
372            vec![
373                (
374                    "2011",
375                    vec![18203.0, 23489.0, 29034.0, 104970.0, 131744.0, 630230.0],
376                )
377                    .into(),
378                (
379                    "2012",
380                    vec![19325.0, 23438.0, 31000.0, 121594.0, 134141.0, 681807.0],
381                )
382                    .into(),
383            ],
384            vec![
385                "Brazil".to_string(),
386                "Indonesia".to_string(),
387                "USA".to_string(),
388                "India".to_string(),
389                "China".to_string(),
390                "World".to_string(),
391            ],
392        );
393        horizontal_bar_chart.title_text = "World Population".to_string();
394        horizontal_bar_chart.series_label_formatter = "{t}".to_string();
395        horizontal_bar_chart.margin.right = 15.0;
396        horizontal_bar_chart.series_list[0].label_show = true;
397        horizontal_bar_chart.title_align = Align::Left;
398        horizontal_bar_chart.series_label_position = Some(Position::Inside);
399        assert_eq!(
400            include_str!("../../asset/horizontal_bar_chart/basic_label_inside.svg"),
401            horizontal_bar_chart.svg().unwrap()
402        );
403    }
404
405    #[test]
406    fn horizontal_bar_chart_nil_value() {
407        let mut horizontal_bar_chart = HorizontalBarChart::new(
408            vec![
409                (
410                    "2011",
411                    vec![18203.0, 23489.0, NIL_VALUE, 104970.0, 131744.0, 630230.0],
412                )
413                    .into(),
414                (
415                    "2012",
416                    vec![19325.0, 23438.0, 31000.0, 121594.0, NIL_VALUE, 681807.0],
417                )
418                    .into(),
419            ],
420            vec![
421                "Brazil".to_string(),
422                "Indonesia".to_string(),
423                "USA".to_string(),
424                "India".to_string(),
425                "China".to_string(),
426                "World".to_string(),
427            ],
428        );
429        horizontal_bar_chart.title_text = "World Population".to_string();
430        horizontal_bar_chart.margin.right = 15.0;
431        horizontal_bar_chart.series_list[0].label_show = true;
432        horizontal_bar_chart.title_align = Align::Left;
433        assert_eq!(
434            include_str!("../../asset/horizontal_bar_chart/nil_value.svg"),
435            horizontal_bar_chart.svg().unwrap()
436        );
437    }
438}