Skip to main content

excelize_rs/
sparkline.rs

1//! Sparkline support.
2//!
3//! Ported from Go `sparkline.go`.
4
5use quick_xml::se::to_string as xml_to_string;
6
7use crate::File;
8use crate::constants::{
9    EXT_URI_SPARKLINE_GROUPS, NAMESPACE_SPREADSHEET_EXCEL_2006_MAIN, NAMESPACE_SPREADSHEET_X14,
10    WORKSHEET_EXT_URI_PRIORITY,
11};
12use crate::errors::{
13    ErrSparkline, ErrSparklineLocation, ErrSparklineRange, ErrSparklineStyle, ErrSparklineType,
14    Result,
15};
16use crate::lib_util::{in_str_slice, int_ptr};
17use crate::xml::common::{XlsxColor, XlsxExt, XlsxExtLst};
18use crate::xml::worksheet::{
19    SparklineOptions, XlsxWorksheet, XlsxX14Sparkline, XlsxX14SparklineGroup,
20    XlsxX14SparklineGroups,
21};
22
23impl File {
24    /// Add sparklines to a worksheet.
25    ///
26    /// Sparklines are small charts that fit in a single cell and are used to
27    /// show trends in data. Sparklines are a feature of Excel 2010 and later
28    /// only.
29    pub fn add_sparkline(&self, sheet: &str, opts: &SparklineOptions) -> Result<()> {
30        let mut ws = self.parse_format_add_sparkline_set(sheet, opts)?;
31
32        let spark_types = [
33            ("line", "line"),
34            ("column", "column"),
35            ("win_loss", "stacked"),
36        ]
37        .iter()
38        .cloned()
39        .collect::<std::collections::HashMap<_, _>>();
40        let mut spark_type = "line".to_string();
41        if !opts.r#type.is_empty() {
42            spark_type = spark_types
43                .get(opts.r#type.as_str())
44                .ok_or_else(|| ErrSparklineType)?
45                .to_string();
46        }
47
48        let presets = get_sparkline_group_presets();
49        let mut group = presets
50            .get(opts.style as usize)
51            .cloned()
52            .unwrap_or_else(|| presets[0].clone());
53        group.r#type = Some(spark_type);
54        group.color_axis = Some(XlsxColor {
55            rgb: Some("FF000000".to_string()),
56            ..Default::default()
57        });
58        group.display_empty_cells_as = Some("gap".to_string());
59        group.high = Some(opts.high).filter(|&v| v);
60        group.low = Some(opts.low).filter(|&v| v);
61        group.first = Some(opts.first).filter(|&v| v);
62        group.last = Some(opts.last).filter(|&v| v);
63        group.negative = Some(opts.negative).filter(|&v| v);
64        group.display_x_axis = Some(opts.axis).filter(|&v| v);
65        group.markers = Some(opts.markers).filter(|&v| v);
66        if !opts.series_color.is_empty() {
67            group.color_series = Some(XlsxColor {
68                rgb: Some(crate::styles::get_palette_color(&opts.series_color)),
69                ..Default::default()
70            });
71        }
72        if opts.reverse {
73            group.right_to_left = Some(opts.reverse);
74        }
75
76        add_sparkline_items(opts, &mut group);
77        self.append_sparkline_group(&mut ws, &group)?;
78        self.add_sheet_name_space(sheet, NAMESPACE_SPREADSHEET_X14);
79
80        let path =
81            self.get_sheet_xml_path(sheet)
82                .ok_or_else(|| crate::errors::ErrSheetNotExist {
83                    sheet_name: sheet.to_string(),
84                })?;
85        self.sheet.insert(path, ws);
86        Ok(())
87    }
88
89    /// Validate sparkline properties.
90    fn parse_format_add_sparkline_set(
91        &self,
92        sheet: &str,
93        opts: &SparklineOptions,
94    ) -> Result<XlsxWorksheet> {
95        let mut ws = self.work_sheet_reader(sheet)?;
96        if opts.location.is_empty() {
97            return Err(Box::new(ErrSparklineLocation));
98        }
99        if opts.range.is_empty() {
100            return Err(Box::new(ErrSparklineRange));
101        }
102        if opts.location.len() != opts.range.len() {
103            return Err(Box::new(ErrSparkline));
104        }
105        if opts.style < 0 || opts.style > 35 {
106            return Err(Box::new(ErrSparklineStyle));
107        }
108        if ws.ext_lst.is_none() {
109            ws.ext_lst = Some(XlsxExtLst::default());
110        }
111        Ok(ws)
112    }
113
114    /// Append a sparkline group to the worksheet extension list.
115    fn append_sparkline_group(
116        &self,
117        ws: &mut XlsxWorksheet,
118        group: &XlsxX14SparklineGroup,
119    ) -> Result<()> {
120        let new_group_xml = xml_to_string(group)?;
121        let mut append_mode = false;
122
123        let ext_lst = ws.ext_lst.as_mut().unwrap();
124        for ext in &mut ext_lst.ext {
125            if ext.uri.as_deref() == Some(EXT_URI_SPARKLINE_GROUPS) {
126                let new_content = if ext.content.contains("</x14:sparklineGroups>") {
127                    ext.content.replacen(
128                        "</x14:sparklineGroups>",
129                        &format!("{new_group_xml}</x14:sparklineGroups>"),
130                        1,
131                    )
132                } else {
133                    format!(
134                        r#"<x14:sparklineGroups xmlns:xm="{}">{new_group_xml}</x14:sparklineGroups>"#,
135                        NAMESPACE_SPREADSHEET_EXCEL_2006_MAIN
136                    )
137                };
138                ext.content = new_content;
139                append_mode = true;
140            }
141        }
142
143        if !append_mode {
144            let groups = XlsxX14SparklineGroups {
145                xmlns_xm: Some(NAMESPACE_SPREADSHEET_EXCEL_2006_MAIN.to_string()),
146                sparkline_groups: vec![group.clone()],
147                ..Default::default()
148            };
149            let groups_xml = xml_to_string(&groups)?;
150            ext_lst.ext.push(XlsxExt {
151                uri: Some(EXT_URI_SPARKLINE_GROUPS.to_string()),
152                content: groups_xml,
153                ..Default::default()
154            });
155        }
156
157        ext_lst.ext.sort_by(|a, b| {
158            let ia = in_str_slice(
159                WORKSHEET_EXT_URI_PRIORITY,
160                a.uri.as_deref().unwrap_or(""),
161                false,
162            );
163            let ib = in_str_slice(
164                WORKSHEET_EXT_URI_PRIORITY,
165                b.uri.as_deref().unwrap_or(""),
166                false,
167            );
168            ia.cmp(&ib)
169        });
170
171        Ok(())
172    }
173}
174
175/// Add individual sparkline entries to a sparkline group.
176fn add_sparkline_items(opts: &SparklineOptions, group: &mut XlsxX14SparklineGroup) {
177    for (location, range) in opts.location.iter().zip(opts.range.iter()) {
178        group.sparklines.sparkline.push(XlsxX14Sparkline {
179            f: range.clone(),
180            sqref: location.clone(),
181        });
182    }
183}
184
185/// Build a color from a theme index and optional tint.
186fn theme_color(theme: i64, tint: Option<f64>) -> Option<XlsxColor> {
187    Some(XlsxColor {
188        theme: int_ptr(theme),
189        tint,
190        ..Default::default()
191    })
192}
193
194/// Build a color from an RGB hex string.
195fn rgb_color(rgb: &str) -> Option<XlsxColor> {
196    Some(XlsxColor {
197        rgb: Some(rgb.to_string()),
198        ..Default::default()
199    })
200}
201
202/// Return the preset list of sparkline groups used to create x14:sparklineGroups.
203#[allow(clippy::too_many_lines)]
204fn get_sparkline_group_presets() -> Vec<XlsxX14SparklineGroup> {
205    vec![
206        XlsxX14SparklineGroup {
207            color_series: theme_color(4, Some(-0.499984740745262)),
208            color_negative: theme_color(5, None),
209            color_markers: theme_color(4, Some(-0.499984740745262)),
210            color_first: theme_color(4, Some(0.39997558519241921)),
211            color_last: theme_color(4, Some(0.39997558519241921)),
212            color_high: theme_color(4, None),
213            color_low: theme_color(4, None),
214            ..Default::default()
215        }, // 0
216        XlsxX14SparklineGroup {
217            color_series: theme_color(4, Some(-0.499984740745262)),
218            color_negative: theme_color(5, None),
219            color_markers: theme_color(4, Some(-0.499984740745262)),
220            color_first: theme_color(4, Some(0.39997558519241921)),
221            color_last: theme_color(4, Some(0.39997558519241921)),
222            color_high: theme_color(4, None),
223            color_low: theme_color(4, None),
224            ..Default::default()
225        }, // 1
226        XlsxX14SparklineGroup {
227            color_series: theme_color(5, Some(-0.499984740745262)),
228            color_negative: theme_color(6, None),
229            color_markers: theme_color(5, Some(-0.499984740745262)),
230            color_first: theme_color(5, Some(0.39997558519241921)),
231            color_last: theme_color(5, Some(0.39997558519241921)),
232            color_high: theme_color(5, None),
233            color_low: theme_color(5, None),
234            ..Default::default()
235        }, // 2
236        XlsxX14SparklineGroup {
237            color_series: theme_color(6, Some(-0.499984740745262)),
238            color_negative: theme_color(7, None),
239            color_markers: theme_color(6, Some(-0.499984740745262)),
240            color_first: theme_color(6, Some(0.39997558519241921)),
241            color_last: theme_color(6, Some(0.39997558519241921)),
242            color_high: theme_color(6, None),
243            color_low: theme_color(6, None),
244            ..Default::default()
245        }, // 3
246        XlsxX14SparklineGroup {
247            color_series: theme_color(7, Some(-0.499984740745262)),
248            color_negative: theme_color(8, None),
249            color_markers: theme_color(7, Some(-0.499984740745262)),
250            color_first: theme_color(7, Some(0.39997558519241921)),
251            color_last: theme_color(7, Some(0.39997558519241921)),
252            color_high: theme_color(7, None),
253            color_low: theme_color(7, None),
254            ..Default::default()
255        }, // 4
256        XlsxX14SparklineGroup {
257            color_series: theme_color(8, Some(-0.499984740745262)),
258            color_negative: theme_color(9, None),
259            color_markers: theme_color(8, Some(-0.499984740745262)),
260            color_first: theme_color(8, Some(0.39997558519241921)),
261            color_last: theme_color(8, Some(0.39997558519241921)),
262            color_high: theme_color(8, None),
263            color_low: theme_color(8, None),
264            ..Default::default()
265        }, // 5
266        XlsxX14SparklineGroup {
267            color_series: theme_color(9, Some(-0.499984740745262)),
268            color_negative: theme_color(4, None),
269            color_markers: theme_color(9, Some(-0.499984740745262)),
270            color_first: theme_color(9, Some(0.39997558519241921)),
271            color_last: theme_color(9, Some(0.39997558519241921)),
272            color_high: theme_color(9, None),
273            color_low: theme_color(9, None),
274            ..Default::default()
275        }, // 6
276        XlsxX14SparklineGroup {
277            color_series: theme_color(4, Some(-0.249977111117893)),
278            color_negative: theme_color(5, None),
279            color_markers: theme_color(5, Some(-0.249977111117893)),
280            color_first: theme_color(5, Some(-0.249977111117893)),
281            color_last: theme_color(5, Some(-0.249977111117893)),
282            color_high: theme_color(5, None),
283            color_low: theme_color(5, None),
284            ..Default::default()
285        }, // 7
286        XlsxX14SparklineGroup {
287            color_series: theme_color(5, Some(-0.249977111117893)),
288            color_negative: theme_color(6, None),
289            color_markers: theme_color(6, Some(-0.249977111117893)),
290            color_first: theme_color(6, Some(-0.249977111117893)),
291            color_last: theme_color(6, Some(-0.249977111117893)),
292            color_high: theme_color(6, Some(-0.249977111117893)),
293            color_low: theme_color(6, Some(-0.249977111117893)),
294            ..Default::default()
295        }, // 8
296        XlsxX14SparklineGroup {
297            color_series: theme_color(6, Some(-0.249977111117893)),
298            color_negative: theme_color(7, None),
299            color_markers: theme_color(7, Some(-0.249977111117893)),
300            color_first: theme_color(7, Some(-0.249977111117893)),
301            color_last: theme_color(7, Some(-0.249977111117893)),
302            color_high: theme_color(7, Some(-0.249977111117893)),
303            color_low: theme_color(7, Some(-0.249977111117893)),
304            ..Default::default()
305        }, // 9
306        XlsxX14SparklineGroup {
307            color_series: theme_color(7, Some(-0.249977111117893)),
308            color_negative: theme_color(8, None),
309            color_markers: theme_color(8, Some(-0.249977111117893)),
310            color_first: theme_color(8, Some(-0.249977111117893)),
311            color_last: theme_color(8, Some(-0.249977111117893)),
312            color_high: theme_color(8, Some(-0.249977111117893)),
313            color_low: theme_color(8, Some(-0.249977111117893)),
314            ..Default::default()
315        }, // 10
316        XlsxX14SparklineGroup {
317            color_series: theme_color(8, Some(-0.249977111117893)),
318            color_negative: theme_color(9, None),
319            color_markers: theme_color(9, Some(-0.249977111117893)),
320            color_first: theme_color(9, Some(-0.249977111117893)),
321            color_last: theme_color(9, Some(-0.249977111117893)),
322            color_high: theme_color(9, Some(-0.249977111117893)),
323            color_low: theme_color(9, Some(-0.249977111117893)),
324            ..Default::default()
325        }, // 11
326        XlsxX14SparklineGroup {
327            color_series: theme_color(9, Some(-0.249977111117893)),
328            color_negative: theme_color(4, None),
329            color_markers: theme_color(4, Some(-0.249977111117893)),
330            color_first: theme_color(4, Some(-0.249977111117893)),
331            color_last: theme_color(4, Some(-0.249977111117893)),
332            color_high: theme_color(4, Some(-0.249977111117893)),
333            color_low: theme_color(4, Some(-0.249977111117893)),
334            ..Default::default()
335        }, // 12
336        XlsxX14SparklineGroup {
337            color_series: theme_color(4, None),
338            color_negative: theme_color(5, None),
339            color_markers: theme_color(4, Some(-0.249977111117893)),
340            color_first: theme_color(4, Some(-0.249977111117893)),
341            color_last: theme_color(4, Some(-0.249977111117893)),
342            color_high: theme_color(4, Some(-0.249977111117893)),
343            color_low: theme_color(4, Some(-0.249977111117893)),
344            ..Default::default()
345        }, // 13
346        XlsxX14SparklineGroup {
347            color_series: theme_color(5, None),
348            color_negative: theme_color(6, None),
349            color_markers: theme_color(5, Some(-0.249977111117893)),
350            color_first: theme_color(5, Some(-0.249977111117893)),
351            color_last: theme_color(5, Some(-0.249977111117893)),
352            color_high: theme_color(5, Some(-0.249977111117893)),
353            color_low: theme_color(5, Some(-0.249977111117893)),
354            ..Default::default()
355        }, // 14
356        XlsxX14SparklineGroup {
357            color_series: theme_color(6, None),
358            color_negative: theme_color(7, None),
359            color_markers: theme_color(6, Some(-0.249977111117893)),
360            color_first: theme_color(6, Some(-0.249977111117893)),
361            color_last: theme_color(6, Some(-0.249977111117893)),
362            color_high: theme_color(6, Some(-0.249977111117893)),
363            color_low: theme_color(6, Some(-0.249977111117893)),
364            ..Default::default()
365        }, // 15
366        XlsxX14SparklineGroup {
367            color_series: theme_color(7, None),
368            color_negative: theme_color(8, None),
369            color_markers: theme_color(7, Some(-0.249977111117893)),
370            color_first: theme_color(7, Some(-0.249977111117893)),
371            color_last: theme_color(7, Some(-0.249977111117893)),
372            color_high: theme_color(7, Some(-0.249977111117893)),
373            color_low: theme_color(7, Some(-0.249977111117893)),
374            ..Default::default()
375        }, // 16
376        XlsxX14SparklineGroup {
377            color_series: theme_color(8, None),
378            color_negative: theme_color(9, None),
379            color_markers: theme_color(8, Some(-0.249977111117893)),
380            color_first: theme_color(8, Some(-0.249977111117893)),
381            color_last: theme_color(8, Some(-0.249977111117893)),
382            color_high: theme_color(8, Some(-0.249977111117893)),
383            color_low: theme_color(8, Some(-0.249977111117893)),
384            ..Default::default()
385        }, // 17
386        XlsxX14SparklineGroup {
387            color_series: theme_color(9, None),
388            color_negative: theme_color(4, None),
389            color_markers: theme_color(9, Some(-0.249977111117893)),
390            color_first: theme_color(9, Some(-0.249977111117893)),
391            color_last: theme_color(9, Some(-0.249977111117893)),
392            color_high: theme_color(9, Some(-0.249977111117893)),
393            color_low: theme_color(9, Some(-0.249977111117893)),
394            ..Default::default()
395        }, // 18
396        XlsxX14SparklineGroup {
397            color_series: theme_color(4, Some(0.39997558519241921)),
398            color_negative: theme_color(0, Some(-0.499984740745262)),
399            color_markers: theme_color(4, Some(0.79998168889431442)),
400            color_first: theme_color(4, Some(-0.249977111117893)),
401            color_last: theme_color(4, Some(-0.249977111117893)),
402            color_high: theme_color(4, Some(-0.499984740745262)),
403            color_low: theme_color(4, Some(-0.499984740745262)),
404            ..Default::default()
405        }, // 19
406        XlsxX14SparklineGroup {
407            color_series: theme_color(5, Some(0.39997558519241921)),
408            color_negative: theme_color(0, Some(-0.499984740745262)),
409            color_markers: theme_color(5, Some(0.79998168889431442)),
410            color_first: theme_color(5, Some(-0.249977111117893)),
411            color_last: theme_color(5, Some(-0.249977111117893)),
412            color_high: theme_color(5, Some(-0.499984740745262)),
413            color_low: theme_color(5, Some(-0.499984740745262)),
414            ..Default::default()
415        }, // 20
416        XlsxX14SparklineGroup {
417            color_series: theme_color(6, Some(0.39997558519241921)),
418            color_negative: theme_color(0, Some(-0.499984740745262)),
419            color_markers: theme_color(6, Some(0.79998168889431442)),
420            color_first: theme_color(6, Some(-0.249977111117893)),
421            color_last: theme_color(6, Some(-0.249977111117893)),
422            color_high: theme_color(6, Some(-0.499984740745262)),
423            color_low: theme_color(6, Some(-0.499984740745262)),
424            ..Default::default()
425        }, // 21
426        XlsxX14SparklineGroup {
427            color_series: theme_color(7, Some(0.39997558519241921)),
428            color_negative: theme_color(0, Some(-0.499984740745262)),
429            color_markers: theme_color(7, Some(0.79998168889431442)),
430            color_first: theme_color(7, Some(-0.249977111117893)),
431            color_last: theme_color(7, Some(-0.249977111117893)),
432            color_high: theme_color(7, Some(-0.499984740745262)),
433            color_low: theme_color(7, Some(-0.499984740745262)),
434            ..Default::default()
435        }, // 22
436        XlsxX14SparklineGroup {
437            color_series: theme_color(8, Some(0.39997558519241921)),
438            color_negative: theme_color(0, Some(-0.499984740745262)),
439            color_markers: theme_color(8, Some(0.79998168889431442)),
440            color_first: theme_color(8, Some(-0.249977111117893)),
441            color_last: theme_color(8, Some(-0.249977111117893)),
442            color_high: theme_color(8, Some(-0.499984740745262)),
443            color_low: theme_color(8, Some(-0.499984740745262)),
444            ..Default::default()
445        }, // 23
446        XlsxX14SparklineGroup {
447            color_series: theme_color(9, Some(0.39997558519241921)),
448            color_negative: theme_color(0, Some(-0.499984740745262)),
449            color_markers: theme_color(9, Some(0.79998168889431442)),
450            color_first: theme_color(9, Some(-0.249977111117893)),
451            color_last: theme_color(9, Some(-0.249977111117893)),
452            color_high: theme_color(9, Some(-0.499984740745262)),
453            color_low: theme_color(9, Some(-0.499984740745262)),
454            ..Default::default()
455        }, // 24
456        XlsxX14SparklineGroup {
457            color_series: theme_color(1, Some(0.499984740745262)),
458            color_negative: theme_color(1, Some(0.249977111117893)),
459            color_markers: theme_color(1, Some(0.249977111117893)),
460            color_first: theme_color(1, Some(0.249977111117893)),
461            color_last: theme_color(1, Some(0.249977111117893)),
462            color_high: theme_color(1, Some(0.249977111117893)),
463            color_low: theme_color(1, Some(0.249977111117893)),
464            ..Default::default()
465        }, // 25
466        XlsxX14SparklineGroup {
467            color_series: theme_color(1, Some(0.34998626667073579)),
468            color_negative: theme_color(0, Some(0.249977111117893)),
469            color_markers: theme_color(0, Some(0.249977111117893)),
470            color_first: theme_color(0, Some(0.249977111117893)),
471            color_last: theme_color(0, Some(0.249977111117893)),
472            color_high: theme_color(0, Some(0.249977111117893)),
473            color_low: theme_color(0, Some(0.249977111117893)),
474            ..Default::default()
475        }, // 26
476        XlsxX14SparklineGroup {
477            color_series: rgb_color("FF323232"),
478            color_negative: rgb_color("FFD00000"),
479            color_markers: rgb_color("FFD00000"),
480            color_first: rgb_color("FFD00000"),
481            color_last: rgb_color("FFD00000"),
482            color_high: rgb_color("FFD00000"),
483            color_low: rgb_color("FFD00000"),
484            ..Default::default()
485        }, // 27
486        XlsxX14SparklineGroup {
487            color_series: rgb_color("FF000000"),
488            color_negative: rgb_color("FF0070C0"),
489            color_markers: rgb_color("FF0070C0"),
490            color_first: rgb_color("FF0070C0"),
491            color_last: rgb_color("FF0070C0"),
492            color_high: rgb_color("FF0070C0"),
493            color_low: rgb_color("FF0070C0"),
494            ..Default::default()
495        }, // 28
496        XlsxX14SparklineGroup {
497            color_series: rgb_color("FF376092"),
498            color_negative: rgb_color("FFD00000"),
499            color_markers: rgb_color("FFD00000"),
500            color_first: rgb_color("FFD00000"),
501            color_last: rgb_color("FFD00000"),
502            color_high: rgb_color("FFD00000"),
503            color_low: rgb_color("FFD00000"),
504            ..Default::default()
505        }, // 29
506        XlsxX14SparklineGroup {
507            color_series: rgb_color("FF0070C0"),
508            color_negative: rgb_color("FF000000"),
509            color_markers: rgb_color("FF000000"),
510            color_first: rgb_color("FF000000"),
511            color_last: rgb_color("FF000000"),
512            color_high: rgb_color("FF000000"),
513            color_low: rgb_color("FF000000"),
514            ..Default::default()
515        }, // 30
516        XlsxX14SparklineGroup {
517            color_series: rgb_color("FF5F5F5F"),
518            color_negative: rgb_color("FFFFB620"),
519            color_markers: rgb_color("FFD70077"),
520            color_first: rgb_color("FF5687C2"),
521            color_last: rgb_color("FF359CEB"),
522            color_high: rgb_color("FF56BE79"),
523            color_low: rgb_color("FFFF5055"),
524            ..Default::default()
525        }, // 31
526        XlsxX14SparklineGroup {
527            color_series: rgb_color("FF5687C2"),
528            color_negative: rgb_color("FFFFB620"),
529            color_markers: rgb_color("FFD70077"),
530            color_first: rgb_color("FF777777"),
531            color_last: rgb_color("FF359CEB"),
532            color_high: rgb_color("FF56BE79"),
533            color_low: rgb_color("FFFF5055"),
534            ..Default::default()
535        }, // 32
536        XlsxX14SparklineGroup {
537            color_series: rgb_color("FFC6EFCE"),
538            color_negative: rgb_color("FFFFC7CE"),
539            color_markers: rgb_color("FF8CADD6"),
540            color_first: rgb_color("FFFFDC47"),
541            color_last: rgb_color("FFFFEB9C"),
542            color_high: rgb_color("FF60D276"),
543            color_low: rgb_color("FFFF5367"),
544            ..Default::default()
545        }, // 33
546        XlsxX14SparklineGroup {
547            color_series: rgb_color("FF00B050"),
548            color_negative: rgb_color("FFFF0000"),
549            color_markers: rgb_color("FF0070C0"),
550            color_first: rgb_color("FFFFC000"),
551            color_last: rgb_color("FFFFC000"),
552            color_high: rgb_color("FF00B050"),
553            color_low: rgb_color("FFFF0000"),
554            ..Default::default()
555        }, // 34
556        XlsxX14SparklineGroup {
557            color_series: theme_color(3, None),
558            color_negative: theme_color(9, None),
559            color_markers: theme_color(8, None),
560            color_first: theme_color(4, None),
561            color_last: theme_color(5, None),
562            color_high: theme_color(6, None),
563            color_low: theme_color(7, None),
564            ..Default::default()
565        }, // 35
566        XlsxX14SparklineGroup {
567            color_series: theme_color(1, None),
568            color_negative: theme_color(9, None),
569            color_markers: theme_color(8, None),
570            color_first: theme_color(4, None),
571            color_last: theme_color(5, None),
572            color_high: theme_color(6, None),
573            color_low: theme_color(7, None),
574            ..Default::default()
575        }, // 36
576    ]
577}
578
579#[cfg(test)]
580mod tests {
581    use super::*;
582    use crate::options::Options;
583    use crate::xml::common::serialize_ext_lst;
584
585    #[test]
586    fn add_sparkline_basic() {
587        let f = File::new_with_options(Options::default());
588        f.add_sparkline(
589            "Sheet1",
590            &SparklineOptions {
591                location: vec!["A2".to_string()],
592                range: vec!["Sheet2!A1:J1".to_string()],
593                ..Default::default()
594            },
595        )
596        .unwrap();
597
598        let ws = f.work_sheet_reader("Sheet1").unwrap();
599        let ext_lst = ws.ext_lst.as_ref().unwrap();
600        assert_eq!(ext_lst.ext.len(), 1);
601        assert_eq!(
602            ext_lst.ext[0].uri.as_deref(),
603            Some(EXT_URI_SPARKLINE_GROUPS)
604        );
605        assert!(ext_lst.ext[0].content.contains("x14:sparklineGroup"));
606        assert!(ext_lst.ext[0].content.contains("Sheet2!A1:J1"));
607        assert!(ext_lst.ext[0].content.contains("A2"));
608    }
609
610    #[test]
611    fn add_sparkline_grouped() {
612        let f = File::new_with_options(Options::default());
613        f.add_sparkline(
614            "Sheet1",
615            &SparklineOptions {
616                location: vec!["A27".to_string(), "A28".to_string(), "A29".to_string()],
617                range: vec![
618                    "Sheet3!A5:J5".to_string(),
619                    "Sheet3!A6:J6".to_string(),
620                    "Sheet3!A7:J7".to_string(),
621                ],
622                markers: true,
623                ..Default::default()
624            },
625        )
626        .unwrap();
627
628        let ws = f.work_sheet_reader("Sheet1").unwrap();
629        let ext_lst = ws.ext_lst.as_ref().unwrap();
630        assert_eq!(ext_lst.ext.len(), 1);
631        let content = &ext_lst.ext[0].content;
632        assert_eq!(content.matches("</x14:sparklineGroup>").count(), 1);
633        assert_eq!(content.matches("<x14:sparkline>").count(), 3);
634    }
635
636    #[test]
637    fn add_sparkline_invalid_type() {
638        let f = File::new_with_options(Options::default());
639        let err = f
640            .add_sparkline(
641                "Sheet1",
642                &SparklineOptions {
643                    location: vec!["A1".to_string()],
644                    range: vec!["Sheet2!A1:J1".to_string()],
645                    r#type: "unknown_type".to_string(),
646                    ..Default::default()
647                },
648            )
649            .unwrap_err();
650        assert!(err.to_string().contains("'Type' value must be one of"));
651    }
652
653    #[test]
654    fn add_sparkline_invalid_style() {
655        let f = File::new_with_options(Options::default());
656        let err = f
657            .add_sparkline(
658                "Sheet1",
659                &SparklineOptions {
660                    location: vec!["A1".to_string()],
661                    range: vec!["Sheet2!A1:J1".to_string()],
662                    style: -1,
663                    ..Default::default()
664                },
665            )
666            .unwrap_err();
667        assert!(err.to_string().contains("'Style' value must be an integer"));
668    }
669
670    #[test]
671    fn add_sparkline_mismatched_location_range() {
672        let f = File::new_with_options(Options::default());
673        let err = f
674            .add_sparkline(
675                "Sheet1",
676                &SparklineOptions {
677                    location: vec!["A1".to_string(), "A2".to_string()],
678                    range: vec!["Sheet2!A1:J1".to_string()],
679                    ..Default::default()
680                },
681            )
682            .unwrap_err();
683        assert!(err.to_string().contains("Location") && err.to_string().contains("Range"));
684    }
685
686    #[test]
687    fn add_sparkline_append_mode() {
688        let f = File::new_with_options(Options::default());
689        f.add_sparkline(
690            "Sheet1",
691            &SparklineOptions {
692                location: vec!["A2".to_string()],
693                range: vec!["Sheet2!A1:J1".to_string()],
694                ..Default::default()
695            },
696        )
697        .unwrap();
698        f.add_sparkline(
699            "Sheet1",
700            &SparklineOptions {
701                location: vec!["A3".to_string()],
702                range: vec!["Sheet2!A2:J2".to_string()],
703                r#type: "column".to_string(),
704                ..Default::default()
705            },
706        )
707        .unwrap();
708
709        let ws = f.work_sheet_reader("Sheet1").unwrap();
710        let ext_lst = ws.ext_lst.as_ref().unwrap();
711        assert_eq!(ext_lst.ext.len(), 1);
712        let content = &ext_lst.ext[0].content;
713        assert_eq!(content.matches("</x14:sparklineGroup>").count(), 2);
714    }
715
716    #[test]
717    fn add_sparkline_with_existing_ext() {
718        let f = File::new_with_options(Options::default());
719        let path = f.get_sheet_xml_path("Sheet1").unwrap();
720        let mut ws = f.work_sheet_reader("Sheet1").unwrap();
721        ws.ext_lst = Some(XlsxExtLst {
722            ext: vec![
723                XlsxExt {
724                    uri: Some("{A8765BA9-456A-4dab-B4F3-ACF838C121DE}".to_string()),
725                    content: "<x14:slicerList />".to_string(),
726                    ..Default::default()
727                },
728                XlsxExt {
729                    uri: Some(EXT_URI_SPARKLINE_GROUPS.to_string()),
730                    content: format!(
731                        r#"<x14:sparklineGroups xmlns:xm="{}"></x14:sparklineGroups>"#,
732                        NAMESPACE_SPREADSHEET_EXCEL_2006_MAIN
733                    ),
734                    ..Default::default()
735                },
736            ],
737        });
738        f.sheet.insert(path, ws);
739
740        f.add_sparkline(
741            "Sheet1",
742            &SparklineOptions {
743                location: vec!["A3".to_string()],
744                range: vec!["Sheet2!A2:J2".to_string()],
745                r#type: "column".to_string(),
746                ..Default::default()
747            },
748        )
749        .unwrap();
750
751        let ws = f.work_sheet_reader("Sheet1").unwrap();
752        let ext_lst = ws.ext_lst.as_ref().unwrap();
753        assert_eq!(ext_lst.ext.len(), 2);
754        // Sparkline groups have higher priority than slicer list, so they come first.
755        assert_eq!(
756            ext_lst.ext[0].uri.as_deref(),
757            Some(EXT_URI_SPARKLINE_GROUPS)
758        );
759        assert_eq!(
760            ext_lst.ext[1].uri.as_deref(),
761            Some("{A8765BA9-456A-4dab-B4F3-ACF838C121DE}")
762        );
763        assert!(ext_lst.ext[0].content.contains("x14:sparklineGroup"));
764    }
765
766    #[test]
767    fn sparkline_presets_count() {
768        // Go defines 37 preset entries (labeled 0..36), but only styles 0..35
769        // are accepted by the validation check.
770        assert_eq!(get_sparkline_group_presets().len(), 37);
771    }
772
773    #[test]
774    fn serialize_ext_lst_round_trip() {
775        let ext_lst = XlsxExtLst {
776            ext: vec![XlsxExt {
777                uri: Some(EXT_URI_SPARKLINE_GROUPS.to_string()),
778                content: format!(
779                    r#"<x14:sparklineGroups xmlns:xm="{}"><x14:sparklineGroup type="line"><x14:sparklines><x14:sparkline><xm:f>Sheet2!A1:J1</xm:f><xm:sqref>A2</xm:sqref></x14:sparkline></x14:sparklines></x14:sparklineGroup></x14:sparklineGroups>"#,
780                    NAMESPACE_SPREADSHEET_EXCEL_2006_MAIN
781                ),
782                ..Default::default()
783            }],
784        };
785        let xml = serialize_ext_lst(&ext_lst);
786        assert!(xml.contains("x14:sparklineGroup"));
787        assert!(xml.contains("Sheet2!A1:J1"));
788    }
789
790    #[test]
791    fn save_sparkline_writes_valid_xml() {
792        let mut f = File::new_with_options(Options::default());
793        f.add_sparkline(
794            "Sheet1",
795            &SparklineOptions {
796                location: vec!["A2".to_string()],
797                range: vec!["Sheet2!A1:J1".to_string()],
798                markers: true,
799                ..Default::default()
800            },
801        )
802        .unwrap();
803
804        let tmp = std::env::temp_dir().join("excelize_rust_sparkline_test.xlsx");
805        f.save_as(tmp.to_str().unwrap()).unwrap();
806
807        let file = std::fs::File::open(&tmp).unwrap();
808        let mut archive = zip::ZipArchive::new(file).unwrap();
809        let mut sheet = archive.by_name("xl/worksheets/sheet1.xml").unwrap();
810        let mut xml = String::new();
811        std::io::Read::read_to_string(&mut sheet, &mut xml).unwrap();
812        drop(sheet);
813        drop(archive);
814        let _ = std::fs::remove_file(&tmp);
815
816        assert!(xml.contains("x14:sparklineGroups"));
817        assert!(xml.contains("x14:sparklineGroup"));
818        assert!(xml.contains("Sheet2!A1:J1"));
819        assert!(xml.contains("A2"));
820        assert!(xml.contains("xmlns:x14"));
821    }
822}