From 0eada917661f09185cf7a6703f744134a805fb96 Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 24 Aug 2023 21:09:53 +0800 Subject: [PATCH] feat: support nil value for bar chart --- TODO.md | 4 +- asset/bar_chart/nil_value.svg | 137 ++++++++++++++++++++++++++++ asset/bar_chart/nil_value_json.svg | 140 +++++++++++++++++++++++++++++ charts-rs-derive/src/lib.rs | 7 +- src/charts/bar_chart.rs | 54 ++++++++++- src/charts/params.rs | 8 ++ src/charts/util.rs | 5 ++ tests/bar_chart.rs | 68 ++++++++++++++ 8 files changed, 420 insertions(+), 3 deletions(-) create mode 100644 asset/bar_chart/nil_value.svg create mode 100644 asset/bar_chart/nil_value_json.svg diff --git a/TODO.md b/TODO.md index 4fd3417..89f0d89 100644 --- a/TODO.md +++ b/TODO.md @@ -9,4 +9,6 @@ - [x] 支持scatter - [x] 支持多图合并处理 - [ ] 支持空值处理(中间值为空值) -- [x] 支持candlestick \ No newline at end of file +- [x] 支持candlestick +- [ ] 平均值线、最大最小值标记 +- [ ] 柱状图指定单个背景色 \ No newline at end of file diff --git a/asset/bar_chart/nil_value.svg b/asset/bar_chart/nil_value.svg new file mode 100644 index 0000000..587d031 --- /dev/null +++ b/asset/bar_chart/nil_value.svg @@ -0,0 +1,137 @@ + + + +Bar Chart + + + + + +Email + + + + + + +Union Ads + + + + + + +Direct + + + + + + +Search Engine + + + + + + + + +1.8k ml + + +1.5k ml + + +1.2k ml + + +900 ml + + +600 ml + + +300 ml + + +0 ml + + + + + + + + + + + + + + + +Mon + + +Tue + + +Wed + + +Thu + + +Fri + + +Sat + + +Sun + + + + + + + + + + + + + + + + + + + + + + + + + + + +120 + + +132 + + +101 + + +134 + + +90 + + +230 + + \ No newline at end of file diff --git a/asset/bar_chart/nil_value_json.svg b/asset/bar_chart/nil_value_json.svg new file mode 100644 index 0000000..bf23963 --- /dev/null +++ b/asset/bar_chart/nil_value_json.svg @@ -0,0 +1,140 @@ + + + +Bar Chart + + +demo + + + + + +Email + + + + + + +Union Ads + + + + + + +Direct + + + + + + +Search Engine + + + + + + + + +1.8k + + +1.5k + + +1.2k + + +900 + + +600 + + +300 + + +0 + + + + + + + + + + + + + + + +Mon + + +Tue + + +Wed + + +Thu + + +Fri + + +Sat + + +Sun + + + + + + + + + + + + + + + + + + + + + + + + + + + +120 + + +101 + + +134 + + +90 + + +230 + + +210 + + \ No newline at end of file diff --git a/charts-rs-derive/src/lib.rs b/charts-rs-derive/src/lib.rs index 3659528..3f77720 100644 --- a/charts-rs-derive/src/lib.rs +++ b/charts-rs-derive/src/lib.rs @@ -547,10 +547,15 @@ pub fn my_default(input: TokenStream) -> TokenStream { .unwrap_or_else(|| &self.series_colors[0]); let mut series_labels = vec![]; for (i, p) in series.data.iter().enumerate() { + let value = p.to_owned(); + // nil value忽略 + if value == NIL_VALUE { + continue; + } let mut left = unit_width * (i + series.start_index) as f32 + bar_chart_margin; left += (bar_width + bar_chart_gap) * index as f32; - let y = y_axis_values.get_offset_height(p.to_owned(), max_height); + let y = y_axis_values.get_offset_height(value, max_height); c1.rect(Rect { fill: Some(color), left, diff --git a/src/charts/bar_chart.rs b/src/charts/bar_chart.rs index 45e18b8..5854965 100644 --- a/src/charts/bar_chart.rs +++ b/src/charts/bar_chart.rs @@ -257,7 +257,9 @@ impl BarChart { #[cfg(test)] mod tests { use super::BarChart; - use crate::{Box, LegendCategory, SeriesCategory, THEME_ANT, THEME_DARK, THEME_GRAFANA}; + use crate::{ + Box, LegendCategory, SeriesCategory, NIL_VALUE, THEME_ANT, THEME_DARK, THEME_GRAFANA, + }; use pretty_assertions::assert_eq; #[test] fn bar_chart_basic() { @@ -666,4 +668,54 @@ mod tests { bar_chart.svg().unwrap() ); } + + #[test] + fn bar_chart_nil_value() { + let mut bar_chart = BarChart::new( + vec![ + ( + "Email", + vec![120.0, NIL_VALUE, 132.0, 101.0, 134.0, 90.0, 230.0], + ) + .into(), + ( + "Union Ads", + vec![220.0, 182.0, 191.0, NIL_VALUE, 290.0, 330.0, 310.0], + ) + .into(), + ( + "Direct", + vec![320.0, 332.0, 301.0, 334.0, 390.0, NIL_VALUE, 320.0], + ) + .into(), + ( + "Search Engine", + vec![NIL_VALUE, 932.0, 901.0, 934.0, 1290.0, 1330.0, 1320.0], + ) + .into(), + ], + vec![ + "Mon".to_string(), + "Tue".to_string(), + "Wed".to_string(), + "Thu".to_string(), + "Fri".to_string(), + "Sat".to_string(), + "Sun".to_string(), + ], + ); + bar_chart.y_axis_configs[0].axis_width = Some(55.0); + bar_chart.title_text = "Bar Chart".to_string(); + bar_chart.legend_margin = Some(Box { + top: 35.0, + bottom: 10.0, + ..Default::default() + }); + bar_chart.y_axis_configs[0].axis_formatter = Some("{c} ml".to_string()); + bar_chart.series_list[0].label_show = true; + assert_eq!( + include_str!("../../asset/bar_chart/nil_value.svg"), + bar_chart.svg().unwrap() + ); + } } diff --git a/src/charts/params.rs b/src/charts/params.rs index 65e7fcd..31e48c3 100644 --- a/src/charts/params.rs +++ b/src/charts/params.rs @@ -1,3 +1,5 @@ +use crate::NIL_VALUE; + use super::{Align, Box, Color, LegendCategory, Series, SeriesCategory, Theme, YAxisConfig}; pub(crate) fn get_bool_from_value(value: &serde_json::Value, key: &str) -> Option { @@ -42,6 +44,9 @@ pub(crate) fn get_usize_slice_from_value( pub(crate) fn get_f32_from_value(value: &serde_json::Value, key: &str) -> Option { if let Some(value) = value.get(key) { + if value.is_null() { + return Some(NIL_VALUE); + } if let Some(v) = value.as_f64() { return Some(v as f32); } @@ -55,6 +60,9 @@ pub(crate) fn get_f32_slice_from_value(value: &serde_json::Value, key: &str) -> values .iter() .map(|item| { + if item.is_null() { + return NIL_VALUE; + } if let Some(v) = item.as_f64() { v as f32 } else { diff --git a/src/charts/util.rs b/src/charts/util.rs index ec67f73..5c5e189 100644 --- a/src/charts/util.rs +++ b/src/charts/util.rs @@ -2,6 +2,8 @@ use serde::{Deserialize, Serialize}; use std::fmt; use substring::Substring; +pub static NIL_VALUE: f32 = f32::MIN; + #[derive(Clone, Copy, PartialEq, Debug, Default)] pub struct Point { pub x: f32, @@ -165,6 +167,9 @@ pub(crate) fn get_axis_values(params: AxisValueParams) -> AxisValues { } for item in params.data_list.iter() { let value = item.to_owned(); + if value == NIL_VALUE { + continue; + } if value > max { max = value; } diff --git a/tests/bar_chart.rs b/tests/bar_chart.rs index b9f73a6..0b5f7cd 100644 --- a/tests/bar_chart.rs +++ b/tests/bar_chart.rs @@ -70,6 +70,74 @@ fn bar_chart() { ); } +#[test] +fn bar_chart_nil_value() { + let bar_chart = BarChart::from_json( + r###"{ + "width": 630, + "height": 410, + "margin": { + "left": 10, + "top": 5, + "right": 10 + }, + "title_text": "Bar Chart", + "title_font_color": "#345", + "title_align": "right", + "sub_title_text": "demo", + "sub_title_font_weight": "bold", + "legend_align": "left", + "legend_font_weight": "bold", + "y_axis_configs": [ + { + "axis_font_weight": "bold" + } + ], + "series_label_font_weight": "bold", + "series_list": [ + { + "name": "Email", + "label_show": true, + "data": [120.0, null, 101.0, 134.0, 90.0, 230.0, 210.0] + }, + { + "name": "Union Ads", + "data": [220.0, 182.0, 191.0, null, 290.0, 330.0, 310.0] + }, + { + "name": "Direct", + "data": [320.0, null, 301.0, 334.0, 390.0, 330.0, 320.0] + }, + { + "name": "Search Engine", + "data": [820.0, 932.0, 901.0, 934.0, 1290.0, null, 1320.0] + } + ], + "x_axis_data": [ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun" + ], + "x_axis_margin": { + "left": 1, + "top": 0, + "right": 0, + "bottom": 0 + }, + "x_axis_font_weight": "bold" + }"###, + ) + .unwrap(); + assert_eq!( + include_str!("../asset/bar_chart/nil_value_json.svg"), + bar_chart.svg().unwrap() + ); +} + #[test] fn bar_chart_mixin() { let bar_chart = BarChart::from_json(