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