1use crate::error::{Error, Result};
4
5#[derive(Debug, Clone, PartialEq, Default)]
7pub enum SparklineType {
8 #[default]
10 Line,
11 Column,
13 WinLoss,
15}
16
17impl SparklineType {
18 pub fn as_str(&self) -> &str {
20 match self {
21 SparklineType::Line => "line",
22 SparklineType::Column => "column",
23 SparklineType::WinLoss => "stacked",
24 }
25 }
26
27 pub fn parse(s: &str) -> Option<Self> {
29 match s {
30 "line" => Some(SparklineType::Line),
31 "column" => Some(SparklineType::Column),
32 "stacked" | "winloss" => Some(SparklineType::WinLoss),
33 _ => None,
34 }
35 }
36}
37
38#[derive(Debug, Clone)]
40pub struct SparklineConfig {
41 pub data_range: String,
43 pub location: String,
45 pub sparkline_type: SparklineType,
47 pub markers: bool,
49 pub high_point: bool,
51 pub low_point: bool,
53 pub first_point: bool,
55 pub last_point: bool,
57 pub negative_points: bool,
59 pub show_axis: bool,
61 pub line_weight: Option<f64>,
63 pub style: Option<u32>,
65}
66
67impl SparklineConfig {
68 pub fn new(data_range: &str, location: &str) -> Self {
70 Self {
71 data_range: data_range.to_string(),
72 location: location.to_string(),
73 sparkline_type: SparklineType::Line,
74 markers: false,
75 high_point: false,
76 low_point: false,
77 first_point: false,
78 last_point: false,
79 negative_points: false,
80 show_axis: false,
81 line_weight: None,
82 style: None,
83 }
84 }
85}
86
87#[derive(Debug, Clone)]
89pub struct SparklineGroupConfig {
90 pub sparklines: Vec<SparklineConfig>,
92 pub sparkline_type: SparklineType,
94 pub markers: bool,
96 pub high_point: bool,
98 pub low_point: bool,
100 pub line_weight: Option<f64>,
102}
103
104#[allow(dead_code)]
106pub(crate) fn config_to_xml_group(
107 config: &SparklineConfig,
108) -> sheetkit_xml::sparkline::SparklineGroup {
109 use sheetkit_xml::sparkline::*;
110
111 SparklineGroup {
112 sparkline_type: match config.sparkline_type {
113 SparklineType::Line => None,
114 _ => Some(config.sparkline_type.as_str().to_string()),
115 },
116 markers: if config.markers { Some(true) } else { None },
117 high: if config.high_point { Some(true) } else { None },
118 low: if config.low_point { Some(true) } else { None },
119 first: if config.first_point { Some(true) } else { None },
120 last: if config.last_point { Some(true) } else { None },
121 negative: if config.negative_points {
122 Some(true)
123 } else {
124 None
125 },
126 display_x_axis: if config.show_axis { Some(true) } else { None },
127 line_weight: config.line_weight,
128 min_axis_type: None,
129 max_axis_type: None,
130 sparklines: SparklineList {
131 items: vec![Sparkline {
132 formula: config.data_range.clone(),
133 sqref: config.location.clone(),
134 }],
135 },
136 }
137}
138
139pub fn validate_sparkline_config(config: &SparklineConfig) -> Result<()> {
141 if config.data_range.is_empty() {
142 return Err(Error::Internal("sparkline data range is empty".to_string()));
143 }
144 if config.location.is_empty() {
145 return Err(Error::Internal("sparkline location is empty".to_string()));
146 }
147 if let Some(weight) = config.line_weight {
148 if weight <= 0.0 {
149 return Err(Error::Internal(
150 "sparkline line weight must be positive".to_string(),
151 ));
152 }
153 }
154 if let Some(style) = config.style {
155 if style > 35 {
156 return Err(Error::Internal(format!(
157 "sparkline style {} out of range (0-35)",
158 style
159 )));
160 }
161 }
162 Ok(())
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn test_sparkline_type_default() {
171 assert_eq!(SparklineType::default(), SparklineType::Line);
172 }
173
174 #[test]
175 fn test_sparkline_type_as_str() {
176 assert_eq!(SparklineType::Line.as_str(), "line");
177 assert_eq!(SparklineType::Column.as_str(), "column");
178 assert_eq!(SparklineType::WinLoss.as_str(), "stacked");
179 }
180
181 #[test]
182 fn test_sparkline_type_parse() {
183 assert_eq!(SparklineType::parse("line"), Some(SparklineType::Line));
184 assert_eq!(SparklineType::parse("column"), Some(SparklineType::Column));
185 assert_eq!(
186 SparklineType::parse("stacked"),
187 Some(SparklineType::WinLoss)
188 );
189 assert_eq!(
190 SparklineType::parse("winloss"),
191 Some(SparklineType::WinLoss)
192 );
193 assert_eq!(SparklineType::parse("invalid"), None);
194 }
195
196 #[test]
197 fn test_sparkline_config_new() {
198 let config = SparklineConfig::new("Sheet1!A1:A10", "B1");
199 assert_eq!(config.data_range, "Sheet1!A1:A10");
200 assert_eq!(config.location, "B1");
201 assert_eq!(config.sparkline_type, SparklineType::Line);
202 assert!(!config.markers);
203 }
204
205 #[test]
206 fn test_validate_sparkline_config_ok() {
207 let config = SparklineConfig::new("Sheet1!A1:A10", "B1");
208 assert!(validate_sparkline_config(&config).is_ok());
209 }
210
211 #[test]
212 fn test_validate_sparkline_empty_range() {
213 let config = SparklineConfig {
214 data_range: String::new(),
215 ..SparklineConfig::new("", "B1")
216 };
217 assert!(validate_sparkline_config(&config).is_err());
218 }
219
220 #[test]
221 fn test_validate_sparkline_empty_location() {
222 let config = SparklineConfig {
223 location: String::new(),
224 ..SparklineConfig::new("Sheet1!A1:A10", "")
225 };
226 assert!(validate_sparkline_config(&config).is_err());
227 }
228
229 #[test]
230 fn test_validate_sparkline_invalid_weight() {
231 let mut config = SparklineConfig::new("Sheet1!A1:A10", "B1");
232 config.line_weight = Some(-1.0);
233 assert!(validate_sparkline_config(&config).is_err());
234 }
235
236 #[test]
237 fn test_validate_sparkline_invalid_style() {
238 let mut config = SparklineConfig::new("Sheet1!A1:A10", "B1");
239 config.style = Some(36);
240 assert!(validate_sparkline_config(&config).is_err());
241 }
242
243 #[test]
244 fn test_config_to_xml_group_line() {
245 let config = SparklineConfig::new("Sheet1!A1:A10", "B1");
246 let group = config_to_xml_group(&config);
247 assert!(group.sparkline_type.is_none());
248 assert_eq!(group.sparklines.items.len(), 1);
249 }
250
251 #[test]
252 fn test_config_to_xml_group_column() {
253 let mut config = SparklineConfig::new("Sheet1!A1:A10", "B1");
254 config.sparkline_type = SparklineType::Column;
255 config.markers = true;
256 config.high_point = true;
257 let group = config_to_xml_group(&config);
258 assert_eq!(group.sparkline_type, Some("column".to_string()));
259 assert_eq!(group.markers, Some(true));
260 assert_eq!(group.high, Some(true));
261 }
262
263 #[test]
264 fn test_config_to_xml_group_winloss() {
265 let mut config = SparklineConfig::new("Sheet1!A1:A10", "B1");
266 config.sparkline_type = SparklineType::WinLoss;
267 let group = config_to_xml_group(&config);
268 assert_eq!(group.sparkline_type, Some("stacked".to_string()));
269 }
270}