Skip to main content

excelize_rs/
chart.rs

1//! Chart API.
2//!
3//! Ported from Go `chart.go` and `drawing.go`.
4//!
5//! This is a functional subset: all chart types can be written, but only the
6//! most common types are fully tuned. Reading charts back via `get_charts`
7//! returns a lightweight representation of the chart series and title.
8
9use std::collections::HashMap;
10
11use quick_xml::de::from_reader as xml_from_reader;
12use quick_xml::se::to_string as xml_to_string;
13
14use crate::constants::{
15    DEFAULT_CHART_DIMENSION_HEIGHT, DEFAULT_CHART_DIMENSION_WIDTH, DEFAULT_CHART_LEGEND_POSITION,
16    DEFAULT_CHART_SHOW_BLANKS_AS, DEFAULT_DRAWING_SCALE, NAMESPACE_DRAWING_ML_CHART,
17    NAMESPACE_DRAWING_ML_MAIN, NAMESPACE_DRAWING_ML_SPREADSHEET, NAMESPACE_SPREADSHEET,
18    SOURCE_RELATIONSHIP, SOURCE_RELATIONSHIP_CHART, SOURCE_RELATIONSHIP_CHARTSHEET,
19    SOURCE_RELATIONSHIP_DRAWING_ML,
20};
21use crate::errors::Result;
22use crate::errors::{
23    ErrChartTitle, ErrExistsSheet, ErrTransparency, new_unsupported_chart_type_error,
24};
25use crate::file::File;
26use crate::lib_util::cell_name_to_coordinates;
27use crate::xml::chart::{
28    CAutoTitleDeleted, CAxs, CChart, CCharts, CLegend, CNumFmt, CPageMargins, CPlotArea,
29    CPrintSettings, CScaling, CSer, CSpPr, CStrRef, CThicknessSpPr, CTitle, CTx, CVal, CView3D,
30    XlsxChartSpace,
31};
32use crate::xml::decode_chart::{
33    DecodeARPr, DecodeCAxis, DecodeCCharts, DecodeCDLbls, DecodeCDPt, DecodeCMarker, DecodeCNumFmt,
34    DecodeCPlotArea, DecodeCSer, DecodeCSpPr, DecodeCTitle, DecodeChartSpace,
35};
36use crate::xml::drawing::{Fill, LineOptions, LineType};
37use crate::xml::chart_sheet::{
38    XlsxChartsheet, XlsxChartsheetView, XlsxChartsheetViews, XlsxDrawing as ChartsheetDrawing,
39};
40use crate::xml::common::{AttrValBool, AttrValFloat, AttrValInt, AttrValString, RichTextRun};
41use crate::xml::decode_drawing::{
42    DecodeCellAnchor, DecodeClientData, DecodeFrom, DecodePic, DecodeTo, DecodeWsDr,
43};
44use crate::xml::drawing::{
45    GraphicOptions, XdrCellAnchor, XdrClientData, XlsxBlip, XlsxBlipFill, XlsxCNvPicPr, XlsxCNvPr,
46    XlsxChart as XlsxChartRef, XlsxFrom, XlsxGraphic, XlsxGraphicData, XlsxGraphicFrame,
47    XlsxHlinkClick, XlsxNvGraphicFramePr, XlsxNvPicPr, XlsxOff, XlsxPic, XlsxPicLocks, XlsxPoint2D,
48    XlsxPositiveSize2D, XlsxPrstGeom, XlsxSpPr, XlsxStretch, XlsxTo, XlsxWsDr, XlsxXfrm,
49};
50use crate::xml::workbook::XlsxSheet;
51
52// ------------------------------------------------------------------
53// Re-exports of public types
54// ------------------------------------------------------------------
55
56pub use crate::xml::chart::{
57    Chart, ChartAxis, ChartDataLabel, ChartDataLabelPositionType, ChartDataPoint, ChartDimension,
58    ChartLegend, ChartMarker, ChartNumFmt, ChartPlotArea, ChartSeries, ChartTickLabelPositionType,
59    ChartTitle, ChartType, ChartUpDownBar,
60};
61
62// ------------------------------------------------------------------
63// Default value tables
64// ------------------------------------------------------------------
65
66const DEFAULT_EMU: i32 = 9525;
67
68const SUPPORTED_DRAWING_UNDERLINE_TYPES: &[&str] = &[
69    "none",
70    "words",
71    "sng",
72    "dbl",
73    "heavy",
74    "dotted",
75    "dottedHeavy",
76    "dash",
77    "dashHeavy",
78    "dashLong",
79    "dashLongHeavy",
80    "dotDash",
81    "dotDashHeavy",
82    "dotDotDash",
83    "dotDotDashHeavy",
84    "wavy",
85    "wavyHeavy",
86    "wavyDbl",
87];
88
89const SUPPORTED_DRAWING_TEXT_VERTICAL_TYPE: &[&str] = &[
90    "horz",
91    "vert",
92    "vert270",
93    "wordArtVert",
94    "eaVert",
95    "mongolianVert",
96    "wordArtVertRtl",
97];
98
99const CHART_LEGEND_POSITION: &[(&str, &str)] = &[
100    ("top", "t"),
101    ("left", "l"),
102    ("right", "r"),
103    ("top_right", "tr"),
104];
105
106fn chart_data_labels_position_types(t: ChartDataLabelPositionType) -> &'static str {
107    match t.0 {
108        1 => "bestFit",
109        2 => "b",
110        3 => "ctr",
111        4 => "inBase",
112        5 => "inEnd",
113        6 => "l",
114        7 => "outEnd",
115        8 => "r",
116        9 => "t",
117        _ => "",
118    }
119}
120
121fn supported_chart_data_labels_position(t: ChartType) -> &'static [ChartDataLabelPositionType] {
122    match t.0 {
123        6 | 7 | 8 => &[
124            ChartDataLabelPositionType::CENTER,
125            ChartDataLabelPositionType::INSIDE_BASE,
126            ChartDataLabelPositionType::INSIDE_END,
127            ChartDataLabelPositionType::OUTSIDE_END,
128        ],
129        21 | 22 | 23 => &[
130            ChartDataLabelPositionType::CENTER,
131            ChartDataLabelPositionType::INSIDE_BASE,
132            ChartDataLabelPositionType::INSIDE_END,
133            ChartDataLabelPositionType::OUTSIDE_END,
134        ],
135        0 | 3 | 24 => &[
136            ChartDataLabelPositionType::CENTER,
137            ChartDataLabelPositionType::INSIDE_BASE,
138            ChartDataLabelPositionType::INSIDE_END,
139        ],
140        1 | 4 | 25 => &[
141            ChartDataLabelPositionType::CENTER,
142            ChartDataLabelPositionType::INSIDE_BASE,
143            ChartDataLabelPositionType::INSIDE_END,
144        ],
145        2 | 5 | 26 | 27 => &[
146            ChartDataLabelPositionType::CENTER,
147            ChartDataLabelPositionType::INSIDE_BASE,
148            ChartDataLabelPositionType::INSIDE_END,
149        ],
150        41 => &[
151            ChartDataLabelPositionType::CENTER,
152            ChartDataLabelPositionType::LEFT,
153            ChartDataLabelPositionType::RIGHT,
154            ChartDataLabelPositionType::ABOVE,
155            ChartDataLabelPositionType::BELOW,
156        ],
157        43 | 44 | 45 | 46 => &[
158            ChartDataLabelPositionType::BEST_FIT,
159            ChartDataLabelPositionType::INSIDE_END,
160            ChartDataLabelPositionType::OUTSIDE_END,
161        ],
162        40 => &[
163            ChartDataLabelPositionType::BEST_FIT,
164            ChartDataLabelPositionType::INSIDE_END,
165            ChartDataLabelPositionType::OUTSIDE_END,
166        ],
167        47 => &[
168            ChartDataLabelPositionType::CENTER,
169            ChartDataLabelPositionType::LEFT,
170            ChartDataLabelPositionType::RIGHT,
171            ChartDataLabelPositionType::ABOVE,
172            ChartDataLabelPositionType::BELOW,
173        ],
174        _ => &[],
175    }
176}
177
178fn chart_view_3d_rot_x(t: ChartType) -> i64 {
179    match t.0 {
180        3..=5 | 9..=20 | 24..=39 | 49 | 50 => 15,
181        42 => 20,
182        44 => 30,
183        51 | 52 => 90,
184        _ => 0,
185    }
186}
187
188fn chart_view_3d_rot_y(t: ChartType) -> i64 {
189    match t.0 {
190        3..=5 | 9..=20 | 24..=39 | 49 | 50 => 20,
191        42 => 15,
192        _ => 0,
193    }
194}
195
196fn chart_view_3d_perspective(t: ChartType) -> i64 {
197    match t.0 {
198        42 => 30,
199        _ => 0,
200    }
201}
202
203fn chart_view_3d_rang_ax(t: ChartType) -> i64 {
204    match t.0 {
205        3..=5 | 9..=20 | 24..=39 => 1,
206        _ => 0,
207    }
208}
209
210fn plot_area_chart_grouping(t: ChartType) -> &'static str {
211    match t.0 {
212        2 | 5 | 8 | 11 | 14 | 17 | 20 | 23 | 27 | 31 | 35 | 39 => "percentStacked",
213        1 | 4 | 7 | 10 | 13 | 16 | 19 | 22 | 26 | 30 | 34 | 38 => "stacked",
214        0 | 3 | 24 | 28 | 32 | 36 | 41 | 42 => "standard",
215        _ => "clustered",
216    }
217}
218
219fn plot_area_chart_bar_dir(t: ChartType) -> Option<&'static str> {
220    match t.0 {
221        6..=20 => Some("bar"),
222        21..=39 => Some("col"),
223        _ => None,
224    }
225}
226
227fn chart_val_ax_num_fmt(t: ChartType) -> &'static str {
228    match t.0 {
229        2 | 5 | 8 | 11 | 14 | 17 | 20 | 23 | 27 | 31 | 35 | 39 => "0%",
230        _ => "General",
231    }
232}
233
234fn chart_val_ax_cross_between(t: ChartType) -> &'static str {
235    match t.0 {
236        0..=5 | 49..=52 | 53 | 54 => "midCat",
237        _ => "between",
238    }
239}
240
241fn chart_shape(t: ChartType) -> Option<&'static str> {
242    match t.0 {
243        12 | 13 | 14 | 28 | 29 | 30 | 31 => Some("cone"),
244        15 | 16 | 17 | 32 | 33 | 34 | 35 => Some("pyramid"),
245        18 | 19 | 20 | 36 | 37 | 38 | 39 => Some("cylinder"),
246        _ => None,
247    }
248}
249
250fn is_bar_col(t: ChartType) -> bool {
251    (6..=39).contains(&t.0)
252}
253
254fn is_3d_chart(t: ChartType) -> bool {
255    matches!(t.0, 3..=5 | 9..=20 | 24..=39 | 42 | 44 | 49 | 50)
256}
257
258fn chart_element_name(t: ChartType) -> Option<&'static str> {
259    match t.0 {
260        0..=2 => Some("areaChart"),
261        3..=5 => Some("area3DChart"),
262        6..=8 | 21..=23 => Some("barChart"),
263        9..=20 | 24..=39 => Some("bar3DChart"),
264        40 => Some("doughnutChart"),
265        41 => Some("lineChart"),
266        42 => Some("line3DChart"),
267        43 => Some("pieChart"),
268        44 => Some("pie3DChart"),
269        45 | 46 => Some("ofPieChart"),
270        47 => Some("radarChart"),
271        48 => Some("scatterChart"),
272        49 | 50 => Some("surface3DChart"),
273        51 | 52 => Some("surfaceChart"),
274        53 | 54 => Some("bubbleChart"),
275        55 | 56 => Some("stockChart"),
276        _ => None,
277    }
278}
279
280fn chart_series_uses_xy(t: ChartType) -> bool {
281    matches!(t.0, 48 | 53 | 54)
282}
283
284fn chart_series_uses_bubble_size(t: ChartType) -> bool {
285    matches!(t.0, 53 | 54)
286}
287
288// ------------------------------------------------------------------
289// Public API
290// ------------------------------------------------------------------
291
292impl File {
293    /// Add a chart to a worksheet.
294    pub fn add_chart(&mut self, sheet: &str, cell: &str, chart: &Chart) -> Result<()> {
295        let _ = self.work_sheet_reader(sheet)?;
296        let opts = parse_chart_options(chart)?;
297        let mut combo = Vec::new();
298        let mut order = opts.series.len() as i64;
299        for c in &chart.combo {
300            if c.r#type.0 > 56 {
301                return Err(new_unsupported_chart_type_error(c.r#type.0 as i32).into());
302            }
303            let mut co = parse_chart_options(c)?;
304            co.order = order;
305            order += co.series.len() as i64;
306            combo.push(co);
307        }
308
309        let drawing_id = self.count_drawings() + 1;
310        let chart_id = self.count_charts() + 1;
311        let drawing_xml = format!("xl/drawings/drawing{drawing_id}.xml");
312        let (drawing_id, drawing_xml) = self.prepare_drawing(sheet, drawing_id, &drawing_xml)?;
313        let drawing_rels = format!("xl/drawings/_rels/drawing{drawing_id}.xml.rels");
314        let drawing_rid = self.add_rels(
315            &drawing_rels,
316            SOURCE_RELATIONSHIP_CHART,
317            &format!("../charts/chart{chart_id}.xml"),
318            "",
319        );
320
321        self.add_drawing_chart(
322            sheet,
323            &drawing_xml,
324            cell,
325            opts.dimension.width as i32,
326            opts.dimension.height as i32,
327            drawing_rid,
328            &opts.format,
329        )?;
330        self.add_chart_xml(&opts, &combo);
331        self.add_content_type_part(chart_id, "chart")?;
332        let _ = self.add_content_type_part(drawing_id, "drawings");
333        self.add_sheet_name_space(sheet, NAMESPACE_SPREADSHEET);
334        Ok(())
335    }
336
337    /// Create a chartsheet (a worksheet containing only a chart).
338    pub fn add_chart_sheet(&mut self, sheet: &str, chart: &Chart) -> Result<()> {
339        if self.get_sheet_index(sheet).is_ok() {
340            return Err(Box::new(ErrExistsSheet));
341        }
342        let opts = parse_chart_options(chart)?;
343        let mut combo = Vec::new();
344        let mut order = opts.series.len() as i64;
345        for c in &chart.combo {
346            if c.r#type.0 > 56 {
347                return Err(new_unsupported_chart_type_error(c.r#type.0 as i32).into());
348            }
349            let mut co = parse_chart_options(c)?;
350            co.order = order;
351            order += co.series.len() as i64;
352            combo.push(co);
353        }
354
355        let mut wb = self.workbook_reader()?;
356        let mut max_sheet_id = 0i64;
357        let mut max_rid = 0i32;
358        for s in &wb.sheets.sheet {
359            if let Some(id) = s.sheet_id {
360                if id > max_sheet_id {
361                    max_sheet_id = id;
362                }
363            }
364            if let Some(rid) = &s.id {
365                if let Ok(n) = rid.trim_start_matches("rId").parse::<i32>() {
366                    if n > max_rid {
367                        max_rid = n;
368                    }
369                }
370            }
371        }
372        for rel in &self
373            .rels_reader(&self.get_workbook_rels_path())?
374            .unwrap_or_default()
375            .relationships
376        {
377            if let Ok(n) = rel.id.trim_start_matches("rId").parse::<i32>() {
378                if n > max_rid {
379                    max_rid = n;
380                }
381            }
382        }
383        max_sheet_id += 1;
384        max_rid += 1;
385
386        let path = format!("xl/chartsheets/sheet{max_sheet_id}.xml");
387        let new_rid = format!("rId{max_rid}");
388        wb.sheets.sheet.push(XlsxSheet {
389            name: Some(sheet.to_string()),
390            sheet_id: Some(max_sheet_id),
391            id: Some(new_rid.clone()),
392            plain_id: None,
393            state: None,
394        });
395
396        let drawing_id = self.count_drawings() + 1;
397        let chart_id = self.count_charts() + 1;
398        let drawing_xml = format!("xl/drawings/drawing{drawing_id}.xml");
399        let sheet_rels = format!(
400            "xl/chartsheets/_rels/{}.rels",
401            path.trim_start_matches("xl/chartsheets/")
402        );
403        let sheet_drawing_target = format!("../drawings/drawing{drawing_id}.xml");
404        let sheet_drawing_rid = self.add_rels(
405            &sheet_rels,
406            SOURCE_RELATIONSHIP_DRAWING_ML,
407            &sheet_drawing_target,
408            "",
409        );
410
411        let cs = XlsxChartsheet {
412            sheet_views: Some(XlsxChartsheetViews {
413                sheet_view: vec![XlsxChartsheetView {
414                    zoom_scale: Some(100),
415                    zoom_to_fit: Some(true),
416                    ..Default::default()
417                }],
418                ..Default::default()
419            }),
420            drawing: Some(ChartsheetDrawing {
421                r_id: Some(format!("rId{sheet_drawing_rid}")),
422            }),
423            ..Default::default()
424        };
425        let cs_bytes = xml_to_string(&cs)?.into_bytes();
426        self.save_file_list(&path, &cs_bytes);
427        self.sheet_map
428            .lock()
429            .unwrap()
430            .insert(sheet.to_string(), path.clone());
431
432        let drawing_rels = format!("xl/drawings/_rels/drawing{drawing_id}.xml.rels");
433        let drawing_rid = self.add_rels(
434            &drawing_rels,
435            SOURCE_RELATIONSHIP_CHART,
436            &format!("../charts/chart{chart_id}.xml"),
437            "",
438        );
439        self.add_sheet_drawing_chart(&drawing_xml, drawing_rid, &opts.format)?;
440        self.add_chart_xml(&opts, &combo);
441        self.add_content_type_part(chart_id, "chart")?;
442        let _ = self.add_content_type_part(max_sheet_id as i32, "chartsheet");
443        let _ = self.add_content_type_part(drawing_id, "drawings");
444
445        self.relationships.insert(self.get_workbook_rels_path(), {
446            let mut rels = self
447                .rels_reader(&self.get_workbook_rels_path())?
448                .unwrap_or_default();
449            rels.relationships
450                .push(crate::xml::workbook::XlsxRelationship {
451                    id: new_rid,
452                    r#type: SOURCE_RELATIONSHIP_CHARTSHEET.to_string(),
453                    target: format!("/xl/chartsheets/sheet{max_sheet_id}.xml"),
454                    target_mode: None,
455                });
456            rels
457        });
458        *self.workbook.lock().unwrap() = Some(wb);
459        *self.sheet_count.lock().unwrap() += 1;
460        self.add_sheet_name_space(sheet, NAMESPACE_SPREADSHEET);
461        Ok(())
462    }
463
464    /// Delete the chart at a given cell in a worksheet.
465    pub fn delete_chart(&mut self, sheet: &str, cell: &str) -> Result<()> {
466        let (col, row) = cell_name_to_coordinates(cell)?;
467        let col = col - 1;
468        let row = row - 1;
469        let ws = self.work_sheet_reader(sheet)?;
470        if ws.drawing.is_none() {
471            return Ok(());
472        }
473        let drawing_xml = self
474            .get_sheet_relationships_target_by_id(
475                sheet,
476                ws.drawing.as_ref().unwrap().rid.as_deref().unwrap_or(""),
477            )
478            .replace("..", "xl")
479            .trim_start_matches('/')
480            .to_string();
481        self.delete_drawing(col, row, &drawing_xml, "Chart")?;
482        Ok(())
483    }
484
485    /// Return all charts embedded in a worksheet.
486    pub fn get_charts(&self, sheet: &str) -> Result<Vec<Chart>> {
487        let ws = self.work_sheet_reader(sheet)?;
488        if ws.drawing.is_none() {
489            return Ok(Vec::new());
490        }
491        let drawing_xml = self
492            .get_sheet_relationships_target_by_id(
493                sheet,
494                ws.drawing.as_ref().unwrap().rid.as_deref().unwrap_or(""),
495            )
496            .replace("..", "xl")
497            .trim_start_matches('/')
498            .to_string();
499        let drawing_rels = drawing_xml
500            .replace("xl/drawings", "xl/drawings/_rels")
501            .replace(".xml", ".xml.rels");
502        self.get_charts_from_drawing(&drawing_xml, &drawing_rels)
503    }
504}
505
506// ------------------------------------------------------------------
507// Option parsing
508// ------------------------------------------------------------------
509
510fn parse_chart_options(chart: &Chart) -> Result<Chart> {
511    if chart.r#type.0 > 56 {
512        return Err(new_unsupported_chart_type_error(chart.r#type.0 as i32).into());
513    }
514    let mut opts = chart.clone();
515    if opts.dimension.width == 0 {
516        opts.dimension.width = DEFAULT_CHART_DIMENSION_WIDTH as u64;
517    }
518    if opts.dimension.height == 0 {
519        opts.dimension.height = DEFAULT_CHART_DIMENSION_HEIGHT as u64;
520    }
521    if opts.legend.position.is_empty() {
522        opts.legend.position = DEFAULT_CHART_LEGEND_POSITION.to_string();
523    }
524    parse_title(&mut opts.title)?;
525    if !(0..=100).contains(&opts.fill.transparency) {
526        return Err(Box::new(ErrTransparency));
527    }
528    if opts.vary_colors.is_none() {
529        opts.vary_colors = Some(true);
530    }
531    if opts.border.width == 0.0 {
532        opts.border.width = 0.75;
533    }
534    if opts.show_blanks_as.is_empty() {
535        opts.show_blanks_as = DEFAULT_CHART_SHOW_BLANKS_AS.to_string();
536    }
537    opts.format = parse_graphic_options(&opts.format)?;
538    for series in &opts.series {
539        if !(0..=100).contains(&series.fill.transparency) {
540            return Err(Box::new(ErrTransparency));
541        }
542    }
543    Ok(opts)
544}
545
546fn parse_title(title: &mut ChartTitle) -> Result<()> {
547    if !(0..=100).contains(&title.offset_x)
548        || !(0..=100).contains(&title.offset_y)
549        || !(0..=100).contains(&title.width)
550        || !(0..=100).contains(&title.height)
551    {
552        return Err(Box::new(ErrChartTitle));
553    }
554    if !title.paragraph.is_empty() && !title.formula.is_empty() {
555        return Err(Box::new(ErrChartTitle));
556    }
557    if title.font.is_none() {
558        title.font = Some(crate::styles::Font::default());
559    }
560    let font = title.font.as_mut().unwrap();
561    if font.color.as_deref().unwrap_or("").is_empty() {
562        font.color = Some("595959".to_string());
563    }
564    if font.size.unwrap_or(0.0) == 0.0 {
565        font.size = Some(14.0);
566    }
567    for run in &mut title.paragraph {
568        if run.font.is_none() {
569            run.font = Some(crate::styles::Font::default());
570        }
571        let f = run.font.as_mut().unwrap();
572        if f.color.as_deref().unwrap_or("").is_empty() {
573            f.color = Some("595959".to_string());
574        }
575        if f.size.unwrap_or(0.0) == 0.0 {
576            f.size = Some(14.0);
577        }
578    }
579    Ok(())
580}
581
582pub(crate) fn parse_graphic_options(opts: &GraphicOptions) -> Result<GraphicOptions> {
583    let mut out = opts.clone();
584    if out.print_object.is_none() {
585        out.print_object = Some(true);
586    }
587    if out.locked.is_none() {
588        out.locked = Some(true);
589    }
590    if out.scale_x == 0.0 {
591        out.scale_x = DEFAULT_DRAWING_SCALE;
592    }
593    if out.scale_y == 0.0 {
594        out.scale_y = DEFAULT_DRAWING_SCALE;
595    }
596    if !out.positioning.is_empty()
597        && !["oneCell", "twoCell", "absolute"].contains(&out.positioning.as_str())
598    {
599        return Err(crate::errors::new_invalid_optional_value(
600            "Positioning",
601            &out.positioning,
602            &["oneCell", "twoCell", "absolute"],
603        )
604        .into());
605    }
606    Ok(out)
607}
608
609// ------------------------------------------------------------------
610// Chart XML generation
611// ------------------------------------------------------------------
612
613impl File {
614    fn add_chart_xml(&self, opts: &Chart, combo: &[Chart]) {
615        let chart_id = self.count_charts() + 1;
616        let mut space = XlsxChartSpace {
617            xmlns_a: Some(NAMESPACE_DRAWING_ML_MAIN.to_string()),
618            xmlns_c: Some(NAMESPACE_DRAWING_ML_CHART.to_string()),
619            date_1904: Some(AttrValBool { val: Some(false) }),
620            lang: Some(AttrValString {
621                val: Some("en-US".to_string()),
622            }),
623            rounded_corners: Some(AttrValBool { val: Some(false) }),
624            chart: CChart {
625                title: draw_title(&opts.title, ""),
626                auto_title_deleted: Some(CAutoTitleDeleted { val: false }),
627                view_3d: if is_3d_chart(opts.r#type) {
628                    Some(CView3D {
629                        rot_x: Some(AttrValInt {
630                            val: Some(chart_view_3d_rot_x(opts.r#type)),
631                        }),
632                        rot_y: Some(AttrValInt {
633                            val: Some(chart_view_3d_rot_y(opts.r#type)),
634                        }),
635                        perspective: Some(AttrValInt {
636                            val: Some(chart_view_3d_perspective(opts.r#type)),
637                        }),
638                        r_ang_ax: Some(AttrValInt {
639                            val: Some(chart_view_3d_rang_ax(opts.r#type)),
640                        }),
641                        ..Default::default()
642                    })
643                } else {
644                    None
645                },
646                floor: if is_3d_chart(opts.r#type) {
647                    Some(CThicknessSpPr {
648                        thickness: Some(AttrValInt { val: Some(0) }),
649                        ..Default::default()
650                    })
651                } else {
652                    None
653                },
654                side_wall: if is_3d_chart(opts.r#type) {
655                    Some(CThicknessSpPr {
656                        thickness: Some(AttrValInt { val: Some(0) }),
657                        ..Default::default()
658                    })
659                } else {
660                    None
661                },
662                back_wall: if is_3d_chart(opts.r#type) {
663                    Some(CThicknessSpPr {
664                        thickness: Some(AttrValInt { val: Some(0) }),
665                        ..Default::default()
666                    })
667                } else {
668                    None
669                },
670                plot_area: Some(build_plot_area(opts, combo)),
671                plot_vis_only: Some(AttrValBool { val: Some(false) }),
672                disp_blanks_as: Some(AttrValString {
673                    val: Some(opts.show_blanks_as.clone()),
674                }),
675                show_d_lbls_over_max: Some(AttrValBool { val: Some(false) }),
676                ..Default::default()
677            },
678            sp_pr: Some(CSpPr {
679                solid_fill: Some(crate::xml::drawing::ASolidFill {
680                    scheme_clr: Some(crate::xml::drawing::ASchemeClr {
681                        val: Some("bg1".to_string()),
682                        ..Default::default()
683                    }),
684                    ..Default::default()
685                }),
686                ln: draw_chart_ln(&opts.border),
687                ..Default::default()
688            }),
689            print_settings: Some(CPrintSettings {
690                page_margins: Some(CPageMargins {
691                    b: 0.75,
692                    l: 0.7,
693                    r: 0.7,
694                    t: 0.7,
695                    header: 0.3,
696                    footer: 0.3,
697                }),
698                ..Default::default()
699            }),
700            ..Default::default()
701        };
702        if let Some(sp_pr) = &mut space.sp_pr {
703            apply_fill(sp_pr, &opts.fill);
704        }
705        space.chart.legend = draw_legend(opts);
706        if let Some(pa) = &mut space.chart.plot_area {
707            pa.sp_pr = Some(draw_plot_area_sp_pr());
708            if let Some(sp_pr) = &mut pa.sp_pr {
709                apply_fill(sp_pr, &opts.plot_area.fill);
710            }
711            pa.d_table = draw_plot_area_d_table(opts);
712        }
713
714        let chart_xml = xml_to_string(&space).unwrap_or_default().into_bytes();
715        self.save_file_list(&format!("xl/charts/chart{chart_id}.xml"), &chart_xml);
716    }
717}
718
719fn build_plot_area(opts: &Chart, combo: &[Chart]) -> CPlotArea {
720    let element = match chart_element_name(opts.r#type) {
721        Some(name) => name,
722        None => return CPlotArea::default(),
723    };
724    let chart = build_chart(opts);
725    let mut pa = CPlotArea::default();
726    match element {
727        "areaChart" => pa.area_chart = vec![chart],
728        "area3DChart" => pa.area_3d_chart = vec![chart],
729        "barChart" => pa.bar_chart = vec![chart],
730        "bar3DChart" => pa.bar_3d_chart = vec![chart],
731        "bubbleChart" => pa.bubble_chart = vec![chart],
732        "doughnutChart" => pa.doughnut_chart = vec![chart],
733        "lineChart" => pa.line_chart = vec![chart],
734        "line3DChart" => pa.line_3d_chart = vec![chart],
735        "pieChart" => pa.pie_chart = vec![chart],
736        "pie3DChart" => pa.pie_3d_chart = vec![chart],
737        "ofPieChart" => pa.of_pie_chart = vec![chart],
738        "radarChart" => pa.radar_chart = vec![chart],
739        "scatterChart" => pa.scatter_chart = vec![chart],
740        "surface3DChart" => pa.surface_3d_chart = vec![chart],
741        "surfaceChart" => pa.surface_chart = vec![chart],
742        "stockChart" => pa.stock_chart = vec![chart],
743        _ => {}
744    }
745
746    for co in combo {
747        if let Some(name) = chart_element_name(co.r#type) {
748            let co_chart = build_chart(co);
749            match name {
750                "areaChart" => pa.area_chart.push(co_chart),
751                "area3DChart" => pa.area_3d_chart.push(co_chart),
752                "barChart" => pa.bar_chart.push(co_chart),
753                "bar3DChart" => pa.bar_3d_chart.push(co_chart),
754                "bubbleChart" => pa.bubble_chart.push(co_chart),
755                "doughnutChart" => pa.doughnut_chart.push(co_chart),
756                "lineChart" => pa.line_chart.push(co_chart),
757                "line3DChart" => pa.line_3d_chart.push(co_chart),
758                "pieChart" => pa.pie_chart.push(co_chart),
759                "pie3DChart" => pa.pie_3d_chart.push(co_chart),
760                "ofPieChart" => pa.of_pie_chart.push(co_chart),
761                "radarChart" => pa.radar_chart.push(co_chart),
762                "scatterChart" => pa.scatter_chart.push(co_chart),
763                "surface3DChart" => pa.surface_3d_chart.push(co_chart),
764                "surfaceChart" => pa.surface_chart.push(co_chart),
765                "stockChart" => pa.stock_chart.push(co_chart),
766                _ => {}
767            }
768        }
769    }
770
771    if !chart_series_uses_xy(opts.r#type) {
772        pa.cat_ax = draw_plot_area_cat_ax(&pa, opts);
773    }
774    if chart_series_uses_xy(opts.r#type) {
775        pa.val_ax = draw_plot_area_cat_ax(&pa, opts);
776        pa.val_ax.extend(draw_plot_area_val_ax(&pa, opts));
777    } else {
778        pa.val_ax = draw_plot_area_val_ax(&pa, opts);
779    }
780
781    // Generate secondary axes for combo charts that request a secondary Y-axis.
782    if let Some(secondary_opts) = combo
783        .iter()
784        .rev()
785        .find(|co| co.order > 0 && co.y_axis.secondary)
786    {
787        pa.cat_ax.extend(draw_plot_area_cat_ax(&pa, secondary_opts));
788        pa.val_ax.extend(draw_plot_area_val_ax(&pa, secondary_opts));
789    }
790
791    if opts.r#type == ChartType::STOCK_HIGH_LOW_CLOSE
792        || opts.r#type == ChartType::STOCK_OPEN_HIGH_LOW_CLOSE
793    {
794        pa.date_ax = pa.cat_ax.clone();
795        pa.cat_ax.clear();
796    }
797    if (49..=52).contains(&opts.r#type.0) {
798        pa.ser_ax = draw_plot_area_ser_ax(opts);
799    }
800    pa
801}
802
803fn build_chart(opts: &Chart) -> CCharts {
804    let mut c = CCharts {
805        vary_colors: Some(AttrValBool {
806            val: Some(opts.vary_colors.unwrap_or(true)),
807        }),
808        ser: Some(draw_chart_series(opts)),
809        ax_id: gen_ax_id(opts),
810        ..Default::default()
811    };
812    if let Some(dir) = plot_area_chart_bar_dir(opts.r#type) {
813        c.bar_dir = Some(AttrValString {
814            val: Some(dir.to_string()),
815        });
816        c.grouping = Some(AttrValString {
817            val: Some(plot_area_chart_grouping(opts.r#type).to_string()),
818        });
819    }
820    if matches!(opts.r#type.0, 0..=5) {
821        c.grouping = Some(AttrValString {
822            val: Some(plot_area_chart_grouping(opts.r#type).to_string()),
823        });
824    }
825    if let Some(shape) = chart_shape(opts.r#type) {
826        c.shape = Some(AttrValString {
827            val: Some(shape.to_string()),
828        });
829    }
830    if opts.r#type == ChartType::DOUGHNUT {
831        let hole = if opts.hole_size > 0 && opts.hole_size <= 90 {
832            opts.hole_size
833        } else {
834            75
835        };
836        c.hole_size = Some(AttrValInt { val: Some(hole) });
837    }
838    if is_bar_col(opts.r#type) {
839        if let Some(gw) = opts.gap_width {
840            if gw <= 500 {
841                c.gap_width = Some(AttrValInt {
842                    val: Some(gw as i64),
843                });
844            }
845        }
846        let mut overlap = if matches!(opts.r#type.0, 7 | 8 | 22 | 23) {
847            Some(100)
848        } else {
849            None
850        };
851        if let Some(o) = opts.overlap {
852            if (-100..=100).contains(&o) {
853                overlap = Some(o);
854            }
855        }
856        if let Some(o) = overlap {
857            c.overlap = Some(AttrValInt { val: Some(o) });
858        }
859    }
860    if matches!(opts.r#type.0, 53 | 54) && opts.bubble_size > 0 && opts.bubble_size <= 300 {
861        c.bubble_scale = Some(AttrValFloat {
862            val: Some(opts.bubble_size as f64),
863        });
864    }
865    if opts.r#type == ChartType::LINE || opts.r#type == ChartType::LINE_3D {
866        c.grouping = Some(AttrValString {
867            val: Some("standard".to_string()),
868        });
869        c.vary_colors = Some(AttrValBool { val: Some(false) });
870    }
871    if opts.r#type == ChartType::RADAR {
872        c.radar_style = Some(AttrValString {
873            val: Some("marker".to_string()),
874        });
875        c.vary_colors = Some(AttrValBool { val: Some(false) });
876    }
877    if opts.r#type == ChartType::SCATTER {
878        c.scatter_style = Some(AttrValString {
879            val: Some("smoothMarker".to_string()),
880        });
881        c.vary_colors = Some(AttrValBool { val: Some(false) });
882    }
883    if opts.r#type == ChartType::PIE_OF_PIE {
884        c.of_pie_type = Some(AttrValString {
885            val: Some("pie".to_string()),
886        });
887        if opts.plot_area.second_plot_values > 0 {
888            c.split_pos = Some(AttrValInt {
889                val: Some(opts.plot_area.second_plot_values),
890            });
891        }
892        c.ser_lines = Some(AttrValString::default());
893    }
894    if opts.r#type == ChartType::BAR_OF_PIE {
895        c.of_pie_type = Some(AttrValString {
896            val: Some("bar".to_string()),
897        });
898        if opts.plot_area.second_plot_values > 0 {
899            c.split_pos = Some(AttrValInt {
900                val: Some(opts.plot_area.second_plot_values),
901            });
902        }
903        c.ser_lines = Some(AttrValString::default());
904    }
905    if opts.x_axis.drop_lines && matches!(opts.r#type.0, 0..=5 | 41 | 42) {
906        c.drop_lines = Some(crate::xml::chart::CLines::default());
907    }
908    if opts.x_axis.high_low_lines && opts.r#type == ChartType::LINE {
909        c.hi_low_lines = Some(crate::xml::chart::CLines::default());
910    }
911    if opts.r#type == ChartType::WIREFRAME_SURFACE_3D || opts.r#type == ChartType::WIREFRAME_CONTOUR
912    {
913        c.wireframe = Some(AttrValBool { val: Some(true) });
914    }
915    if opts.r#type == ChartType::STOCK_HIGH_LOW_CLOSE {
916        c.hi_low_lines = Some(crate::xml::chart::CLines::default());
917    }
918    if opts.r#type == ChartType::STOCK_OPEN_HIGH_LOW_CLOSE {
919        c.hi_low_lines = Some(crate::xml::chart::CLines::default());
920        let mut up_sp_pr = CSpPr::default();
921        if let Some(ln) = draw_chart_ln(&opts.plot_area.up_bars.border) {
922            up_sp_pr.ln = Some(ln);
923        }
924        apply_fill(&mut up_sp_pr, &opts.plot_area.up_bars.fill);
925        let mut down_sp_pr = CSpPr::default();
926        if let Some(ln) = draw_chart_ln(&opts.plot_area.down_bars.border) {
927            down_sp_pr.ln = Some(ln);
928        }
929        apply_fill(&mut down_sp_pr, &opts.plot_area.down_bars.fill);
930        c.up_down_bars = Some(crate::xml::chart::CUpDownBars {
931            gap_width: Some(AttrValString {
932                val: Some("150".to_string()),
933            }),
934            up_bars: Some(crate::xml::chart::CLines {
935                sp_pr: Some(up_sp_pr),
936            }),
937            down_bars: Some(crate::xml::chart::CLines {
938                sp_pr: Some(down_sp_pr),
939            }),
940            ..Default::default()
941        });
942    }
943    c.d_lbls = Some(draw_chart_d_lbls(opts));
944    c
945}
946
947fn gen_ax_id(opts: &Chart) -> Vec<AttrValInt> {
948    let (x, y) = if opts.order > 0 && opts.y_axis.secondary {
949        (100000003, 100000004)
950    } else {
951        (100000000, 100000001)
952    };
953    let mut ids = vec![AttrValInt { val: Some(x) }, AttrValInt { val: Some(y) }];
954    if (49..=52).contains(&opts.r#type.0) {
955        ids.push(AttrValInt {
956            val: Some(100000005),
957        });
958    }
959    ids
960}
961
962fn draw_chart_series(opts: &Chart) -> Vec<CSer> {
963    opts.series
964        .iter()
965        .enumerate()
966        .map(|(k, s)| CSer {
967            idx: Some(AttrValInt {
968                val: Some(k as i64 + opts.order),
969            }),
970            order: Some(AttrValInt {
971                val: Some(k as i64 + opts.order),
972            }),
973            tx: Some(CTx {
974                str_ref: Some(CStrRef {
975                    f: s.name.clone(),
976                    ..Default::default()
977                }),
978                ..Default::default()
979            }),
980            sp_pr: draw_chart_series_sp_pr(k, s, opts),
981            marker: draw_series_marker(k, s, opts),
982            d_pt: draw_chart_series_d_pt(k, s, opts),
983            d_lbls: draw_chart_series_d_lbls(k, s, opts),
984            invert_if_negative: Some(AttrValBool { val: Some(false) }),
985            cat: if chart_series_uses_xy(opts.r#type) || s.categories.is_empty() {
986                None
987            } else {
988                Some(crate::xml::chart::CCat {
989                    str_ref: Some(CStrRef {
990                        f: s.categories.clone(),
991                        ..Default::default()
992                    }),
993                })
994            },
995            x_val: if chart_series_uses_xy(opts.r#type) && !s.categories.is_empty() {
996                Some(crate::xml::chart::CCat {
997                    str_ref: Some(CStrRef {
998                        f: s.categories.clone(),
999                        ..Default::default()
1000                    }),
1001                })
1002            } else {
1003                None
1004            },
1005            val: if chart_series_uses_xy(opts.r#type) {
1006                None
1007            } else {
1008                Some(CVal {
1009                    num_ref: Some(crate::xml::chart::CNumRef {
1010                        f: s.values.clone(),
1011                        num_cache: Some(crate::xml::chart::CNumCache::default()),
1012                    }),
1013                })
1014            },
1015            y_val: if chart_series_uses_xy(opts.r#type) {
1016                Some(CVal {
1017                    num_ref: Some(crate::xml::chart::CNumRef {
1018                        f: s.values.clone(),
1019                        num_cache: Some(crate::xml::chart::CNumCache::default()),
1020                    }),
1021                })
1022            } else {
1023                None
1024            },
1025            smooth: Some(AttrValBool {
1026                val: Some(s.line.smooth),
1027            }),
1028            bubble_size: if chart_series_uses_bubble_size(opts.r#type) {
1029                Some(CVal {
1030                    num_ref: Some(crate::xml::chart::CNumRef {
1031                        f: if s.sizes.is_empty() {
1032                            s.values.clone()
1033                        } else {
1034                            s.sizes.clone()
1035                        },
1036                        num_cache: Some(crate::xml::chart::CNumCache::default()),
1037                    }),
1038                })
1039            } else {
1040                None
1041            },
1042            bubble_3d: if opts.r#type == ChartType::BUBBLE_3D {
1043                Some(AttrValBool { val: Some(true) })
1044            } else {
1045                None
1046            },
1047            ..Default::default()
1048        })
1049        .collect()
1050}
1051
1052fn draw_series_marker(
1053    _idx: usize,
1054    series: &ChartSeries,
1055    opts: &Chart,
1056) -> Option<crate::xml::chart::CMarker> {
1057    if !matches!(opts.r#type.0, 41 | 42 | 47 | 48 | 55 | 56) {
1058        return None;
1059    }
1060    let mut m = crate::xml::chart::CMarker {
1061        symbol: Some(AttrValString {
1062            val: Some(series.marker.symbol.clone()),
1063        }),
1064        size: Some(AttrValInt {
1065            val: Some(if series.marker.size > 0 {
1066                series.marker.size
1067            } else {
1068                5
1069            }),
1070        }),
1071        ..Default::default()
1072    };
1073    if series.marker.symbol.is_empty() {
1074        let default_symbol = match opts.r#type.0 {
1075            48 => "circle",
1076            55 => "dot",
1077            56 => "none",
1078            _ => "auto",
1079        };
1080        m.symbol = Some(AttrValString {
1081            val: Some(default_symbol.to_string()),
1082        });
1083    }
1084    Some(m)
1085}
1086
1087fn draw_chart_series_sp_pr(idx: usize, series: &ChartSeries, opts: &Chart) -> Option<CSpPr> {
1088    let accent = format!("accent{}", (opts.order as usize + idx) % 6 + 1);
1089    let mut sp_pr = CSpPr {
1090        solid_fill: Some(crate::xml::drawing::ASolidFill {
1091            scheme_clr: Some(crate::xml::drawing::ASchemeClr {
1092                val: Some(accent),
1093                ..Default::default()
1094            }),
1095            ..Default::default()
1096        }),
1097        ..Default::default()
1098    };
1099    apply_fill(&mut sp_pr, &series.fill);
1100    let solid_ln = crate::xml::drawing::ALn {
1101        w: Some(pt_to_emus(series.line.width)),
1102        cap: Some("rnd".to_string()),
1103        solid_fill: sp_pr.solid_fill.clone(),
1104        ..Default::default()
1105    };
1106    let mut solid_ln = solid_ln;
1107    if series.line.dash != crate::xml::drawing::LineDashType::UNSET {
1108        if let Some(dash) = line_dash_type(series.line.dash) {
1109            solid_ln.prst_dash = Some(AttrValString {
1110                val: Some(dash.to_string()),
1111            });
1112        }
1113    }
1114    let no_ln = crate::xml::drawing::ALn {
1115        no_fill: Some(AttrValString::default()),
1116        ..Default::default()
1117    };
1118    let solid_sp_pr = CSpPr {
1119        ln: Some(solid_ln),
1120        ..Default::default()
1121    };
1122    let no_sp_pr = CSpPr {
1123        ln: Some(no_ln),
1124        ..Default::default()
1125    };
1126    let line_sp_pr = match opts.r#type.0 {
1127        41 => match series.line.r#type {
1128            crate::xml::drawing::LineType::UNSET
1129            | crate::xml::drawing::LineType::SOLID
1130            | crate::xml::drawing::LineType::AUTOMATIC => Some(solid_sp_pr),
1131            crate::xml::drawing::LineType::NONE => Some(no_sp_pr),
1132            crate::xml::drawing::LineType(_) => None,
1133        },
1134        48 | 55 | 56 => match series.line.r#type {
1135            crate::xml::drawing::LineType::SOLID => Some(solid_sp_pr),
1136            crate::xml::drawing::LineType::NONE => Some(no_sp_pr),
1137            crate::xml::drawing::LineType(_) => Some(no_sp_pr),
1138        },
1139        _ => None,
1140    };
1141    if let Some(sp) = line_sp_pr {
1142        return Some(sp);
1143    }
1144    if sp_pr
1145        .solid_fill
1146        .as_ref()
1147        .and_then(|f| f.srgb_clr.as_ref())
1148        .is_some()
1149        || sp_pr.no_fill.is_some()
1150    {
1151        return Some(sp_pr);
1152    }
1153    None
1154}
1155
1156fn line_dash_type(dash: crate::xml::drawing::LineDashType) -> Option<&'static str> {
1157    match dash.0 {
1158        1 => Some("solid"),
1159        2 => Some("dot"),
1160        3 => Some("dash"),
1161        4 => Some("lgDash"),
1162        5 => Some("dashDot"),
1163        6 => Some("lgDashDot"),
1164        7 => Some("lgDashDotDot"),
1165        8 => Some("sysDash"),
1166        9 => Some("sysDot"),
1167        10 => Some("sysDashDot"),
1168        11 => Some("sysDashDotDot"),
1169        _ => None,
1170    }
1171}
1172
1173fn draw_chart_series_d_pt(
1174    idx: usize,
1175    series: &ChartSeries,
1176    opts: &Chart,
1177) -> Vec<crate::xml::chart::CDPt> {
1178    if !matches!(opts.r#type.0, 40 | 43 | 44) && series.data_point.is_empty() {
1179        return Vec::new();
1180    }
1181    if !series.data_point.is_empty() {
1182        return series
1183            .data_point
1184            .iter()
1185            .map(|dp| {
1186                let mut sp_pr = CSpPr::default();
1187                apply_fill(&mut sp_pr, &dp.fill);
1188                crate::xml::chart::CDPt {
1189                    idx: Some(AttrValInt {
1190                        val: Some(dp.index),
1191                    }),
1192                    bubble_3d: Some(AttrValBool { val: Some(false) }),
1193                    sp_pr: Some(sp_pr),
1194                }
1195            })
1196            .collect();
1197    }
1198    let accent = format!("accent{}", idx + 1);
1199    vec![crate::xml::chart::CDPt {
1200        idx: Some(AttrValInt {
1201            val: Some(idx as i64),
1202        }),
1203        bubble_3d: Some(AttrValBool { val: Some(false) }),
1204        sp_pr: Some(CSpPr {
1205            solid_fill: Some(crate::xml::drawing::ASolidFill {
1206                scheme_clr: Some(crate::xml::drawing::ASchemeClr {
1207                    val: Some(accent),
1208                    ..Default::default()
1209                }),
1210                ..Default::default()
1211            }),
1212            ln: Some(crate::xml::drawing::ALn {
1213                w: Some(25400),
1214                cap: Some("rnd".to_string()),
1215                solid_fill: Some(crate::xml::drawing::ASolidFill {
1216                    scheme_clr: Some(crate::xml::drawing::ASchemeClr {
1217                        val: Some("lt1".to_string()),
1218                        ..Default::default()
1219                    }),
1220                    ..Default::default()
1221                }),
1222                ..Default::default()
1223            }),
1224            sp_3d: Some(crate::xml::chart::ASp3D {
1225                contour_w: 25400,
1226                contour_clr: Some(crate::xml::chart::AContourClr {
1227                    scheme_clr: Some(crate::xml::drawing::ASchemeClr {
1228                        val: Some("lt1".to_string()),
1229                        ..Default::default()
1230                    }),
1231                }),
1232            }),
1233            ..Default::default()
1234        }),
1235    }]
1236}
1237
1238fn draw_chart_series_d_lbls(
1239    idx: usize,
1240    series: &ChartSeries,
1241    opts: &Chart,
1242) -> Option<crate::xml::chart::CDLbls> {
1243    if matches!(opts.r#type.0, 48 | 49 | 50 | 51 | 52) {
1244        return None;
1245    }
1246    let mut d_lbls = draw_chart_d_lbls(opts);
1247    if let Some(types) = Some(supported_chart_data_labels_position(opts.r#type))
1248        .filter(|_| series.data_label_position != ChartDataLabelPositionType::UNSET)
1249    {
1250        if types.contains(&series.data_label_position) {
1251            d_lbls.d_lbl_pos = Some(AttrValString {
1252                val: Some(chart_data_labels_position_types(series.data_label_position).to_string()),
1253            });
1254        }
1255    }
1256    d_lbls.sp_pr = {
1257        let mut sp_pr = d_lbls.sp_pr.unwrap_or_default();
1258        apply_fill(&mut sp_pr, &series.data_label.fill);
1259        Some(sp_pr)
1260    };
1261    let mut tx_pr = crate::xml::chart::CTxPr {
1262        body_pr: Some(crate::xml::drawing::ABodyPr::default()),
1263        p: Some(crate::xml::drawing::AP {
1264            p_pr: Some(crate::xml::drawing::APPr {
1265                def_r_pr: crate::xml::drawing::ARPr::default(),
1266            }),
1267            ..Default::default()
1268        }),
1269        ..Default::default()
1270    };
1271    if let Some(p) = tx_pr.p.as_mut() {
1272        if let Some(p_pr) = p.p_pr.as_mut() {
1273            draw_chart_font(&series.data_label.font, &mut p_pr.def_r_pr);
1274        }
1275    }
1276    d_lbls.tx_pr = Some(tx_pr);
1277    Some(d_lbls)
1278}
1279
1280fn draw_chart_font(font: &crate::styles::Font, rpr: &mut crate::xml::drawing::ARPr) {
1281    rpr.b = font.bold.unwrap_or(false);
1282    rpr.i = font.italic.unwrap_or(false);
1283    if let Some(u) = &font.underline {
1284        if let Some(idx) = SUPPORTED_DRAWING_UNDERLINE_TYPES
1285            .iter()
1286            .position(|&t| t.eq_ignore_ascii_case(u))
1287        {
1288            rpr.u = Some(SUPPORTED_DRAWING_UNDERLINE_TYPES[idx].to_string());
1289        }
1290    }
1291    if let Some(color) = &font.color {
1292        if !color.is_empty() {
1293            if rpr.solid_fill.is_none() {
1294                rpr.solid_fill = Some(crate::xml::drawing::ASolidFill::default());
1295            }
1296            let fill = rpr.solid_fill.as_mut().unwrap();
1297            fill.scheme_clr = None;
1298            fill.srgb_clr = Some(crate::xml::drawing::ASrgbClr {
1299                val: Some(color.trim_start_matches('#').to_uppercase()),
1300                ..Default::default()
1301            });
1302        }
1303    }
1304    if let Some(family) = &font.name {
1305        if !family.is_empty() {
1306            if rpr.latin.is_none() {
1307                rpr.latin = Some(crate::xml::drawing::XlsxCTTextFont::default());
1308            }
1309            if rpr.ea.is_none() {
1310                rpr.ea = Some(crate::xml::drawing::XlsxCTTextFont::default());
1311            }
1312            if rpr.cs.is_none() {
1313                rpr.cs = Some(crate::xml::drawing::XlsxCTTextFont::default());
1314            }
1315            rpr.latin.as_mut().unwrap().typeface = family.clone();
1316            rpr.ea.as_mut().unwrap().typeface = family.clone();
1317            rpr.cs.as_mut().unwrap().typeface = family.clone();
1318        }
1319    }
1320    if let Some(size) = font.size {
1321        if size > 0.0 {
1322            rpr.sz = Some(size * 100.0);
1323        }
1324    }
1325    if font.strike.unwrap_or(false) {
1326        rpr.strike = Some("sngStrike".to_string());
1327    }
1328}
1329
1330fn draw_plot_area_tx_pr(axis: &ChartAxis) -> crate::xml::chart::CTxPr {
1331    let mut body_pr = crate::xml::drawing::ABodyPr {
1332        rot: -60000000,
1333        spc_first_last_para: true,
1334        vert: Some("horz".to_string()),
1335        vert_overflow: Some("ellipsis".to_string()),
1336        wrap: Some("square".to_string()),
1337        anchor: Some("ctr".to_string()),
1338        anchor_ctr: true,
1339        ..Default::default()
1340    };
1341    let mut def_r_pr = crate::xml::drawing::ARPr {
1342        sz: Some(900.0),
1343        kern: 1200,
1344        solid_fill: Some(crate::xml::drawing::ASolidFill {
1345            scheme_clr: Some(crate::xml::drawing::ASchemeClr {
1346                val: Some("tx1".to_string()),
1347                lum_mod: Some(AttrValInt { val: Some(15000) }),
1348                lum_off: Some(AttrValInt { val: Some(85000) }),
1349                ..Default::default()
1350            }),
1351            ..Default::default()
1352        }),
1353        latin: Some(crate::xml::drawing::XlsxCTTextFont {
1354            typeface: "+mn-lt".to_string(),
1355            ..Default::default()
1356        }),
1357        ea: Some(crate::xml::drawing::XlsxCTTextFont {
1358            typeface: "+mn-ea".to_string(),
1359            ..Default::default()
1360        }),
1361        cs: Some(crate::xml::drawing::XlsxCTTextFont {
1362            typeface: "+mn-cs".to_string(),
1363            ..Default::default()
1364        }),
1365        ..Default::default()
1366    };
1367    draw_chart_font(&axis.font, &mut def_r_pr);
1368    if (-90..=90).contains(&axis.alignment.text_rotation) {
1369        body_pr.rot = axis.alignment.text_rotation * 60000;
1370    }
1371    if let Some(idx) = SUPPORTED_DRAWING_TEXT_VERTICAL_TYPE
1372        .iter()
1373        .position(|&t| t.eq_ignore_ascii_case(&axis.alignment.vertical))
1374    {
1375        body_pr.vert = Some(SUPPORTED_DRAWING_TEXT_VERTICAL_TYPE[idx].to_string());
1376    }
1377    crate::xml::chart::CTxPr {
1378        body_pr: Some(body_pr),
1379        p: Some(crate::xml::drawing::AP {
1380            p_pr: Some(crate::xml::drawing::APPr { def_r_pr }),
1381            end_para_r_pr: Some(crate::xml::drawing::AEndParaRPr {
1382                lang: "en-US".to_string(),
1383                ..Default::default()
1384            }),
1385            ..Default::default()
1386        }),
1387        ..Default::default()
1388    }
1389}
1390
1391fn draw_plot_area_sp_pr() -> CSpPr {
1392    CSpPr {
1393        ln: Some(crate::xml::drawing::ALn {
1394            w: Some(9525),
1395            cap: Some("flat".to_string()),
1396            cmpd: Some("sng".to_string()),
1397            algn: Some("ctr".to_string()),
1398            solid_fill: Some(crate::xml::drawing::ASolidFill {
1399                scheme_clr: Some(crate::xml::drawing::ASchemeClr {
1400                    val: Some("tx1".to_string()),
1401                    lum_mod: Some(AttrValInt { val: Some(15000) }),
1402                    lum_off: Some(AttrValInt { val: Some(85000) }),
1403                    ..Default::default()
1404                }),
1405                ..Default::default()
1406            }),
1407            ..Default::default()
1408        }),
1409        ..Default::default()
1410    }
1411}
1412
1413fn draw_plot_area_d_table(opts: &Chart) -> Option<crate::xml::chart::CDTable> {
1414    if matches!(
1415        opts.r#type.0,
1416        0..=5 | 6..=39 | 41 | 42
1417    ) && opts.plot_area.show_data_table
1418    {
1419        Some(crate::xml::chart::CDTable {
1420            show_horz_border: Some(AttrValBool { val: Some(true) }),
1421            show_vert_border: Some(AttrValBool { val: Some(true) }),
1422            show_outline: Some(AttrValBool { val: Some(true) }),
1423            show_keys: Some(AttrValBool {
1424                val: Some(opts.plot_area.show_data_table_keys),
1425            }),
1426            ..Default::default()
1427        })
1428    } else {
1429        None
1430    }
1431}
1432
1433fn draw_title(title: &ChartTitle, vert: &str) -> Option<CTitle> {
1434    if title.paragraph.is_empty() && title.formula.is_empty() {
1435        return None;
1436    }
1437    let mut t = CTitle {
1438        overlay: Some(AttrValBool {
1439            val: Some(title.overlay),
1440        }),
1441        ..Default::default()
1442    };
1443    let default_p_pr = || crate::xml::drawing::APPr {
1444        def_r_pr: crate::xml::drawing::ARPr {
1445            latin: Some(crate::xml::drawing::XlsxCTTextFont {
1446                typeface: "+mn-lt".to_string(),
1447                ..Default::default()
1448            }),
1449            ea: Some(crate::xml::drawing::XlsxCTTextFont {
1450                typeface: "+mn-ea".to_string(),
1451                ..Default::default()
1452            }),
1453            cs: Some(crate::xml::drawing::XlsxCTTextFont {
1454                typeface: "+mn-cs".to_string(),
1455                ..Default::default()
1456            }),
1457            ..Default::default()
1458        },
1459    };
1460    let end_para_r_pr = crate::xml::drawing::AEndParaRPr {
1461        lang: "en-US".to_string(),
1462        ..Default::default()
1463    };
1464    if !title.formula.is_empty() {
1465        t.tx = Some(CTx {
1466            str_ref: Some(CStrRef {
1467                f: title.formula.clone(),
1468                ..Default::default()
1469            }),
1470            ..Default::default()
1471        });
1472        let mut p_pr = default_p_pr();
1473        draw_chart_font(
1474            title
1475                .font
1476                .as_ref()
1477                .unwrap_or(&crate::styles::Font::default()),
1478            &mut p_pr.def_r_pr,
1479        );
1480        t.tx_pr = Some(crate::xml::chart::CTxPr {
1481            body_pr: Some(crate::xml::drawing::ABodyPr {
1482                rot: if vert == "horz" { -5400000 } else { 0 },
1483                vert: Some(vert.to_string()),
1484                ..Default::default()
1485            }),
1486            p: Some(crate::xml::drawing::AP {
1487                p_pr: Some(p_pr),
1488                end_para_r_pr: Some(end_para_r_pr),
1489                ..Default::default()
1490            }),
1491            ..Default::default()
1492        });
1493    } else {
1494        let mut rich = crate::xml::chart::CRich::default();
1495        for run in &title.paragraph {
1496            let mut r = crate::xml::drawing::AR {
1497                t: Some(run.text.clone()),
1498                ..Default::default()
1499            };
1500            if let Some(font) = &run.font {
1501                let mut rpr = crate::xml::drawing::ARPr::default();
1502                draw_chart_font(font, &mut rpr);
1503                r.r_pr = Some(rpr);
1504            }
1505            rich.p.push(crate::xml::drawing::AP {
1506                p_pr: Some(default_p_pr()),
1507                r: Some(r),
1508                end_para_r_pr: Some(end_para_r_pr.clone()),
1509                ..Default::default()
1510            });
1511        }
1512        if !title.paragraph.is_empty() && vert == "horz" {
1513            rich.body_pr = Some(crate::xml::drawing::ABodyPr {
1514                rot: -5400000,
1515                vert: Some(vert.to_string()),
1516                ..Default::default()
1517            });
1518        }
1519        t.tx = Some(CTx {
1520            rich: Some(rich),
1521            ..Default::default()
1522        });
1523    }
1524    if title.offset_x > 0 || title.offset_y > 0 || title.width > 0 || title.height > 0 {
1525        let mut layout = crate::xml::chart::CLayout::default();
1526        let mut manual = crate::xml::chart::CManualLayout::default();
1527        if title.offset_x > 0 {
1528            manual.x_mode = Some(AttrValString {
1529                val: Some("edge".to_string()),
1530            });
1531            manual.x = Some(AttrValFloat {
1532                val: Some(title.offset_x as f64 / 100.0),
1533            });
1534        }
1535        if title.offset_y > 0 {
1536            manual.y_mode = Some(AttrValString {
1537                val: Some("edge".to_string()),
1538            });
1539            manual.y = Some(AttrValFloat {
1540                val: Some(title.offset_y as f64 / 100.0),
1541            });
1542        }
1543        if title.width > 0 {
1544            manual.w_mode = Some(AttrValString {
1545                val: Some("edge".to_string()),
1546            });
1547            manual.w = Some(AttrValFloat {
1548                val: Some(title.width as f64 / 100.0),
1549            });
1550        }
1551        if title.height > 0 {
1552            manual.h_mode = Some(AttrValString {
1553                val: Some("edge".to_string()),
1554            });
1555            manual.h = Some(AttrValFloat {
1556                val: Some(title.height as f64 / 100.0),
1557            });
1558        }
1559        layout.manual_layout = Some(manual);
1560        t.layout = Some(layout);
1561    }
1562    let mut sp_pr = CSpPr::default();
1563    apply_fill(&mut sp_pr, &title.fill);
1564    if let Some(ln) = draw_chart_ln(&title.border) {
1565        sp_pr.ln = Some(ln);
1566    }
1567    t.sp_pr = Some(sp_pr);
1568    Some(t)
1569}
1570
1571fn draw_legend(opts: &Chart) -> Option<CLegend> {
1572    if opts.legend.position == "none" {
1573        return None;
1574    }
1575    let pos = CHART_LEGEND_POSITION
1576        .iter()
1577        .find(|&&(k, _)| k == opts.legend.position)
1578        .map(|&(_, v)| v)
1579        .unwrap_or("b");
1580    let mut legend = CLegend {
1581        legend_pos: Some(AttrValString {
1582            val: Some(pos.to_string()),
1583        }),
1584        overlay: Some(AttrValBool { val: Some(false) }),
1585        ..Default::default()
1586    };
1587    if let Some(font) = &opts.legend.font {
1588        let mut def_r_pr = crate::xml::drawing::ARPr::default();
1589        draw_chart_font(font, &mut def_r_pr);
1590        legend.tx_pr = Some(crate::xml::chart::CTxPr {
1591            p: Some(crate::xml::drawing::AP {
1592                p_pr: Some(crate::xml::drawing::APPr { def_r_pr }),
1593                ..Default::default()
1594            }),
1595            ..Default::default()
1596        });
1597    }
1598    for (k, s) in opts.series.iter().enumerate() {
1599        if let Some(font) = &s.legend.font {
1600            let mut def_r_pr = crate::xml::drawing::ARPr::default();
1601            draw_chart_font(font, &mut def_r_pr);
1602            legend.legend_entry.push(crate::xml::chart::CLegendEntry {
1603                idx: Some(AttrValInt {
1604                    val: Some(k as i64 + opts.order),
1605                }),
1606                tx_pr: Some(crate::xml::chart::CTxPr {
1607                    p: Some(crate::xml::drawing::AP {
1608                        p_pr: Some(crate::xml::drawing::APPr { def_r_pr }),
1609                        ..Default::default()
1610                    }),
1611                    ..Default::default()
1612                }),
1613                ..Default::default()
1614            });
1615        }
1616    }
1617    Some(legend)
1618}
1619
1620fn draw_chart_d_lbls(opts: &Chart) -> crate::xml::chart::CDLbls {
1621    crate::xml::chart::CDLbls {
1622        show_legend_key: Some(AttrValBool {
1623            val: Some(opts.legend.show_legend_key),
1624        }),
1625        show_val: Some(AttrValBool {
1626            val: Some(opts.plot_area.show_val),
1627        }),
1628        show_cat_name: Some(AttrValBool {
1629            val: Some(opts.plot_area.show_cat_name),
1630        }),
1631        show_ser_name: Some(AttrValBool {
1632            val: Some(opts.plot_area.show_ser_name),
1633        }),
1634        show_percent: Some(AttrValBool {
1635            val: Some(opts.plot_area.show_percent),
1636        }),
1637        show_bubble_size: Some(AttrValBool {
1638            val: Some(opts.plot_area.show_bubble_size),
1639        }),
1640        show_leader_lines: Some(AttrValBool {
1641            val: Some(opts.plot_area.show_leader_lines),
1642        }),
1643        num_fmt: draw_chart_num_fmt(&opts.plot_area.num_fmt),
1644        ..Default::default()
1645    }
1646}
1647
1648fn draw_chart_num_fmt(fmt: &ChartNumFmt) -> Option<CNumFmt> {
1649    if fmt.custom_num_fmt.is_empty() && !fmt.source_linked {
1650        return None;
1651    }
1652    Some(CNumFmt {
1653        format_code: fmt.custom_num_fmt.clone(),
1654        source_linked: fmt.source_linked,
1655    })
1656}
1657
1658fn tick_lbl_pos(pos: ChartTickLabelPositionType) -> String {
1659    match pos.0 {
1660        1 => "high",
1661        2 => "low",
1662        3 => "none",
1663        _ => "nextTo",
1664    }
1665    .to_string()
1666}
1667
1668fn draw_plot_area_cat_ax(_pa: &CPlotArea, opts: &Chart) -> Vec<CAxs> {
1669    let (ax_id, cross_ax) = if opts.order > 0 && opts.y_axis.secondary {
1670        (100000003, 100000004)
1671    } else {
1672        (100000000, 100000001)
1673    };
1674    let mut ax = CAxs {
1675        ax_id: Some(AttrValInt { val: Some(ax_id) }),
1676        scaling: Some(CScaling {
1677            orientation: Some(AttrValString {
1678                val: Some(if opts.x_axis.reverse_order {
1679                    "maxMin".to_string()
1680                } else {
1681                    "minMax".to_string()
1682                }),
1683            }),
1684            max: opts.x_axis.maximum.map(|v| AttrValFloat { val: Some(v) }),
1685            min: opts.x_axis.minimum.map(|v| AttrValFloat { val: Some(v) }),
1686            ..Default::default()
1687        }),
1688        delete: Some(AttrValBool {
1689            val: Some(opts.x_axis.none),
1690        }),
1691        ax_pos: Some(AttrValString {
1692            val: Some(if opts.x_axis.reverse_order {
1693                "t".to_string()
1694            } else {
1695                "b".to_string()
1696            }),
1697        }),
1698        num_fmt: Some(CNumFmt {
1699            format_code: "General".to_string(),
1700            source_linked: false,
1701        }),
1702        major_tick_mark: Some(AttrValString {
1703            val: Some("none".to_string()),
1704        }),
1705        minor_tick_mark: Some(AttrValString {
1706            val: Some("none".to_string()),
1707        }),
1708        tick_lbl_pos: Some(AttrValString {
1709            val: Some(tick_lbl_pos(opts.x_axis.tick_label_position)),
1710        }),
1711        cross_ax: Some(AttrValInt {
1712            val: Some(cross_ax),
1713        }),
1714        crosses: Some(AttrValString {
1715            val: Some("autoZero".to_string()),
1716        }),
1717        auto: Some(AttrValBool { val: Some(true) }),
1718        lbl_algn: Some(AttrValString {
1719            val: Some("ctr".to_string()),
1720        }),
1721        lbl_offset: Some(AttrValInt { val: Some(100) }),
1722        no_multi_lvl_lbl: Some(AttrValBool { val: Some(false) }),
1723        title: draw_title(&opts.x_axis.title, "horz"),
1724        sp_pr: Some(draw_plot_area_sp_pr()),
1725        tx_pr: Some(draw_plot_area_tx_pr(&opts.x_axis)),
1726        ..Default::default()
1727    };
1728    if let Some(num_fmt) = draw_chart_num_fmt(&opts.x_axis.num_fmt) {
1729        ax.num_fmt = Some(num_fmt);
1730    }
1731    if opts.x_axis.major_grid_lines {
1732        ax.major_gridlines = Some(crate::xml::chart::CLines {
1733            sp_pr: Some(draw_plot_area_sp_pr()),
1734        });
1735    }
1736    if opts.x_axis.minor_grid_lines {
1737        ax.minor_gridlines = Some(crate::xml::chart::CLines {
1738            sp_pr: Some(draw_plot_area_sp_pr()),
1739        });
1740    }
1741    if opts.x_axis.tick_label_skip != 0 {
1742        ax.tick_lbl_skip = Some(AttrValInt {
1743            val: Some(opts.x_axis.tick_label_skip),
1744        });
1745    }
1746    if opts.order > 0 && opts.y_axis.secondary {
1747        ax.delete = Some(AttrValBool { val: Some(true) });
1748        ax.crosses = None;
1749    }
1750    vec![ax]
1751}
1752
1753fn draw_plot_area_val_ax(_pa: &CPlotArea, opts: &Chart) -> Vec<CAxs> {
1754    let (ax_id, cross_ax) = if opts.order > 0 && opts.y_axis.secondary {
1755        (100000004, 100000003)
1756    } else {
1757        (100000001, 100000000)
1758    };
1759    let log_base = if opts.y_axis.log_base >= 2.0 && opts.y_axis.log_base <= 1000.0 {
1760        Some(AttrValFloat {
1761            val: Some(opts.y_axis.log_base),
1762        })
1763    } else {
1764        None
1765    };
1766    let mut ax = CAxs {
1767        ax_id: Some(AttrValInt { val: Some(ax_id) }),
1768        scaling: Some(CScaling {
1769            log_base,
1770            orientation: Some(AttrValString {
1771                val: Some(if opts.y_axis.reverse_order {
1772                    "maxMin".to_string()
1773                } else {
1774                    "minMax".to_string()
1775                }),
1776            }),
1777            max: opts.y_axis.maximum.map(|v| AttrValFloat { val: Some(v) }),
1778            min: opts.y_axis.minimum.map(|v| AttrValFloat { val: Some(v) }),
1779            ..Default::default()
1780        }),
1781        delete: Some(AttrValBool {
1782            val: Some(opts.y_axis.none),
1783        }),
1784        ax_pos: Some(AttrValString {
1785            val: Some(if opts.y_axis.reverse_order {
1786                "r".to_string()
1787            } else {
1788                "l".to_string()
1789            }),
1790        }),
1791        num_fmt: Some(CNumFmt {
1792            format_code: chart_val_ax_num_fmt(opts.r#type).to_string(),
1793            source_linked: false,
1794        }),
1795        major_tick_mark: Some(AttrValString {
1796            val: Some("none".to_string()),
1797        }),
1798        minor_tick_mark: Some(AttrValString {
1799            val: Some("none".to_string()),
1800        }),
1801        tick_lbl_pos: Some(AttrValString {
1802            val: Some(tick_lbl_pos(opts.y_axis.tick_label_position)),
1803        }),
1804        cross_ax: Some(AttrValInt {
1805            val: Some(cross_ax),
1806        }),
1807        crosses: Some(AttrValString {
1808            val: Some(if opts.order > 0 && opts.y_axis.secondary {
1809                "max".to_string()
1810            } else {
1811                "autoZero".to_string()
1812            }),
1813        }),
1814        cross_between: Some(AttrValString {
1815            val: Some(chart_val_ax_cross_between(opts.r#type).to_string()),
1816        }),
1817        title: draw_title(&opts.y_axis.title, ""),
1818        sp_pr: Some(draw_plot_area_sp_pr()),
1819        tx_pr: Some(draw_plot_area_tx_pr(&opts.y_axis)),
1820        ..Default::default()
1821    };
1822    if let Some(num_fmt) = draw_chart_num_fmt(&opts.y_axis.num_fmt) {
1823        ax.num_fmt = Some(num_fmt);
1824    }
1825    if opts.y_axis.major_grid_lines {
1826        ax.major_gridlines = Some(crate::xml::chart::CLines {
1827            sp_pr: Some(draw_plot_area_sp_pr()),
1828        });
1829    }
1830    if opts.y_axis.minor_grid_lines {
1831        ax.minor_gridlines = Some(crate::xml::chart::CLines {
1832            sp_pr: Some(draw_plot_area_sp_pr()),
1833        });
1834    }
1835    if opts.r#type == ChartType::CONTOUR || opts.r#type == ChartType::WIREFRAME_CONTOUR {
1836        ax.tick_lbl_pos = Some(AttrValString {
1837            val: Some("none".to_string()),
1838        });
1839    }
1840    if opts.y_axis.major_unit != 0.0 {
1841        ax.major_unit = Some(AttrValFloat {
1842            val: Some(opts.y_axis.major_unit),
1843        });
1844    }
1845    vec![ax]
1846}
1847
1848fn draw_plot_area_ser_ax(opts: &Chart) -> Vec<CAxs> {
1849    let max_val = opts.y_axis.maximum.map(|v| AttrValFloat { val: Some(v) });
1850    let min_val = opts.y_axis.minimum.map(|v| AttrValFloat { val: Some(v) });
1851    vec![CAxs {
1852        ax_id: Some(AttrValInt {
1853            val: Some(100000005),
1854        }),
1855        scaling: Some(CScaling {
1856            orientation: Some(AttrValString {
1857                val: Some(if opts.y_axis.reverse_order {
1858                    "maxMin".to_string()
1859                } else {
1860                    "minMax".to_string()
1861                }),
1862            }),
1863            max: max_val,
1864            min: min_val,
1865            ..Default::default()
1866        }),
1867        delete: Some(AttrValBool {
1868            val: Some(opts.y_axis.none),
1869        }),
1870        ax_pos: Some(AttrValString {
1871            val: Some(if opts.x_axis.reverse_order {
1872                "t".to_string()
1873            } else {
1874                "b".to_string()
1875            }),
1876        }),
1877        tick_lbl_pos: Some(AttrValString {
1878            val: Some(tick_lbl_pos(opts.y_axis.tick_label_position)),
1879        }),
1880        cross_ax: Some(AttrValInt {
1881            val: Some(100000001),
1882        }),
1883        sp_pr: Some(draw_plot_area_sp_pr()),
1884        tx_pr: Some(draw_plot_area_tx_pr(&ChartAxis::default())),
1885        ..Default::default()
1886    }]
1887}
1888
1889pub(crate) fn draw_chart_ln(
1890    opts: &crate::xml::drawing::LineOptions,
1891) -> Option<crate::xml::drawing::ALn> {
1892    match opts.r#type {
1893        crate::xml::drawing::LineType::NONE => Some(crate::xml::drawing::ALn {
1894            no_fill: Some(AttrValString::default()),
1895            ..Default::default()
1896        }),
1897        crate::xml::drawing::LineType::SOLID => Some(crate::xml::drawing::ALn {
1898            w: Some(pt_to_emus(opts.width)),
1899            cap: Some("flat".to_string()),
1900            cmpd: Some("sng".to_string()),
1901            algn: Some("ctr".to_string()),
1902            solid_fill: apply_fill_to_solid(&opts.fill),
1903            ..Default::default()
1904        }),
1905        _ => None,
1906    }
1907}
1908
1909fn apply_fill(sp_pr: &mut CSpPr, fill: &crate::xml::drawing::Fill) {
1910    if fill.r#type == "pattern" && fill.pattern == 1 && fill.color.len() == 1 {
1911        let color = fill.color[0].trim_start_matches('#').to_uppercase();
1912        sp_pr.solid_fill = Some(crate::xml::drawing::ASolidFill {
1913            srgb_clr: Some(crate::xml::drawing::ASrgbClr {
1914                val: Some(color),
1915                ..Default::default()
1916            }),
1917            ..Default::default()
1918        });
1919    }
1920}
1921
1922fn apply_fill_to_solid(
1923    fill: &crate::xml::drawing::Fill,
1924) -> Option<crate::xml::drawing::ASolidFill> {
1925    if fill.r#type == "pattern" && fill.pattern == 1 && fill.color.len() == 1 {
1926        let color = fill.color[0].trim_start_matches('#').to_uppercase();
1927        return Some(crate::xml::drawing::ASolidFill {
1928            srgb_clr: Some(crate::xml::drawing::ASrgbClr {
1929                val: Some(color),
1930                ..Default::default()
1931            }),
1932            ..Default::default()
1933        });
1934    }
1935    None
1936}
1937
1938fn pt_to_emus(pt: f64) -> i64 {
1939    if pt < 0.25 || pt > 999.0 {
1940        25400
1941    } else {
1942        (pt * 12700.0) as i64
1943    }
1944}
1945
1946// ------------------------------------------------------------------
1947// Drawing helpers
1948// ------------------------------------------------------------------
1949
1950impl File {
1951    pub(crate) fn prepare_drawing(
1952        &self,
1953        sheet: &str,
1954        drawing_id: i32,
1955        drawing_xml: &str,
1956    ) -> Result<(i32, String)> {
1957        let ws = self.work_sheet_reader(sheet)?;
1958        if let Some(drawing) = &ws.drawing {
1959            let target = self
1960                .get_sheet_relationships_target_by_id(sheet, drawing.rid.as_deref().unwrap_or(""));
1961            let target = target.replace("/xl/drawings/", "../drawings/");
1962            let id_str = target
1963                .trim_start_matches("../drawings/drawing")
1964                .trim_end_matches(".xml");
1965            let id = id_str.parse::<i32>().unwrap_or(drawing_id);
1966            let xml = target.replace("..", "xl");
1967            return Ok((id, xml));
1968        }
1969        let sheet_xml_path = self.get_sheet_xml_path(sheet).unwrap_or_default();
1970        let sheet_rels = format!(
1971            "xl/worksheets/_rels/{}.rels",
1972            sheet_xml_path.trim_start_matches("xl/worksheets/")
1973        );
1974        let target = format!("../drawings/drawing{drawing_id}.xml");
1975        let r_id = self.add_rels(&sheet_rels, SOURCE_RELATIONSHIP_DRAWING_ML, &target, "");
1976        self.add_sheet_drawing(sheet, r_id)?;
1977        Ok((drawing_id, drawing_xml.to_string()))
1978    }
1979
1980    pub(crate) fn drawing_parser(&self, path: &str) -> Result<(XlsxWsDr, i64)> {
1981        if let Some(d) = self.drawings.get(path) {
1982            let c_nv_pr_id = (d.one_cell_anchor.len() + d.two_cell_anchor.len() + 2) as i64;
1983            return Ok((d.clone(), c_nv_pr_id));
1984        }
1985        let mut content = XlsxWsDr {
1986            ns: Some(NAMESPACE_DRAWING_ML_SPREADSHEET.to_string()),
1987            xmlns_xdr: Some(NAMESPACE_DRAWING_ML_SPREADSHEET.to_string()),
1988            xmlns_a: Some(NAMESPACE_DRAWING_ML_MAIN.to_string()),
1989            xmlns_r: Some(SOURCE_RELATIONSHIP.to_string()),
1990            ..Default::default()
1991        };
1992        if let Some(bytes) = self.pkg.get(path) {
1993            let data = bytes.value().clone();
1994            let decoded: DecodeWsDr =
1995                xml_from_reader::<_, DecodeWsDr>(data.as_slice()).unwrap_or_default();
1996            content.xmlns_r = Some(decoded.r);
1997            for a in decoded.one_cell_anchor {
1998                content.one_cell_anchor.push(decode_cell_anchor(a));
1999            }
2000            for a in decoded.two_cell_anchor {
2001                content.two_cell_anchor.push(decode_cell_anchor(a));
2002            }
2003        }
2004        let c_nv_pr_id = (content.one_cell_anchor.len() + content.two_cell_anchor.len() + 2) as i64;
2005        self.drawings.insert(path.to_string(), content.clone());
2006        Ok((content, c_nv_pr_id))
2007    }
2008}
2009
2010fn decode_cell_anchor(a: DecodeCellAnchor) -> XdrCellAnchor {
2011    let graphic_frame = a.graphic_frame;
2012    XdrCellAnchor {
2013        edit_as: a.edit_as,
2014        from: a.from.map(decode_from),
2015        to: a.to.map(decode_to),
2016        ext: a.ext.map(|e| XlsxPositiveSize2D { cx: e.cx, cy: e.cy }),
2017        pic: a.pic.map(decode_pic),
2018        graphic_frame,
2019        client_data: a.client_data.map(decode_client_data),
2020        ..Default::default()
2021    }
2022}
2023
2024fn decode_from(f: DecodeFrom) -> XlsxFrom {
2025    XlsxFrom {
2026        col: f.col as i64,
2027        col_off: f.col_off,
2028        row: f.row as i64,
2029        row_off: f.row_off,
2030    }
2031}
2032
2033fn decode_to(t: DecodeTo) -> XlsxTo {
2034    XlsxTo {
2035        col: t.col as i64,
2036        col_off: t.col_off,
2037        row: t.row as i64,
2038        row_off: t.row_off,
2039    }
2040}
2041
2042fn decode_pic(p: DecodePic) -> XlsxPic {
2043    XlsxPic {
2044        nv_pic_pr: XlsxNvPicPr {
2045            c_nv_pr: XlsxCNvPr {
2046                id: p.nv_pic_pr.c_nv_pr.id as i64,
2047                name: p.nv_pic_pr.c_nv_pr.name,
2048                descr: p.nv_pic_pr.c_nv_pr.descr,
2049                title: p.nv_pic_pr.c_nv_pr.title,
2050                hlink_click: p.nv_pic_pr.c_nv_pr.hlink_click.map(|h| XlsxHlinkClick {
2051                    xmlns_r: h.id.as_ref().map(|_| SOURCE_RELATIONSHIP.to_string()),
2052                    r_id: h.id,
2053                    invalid_url: h.invalid_url,
2054                    action: h.action,
2055                    tgt_frame: h.tgt_frame,
2056                    tooltip: h.tooltip,
2057                    history: Some(h.history),
2058                    highlight_click: Some(h.highlight_click),
2059                    end_snd: Some(h.end_snd),
2060                }),
2061            },
2062            c_nv_pic_pr: XlsxCNvPicPr {
2063                pic_locks: XlsxPicLocks {
2064                    no_change_aspect: p.nv_pic_pr.c_nv_pic_pr.pic_locks.no_change_aspect,
2065                    ..Default::default()
2066                },
2067            },
2068        },
2069        blip_fill: XlsxBlipFill {
2070            blip: XlsxBlip {
2071                embed: p.blip_fill.blip.embed,
2072                cstate: p.blip_fill.blip.cstate,
2073                xmlns_r: SOURCE_RELATIONSHIP.to_string(),
2074                ..Default::default()
2075            },
2076            stretch: XlsxStretch {
2077                fill_rect: p.blip_fill.stretch.fill_rect,
2078            },
2079        },
2080        sp_pr: XlsxSpPr {
2081            xfrm: XlsxXfrm {
2082                off: XlsxOff {
2083                    x: p.sp_pr.xfrm.off.x,
2084                    y: p.sp_pr.xfrm.off.y,
2085                },
2086                ext: XlsxPositiveSize2D {
2087                    cx: p.sp_pr.xfrm.ext.cx,
2088                    cy: p.sp_pr.xfrm.ext.cy,
2089                },
2090            },
2091            prst_geom: XlsxPrstGeom {
2092                prst: p.sp_pr.prst_geom.prst,
2093            },
2094            ..Default::default()
2095        },
2096    }
2097}
2098
2099fn decode_client_data(c: DecodeClientData) -> XdrClientData {
2100    XdrClientData {
2101        f_locks_with_sheet: c.f_locks_with_sheet,
2102        f_prints_with_sheet: c.f_prints_with_sheet,
2103    }
2104}
2105
2106impl File {
2107    fn add_drawing_chart(
2108        &self,
2109        sheet: &str,
2110        drawing_xml: &str,
2111        cell: &str,
2112        width: i32,
2113        height: i32,
2114        r_id: i32,
2115        opts: &GraphicOptions,
2116    ) -> Result<()> {
2117        let (col, row) = cell_name_to_coordinates(cell)?;
2118        let width = (width as f64 * opts.scale_x) as i32;
2119        let height = (height as f64 * opts.scale_y) as i32;
2120        let (col_start, row_start, col_end, row_end, x1, y1, x2, y2) =
2121            self.position_object_pixels(sheet, col, row, width, height, opts)?;
2122        let (mut content, c_nv_pr_id) = self.drawing_parser(drawing_xml)?;
2123
2124        let mut anchor = XdrCellAnchor {
2125            edit_as: if opts.positioning.is_empty() {
2126                None
2127            } else {
2128                Some(opts.positioning.clone())
2129            },
2130            from: Some(XlsxFrom {
2131                col: col_start as i64,
2132                col_off: x1 as i64 * DEFAULT_EMU as i64,
2133                row: row_start as i64,
2134                row_off: y1 as i64 * DEFAULT_EMU as i64,
2135            }),
2136            to: Some(XlsxTo {
2137                col: col_end as i64,
2138                col_off: x2 as i64 * DEFAULT_EMU as i64,
2139                row: row_end as i64,
2140                row_off: y2 as i64 * DEFAULT_EMU as i64,
2141            }),
2142            ..Default::default()
2143        };
2144
2145        let graphic_frame = XlsxGraphicFrame {
2146            macro_name: String::new(),
2147            nv_graphic_frame_pr: XlsxNvGraphicFramePr {
2148                c_nv_pr: Some(XlsxCNvPr {
2149                    id: c_nv_pr_id,
2150                    name: if opts.name.is_empty() {
2151                        format!("Chart {c_nv_pr_id}")
2152                    } else {
2153                        opts.name.clone()
2154                    },
2155                    descr: opts.alt_text.clone(),
2156                    ..Default::default()
2157                }),
2158                c_nv_graphic_frame_pr: String::new(),
2159            },
2160            xfrm: XlsxXfrm {
2161                off: XlsxOff { x: 0, y: 0 },
2162                ext: XlsxPositiveSize2D { cx: 0, cy: 0 },
2163            },
2164            graphic: Some(XlsxGraphic {
2165                graphic_data: Some(XlsxGraphicData {
2166                    uri: NAMESPACE_DRAWING_ML_CHART.to_string(),
2167                    chart: Some(XlsxChartRef {
2168                        xmlns_c: NAMESPACE_DRAWING_ML_CHART.to_string(),
2169                        r_id: format!("rId{r_id}"),
2170                        xmlns_r: SOURCE_RELATIONSHIP.to_string(),
2171                    }),
2172                    ..Default::default()
2173                }),
2174            }),
2175        };
2176        anchor.graphic_frame = Some(graphic_frame);
2177        anchor.client_data = Some(crate::xml::drawing::XdrClientData {
2178            f_locks_with_sheet: opts.locked.unwrap_or(true),
2179            f_prints_with_sheet: opts.print_object.unwrap_or(true),
2180        });
2181        content.two_cell_anchor.push(anchor);
2182        self.drawings.insert(drawing_xml.to_string(), content);
2183        Ok(())
2184    }
2185
2186    fn add_sheet_drawing_chart(
2187        &self,
2188        drawing_xml: &str,
2189        r_id: i32,
2190        opts: &GraphicOptions,
2191    ) -> Result<()> {
2192        let (mut content, c_nv_pr_id) = self.drawing_parser(drawing_xml)?;
2193        let mut anchor = XdrCellAnchor {
2194            edit_as: if opts.positioning.is_empty() {
2195                None
2196            } else {
2197                Some(opts.positioning.clone())
2198            },
2199            pos: Some(XlsxPoint2D { x: 0, y: 0 }),
2200            ext: Some(XlsxPositiveSize2D {
2201                cx: 9280533,
2202                cy: 6051719,
2203            }),
2204            ..Default::default()
2205        };
2206        let graphic_frame = XlsxGraphicFrame {
2207            macro_name: String::new(),
2208            nv_graphic_frame_pr: XlsxNvGraphicFramePr {
2209                c_nv_pr: Some(XlsxCNvPr {
2210                    id: c_nv_pr_id,
2211                    name: format!("Chart {c_nv_pr_id}"),
2212                    ..Default::default()
2213                }),
2214                c_nv_graphic_frame_pr: String::new(),
2215            },
2216            xfrm: XlsxXfrm {
2217                off: XlsxOff { x: 0, y: 0 },
2218                ext: XlsxPositiveSize2D { cx: 0, cy: 0 },
2219            },
2220            graphic: Some(XlsxGraphic {
2221                graphic_data: Some(XlsxGraphicData {
2222                    uri: NAMESPACE_DRAWING_ML_CHART.to_string(),
2223                    chart: Some(XlsxChartRef {
2224                        xmlns_c: NAMESPACE_DRAWING_ML_CHART.to_string(),
2225                        r_id: format!("rId{r_id}"),
2226                        xmlns_r: SOURCE_RELATIONSHIP.to_string(),
2227                    }),
2228                    ..Default::default()
2229                }),
2230            }),
2231        };
2232        anchor.graphic_frame = Some(graphic_frame);
2233        anchor.client_data = Some(crate::xml::drawing::XdrClientData {
2234            f_locks_with_sheet: opts.locked.unwrap_or(true),
2235            f_prints_with_sheet: opts.print_object.unwrap_or(true),
2236        });
2237        content.absolute_anchor.push(anchor);
2238        self.drawings.insert(drawing_xml.to_string(), content);
2239        Ok(())
2240    }
2241
2242    pub(crate) fn delete_drawing(
2243        &self,
2244        col: i32,
2245        row: i32,
2246        drawing_xml: &str,
2247        drawing_type: &str,
2248    ) -> Result<Vec<String>> {
2249        let (mut wsdr, _) = self.drawing_parser(drawing_xml)?;
2250        let mut del_rid = Vec::new();
2251        let mut ref_rid = Vec::new();
2252        let mut r_id_maps: HashMap<String, i32> = HashMap::new();
2253
2254        let matches_anchor = |a: &XdrCellAnchor| -> bool {
2255            match drawing_type {
2256                "Chart" => a.pic.is_none(),
2257                "Pic" => a.pic.is_some(),
2258                _ => true,
2259            }
2260        };
2261
2262        let mut delete_from = |anchors: &mut Vec<XdrCellAnchor>| {
2263            let mut i = 0;
2264            while i < anchors.len() {
2265                let remove = {
2266                    let a = &anchors[i];
2267                    if let Some(from) = &a.from {
2268                        if matches_anchor(a) && from.col == col as i64 && from.row == row as i64 {
2269                            true
2270                        } else {
2271                            false
2272                        }
2273                    } else {
2274                        false
2275                    }
2276                };
2277                if remove {
2278                    let r_id = extract_embed_rid(&anchors[i]);
2279                    *r_id_maps.entry(r_id.clone()).or_insert(0) += 1;
2280                    ref_rid.push(r_id.clone());
2281                    anchors.remove(i);
2282                    *r_id_maps.entry(r_id).or_insert(0) -= 1;
2283                } else {
2284                    i += 1;
2285                }
2286            }
2287        };
2288        delete_from(&mut wsdr.one_cell_anchor);
2289        delete_from(&mut wsdr.two_cell_anchor);
2290        self.drawings.insert(drawing_xml.to_string(), wsdr);
2291        for r_id in ref_rid {
2292            if r_id_maps.get(&r_id).copied().unwrap_or(0) == 0 && !del_rid.contains(&r_id) {
2293                del_rid.push(r_id);
2294            }
2295        }
2296        Ok(del_rid)
2297    }
2298
2299    fn get_charts_from_drawing(&self, drawing_xml: &str, drawing_rels: &str) -> Result<Vec<Chart>> {
2300        let (wsdr, _) = self.drawing_parser(drawing_xml)?;
2301        let mut charts = Vec::new();
2302        for anchor in wsdr
2303            .one_cell_anchor
2304            .iter()
2305            .chain(wsdr.two_cell_anchor.iter())
2306        {
2307            let r_id = anchor
2308                .graphic_frame
2309                .as_ref()
2310                .and_then(|g| g.graphic.as_ref())
2311                .and_then(|g| g.graphic_data.as_ref())
2312                .and_then(|d| d.chart.as_ref())
2313                .map(|c| c.r_id.clone())
2314                .unwrap_or_default();
2315            if r_id.is_empty() {
2316                continue;
2317            }
2318            if let Some(rel) = self.get_drawing_relationship(drawing_rels, &r_id) {
2319                let chart_path = rel
2320                    .target
2321                    .replace("../", "xl/")
2322                    .trim_start_matches('/')
2323                    .to_string();
2324                if let Some(bytes) = self.pkg.get(&chart_path) {
2325                    let xml = String::from_utf8_lossy(bytes.value());
2326                    let mut chart = chart_from_xml(&xml);
2327                    if let Some(ext) = anchor.ext.as_ref() {
2328                        chart.dimension = ChartDimension {
2329                            width: (ext.cx / DEFAULT_EMU as i64) as u64,
2330                            height: (ext.cy / DEFAULT_EMU as i64) as u64,
2331                        };
2332                    }
2333                    charts.push(chart);
2334                }
2335            }
2336        }
2337        Ok(charts)
2338    }
2339
2340    pub(crate) fn get_drawing_relationship(
2341        &self,
2342        rels_path: &str,
2343        r_id: &str,
2344    ) -> Option<crate::xml::workbook::XlsxRelationship> {
2345        if let Ok(Some(rels)) = self.rels_reader(rels_path) {
2346            for rel in &rels.relationships {
2347                if rel.id == r_id {
2348                    return Some(rel.clone());
2349                }
2350            }
2351        }
2352        None
2353    }
2354
2355    pub(crate) fn position_object_pixels(
2356        &self,
2357        sheet: &str,
2358        col: i32,
2359        row: i32,
2360        width: i32,
2361        height: i32,
2362        opts: &GraphicOptions,
2363    ) -> Result<(i32, i32, i32, i32, i32, i32, i32, i32)> {
2364        let col_idx = col - 1;
2365        let row_idx = row - 1;
2366        let mut col_end = col_idx;
2367        let mut row_end = row_idx;
2368        let x1 = opts.offset_x as i32;
2369        let y1 = opts.offset_y as i32;
2370        let mut x2 = width;
2371        let mut y2 = height;
2372        if opts.positioning != "oneCell" {
2373            let col_width = self.get_col_width_pixels(sheet, col)?;
2374            let row_height = self.get_row_height_pixels(sheet, row)?;
2375            let x1 = x1.min(col_width);
2376            let y1 = y1.min(row_height);
2377            x2 += x1;
2378            y2 += y1;
2379            while x2 >= self.get_col_width_pixels(sheet, col_end + 1)? {
2380                col_end += 1;
2381                x2 -= self.get_col_width_pixels(sheet, col_end)?;
2382            }
2383            while y2 >= self.get_row_height_pixels(sheet, row_end + 1)? {
2384                row_end += 1;
2385                y2 -= self.get_row_height_pixels(sheet, row_end)?;
2386            }
2387            return Ok((col_idx, row_idx, col_end, row_end, x1, y1, x2, y2));
2388        }
2389        Ok((col_idx, row_idx, col_end, row_end, x1, y1, x2, y2))
2390    }
2391
2392    pub(crate) fn get_col_width_pixels(&self, sheet: &str, col: i32) -> Result<i32> {
2393        let width = self.get_col_width(sheet, &crate::lib_util::column_number_to_name(col)?)?;
2394        Ok(convert_col_width_to_pixels(width) as i32)
2395    }
2396
2397    pub(crate) fn get_row_height_pixels(&self, sheet: &str, row: i32) -> Result<i32> {
2398        let height = self.get_row_height(sheet, row)?;
2399        Ok(convert_row_height_to_pixels(height) as i32)
2400    }
2401}
2402
2403fn extract_embed_rid(anchor: &XdrCellAnchor) -> String {
2404    if let Some(pic) = &anchor.pic {
2405        pic.blip_fill.blip.embed.clone()
2406    } else {
2407        String::new()
2408    }
2409}
2410
2411fn chart_from_xml(xml: &str) -> Chart {
2412    let mut chart = Chart::default();
2413
2414    if let Ok(space) = xml_from_reader::<_, DecodeChartSpace>(xml.as_bytes()) {
2415        chart.r#type = chart_type_from_chart_space(&space)
2416            .unwrap_or_else(|| detect_chart_type(xml));
2417        chart.title = chart_title_from_chart_space(&space);
2418        chart.legend = legend_from_chart_space(&space);
2419        chart.series = series_from_chart_space(&space);
2420        let (x_axis, y_axis) = axes_from_chart_space(&space);
2421        chart.x_axis = x_axis;
2422        chart.y_axis = y_axis;
2423        chart.plot_area = plot_area_from_chart_space(&space);
2424        chart.fill = chart_level_fill_from_sp_pr(space.sp_pr.as_ref());
2425        chart.border = line_from_sp_pr(space.sp_pr.as_ref());
2426        if let Some(val) = space
2427            .chart
2428            .disp_blanks_as
2429            .as_ref()
2430            .and_then(|v| v.val.clone())
2431        {
2432            chart.show_blanks_as = val;
2433        }
2434        if let Some(group) = space.chart.plot_area.as_ref().and_then(first_chart_group) {
2435            chart.vary_colors = group.vary_colors.as_ref().and_then(|v| v.val);
2436            chart.gap_width = group.gap_width.as_ref().and_then(|v| v.val).map(|v| v as u64);
2437            chart.overlap = group.overlap.as_ref().and_then(|v| v.val);
2438            chart.hole_size = group.hole_size.as_ref().and_then(|v| v.val).unwrap_or(0);
2439            chart.bubble_size = group
2440                .bubble_scale
2441                .as_ref()
2442                .and_then(|v| v.val)
2443                .map(|v| v as i64)
2444                .unwrap_or(0);
2445        }
2446    } else {
2447        chart.r#type = detect_chart_type(xml);
2448        chart.title.paragraph = extract_chart_title_text(xml);
2449    }
2450
2451    chart
2452}
2453
2454fn chart_attr_str(v: &Option<AttrValString>) -> &str {
2455    v.as_ref().and_then(|v| v.val.as_deref()).unwrap_or("")
2456}
2457
2458fn chart_type_from_chart_space(space: &DecodeChartSpace) -> Option<ChartType> {
2459    let pa = space.chart.plot_area.as_ref()?;
2460    let attr_str = chart_attr_str;
2461    if let Some(g) = pa.bar_chart.first() {
2462        let col = attr_str(&g.bar_dir) == "col";
2463        return Some(match (col, attr_str(&g.grouping)) {
2464            (false, "stacked") => ChartType::BAR_STACKED,
2465            (false, "percentStacked") => ChartType::BAR_PERCENT_STACKED,
2466            (false, _) => ChartType::BAR,
2467            (true, "stacked") => ChartType::COL_STACKED,
2468            (true, "percentStacked") => ChartType::COL_PERCENT_STACKED,
2469            (true, _) => ChartType::COL,
2470        });
2471    }
2472    if let Some(g) = pa.bar_3d_chart.first() {
2473        let col = attr_str(&g.bar_dir) == "col";
2474        let base = match (attr_str(&g.shape), col) {
2475            ("cone", false) => 12,
2476            ("pyramid", false) => 15,
2477            ("cylinder", false) => 18,
2478            ("cone", true) => 28,
2479            ("pyramid", true) => 32,
2480            ("cylinder", true) => 36,
2481            (_, false) => 9,
2482            (_, true) => 24,
2483        };
2484        // Bar-direction 3D blocks run clustered/stacked/percentStacked, while
2485        // column-direction blocks run standard/clustered/stacked/percentStacked.
2486        let idx = if col {
2487            match attr_str(&g.grouping) {
2488                "clustered" => 1,
2489                "stacked" => 2,
2490                "percentStacked" => 3,
2491                _ => 0,
2492            }
2493        } else {
2494            match attr_str(&g.grouping) {
2495                "stacked" => 1,
2496                "percentStacked" => 2,
2497                _ => 0,
2498            }
2499        };
2500        return Some(ChartType(base + idx));
2501    }
2502    if let Some(g) = pa.area_chart.first() {
2503        return Some(match attr_str(&g.grouping) {
2504            "stacked" => ChartType::AREA_STACKED,
2505            "percentStacked" => ChartType::AREA_PERCENT_STACKED,
2506            _ => ChartType::AREA,
2507        });
2508    }
2509    if let Some(g) = pa.area_3d_chart.first() {
2510        return Some(match attr_str(&g.grouping) {
2511            "stacked" => ChartType::AREA_3D_STACKED,
2512            "percentStacked" => ChartType::AREA_3D_PERCENT_STACKED,
2513            _ => ChartType::AREA_3D,
2514        });
2515    }
2516    if pa.line_chart.first().is_some() {
2517        return Some(ChartType::LINE);
2518    }
2519    if pa.line_3d_chart.first().is_some() {
2520        return Some(ChartType::LINE_3D);
2521    }
2522    if pa.pie_chart.first().is_some() {
2523        return Some(ChartType::PIE);
2524    }
2525    if pa.pie_3d_chart.first().is_some() {
2526        return Some(ChartType::PIE_3D);
2527    }
2528    if let Some(g) = pa.of_pie_chart.first() {
2529        return Some(if attr_str(&g.of_pie_type) == "bar" {
2530            ChartType::BAR_OF_PIE
2531        } else {
2532            ChartType::PIE_OF_PIE
2533        });
2534    }
2535    if pa.doughnut_chart.first().is_some() {
2536        return Some(ChartType::DOUGHNUT);
2537    }
2538    if pa.radar_chart.first().is_some() {
2539        return Some(ChartType::RADAR);
2540    }
2541    if pa.scatter_chart.first().is_some() {
2542        return Some(ChartType::SCATTER);
2543    }
2544    if let Some(g) = pa.surface_chart.first() {
2545        let wireframe = g.wireframe.as_ref().and_then(|v| v.val).unwrap_or_default();
2546        return Some(if wireframe {
2547            ChartType::WIREFRAME_CONTOUR
2548        } else {
2549            ChartType::CONTOUR
2550        });
2551    }
2552    if let Some(g) = pa.surface_3d_chart.first() {
2553        let wireframe = g.wireframe.as_ref().and_then(|v| v.val).unwrap_or_default();
2554        return Some(if wireframe {
2555            ChartType::WIREFRAME_SURFACE_3D
2556        } else {
2557            ChartType::SURFACE_3D
2558        });
2559    }
2560    if pa.bubble_chart.first().is_some() {
2561        return Some(ChartType::BUBBLE);
2562    }
2563    if let Some(g) = pa.stock_chart.first() {
2564        let has_up_down_bars = g
2565            .up_down_bars
2566            .as_ref()
2567            .map(|u| u.up_bars.is_some() || u.down_bars.is_some())
2568            .unwrap_or_default();
2569        return Some(if has_up_down_bars {
2570            ChartType::STOCK_OPEN_HIGH_LOW_CLOSE
2571        } else {
2572            ChartType::STOCK_HIGH_LOW_CLOSE
2573        });
2574    }
2575    None
2576}
2577
2578fn detect_chart_type(xml: &str) -> ChartType {
2579    // Chart type is determined by the chart element present in the plot area.
2580    // Bar charts are further split by the c:barDir value.
2581    if xml.contains("<c:barChart") {
2582        if xml.contains(r#"<c:barDir val="col""#) {
2583            return ChartType::COL;
2584        }
2585        return ChartType::BAR;
2586    }
2587    if xml.contains("<c:lineChart") {
2588        return ChartType::LINE;
2589    }
2590    if xml.contains("<c:pieChart") {
2591        return ChartType::PIE;
2592    }
2593    if xml.contains("<c:doughnutChart") {
2594        return ChartType::DOUGHNUT;
2595    }
2596    if xml.contains("<c:areaChart") {
2597        return ChartType::AREA;
2598    }
2599    if xml.contains("<c:scatterChart") {
2600        return ChartType::SCATTER;
2601    }
2602    if xml.contains("<c:radarChart") {
2603        return ChartType::RADAR;
2604    }
2605    if xml.contains("<c:surfaceChart") || xml.contains("<c:surface3DChart") {
2606        return ChartType::SURFACE_3D;
2607    }
2608    if xml.contains("<c:bubbleChart") {
2609        return ChartType::BUBBLE;
2610    }
2611    if xml.contains("<c:stockChart") {
2612        return ChartType::STOCK_HIGH_LOW_CLOSE;
2613    }
2614    ChartType::default()
2615}
2616
2617fn extract_chart_title_text(xml: &str) -> Vec<crate::xml::common::RichTextRun> {
2618    // Extract the contents of the first c:title element.
2619    let Some(start) = xml.find("<c:title") else {
2620        return Vec::new();
2621    };
2622    let Some(end) = xml[start..].find("</c:title>") else {
2623        return Vec::new();
2624    };
2625    let title_xml = &xml[start..start + end + "</c:title>".len()];
2626
2627    // Collect text from all a:t runs inside the title.
2628    let mut runs = Vec::new();
2629    let mut pos = 0;
2630    while let Some(tag_start) = title_xml[pos..].find("<a:t>") {
2631        let tag_start = pos + tag_start + "<a:t>".len();
2632        let Some(tag_end) = title_xml[tag_start..].find("</a:t>") else {
2633            break;
2634        };
2635        runs.push(crate::xml::common::RichTextRun {
2636            text: title_xml[tag_start..tag_start + tag_end].to_string(),
2637            ..Default::default()
2638        });
2639        pos = tag_start + tag_end + "</a:t>".len();
2640    }
2641    runs
2642}
2643
2644fn axes_from_chart_space(space: &DecodeChartSpace) -> (ChartAxis, ChartAxis) {
2645    let Some(pa) = space.chart.plot_area.as_ref() else {
2646        return (ChartAxis::default(), ChartAxis::default());
2647    };
2648
2649    let x_axis = pa
2650        .cat_ax
2651        .first()
2652        .or_else(|| pa.date_ax.first())
2653        .or_else(|| pa.ser_ax.first())
2654        .map(axis_from_decode)
2655        .unwrap_or_default();
2656
2657    // Use the last value axis as the active Y axis; when a secondary axis is
2658    // present it is typically the second (last) valAx, otherwise this falls
2659    // back to the primary axis.
2660    let mut y_axis = pa
2661        .val_ax
2662        .last()
2663        .or_else(|| pa.date_ax.last())
2664        .map(axis_from_decode)
2665        .unwrap_or_default();
2666    y_axis.secondary = pa.val_ax.len() > 1 || pa.date_ax.len() > 1;
2667
2668    (x_axis, y_axis)
2669}
2670
2671fn axis_from_decode(axis: &DecodeCAxis) -> ChartAxis {
2672    let mut a = ChartAxis::default();
2673
2674    a.none = axis
2675        .delete
2676        .as_ref()
2677        .and_then(|d| d.val)
2678        .unwrap_or(false);
2679    a.major_grid_lines = axis.major_grid_lines.is_some();
2680    a.minor_grid_lines = axis.minor_grid_lines.is_some();
2681    a.ax_id = axis.ax_id.as_ref().and_then(|v| v.val).unwrap_or(0);
2682    a.tick_label_skip = axis
2683        .tick_lbl_skip
2684        .as_ref()
2685        .and_then(|v| v.val)
2686        .unwrap_or(0);
2687    a.tick_label_position = tick_label_position_from_str(
2688        axis.tick_lbl_pos.as_ref().and_then(|v| v.val.as_deref()),
2689    );
2690
2691    if let Some(scaling) = &axis.scaling {
2692        a.reverse_order = scaling
2693            .orientation
2694            .as_ref()
2695            .and_then(|v| v.val.as_deref())
2696            == Some("maxMin");
2697        a.maximum = scaling.max.as_ref().and_then(|v| v.val);
2698        a.minimum = scaling.min.as_ref().and_then(|v| v.val);
2699        a.log_base = scaling.log_base.as_ref().and_then(|v| v.val).unwrap_or(0.0);
2700    }
2701
2702    a.major_unit = axis
2703        .major_unit
2704        .as_ref()
2705        .and_then(|v| v.val)
2706        .unwrap_or(0.0);
2707
2708    if let Some(num_fmt) = &axis.num_fmt {
2709        a.num_fmt = chart_num_fmt_from_decode(num_fmt);
2710    }
2711
2712    if let Some(title) = &axis.title {
2713        a.title = chart_title_from_decode_title(title);
2714    }
2715
2716    a
2717}
2718
2719fn tick_label_position_from_str(pos: Option<&str>) -> ChartTickLabelPositionType {
2720    match pos {
2721        Some("high") => ChartTickLabelPositionType::HIGH,
2722        Some("low") => ChartTickLabelPositionType::LOW,
2723        Some("none") => ChartTickLabelPositionType::NONE,
2724        _ => ChartTickLabelPositionType::NEXT_TO_AXIS,
2725    }
2726}
2727
2728fn chart_num_fmt_from_decode(num_fmt: &DecodeCNumFmt) -> crate::xml::chart::ChartNumFmt {
2729    crate::xml::chart::ChartNumFmt {
2730        custom_num_fmt: num_fmt.format_code.clone(),
2731        source_linked: num_fmt.source_linked,
2732    }
2733}
2734
2735fn chart_title_from_chart_space(space: &DecodeChartSpace) -> ChartTitle {
2736    space
2737        .chart
2738        .title
2739        .as_ref()
2740        .map(chart_title_from_decode_title)
2741        .unwrap_or_default()
2742}
2743
2744fn chart_title_from_decode_title(title: &DecodeCTitle) -> ChartTitle {
2745    let mut t = ChartTitle::default();
2746    if let Some(tx) = &title.tx {
2747        let has_formula = tx
2748            .str_ref
2749            .as_ref()
2750            .map(|r| !r.f.is_empty())
2751            .unwrap_or(false);
2752        if has_formula {
2753            t.formula = tx.str_ref.as_ref().unwrap().f.clone();
2754        } else if let Some(rich) = &tx.rich {
2755            for p in &rich.p {
2756                if let Some(text) = &p.t {
2757                    t.paragraph.push(RichTextRun {
2758                        text: text.clone(),
2759                        font: None,
2760                    });
2761                }
2762                for r in &p.r {
2763                    if let Some(text) = &r.t {
2764                        t.paragraph.push(RichTextRun {
2765                            text: text.clone(),
2766                            font: font_from_decode_rpr(r.r_pr.as_ref()),
2767                        });
2768                    }
2769                }
2770            }
2771        }
2772    }
2773    t.font = font_from_decode_rpr(
2774        title
2775            .tx_pr
2776            .as_ref()
2777            .and_then(|tx_pr| tx_pr.p.as_ref())
2778            .and_then(|p| p.p_pr.as_ref())
2779            .and_then(|p_pr| p_pr.def_r_pr.as_ref()),
2780    );
2781    t.overlay = title
2782        .overlay
2783        .as_ref()
2784        .and_then(|v| v.val)
2785        .unwrap_or(false);
2786    t.fill = fill_from_sp_pr(title.sp_pr.as_ref());
2787    t.border = line_from_sp_pr(title.sp_pr.as_ref());
2788    t
2789}
2790
2791fn legend_from_chart_space(space: &DecodeChartSpace) -> ChartLegend {
2792    let Some(legend) = space.chart.legend.as_ref() else {
2793        return ChartLegend::default();
2794    };
2795
2796    let position = legend
2797        .legend_pos
2798        .as_ref()
2799        .and_then(|p| p.val.as_deref())
2800        .map(chart_legend_position_from_xml)
2801        .unwrap_or_default();
2802
2803    let font = font_from_decode_rpr(
2804        legend
2805            .tx_pr
2806            .as_ref()
2807            .and_then(|tx_pr| tx_pr.p.as_ref())
2808            .and_then(|p| p.p_pr.as_ref())
2809            .and_then(|p_pr| p_pr.def_r_pr.as_ref()),
2810    );
2811
2812    let show_legend_key = space
2813        .chart
2814        .plot_area
2815        .as_ref()
2816        .and_then(show_legend_key_from_plot_area)
2817        .unwrap_or(false);
2818
2819    ChartLegend {
2820        position,
2821        show_legend_key,
2822        font,
2823    }
2824}
2825
2826fn chart_legend_position_from_xml(pos: &str) -> String {
2827    CHART_LEGEND_POSITION
2828        .iter()
2829        .find(|&&(_, v)| v == pos)
2830        .map(|&(k, _)| k.to_string())
2831        .unwrap_or_else(|| match pos {
2832            "b" => "bottom".to_string(),
2833            "none" => "none".to_string(),
2834            _ => String::new(),
2835        })
2836}
2837
2838fn first_chart_group(pa: &DecodeCPlotArea) -> Option<&DecodeCCharts> {
2839    let groups: [&Vec<DecodeCCharts>; 16] = [
2840        &pa.area_chart,
2841        &pa.area_3d_chart,
2842        &pa.bar_chart,
2843        &pa.bar_3d_chart,
2844        &pa.bubble_chart,
2845        &pa.doughnut_chart,
2846        &pa.line_chart,
2847        &pa.line_3d_chart,
2848        &pa.of_pie_chart,
2849        &pa.pie_chart,
2850        &pa.pie_3d_chart,
2851        &pa.radar_chart,
2852        &pa.scatter_chart,
2853        &pa.stock_chart,
2854        &pa.surface_chart,
2855        &pa.surface_3d_chart,
2856    ];
2857    groups.into_iter().flat_map(|g| g.iter()).next()
2858}
2859
2860fn plot_area_from_chart_space(space: &DecodeChartSpace) -> ChartPlotArea {
2861    let mut plot_area = ChartPlotArea::default();
2862    let Some(pa) = space.chart.plot_area.as_ref() else {
2863        return plot_area;
2864    };
2865    if let Some(group) = first_chart_group(pa) {
2866        if let Some(d_lbls) = group.d_lbls.as_ref() {
2867            plot_area.show_val = d_lbls.show_val.as_ref().and_then(|v| v.val).unwrap_or_default();
2868            plot_area.show_cat_name = d_lbls
2869                .show_cat_name
2870                .as_ref()
2871                .and_then(|v| v.val)
2872                .unwrap_or_default();
2873            plot_area.show_ser_name = d_lbls
2874                .show_ser_name
2875                .as_ref()
2876                .and_then(|v| v.val)
2877                .unwrap_or_default();
2878            plot_area.show_percent = d_lbls
2879                .show_percent
2880                .as_ref()
2881                .and_then(|v| v.val)
2882                .unwrap_or_default();
2883            plot_area.show_bubble_size = d_lbls
2884                .show_bubble_size
2885                .as_ref()
2886                .and_then(|v| v.val)
2887                .unwrap_or_default();
2888            plot_area.show_leader_lines = d_lbls
2889                .show_leader_lines
2890                .as_ref()
2891                .and_then(|v| v.val)
2892                .unwrap_or_default();
2893        }
2894        if let Some(split_pos) = group.split_pos.as_ref().and_then(|v| v.val) {
2895            plot_area.second_plot_values = split_pos;
2896        }
2897    }
2898    if let Some(d_table) = pa.d_table.as_ref() {
2899        plot_area.show_data_table = true;
2900        plot_area.show_data_table_keys = d_table
2901            .show_keys
2902            .as_ref()
2903            .and_then(|v| v.val)
2904            .unwrap_or_default();
2905    }
2906    plot_area.fill = fill_from_sp_pr(pa.sp_pr.as_ref());
2907    plot_area
2908}
2909
2910fn show_legend_key_from_plot_area(pa: &DecodeCPlotArea) -> Option<bool> {
2911    let groups: [&Vec<DecodeCCharts>; 16] = [
2912        &pa.area_chart,
2913        &pa.area_3d_chart,
2914        &pa.bar_chart,
2915        &pa.bar_3d_chart,
2916        &pa.bubble_chart,
2917        &pa.doughnut_chart,
2918        &pa.line_chart,
2919        &pa.line_3d_chart,
2920        &pa.of_pie_chart,
2921        &pa.pie_chart,
2922        &pa.pie_3d_chart,
2923        &pa.radar_chart,
2924        &pa.scatter_chart,
2925        &pa.stock_chart,
2926        &pa.surface_chart,
2927        &pa.surface_3d_chart,
2928    ];
2929    groups
2930        .iter()
2931        .flat_map(|g| g.iter())
2932        .filter_map(|c| c.d_lbls.as_ref())
2933        .filter_map(|d| d.show_legend_key.as_ref())
2934        .filter_map(|v| v.val)
2935        .next()
2936}
2937
2938fn font_from_decode_rpr(rpr: Option<&DecodeARPr>) -> Option<crate::styles::Font> {
2939    let rpr = rpr?;
2940    let mut font = crate::styles::Font::default();
2941    if let Some(sz) = rpr.sz.as_ref().and_then(|s| s.parse::<f64>().ok()) {
2942        if sz > 0.0 {
2943            font.size = Some(sz / 100.0);
2944        }
2945    }
2946    if let Some(b) = &rpr.b {
2947        font.bold = Some(parse_bool_string(b));
2948    }
2949    if let Some(i) = &rpr.i {
2950        font.italic = Some(parse_bool_string(i));
2951    }
2952    if let Some(strike) = &rpr.strike {
2953        font.strike = Some(matches!(strike.as_str(), "sngStrike" | "dblStrike" | "true" | "1"));
2954    }
2955    if let Some(u) = &rpr.u {
2956        if u != "none" {
2957            font.underline = Some(u.clone());
2958        }
2959    }
2960    if let Some(vert_align) = &rpr.vert_align {
2961        font.vert_align = Some(vert_align.clone());
2962    }
2963    if let Some(name) = rpr.latin.as_ref().and_then(|l| l.typeface.as_ref()) {
2964        font.name = Some(name.clone());
2965    }
2966    if let Some(color) = rpr.solid_fill.as_ref().and_then(|sf| {
2967        sf.srgb_clr
2968            .as_ref()
2969            .and_then(|c| c.val.clone())
2970            .or_else(|| sf.scheme_clr.as_ref().and_then(|c| c.val.clone()))
2971    }) {
2972        font.color = Some(color);
2973    }
2974    Some(font)
2975}
2976
2977fn parse_bool_string(s: &str) -> bool {
2978    matches!(s.to_ascii_lowercase().as_str(), "true" | "1" | "on")
2979}
2980
2981fn chart_groups_with_types(pa: &DecodeCPlotArea) -> Vec<(&DecodeCCharts, ChartType)> {
2982    let groups: [(&Vec<DecodeCCharts>, ChartType); 16] = [
2983        (&pa.area_chart, ChartType::AREA),
2984        (&pa.area_3d_chart, ChartType::AREA_3D),
2985        (&pa.bar_chart, ChartType::BAR),
2986        (&pa.bar_3d_chart, ChartType::BAR_3D_CLUSTERED),
2987        (&pa.bubble_chart, ChartType::BUBBLE),
2988        (&pa.doughnut_chart, ChartType::DOUGHNUT),
2989        (&pa.line_chart, ChartType::LINE),
2990        (&pa.line_3d_chart, ChartType::LINE_3D),
2991        (&pa.of_pie_chart, ChartType::PIE_OF_PIE),
2992        (&pa.pie_chart, ChartType::PIE),
2993        (&pa.pie_3d_chart, ChartType::PIE_3D),
2994        (&pa.radar_chart, ChartType::RADAR),
2995        (&pa.scatter_chart, ChartType::SCATTER),
2996        (&pa.stock_chart, ChartType::STOCK_HIGH_LOW_CLOSE),
2997        (&pa.surface_chart, ChartType::CONTOUR),
2998        (&pa.surface_3d_chart, ChartType::SURFACE_3D),
2999    ];
3000    let mut out = Vec::new();
3001    for (group, chart_type) in groups {
3002        for c in group.iter() {
3003            out.push((c, chart_type));
3004        }
3005    }
3006    out
3007}
3008
3009fn series_from_chart_space(space: &DecodeChartSpace) -> Vec<ChartSeries> {
3010    let Some(pa) = space.chart.plot_area.as_ref() else {
3011        return Vec::new();
3012    };
3013    chart_groups_with_types(pa)
3014        .into_iter()
3015        .flat_map(|(group, chart_type)| {
3016            group
3017                .ser
3018                .iter()
3019                .map(move |ser| chart_series_from_cser(ser, chart_type))
3020        })
3021        .collect()
3022}
3023
3024fn chart_series_from_cser(ser: &DecodeCSer, chart_type: ChartType) -> ChartSeries {
3025    let mut series = ChartSeries::default();
3026
3027    series.name = ser
3028        .tx
3029        .as_ref()
3030        .and_then(|tx| tx.str_ref.as_ref())
3031        .map(|r| r.f.clone())
3032        .unwrap_or_default();
3033
3034    if chart_series_uses_xy(chart_type) {
3035        series.categories = ser
3036            .x_val
3037            .as_ref()
3038            .and_then(|x| x.str_ref.as_ref())
3039            .map(|r| r.f.clone())
3040            .unwrap_or_default();
3041        series.values = ser
3042            .y_val
3043            .as_ref()
3044            .and_then(|y| y.num_ref.as_ref())
3045            .map(|r| r.f.clone())
3046            .unwrap_or_default();
3047    } else {
3048        series.categories = ser
3049            .cat
3050            .as_ref()
3051            .and_then(|cat| cat.str_ref.as_ref())
3052            .map(|r| r.f.clone())
3053            .unwrap_or_default();
3054        series.values = ser
3055            .val
3056            .as_ref()
3057            .and_then(|val| val.num_ref.as_ref())
3058            .map(|r| r.f.clone())
3059            .unwrap_or_default();
3060    }
3061
3062    if chart_series_uses_bubble_size(chart_type) {
3063        series.sizes = ser
3064            .bubble_size
3065            .as_ref()
3066            .and_then(|bs| bs.num_ref.as_ref())
3067            .map(|r| r.f.clone())
3068            .unwrap_or_default();
3069    }
3070
3071    series.marker = marker_from_cmarker(ser.marker.as_ref());
3072    series.data_point = data_points_from_cdpt(&ser.d_pt);
3073    series.data_label_position = data_label_position_from_d_lbls(ser.d_lbls.as_ref());
3074
3075    if let Some(d_lbls) = ser.d_lbls.as_ref() {
3076        series.data_label.fill = fill_from_sp_pr(d_lbls.sp_pr.as_ref());
3077        if let Some(font) = d_lbls
3078            .tx_pr
3079            .as_ref()
3080            .and_then(|tx| tx.p.as_ref())
3081            .and_then(|p| p.p_pr.as_ref())
3082            .and_then(|p_pr| font_from_decode_rpr(p_pr.def_r_pr.as_ref()))
3083        {
3084            series.data_label.font = font;
3085        }
3086    }
3087
3088    if let Some(sp_pr) = &ser.sp_pr {
3089        series.fill = fill_from_sp_pr(Some(sp_pr));
3090        series.line = line_from_sp_pr(Some(sp_pr));
3091    }
3092
3093    if let Some(smooth) = ser.smooth.as_ref().and_then(|s| s.val) {
3094        series.line.smooth = smooth;
3095    }
3096
3097    series
3098}
3099
3100fn marker_from_cmarker(marker: Option<&DecodeCMarker>) -> ChartMarker {
3101    let Some(m) = marker else {
3102        return ChartMarker::default();
3103    };
3104    ChartMarker {
3105        symbol: m
3106            .symbol
3107            .as_ref()
3108            .and_then(|s| s.val.clone())
3109            .unwrap_or_default(),
3110        size: m.size.as_ref().and_then(|s| s.val).unwrap_or(0),
3111        ..Default::default()
3112    }
3113}
3114
3115fn data_points_from_cdpt(d_pts: &[DecodeCDPt]) -> Vec<ChartDataPoint> {
3116    d_pts
3117        .iter()
3118        .map(|dp| ChartDataPoint {
3119            index: dp.idx.as_ref().and_then(|i| i.val).unwrap_or(0),
3120            fill: fill_from_sp_pr(dp.sp_pr.as_ref()),
3121        })
3122        .collect()
3123}
3124
3125fn data_label_position_from_d_lbls(d_lbls: Option<&DecodeCDLbls>) -> ChartDataLabelPositionType {
3126    let Some(pos) = d_lbls.and_then(|d| d.d_lbl_pos.as_ref()).and_then(|p| p.val.as_deref()) else {
3127        return ChartDataLabelPositionType::UNSET;
3128    };
3129    match pos {
3130        "bestFit" => ChartDataLabelPositionType::BEST_FIT,
3131        "b" => ChartDataLabelPositionType::BELOW,
3132        "ctr" => ChartDataLabelPositionType::CENTER,
3133        "inBase" => ChartDataLabelPositionType::INSIDE_BASE,
3134        "inEnd" => ChartDataLabelPositionType::INSIDE_END,
3135        "l" => ChartDataLabelPositionType::LEFT,
3136        "outEnd" => ChartDataLabelPositionType::OUTSIDE_END,
3137        "r" => ChartDataLabelPositionType::RIGHT,
3138        "t" => ChartDataLabelPositionType::ABOVE,
3139        _ => ChartDataLabelPositionType::UNSET,
3140    }
3141}
3142
3143fn chart_level_fill_from_sp_pr(sp_pr: Option<&DecodeCSpPr>) -> Fill {
3144    // The writer always emits a schemeClr "bg1" chart background, so only an
3145    // explicit srgbClr represents a user-configured chart fill.
3146    let Some(sp_pr) = sp_pr else {
3147        return Fill::default();
3148    };
3149    let Some(solid) = sp_pr.solid_fill.as_ref() else {
3150        return if sp_pr.no_fill.is_some() {
3151            Fill {
3152                r#type: "none".to_string(),
3153                ..Default::default()
3154            }
3155        } else {
3156            Fill::default()
3157        };
3158    };
3159    let Some(color) = solid.srgb_clr.as_ref().and_then(|c| c.val.clone()) else {
3160        return Fill::default();
3161    };
3162    Fill {
3163        r#type: "pattern".to_string(),
3164        pattern: 1,
3165        color: vec![format!("#{}", color.trim_start_matches('#'))],
3166        ..Default::default()
3167    }
3168}
3169
3170fn fill_from_sp_pr(sp_pr: Option<&DecodeCSpPr>) -> Fill {
3171    let Some(sp_pr) = sp_pr else {
3172        return Fill::default();
3173    };
3174
3175    let color = sp_pr.solid_fill.as_ref().and_then(|sf| {
3176        sf.srgb_clr
3177            .as_ref()
3178            .and_then(|c| c.val.clone())
3179            .or_else(|| sf.scheme_clr.as_ref().and_then(|c| c.val.clone()))
3180    });
3181
3182    if let Some(color) = color {
3183        return Fill {
3184            r#type: "pattern".to_string(),
3185            pattern: 1,
3186            color: vec![format!("#{}", color.trim_start_matches('#'))],
3187            ..Default::default()
3188        };
3189    }
3190
3191    if sp_pr.no_fill.is_some() {
3192        return Fill {
3193            r#type: "none".to_string(),
3194            ..Default::default()
3195        };
3196    }
3197
3198    Fill::default()
3199}
3200
3201fn line_from_sp_pr(sp_pr: Option<&DecodeCSpPr>) -> LineOptions {
3202    let Some(sp_pr) = sp_pr else {
3203        return LineOptions::default();
3204    };
3205    let Some(ln) = &sp_pr.ln else {
3206        return LineOptions::default();
3207    };
3208
3209    let fill = fill_from_sp_pr(Some(&DecodeCSpPr {
3210        solid_fill: ln.solid_fill.clone(),
3211        ..Default::default()
3212    }));
3213
3214    if fill.color.is_empty() && ln.no_fill.is_some() {
3215        return LineOptions {
3216            r#type: LineType::NONE,
3217            ..Default::default()
3218        };
3219    }
3220
3221    let width = ln
3222        .w
3223        .as_deref()
3224        .and_then(|w| w.parse::<i64>().ok())
3225        .map(|w| w as f64 / 12700.0)
3226        .unwrap_or(0.0);
3227
3228    LineOptions {
3229        r#type: LineType::SOLID,
3230        width,
3231        fill,
3232        dash: dash_from_prst_dash(ln.prst_dash.as_ref()),
3233        ..Default::default()
3234    }
3235}
3236
3237fn dash_from_prst_dash(prst_dash: Option<&AttrValString>) -> crate::xml::drawing::LineDashType {
3238    use crate::xml::drawing::LineDashType;
3239    let Some(val) = prst_dash.and_then(|v| v.val.as_deref()) else {
3240        return LineDashType::UNSET;
3241    };
3242    match val {
3243        "solid" => LineDashType::SOLID,
3244        "dot" => LineDashType::DOT,
3245        "dash" => LineDashType::DASH,
3246        "lgDash" => LineDashType::LG_DASH,
3247        "dashDot" => LineDashType::SASH_DOT,
3248        "lgDashDot" => LineDashType::LG_DASH_DOT,
3249        "lgDashDotDot" => LineDashType::LG_DASH_DOT_DOT,
3250        "sysDash" => LineDashType::SYS_DASH,
3251        "sysDot" => LineDashType::SYS_DOT,
3252        "sysDashDot" => LineDashType::SYS_DASH_DOT,
3253        "sysDashDotDot" => LineDashType::SYS_DASH_DOT_DOT,
3254        _ => LineDashType::UNSET,
3255    }
3256}
3257
3258fn convert_col_width_to_pixels(width: f64) -> f64 {
3259    width * 8.0 + 0.5
3260}
3261
3262fn convert_row_height_to_pixels(height: f64) -> f64 {
3263    (4.0 / 3.4 * height).ceil()
3264}
3265
3266// ------------------------------------------------------------------
3267// Tests
3268// ------------------------------------------------------------------
3269
3270#[cfg(test)]
3271mod tests {
3272    use super::*;
3273    use crate::Options;
3274
3275    #[test]
3276    fn add_column_chart_round_trip() {
3277        let mut f = File::new_with_options(Options::default());
3278        f.set_cell_str("Sheet1", "A1", "Fruit").unwrap();
3279        f.set_cell_str("Sheet1", "B1", "Apple").unwrap();
3280        f.set_cell_str("Sheet1", "C1", "Orange").unwrap();
3281        f.set_cell_str("Sheet1", "A2", "Small").unwrap();
3282        f.set_cell_int("Sheet1", "B2", 2).unwrap();
3283        f.set_cell_int("Sheet1", "C2", 3).unwrap();
3284
3285        let chart = Chart {
3286            r#type: ChartType::COL,
3287            series: vec![ChartSeries {
3288                name: "Sheet1!$A$2".to_string(),
3289                categories: "Sheet1!$B$1:$C$1".to_string(),
3290                values: "Sheet1!$B$2:$C$2".to_string(),
3291                ..Default::default()
3292            }],
3293            title: ChartTitle {
3294                paragraph: vec![crate::xml::common::RichTextRun {
3295                    text: "Fruit Chart".to_string(),
3296                    ..Default::default()
3297                }],
3298                ..Default::default()
3299            },
3300            ..Default::default()
3301        };
3302        f.add_chart("Sheet1", "E1", &chart).unwrap();
3303
3304        let charts = f.get_charts("Sheet1").unwrap();
3305        assert_eq!(charts.len(), 1);
3306        assert_eq!(charts[0].r#type, ChartType::COL);
3307        assert_eq!(charts[0].title.paragraph.len(), 1);
3308        assert_eq!(charts[0].title.paragraph[0].text, "Fruit Chart");
3309    }
3310
3311    #[test]
3312    fn combo_chart_generates_two_chart_groups() {
3313        let mut f = File::new_with_options(Options::default());
3314        f.set_cell_str("Sheet1", "A1", "Fruit").unwrap();
3315        f.set_cell_str("Sheet1", "B1", "Apple").unwrap();
3316        f.set_cell_str("Sheet1", "C1", "Orange").unwrap();
3317        f.set_cell_str("Sheet1", "A2", "Small").unwrap();
3318        f.set_cell_int("Sheet1", "B2", 2).unwrap();
3319        f.set_cell_int("Sheet1", "C2", 3).unwrap();
3320        f.set_cell_str("Sheet1", "A3", "Large").unwrap();
3321        f.set_cell_int("Sheet1", "B3", 5).unwrap();
3322        f.set_cell_int("Sheet1", "C3", 7).unwrap();
3323
3324        let combo = Chart {
3325            r#type: ChartType::LINE,
3326            series: vec![ChartSeries {
3327                name: "Sheet1!$A$3".to_string(),
3328                categories: "Sheet1!$B$1:$C$1".to_string(),
3329                values: "Sheet1!$B$3:$C$3".to_string(),
3330                marker: ChartMarker {
3331                    symbol: "none".to_string(),
3332                    size: 10,
3333                    ..Default::default()
3334                },
3335                ..Default::default()
3336            }],
3337            ..Default::default()
3338        };
3339        let chart = Chart {
3340            r#type: ChartType::COL,
3341            series: vec![ChartSeries {
3342                name: "Sheet1!$A$2".to_string(),
3343                categories: "Sheet1!$B$1:$C$1".to_string(),
3344                values: "Sheet1!$B$2:$C$2".to_string(),
3345                ..Default::default()
3346            }],
3347            combo: vec![combo],
3348            ..Default::default()
3349        };
3350        f.add_chart("Sheet1", "E1", &chart).unwrap();
3351
3352        let charts = f.get_charts("Sheet1").unwrap();
3353        assert_eq!(charts.len(), 1);
3354        assert_eq!(charts[0].r#type, ChartType::COL);
3355    }
3356
3357    #[test]
3358    fn chart_secondary_axis_uses_separate_ax_ids() {
3359        let mut f = File::new_with_options(Options::default());
3360        f.set_cell_str("Sheet1", "A1", "Fruit").unwrap();
3361        f.set_cell_str("Sheet1", "B1", "Apple").unwrap();
3362        f.set_cell_str("Sheet1", "C1", "Orange").unwrap();
3363        f.set_cell_str("Sheet1", "A2", "Small").unwrap();
3364        f.set_cell_int("Sheet1", "B2", 2).unwrap();
3365        f.set_cell_int("Sheet1", "C2", 3).unwrap();
3366        f.set_cell_str("Sheet1", "A3", "Large").unwrap();
3367        f.set_cell_int("Sheet1", "B3", 500).unwrap();
3368        f.set_cell_int("Sheet1", "C3", 700).unwrap();
3369
3370        let mut combo = Chart {
3371            r#type: ChartType::LINE,
3372            series: vec![ChartSeries {
3373                name: "Sheet1!$A$3".to_string(),
3374                categories: "Sheet1!$B$1:$C$1".to_string(),
3375                values: "Sheet1!$B$3:$C$3".to_string(),
3376                ..Default::default()
3377            }],
3378            ..Default::default()
3379        };
3380        combo.y_axis.secondary = true;
3381        let chart = Chart {
3382            r#type: ChartType::COL,
3383            series: vec![ChartSeries {
3384                name: "Sheet1!$A$2".to_string(),
3385                categories: "Sheet1!$B$1:$C$1".to_string(),
3386                values: "Sheet1!$B$2:$C$2".to_string(),
3387                ..Default::default()
3388            }],
3389            combo: vec![combo],
3390            ..Default::default()
3391        };
3392        f.add_chart("Sheet1", "E1", &chart).unwrap();
3393
3394        let charts = f.get_charts("Sheet1").unwrap();
3395        assert_eq!(charts.len(), 1);
3396    }
3397
3398    #[test]
3399    fn chart_series_style_serializes_sp_pr() {
3400        let mut f = File::new_with_options(Options::default());
3401        f.set_cell_str("Sheet1", "A1", "Fruit").unwrap();
3402        f.set_cell_str("Sheet1", "B1", "Apple").unwrap();
3403        f.set_cell_str("Sheet1", "C1", "Orange").unwrap();
3404        f.set_cell_str("Sheet1", "A2", "Small").unwrap();
3405        f.set_cell_int("Sheet1", "B2", 2).unwrap();
3406        f.set_cell_int("Sheet1", "C2", 3).unwrap();
3407
3408        let chart = Chart {
3409            r#type: ChartType::COL,
3410            series: vec![ChartSeries {
3411                name: "Sheet1!$A$2".to_string(),
3412                categories: "Sheet1!$B$1:$C$1".to_string(),
3413                values: "Sheet1!$B$2:$C$2".to_string(),
3414                fill: crate::xml::drawing::Fill {
3415                    r#type: "pattern".to_string(),
3416                    pattern: 1,
3417                    color: vec!["#FF0000".to_string()],
3418                    ..Default::default()
3419                },
3420                ..Default::default()
3421            }],
3422            ..Default::default()
3423        };
3424        f.add_chart("Sheet1", "E1", &chart).unwrap();
3425
3426        let charts = f.get_charts("Sheet1").unwrap();
3427        assert_eq!(charts.len(), 1);
3428    }
3429
3430    #[test]
3431    fn add_chart_sheet_uses_sheet_drawing_rel_id() {
3432        let mut f = File::new_with_options(Options::default());
3433        f.set_cell_str("Sheet1", "A1", "Fruit").unwrap();
3434        f.set_cell_str("Sheet1", "B1", "Apple").unwrap();
3435        f.set_cell_str("Sheet1", "C1", "Orange").unwrap();
3436        f.set_cell_str("Sheet1", "A2", "Small").unwrap();
3437        f.set_cell_int("Sheet1", "B2", 2).unwrap();
3438        f.set_cell_int("Sheet1", "C2", 3).unwrap();
3439
3440        let chart = Chart {
3441            r#type: ChartType::COL,
3442            series: vec![ChartSeries {
3443                name: "Sheet1!$A$2".to_string(),
3444                categories: "Sheet1!$B$1:$C$1".to_string(),
3445                values: "Sheet1!$B$2:$C$2".to_string(),
3446                ..Default::default()
3447            }],
3448            ..Default::default()
3449        };
3450        f.add_chart_sheet("Chart1", &chart).unwrap();
3451
3452        // The chartsheet XML should reference the sheet-to-drawing relationship,
3453        // not the workbook-level chartsheet relationship.
3454        let cs_path = f.get_sheet_xml_path("Chart1").expect("chartsheet path");
3455        let cs_xml = String::from_utf8(f.read_xml(&cs_path)).unwrap();
3456        let sheet_rels_path = cs_path.replace("chartsheets/", "chartsheets/_rels/") + ".rels";
3457        let sheet_rels = f
3458            .relationships
3459            .get(&sheet_rels_path)
3460            .expect("missing chartsheet relationships");
3461        let drawing_rel = sheet_rels
3462            .relationships
3463            .iter()
3464            .find(|r| r.r#type == SOURCE_RELATIONSHIP_DRAWING_ML)
3465            .expect("missing sheet-to-drawing relationship");
3466        assert!(drawing_rel.target.contains("drawing1.xml"));
3467        assert!(
3468            cs_xml.contains(&format!("r:id=\"{}\"", drawing_rel.id)),
3469            "chartsheet drawing r:id {} not found in: {}",
3470            drawing_rel.id,
3471            cs_xml
3472        );
3473
3474        // Make sure the workbook relationship for the chartsheet uses a different rId.
3475        let wb_rels_path = f.get_workbook_rels_path();
3476        let wb_rels = f
3477            .relationships
3478            .get(&wb_rels_path)
3479            .expect("missing workbook relationships");
3480        let chartsheet_wb_rel = wb_rels
3481            .relationships
3482            .iter()
3483            .find(|r| r.r#type == SOURCE_RELATIONSHIP_CHARTSHEET)
3484            .expect("missing workbook chartsheet relationship");
3485        assert_ne!(chartsheet_wb_rel.id, drawing_rel.id);
3486    }
3487
3488    #[test]
3489    fn chart_series_round_trip_basic() {
3490        let mut f = File::new_with_options(Options::default());
3491        f.set_cell_str("Sheet1", "A1", "Fruit").unwrap();
3492        f.set_cell_str("Sheet1", "B1", "Apple").unwrap();
3493        f.set_cell_str("Sheet1", "C1", "Orange").unwrap();
3494        f.set_cell_str("Sheet1", "A2", "Small").unwrap();
3495        f.set_cell_int("Sheet1", "B2", 2).unwrap();
3496        f.set_cell_int("Sheet1", "C2", 3).unwrap();
3497
3498        let chart = Chart {
3499            r#type: ChartType::COL,
3500            series: vec![ChartSeries {
3501                name: "Sheet1!$A$2".to_string(),
3502                categories: "Sheet1!$B$1:$C$1".to_string(),
3503                values: "Sheet1!$B$2:$C$2".to_string(),
3504                fill: crate::xml::drawing::Fill {
3505                    r#type: "pattern".to_string(),
3506                    pattern: 1,
3507                    color: vec!["#FF0000".to_string()],
3508                    ..Default::default()
3509                },
3510                ..Default::default()
3511            }],
3512            ..Default::default()
3513        };
3514        f.add_chart("Sheet1", "E1", &chart).unwrap();
3515
3516        let charts = f.get_charts("Sheet1").unwrap();
3517        assert_eq!(charts.len(), 1);
3518        assert_eq!(charts[0].series.len(), 1);
3519        let s = &charts[0].series[0];
3520        assert_eq!(s.name, "Sheet1!$A$2");
3521        assert_eq!(s.categories, "Sheet1!$B$1:$C$1");
3522        assert_eq!(s.values, "Sheet1!$B$2:$C$2");
3523        assert_eq!(s.fill.color, vec!["#FF0000".to_string()]);
3524    }
3525
3526    #[test]
3527    fn chart_series_round_trip_line_marker() {
3528        let mut f = File::new_with_options(Options::default());
3529        f.set_cell_str("Sheet1", "A1", "Month").unwrap();
3530        f.set_cell_str("Sheet1", "B1", "Jan").unwrap();
3531        f.set_cell_str("Sheet1", "C1", "Feb").unwrap();
3532        f.set_cell_str("Sheet1", "A2", "Sales").unwrap();
3533        f.set_cell_int("Sheet1", "B2", 10).unwrap();
3534        f.set_cell_int("Sheet1", "C2", 20).unwrap();
3535
3536        let chart = Chart {
3537            r#type: ChartType::LINE,
3538            series: vec![ChartSeries {
3539                name: "Sheet1!$A$2".to_string(),
3540                categories: "Sheet1!$B$1:$C$1".to_string(),
3541                values: "Sheet1!$B$2:$C$2".to_string(),
3542                marker: ChartMarker {
3543                    symbol: "diamond".to_string(),
3544                    size: 7,
3545                    ..Default::default()
3546                },
3547                ..Default::default()
3548            }],
3549            ..Default::default()
3550        };
3551        f.add_chart("Sheet1", "E1", &chart).unwrap();
3552
3553        let charts = f.get_charts("Sheet1").unwrap();
3554        assert_eq!(charts.len(), 1);
3555        assert_eq!(charts[0].r#type, ChartType::LINE);
3556        assert_eq!(charts[0].series.len(), 1);
3557        let s = &charts[0].series[0];
3558        assert_eq!(s.marker.symbol, "diamond");
3559        assert_eq!(s.marker.size, 7);
3560    }
3561
3562    #[test]
3563    fn chart_axis_round_trip_basic() {
3564        let mut f = File::new_with_options(Options::default());
3565        f.set_cell_str("Sheet1", "A1", "Fruit").unwrap();
3566        f.set_cell_str("Sheet1", "B1", "Apple").unwrap();
3567        f.set_cell_str("Sheet1", "C1", "Orange").unwrap();
3568        f.set_cell_str("Sheet1", "A2", "Small").unwrap();
3569        f.set_cell_int("Sheet1", "B2", 2).unwrap();
3570        f.set_cell_int("Sheet1", "C2", 3).unwrap();
3571
3572        let chart = Chart {
3573            r#type: ChartType::COL,
3574            series: vec![ChartSeries {
3575                name: "Sheet1!$A$2".to_string(),
3576                categories: "Sheet1!$B$1:$C$1".to_string(),
3577                values: "Sheet1!$B$2:$C$2".to_string(),
3578                ..Default::default()
3579            }],
3580            x_axis: ChartAxis {
3581                reverse_order: true,
3582                major_grid_lines: true,
3583                maximum: Some(100.0),
3584                minimum: Some(10.0),
3585                title: ChartTitle {
3586                    paragraph: vec![RichTextRun {
3587                        text: "Quarter".to_string(),
3588                        ..Default::default()
3589                    }],
3590                    ..Default::default()
3591                },
3592                ..Default::default()
3593            },
3594            y_axis: ChartAxis {
3595                major_unit: 20.0,
3596                tick_label_position: ChartTickLabelPositionType::HIGH,
3597                num_fmt: ChartNumFmt {
3598                    custom_num_fmt: "0.00".to_string(),
3599                    source_linked: false,
3600                },
3601                ..Default::default()
3602            },
3603            ..Default::default()
3604        };
3605        f.add_chart("Sheet1", "E1", &chart).unwrap();
3606
3607        let charts = f.get_charts("Sheet1").unwrap();
3608        assert_eq!(charts.len(), 1);
3609        let x = &charts[0].x_axis;
3610        assert!(x.reverse_order);
3611        assert!(x.major_grid_lines);
3612        assert_eq!(x.maximum, Some(100.0));
3613        assert_eq!(x.minimum, Some(10.0));
3614        assert_eq!(x.title.paragraph.len(), 1);
3615        assert_eq!(x.title.paragraph[0].text, "Quarter");
3616
3617        let y = &charts[0].y_axis;
3618        assert_eq!(y.major_unit, 20.0);
3619        assert_eq!(y.tick_label_position, ChartTickLabelPositionType::HIGH);
3620        assert_eq!(y.num_fmt.custom_num_fmt, "0.00");
3621    }
3622
3623    #[test]
3624    fn chart_axis_round_trip_secondary() {
3625        let mut f = File::new_with_options(Options::default());
3626        f.set_cell_str("Sheet1", "A1", "Fruit").unwrap();
3627        f.set_cell_str("Sheet1", "B1", "Apple").unwrap();
3628        f.set_cell_str("Sheet1", "C1", "Orange").unwrap();
3629        f.set_cell_str("Sheet1", "A2", "Small").unwrap();
3630        f.set_cell_int("Sheet1", "B2", 2).unwrap();
3631        f.set_cell_int("Sheet1", "C2", 3).unwrap();
3632        f.set_cell_str("Sheet1", "A3", "Large").unwrap();
3633        f.set_cell_int("Sheet1", "B3", 500).unwrap();
3634        f.set_cell_int("Sheet1", "C3", 700).unwrap();
3635
3636        let mut combo = Chart {
3637            r#type: ChartType::LINE,
3638            series: vec![ChartSeries {
3639                name: "Sheet1!$A$3".to_string(),
3640                categories: "Sheet1!$B$1:$C$1".to_string(),
3641                values: "Sheet1!$B$3:$C$3".to_string(),
3642                ..Default::default()
3643            }],
3644            ..Default::default()
3645        };
3646        combo.y_axis.secondary = true;
3647        let chart = Chart {
3648            r#type: ChartType::COL,
3649            series: vec![ChartSeries {
3650                name: "Sheet1!$A$2".to_string(),
3651                categories: "Sheet1!$B$1:$C$1".to_string(),
3652                values: "Sheet1!$B$2:$C$2".to_string(),
3653                ..Default::default()
3654            }],
3655            combo: vec![combo],
3656            ..Default::default()
3657        };
3658        f.add_chart("Sheet1", "E1", &chart).unwrap();
3659
3660        let charts = f.get_charts("Sheet1").unwrap();
3661        assert_eq!(charts.len(), 1);
3662        assert!(charts[0].y_axis.secondary);
3663    }
3664
3665    #[test]
3666    fn chart_legend_round_trip() {
3667        let mut f = File::new_with_options(Options::default());
3668        f.set_cell_str("Sheet1", "A1", "Fruit").unwrap();
3669        f.set_cell_str("Sheet1", "B1", "Apple").unwrap();
3670        f.set_cell_str("Sheet1", "C1", "Orange").unwrap();
3671        f.set_cell_str("Sheet1", "A2", "Small").unwrap();
3672        f.set_cell_int("Sheet1", "B2", 2).unwrap();
3673        f.set_cell_int("Sheet1", "C2", 3).unwrap();
3674
3675        let chart = Chart {
3676            r#type: ChartType::COL,
3677            series: vec![ChartSeries {
3678                name: "Sheet1!$A$2".to_string(),
3679                categories: "Sheet1!$B$1:$C$1".to_string(),
3680                values: "Sheet1!$B$2:$C$2".to_string(),
3681                ..Default::default()
3682            }],
3683            legend: ChartLegend {
3684                position: "right".to_string(),
3685                show_legend_key: true,
3686                font: Some(crate::styles::Font {
3687                    size: Some(12.0),
3688                    italic: Some(true),
3689                    color: Some("FF0000".to_string()),
3690                    name: Some("Arial".to_string()),
3691                    ..Default::default()
3692                }),
3693            },
3694            ..Default::default()
3695        };
3696        f.add_chart("Sheet1", "E1", &chart).unwrap();
3697
3698        let charts = f.get_charts("Sheet1").unwrap();
3699        assert_eq!(charts.len(), 1);
3700        let legend = &charts[0].legend;
3701        assert_eq!(legend.position, "right");
3702        assert!(legend.show_legend_key);
3703        let font = legend.font.as_ref().expect("legend font");
3704        assert_eq!(font.size, Some(12.0));
3705        assert_eq!(font.italic, Some(true));
3706        assert_eq!(font.color.as_deref(), Some("FF0000"));
3707        assert_eq!(font.name.as_deref(), Some("Arial"));
3708    }
3709
3710    #[test]
3711    fn chart_title_format_round_trip() {
3712        let mut f = File::new_with_options(Options::default());
3713        f.set_cell_str("Sheet1", "A1", "Sales Report").unwrap();
3714        f.set_cell_str("Sheet1", "B1", "Apple").unwrap();
3715        f.set_cell_str("Sheet1", "A2", "Small").unwrap();
3716        f.set_cell_int("Sheet1", "B2", 2).unwrap();
3717
3718        let chart = Chart {
3719            r#type: ChartType::COL,
3720            series: vec![ChartSeries {
3721                name: "Sheet1!$A$2".to_string(),
3722                categories: "Sheet1!$B$1".to_string(),
3723                values: "Sheet1!$B$2".to_string(),
3724                ..Default::default()
3725            }],
3726            title: ChartTitle {
3727                formula: "Sheet1!$A$1".to_string(),
3728                overlay: true,
3729                font: Some(crate::styles::Font {
3730                    size: Some(16.0),
3731                    bold: Some(true),
3732                    color: Some("FF0000".to_string()),
3733                    ..Default::default()
3734                }),
3735                fill: crate::xml::drawing::Fill {
3736                    r#type: "pattern".to_string(),
3737                    pattern: 1,
3738                    color: vec!["#FF0000".to_string()],
3739                    ..Default::default()
3740                },
3741                border: crate::xml::drawing::LineOptions {
3742                    r#type: crate::xml::drawing::LineType::SOLID,
3743                    width: 1.0,
3744                    fill: crate::xml::drawing::Fill {
3745                        r#type: "pattern".to_string(),
3746                        pattern: 1,
3747                        color: vec!["#0000FF".to_string()],
3748                        ..Default::default()
3749                    },
3750                    ..Default::default()
3751                },
3752                ..Default::default()
3753            },
3754            ..Default::default()
3755        };
3756        f.add_chart("Sheet1", "E1", &chart).unwrap();
3757
3758        let charts = f.get_charts("Sheet1").unwrap();
3759        assert_eq!(charts.len(), 1);
3760        let title = &charts[0].title;
3761        assert_eq!(title.formula, "Sheet1!$A$1");
3762        assert!(title.overlay);
3763        assert_eq!(title.fill.color, vec!["#FF0000".to_string()]);
3764        assert_eq!(title.border.width, 1.0);
3765        assert_eq!(title.border.fill.color, vec!["#0000FF".to_string()]);
3766        let font = title.font.as_ref().expect("title font");
3767        assert_eq!(font.size, Some(16.0));
3768        assert_eq!(font.bold, Some(true));
3769        assert_eq!(font.color.as_deref(), Some("FF0000"));
3770    }
3771
3772    #[test]
3773    fn chart_plot_area_round_trip() {
3774        let mut f = File::new_with_options(Options::default());
3775        f.set_cell_str("Sheet1", "A1", "Fruit").unwrap();
3776        f.set_cell_str("Sheet1", "B1", "Apple").unwrap();
3777        f.set_cell_str("Sheet1", "C1", "Orange").unwrap();
3778        f.set_cell_str("Sheet1", "A2", "Small").unwrap();
3779        f.set_cell_int("Sheet1", "B2", 2).unwrap();
3780        f.set_cell_int("Sheet1", "C2", 3).unwrap();
3781
3782        let chart = Chart {
3783            r#type: ChartType::COL,
3784            series: vec![ChartSeries {
3785                name: "Sheet1!$A$2".to_string(),
3786                categories: "Sheet1!$B$1:$C$1".to_string(),
3787                values: "Sheet1!$B$2:$C$2".to_string(),
3788                ..Default::default()
3789            }],
3790            plot_area: ChartPlotArea {
3791                show_val: true,
3792                show_cat_name: true,
3793                show_percent: true,
3794                show_data_table: true,
3795                show_data_table_keys: true,
3796                fill: crate::xml::drawing::Fill {
3797                    r#type: "pattern".to_string(),
3798                    pattern: 1,
3799                    color: vec!["#DDEBF7".to_string()],
3800                    ..Default::default()
3801                },
3802                ..Default::default()
3803            },
3804            vary_colors: Some(false),
3805            gap_width: Some(200),
3806            overlap: Some(-20),
3807            show_blanks_as: "zero".to_string(),
3808            ..Default::default()
3809        };
3810        f.add_chart("Sheet1", "E1", &chart).unwrap();
3811
3812        let charts = f.get_charts("Sheet1").unwrap();
3813        assert_eq!(charts.len(), 1);
3814        let got = &charts[0];
3815        let pa = &got.plot_area;
3816        assert!(pa.show_val);
3817        assert!(pa.show_cat_name);
3818        assert!(pa.show_percent);
3819        assert!(!pa.show_ser_name);
3820        assert!(!pa.show_bubble_size);
3821        assert!(pa.show_data_table);
3822        assert!(pa.show_data_table_keys);
3823        assert_eq!(pa.fill.color, vec!["#DDEBF7".to_string()]);
3824        assert_eq!(got.vary_colors, Some(false));
3825        assert_eq!(got.gap_width, Some(200));
3826        assert_eq!(got.overlap, Some(-20));
3827        assert_eq!(got.show_blanks_as, "zero");
3828    }
3829
3830    #[test]
3831    fn chart_level_fill_border_round_trip() {
3832        let mut f = File::new_with_options(Options::default());
3833        f.set_cell_str("Sheet1", "A1", "Fruit").unwrap();
3834        f.set_cell_str("Sheet1", "B1", "Apple").unwrap();
3835        f.set_cell_str("Sheet1", "A2", "Small").unwrap();
3836        f.set_cell_int("Sheet1", "B2", 2).unwrap();
3837
3838        let chart = Chart {
3839            r#type: ChartType::COL,
3840            series: vec![ChartSeries {
3841                name: "Sheet1!$A$2".to_string(),
3842                categories: "Sheet1!$B$1".to_string(),
3843                values: "Sheet1!$B$2".to_string(),
3844                ..Default::default()
3845            }],
3846            fill: crate::xml::drawing::Fill {
3847                r#type: "pattern".to_string(),
3848                pattern: 1,
3849                color: vec!["#FFE699".to_string()],
3850                ..Default::default()
3851            },
3852            border: crate::xml::drawing::LineOptions {
3853                r#type: crate::xml::drawing::LineType::SOLID,
3854                width: 2.0,
3855                fill: crate::xml::drawing::Fill {
3856                    r#type: "pattern".to_string(),
3857                    pattern: 1,
3858                    color: vec!["#00B050".to_string()],
3859                    ..Default::default()
3860                },
3861                ..Default::default()
3862            },
3863            ..Default::default()
3864        };
3865        f.add_chart("Sheet1", "E1", &chart).unwrap();
3866
3867        let charts = f.get_charts("Sheet1").unwrap();
3868        assert_eq!(charts.len(), 1);
3869        let got = &charts[0];
3870        assert_eq!(got.fill.r#type, "pattern");
3871        assert_eq!(got.fill.pattern, 1);
3872        assert_eq!(got.fill.color, vec!["#FFE699".to_string()]);
3873        assert_eq!(got.border.r#type, crate::xml::drawing::LineType::SOLID);
3874        assert_eq!(got.border.width, 2.0);
3875        assert_eq!(got.border.fill.color, vec!["#00B050".to_string()]);
3876    }
3877
3878    #[test]
3879    fn chart_doughnut_hole_size_round_trip() {
3880        let mut f = File::new_with_options(Options::default());
3881        f.set_cell_str("Sheet1", "A1", "Fruit").unwrap();
3882        f.set_cell_str("Sheet1", "B1", "Apple").unwrap();
3883        f.set_cell_str("Sheet1", "A2", "Small").unwrap();
3884        f.set_cell_int("Sheet1", "B2", 2).unwrap();
3885
3886        let chart = Chart {
3887            r#type: ChartType::DOUGHNUT,
3888            series: vec![ChartSeries {
3889                name: "Sheet1!$A$2".to_string(),
3890                categories: "Sheet1!$B$1".to_string(),
3891                values: "Sheet1!$B$2".to_string(),
3892                ..Default::default()
3893            }],
3894            hole_size: 50,
3895            ..Default::default()
3896        };
3897        f.add_chart("Sheet1", "E1", &chart).unwrap();
3898
3899        let charts = f.get_charts("Sheet1").unwrap();
3900        assert_eq!(charts.len(), 1);
3901        assert_eq!(charts[0].r#type, ChartType::DOUGHNUT);
3902        assert_eq!(charts[0].hole_size, 50);
3903    }
3904
3905    #[test]
3906    fn chart_bubble_size_round_trip() {
3907        let mut f = File::new_with_options(Options::default());
3908        f.set_cell_str("Sheet1", "A1", "Fruit").unwrap();
3909        f.set_cell_str("Sheet1", "B1", "Apple").unwrap();
3910        f.set_cell_str("Sheet1", "A2", "Small").unwrap();
3911        f.set_cell_int("Sheet1", "B2", 2).unwrap();
3912
3913        let chart = Chart {
3914            r#type: ChartType::BUBBLE,
3915            series: vec![ChartSeries {
3916                name: "Sheet1!$A$2".to_string(),
3917                categories: "Sheet1!$B$1".to_string(),
3918                values: "Sheet1!$B$2".to_string(),
3919                ..Default::default()
3920            }],
3921            bubble_size: 150,
3922            ..Default::default()
3923        };
3924        f.add_chart("Sheet1", "E1", &chart).unwrap();
3925
3926        let charts = f.get_charts("Sheet1").unwrap();
3927        assert_eq!(charts.len(), 1);
3928        assert_eq!(charts[0].r#type, ChartType::BUBBLE);
3929        assert_eq!(charts[0].bubble_size, 150);
3930    }
3931
3932    #[test]
3933    fn chart_of_pie_second_plot_values_round_trip() {
3934        let mut f = File::new_with_options(Options::default());
3935        f.set_cell_str("Sheet1", "A1", "Fruit").unwrap();
3936        f.set_cell_str("Sheet1", "B1", "Apple").unwrap();
3937        f.set_cell_str("Sheet1", "A2", "Small").unwrap();
3938        f.set_cell_int("Sheet1", "B2", 2).unwrap();
3939
3940        let chart = Chart {
3941            r#type: ChartType::PIE_OF_PIE,
3942            series: vec![ChartSeries {
3943                name: "Sheet1!$A$2".to_string(),
3944                categories: "Sheet1!$B$1".to_string(),
3945                values: "Sheet1!$B$2".to_string(),
3946                ..Default::default()
3947            }],
3948            plot_area: ChartPlotArea {
3949                second_plot_values: 3,
3950                ..Default::default()
3951            },
3952            ..Default::default()
3953        };
3954        f.add_chart("Sheet1", "E1", &chart).unwrap();
3955
3956        let charts = f.get_charts("Sheet1").unwrap();
3957        assert_eq!(charts.len(), 1);
3958        assert_eq!(charts[0].plot_area.second_plot_values, 3);
3959    }
3960
3961    #[test]
3962    fn chart_type_round_trip_all_subtypes() {
3963        let types = [
3964            ChartType::AREA,
3965            ChartType::AREA_STACKED,
3966            ChartType::AREA_PERCENT_STACKED,
3967            ChartType::AREA_3D,
3968            ChartType::AREA_3D_STACKED,
3969            ChartType::AREA_3D_PERCENT_STACKED,
3970            ChartType::BAR,
3971            ChartType::BAR_STACKED,
3972            ChartType::BAR_PERCENT_STACKED,
3973            ChartType::BAR_3D_CLUSTERED,
3974            ChartType::BAR_3D_STACKED,
3975            ChartType::BAR_3D_PERCENT_STACKED,
3976            ChartType::BAR_3D_CONE_CLUSTERED,
3977            ChartType::BAR_3D_CONE_STACKED,
3978            ChartType::BAR_3D_CONE_PERCENT_STACKED,
3979            ChartType::BAR_3D_PYRAMID_CLUSTERED,
3980            ChartType::BAR_3D_PYRAMID_STACKED,
3981            ChartType::BAR_3D_PYRAMID_PERCENT_STACKED,
3982            ChartType::BAR_3D_CYLINDER_CLUSTERED,
3983            ChartType::BAR_3D_CYLINDER_STACKED,
3984            ChartType::BAR_3D_CYLINDER_PERCENT_STACKED,
3985            ChartType::COL,
3986            ChartType::COL_STACKED,
3987            ChartType::COL_PERCENT_STACKED,
3988            ChartType::COL_3D,
3989            ChartType::COL_3D_CLUSTERED,
3990            ChartType::COL_3D_STACKED,
3991            ChartType::COL_3D_PERCENT_STACKED,
3992            ChartType::COL_3D_CONE,
3993            ChartType::COL_3D_CONE_CLUSTERED,
3994            ChartType::COL_3D_CONE_STACKED,
3995            ChartType::COL_3D_CONE_PERCENT_STACKED,
3996            ChartType::COL_3D_PYRAMID,
3997            ChartType::COL_3D_PYRAMID_CLUSTERED,
3998            ChartType::COL_3D_PYRAMID_STACKED,
3999            ChartType::COL_3D_PYRAMID_PERCENT_STACKED,
4000            ChartType::COL_3D_CYLINDER,
4001            ChartType::COL_3D_CYLINDER_CLUSTERED,
4002            ChartType::COL_3D_CYLINDER_STACKED,
4003            ChartType::COL_3D_CYLINDER_PERCENT_STACKED,
4004            ChartType::DOUGHNUT,
4005            ChartType::LINE,
4006            ChartType::LINE_3D,
4007            ChartType::PIE,
4008            ChartType::PIE_3D,
4009            ChartType::PIE_OF_PIE,
4010            ChartType::BAR_OF_PIE,
4011            ChartType::RADAR,
4012            ChartType::SCATTER,
4013            ChartType::SURFACE_3D,
4014            ChartType::WIREFRAME_SURFACE_3D,
4015            ChartType::CONTOUR,
4016            ChartType::WIREFRAME_CONTOUR,
4017            ChartType::BUBBLE,
4018            ChartType::STOCK_HIGH_LOW_CLOSE,
4019            ChartType::STOCK_OPEN_HIGH_LOW_CLOSE,
4020        ];
4021        for chart_type in types {
4022            let mut f = File::new_with_options(Options::default());
4023            f.set_cell_str("Sheet1", "A1", "Fruit").unwrap();
4024            f.set_cell_str("Sheet1", "B1", "Apple").unwrap();
4025            f.set_cell_str("Sheet1", "C1", "Orange").unwrap();
4026            f.set_cell_str("Sheet1", "A2", "Small").unwrap();
4027            f.set_cell_int("Sheet1", "B2", 2).unwrap();
4028            f.set_cell_int("Sheet1", "C2", 3).unwrap();
4029            f.set_cell_str("Sheet1", "A3", "Large").unwrap();
4030            f.set_cell_int("Sheet1", "B3", 5).unwrap();
4031            f.set_cell_int("Sheet1", "C3", 7).unwrap();
4032
4033            let chart = Chart {
4034                r#type: chart_type,
4035                series: vec![
4036                    ChartSeries {
4037                        name: "Sheet1!$A$2".to_string(),
4038                        categories: "Sheet1!$B$1:$C$1".to_string(),
4039                        values: "Sheet1!$B$2:$C$2".to_string(),
4040                        ..Default::default()
4041                    },
4042                    ChartSeries {
4043                        name: "Sheet1!$A$3".to_string(),
4044                        categories: "Sheet1!$B$1:$C$1".to_string(),
4045                        values: "Sheet1!$B$3:$C$3".to_string(),
4046                        ..Default::default()
4047                    },
4048                ],
4049                ..Default::default()
4050            };
4051            f.add_chart("Sheet1", "E1", &chart).unwrap();
4052
4053            let charts = f.get_charts("Sheet1").unwrap();
4054            assert_eq!(charts.len(), 1, "chart type {chart_type:?}");
4055            assert_eq!(charts[0].r#type, chart_type, "chart type {chart_type:?}");
4056        }
4057    }
4058
4059    #[test]
4060    fn combo_chart_series_merged_on_readback() {
4061        let mut f = File::new_with_options(Options::default());
4062        f.set_cell_str("Sheet1", "A1", "Fruit").unwrap();
4063        f.set_cell_str("Sheet1", "B1", "Apple").unwrap();
4064        f.set_cell_str("Sheet1", "C1", "Orange").unwrap();
4065        f.set_cell_str("Sheet1", "A2", "Small").unwrap();
4066        f.set_cell_int("Sheet1", "B2", 2).unwrap();
4067        f.set_cell_int("Sheet1", "C2", 3).unwrap();
4068        f.set_cell_str("Sheet1", "A3", "Large").unwrap();
4069        f.set_cell_int("Sheet1", "B3", 5).unwrap();
4070        f.set_cell_int("Sheet1", "C3", 7).unwrap();
4071        f.set_cell_str("Sheet1", "A4", "Total").unwrap();
4072        f.set_cell_int("Sheet1", "B4", 7).unwrap();
4073        f.set_cell_int("Sheet1", "C4", 10).unwrap();
4074
4075        let combo = Chart {
4076            r#type: ChartType::LINE,
4077            series: vec![ChartSeries {
4078                name: "Sheet1!$A$4".to_string(),
4079                categories: "Sheet1!$B$1:$C$1".to_string(),
4080                values: "Sheet1!$B$4:$C$4".to_string(),
4081                ..Default::default()
4082            }],
4083            ..Default::default()
4084        };
4085        let chart = Chart {
4086            r#type: ChartType::COL,
4087            series: vec![
4088                ChartSeries {
4089                    name: "Sheet1!$A$2".to_string(),
4090                    categories: "Sheet1!$B$1:$C$1".to_string(),
4091                    values: "Sheet1!$B$2:$C$2".to_string(),
4092                    ..Default::default()
4093                },
4094                ChartSeries {
4095                    name: "Sheet1!$A$3".to_string(),
4096                    categories: "Sheet1!$B$1:$C$1".to_string(),
4097                    values: "Sheet1!$B$3:$C$3".to_string(),
4098                    ..Default::default()
4099                },
4100            ],
4101            combo: vec![combo],
4102            ..Default::default()
4103        };
4104        f.add_chart("Sheet1", "E1", &chart).unwrap();
4105
4106        let charts = f.get_charts("Sheet1").unwrap();
4107        assert_eq!(charts.len(), 1);
4108        assert_eq!(charts[0].r#type, ChartType::COL);
4109        assert_eq!(charts[0].series.len(), 3);
4110        assert_eq!(charts[0].series[0].name, "Sheet1!$A$2");
4111        assert_eq!(charts[0].series[1].name, "Sheet1!$A$3");
4112        assert_eq!(charts[0].series[2].name, "Sheet1!$A$4");
4113        assert_eq!(charts[0].series[2].values, "Sheet1!$B$4:$C$4");
4114    }
4115
4116    #[test]
4117    fn chart_series_line_dash_round_trip() {
4118        let mut f = File::new_with_options(Options::default());
4119        f.set_cell_str("Sheet1", "A1", "Month").unwrap();
4120        f.set_cell_str("Sheet1", "B1", "Jan").unwrap();
4121        f.set_cell_str("Sheet1", "A2", "Sales").unwrap();
4122        f.set_cell_int("Sheet1", "B2", 10).unwrap();
4123
4124        let chart = Chart {
4125            r#type: ChartType::LINE,
4126            series: vec![ChartSeries {
4127                name: "Sheet1!$A$2".to_string(),
4128                categories: "Sheet1!$B$1".to_string(),
4129                values: "Sheet1!$B$2".to_string(),
4130                line: crate::xml::drawing::LineOptions {
4131                    r#type: crate::xml::drawing::LineType::SOLID,
4132                    width: 1.5,
4133                    dash: crate::xml::drawing::LineDashType::DASH,
4134                    smooth: true,
4135                    ..Default::default()
4136                },
4137                ..Default::default()
4138            }],
4139            ..Default::default()
4140        };
4141        f.add_chart("Sheet1", "E1", &chart).unwrap();
4142
4143        let charts = f.get_charts("Sheet1").unwrap();
4144        assert_eq!(charts.len(), 1);
4145        let line = &charts[0].series[0].line;
4146        assert_eq!(line.r#type, crate::xml::drawing::LineType::SOLID);
4147        assert_eq!(line.width, 1.5);
4148        assert_eq!(line.dash, crate::xml::drawing::LineDashType::DASH);
4149        assert!(line.smooth);
4150    }
4151
4152    #[test]
4153    fn chart_series_data_label_round_trip() {
4154        let mut f = File::new_with_options(Options::default());
4155        f.set_cell_str("Sheet1", "A1", "Fruit").unwrap();
4156        f.set_cell_str("Sheet1", "B1", "Apple").unwrap();
4157        f.set_cell_str("Sheet1", "A2", "Small").unwrap();
4158        f.set_cell_int("Sheet1", "B2", 2).unwrap();
4159
4160        let chart = Chart {
4161            r#type: ChartType::COL,
4162            series: vec![ChartSeries {
4163                name: "Sheet1!$A$2".to_string(),
4164                categories: "Sheet1!$B$1".to_string(),
4165                values: "Sheet1!$B$2".to_string(),
4166                data_label: crate::xml::chart::ChartDataLabel {
4167                    fill: crate::xml::drawing::Fill {
4168                        r#type: "pattern".to_string(),
4169                        pattern: 1,
4170                        color: vec!["#FFFF00".to_string()],
4171                        ..Default::default()
4172                    },
4173                    font: crate::styles::Font {
4174                        size: Some(10.0),
4175                        bold: Some(true),
4176                        color: Some("FF0000".to_string()),
4177                        name: Some("Arial".to_string()),
4178                        ..Default::default()
4179                    },
4180                    ..Default::default()
4181                },
4182                ..Default::default()
4183            }],
4184            ..Default::default()
4185        };
4186        f.add_chart("Sheet1", "E1", &chart).unwrap();
4187
4188        let charts = f.get_charts("Sheet1").unwrap();
4189        assert_eq!(charts.len(), 1);
4190        let label = &charts[0].series[0].data_label;
4191        assert_eq!(label.fill.color, vec!["#FFFF00".to_string()]);
4192        assert_eq!(label.font.size, Some(10.0));
4193        assert_eq!(label.font.bold, Some(true));
4194        assert_eq!(label.font.color.as_deref(), Some("FF0000"));
4195        assert_eq!(label.font.name.as_deref(), Some("Arial"));
4196    }
4197
4198}