1use crate::oxml::color::Color;
23use crate::oxml::sppr::{Fill, Line};
24use crate::oxml::writer::XmlWriter;
25
26#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
28pub enum ChartType {
29 Column,
31 Bar,
33 Line,
35 Pie,
37 Scatter,
39 Area,
41 Radar,
43 Bubble,
45}
46
47impl ChartType {
48 pub fn chart_element(self) -> &'static str {
50 match self {
51 ChartType::Column | ChartType::Bar => "c:barChart",
52 ChartType::Line => "c:lineChart",
53 ChartType::Pie => "c:pieChart",
54 ChartType::Scatter => "c:scatterChart",
55 ChartType::Area => "c:areaChart",
56 ChartType::Radar => "c:radarChart",
57 ChartType::Bubble => "c:bubbleChart",
58 }
59 }
60
61 pub fn bar_dir(self) -> &'static str {
63 match self {
64 ChartType::Column => "col",
65 ChartType::Bar => "bar",
66 _ => "",
67 }
68 }
69
70 pub fn is_scatter(self) -> bool {
72 matches!(self, ChartType::Scatter)
73 }
74
75 pub fn is_xy_chart(self) -> bool {
79 matches!(self, ChartType::Scatter | ChartType::Bubble)
80 }
81
82 pub fn is_bubble(self) -> bool {
84 matches!(self, ChartType::Bubble)
85 }
86}
87
88#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
93pub enum LabelPosition {
94 #[default]
96 BestFit,
97 Above,
99 Below,
101 Center,
103 Base,
105 InsideBase,
107 InsideEnd,
109 OutsideEnd,
111 Left,
113 Right,
115 OutsideEndPie,
117 InsideEndPie,
119}
120
121impl LabelPosition {
122 pub fn as_str(self) -> &'static str {
124 match self {
125 LabelPosition::BestFit => "bestFit",
126 LabelPosition::Above => "t",
127 LabelPosition::Below => "b",
128 LabelPosition::Center => "ctr",
129 LabelPosition::Base => "inBase",
130 LabelPosition::InsideBase => "inBase",
131 LabelPosition::InsideEnd => "inEnd",
132 LabelPosition::OutsideEnd => "outEnd",
133 LabelPosition::Left => "l",
134 LabelPosition::Right => "r",
135 LabelPosition::OutsideEndPie => "outEnd",
136 LabelPosition::InsideEndPie => "inEnd",
137 }
138 }
139
140 pub fn parse(s: &str) -> Option<Self> {
144 match s {
145 "bestFit" => Some(LabelPosition::BestFit),
146 "t" => Some(LabelPosition::Above),
147 "b" => Some(LabelPosition::Below),
148 "ctr" => Some(LabelPosition::Center),
149 "inBase" => Some(LabelPosition::Base),
150 "inEnd" => Some(LabelPosition::InsideEnd),
151 "outEnd" => Some(LabelPosition::OutsideEnd),
152 "l" => Some(LabelPosition::Left),
153 "r" => Some(LabelPosition::Right),
154 _ => None,
155 }
156 }
157}
158
159#[derive(Clone, Debug, Default)]
176pub struct DataLabels {
177 pub show_val: Option<bool>,
179 pub show_cat_name: Option<bool>,
181 pub show_ser_name: Option<bool>,
183 pub show_legend_key: Option<bool>,
185 pub show_percent: Option<bool>,
187 pub show_bubble_size: Option<bool>,
189 pub position: Option<LabelPosition>,
191 pub separator: Option<String>,
193 pub num_fmt: Option<String>,
195}
196
197impl DataLabels {
198 pub fn show_values() -> Self {
200 DataLabels {
201 show_val: Some(true),
202 ..Default::default()
203 }
204 }
205
206 pub fn show_percent_pie() -> Self {
208 DataLabels {
209 show_percent: Some(true),
210 ..Default::default()
211 }
212 }
213
214 pub fn write_xml(&self, w: &mut XmlWriter) {
224 w.open("c:dLbls");
225 if let Some(b) = self.show_legend_key {
226 w.empty_with("c:showLegendKey", &[("val", if b { "1" } else { "0" })]);
227 }
228 if let Some(b) = self.show_val {
229 w.empty_with("c:showVal", &[("val", if b { "1" } else { "0" })]);
230 }
231 if let Some(b) = self.show_cat_name {
232 w.empty_with("c:showCatName", &[("val", if b { "1" } else { "0" })]);
233 }
234 if let Some(b) = self.show_ser_name {
235 w.empty_with("c:showSerName", &[("val", if b { "1" } else { "0" })]);
236 }
237 if let Some(b) = self.show_percent {
238 w.empty_with("c:showPercent", &[("val", if b { "1" } else { "0" })]);
239 }
240 if let Some(b) = self.show_bubble_size {
241 w.empty_with("c:showBubbleSize", &[("val", if b { "1" } else { "0" })]);
242 }
243 if let Some(pos) = self.position {
244 w.empty_with("c:dLblPos", &[("val", pos.as_str())]);
245 }
246 if let Some(sep) = &self.separator {
247 w.empty_with("c:separator", &[("val", sep.as_str())]);
248 }
249 if let Some(fmt) = &self.num_fmt {
250 w.empty_with(
251 "c:numFmt",
252 &[("formatCode", fmt.as_str()), ("sourceLinked", "0")],
253 );
254 }
255 w.close("c:dLbls");
256 }
257
258 pub fn is_empty(&self) -> bool {
260 self.show_val.is_none()
261 && self.show_cat_name.is_none()
262 && self.show_ser_name.is_none()
263 && self.show_legend_key.is_none()
264 && self.show_percent.is_none()
265 && self.show_bubble_size.is_none()
266 && self.position.is_none()
267 && self.separator.is_none()
268 && self.num_fmt.is_none()
269 }
270}
271
272#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
276pub enum LegendPosition {
277 #[default]
279 Bottom,
280 Top,
282 Right,
284 Left,
286}
287
288impl LegendPosition {
289 pub fn as_str(self) -> &'static str {
291 match self {
292 LegendPosition::Bottom => "b",
293 LegendPosition::Top => "t",
294 LegendPosition::Right => "r",
295 LegendPosition::Left => "l",
296 }
297 }
298}
299
300#[derive(Clone, Debug, Default)]
310pub struct Legend {
311 pub position: LegendPosition,
313 pub overlay: bool,
317}
318
319#[derive(Clone, Debug, Default)]
324pub struct Gridlines {
325 pub major: bool,
327 pub minor: bool,
329}
330
331#[derive(Clone, Debug, Default)]
336pub struct AxisTitle {
337 pub text: String,
339}
340
341impl AxisTitle {
342 pub fn new(text: impl Into<String>) -> Self {
344 AxisTitle { text: text.into() }
345 }
346}
347
348#[derive(Clone, Debug, Default)]
350pub struct ChartData {
351 pub categories: Vec<ChartCategory>,
355 pub series: Vec<ChartSeries>,
357 pub title: Option<String>,
359 pub data_labels: Option<DataLabels>,
364 pub legend: Option<Legend>,
368 pub category_axis_gridlines: Gridlines,
370 pub value_axis_gridlines: Gridlines,
372 pub category_axis_title: Option<AxisTitle>,
374 pub value_axis_title: Option<AxisTitle>,
376 pub secondary_chart_type: Option<ChartType>,
384}
385
386#[derive(Clone, Debug, Default)]
388pub struct ChartCategory {
389 pub name: String,
391}
392
393impl ChartCategory {
394 pub fn new(name: impl Into<String>) -> Self {
396 ChartCategory { name: name.into() }
397 }
398}
399
400#[derive(Clone, Debug, Default)]
410pub struct SeriesFormat {
411 pub fill: Fill,
414 pub line: Option<Line>,
416}
417
418impl SeriesFormat {
419 pub fn solid_fill(color: Color) -> Self {
424 SeriesFormat {
425 fill: Fill::Solid(color),
426 line: None,
427 }
428 }
429
430 pub fn write_xml(&self, w: &mut XmlWriter) -> bool {
442 let has_fill = !matches!(self.fill, Fill::Inherit);
443 let has_line = self.line.is_some();
444 if !has_fill && !has_line {
445 return false;
446 }
447 w.open("c:spPr");
448 self.fill.write_xml(w);
449 if let Some(ln) = &self.line {
450 ln.write_xml(w);
451 }
452 w.close("c:spPr");
453 true
454 }
455}
456
457#[derive(Clone, Debug, Default)]
459pub struct ChartSeries {
460 pub name: String,
462 pub values: Vec<f64>,
466 pub x_values: Option<Vec<f64>>,
471 pub bubble_sizes: Option<Vec<f64>>,
475 pub data_labels: Option<DataLabels>,
479 pub secondary_axis: bool,
488 pub format: Option<SeriesFormat>,
493}
494
495impl ChartSeries {
496 pub fn new(name: impl Into<String>, values: Vec<f64>) -> Self {
500 ChartSeries {
501 name: name.into(),
502 values,
503 x_values: None,
504 bubble_sizes: None,
505 data_labels: None,
506 secondary_axis: false,
507 format: None,
508 }
509 }
510
511 pub fn new_scatter(name: impl Into<String>, x_values: Vec<f64>, y_values: Vec<f64>) -> Self {
520 ChartSeries {
521 name: name.into(),
522 values: y_values,
523 x_values: Some(x_values),
524 bubble_sizes: None,
525 data_labels: None,
526 secondary_axis: false,
527 format: None,
528 }
529 }
530
531 pub fn new_bubble(
541 name: impl Into<String>,
542 x_values: Vec<f64>,
543 y_values: Vec<f64>,
544 bubble_sizes: Vec<f64>,
545 ) -> Self {
546 ChartSeries {
547 name: name.into(),
548 values: y_values,
549 x_values: Some(x_values),
550 bubble_sizes: Some(bubble_sizes),
551 data_labels: None,
552 secondary_axis: false,
553 format: None,
554 }
555 }
556
557 pub fn with_format(mut self, format: SeriesFormat) -> Self {
573 self.format = Some(format);
574 self
575 }
576}
577
578#[derive(Clone, Debug)]
580pub struct Chart {
581 pub chart_type: ChartType,
583 pub data: ChartData,
585 pub rid: String,
590 pub external_data_rid: Option<String>,
603}
604
605impl Chart {
606 pub fn new(chart_type: ChartType, data: ChartData) -> Self {
608 Chart {
609 chart_type,
610 data,
611 rid: String::new(),
612 external_data_rid: None,
613 }
614 }
615
616 pub fn parse_from_xml(xml: &str) -> crate::Result<Chart> {
626 parse_from_xml(xml)
627 }
628}
629
630impl Default for Chart {
631 fn default() -> Self {
633 Chart {
634 chart_type: ChartType::Column,
635 data: ChartData::default(),
636 rid: String::new(),
637 external_data_rid: None,
638 }
639 }
640}
641
642impl Chart {
643 pub fn to_xml(&self) -> String {
672 let mut w = XmlWriter::new();
673 w.raw("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n");
674 w.open_with(
675 "c:chartSpace",
676 &[
677 (
678 "xmlns:c",
679 "http://schemas.openxmlformats.org/drawingml/2006/chart",
680 ),
681 (
682 "xmlns:a",
683 "http://schemas.openxmlformats.org/drawingml/2006/main",
684 ),
685 (
686 "xmlns:r",
687 "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
688 ),
689 ],
690 );
691 w.open("c:chart");
692 if let Some(title) = &self.data.title {
694 w.open("c:title");
695 w.open("a:tx");
696 w.open("a:rich");
697 w.open("a:bodyPr");
698 w.close("a:bodyPr");
699 w.open("a:lstStyle");
700 w.close("a:lstStyle");
701 w.open("a:p");
702 w.open("a:pPr");
703 w.empty_with("a:defRPr", &[("sz", "1400"), ("b", "1")]);
704 w.close("a:pPr");
705 w.leaf("a:t", title.as_str());
706 w.close("a:p");
707 w.close("a:rich");
708 w.close("a:tx");
709 w.empty_with("c:overlay", &[("val", "0")]);
710 w.close("c:title");
711 }
712 w.open("c:plotArea");
714 w.open("c:layout");
715 w.close("c:layout");
716
717 let chart_elem = self.chart_type.chart_element();
718 w.open(chart_elem);
719 if matches!(self.chart_type, ChartType::Column | ChartType::Bar) {
720 w.empty_with("c:barDir", &[("val", self.chart_type.bar_dir())]);
721 w.empty_with("c:grouping", &[("val", "clustered")]);
722 }
723 if matches!(self.chart_type, ChartType::Area) {
725 w.empty_with("c:grouping", &[("val", "standard")]);
726 }
727 if matches!(self.chart_type, ChartType::Scatter) {
729 w.empty_with("c:scatterStyle", &[("val", "lineMarker")]);
730 }
731 if matches!(self.chart_type, ChartType::Radar) {
733 w.empty_with("c:radarStyle", &[("val", "marker")]);
734 }
735 if matches!(self.chart_type, ChartType::Bubble) {
737 w.empty_with("c:bubbleScale", &[("val", "100")]);
738 }
739 if matches!(self.chart_type, ChartType::Pie) {
741 w.empty_with("c:varyColors", &[("val", "1")]);
742 }
743
744 if let Some(dl) = &self.data.data_labels {
747 if !dl.is_empty() {
748 dl.write_xml(&mut w);
749 }
750 }
751
752 let has_combo = self.data.secondary_chart_type.is_some();
761 for (idx, s) in self.data.series.iter().enumerate() {
762 if has_combo && s.secondary_axis {
764 continue;
765 }
766 write_series(&mut w, self.chart_type, s, idx, &self.data.categories);
767 }
768
769 let has_secondary = !self.chart_type.is_xy_chart()
777 && !matches!(self.chart_type, ChartType::Pie)
778 && self.data.series.iter().any(|s| s.secondary_axis);
779 if matches!(self.chart_type, ChartType::Pie) {
780 } else if self.chart_type.is_xy_chart() {
782 w.empty_with("c:axId", &[("val", "111111111")]);
784 w.empty_with("c:axId", &[("val", "222222222")]);
785 } else {
786 w.empty_with("c:axId", &[("val", "111111111")]);
788 w.empty_with("c:axId", &[("val", "222222222")]);
789 if has_secondary {
792 w.empty_with("c:axId", &[("val", "444444444")]);
793 }
794 }
795 w.close(chart_elem);
796
797 if let Some(sec_ct) = self.data.secondary_chart_type {
809 if self.data.series.iter().any(|s| s.secondary_axis) {
810 let sec_elem = sec_ct.chart_element();
811 w.open(sec_elem);
812 if matches!(sec_ct, ChartType::Column | ChartType::Bar) {
814 w.empty_with("c:barDir", &[("val", sec_ct.bar_dir())]);
815 w.empty_with("c:grouping", &[("val", "clustered")]);
816 }
817 if matches!(sec_ct, ChartType::Area) {
818 w.empty_with("c:grouping", &[("val", "standard")]);
819 }
820 if matches!(sec_ct, ChartType::Scatter) {
821 w.empty_with("c:scatterStyle", &[("val", "lineMarker")]);
822 }
823 if matches!(sec_ct, ChartType::Radar) {
824 w.empty_with("c:radarStyle", &[("val", "marker")]);
825 }
826 if matches!(sec_ct, ChartType::Bubble) {
827 w.empty_with("c:bubbleScale", &[("val", "100")]);
828 }
829 if let Some(dl) = &self.data.data_labels {
831 if !dl.is_empty() {
832 dl.write_xml(&mut w);
833 }
834 }
835 for (idx, s) in self.data.series.iter().enumerate() {
837 if s.secondary_axis {
838 write_series(&mut w, sec_ct, s, idx, &self.data.categories);
839 }
840 }
841 w.empty_with("c:axId", &[("val", "111111111")]);
843 w.empty_with("c:axId", &[("val", "444444444")]);
844 w.close(sec_elem);
845 }
846 }
847
848 if !matches!(self.chart_type, ChartType::Pie) {
853 if self.chart_type.is_xy_chart() {
854 w.open("c:valAx");
857 w.empty_with("c:axId", &[("val", "111111111")]);
858 w.empty_with("c:scaling", &[("orientation", "minMax")]);
859 w.empty_with("c:delete", &[("val", "0")]);
860 w.empty_with("c:axPos", &[("val", "b")]);
861 write_gridlines(&mut w, &self.data.category_axis_gridlines);
862 if let Some(t) = &self.data.category_axis_title {
863 write_axis_title(&mut w, t.text.as_str());
864 }
865 w.empty_with("c:crossAx", &[("val", "222222222")]);
866 w.close("c:valAx");
867 w.open("c:valAx");
869 w.empty_with("c:axId", &[("val", "222222222")]);
870 w.empty_with("c:scaling", &[("orientation", "minMax")]);
871 w.empty_with("c:delete", &[("val", "0")]);
872 w.empty_with("c:axPos", &[("val", "l")]);
873 write_gridlines(&mut w, &self.data.value_axis_gridlines);
874 if let Some(t) = &self.data.value_axis_title {
875 write_axis_title(&mut w, t.text.as_str());
876 }
877 w.empty_with("c:crossAx", &[("val", "111111111")]);
878 w.close("c:valAx");
879 } else {
880 w.open("c:catAx");
883 w.empty_with("c:axId", &[("val", "111111111")]);
884 w.empty_with("c:scaling", &[("orientation", "minMax")]);
885 w.empty_with("c:delete", &[("val", "0")]);
886 w.empty_with("c:axPos", &[("val", "b")]);
887 write_gridlines(&mut w, &self.data.category_axis_gridlines);
888 if let Some(t) = &self.data.category_axis_title {
889 write_axis_title(&mut w, t.text.as_str());
890 }
891 w.empty_with("c:crossAx", &[("val", "222222222")]);
892 w.close("c:catAx");
893 w.open("c:valAx");
895 w.empty_with("c:axId", &[("val", "222222222")]);
896 w.empty_with("c:scaling", &[("orientation", "minMax")]);
897 w.empty_with("c:delete", &[("val", "0")]);
898 w.empty_with("c:axPos", &[("val", "l")]);
899 write_gridlines(&mut w, &self.data.value_axis_gridlines);
900 if let Some(t) = &self.data.value_axis_title {
901 write_axis_title(&mut w, t.text.as_str());
902 }
903 w.empty_with("c:crossAx", &[("val", "111111111")]);
904 w.close("c:valAx");
905 if has_secondary {
912 w.open("c:valAx");
913 w.empty_with("c:axId", &[("val", "444444444")]);
914 w.empty_with("c:scaling", &[("orientation", "minMax")]);
915 w.empty_with("c:delete", &[("val", "0")]);
916 w.empty_with("c:axPos", &[("val", "r")]);
917 write_gridlines(&mut w, &self.data.value_axis_gridlines);
918 w.empty_with("c:crossAx", &[("val", "111111111")]);
919 w.empty_with("c:crosses", &[("val", "max")]);
920 w.close("c:valAx");
921 }
922 }
923 }
924 w.close("c:plotArea");
925
926 if let Some(legend) = &self.data.legend {
928 w.open("c:legend");
929 w.empty_with("c:legendPos", &[("val", legend.position.as_str())]);
930 let overlay_val = if legend.overlay { "1" } else { "0" };
931 w.empty_with("c:overlay", &[("val", overlay_val)]);
932 w.close("c:legend");
933 }
934
935 w.empty_with("c:plotVisOnly", &[("val", "1")]);
936 w.empty_with("c:dispBlanksAs", &[("val", "gap")]);
937 w.close("c:chart");
938
939 if let Some(ext_rid) = &self.external_data_rid {
948 w.open_with("c:externalData", &[("r:id", ext_rid.as_str())]);
949 w.empty_with("c:autoUpdate", &[("val", "0")]);
950 w.close("c:externalData");
951 }
952
953 w.close("c:chartSpace");
954 w.into_string()
955 }
956}
957
958fn write_gridlines(w: &mut XmlWriter, gridlines: &Gridlines) {
963 if gridlines.major {
964 w.open("c:majorGridlines");
965 w.close("c:majorGridlines");
966 }
967 if gridlines.minor {
968 w.open("c:minorGridlines");
969 w.close("c:minorGridlines");
970 }
971}
972
973fn write_axis_title(w: &mut XmlWriter, text: &str) {
978 w.open("c:title");
979 w.open("a:tx");
980 w.open("a:rich");
981 w.open("a:bodyPr");
982 w.close("a:bodyPr");
983 w.open("a:lstStyle");
984 w.close("a:lstStyle");
985 w.open("a:p");
986 w.open("a:pPr");
987 w.empty_with("a:defRPr", &[("sz", "1000"), ("b", "0")]);
988 w.close("a:pPr");
989 w.leaf("a:t", text);
990 w.close("a:p");
991 w.close("a:rich");
992 w.close("a:tx");
993 w.empty_with("c:overlay", &[("val", "0")]);
994 w.close("c:title");
995}
996
997fn write_series(
1018 w: &mut XmlWriter,
1019 chart_type: ChartType,
1020 s: &ChartSeries,
1021 idx: usize,
1022 categories: &[ChartCategory],
1023) {
1024 let idx_s = idx.to_string();
1025 w.open("c:ser");
1026 w.empty_with("c:idx", &[("val", idx_s.as_str())]);
1027 w.empty_with("c:order", &[("val", idx_s.as_str())]);
1028 w.open("c:tx");
1030 w.open("c:strRef");
1031 w.leaf("c:f", &format!("Sheet1!${}$1", col_letter(idx + 1)));
1032 w.open("c:strCache");
1033 w.open_with("c:pt", &[("idx", "0")]);
1034 w.leaf("c:v", s.name.as_str());
1035 w.close("c:pt");
1036 w.close("c:strCache");
1037 w.close("c:strRef");
1038 w.close("c:tx");
1039 if let Some(fmt) = &s.format {
1041 fmt.write_xml(w);
1042 }
1043 if chart_type.is_xy_chart() {
1044 w.open("c:xVal");
1047 w.open("c:numRef");
1048 w.leaf("c:f", "Sheet1!$A$2:$A$100");
1049 w.open("c:numCache");
1050 w.empty_with("c:formatCode", &[("val", "General")]);
1051 if let Some(xs) = &s.x_values {
1052 for (i, v) in xs.iter().enumerate() {
1053 let i_s = i.to_string();
1054 let v_s = format_f64(*v);
1055 w.open_with("c:pt", &[("idx", i_s.as_str())]);
1056 w.leaf("c:v", v_s.as_str());
1057 w.close("c:pt");
1058 }
1059 }
1060 w.close("c:numCache");
1061 w.close("c:numRef");
1062 w.close("c:xVal");
1063 w.open("c:yVal");
1065 w.open("c:numRef");
1066 w.leaf(
1067 "c:f",
1068 &format!(
1069 "Sheet1!${}$2:${}$100",
1070 col_letter(idx + 1),
1071 col_letter(idx + 1)
1072 ),
1073 );
1074 w.open("c:numCache");
1075 w.empty_with("c:formatCode", &[("val", "General")]);
1076 for (i, v) in s.values.iter().enumerate() {
1077 let i_s = i.to_string();
1078 let v_s = format_f64(*v);
1079 w.open_with("c:pt", &[("idx", i_s.as_str())]);
1080 w.leaf("c:v", v_s.as_str());
1081 w.close("c:pt");
1082 }
1083 w.close("c:numCache");
1084 w.close("c:numRef");
1085 w.close("c:yVal");
1086 if chart_type.is_bubble() {
1088 w.open("c:bubbleSize");
1089 w.open("c:numRef");
1090 w.leaf(
1091 "c:f",
1092 &format!(
1093 "Sheet1!${}$2:${}$100",
1094 col_letter(idx + 2),
1095 col_letter(idx + 2)
1096 ),
1097 );
1098 w.open("c:numCache");
1099 w.empty_with("c:formatCode", &[("val", "General")]);
1100 if let Some(sizes) = &s.bubble_sizes {
1101 for (i, v) in sizes.iter().enumerate() {
1102 let i_s = i.to_string();
1103 let v_s = format_f64(*v);
1104 w.open_with("c:pt", &[("idx", i_s.as_str())]);
1105 w.leaf("c:v", v_s.as_str());
1106 w.close("c:pt");
1107 }
1108 }
1109 w.close("c:numCache");
1110 w.close("c:numRef");
1111 w.close("c:bubbleSize");
1112 }
1113 } else {
1114 if !matches!(chart_type, ChartType::Pie) && !categories.is_empty() {
1117 w.open("c:cat");
1118 w.open("c:strRef");
1119 w.leaf("c:f", "Sheet1!$A$2:$A$100");
1120 w.open("c:strCache");
1121 for (i, cat) in categories.iter().enumerate() {
1122 let i_s = i.to_string();
1123 w.open_with("c:pt", &[("idx", i_s.as_str())]);
1124 w.leaf("c:v", cat.name.as_str());
1125 w.close("c:pt");
1126 }
1127 w.close("c:strCache");
1128 w.close("c:strRef");
1129 w.close("c:cat");
1130 } else if matches!(chart_type, ChartType::Pie) && !categories.is_empty() {
1131 w.open("c:cat");
1133 w.open("c:strRef");
1134 w.leaf("c:f", "Sheet1!$A$2:$A$100");
1135 w.open("c:strCache");
1136 for (i, cat) in categories.iter().enumerate() {
1137 let i_s = i.to_string();
1138 w.open_with("c:pt", &[("idx", i_s.as_str())]);
1139 w.leaf("c:v", cat.name.as_str());
1140 w.close("c:pt");
1141 }
1142 w.close("c:strCache");
1143 w.close("c:strRef");
1144 w.close("c:cat");
1145 }
1146 w.open("c:val");
1148 w.open("c:numRef");
1149 w.leaf(
1150 "c:f",
1151 &format!(
1152 "Sheet1!${}$2:${}$100",
1153 col_letter(idx + 1),
1154 col_letter(idx + 1)
1155 ),
1156 );
1157 w.open("c:numCache");
1158 w.empty_with("c:formatCode", &[("val", "General")]);
1159 for (i, v) in s.values.iter().enumerate() {
1160 let i_s = i.to_string();
1161 let v_s = format_f64(*v);
1162 w.open_with("c:pt", &[("idx", i_s.as_str())]);
1163 w.leaf("c:v", v_s.as_str());
1164 w.close("c:pt");
1165 }
1166 w.close("c:numCache");
1167 w.close("c:numRef");
1168 w.close("c:val");
1169 }
1170 if let Some(dl) = &s.data_labels {
1173 if !dl.is_empty() {
1174 dl.write_xml(w);
1175 }
1176 }
1177 w.close("c:ser");
1178}
1179
1180fn col_letter(n: usize) -> String {
1182 let mut s = String::new();
1183 let mut n = n;
1184 while n > 0 {
1185 n -= 1;
1186 let ch = (b'A' + (n % 26) as u8) as char;
1187 s.insert(0, ch);
1188 n /= 26;
1189 }
1190 s
1191}
1192
1193fn format_f64(v: f64) -> String {
1195 if v.is_nan() || v.is_infinite() {
1196 return "0".to_string();
1197 }
1198 if v.fract() == 0.0 && v.abs() < 1e15 {
1199 format!("{}", v as i64)
1200 } else {
1201 format!("{}", v)
1202 }
1203}
1204
1205pub fn parse_from_xml(xml: &str) -> crate::Result<Chart> {
1247 use quick_xml::events::Event;
1248 use quick_xml::reader::Reader;
1249
1250 let mut chart_type = ChartType::Column;
1251 let mut title: Option<String> = None;
1252 let mut series: Vec<ChartSeries> = Vec::new();
1253 let mut categories: Vec<ChartCategory> = Vec::new();
1254
1255 let mut rd = Reader::from_str(xml);
1256 rd.config_mut().trim_text(true);
1257 let mut buf = Vec::new();
1258 let mut in_chart_elem: Option<&'static str> = None;
1260 let mut in_plot_area = false;
1263 let mut bar_dir: Option<String> = None;
1265 let mut in_title_text = false;
1267 let mut cur_ser: Option<ChartSeries> = None;
1269 let mut ser_field: Option<SerField> = None;
1271 let mut in_cache = false;
1273 let mut external_data_rid: Option<String> = None;
1275 let mut cur_values: Vec<f64> = Vec::new();
1277 let mut cur_strings: Vec<String> = Vec::new();
1278
1279 let mut chart_dl: Option<DataLabels> = None;
1287 let mut series_dl: Option<DataLabels> = None;
1288 let mut dl_target: Option<DlTarget> = None;
1290 let mut has_secondary_axis = false;
1293
1294 let mut primary_chart_seen = false;
1306 let mut secondary_chart_type: Option<ChartType> = None;
1307 let mut in_secondary_chart = false;
1308 let mut secondary_bar_dir: Option<String> = None;
1310
1311 let mut legend_position: Option<LegendPosition> = None;
1317 let mut legend_overlay: Option<bool> = None;
1318 let mut in_legend = false;
1319 let mut legend_buf: Option<Legend> = None;
1320
1321 let mut in_sp_pr = false;
1326 let mut cur_series_format: Option<SeriesFormat> = None;
1327 let mut sppr_color: Option<Color> = None;
1328
1329 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
1331 enum SerField {
1332 Name,
1334 Cat,
1336 Val,
1338 XVal,
1340 YVal,
1342 BubbleSize,
1344 }
1345
1346 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
1348 enum DlTarget {
1349 Chart,
1351 Series,
1353 }
1354
1355 fn chart_elem_for(local: &[u8]) -> Option<(&'static str, ChartType)> {
1357 match local {
1358 b"barChart" => Some(("barChart", ChartType::Column)), b"lineChart" => Some(("lineChart", ChartType::Line)),
1360 b"pieChart" => Some(("pieChart", ChartType::Pie)),
1361 b"scatterChart" => Some(("scatterChart", ChartType::Scatter)),
1362 b"areaChart" => Some(("areaChart", ChartType::Area)),
1363 b"radarChart" => Some(("radarChart", ChartType::Radar)),
1364 b"bubbleChart" => Some(("bubbleChart", ChartType::Bubble)),
1365 _ => None,
1366 }
1367 }
1368
1369 fn local_name_quick(name: &[u8]) -> &[u8] {
1371 match name.iter().position(|&b| b == b':') {
1372 Some(i) => &name[i + 1..],
1373 None => name,
1374 }
1375 }
1376
1377 loop {
1378 match rd.read_event_into(&mut buf) {
1379 Ok(Event::Start(e)) => {
1380 let name = e.name();
1382 let local = local_name_quick(name.as_ref());
1383 if local == b"externalData" {
1388 for a in e.attributes().flatten() {
1389 let key = a.key.as_ref();
1394 if key == b"r:id" || key == b"id" || key.ends_with(b":id") {
1395 external_data_rid = Some(
1396 a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
1397 .unwrap_or_default()
1398 .to_string(),
1399 );
1400 }
1401 }
1402 }
1403 if local == b"plotArea" {
1405 in_plot_area = true;
1406 }
1407 if let Some((elem, ct)) = chart_elem_for(local) {
1413 if primary_chart_seen && in_chart_elem.is_none() {
1414 secondary_chart_type = Some(ct);
1416 in_secondary_chart = true;
1417 secondary_bar_dir = None;
1418 } else if !primary_chart_seen {
1419 chart_type = ct;
1421 primary_chart_seen = true;
1422 }
1423 in_chart_elem = Some(elem);
1424 }
1426 if local == b"barDir" && in_chart_elem.is_some() {
1431 for a in e.attributes().flatten() {
1432 if a.key.as_ref() == b"val" {
1433 let v = a
1434 .normalized_value(quick_xml::XmlVersion::Implicit1_0)
1435 .unwrap_or_default()
1436 .to_string();
1437 if in_secondary_chart {
1438 secondary_bar_dir = Some(v);
1439 } else {
1440 bar_dir = Some(v);
1441 }
1442 }
1443 }
1444 }
1445 if local == b"t" {
1447 in_title_text = true;
1448 }
1449 if local == b"ser" && in_chart_elem.is_some() {
1451 cur_ser = Some(ChartSeries::default());
1452 series_dl = None;
1454 }
1455 if local == b"spPr" && cur_ser.is_some() {
1460 in_sp_pr = true;
1461 cur_series_format = Some(SeriesFormat::default());
1462 sppr_color = None;
1463 }
1464 if local == b"dLbls" {
1469 if cur_ser.is_some() {
1470 dl_target = Some(DlTarget::Series);
1471 series_dl = Some(DataLabels::default());
1472 } else if in_chart_elem.is_some() {
1473 dl_target = Some(DlTarget::Chart);
1474 chart_dl = Some(DataLabels::default());
1475 }
1476 }
1477 if cur_ser.is_some() {
1479 match local {
1480 b"tx" => ser_field = Some(SerField::Name),
1481 b"cat" => ser_field = Some(SerField::Cat),
1482 b"val" => ser_field = Some(SerField::Val),
1483 b"xVal" => ser_field = Some(SerField::XVal),
1484 b"yVal" => ser_field = Some(SerField::YVal),
1485 b"bubbleSize" => ser_field = Some(SerField::BubbleSize),
1486 _ => {}
1487 }
1488 }
1489 if matches!(local, b"numCache" | b"strCache") {
1491 in_cache = true;
1492 cur_values.clear();
1493 cur_strings.clear();
1494 }
1495 if local == b"crosses" && in_plot_area {
1504 for a in e.attributes().flatten() {
1505 if a.key.as_ref() == b"val" {
1506 let v = a
1507 .normalized_value(quick_xml::XmlVersion::Implicit1_0)
1508 .unwrap_or_default();
1509 if v == "max" {
1510 has_secondary_axis = true;
1511 }
1512 }
1513 }
1514 }
1515 if local == b"legend" {
1521 in_legend = true;
1522 }
1523 if in_legend {
1526 if local == b"legendPos" {
1527 if let Some(v) = attr_val(&e, "val") {
1528 legend_position = match v.as_str() {
1529 "b" => Some(LegendPosition::Bottom),
1530 "t" => Some(LegendPosition::Top),
1531 "r" => Some(LegendPosition::Right),
1532 "l" => Some(LegendPosition::Left),
1533 _ => None,
1534 };
1535 }
1536 }
1537 if local == b"overlay" {
1538 legend_overlay = parse_bool_val(&e);
1539 }
1540 }
1541 }
1542 Ok(Event::Empty(e)) => {
1543 let name = e.name();
1545 let local = local_name_quick(name.as_ref());
1546 if local == b"barDir" && in_chart_elem.is_some() {
1551 for a in e.attributes().flatten() {
1552 if a.key.as_ref() == b"val" {
1553 let v = a
1554 .normalized_value(quick_xml::XmlVersion::Implicit1_0)
1555 .unwrap_or_default()
1556 .to_string();
1557 if in_secondary_chart {
1558 secondary_bar_dir = Some(v);
1559 } else {
1560 bar_dir = Some(v);
1561 }
1562 }
1563 }
1564 }
1565 if local == b"v" && in_cache {
1568 if let Some(v) = e.attributes().flatten().find(|a| a.key.as_ref() == b"val") {
1569 let s = v
1570 .normalized_value(quick_xml::XmlVersion::Implicit1_0)
1571 .unwrap_or_default()
1572 .to_string();
1573 if let Ok(f) = s.parse::<f64>() {
1574 cur_values.push(f);
1575 }
1576 cur_strings.push(s);
1577 }
1578 }
1579 if let Some(target) = dl_target.as_ref() {
1584 let dl_buf: &mut Option<DataLabels> = if *target == DlTarget::Chart {
1586 &mut chart_dl
1587 } else {
1588 &mut series_dl
1589 };
1590 let dl = dl_buf.get_or_insert_with(DataLabels::default);
1591 match local {
1592 b"showVal" => {
1593 dl.show_val = parse_bool_val(&e);
1594 }
1595 b"showCatName" => {
1596 dl.show_cat_name = parse_bool_val(&e);
1597 }
1598 b"showSerName" => {
1599 dl.show_ser_name = parse_bool_val(&e);
1600 }
1601 b"showLegendKey" => {
1602 dl.show_legend_key = parse_bool_val(&e);
1603 }
1604 b"showPercent" => {
1605 dl.show_percent = parse_bool_val(&e);
1606 }
1607 b"showBubbleSize" => {
1608 dl.show_bubble_size = parse_bool_val(&e);
1609 }
1610 b"dLblPos" => {
1611 if let Some(v) = attr_val(&e, "val") {
1612 dl.position = LabelPosition::parse(&v);
1613 }
1614 }
1615 b"separator" => {
1616 if let Some(v) = attr_val(&e, "val") {
1617 dl.separator = Some(v);
1618 }
1619 }
1620 b"numFmt" => {
1621 if let Some(fc) = attr_val(&e, "formatCode") {
1622 dl.num_fmt = Some(fc);
1623 }
1624 }
1625 _ => {}
1626 }
1627 }
1628 if local == b"crosses" && in_plot_area {
1631 if let Some(v) = attr_val(&e, "val") {
1632 if v == "max" {
1633 has_secondary_axis = true;
1634 }
1635 }
1636 }
1637 if in_sp_pr && local == b"srgbClr" {
1642 if let Some(v) = attr_val(&e, "val") {
1643 if let Some(c) = parse_hex_color(&v) {
1644 sppr_color = Some(c);
1645 }
1646 }
1647 }
1648 if in_legend {
1653 if local == b"legendPos" {
1654 if let Some(v) = attr_val(&e, "val") {
1655 legend_position = match v.as_str() {
1656 "b" => Some(LegendPosition::Bottom),
1657 "t" => Some(LegendPosition::Top),
1658 "r" => Some(LegendPosition::Right),
1659 "l" => Some(LegendPosition::Left),
1660 _ => None,
1661 };
1662 }
1663 }
1664 if local == b"overlay" {
1665 legend_overlay = parse_bool_val(&e);
1666 }
1667 }
1668 }
1669 Ok(Event::Text(t)) => {
1670 if in_title_text && title.is_none() {
1672 let text_str = std::str::from_utf8(t.as_ref()).unwrap_or("");
1676 let text = quick_xml::escape::unescape(text_str)
1677 .unwrap_or_default()
1678 .to_string();
1679 if !text.is_empty() {
1680 title = Some(text);
1681 }
1682 }
1683 if in_cache {
1685 let text_str = std::str::from_utf8(t.as_ref()).unwrap_or("");
1686 let text = quick_xml::escape::unescape(text_str)
1687 .unwrap_or_default()
1688 .to_string();
1689 if !text.is_empty() {
1690 if let Ok(f) = text.parse::<f64>() {
1691 cur_values.push(f);
1692 }
1693 cur_strings.push(text);
1694 }
1695 }
1696 }
1697 Ok(Event::End(e)) => {
1698 let name = e.name();
1700 let local = local_name_quick(name.as_ref());
1701 if local == b"plotArea" {
1703 in_plot_area = false;
1704 }
1705 if let Some(elem) = in_chart_elem {
1711 if local == elem.as_bytes() {
1712 if !in_secondary_chart {
1713 if elem == "barChart" {
1715 if let Some(dir) = &bar_dir {
1716 if dir == "bar" {
1717 chart_type = ChartType::Bar;
1718 }
1719 }
1720 }
1721 } else {
1722 if elem == "barChart" {
1724 if let Some(dir) = &secondary_bar_dir {
1725 if dir == "bar" {
1726 secondary_chart_type = Some(ChartType::Bar);
1727 }
1728 }
1729 }
1730 }
1731 in_chart_elem = None;
1732 in_secondary_chart = false;
1733 }
1734 }
1735 if local == b"t" {
1737 in_title_text = false;
1738 }
1739 if local == b"dLbls" {
1741 dl_target = None;
1742 }
1743 if local == b"spPr" && in_sp_pr {
1748 if let Some(color) = sppr_color.take() {
1749 if let Some(fmt) = cur_series_format.as_mut() {
1750 fmt.fill = Fill::Solid(color);
1751 }
1752 }
1753 if let Some(fmt) = cur_series_format.take() {
1754 if let Some(s) = cur_ser.as_mut() {
1755 s.format = Some(fmt);
1756 }
1757 }
1758 in_sp_pr = false;
1759 }
1760 if local == b"legend" {
1765 let pos = legend_position.take().unwrap_or_default();
1766 legend_buf = Some(Legend {
1767 position: pos,
1768 overlay: legend_overlay.take().unwrap_or(false),
1769 });
1770 in_legend = false;
1771 }
1772 if matches!(local, b"numCache" | b"strCache") && cur_ser.is_some() {
1774 let field = ser_field.take();
1775 if let Some(f) = field {
1776 match f {
1777 SerField::Name => {
1778 if let Some(s) = cur_strings.first() {
1780 if let Some(s_obj) = cur_ser.as_mut() {
1781 s_obj.name = s.clone();
1782 }
1783 }
1784 ser_field = None;
1785 }
1786 SerField::Cat => {
1787 if categories.is_empty() {
1789 for s in &cur_strings {
1790 categories.push(ChartCategory::new(s.clone()));
1791 }
1792 }
1793 ser_field = None;
1794 }
1795 SerField::Val | SerField::YVal => {
1796 if let Some(s_obj) = cur_ser.as_mut() {
1797 s_obj.values = cur_values.clone();
1798 }
1799 ser_field = None;
1800 }
1801 SerField::XVal => {
1802 if let Some(s_obj) = cur_ser.as_mut() {
1803 s_obj.x_values = Some(cur_values.clone());
1804 }
1805 ser_field = None;
1806 }
1807 SerField::BubbleSize => {
1808 if let Some(s_obj) = cur_ser.as_mut() {
1809 s_obj.bubble_sizes = Some(cur_values.clone());
1810 }
1811 ser_field = None;
1812 }
1813 }
1814 }
1815 in_cache = false;
1816 cur_values.clear();
1817 cur_strings.clear();
1818 }
1819 if local == b"ser" && cur_ser.is_some() {
1824 if let Some(mut s_obj) = cur_ser.take() {
1825 if let Some(dl) = series_dl.take() {
1827 if !dl.is_empty() {
1828 s_obj.data_labels = Some(dl);
1829 }
1830 }
1831 if in_secondary_chart {
1833 s_obj.secondary_axis = true;
1834 }
1835 series.push(s_obj);
1836 }
1837 ser_field = None;
1838 series_dl = None;
1840 if dl_target == Some(DlTarget::Series) {
1841 dl_target = None;
1842 }
1843 }
1844 }
1845 Ok(Event::Eof) => break,
1846 Err(e) => return Err(crate::Error::Xml(format!("chart parse_from_xml: {e}"))),
1847 _ => {}
1848 }
1849 buf.clear();
1850 }
1851
1852 if has_secondary_axis
1863 && secondary_chart_type.is_none()
1864 && !chart_type.is_xy_chart()
1865 && !matches!(chart_type, ChartType::Pie)
1866 {
1867 for s in series.iter_mut() {
1868 s.secondary_axis = true;
1869 }
1870 }
1871
1872 let data = ChartData {
1873 categories,
1874 series,
1875 title,
1876 data_labels: chart_dl.filter(|dl| !dl.is_empty()),
1877 legend: legend_buf,
1878 secondary_chart_type,
1879 ..Default::default()
1880 };
1881 Ok(Chart {
1882 chart_type,
1883 data,
1884 rid: String::new(),
1885 external_data_rid,
1886 })
1887}
1888
1889fn parse_bool_val(e: &quick_xml::events::BytesStart<'_>) -> Option<bool> {
1894 for a in e.attributes().flatten() {
1895 if a.key.as_ref() == b"val" {
1896 let v = a
1897 .normalized_value(quick_xml::XmlVersion::Implicit1_0)
1898 .unwrap_or_default();
1899 return match v.as_ref() {
1900 "1" | "true" | "True" => Some(true),
1901 "0" | "false" | "False" => Some(false),
1902 _ => None,
1903 };
1904 }
1905 }
1906 None
1907}
1908
1909fn attr_val(e: &quick_xml::events::BytesStart<'_>, key: &str) -> Option<String> {
1913 let key_bytes = key.as_bytes();
1914 let suffix: Vec<u8> = std::iter::once(b':')
1916 .chain(key_bytes.iter().copied())
1917 .collect();
1918 for a in e.attributes().flatten() {
1919 let k = a.key.as_ref();
1920 if k == key_bytes || k.ends_with(&suffix) {
1921 return Some(
1922 a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
1923 .unwrap_or_default()
1924 .to_string(),
1925 );
1926 }
1927 }
1928 None
1929}
1930
1931fn parse_hex_color(s: &str) -> Option<Color> {
1942 use crate::units::RGBColor;
1943 let s = s.trim();
1944 let (r, g, b) = if s.len() == 6 {
1945 (
1946 u8::from_str_radix(&s[0..2], 16).ok()?,
1947 u8::from_str_radix(&s[2..4], 16).ok()?,
1948 u8::from_str_radix(&s[4..6], 16).ok()?,
1949 )
1950 } else if s.len() == 3 {
1951 (
1952 u8::from_str_radix(&s[0..1].repeat(2), 16).ok()?,
1953 u8::from_str_radix(&s[1..2].repeat(2), 16).ok()?,
1954 u8::from_str_radix(&s[2..3].repeat(2), 16).ok()?,
1955 )
1956 } else {
1957 return None;
1958 };
1959 Some(Color::RGB(RGBColor(r, g, b)))
1960}
1961
1962#[cfg(test)]
1963mod tests {
1964 use super::*;
1965
1966 #[test]
1968 fn column_chart_minimal_xml() {
1969 let data = ChartData {
1970 categories: vec![ChartCategory::new("Q1"), ChartCategory::new("Q2")],
1971 series: vec![ChartSeries::new("Sales", vec![10.0, 20.0])],
1972 title: Some("Revenue".to_string()),
1973 data_labels: None,
1974 ..Default::default()
1975 };
1976 let chart = Chart::new(ChartType::Column, data);
1977 let xml = chart.to_xml();
1978 assert!(xml.contains("<c:chartSpace"), "xml: {}", xml);
1979 assert!(xml.contains("<c:barChart>"), "xml: {}", xml);
1980 assert!(xml.contains("barDir"), "xml: {}", xml);
1981 assert!(xml.contains("val=\"col\""), "xml: {}", xml);
1982 assert!(xml.contains("<c:ser>"), "xml: {}", xml);
1983 assert!(xml.contains("<c:catAx>"), "xml: {}", xml);
1984 assert!(xml.contains("<c:valAx>"), "xml: {}", xml);
1985 assert!(xml.contains("Revenue"), "xml: {}", xml);
1986 }
1987
1988 #[test]
1990 fn pie_chart_no_axes() {
1991 let data = ChartData {
1992 categories: vec![ChartCategory::new("A"), ChartCategory::new("B")],
1993 series: vec![ChartSeries::new("X", vec![1.0, 2.0])],
1994 title: None,
1995 data_labels: None,
1996 ..Default::default()
1997 };
1998 let xml = Chart::new(ChartType::Pie, data).to_xml();
1999 assert!(xml.contains("<c:pieChart>"), "xml: {}", xml);
2000 assert!(!xml.contains("<c:catAx>"), "xml: {}", xml);
2001 assert!(!xml.contains("<c:valAx>"), "xml: {}", xml);
2002 assert!(xml.contains("varyColors"), "xml: {}", xml);
2003 }
2004
2005 #[test]
2007 fn line_chart_uses_line_chart_element() {
2008 let data = ChartData {
2009 categories: vec![ChartCategory::new("X1"), ChartCategory::new("X2")],
2010 series: vec![ChartSeries::new("Y", vec![1.5, 2.5])],
2011 title: None,
2012 data_labels: None,
2013 ..Default::default()
2014 };
2015 let xml = Chart::new(ChartType::Line, data).to_xml();
2016 assert!(xml.contains("<c:lineChart>"), "xml: {}", xml);
2017 assert!(!xml.contains("<c:barChart>"), "xml: {}", xml);
2018 assert!(xml.contains("1.5"), "xml: {}", xml);
2020 assert!(xml.contains("2.5"), "xml: {}", xml);
2021 }
2022
2023 #[test]
2025 fn scatter_chart_uses_scatter_elements() {
2026 let data = ChartData {
2027 categories: vec![], series: vec![ChartSeries::new_scatter(
2029 "Series1",
2030 vec![1.0, 2.0, 3.0], vec![2.0, 4.0, 6.0], )],
2033 title: Some("Scatter".to_string()),
2034 data_labels: None,
2035 ..Default::default()
2036 };
2037 let xml = Chart::new(ChartType::Scatter, data).to_xml();
2038 assert!(xml.contains("<c:scatterChart>"), "xml: {}", xml);
2039 assert!(xml.contains("scatterStyle"), "xml: {}", xml);
2040 assert!(xml.contains("<c:xVal>"), "xml: {}", xml);
2041 assert!(xml.contains("<c:yVal>"), "xml: {}", xml);
2042 assert!(!xml.contains("<c:catAx>"), "xml: {}", xml);
2044 let val_ax_count = xml.matches("<c:valAx>").count();
2046 assert_eq!(
2047 val_ax_count, 2,
2048 "expected 2 valAx, got {}: {}",
2049 val_ax_count, xml
2050 );
2051 assert!(!xml.contains("<c:cat>"), "xml: {}", xml);
2053 assert!(xml.contains(">1<"), "xml: {}", xml);
2055 assert!(xml.contains(">3<"), "xml: {}", xml);
2056 }
2057
2058 #[test]
2060 fn area_chart_uses_area_elements() {
2061 let data = ChartData {
2062 categories: vec![ChartCategory::new("A"), ChartCategory::new("B")],
2063 series: vec![ChartSeries::new("S1", vec![10.0, 20.0])],
2064 title: None,
2065 data_labels: None,
2066 ..Default::default()
2067 };
2068 let xml = Chart::new(ChartType::Area, data).to_xml();
2069 assert!(xml.contains("<c:areaChart>"), "xml: {}", xml);
2070 assert!(xml.contains("grouping"), "xml: {}", xml);
2071 assert!(xml.contains("val=\"standard\""), "xml: {}", xml);
2072 assert!(xml.contains("<c:catAx>"), "xml: {}", xml);
2073 assert!(xml.contains("<c:valAx>"), "xml: {}", xml);
2074 assert!(xml.contains("<c:cat>"), "xml: {}", xml);
2076 }
2077
2078 #[test]
2080 fn radar_chart_uses_radar_elements() {
2081 let data = ChartData {
2082 categories: vec![
2083 ChartCategory::new("速度"),
2084 ChartCategory::new("力量"),
2085 ChartCategory::new("技巧"),
2086 ],
2087 series: vec![ChartSeries::new("选手A", vec![8.0, 6.0, 9.0])],
2088 title: Some("Radar".to_string()),
2089 data_labels: None,
2090 ..Default::default()
2091 };
2092 let xml = Chart::new(ChartType::Radar, data).to_xml();
2093 assert!(xml.contains("<c:radarChart>"), "xml: {}", xml);
2094 assert!(xml.contains("radarStyle"), "xml: {}", xml);
2095 assert!(xml.contains("val=\"marker\""), "xml: {}", xml);
2096 assert!(xml.contains("<c:catAx>"), "xml: {}", xml);
2098 assert!(xml.contains("<c:valAx>"), "xml: {}", xml);
2099 assert!(xml.contains("<c:cat>"), "xml: {}", xml);
2101 assert!(!xml.contains("<c:xVal>"), "xml: {}", xml);
2103 assert!(!xml.contains("<c:bubbleSize>"), "xml: {}", xml);
2104 }
2105
2106 #[test]
2108 fn bubble_chart_uses_bubble_elements() {
2109 let data = ChartData {
2110 categories: vec![],
2111 series: vec![ChartSeries::new_bubble(
2112 "S1",
2113 vec![1.0, 2.0, 3.0], vec![10.0, 20.0, 30.0], vec![5.0, 15.0, 25.0], )],
2117 title: Some("Bubble".to_string()),
2118 data_labels: None,
2119 ..Default::default()
2120 };
2121 let xml = Chart::new(ChartType::Bubble, data).to_xml();
2122 assert!(xml.contains("<c:bubbleChart>"), "xml: {}", xml);
2123 assert!(xml.contains("bubbleScale"), "xml: {}", xml);
2124 assert!(xml.contains("val=\"100\""), "xml: {}", xml);
2125 assert!(xml.contains("<c:xVal>"), "xml: {}", xml);
2127 assert!(xml.contains("<c:yVal>"), "xml: {}", xml);
2128 assert!(xml.contains("<c:bubbleSize>"), "xml: {}", xml);
2129 assert!(!xml.contains("<c:catAx>"), "xml: {}", xml);
2131 assert!(xml.contains("<c:valAx>"), "xml: {}", xml);
2132 assert!(xml.contains(">5<"), "xml: {}", xml);
2134 assert!(xml.contains(">25<"), "xml: {}", xml);
2135 }
2136
2137 #[test]
2139 fn col_letter_basic() {
2140 assert_eq!(col_letter(1), "A");
2141 assert_eq!(col_letter(2), "B");
2142 assert_eq!(col_letter(26), "Z");
2143 assert_eq!(col_letter(27), "AA");
2144 assert_eq!(col_letter(28), "AB");
2145 }
2146
2147 #[test]
2149 fn format_f64_integer() {
2150 assert_eq!(format_f64(10.0), "10");
2151 assert_eq!(format_f64(1.5), "1.5");
2152 assert_eq!(format_f64(f64::NAN), "0");
2153 }
2154
2155 #[test]
2161 fn parse_column_chart_round_trip() {
2162 let original = Chart::new(
2163 ChartType::Column,
2164 ChartData {
2165 categories: vec![
2166 ChartCategory::new("Q1"),
2167 ChartCategory::new("Q2"),
2168 ChartCategory::new("Q3"),
2169 ],
2170 series: vec![ChartSeries::new("Sales", vec![10.0, 20.0, 30.0])],
2171 title: Some("Revenue".to_string()),
2172 data_labels: None,
2173 ..Default::default()
2174 },
2175 );
2176 let xml = original.to_xml();
2177 let parsed = Chart::parse_from_xml(&xml).expect("parse column chart");
2178
2179 assert_eq!(parsed.chart_type, ChartType::Column);
2180 assert_eq!(parsed.data.title.as_deref(), Some("Revenue"));
2181 assert_eq!(parsed.data.categories.len(), 3);
2182 assert_eq!(parsed.data.categories[0].name, "Q1");
2183 assert_eq!(parsed.data.categories[2].name, "Q3");
2184 assert_eq!(parsed.data.series.len(), 1);
2185 assert_eq!(parsed.data.series[0].name, "Sales");
2186 assert_eq!(parsed.data.series[0].values, vec![10.0, 20.0, 30.0]);
2187 }
2188
2189 #[test]
2194 fn parse_bar_chart_distinguishes_bar_from_column() {
2195 let original = Chart::new(
2196 ChartType::Bar,
2197 ChartData {
2198 categories: vec![ChartCategory::new("A"), ChartCategory::new("B")],
2199 series: vec![ChartSeries::new("S1", vec![1.0, 2.0])],
2200 title: None,
2201 data_labels: None,
2202 ..Default::default()
2203 },
2204 );
2205 let xml = original.to_xml();
2206 let parsed = Chart::parse_from_xml(&xml).expect("parse bar chart");
2207
2208 assert_eq!(parsed.chart_type, ChartType::Bar);
2210 assert_ne!(parsed.chart_type, ChartType::Column);
2211 assert_eq!(parsed.data.series.len(), 1);
2212 assert_eq!(parsed.data.series[0].values, vec![1.0, 2.0]);
2213 }
2214
2215 #[test]
2217 fn parse_line_chart_round_trip() {
2218 let original = Chart::new(
2219 ChartType::Line,
2220 ChartData {
2221 categories: vec![ChartCategory::new("X1"), ChartCategory::new("X2")],
2222 series: vec![ChartSeries::new("Y", vec![1.5, 2.5])],
2223 title: None,
2224 data_labels: None,
2225 ..Default::default()
2226 },
2227 );
2228 let xml = original.to_xml();
2229 let parsed = Chart::parse_from_xml(&xml).expect("parse line chart");
2230
2231 assert_eq!(parsed.chart_type, ChartType::Line);
2232 assert_eq!(parsed.data.categories.len(), 2);
2233 assert_eq!(parsed.data.series[0].name, "Y");
2234 assert_eq!(parsed.data.series[0].values, vec![1.5, 2.5]);
2236 }
2237
2238 #[test]
2240 fn parse_pie_chart_round_trip() {
2241 let original = Chart::new(
2242 ChartType::Pie,
2243 ChartData {
2244 categories: vec![ChartCategory::new("A"), ChartCategory::new("B")],
2245 series: vec![ChartSeries::new("X", vec![1.0, 2.0])],
2246 title: None,
2247 data_labels: None,
2248 ..Default::default()
2249 },
2250 );
2251 let xml = original.to_xml();
2252 let parsed = Chart::parse_from_xml(&xml).expect("parse pie chart");
2253
2254 assert_eq!(parsed.chart_type, ChartType::Pie);
2255 assert_eq!(parsed.data.categories.len(), 2);
2257 assert_eq!(parsed.data.categories[0].name, "A");
2258 assert_eq!(parsed.data.categories[1].name, "B");
2259 assert_eq!(parsed.data.series.len(), 1);
2260 assert_eq!(parsed.data.series[0].values, vec![1.0, 2.0]);
2261 }
2262
2263 #[test]
2267 fn parse_scatter_chart_round_trip() {
2268 let original = Chart::new(
2269 ChartType::Scatter,
2270 ChartData {
2271 categories: vec![],
2272 series: vec![ChartSeries::new_scatter(
2273 "Series1",
2274 vec![1.0, 2.0, 3.0], vec![2.0, 4.0, 6.0], )],
2277 title: Some("Scatter".to_string()),
2278 data_labels: None,
2279 ..Default::default()
2280 },
2281 );
2282 let xml = original.to_xml();
2283 let parsed = Chart::parse_from_xml(&xml).expect("parse scatter chart");
2284
2285 assert_eq!(parsed.chart_type, ChartType::Scatter);
2286 assert_eq!(parsed.data.title.as_deref(), Some("Scatter"));
2287 assert_eq!(parsed.data.series.len(), 1);
2288 let s = &parsed.data.series[0];
2289 assert_eq!(s.name, "Series1");
2290 assert_eq!(s.values, vec![2.0, 4.0, 6.0]);
2292 assert_eq!(s.x_values.as_deref(), Some(&[1.0, 2.0, 3.0][..]));
2294 assert!(s.bubble_sizes.is_none());
2296 }
2297
2298 #[test]
2300 fn parse_bubble_chart_round_trip() {
2301 let original = Chart::new(
2302 ChartType::Bubble,
2303 ChartData {
2304 categories: vec![],
2305 series: vec![ChartSeries::new_bubble(
2306 "S1",
2307 vec![1.0, 2.0, 3.0], vec![10.0, 20.0, 30.0], vec![5.0, 15.0, 25.0], )],
2311 title: Some("Bubble".to_string()),
2312 data_labels: None,
2313 ..Default::default()
2314 },
2315 );
2316 let xml = original.to_xml();
2317 let parsed = Chart::parse_from_xml(&xml).expect("parse bubble chart");
2318
2319 assert_eq!(parsed.chart_type, ChartType::Bubble);
2320 assert_eq!(parsed.data.series.len(), 1);
2321 let s = &parsed.data.series[0];
2322 assert_eq!(s.name, "S1");
2323 assert_eq!(s.values, vec![10.0, 20.0, 30.0]);
2324 assert_eq!(s.x_values.as_deref(), Some(&[1.0, 2.0, 3.0][..]));
2325 assert_eq!(s.bubble_sizes.as_deref(), Some(&[5.0, 15.0, 25.0][..]));
2326 }
2327
2328 #[test]
2330 fn parse_radar_chart_round_trip() {
2331 let original = Chart::new(
2332 ChartType::Radar,
2333 ChartData {
2334 categories: vec![
2335 ChartCategory::new("速度"),
2336 ChartCategory::new("力量"),
2337 ChartCategory::new("技巧"),
2338 ],
2339 series: vec![ChartSeries::new("选手A", vec![8.0, 6.0, 9.0])],
2340 title: Some("Radar".to_string()),
2341 data_labels: None,
2342 ..Default::default()
2343 },
2344 );
2345 let xml = original.to_xml();
2346 let parsed = Chart::parse_from_xml(&xml).expect("parse radar chart");
2347
2348 assert_eq!(parsed.chart_type, ChartType::Radar);
2349 assert_eq!(parsed.data.categories.len(), 3);
2350 assert_eq!(parsed.data.categories[0].name, "速度");
2351 assert_eq!(parsed.data.series[0].values, vec![8.0, 6.0, 9.0]);
2352 }
2353
2354 #[test]
2356 fn parse_multi_series_column_chart() {
2357 let original = Chart::new(
2358 ChartType::Column,
2359 ChartData {
2360 categories: vec![ChartCategory::new("A"), ChartCategory::new("B")],
2361 series: vec![
2362 ChartSeries::new("S1", vec![1.0, 2.0]),
2363 ChartSeries::new("S2", vec![3.0, 4.0]),
2364 ChartSeries::new("S3", vec![5.0, 6.0]),
2365 ],
2366 title: None,
2367 data_labels: None,
2368 ..Default::default()
2369 },
2370 );
2371 let xml = original.to_xml();
2372 let parsed = Chart::parse_from_xml(&xml).expect("parse multi-series chart");
2373
2374 assert_eq!(parsed.data.series.len(), 3);
2375 assert_eq!(parsed.data.series[0].name, "S1");
2376 assert_eq!(parsed.data.series[1].name, "S2");
2377 assert_eq!(parsed.data.series[2].name, "S3");
2378 assert_eq!(parsed.data.series[2].values, vec![5.0, 6.0]);
2379 }
2380
2381 #[test]
2383 fn parse_chart_without_title() {
2384 let original = Chart::new(
2385 ChartType::Column,
2386 ChartData {
2387 categories: vec![ChartCategory::new("A")],
2388 series: vec![ChartSeries::new("S", vec![1.0])],
2389 title: None,
2390 data_labels: None,
2391 ..Default::default()
2392 },
2393 );
2394 let xml = original.to_xml();
2395 let parsed = Chart::parse_from_xml(&xml).expect("parse no-title chart");
2396
2397 assert!(parsed.data.title.is_none());
2398 }
2399
2400 #[test]
2404 fn parse_empty_chart_space_no_panic() {
2405 let xml = "<?xml version=\"1.0\"?>\
2406 <c:chartSpace xmlns:c=\"http://schemas.openxmlformats.org/drawingml/2006/chart\">\
2407 <c:chart></c:chart>\
2408 </c:chartSpace>";
2409 let parsed = Chart::parse_from_xml(xml).expect("parse empty chartSpace");
2410 assert_eq!(parsed.chart_type, ChartType::Column);
2412 assert!(parsed.data.series.is_empty());
2413 assert!(parsed.data.categories.is_empty());
2414 }
2415
2416 #[test]
2418 fn parse_malformed_xml_returns_error() {
2419 let xml = "<c:chartSpace><!-- unclosed comment <c:chart/></c:chartSpace>";
2423 let result = Chart::parse_from_xml(xml);
2424 assert!(result.is_err(), "malformed xml should error");
2425 }
2426
2427 #[test]
2430 fn to_xml_writes_external_data() {
2431 let mut chart = Chart::new(
2432 ChartType::Column,
2433 ChartData {
2434 categories: vec![ChartCategory::new("A")],
2435 series: vec![ChartSeries::new("S", vec![1.0])],
2436 title: None,
2437 data_labels: None,
2438 ..Default::default()
2439 },
2440 );
2441 chart.external_data_rid = Some("rIdXlsx1".to_string());
2442 let xml = chart.to_xml();
2443 assert!(
2445 xml.contains(r#"<c:externalData r:id="rIdXlsx1">"#),
2446 "应输出 c:externalData 开标签"
2447 );
2448 assert!(
2449 xml.contains(r#"<c:autoUpdate val="0"/>"#),
2450 "应输出 autoUpdate=0"
2451 );
2452 assert!(xml.contains("</c:externalData>"), "应闭合 c:externalData");
2453 let pos_chart_close = xml.find("</c:chart>").expect("c:chart 关闭存在");
2455 let pos_ext = xml.find("<c:externalData").expect("c:externalData 存在");
2456 assert!(
2457 pos_chart_close < pos_ext,
2458 "c:externalData 必须在 </c:chart> 之后"
2459 );
2460 let pos_chart_space_close = xml.rfind("</c:chartSpace>").expect("c:chartSpace 关闭存在");
2461 assert!(
2462 pos_ext < pos_chart_space_close,
2463 "c:externalData 必须在 </c:chartSpace> 之前"
2464 );
2465 }
2466
2467 #[test]
2469 fn to_xml_omits_external_data_when_none() {
2470 let chart = Chart::new(
2471 ChartType::Line,
2472 ChartData {
2473 categories: vec![ChartCategory::new("A")],
2474 series: vec![ChartSeries::new("S", vec![1.0])],
2475 title: None,
2476 data_labels: None,
2477 ..Default::default()
2478 },
2479 );
2480 let xml = chart.to_xml();
2481 assert!(
2482 !xml.contains("c:externalData"),
2483 "None 时不应输出 c:externalData"
2484 );
2485 }
2486
2487 #[test]
2489 fn parse_external_data_rid_round_trip() {
2490 let mut original = Chart::new(
2491 ChartType::Column,
2492 ChartData {
2493 categories: vec![ChartCategory::new("Q1"), ChartCategory::new("Q2")],
2494 series: vec![ChartSeries::new("Sales", vec![10.0, 20.0])],
2495 title: Some("Revenue".into()),
2496 data_labels: None,
2497 ..Default::default()
2498 },
2499 );
2500 original.external_data_rid = Some("rIdXlsx1".into());
2501 let xml = original.to_xml();
2502 let parsed = Chart::parse_from_xml(&xml).expect("parse round-trip");
2503 assert_eq!(parsed.chart_type, ChartType::Column);
2504 assert_eq!(parsed.external_data_rid, Some("rIdXlsx1".to_string()));
2505 }
2506
2507 #[test]
2509 fn parse_external_data_rid_bare_id_form() {
2510 let xml = "<?xml version=\"1.0\"?>\
2511 <c:chartSpace xmlns:c=\"http://schemas.openxmlformats.org/drawingml/2006/chart\" \
2512 xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">\
2513 <c:chart><c:plotArea><c:barChart><c:barDir val=\"col\"/>\
2514 <c:ser><c:idx val=\"0\"/><c:order val=\"0\"/>\
2515 <c:val><c:numCache><c:formatCode>General</c:formatCode>\
2516 <c:ptCount val=\"1\"/><c:pt idx=\"0\"><c:v>1</c:v></c:pt>\
2517 </c:numCache></c:val>\
2518 </c:ser></c:barChart></c:plotArea></c:chart>\
2519 <c:externalData id=\"rIdBare\"><c:autoUpdate val=\"0\"/></c:externalData>\
2520 </c:chartSpace>";
2521 let parsed = Chart::parse_from_xml(xml).expect("parse bare id form");
2522 assert_eq!(parsed.external_data_rid, Some("rIdBare".to_string()));
2523 }
2524
2525 #[test]
2529 fn data_labels_show_values_constructor() {
2530 let dl = DataLabels::show_values();
2531 assert_eq!(dl.show_val, Some(true));
2532 assert_eq!(dl.show_cat_name, None);
2533 assert_eq!(dl.show_ser_name, None);
2534 assert!(!dl.is_empty());
2535 }
2536
2537 #[test]
2539 fn data_labels_show_percent_pie_constructor() {
2540 let dl = DataLabels::show_percent_pie();
2541 assert_eq!(dl.show_percent, Some(true));
2542 assert_eq!(dl.show_val, None);
2543 assert!(!dl.is_empty());
2544 }
2545
2546 #[test]
2548 fn data_labels_is_empty_for_default() {
2549 let dl = DataLabels::default();
2550 assert!(dl.is_empty());
2551 }
2552
2553 #[test]
2555 fn label_position_round_trip() {
2556 for pos in [
2557 LabelPosition::BestFit,
2558 LabelPosition::Above,
2559 LabelPosition::Below,
2560 LabelPosition::Center,
2561 LabelPosition::InsideEnd,
2562 LabelPosition::OutsideEnd,
2563 LabelPosition::Left,
2564 LabelPosition::Right,
2565 ] {
2566 let s = pos.as_str();
2567 let parsed = LabelPosition::parse(s).expect("known position should parse");
2569 assert_eq!(parsed, pos, "position round-trip failed for {:?}", pos);
2570 }
2571 assert!(LabelPosition::parse("unknown").is_none());
2573 }
2574
2575 #[test]
2579 fn parse_chart_level_data_labels_round_trip() {
2580 let dl = DataLabels {
2581 show_val: Some(true),
2582 show_cat_name: Some(true),
2583 show_ser_name: Some(false),
2584 show_legend_key: Some(false),
2585 show_percent: None,
2586 show_bubble_size: None,
2587 position: Some(LabelPosition::OutsideEnd),
2588 separator: Some(", ".to_string()),
2589 num_fmt: Some("0.00".to_string()),
2590 };
2591 let original = Chart::new(
2592 ChartType::Column,
2593 ChartData {
2594 categories: vec![ChartCategory::new("Q1"), ChartCategory::new("Q2")],
2595 series: vec![ChartSeries::new("Sales", vec![10.0, 20.0])],
2596 title: None,
2597 data_labels: Some(dl.clone()),
2598 ..Default::default()
2599 },
2600 );
2601 let xml = original.to_xml();
2602 assert!(xml.contains("<c:dLbls>"), "xml: {}", xml);
2604 assert!(xml.contains(r#"<c:showVal val="1"/>"#), "xml: {}", xml);
2605 assert!(xml.contains(r#"<c:showCatName val="1"/>"#), "xml: {}", xml);
2606 assert!(xml.contains(r#"<c:showSerName val="0"/>"#), "xml: {}", xml);
2607 assert!(
2608 xml.contains(r#"<c:showLegendKey val="0"/>"#),
2609 "xml: {}",
2610 xml
2611 );
2612 assert!(xml.contains(r#"<c:dLblPos val="outEnd"/>"#), "xml: {}", xml);
2613 assert!(xml.contains(r#"<c:separator val=", "/>"#), "xml: {}", xml);
2614 assert!(xml.contains(r#"formatCode="0.00""#), "xml: {}", xml);
2615
2616 let parsed = Chart::parse_from_xml(&xml).expect("parse chart with dLbls");
2618 let parsed_dl = parsed
2619 .data
2620 .data_labels
2621 .expect("chart-level dLbls should parse");
2622 assert_eq!(parsed_dl.show_val, Some(true));
2623 assert_eq!(parsed_dl.show_cat_name, Some(true));
2624 assert_eq!(parsed_dl.show_ser_name, Some(false));
2625 assert_eq!(parsed_dl.show_legend_key, Some(false));
2626 assert_eq!(parsed_dl.show_percent, None);
2627 assert_eq!(parsed_dl.position, Some(LabelPosition::OutsideEnd));
2628 assert_eq!(parsed_dl.separator.as_deref(), Some(", "));
2629 assert_eq!(parsed_dl.num_fmt.as_deref(), Some("0.00"));
2630 }
2631
2632 #[test]
2634 fn parse_series_level_data_labels_round_trip() {
2635 let series_dl = DataLabels {
2636 show_val: Some(true),
2637 show_percent: Some(true),
2638 position: Some(LabelPosition::InsideEnd),
2639 ..Default::default()
2640 };
2641 let original = Chart::new(
2642 ChartType::Pie,
2643 ChartData {
2644 categories: vec![ChartCategory::new("A"), ChartCategory::new("B")],
2645 series: vec![{
2646 let mut s = ChartSeries::new("X", vec![1.0, 2.0]);
2647 s.data_labels = Some(series_dl.clone());
2648 s
2649 }],
2650 title: None,
2651 data_labels: None,
2652 ..Default::default()
2653 },
2654 );
2655 let xml = original.to_xml();
2656 assert!(xml.contains("<c:dLbls>"), "xml: {}", xml);
2658 assert!(xml.contains(r#"<c:showVal val="1"/>"#), "xml: {}", xml);
2659 assert!(xml.contains(r#"<c:showPercent val="1"/>"#), "xml: {}", xml);
2660 assert!(xml.contains(r#"<c:dLblPos val="inEnd"/>"#), "xml: {}", xml);
2661
2662 let parsed = Chart::parse_from_xml(&xml).expect("parse pie with series dLbls");
2663 assert_eq!(parsed.chart_type, ChartType::Pie);
2664 assert!(parsed.data.data_labels.is_none());
2666 let s = &parsed.data.series[0];
2668 let parsed_dl = s.data_labels.as_ref().expect("series dLbls should parse");
2669 assert_eq!(parsed_dl.show_val, Some(true));
2670 assert_eq!(parsed_dl.show_percent, Some(true));
2671 assert_eq!(parsed_dl.position, Some(LabelPosition::InsideEnd));
2672 }
2673
2674 #[test]
2676 fn parse_empty_dlbls_is_ignored() {
2677 let original = Chart::new(
2679 ChartType::Column,
2680 ChartData {
2681 categories: vec![ChartCategory::new("A")],
2682 series: vec![ChartSeries::new("S", vec![1.0])],
2683 title: None,
2684 data_labels: Some(DataLabels::default()), ..Default::default()
2686 },
2687 );
2688 let xml = original.to_xml();
2689 assert!(
2691 !xml.contains("<c:dLbls>"),
2692 "empty dLbls should not be written: {}",
2693 xml
2694 );
2695 let parsed = Chart::parse_from_xml(&xml).expect("parse");
2697 assert!(parsed.data.data_labels.is_none());
2698 }
2699
2700 #[test]
2704 fn to_xml_writes_secondary_axis() {
2705 let s2 = {
2706 let mut s = ChartSeries::new("Secondary", vec![1.0, 2.0]);
2707 s.secondary_axis = true;
2708 s
2709 };
2710 let original = Chart::new(
2711 ChartType::Column,
2712 ChartData {
2713 categories: vec![ChartCategory::new("Q1"), ChartCategory::new("Q2")],
2714 series: vec![s2],
2715 title: None,
2716 data_labels: None,
2717 ..Default::default()
2718 },
2719 );
2720 let xml = original.to_xml();
2721 assert!(xml.contains(r#"<c:axId val="444444444"/>"#), "xml: {}", xml);
2723 assert!(xml.contains(r#"<c:crosses val="max"/>"#), "xml: {}", xml);
2725 let val_ax_count = xml.matches("<c:valAx>").count();
2727 assert_eq!(
2728 val_ax_count, 2,
2729 "expected 2 valAx (primary + secondary): {}",
2730 xml
2731 );
2732 assert!(xml.contains("<c:catAx>"), "xml: {}", xml);
2733 }
2734
2735 #[test]
2738 fn parse_secondary_axis_round_trip() {
2739 let original = Chart::new(
2740 ChartType::Column,
2741 ChartData {
2742 categories: vec![ChartCategory::new("Q1"), ChartCategory::new("Q2")],
2743 series: vec![{
2744 let mut s = ChartSeries::new("S", vec![10.0, 20.0]);
2745 s.secondary_axis = true;
2746 s
2747 }],
2748 title: None,
2749 data_labels: None,
2750 ..Default::default()
2751 },
2752 );
2753 let xml = original.to_xml();
2754 let parsed = Chart::parse_from_xml(&xml).expect("parse secondary axis chart");
2755 assert_eq!(parsed.data.series.len(), 1);
2757 assert!(
2758 parsed.data.series[0].secondary_axis,
2759 "secondary_axis should be true after round-trip"
2760 );
2761 }
2762
2763 #[test]
2765 fn pie_chart_ignores_secondary_axis() {
2766 let original = Chart::new(
2767 ChartType::Pie,
2768 ChartData {
2769 categories: vec![ChartCategory::new("A"), ChartCategory::new("B")],
2770 series: vec![{
2771 let mut s = ChartSeries::new("X", vec![1.0, 2.0]);
2772 s.secondary_axis = true; s
2774 }],
2775 title: None,
2776 data_labels: None,
2777 ..Default::default()
2778 },
2779 );
2780 let xml = original.to_xml();
2781 assert!(
2783 !xml.contains("444444444"),
2784 "pie should not write secondary axId: {}",
2785 xml
2786 );
2787 assert!(
2788 !xml.contains(r#"<c:crosses val="max"/>"#),
2789 "pie should not write crosses=max: {}",
2790 xml
2791 );
2792 }
2793
2794 #[test]
2796 fn scatter_chart_ignores_secondary_axis() {
2797 let mut s = ChartSeries::new_scatter("S", vec![1.0, 2.0], vec![3.0, 4.0]);
2798 s.secondary_axis = true; let original = Chart::new(
2800 ChartType::Scatter,
2801 ChartData {
2802 categories: vec![],
2803 series: vec![s],
2804 title: None,
2805 data_labels: None,
2806 ..Default::default()
2807 },
2808 );
2809 let xml = original.to_xml();
2810 assert!(
2811 !xml.contains("444444444"),
2812 "scatter should not write secondary axId: {}",
2813 xml
2814 );
2815 assert!(
2816 !xml.contains(r#"<c:crosses val="max"/>"#),
2817 "scatter should not write crosses=max: {}",
2818 xml
2819 );
2820 }
2821
2822 #[test]
2830 fn legend_round_trip() {
2831 let original = Chart::new(
2832 ChartType::Column,
2833 ChartData {
2834 categories: vec![ChartCategory::new("Q1"), ChartCategory::new("Q2")],
2835 series: vec![ChartSeries::new("S", vec![10.0, 20.0])],
2836 title: Some("Legend Test".to_string()),
2837 data_labels: None,
2838 legend: Some(Legend {
2839 position: LegendPosition::Top,
2840 overlay: false,
2841 }),
2842 ..Default::default()
2843 },
2844 );
2845 let xml = original.to_xml();
2846 assert!(xml.contains("<c:legend>"), "xml: {}", xml);
2848 assert!(xml.contains(r#"<c:legendPos val="t"/>"#), "xml: {}", xml);
2849 assert!(xml.contains(r#"<c:overlay val="0"/>"#), "xml: {}", xml);
2850
2851 let parsed = Chart::parse_from_xml(&xml).expect("parse legend chart");
2852 assert_eq!(
2853 parsed
2854 .data
2855 .legend
2856 .as_ref()
2857 .expect("legend should exist")
2858 .position,
2859 LegendPosition::Top,
2860 "legend position should round-trip"
2861 );
2862 assert!(
2863 !parsed.data.legend.as_ref().unwrap().overlay,
2864 "legend overlay should round-trip"
2865 );
2866 }
2867
2868 #[test]
2871 fn no_legend_round_trip() {
2872 let original = Chart::new(
2873 ChartType::Line,
2874 ChartData {
2875 categories: vec![ChartCategory::new("A"), ChartCategory::new("B")],
2876 series: vec![ChartSeries::new("S", vec![1.0, 2.0])],
2877 title: None,
2878 data_labels: None,
2879 legend: None, ..Default::default()
2881 },
2882 );
2883 let xml = original.to_xml();
2884 assert!(
2885 !xml.contains("<c:legend>"),
2886 "should not write legend: {}",
2887 xml
2888 );
2889
2890 let parsed = Chart::parse_from_xml(&xml).expect("parse no-legend chart");
2891 assert!(
2892 parsed.data.legend.is_none(),
2893 "legend should remain None after round-trip"
2894 );
2895 }
2896
2897 #[test]
2899 fn legend_all_positions_round_trip() {
2900 for (pos, val) in [
2901 (LegendPosition::Bottom, "b"),
2902 (LegendPosition::Top, "t"),
2903 (LegendPosition::Right, "r"),
2904 (LegendPosition::Left, "l"),
2905 ] {
2906 let original = Chart::new(
2907 ChartType::Column,
2908 ChartData {
2909 categories: vec![ChartCategory::new("A")],
2910 series: vec![ChartSeries::new("S", vec![1.0])],
2911 title: None,
2912 data_labels: None,
2913 legend: Some(Legend {
2914 position: pos,
2915 overlay: true,
2916 }),
2917 ..Default::default()
2918 },
2919 );
2920 let xml = original.to_xml();
2921 assert!(
2922 xml.contains(&format!(r#"<c:legendPos val="{}"/>"#, val)),
2923 "position {:?} should write val={}: {}",
2924 pos,
2925 val,
2926 xml
2927 );
2928 let parsed = Chart::parse_from_xml(&xml).expect("parse legend positions");
2929 assert_eq!(
2930 parsed.data.legend.as_ref().expect("legend").position,
2931 pos,
2932 "position {:?} should round-trip",
2933 pos
2934 );
2935 assert!(
2936 parsed.data.legend.as_ref().unwrap().overlay,
2937 "overlay=true should round-trip for position {:?}",
2938 pos
2939 );
2940 }
2941 }
2942
2943 #[test]
2948 fn series_format_solid_color_round_trip() {
2949 use crate::oxml::color::Color;
2950 use crate::oxml::sppr::Fill;
2951 use crate::units::RGBColor;
2952 let original = Chart::new(
2953 ChartType::Column,
2954 ChartData {
2955 categories: vec![ChartCategory::new("A"), ChartCategory::new("B")],
2956 series: vec![{
2957 let mut s = ChartSeries::new("S", vec![10.0, 20.0]);
2958 s.format = Some(SeriesFormat::solid_fill(Color::RGB(RGBColor(
2959 0xFF, 0x00, 0x00,
2960 ))));
2961 s
2962 }],
2963 title: None,
2964 data_labels: None,
2965 ..Default::default()
2966 },
2967 );
2968 let xml = original.to_xml();
2969 assert!(xml.contains("<c:spPr>"), "xml should contain spPr: {}", xml);
2971 assert!(
2972 xml.contains(r#"<a:srgbClr val="FF0000"/>"#),
2973 "xml should contain red color: {}",
2974 xml
2975 );
2976
2977 let parsed = Chart::parse_from_xml(&xml).expect("parse series_format chart");
2978 let fmt = parsed.data.series[0]
2979 .format
2980 .as_ref()
2981 .expect("format should round-trip");
2982 match &fmt.fill {
2984 Fill::Solid(Color::RGB(c)) => {
2985 assert_eq!(c.0, 0xFF, "red component");
2986 assert_eq!(c.1, 0x00, "green component");
2987 assert_eq!(c.2, 0x00, "blue component");
2988 }
2989 other => panic!("expected Fill::Solid(Color::RGB), got {:?}", other),
2990 }
2991 }
2992
2993 #[test]
2995 fn series_format_none_no_sp_pr() {
2996 let original = Chart::new(
2997 ChartType::Line,
2998 ChartData {
2999 categories: vec![ChartCategory::new("X")],
3000 series: vec![ChartSeries::new("S", vec![1.0])],
3001 title: None,
3002 data_labels: None,
3003 ..Default::default()
3004 },
3005 );
3006 let xml = original.to_xml();
3007 assert!(
3008 !xml.contains("<c:spPr>"),
3009 "should not write spPr when format is None: {}",
3010 xml
3011 );
3012
3013 let parsed = Chart::parse_from_xml(&xml).expect("parse no-format chart");
3014 assert!(
3015 parsed.data.series[0].format.is_none(),
3016 "format should remain None after round-trip"
3017 );
3018 }
3019
3020 #[test]
3028 fn chart_combo_round_trip() {
3029 let mut s1 = ChartSeries::new("Sales", vec![10.0, 20.0, 15.0]);
3030 s1.secondary_axis = false;
3031 let mut s2 = ChartSeries::new("Growth", vec![5.0, 8.0, 12.0]);
3032 s2.secondary_axis = true;
3033
3034 let original = Chart::new(
3035 ChartType::Column,
3036 ChartData {
3037 categories: vec![
3038 ChartCategory::new("Q1"),
3039 ChartCategory::new("Q2"),
3040 ChartCategory::new("Q3"),
3041 ],
3042 series: vec![s1, s2],
3043 title: Some("Combo Chart".to_string()),
3044 data_labels: None,
3045 secondary_chart_type: Some(ChartType::Line),
3046 ..Default::default()
3047 },
3048 );
3049
3050 let xml = original.to_xml();
3051 assert!(
3053 xml.contains("<c:barChart>"),
3054 "should contain primary c:barChart: {}",
3055 xml
3056 );
3057 assert!(
3058 xml.contains("<c:lineChart>"),
3059 "should contain secondary c:lineChart: {}",
3060 xml
3061 );
3062 assert!(
3064 xml.contains(r#"<c:crosses val="max"/>"#),
3065 "should contain secondary axis crosses=max: {}",
3066 xml
3067 );
3068
3069 let parsed = Chart::parse_from_xml(&xml).expect("parse combo chart");
3070 assert_eq!(
3072 parsed.data.secondary_chart_type,
3073 Some(ChartType::Line),
3074 "secondary_chart_type should round-trip"
3075 );
3076 assert_eq!(parsed.chart_type, ChartType::Column);
3078 assert_eq!(parsed.data.series.len(), 2, "should have 2 series");
3080 assert!(
3082 !parsed.data.series[0].secondary_axis,
3083 "series 0 (Sales) should be on primary axis"
3084 );
3085 assert!(
3086 parsed.data.series[1].secondary_axis,
3087 "series 1 (Growth) should be on secondary axis"
3088 );
3089 }
3090
3091 #[test]
3096 fn chart_combo_series_distribution() {
3097 let mut s1 = ChartSeries::new("Bar1", vec![1.0, 2.0]);
3098 s1.secondary_axis = false;
3099 let mut s2 = ChartSeries::new("Bar2", vec![3.0, 4.0]);
3100 s2.secondary_axis = false;
3101 let mut s3 = ChartSeries::new("Line1", vec![5.0, 6.0]);
3102 s3.secondary_axis = true;
3103
3104 let chart = Chart::new(
3105 ChartType::Column,
3106 ChartData {
3107 categories: vec![ChartCategory::new("A"), ChartCategory::new("B")],
3108 series: vec![s1, s2, s3],
3109 title: None,
3110 data_labels: None,
3111 secondary_chart_type: Some(ChartType::Line),
3112 ..Default::default()
3113 },
3114 );
3115
3116 let xml = chart.to_xml();
3117 let ser_count = xml.matches("<c:ser>").count();
3119 assert_eq!(ser_count, 3, "should have 3 c:ser elements total: {}", xml);
3120
3121 assert!(
3123 xml.contains(r#"<c:axId val="444444444"/>"#),
3124 "should contain secondary axId 444444444: {}",
3125 xml
3126 );
3127 }
3128}