Skip to main content

sheetkit_core/
chart.rs

1//! Chart builder and management.
2//!
3//! Provides types for configuring charts and functions for building the
4//! corresponding XML structures (ChartSpace and drawing anchors).
5
6use sheetkit_xml::chart::{
7    Area3DChart, AreaChart, Bar3DChart, BarChart, BodyPr, BoolVal, BubbleChart, BubbleSeries,
8    CatAx, CategoryRef, Chart, ChartSpace, ChartTitle, DoughnutChart, IntVal, Layout, Legend,
9    Line3DChart, LineChart, NumRef, OfPieChart, Paragraph, Pie3DChart, PieChart, PlotArea,
10    RadarChart, RichText, Run, Scaling, ScatterChart, ScatterSeries, SerAx, SerLines, Series,
11    SeriesText, StockChart, StrRef, StringVal, Surface3DChart, SurfaceChart, TitleTx, UintVal,
12    ValAx, ValueRef, View3D,
13};
14use sheetkit_xml::drawing::{
15    AExt, CNvGraphicFramePr, CNvPr, ChartRef, ClientData, Graphic, GraphicData, GraphicFrame,
16    MarkerType, NvGraphicFramePr, Offset, TwoCellAnchor, WsDr, Xfrm,
17};
18use sheetkit_xml::namespaces;
19
20/// The chart type to render.
21#[derive(Debug, Clone, PartialEq)]
22pub enum ChartType {
23    /// Vertical bar chart (column).
24    Col,
25    /// Vertical bar chart, stacked.
26    ColStacked,
27    /// Vertical bar chart, percent stacked.
28    ColPercentStacked,
29    /// Horizontal bar chart.
30    Bar,
31    /// Horizontal bar chart, stacked.
32    BarStacked,
33    /// Horizontal bar chart, percent stacked.
34    BarPercentStacked,
35    /// Line chart.
36    Line,
37    /// Pie chart.
38    Pie,
39    /// Area chart.
40    Area,
41    /// Area chart, stacked.
42    AreaStacked,
43    /// Area chart, percent stacked.
44    AreaPercentStacked,
45    /// 3D area chart.
46    Area3D,
47    /// 3D area chart, stacked.
48    Area3DStacked,
49    /// 3D area chart, percent stacked.
50    Area3DPercentStacked,
51    /// 3D column chart.
52    Col3D,
53    /// 3D column chart, stacked.
54    Col3DStacked,
55    /// 3D column chart, percent stacked.
56    Col3DPercentStacked,
57    /// 3D horizontal bar chart.
58    Bar3D,
59    /// 3D horizontal bar chart, stacked.
60    Bar3DStacked,
61    /// 3D horizontal bar chart, percent stacked.
62    Bar3DPercentStacked,
63    /// Line chart, stacked.
64    LineStacked,
65    /// Line chart, percent stacked.
66    LinePercentStacked,
67    /// 3D line chart.
68    Line3D,
69    /// 3D pie chart.
70    Pie3D,
71    /// Doughnut chart.
72    Doughnut,
73    /// Scatter chart (markers only).
74    Scatter,
75    /// Scatter chart with straight lines.
76    ScatterLine,
77    /// Scatter chart with smooth lines.
78    ScatterSmooth,
79    /// Radar chart (standard).
80    Radar,
81    /// Radar chart with filled area.
82    RadarFilled,
83    /// Radar chart with markers.
84    RadarMarker,
85    /// Stock chart: High-Low-Close.
86    StockHLC,
87    /// Stock chart: Open-High-Low-Close.
88    StockOHLC,
89    /// Stock chart: Volume-High-Low-Close.
90    StockVHLC,
91    /// Stock chart: Volume-Open-High-Low-Close.
92    StockVOHLC,
93    /// Bubble chart.
94    Bubble,
95    /// Surface chart.
96    Surface,
97    /// 3D surface chart.
98    Surface3D,
99    /// Wireframe surface chart.
100    SurfaceWireframe,
101    /// Wireframe 3D surface chart.
102    SurfaceWireframe3D,
103    /// Combo chart: column + line.
104    ColLine,
105    /// Combo chart: column stacked + line.
106    ColLineStacked,
107    /// Combo chart: column percent stacked + line.
108    ColLinePercentStacked,
109    /// Pie-of-pie chart.
110    PieOfPie,
111    /// Bar-of-pie chart.
112    BarOfPie,
113    /// 3D cone column chart.
114    Col3DCone,
115    /// 3D cone column chart, stacked.
116    Col3DConeStacked,
117    /// 3D cone column chart, percent stacked.
118    Col3DConePercentStacked,
119    /// 3D pyramid column chart.
120    Col3DPyramid,
121    /// 3D pyramid column chart, stacked.
122    Col3DPyramidStacked,
123    /// 3D pyramid column chart, percent stacked.
124    Col3DPyramidPercentStacked,
125    /// 3D cylinder column chart.
126    Col3DCylinder,
127    /// 3D cylinder column chart, stacked.
128    Col3DCylinderStacked,
129    /// 3D cylinder column chart, percent stacked.
130    Col3DCylinderPercentStacked,
131    /// Contour chart (surface chart as 2D projection).
132    Contour,
133    /// Wireframe contour chart.
134    WireframeContour,
135    /// 3D bubble chart.
136    Bubble3D,
137}
138
139/// 3D view configuration for charts.
140#[derive(Debug, Clone, Default)]
141pub struct View3DConfig {
142    /// X-axis rotation angle in degrees.
143    pub rot_x: Option<i32>,
144    /// Y-axis rotation angle in degrees.
145    pub rot_y: Option<i32>,
146    /// Depth as a percentage (100 = normal depth).
147    pub depth_percent: Option<u32>,
148    /// Whether to use right angle axes.
149    pub right_angle_axes: Option<bool>,
150    /// Perspective angle in degrees.
151    pub perspective: Option<u32>,
152}
153
154/// Configuration for a chart.
155#[derive(Debug, Clone)]
156pub struct ChartConfig {
157    /// The type of chart.
158    pub chart_type: ChartType,
159    /// Optional chart title.
160    pub title: Option<String>,
161    /// Data series for the chart.
162    pub series: Vec<ChartSeries>,
163    /// Whether to show the legend.
164    pub show_legend: bool,
165    /// Optional 3D view settings (auto-populated for 3D chart types if not set).
166    pub view_3d: Option<View3DConfig>,
167}
168
169/// A single data series within a chart.
170#[derive(Debug, Clone)]
171pub struct ChartSeries {
172    /// Series name (a literal string or cell reference like `"Sheet1!$A$1"`).
173    pub name: String,
174    /// Category axis data range (e.g., `"Sheet1!$A$2:$A$6"`).
175    pub categories: String,
176    /// Value axis data range (e.g., `"Sheet1!$B$2:$B$6"`).
177    pub values: String,
178    /// X-axis values for scatter/bubble charts (e.g., `"Sheet1!$A$2:$A$6"`).
179    pub x_values: Option<String>,
180    /// Bubble sizes for bubble charts (e.g., `"Sheet1!$C$2:$C$6"`).
181    pub bubble_sizes: Option<String>,
182}
183
184/// Build a `ChartSpace` XML structure from a chart configuration.
185pub fn build_chart_xml(config: &ChartConfig) -> ChartSpace {
186    let title = config.title.as_ref().map(|t| build_chart_title(t));
187    let legend = if config.show_legend {
188        Some(Legend {
189            legend_pos: StringVal {
190                val: "b".to_string(),
191            },
192        })
193    } else {
194        None
195    };
196    let view_3d = build_view_3d(config);
197    let plot_area = build_plot_area(config);
198
199    ChartSpace {
200        chart: Chart {
201            title,
202            view_3d,
203            plot_area,
204            legend,
205            plot_vis_only: Some(BoolVal { val: true }),
206        },
207        ..ChartSpace::default()
208    }
209}
210
211/// Build a drawing XML structure containing a chart reference.
212pub fn build_drawing_with_chart(chart_ref_id: &str, from: MarkerType, to: MarkerType) -> WsDr {
213    let graphic_frame = GraphicFrame {
214        nv_graphic_frame_pr: NvGraphicFramePr {
215            c_nv_pr: CNvPr {
216                id: 2,
217                name: "Chart 1".to_string(),
218            },
219            c_nv_graphic_frame_pr: CNvGraphicFramePr {},
220        },
221        xfrm: Xfrm {
222            off: Offset { x: 0, y: 0 },
223            ext: AExt { cx: 0, cy: 0 },
224        },
225        graphic: Graphic {
226            graphic_data: GraphicData {
227                uri: namespaces::DRAWING_ML_CHART.to_string(),
228                chart: ChartRef {
229                    xmlns_c: namespaces::DRAWING_ML_CHART.to_string(),
230                    r_id: chart_ref_id.to_string(),
231                },
232            },
233        },
234    };
235    let anchor = TwoCellAnchor {
236        from,
237        to,
238        graphic_frame: Some(graphic_frame),
239        pic: None,
240        shape: None,
241        client_data: ClientData {},
242    };
243    WsDr {
244        two_cell_anchors: vec![anchor],
245        ..WsDr::default()
246    }
247}
248
249fn is_no_axis_chart(ct: &ChartType) -> bool {
250    matches!(
251        ct,
252        ChartType::Pie
253            | ChartType::Pie3D
254            | ChartType::Doughnut
255            | ChartType::PieOfPie
256            | ChartType::BarOfPie
257    )
258}
259
260fn is_3d_chart(ct: &ChartType) -> bool {
261    matches!(
262        ct,
263        ChartType::Area3D
264            | ChartType::Area3DStacked
265            | ChartType::Area3DPercentStacked
266            | ChartType::Col3D
267            | ChartType::Col3DStacked
268            | ChartType::Col3DPercentStacked
269            | ChartType::Bar3D
270            | ChartType::Bar3DStacked
271            | ChartType::Bar3DPercentStacked
272            | ChartType::Line3D
273            | ChartType::Pie3D
274            | ChartType::Surface3D
275            | ChartType::SurfaceWireframe3D
276            | ChartType::Col3DCone
277            | ChartType::Col3DConeStacked
278            | ChartType::Col3DConePercentStacked
279            | ChartType::Col3DPyramid
280            | ChartType::Col3DPyramidStacked
281            | ChartType::Col3DPyramidPercentStacked
282            | ChartType::Col3DCylinder
283            | ChartType::Col3DCylinderStacked
284            | ChartType::Col3DCylinderPercentStacked
285    )
286}
287
288fn needs_ser_ax(ct: &ChartType) -> bool {
289    matches!(
290        ct,
291        ChartType::Surface
292            | ChartType::Surface3D
293            | ChartType::SurfaceWireframe
294            | ChartType::SurfaceWireframe3D
295            | ChartType::Contour
296            | ChartType::WireframeContour
297    )
298}
299
300fn build_view_3d(config: &ChartConfig) -> Option<View3D> {
301    if let Some(v) = &config.view_3d {
302        return Some(View3D {
303            rot_x: v.rot_x.map(|val| IntVal { val }),
304            rot_y: v.rot_y.map(|val| IntVal { val }),
305            depth_percent: v.depth_percent.map(|val| UintVal { val }),
306            r_ang_ax: v.right_angle_axes.map(|val| BoolVal { val }),
307            perspective: v.perspective.map(|val| UintVal { val }),
308        });
309    }
310    if is_3d_chart(&config.chart_type) {
311        Some(View3D {
312            rot_x: Some(IntVal { val: 15 }),
313            rot_y: Some(IntVal { val: 20 }),
314            depth_percent: None,
315            r_ang_ax: Some(BoolVal { val: true }),
316            perspective: Some(UintVal { val: 30 }),
317        })
318    } else {
319        None
320    }
321}
322
323fn build_series_text(series: &ChartSeries) -> Option<SeriesText> {
324    if series.name.is_empty() {
325        None
326    } else if series.name.contains('!') {
327        Some(SeriesText {
328            str_ref: Some(StrRef {
329                f: series.name.clone(),
330            }),
331            v: None,
332        })
333    } else {
334        Some(SeriesText {
335            str_ref: None,
336            v: Some(series.name.clone()),
337        })
338    }
339}
340
341fn build_series(index: u32, series: &ChartSeries) -> Series {
342    let tx = build_series_text(series);
343    let cat = if series.categories.is_empty() {
344        None
345    } else {
346        Some(CategoryRef {
347            str_ref: Some(StrRef {
348                f: series.categories.clone(),
349            }),
350            num_ref: None,
351        })
352    };
353    let val = if series.values.is_empty() {
354        None
355    } else {
356        Some(ValueRef {
357            num_ref: Some(NumRef {
358                f: series.values.clone(),
359            }),
360        })
361    };
362    Series {
363        idx: UintVal { val: index },
364        order: UintVal { val: index },
365        tx,
366        cat,
367        val,
368    }
369}
370
371fn build_scatter_series(index: u32, series: &ChartSeries) -> ScatterSeries {
372    let tx = build_series_text(series);
373    let x_val = series
374        .x_values
375        .as_ref()
376        .or(if series.categories.is_empty() {
377            None
378        } else {
379            Some(&series.categories)
380        })
381        .map(|ref_str| CategoryRef {
382            str_ref: None,
383            num_ref: Some(NumRef { f: ref_str.clone() }),
384        });
385    let y_val = if series.values.is_empty() {
386        None
387    } else {
388        Some(ValueRef {
389            num_ref: Some(NumRef {
390                f: series.values.clone(),
391            }),
392        })
393    };
394    ScatterSeries {
395        idx: UintVal { val: index },
396        order: UintVal { val: index },
397        tx,
398        x_val,
399        y_val,
400    }
401}
402
403fn build_bubble_series(index: u32, series: &ChartSeries, is_3d: bool) -> BubbleSeries {
404    let tx = build_series_text(series);
405    let x_val = series
406        .x_values
407        .as_ref()
408        .or(if series.categories.is_empty() {
409            None
410        } else {
411            Some(&series.categories)
412        })
413        .map(|ref_str| CategoryRef {
414            str_ref: None,
415            num_ref: Some(NumRef { f: ref_str.clone() }),
416        });
417    let y_val = if series.values.is_empty() {
418        None
419    } else {
420        Some(ValueRef {
421            num_ref: Some(NumRef {
422                f: series.values.clone(),
423            }),
424        })
425    };
426    let bubble_size = series.bubble_sizes.as_ref().map(|ref_str| ValueRef {
427        num_ref: Some(NumRef { f: ref_str.clone() }),
428    });
429    BubbleSeries {
430        idx: UintVal { val: index },
431        order: UintVal { val: index },
432        tx,
433        x_val,
434        y_val,
435        bubble_size,
436        bubble_3d: if is_3d {
437            Some(BoolVal { val: true })
438        } else {
439            None
440        },
441    }
442}
443
444fn build_chart_title(text: &str) -> ChartTitle {
445    ChartTitle {
446        tx: TitleTx {
447            rich: RichText {
448                body_pr: BodyPr {},
449                paragraphs: vec![Paragraph {
450                    runs: vec![Run {
451                        t: text.to_string(),
452                    }],
453                }],
454            },
455        },
456    }
457}
458
459fn build_standard_axes() -> (Option<CatAx>, Option<ValAx>) {
460    (
461        Some(CatAx {
462            ax_id: UintVal { val: 1 },
463            scaling: Scaling {
464                orientation: StringVal {
465                    val: "minMax".to_string(),
466                },
467            },
468            delete: BoolVal { val: false },
469            ax_pos: StringVal {
470                val: "b".to_string(),
471            },
472            cross_ax: UintVal { val: 2 },
473        }),
474        Some(ValAx {
475            ax_id: UintVal { val: 2 },
476            scaling: Scaling {
477                orientation: StringVal {
478                    val: "minMax".to_string(),
479                },
480            },
481            delete: BoolVal { val: false },
482            ax_pos: StringVal {
483                val: "l".to_string(),
484            },
485            cross_ax: UintVal { val: 1 },
486        }),
487    )
488}
489
490fn build_ser_ax() -> SerAx {
491    SerAx {
492        ax_id: UintVal { val: 3 },
493        scaling: Scaling {
494            orientation: StringVal {
495                val: "minMax".to_string(),
496            },
497        },
498        delete: BoolVal { val: false },
499        ax_pos: StringVal {
500            val: "b".to_string(),
501        },
502        cross_ax: UintVal { val: 1 },
503    }
504}
505
506fn standard_ax_ids() -> Vec<UintVal> {
507    vec![UintVal { val: 1 }, UintVal { val: 2 }]
508}
509
510fn surface_ax_ids() -> Vec<UintVal> {
511    vec![UintVal { val: 1 }, UintVal { val: 2 }, UintVal { val: 3 }]
512}
513
514fn build_plot_area(config: &ChartConfig) -> PlotArea {
515    let ct = &config.chart_type;
516    let no_axes = is_no_axis_chart(ct);
517    let (cat_ax, val_ax) = if no_axes {
518        (None, None)
519    } else {
520        build_standard_axes()
521    };
522    let ser_ax = if needs_ser_ax(ct) {
523        Some(build_ser_ax())
524    } else {
525        None
526    };
527    let ax_ids = if no_axes {
528        vec![]
529    } else if needs_ser_ax(ct) {
530        surface_ax_ids()
531    } else {
532        standard_ax_ids()
533    };
534
535    let xml_series: Vec<Series> = config
536        .series
537        .iter()
538        .enumerate()
539        .map(|(i, s)| build_series(i as u32, s))
540        .collect();
541
542    let mut plot_area = PlotArea {
543        layout: Some(Layout {}),
544        bar_chart: None,
545        bar_3d_chart: None,
546        line_chart: None,
547        line_3d_chart: None,
548        pie_chart: None,
549        pie_3d_chart: None,
550        doughnut_chart: None,
551        area_chart: None,
552        area_3d_chart: None,
553        scatter_chart: None,
554        bubble_chart: None,
555        radar_chart: None,
556        stock_chart: None,
557        surface_chart: None,
558        surface_3d_chart: None,
559        of_pie_chart: None,
560        cat_ax,
561        val_ax,
562        ser_ax,
563    };
564
565    match ct {
566        ChartType::Col => {
567            plot_area.bar_chart = Some(BarChart {
568                bar_dir: StringVal { val: "col".into() },
569                grouping: StringVal {
570                    val: "clustered".into(),
571                },
572                series: xml_series,
573                ax_ids,
574            });
575        }
576        ChartType::ColStacked => {
577            plot_area.bar_chart = Some(BarChart {
578                bar_dir: StringVal { val: "col".into() },
579                grouping: StringVal {
580                    val: "stacked".into(),
581                },
582                series: xml_series,
583                ax_ids,
584            });
585        }
586        ChartType::ColPercentStacked => {
587            plot_area.bar_chart = Some(BarChart {
588                bar_dir: StringVal { val: "col".into() },
589                grouping: StringVal {
590                    val: "percentStacked".into(),
591                },
592                series: xml_series,
593                ax_ids,
594            });
595        }
596        ChartType::Bar => {
597            plot_area.bar_chart = Some(BarChart {
598                bar_dir: StringVal { val: "bar".into() },
599                grouping: StringVal {
600                    val: "clustered".into(),
601                },
602                series: xml_series,
603                ax_ids,
604            });
605        }
606        ChartType::BarStacked => {
607            plot_area.bar_chart = Some(BarChart {
608                bar_dir: StringVal { val: "bar".into() },
609                grouping: StringVal {
610                    val: "stacked".into(),
611                },
612                series: xml_series,
613                ax_ids,
614            });
615        }
616        ChartType::BarPercentStacked => {
617            plot_area.bar_chart = Some(BarChart {
618                bar_dir: StringVal { val: "bar".into() },
619                grouping: StringVal {
620                    val: "percentStacked".into(),
621                },
622                series: xml_series,
623                ax_ids,
624            });
625        }
626        ChartType::Line => {
627            plot_area.line_chart = Some(LineChart {
628                grouping: StringVal {
629                    val: "standard".into(),
630                },
631                series: xml_series,
632                ax_ids,
633            });
634        }
635        ChartType::LineStacked => {
636            plot_area.line_chart = Some(LineChart {
637                grouping: StringVal {
638                    val: "stacked".into(),
639                },
640                series: xml_series,
641                ax_ids,
642            });
643        }
644        ChartType::LinePercentStacked => {
645            plot_area.line_chart = Some(LineChart {
646                grouping: StringVal {
647                    val: "percentStacked".into(),
648                },
649                series: xml_series,
650                ax_ids,
651            });
652        }
653        ChartType::Line3D => {
654            plot_area.line_3d_chart = Some(Line3DChart {
655                grouping: StringVal {
656                    val: "standard".into(),
657                },
658                series: xml_series,
659                ax_ids,
660            });
661        }
662        ChartType::Pie => {
663            plot_area.pie_chart = Some(PieChart { series: xml_series });
664        }
665        ChartType::Pie3D => {
666            plot_area.pie_3d_chart = Some(Pie3DChart { series: xml_series });
667        }
668        ChartType::Doughnut => {
669            plot_area.doughnut_chart = Some(DoughnutChart {
670                series: xml_series,
671                hole_size: Some(UintVal { val: 50 }),
672            });
673        }
674        ChartType::Area => {
675            plot_area.area_chart = Some(AreaChart {
676                grouping: StringVal {
677                    val: "standard".into(),
678                },
679                series: xml_series,
680                ax_ids,
681            });
682        }
683        ChartType::AreaStacked => {
684            plot_area.area_chart = Some(AreaChart {
685                grouping: StringVal {
686                    val: "stacked".into(),
687                },
688                series: xml_series,
689                ax_ids,
690            });
691        }
692        ChartType::AreaPercentStacked => {
693            plot_area.area_chart = Some(AreaChart {
694                grouping: StringVal {
695                    val: "percentStacked".into(),
696                },
697                series: xml_series,
698                ax_ids,
699            });
700        }
701        ChartType::Area3D => {
702            plot_area.area_3d_chart = Some(Area3DChart {
703                grouping: StringVal {
704                    val: "standard".into(),
705                },
706                series: xml_series,
707                ax_ids,
708            });
709        }
710        ChartType::Area3DStacked => {
711            plot_area.area_3d_chart = Some(Area3DChart {
712                grouping: StringVal {
713                    val: "stacked".into(),
714                },
715                series: xml_series,
716                ax_ids,
717            });
718        }
719        ChartType::Area3DPercentStacked => {
720            plot_area.area_3d_chart = Some(Area3DChart {
721                grouping: StringVal {
722                    val: "percentStacked".into(),
723                },
724                series: xml_series,
725                ax_ids,
726            });
727        }
728        ChartType::Col3D => {
729            plot_area.bar_3d_chart = Some(Bar3DChart {
730                bar_dir: StringVal { val: "col".into() },
731                grouping: StringVal {
732                    val: "clustered".into(),
733                },
734                series: xml_series,
735                shape: None,
736                ax_ids,
737            });
738        }
739        ChartType::Col3DStacked => {
740            plot_area.bar_3d_chart = Some(Bar3DChart {
741                bar_dir: StringVal { val: "col".into() },
742                grouping: StringVal {
743                    val: "stacked".into(),
744                },
745                series: xml_series,
746                shape: None,
747                ax_ids,
748            });
749        }
750        ChartType::Col3DPercentStacked => {
751            plot_area.bar_3d_chart = Some(Bar3DChart {
752                bar_dir: StringVal { val: "col".into() },
753                grouping: StringVal {
754                    val: "percentStacked".into(),
755                },
756                series: xml_series,
757                shape: None,
758                ax_ids,
759            });
760        }
761        ChartType::Bar3D => {
762            plot_area.bar_3d_chart = Some(Bar3DChart {
763                bar_dir: StringVal { val: "bar".into() },
764                grouping: StringVal {
765                    val: "clustered".into(),
766                },
767                series: xml_series,
768                shape: None,
769                ax_ids,
770            });
771        }
772        ChartType::Bar3DStacked => {
773            plot_area.bar_3d_chart = Some(Bar3DChart {
774                bar_dir: StringVal { val: "bar".into() },
775                grouping: StringVal {
776                    val: "stacked".into(),
777                },
778                series: xml_series,
779                shape: None,
780                ax_ids,
781            });
782        }
783        ChartType::Bar3DPercentStacked => {
784            plot_area.bar_3d_chart = Some(Bar3DChart {
785                bar_dir: StringVal { val: "bar".into() },
786                grouping: StringVal {
787                    val: "percentStacked".into(),
788                },
789                series: xml_series,
790                shape: None,
791                ax_ids,
792            });
793        }
794        ChartType::Scatter => {
795            let ss: Vec<ScatterSeries> = config
796                .series
797                .iter()
798                .enumerate()
799                .map(|(i, s)| build_scatter_series(i as u32, s))
800                .collect();
801            plot_area.scatter_chart = Some(ScatterChart {
802                scatter_style: StringVal {
803                    val: "lineMarker".into(),
804                },
805                series: ss,
806                ax_ids,
807            });
808        }
809        ChartType::ScatterLine => {
810            let ss: Vec<ScatterSeries> = config
811                .series
812                .iter()
813                .enumerate()
814                .map(|(i, s)| build_scatter_series(i as u32, s))
815                .collect();
816            plot_area.scatter_chart = Some(ScatterChart {
817                scatter_style: StringVal { val: "line".into() },
818                series: ss,
819                ax_ids,
820            });
821        }
822        ChartType::ScatterSmooth => {
823            let ss: Vec<ScatterSeries> = config
824                .series
825                .iter()
826                .enumerate()
827                .map(|(i, s)| build_scatter_series(i as u32, s))
828                .collect();
829            plot_area.scatter_chart = Some(ScatterChart {
830                scatter_style: StringVal {
831                    val: "smoothMarker".into(),
832                },
833                series: ss,
834                ax_ids,
835            });
836        }
837        ChartType::Bubble => {
838            let bs: Vec<BubbleSeries> = config
839                .series
840                .iter()
841                .enumerate()
842                .map(|(i, s)| build_bubble_series(i as u32, s, false))
843                .collect();
844            plot_area.bubble_chart = Some(BubbleChart { series: bs, ax_ids });
845        }
846        ChartType::Bubble3D => {
847            let bs: Vec<BubbleSeries> = config
848                .series
849                .iter()
850                .enumerate()
851                .map(|(i, s)| build_bubble_series(i as u32, s, true))
852                .collect();
853            plot_area.bubble_chart = Some(BubbleChart { series: bs, ax_ids });
854        }
855        ChartType::Radar => {
856            plot_area.radar_chart = Some(RadarChart {
857                radar_style: StringVal {
858                    val: "standard".into(),
859                },
860                series: xml_series,
861                ax_ids,
862            });
863        }
864        ChartType::RadarFilled => {
865            plot_area.radar_chart = Some(RadarChart {
866                radar_style: StringVal {
867                    val: "filled".into(),
868                },
869                series: xml_series,
870                ax_ids,
871            });
872        }
873        ChartType::RadarMarker => {
874            plot_area.radar_chart = Some(RadarChart {
875                radar_style: StringVal {
876                    val: "marker".into(),
877                },
878                series: xml_series,
879                ax_ids,
880            });
881        }
882        ChartType::StockHLC
883        | ChartType::StockOHLC
884        | ChartType::StockVHLC
885        | ChartType::StockVOHLC => {
886            plot_area.stock_chart = Some(StockChart {
887                series: xml_series,
888                ax_ids,
889            });
890        }
891        ChartType::Surface => {
892            plot_area.surface_chart = Some(SurfaceChart {
893                wireframe: None,
894                series: xml_series,
895                ax_ids,
896            });
897        }
898        ChartType::SurfaceWireframe => {
899            plot_area.surface_chart = Some(SurfaceChart {
900                wireframe: Some(BoolVal { val: true }),
901                series: xml_series,
902                ax_ids,
903            });
904        }
905        ChartType::Surface3D => {
906            plot_area.surface_3d_chart = Some(Surface3DChart {
907                wireframe: None,
908                series: xml_series,
909                ax_ids,
910            });
911        }
912        ChartType::SurfaceWireframe3D => {
913            plot_area.surface_3d_chart = Some(Surface3DChart {
914                wireframe: Some(BoolVal { val: true }),
915                series: xml_series,
916                ax_ids,
917            });
918        }
919        ChartType::PieOfPie => {
920            plot_area.of_pie_chart = Some(OfPieChart {
921                of_pie_type: StringVal { val: "pie".into() },
922                series: xml_series,
923                ser_lines: Some(SerLines {}),
924            });
925        }
926        ChartType::BarOfPie => {
927            plot_area.of_pie_chart = Some(OfPieChart {
928                of_pie_type: StringVal { val: "bar".into() },
929                series: xml_series,
930                ser_lines: Some(SerLines {}),
931            });
932        }
933        ChartType::Col3DCone => {
934            plot_area.bar_3d_chart = Some(Bar3DChart {
935                bar_dir: StringVal { val: "col".into() },
936                grouping: StringVal {
937                    val: "clustered".into(),
938                },
939                series: xml_series,
940                shape: Some(StringVal { val: "cone".into() }),
941                ax_ids,
942            });
943        }
944        ChartType::Col3DConeStacked => {
945            plot_area.bar_3d_chart = Some(Bar3DChart {
946                bar_dir: StringVal { val: "col".into() },
947                grouping: StringVal {
948                    val: "stacked".into(),
949                },
950                series: xml_series,
951                shape: Some(StringVal { val: "cone".into() }),
952                ax_ids,
953            });
954        }
955        ChartType::Col3DConePercentStacked => {
956            plot_area.bar_3d_chart = Some(Bar3DChart {
957                bar_dir: StringVal { val: "col".into() },
958                grouping: StringVal {
959                    val: "percentStacked".into(),
960                },
961                series: xml_series,
962                shape: Some(StringVal { val: "cone".into() }),
963                ax_ids,
964            });
965        }
966        ChartType::Col3DPyramid => {
967            plot_area.bar_3d_chart = Some(Bar3DChart {
968                bar_dir: StringVal { val: "col".into() },
969                grouping: StringVal {
970                    val: "clustered".into(),
971                },
972                series: xml_series,
973                shape: Some(StringVal {
974                    val: "pyramid".into(),
975                }),
976                ax_ids,
977            });
978        }
979        ChartType::Col3DPyramidStacked => {
980            plot_area.bar_3d_chart = Some(Bar3DChart {
981                bar_dir: StringVal { val: "col".into() },
982                grouping: StringVal {
983                    val: "stacked".into(),
984                },
985                series: xml_series,
986                shape: Some(StringVal {
987                    val: "pyramid".into(),
988                }),
989                ax_ids,
990            });
991        }
992        ChartType::Col3DPyramidPercentStacked => {
993            plot_area.bar_3d_chart = Some(Bar3DChart {
994                bar_dir: StringVal { val: "col".into() },
995                grouping: StringVal {
996                    val: "percentStacked".into(),
997                },
998                series: xml_series,
999                shape: Some(StringVal {
1000                    val: "pyramid".into(),
1001                }),
1002                ax_ids,
1003            });
1004        }
1005        ChartType::Col3DCylinder => {
1006            plot_area.bar_3d_chart = Some(Bar3DChart {
1007                bar_dir: StringVal { val: "col".into() },
1008                grouping: StringVal {
1009                    val: "clustered".into(),
1010                },
1011                series: xml_series,
1012                shape: Some(StringVal {
1013                    val: "cylinder".into(),
1014                }),
1015                ax_ids,
1016            });
1017        }
1018        ChartType::Col3DCylinderStacked => {
1019            plot_area.bar_3d_chart = Some(Bar3DChart {
1020                bar_dir: StringVal { val: "col".into() },
1021                grouping: StringVal {
1022                    val: "stacked".into(),
1023                },
1024                series: xml_series,
1025                shape: Some(StringVal {
1026                    val: "cylinder".into(),
1027                }),
1028                ax_ids,
1029            });
1030        }
1031        ChartType::Col3DCylinderPercentStacked => {
1032            plot_area.bar_3d_chart = Some(Bar3DChart {
1033                bar_dir: StringVal { val: "col".into() },
1034                grouping: StringVal {
1035                    val: "percentStacked".into(),
1036                },
1037                series: xml_series,
1038                shape: Some(StringVal {
1039                    val: "cylinder".into(),
1040                }),
1041                ax_ids,
1042            });
1043        }
1044        ChartType::Contour => {
1045            plot_area.surface_chart = Some(SurfaceChart {
1046                wireframe: Some(BoolVal { val: false }),
1047                series: xml_series,
1048                ax_ids,
1049            });
1050        }
1051        ChartType::WireframeContour => {
1052            plot_area.surface_chart = Some(SurfaceChart {
1053                wireframe: Some(BoolVal { val: true }),
1054                series: xml_series,
1055                ax_ids,
1056            });
1057        }
1058        ChartType::ColLine | ChartType::ColLineStacked | ChartType::ColLinePercentStacked => {
1059            let grouping = match ct {
1060                ChartType::ColLineStacked => "stacked",
1061                ChartType::ColLinePercentStacked => "percentStacked",
1062                _ => "clustered",
1063            };
1064            let total = xml_series.len();
1065            let bar_count = total.div_ceil(2);
1066            let bar_series: Vec<Series> = xml_series.iter().take(bar_count).cloned().collect();
1067            let line_series: Vec<Series> = xml_series.iter().skip(bar_count).cloned().collect();
1068            plot_area.bar_chart = Some(BarChart {
1069                bar_dir: StringVal { val: "col".into() },
1070                grouping: StringVal {
1071                    val: grouping.into(),
1072                },
1073                series: bar_series,
1074                ax_ids: ax_ids.clone(),
1075            });
1076            plot_area.line_chart = Some(LineChart {
1077                grouping: StringVal {
1078                    val: "standard".into(),
1079                },
1080                series: line_series,
1081                ax_ids,
1082            });
1083        }
1084    }
1085
1086    plot_area
1087}
1088
1089#[cfg(test)]
1090mod tests {
1091    use super::*;
1092
1093    fn ss() -> Vec<ChartSeries> {
1094        vec![ChartSeries {
1095            name: "Revenue".into(),
1096            categories: "Sheet1!$A$2:$A$6".into(),
1097            values: "Sheet1!$B$2:$B$6".into(),
1098            x_values: None,
1099            bubble_sizes: None,
1100        }]
1101    }
1102
1103    fn mc(chart_type: ChartType) -> ChartConfig {
1104        ChartConfig {
1105            chart_type,
1106            title: None,
1107            series: ss(),
1108            show_legend: false,
1109            view_3d: None,
1110        }
1111    }
1112
1113    #[test]
1114    fn test_build_chart_xml_col() {
1115        let config = ChartConfig {
1116            chart_type: ChartType::Col,
1117            title: Some("Sales Chart".into()),
1118            series: ss(),
1119            show_legend: true,
1120            view_3d: None,
1121        };
1122        let cs = build_chart_xml(&config);
1123        assert!(cs.chart.title.is_some());
1124        assert!(cs.chart.legend.is_some());
1125        assert!(cs.chart.plot_area.bar_chart.is_some());
1126        assert!(cs.chart.plot_area.line_chart.is_none());
1127        assert!(cs.chart.plot_area.pie_chart.is_none());
1128        assert!(cs.chart.view_3d.is_none());
1129        let bar = cs.chart.plot_area.bar_chart.unwrap();
1130        assert_eq!(bar.bar_dir.val, "col");
1131        assert_eq!(bar.grouping.val, "clustered");
1132        assert_eq!(bar.series.len(), 1);
1133        assert_eq!(bar.ax_ids.len(), 2);
1134    }
1135
1136    #[test]
1137    fn test_build_chart_xml_bar() {
1138        let cs = build_chart_xml(&ChartConfig {
1139            chart_type: ChartType::Bar,
1140            title: None,
1141            series: vec![],
1142            show_legend: false,
1143            view_3d: None,
1144        });
1145        assert!(cs.chart.title.is_none());
1146        assert!(cs.chart.legend.is_none());
1147        let bar = cs.chart.plot_area.bar_chart.unwrap();
1148        assert_eq!(bar.bar_dir.val, "bar");
1149        assert_eq!(bar.grouping.val, "clustered");
1150    }
1151
1152    #[test]
1153    fn test_bar_stacked() {
1154        let cs = build_chart_xml(&mc(ChartType::BarStacked));
1155        let bar = cs.chart.plot_area.bar_chart.unwrap();
1156        assert_eq!(bar.bar_dir.val, "bar");
1157        assert_eq!(bar.grouping.val, "stacked");
1158    }
1159
1160    #[test]
1161    fn test_col_stacked() {
1162        let cs = build_chart_xml(&mc(ChartType::ColStacked));
1163        let bar = cs.chart.plot_area.bar_chart.unwrap();
1164        assert_eq!(bar.bar_dir.val, "col");
1165        assert_eq!(bar.grouping.val, "stacked");
1166    }
1167
1168    #[test]
1169    fn test_col_percent_stacked() {
1170        let cs = build_chart_xml(&mc(ChartType::ColPercentStacked));
1171        let bar = cs.chart.plot_area.bar_chart.unwrap();
1172        assert_eq!(bar.grouping.val, "percentStacked");
1173    }
1174
1175    #[test]
1176    fn test_bar_percent_stacked() {
1177        let cs = build_chart_xml(&mc(ChartType::BarPercentStacked));
1178        let bar = cs.chart.plot_area.bar_chart.unwrap();
1179        assert_eq!(bar.bar_dir.val, "bar");
1180        assert_eq!(bar.grouping.val, "percentStacked");
1181    }
1182
1183    #[test]
1184    fn test_line() {
1185        let cs = build_chart_xml(&ChartConfig {
1186            chart_type: ChartType::Line,
1187            title: Some("Trend".into()),
1188            series: vec![ChartSeries {
1189                name: "Sheet1!$A$1".into(),
1190                categories: "Sheet1!$A$2:$A$6".into(),
1191                values: "Sheet1!$B$2:$B$6".into(),
1192                x_values: None,
1193                bubble_sizes: None,
1194            }],
1195            show_legend: true,
1196            view_3d: None,
1197        });
1198        assert!(cs.chart.plot_area.line_chart.is_some());
1199        let line = cs.chart.plot_area.line_chart.unwrap();
1200        assert_eq!(line.grouping.val, "standard");
1201        assert_eq!(line.series.len(), 1);
1202        let tx = line.series[0].tx.as_ref().unwrap();
1203        assert!(tx.str_ref.is_some());
1204        assert!(tx.v.is_none());
1205    }
1206
1207    #[test]
1208    fn test_pie() {
1209        let cs = build_chart_xml(&ChartConfig {
1210            chart_type: ChartType::Pie,
1211            title: Some("Distribution".into()),
1212            series: vec![ChartSeries {
1213                name: "Data".into(),
1214                categories: "Sheet1!$A$2:$A$6".into(),
1215                values: "Sheet1!$B$2:$B$6".into(),
1216                x_values: None,
1217                bubble_sizes: None,
1218            }],
1219            show_legend: true,
1220            view_3d: None,
1221        });
1222        assert!(cs.chart.plot_area.pie_chart.is_some());
1223        assert!(cs.chart.plot_area.cat_ax.is_none());
1224        assert!(cs.chart.plot_area.val_ax.is_none());
1225        let pie = cs.chart.plot_area.pie_chart.unwrap();
1226        let tx = pie.series[0].tx.as_ref().unwrap();
1227        assert!(tx.str_ref.is_none());
1228        assert_eq!(tx.v.as_deref(), Some("Data"));
1229    }
1230
1231    #[test]
1232    fn test_no_legend() {
1233        let cs = build_chart_xml(&mc(ChartType::Col));
1234        assert!(cs.chart.legend.is_none());
1235    }
1236
1237    #[test]
1238    fn test_axes_present_for_non_pie() {
1239        let cs = build_chart_xml(&mc(ChartType::Line));
1240        assert!(cs.chart.plot_area.cat_ax.is_some());
1241        assert!(cs.chart.plot_area.val_ax.is_some());
1242    }
1243
1244    #[test]
1245    fn test_drawing_with_chart() {
1246        let from = MarkerType {
1247            col: 1,
1248            col_off: 0,
1249            row: 1,
1250            row_off: 0,
1251        };
1252        let to = MarkerType {
1253            col: 10,
1254            col_off: 0,
1255            row: 15,
1256            row_off: 0,
1257        };
1258        let dr = build_drawing_with_chart("rId1", from, to);
1259        assert_eq!(dr.two_cell_anchors.len(), 1);
1260        let anchor = &dr.two_cell_anchors[0];
1261        assert!(anchor.graphic_frame.is_some());
1262        assert_eq!(anchor.from.col, 1);
1263        assert_eq!(anchor.to.col, 10);
1264        let gf = anchor.graphic_frame.as_ref().unwrap();
1265        assert_eq!(gf.graphic.graphic_data.chart.r_id, "rId1");
1266    }
1267
1268    #[test]
1269    fn test_series_literal_name() {
1270        let s = ChartSeries {
1271            name: "MyName".into(),
1272            categories: "Sheet1!$A$2:$A$6".into(),
1273            values: "Sheet1!$B$2:$B$6".into(),
1274            x_values: None,
1275            bubble_sizes: None,
1276        };
1277        let xs = build_series(0, &s);
1278        let tx = xs.tx.as_ref().unwrap();
1279        assert!(tx.str_ref.is_none());
1280        assert_eq!(tx.v.as_deref(), Some("MyName"));
1281    }
1282
1283    #[test]
1284    fn test_series_cell_ref_name() {
1285        let s = ChartSeries {
1286            name: "Sheet1!$C$1".into(),
1287            categories: "".into(),
1288            values: "Sheet1!$B$2:$B$6".into(),
1289            x_values: None,
1290            bubble_sizes: None,
1291        };
1292        let xs = build_series(0, &s);
1293        let tx = xs.tx.as_ref().unwrap();
1294        assert!(tx.str_ref.is_some());
1295        assert!(tx.v.is_none());
1296        assert!(xs.cat.is_none());
1297    }
1298
1299    #[test]
1300    fn test_series_empty_name() {
1301        let s = ChartSeries {
1302            name: "".into(),
1303            categories: "Sheet1!$A$2:$A$6".into(),
1304            values: "Sheet1!$B$2:$B$6".into(),
1305            x_values: None,
1306            bubble_sizes: None,
1307        };
1308        let xs = build_series(0, &s);
1309        assert!(xs.tx.is_none());
1310    }
1311
1312    #[test]
1313    fn test_multiple_series() {
1314        let cs = build_chart_xml(&ChartConfig {
1315            chart_type: ChartType::Col,
1316            title: None,
1317            series: vec![
1318                ChartSeries {
1319                    name: "A".into(),
1320                    categories: "Sheet1!$A$2:$A$6".into(),
1321                    values: "Sheet1!$B$2:$B$6".into(),
1322                    x_values: None,
1323                    bubble_sizes: None,
1324                },
1325                ChartSeries {
1326                    name: "B".into(),
1327                    categories: "Sheet1!$A$2:$A$6".into(),
1328                    values: "Sheet1!$C$2:$C$6".into(),
1329                    x_values: None,
1330                    bubble_sizes: None,
1331                },
1332            ],
1333            show_legend: true,
1334            view_3d: None,
1335        });
1336        let bar = cs.chart.plot_area.bar_chart.unwrap();
1337        assert_eq!(bar.series.len(), 2);
1338        assert_eq!(bar.series[0].idx.val, 0);
1339        assert_eq!(bar.series[1].idx.val, 1);
1340    }
1341
1342    #[test]
1343    fn test_area_chart() {
1344        let cs = build_chart_xml(&mc(ChartType::Area));
1345        assert!(cs.chart.plot_area.area_chart.is_some());
1346        let a = cs.chart.plot_area.area_chart.unwrap();
1347        assert_eq!(a.grouping.val, "standard");
1348        assert_eq!(a.ax_ids.len(), 2);
1349        assert!(cs.chart.view_3d.is_none());
1350    }
1351
1352    #[test]
1353    fn test_area_stacked() {
1354        let cs = build_chart_xml(&mc(ChartType::AreaStacked));
1355        assert_eq!(
1356            cs.chart.plot_area.area_chart.unwrap().grouping.val,
1357            "stacked"
1358        );
1359    }
1360
1361    #[test]
1362    fn test_area_percent_stacked() {
1363        let cs = build_chart_xml(&mc(ChartType::AreaPercentStacked));
1364        assert_eq!(
1365            cs.chart.plot_area.area_chart.unwrap().grouping.val,
1366            "percentStacked"
1367        );
1368    }
1369
1370    #[test]
1371    fn test_area_3d() {
1372        let cs = build_chart_xml(&mc(ChartType::Area3D));
1373        assert!(cs.chart.view_3d.is_some());
1374        assert_eq!(
1375            cs.chart.plot_area.area_3d_chart.unwrap().grouping.val,
1376            "standard"
1377        );
1378    }
1379
1380    #[test]
1381    fn test_area_3d_stacked() {
1382        let cs = build_chart_xml(&mc(ChartType::Area3DStacked));
1383        assert!(cs.chart.view_3d.is_some());
1384        assert_eq!(
1385            cs.chart.plot_area.area_3d_chart.unwrap().grouping.val,
1386            "stacked"
1387        );
1388    }
1389
1390    #[test]
1391    fn test_area_3d_percent_stacked() {
1392        let cs = build_chart_xml(&mc(ChartType::Area3DPercentStacked));
1393        assert!(cs.chart.view_3d.is_some());
1394        assert_eq!(
1395            cs.chart.plot_area.area_3d_chart.unwrap().grouping.val,
1396            "percentStacked"
1397        );
1398    }
1399
1400    #[test]
1401    fn test_col_3d() {
1402        let cs = build_chart_xml(&mc(ChartType::Col3D));
1403        assert!(cs.chart.view_3d.is_some());
1404        let b = cs.chart.plot_area.bar_3d_chart.unwrap();
1405        assert_eq!(b.bar_dir.val, "col");
1406        assert_eq!(b.grouping.val, "clustered");
1407    }
1408
1409    #[test]
1410    fn test_col_3d_stacked() {
1411        let cs = build_chart_xml(&mc(ChartType::Col3DStacked));
1412        assert_eq!(
1413            cs.chart.plot_area.bar_3d_chart.unwrap().grouping.val,
1414            "stacked"
1415        );
1416    }
1417
1418    #[test]
1419    fn test_col_3d_percent_stacked() {
1420        let cs = build_chart_xml(&mc(ChartType::Col3DPercentStacked));
1421        assert_eq!(
1422            cs.chart.plot_area.bar_3d_chart.unwrap().grouping.val,
1423            "percentStacked"
1424        );
1425    }
1426
1427    #[test]
1428    fn test_bar_3d() {
1429        let cs = build_chart_xml(&mc(ChartType::Bar3D));
1430        assert!(cs.chart.view_3d.is_some());
1431        let b = cs.chart.plot_area.bar_3d_chart.unwrap();
1432        assert_eq!(b.bar_dir.val, "bar");
1433        assert_eq!(b.grouping.val, "clustered");
1434    }
1435
1436    #[test]
1437    fn test_bar_3d_stacked() {
1438        let cs = build_chart_xml(&mc(ChartType::Bar3DStacked));
1439        assert_eq!(
1440            cs.chart.plot_area.bar_3d_chart.unwrap().grouping.val,
1441            "stacked"
1442        );
1443    }
1444
1445    #[test]
1446    fn test_bar_3d_percent_stacked() {
1447        let cs = build_chart_xml(&mc(ChartType::Bar3DPercentStacked));
1448        assert_eq!(
1449            cs.chart.plot_area.bar_3d_chart.unwrap().grouping.val,
1450            "percentStacked"
1451        );
1452    }
1453
1454    #[test]
1455    fn test_line_stacked() {
1456        let cs = build_chart_xml(&mc(ChartType::LineStacked));
1457        assert_eq!(
1458            cs.chart.plot_area.line_chart.unwrap().grouping.val,
1459            "stacked"
1460        );
1461    }
1462
1463    #[test]
1464    fn test_line_percent_stacked() {
1465        let cs = build_chart_xml(&mc(ChartType::LinePercentStacked));
1466        assert_eq!(
1467            cs.chart.plot_area.line_chart.unwrap().grouping.val,
1468            "percentStacked"
1469        );
1470    }
1471
1472    #[test]
1473    fn test_line_3d() {
1474        let cs = build_chart_xml(&mc(ChartType::Line3D));
1475        assert!(cs.chart.view_3d.is_some());
1476        assert!(cs.chart.plot_area.line_3d_chart.is_some());
1477    }
1478
1479    #[test]
1480    fn test_pie_3d() {
1481        let cs = build_chart_xml(&mc(ChartType::Pie3D));
1482        assert!(cs.chart.view_3d.is_some());
1483        assert!(cs.chart.plot_area.pie_3d_chart.is_some());
1484        assert!(cs.chart.plot_area.cat_ax.is_none());
1485    }
1486
1487    #[test]
1488    fn test_doughnut() {
1489        let cs = build_chart_xml(&mc(ChartType::Doughnut));
1490        assert!(cs.chart.plot_area.doughnut_chart.is_some());
1491        assert!(cs.chart.plot_area.cat_ax.is_none());
1492        let d = cs.chart.plot_area.doughnut_chart.unwrap();
1493        assert_eq!(d.hole_size.as_ref().unwrap().val, 50);
1494    }
1495
1496    #[test]
1497    fn test_scatter() {
1498        let cs = build_chart_xml(&ChartConfig {
1499            chart_type: ChartType::Scatter,
1500            title: None,
1501            series: vec![ChartSeries {
1502                name: "XY".into(),
1503                categories: "Sheet1!$A$2:$A$6".into(),
1504                values: "Sheet1!$B$2:$B$6".into(),
1505                x_values: None,
1506                bubble_sizes: None,
1507            }],
1508            show_legend: false,
1509            view_3d: None,
1510        });
1511        let sc = cs.chart.plot_area.scatter_chart.unwrap();
1512        assert_eq!(sc.scatter_style.val, "lineMarker");
1513        assert_eq!(sc.series.len(), 1);
1514        let s = &sc.series[0];
1515        assert_eq!(
1516            s.x_val.as_ref().unwrap().num_ref.as_ref().unwrap().f,
1517            "Sheet1!$A$2:$A$6"
1518        );
1519        assert_eq!(
1520            s.y_val.as_ref().unwrap().num_ref.as_ref().unwrap().f,
1521            "Sheet1!$B$2:$B$6"
1522        );
1523    }
1524
1525    #[test]
1526    fn test_scatter_explicit_x() {
1527        let cs = build_chart_xml(&ChartConfig {
1528            chart_type: ChartType::Scatter,
1529            title: None,
1530            series: vec![ChartSeries {
1531                name: "".into(),
1532                categories: "Sheet1!$A$2:$A$6".into(),
1533                values: "Sheet1!$B$2:$B$6".into(),
1534                x_values: Some("Sheet1!$D$2:$D$6".into()),
1535                bubble_sizes: None,
1536            }],
1537            show_legend: false,
1538            view_3d: None,
1539        });
1540        let s = &cs.chart.plot_area.scatter_chart.unwrap().series[0];
1541        assert_eq!(
1542            s.x_val.as_ref().unwrap().num_ref.as_ref().unwrap().f,
1543            "Sheet1!$D$2:$D$6"
1544        );
1545    }
1546
1547    #[test]
1548    fn test_scatter_line() {
1549        let cs = build_chart_xml(&mc(ChartType::ScatterLine));
1550        assert_eq!(
1551            cs.chart.plot_area.scatter_chart.unwrap().scatter_style.val,
1552            "line"
1553        );
1554    }
1555
1556    #[test]
1557    fn test_scatter_smooth() {
1558        let cs = build_chart_xml(&mc(ChartType::ScatterSmooth));
1559        assert_eq!(
1560            cs.chart.plot_area.scatter_chart.unwrap().scatter_style.val,
1561            "smoothMarker"
1562        );
1563    }
1564
1565    #[test]
1566    fn test_bubble() {
1567        let cs = build_chart_xml(&ChartConfig {
1568            chart_type: ChartType::Bubble,
1569            title: None,
1570            series: vec![ChartSeries {
1571                name: "B".into(),
1572                categories: "Sheet1!$A$2:$A$6".into(),
1573                values: "Sheet1!$B$2:$B$6".into(),
1574                x_values: None,
1575                bubble_sizes: Some("Sheet1!$C$2:$C$6".into()),
1576            }],
1577            show_legend: false,
1578            view_3d: None,
1579        });
1580        let b = cs.chart.plot_area.bubble_chart.unwrap();
1581        assert_eq!(b.series.len(), 1);
1582        assert_eq!(
1583            b.series[0]
1584                .bubble_size
1585                .as_ref()
1586                .unwrap()
1587                .num_ref
1588                .as_ref()
1589                .unwrap()
1590                .f,
1591            "Sheet1!$C$2:$C$6"
1592        );
1593    }
1594
1595    #[test]
1596    fn test_radar() {
1597        let cs = build_chart_xml(&mc(ChartType::Radar));
1598        assert_eq!(
1599            cs.chart.plot_area.radar_chart.unwrap().radar_style.val,
1600            "standard"
1601        );
1602    }
1603
1604    #[test]
1605    fn test_radar_filled() {
1606        let cs = build_chart_xml(&mc(ChartType::RadarFilled));
1607        assert_eq!(
1608            cs.chart.plot_area.radar_chart.unwrap().radar_style.val,
1609            "filled"
1610        );
1611    }
1612
1613    #[test]
1614    fn test_radar_marker() {
1615        let cs = build_chart_xml(&mc(ChartType::RadarMarker));
1616        assert_eq!(
1617            cs.chart.plot_area.radar_chart.unwrap().radar_style.val,
1618            "marker"
1619        );
1620    }
1621
1622    #[test]
1623    fn test_stock_hlc() {
1624        let cs = build_chart_xml(&mc(ChartType::StockHLC));
1625        assert!(cs.chart.plot_area.stock_chart.is_some());
1626    }
1627
1628    #[test]
1629    fn test_stock_ohlc() {
1630        let cs = build_chart_xml(&mc(ChartType::StockOHLC));
1631        assert!(cs.chart.plot_area.stock_chart.is_some());
1632    }
1633
1634    #[test]
1635    fn test_stock_vhlc() {
1636        let cs = build_chart_xml(&mc(ChartType::StockVHLC));
1637        assert!(cs.chart.plot_area.stock_chart.is_some());
1638    }
1639
1640    #[test]
1641    fn test_stock_vohlc() {
1642        let cs = build_chart_xml(&mc(ChartType::StockVOHLC));
1643        assert!(cs.chart.plot_area.stock_chart.is_some());
1644    }
1645
1646    #[test]
1647    fn test_surface() {
1648        let cs = build_chart_xml(&mc(ChartType::Surface));
1649        assert!(cs.chart.plot_area.surface_chart.is_some());
1650        assert!(cs.chart.plot_area.ser_ax.is_some());
1651        let sf = cs.chart.plot_area.surface_chart.unwrap();
1652        assert!(sf.wireframe.is_none());
1653        assert_eq!(sf.ax_ids.len(), 3);
1654    }
1655
1656    #[test]
1657    fn test_surface_wireframe() {
1658        let cs = build_chart_xml(&mc(ChartType::SurfaceWireframe));
1659        let sf = cs.chart.plot_area.surface_chart.unwrap();
1660        assert!(sf.wireframe.as_ref().unwrap().val);
1661        assert_eq!(sf.ax_ids.len(), 3);
1662    }
1663
1664    #[test]
1665    fn test_surface_3d() {
1666        let cs = build_chart_xml(&mc(ChartType::Surface3D));
1667        assert!(cs.chart.view_3d.is_some());
1668        assert!(cs.chart.plot_area.surface_3d_chart.is_some());
1669        assert!(cs.chart.plot_area.ser_ax.is_some());
1670    }
1671
1672    #[test]
1673    fn test_surface_wireframe_3d() {
1674        let cs = build_chart_xml(&mc(ChartType::SurfaceWireframe3D));
1675        assert!(cs.chart.view_3d.is_some());
1676        let sf = cs.chart.plot_area.surface_3d_chart.unwrap();
1677        assert!(sf.wireframe.as_ref().unwrap().val);
1678    }
1679
1680    #[test]
1681    fn test_col_line_combo() {
1682        let cs = build_chart_xml(&ChartConfig {
1683            chart_type: ChartType::ColLine,
1684            title: None,
1685            series: vec![
1686                ChartSeries {
1687                    name: "A".into(),
1688                    categories: "Sheet1!$A$2:$A$6".into(),
1689                    values: "Sheet1!$B$2:$B$6".into(),
1690                    x_values: None,
1691                    bubble_sizes: None,
1692                },
1693                ChartSeries {
1694                    name: "B".into(),
1695                    categories: "Sheet1!$A$2:$A$6".into(),
1696                    values: "Sheet1!$C$2:$C$6".into(),
1697                    x_values: None,
1698                    bubble_sizes: None,
1699                },
1700            ],
1701            show_legend: true,
1702            view_3d: None,
1703        });
1704        assert!(cs.chart.plot_area.bar_chart.is_some());
1705        assert!(cs.chart.plot_area.line_chart.is_some());
1706        let bar = cs.chart.plot_area.bar_chart.unwrap();
1707        assert_eq!(bar.grouping.val, "clustered");
1708        assert_eq!(bar.series.len(), 1);
1709        assert_eq!(cs.chart.plot_area.line_chart.unwrap().series.len(), 1);
1710    }
1711
1712    #[test]
1713    fn test_col_line_stacked_combo() {
1714        let cs = build_chart_xml(&ChartConfig {
1715            chart_type: ChartType::ColLineStacked,
1716            title: None,
1717            series: vec![
1718                ChartSeries {
1719                    name: "A".into(),
1720                    categories: "".into(),
1721                    values: "Sheet1!$B$2:$B$6".into(),
1722                    x_values: None,
1723                    bubble_sizes: None,
1724                },
1725                ChartSeries {
1726                    name: "B".into(),
1727                    categories: "".into(),
1728                    values: "Sheet1!$C$2:$C$6".into(),
1729                    x_values: None,
1730                    bubble_sizes: None,
1731                },
1732                ChartSeries {
1733                    name: "C".into(),
1734                    categories: "".into(),
1735                    values: "Sheet1!$D$2:$D$6".into(),
1736                    x_values: None,
1737                    bubble_sizes: None,
1738                },
1739            ],
1740            show_legend: false,
1741            view_3d: None,
1742        });
1743        let bar = cs.chart.plot_area.bar_chart.unwrap();
1744        assert_eq!(bar.grouping.val, "stacked");
1745        assert_eq!(bar.series.len(), 2);
1746        assert_eq!(cs.chart.plot_area.line_chart.unwrap().series.len(), 1);
1747    }
1748
1749    #[test]
1750    fn test_col_line_percent_stacked_combo() {
1751        let cs = build_chart_xml(&ChartConfig {
1752            chart_type: ChartType::ColLinePercentStacked,
1753            title: None,
1754            series: vec![ChartSeries {
1755                name: "A".into(),
1756                categories: "".into(),
1757                values: "Sheet1!$B$2:$B$6".into(),
1758                x_values: None,
1759                bubble_sizes: None,
1760            }],
1761            show_legend: false,
1762            view_3d: None,
1763        });
1764        let bar = cs.chart.plot_area.bar_chart.unwrap();
1765        assert_eq!(bar.grouping.val, "percentStacked");
1766        assert_eq!(bar.series.len(), 1);
1767        assert_eq!(cs.chart.plot_area.line_chart.unwrap().series.len(), 0);
1768    }
1769
1770    #[test]
1771    fn test_view_3d_explicit() {
1772        let cs = build_chart_xml(&ChartConfig {
1773            chart_type: ChartType::Col3D,
1774            title: None,
1775            series: vec![],
1776            show_legend: false,
1777            view_3d: Some(View3DConfig {
1778                rot_x: Some(30),
1779                rot_y: Some(40),
1780                depth_percent: Some(200),
1781                right_angle_axes: Some(false),
1782                perspective: Some(10),
1783            }),
1784        });
1785        let v = cs.chart.view_3d.unwrap();
1786        assert_eq!(v.rot_x.unwrap().val, 30);
1787        assert_eq!(v.rot_y.unwrap().val, 40);
1788        assert_eq!(v.depth_percent.unwrap().val, 200);
1789        assert!(!v.r_ang_ax.unwrap().val);
1790        assert_eq!(v.perspective.unwrap().val, 10);
1791    }
1792
1793    #[test]
1794    fn test_view_3d_auto_defaults() {
1795        let cs = build_chart_xml(&mc(ChartType::Col3D));
1796        let v = cs.chart.view_3d.unwrap();
1797        assert_eq!(v.rot_x.unwrap().val, 15);
1798        assert_eq!(v.rot_y.unwrap().val, 20);
1799        assert!(v.r_ang_ax.unwrap().val);
1800        assert_eq!(v.perspective.unwrap().val, 30);
1801    }
1802
1803    #[test]
1804    fn test_non_3d_no_view() {
1805        let cs = build_chart_xml(&mc(ChartType::Col));
1806        assert!(cs.chart.view_3d.is_none());
1807    }
1808
1809    #[test]
1810    fn test_chart_type_enum_coverage() {
1811        let types = [
1812            ChartType::Col,
1813            ChartType::ColStacked,
1814            ChartType::ColPercentStacked,
1815            ChartType::Bar,
1816            ChartType::BarStacked,
1817            ChartType::BarPercentStacked,
1818            ChartType::Line,
1819            ChartType::Pie,
1820            ChartType::Area,
1821            ChartType::AreaStacked,
1822            ChartType::AreaPercentStacked,
1823            ChartType::Area3D,
1824            ChartType::Area3DStacked,
1825            ChartType::Area3DPercentStacked,
1826            ChartType::Col3D,
1827            ChartType::Col3DStacked,
1828            ChartType::Col3DPercentStacked,
1829            ChartType::Bar3D,
1830            ChartType::Bar3DStacked,
1831            ChartType::Bar3DPercentStacked,
1832            ChartType::LineStacked,
1833            ChartType::LinePercentStacked,
1834            ChartType::Line3D,
1835            ChartType::Pie3D,
1836            ChartType::Doughnut,
1837            ChartType::Scatter,
1838            ChartType::ScatterLine,
1839            ChartType::ScatterSmooth,
1840            ChartType::Radar,
1841            ChartType::RadarFilled,
1842            ChartType::RadarMarker,
1843            ChartType::StockHLC,
1844            ChartType::StockOHLC,
1845            ChartType::StockVHLC,
1846            ChartType::StockVOHLC,
1847            ChartType::Bubble,
1848            ChartType::Surface,
1849            ChartType::Surface3D,
1850            ChartType::SurfaceWireframe,
1851            ChartType::SurfaceWireframe3D,
1852            ChartType::ColLine,
1853            ChartType::ColLineStacked,
1854            ChartType::ColLinePercentStacked,
1855            ChartType::PieOfPie,
1856            ChartType::BarOfPie,
1857            ChartType::Col3DCone,
1858            ChartType::Col3DConeStacked,
1859            ChartType::Col3DConePercentStacked,
1860            ChartType::Col3DPyramid,
1861            ChartType::Col3DPyramidStacked,
1862            ChartType::Col3DPyramidPercentStacked,
1863            ChartType::Col3DCylinder,
1864            ChartType::Col3DCylinderStacked,
1865            ChartType::Col3DCylinderPercentStacked,
1866            ChartType::Contour,
1867            ChartType::WireframeContour,
1868            ChartType::Bubble3D,
1869        ];
1870        for ct in &types {
1871            let _ = build_chart_xml(&ChartConfig {
1872                chart_type: ct.clone(),
1873                title: None,
1874                series: vec![],
1875                show_legend: false,
1876                view_3d: None,
1877            });
1878        }
1879    }
1880
1881    #[test]
1882    fn test_scatter_empty_categories() {
1883        let cs = build_chart_xml(&ChartConfig {
1884            chart_type: ChartType::Scatter,
1885            title: None,
1886            series: vec![ChartSeries {
1887                name: "".into(),
1888                categories: "".into(),
1889                values: "Sheet1!$B$2:$B$6".into(),
1890                x_values: None,
1891                bubble_sizes: None,
1892            }],
1893            show_legend: false,
1894            view_3d: None,
1895        });
1896        let s = &cs.chart.plot_area.scatter_chart.unwrap().series[0];
1897        assert!(s.x_val.is_none());
1898        assert!(s.y_val.is_some());
1899    }
1900
1901    #[test]
1902    fn test_bubble_no_sizes() {
1903        let cs = build_chart_xml(&ChartConfig {
1904            chart_type: ChartType::Bubble,
1905            title: None,
1906            series: vec![ChartSeries {
1907                name: "".into(),
1908                categories: "Sheet1!$A$2:$A$6".into(),
1909                values: "Sheet1!$B$2:$B$6".into(),
1910                x_values: None,
1911                bubble_sizes: None,
1912            }],
1913            show_legend: false,
1914            view_3d: None,
1915        });
1916        assert!(cs.chart.plot_area.bubble_chart.unwrap().series[0]
1917            .bubble_size
1918            .is_none());
1919    }
1920
1921    #[test]
1922    fn test_pie_of_pie() {
1923        let cs = build_chart_xml(&mc(ChartType::PieOfPie));
1924        assert!(cs.chart.plot_area.of_pie_chart.is_some());
1925        assert!(cs.chart.plot_area.cat_ax.is_none());
1926        assert!(cs.chart.plot_area.val_ax.is_none());
1927        let op = cs.chart.plot_area.of_pie_chart.unwrap();
1928        assert_eq!(op.of_pie_type.val, "pie");
1929        assert!(op.ser_lines.is_some());
1930        assert_eq!(op.series.len(), 1);
1931    }
1932
1933    #[test]
1934    fn test_bar_of_pie() {
1935        let cs = build_chart_xml(&mc(ChartType::BarOfPie));
1936        assert!(cs.chart.plot_area.of_pie_chart.is_some());
1937        assert!(cs.chart.plot_area.cat_ax.is_none());
1938        let op = cs.chart.plot_area.of_pie_chart.unwrap();
1939        assert_eq!(op.of_pie_type.val, "bar");
1940        assert!(op.ser_lines.is_some());
1941    }
1942
1943    #[test]
1944    fn test_col_3d_cone() {
1945        let cs = build_chart_xml(&mc(ChartType::Col3DCone));
1946        assert!(cs.chart.view_3d.is_some());
1947        let b = cs.chart.plot_area.bar_3d_chart.unwrap();
1948        assert_eq!(b.bar_dir.val, "col");
1949        assert_eq!(b.grouping.val, "clustered");
1950        assert_eq!(b.shape.as_ref().unwrap().val, "cone");
1951    }
1952
1953    #[test]
1954    fn test_col_3d_cone_stacked() {
1955        let cs = build_chart_xml(&mc(ChartType::Col3DConeStacked));
1956        let b = cs.chart.plot_area.bar_3d_chart.unwrap();
1957        assert_eq!(b.grouping.val, "stacked");
1958        assert_eq!(b.shape.as_ref().unwrap().val, "cone");
1959    }
1960
1961    #[test]
1962    fn test_col_3d_cone_percent_stacked() {
1963        let cs = build_chart_xml(&mc(ChartType::Col3DConePercentStacked));
1964        let b = cs.chart.plot_area.bar_3d_chart.unwrap();
1965        assert_eq!(b.grouping.val, "percentStacked");
1966        assert_eq!(b.shape.as_ref().unwrap().val, "cone");
1967    }
1968
1969    #[test]
1970    fn test_col_3d_pyramid() {
1971        let cs = build_chart_xml(&mc(ChartType::Col3DPyramid));
1972        assert!(cs.chart.view_3d.is_some());
1973        let b = cs.chart.plot_area.bar_3d_chart.unwrap();
1974        assert_eq!(b.bar_dir.val, "col");
1975        assert_eq!(b.grouping.val, "clustered");
1976        assert_eq!(b.shape.as_ref().unwrap().val, "pyramid");
1977    }
1978
1979    #[test]
1980    fn test_col_3d_pyramid_stacked() {
1981        let cs = build_chart_xml(&mc(ChartType::Col3DPyramidStacked));
1982        let b = cs.chart.plot_area.bar_3d_chart.unwrap();
1983        assert_eq!(b.grouping.val, "stacked");
1984        assert_eq!(b.shape.as_ref().unwrap().val, "pyramid");
1985    }
1986
1987    #[test]
1988    fn test_col_3d_pyramid_percent_stacked() {
1989        let cs = build_chart_xml(&mc(ChartType::Col3DPyramidPercentStacked));
1990        let b = cs.chart.plot_area.bar_3d_chart.unwrap();
1991        assert_eq!(b.grouping.val, "percentStacked");
1992        assert_eq!(b.shape.as_ref().unwrap().val, "pyramid");
1993    }
1994
1995    #[test]
1996    fn test_col_3d_cylinder() {
1997        let cs = build_chart_xml(&mc(ChartType::Col3DCylinder));
1998        assert!(cs.chart.view_3d.is_some());
1999        let b = cs.chart.plot_area.bar_3d_chart.unwrap();
2000        assert_eq!(b.bar_dir.val, "col");
2001        assert_eq!(b.grouping.val, "clustered");
2002        assert_eq!(b.shape.as_ref().unwrap().val, "cylinder");
2003    }
2004
2005    #[test]
2006    fn test_col_3d_cylinder_stacked() {
2007        let cs = build_chart_xml(&mc(ChartType::Col3DCylinderStacked));
2008        let b = cs.chart.plot_area.bar_3d_chart.unwrap();
2009        assert_eq!(b.grouping.val, "stacked");
2010        assert_eq!(b.shape.as_ref().unwrap().val, "cylinder");
2011    }
2012
2013    #[test]
2014    fn test_col_3d_cylinder_percent_stacked() {
2015        let cs = build_chart_xml(&mc(ChartType::Col3DCylinderPercentStacked));
2016        let b = cs.chart.plot_area.bar_3d_chart.unwrap();
2017        assert_eq!(b.grouping.val, "percentStacked");
2018        assert_eq!(b.shape.as_ref().unwrap().val, "cylinder");
2019    }
2020
2021    #[test]
2022    fn test_contour() {
2023        let cs = build_chart_xml(&mc(ChartType::Contour));
2024        assert!(cs.chart.plot_area.surface_chart.is_some());
2025        assert!(cs.chart.plot_area.ser_ax.is_some());
2026        let sf = cs.chart.plot_area.surface_chart.unwrap();
2027        assert!(!sf.wireframe.as_ref().unwrap().val);
2028        assert_eq!(sf.ax_ids.len(), 3);
2029    }
2030
2031    #[test]
2032    fn test_wireframe_contour() {
2033        let cs = build_chart_xml(&mc(ChartType::WireframeContour));
2034        assert!(cs.chart.plot_area.surface_chart.is_some());
2035        assert!(cs.chart.plot_area.ser_ax.is_some());
2036        let sf = cs.chart.plot_area.surface_chart.unwrap();
2037        assert!(sf.wireframe.as_ref().unwrap().val);
2038        assert_eq!(sf.ax_ids.len(), 3);
2039    }
2040
2041    #[test]
2042    fn test_bubble_3d() {
2043        let cs = build_chart_xml(&ChartConfig {
2044            chart_type: ChartType::Bubble3D,
2045            title: None,
2046            series: vec![ChartSeries {
2047                name: "B3D".into(),
2048                categories: "Sheet1!$A$2:$A$6".into(),
2049                values: "Sheet1!$B$2:$B$6".into(),
2050                x_values: None,
2051                bubble_sizes: Some("Sheet1!$C$2:$C$6".into()),
2052            }],
2053            show_legend: false,
2054            view_3d: None,
2055        });
2056        let b = cs.chart.plot_area.bubble_chart.unwrap();
2057        assert_eq!(b.series.len(), 1);
2058        assert!(b.series[0].bubble_3d.as_ref().unwrap().val);
2059    }
2060
2061    #[test]
2062    fn test_bubble_no_3d_flag() {
2063        let cs = build_chart_xml(&mc(ChartType::Bubble));
2064        let b = cs.chart.plot_area.bubble_chart.unwrap();
2065        assert!(b.series[0].bubble_3d.is_none());
2066    }
2067
2068    #[test]
2069    fn test_col_3d_no_shape() {
2070        let cs = build_chart_xml(&mc(ChartType::Col3D));
2071        let b = cs.chart.plot_area.bar_3d_chart.unwrap();
2072        assert!(b.shape.is_none());
2073    }
2074}