1use crate::oxml::writer::XmlWriter;
23
24#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
26pub enum ChartType {
27 Column,
29 Bar,
31 Line,
33 Pie,
35 Scatter,
37 Area,
39 Radar,
41 Bubble,
43}
44
45impl ChartType {
46 pub fn chart_element(self) -> &'static str {
48 match self {
49 ChartType::Column | ChartType::Bar => "c:barChart",
50 ChartType::Line => "c:lineChart",
51 ChartType::Pie => "c:pieChart",
52 ChartType::Scatter => "c:scatterChart",
53 ChartType::Area => "c:areaChart",
54 ChartType::Radar => "c:radarChart",
55 ChartType::Bubble => "c:bubbleChart",
56 }
57 }
58
59 pub fn bar_dir(self) -> &'static str {
61 match self {
62 ChartType::Column => "col",
63 ChartType::Bar => "bar",
64 _ => "",
65 }
66 }
67
68 pub fn is_scatter(self) -> bool {
70 matches!(self, ChartType::Scatter)
71 }
72
73 pub fn is_xy_chart(self) -> bool {
77 matches!(self, ChartType::Scatter | ChartType::Bubble)
78 }
79
80 pub fn is_bubble(self) -> bool {
82 matches!(self, ChartType::Bubble)
83 }
84}
85
86#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
91pub enum LabelPosition {
92 #[default]
94 BestFit,
95 Above,
97 Below,
99 Center,
101 Base,
103 InsideBase,
105 InsideEnd,
107 OutsideEnd,
109 Left,
111 Right,
113 OutsideEndPie,
115 InsideEndPie,
117}
118
119impl LabelPosition {
120 pub fn as_str(self) -> &'static str {
122 match self {
123 LabelPosition::BestFit => "bestFit",
124 LabelPosition::Above => "t",
125 LabelPosition::Below => "b",
126 LabelPosition::Center => "ctr",
127 LabelPosition::Base => "inBase",
128 LabelPosition::InsideBase => "inBase",
129 LabelPosition::InsideEnd => "inEnd",
130 LabelPosition::OutsideEnd => "outEnd",
131 LabelPosition::Left => "l",
132 LabelPosition::Right => "r",
133 LabelPosition::OutsideEndPie => "outEnd",
134 LabelPosition::InsideEndPie => "inEnd",
135 }
136 }
137
138 pub fn parse(s: &str) -> Option<Self> {
142 match s {
143 "bestFit" => Some(LabelPosition::BestFit),
144 "t" => Some(LabelPosition::Above),
145 "b" => Some(LabelPosition::Below),
146 "ctr" => Some(LabelPosition::Center),
147 "inBase" => Some(LabelPosition::Base),
148 "inEnd" => Some(LabelPosition::InsideEnd),
149 "outEnd" => Some(LabelPosition::OutsideEnd),
150 "l" => Some(LabelPosition::Left),
151 "r" => Some(LabelPosition::Right),
152 _ => None,
153 }
154 }
155}
156
157#[derive(Clone, Debug, Default)]
174pub struct DataLabels {
175 pub show_val: Option<bool>,
177 pub show_cat_name: Option<bool>,
179 pub show_ser_name: Option<bool>,
181 pub show_legend_key: Option<bool>,
183 pub show_percent: Option<bool>,
185 pub show_bubble_size: Option<bool>,
187 pub position: Option<LabelPosition>,
189 pub separator: Option<String>,
191 pub num_fmt: Option<String>,
193}
194
195impl DataLabels {
196 pub fn show_values() -> Self {
198 DataLabels {
199 show_val: Some(true),
200 ..Default::default()
201 }
202 }
203
204 pub fn show_percent_pie() -> Self {
206 DataLabels {
207 show_percent: Some(true),
208 ..Default::default()
209 }
210 }
211
212 pub fn write_xml(&self, w: &mut XmlWriter) {
222 w.open("c:dLbls");
223 if let Some(b) = self.show_legend_key {
224 w.empty_with("c:showLegendKey", &[("val", if b { "1" } else { "0" })]);
225 }
226 if let Some(b) = self.show_val {
227 w.empty_with("c:showVal", &[("val", if b { "1" } else { "0" })]);
228 }
229 if let Some(b) = self.show_cat_name {
230 w.empty_with("c:showCatName", &[("val", if b { "1" } else { "0" })]);
231 }
232 if let Some(b) = self.show_ser_name {
233 w.empty_with("c:showSerName", &[("val", if b { "1" } else { "0" })]);
234 }
235 if let Some(b) = self.show_percent {
236 w.empty_with("c:showPercent", &[("val", if b { "1" } else { "0" })]);
237 }
238 if let Some(b) = self.show_bubble_size {
239 w.empty_with("c:showBubbleSize", &[("val", if b { "1" } else { "0" })]);
240 }
241 if let Some(pos) = self.position {
242 w.empty_with("c:dLblPos", &[("val", pos.as_str())]);
243 }
244 if let Some(sep) = &self.separator {
245 w.empty_with("c:separator", &[("val", sep.as_str())]);
246 }
247 if let Some(fmt) = &self.num_fmt {
248 w.empty_with(
249 "c:numFmt",
250 &[("formatCode", fmt.as_str()), ("sourceLinked", "0")],
251 );
252 }
253 w.close("c:dLbls");
254 }
255
256 pub fn is_empty(&self) -> bool {
258 self.show_val.is_none()
259 && self.show_cat_name.is_none()
260 && self.show_ser_name.is_none()
261 && self.show_legend_key.is_none()
262 && self.show_percent.is_none()
263 && self.show_bubble_size.is_none()
264 && self.position.is_none()
265 && self.separator.is_none()
266 && self.num_fmt.is_none()
267 }
268}
269
270#[derive(Clone, Debug, Default)]
272pub struct ChartData {
273 pub categories: Vec<ChartCategory>,
277 pub series: Vec<ChartSeries>,
279 pub title: Option<String>,
281 pub data_labels: Option<DataLabels>,
286}
287
288#[derive(Clone, Debug, Default)]
290pub struct ChartCategory {
291 pub name: String,
293}
294
295impl ChartCategory {
296 pub fn new(name: impl Into<String>) -> Self {
298 ChartCategory { name: name.into() }
299 }
300}
301
302#[derive(Clone, Debug, Default)]
304pub struct ChartSeries {
305 pub name: String,
307 pub values: Vec<f64>,
311 pub x_values: Option<Vec<f64>>,
316 pub bubble_sizes: Option<Vec<f64>>,
320 pub data_labels: Option<DataLabels>,
324 pub secondary_axis: bool,
333}
334
335impl ChartSeries {
336 pub fn new(name: impl Into<String>, values: Vec<f64>) -> Self {
340 ChartSeries {
341 name: name.into(),
342 values,
343 x_values: None,
344 bubble_sizes: None,
345 data_labels: None,
346 secondary_axis: false,
347 }
348 }
349
350 pub fn new_scatter(name: impl Into<String>, x_values: Vec<f64>, y_values: Vec<f64>) -> Self {
359 ChartSeries {
360 name: name.into(),
361 values: y_values,
362 x_values: Some(x_values),
363 bubble_sizes: None,
364 data_labels: None,
365 secondary_axis: false,
366 }
367 }
368
369 pub fn new_bubble(
379 name: impl Into<String>,
380 x_values: Vec<f64>,
381 y_values: Vec<f64>,
382 bubble_sizes: Vec<f64>,
383 ) -> Self {
384 ChartSeries {
385 name: name.into(),
386 values: y_values,
387 x_values: Some(x_values),
388 bubble_sizes: Some(bubble_sizes),
389 data_labels: None,
390 secondary_axis: false,
391 }
392 }
393}
394
395#[derive(Clone, Debug)]
397pub struct Chart {
398 pub chart_type: ChartType,
400 pub data: ChartData,
402 pub rid: String,
407 pub external_data_rid: Option<String>,
420}
421
422impl Chart {
423 pub fn new(chart_type: ChartType, data: ChartData) -> Self {
425 Chart {
426 chart_type,
427 data,
428 rid: String::new(),
429 external_data_rid: None,
430 }
431 }
432
433 pub fn parse_from_xml(xml: &str) -> crate::Result<Chart> {
443 parse_from_xml(xml)
444 }
445}
446
447impl Default for Chart {
448 fn default() -> Self {
450 Chart {
451 chart_type: ChartType::Column,
452 data: ChartData::default(),
453 rid: String::new(),
454 external_data_rid: None,
455 }
456 }
457}
458
459impl Chart {
460 pub fn to_xml(&self) -> String {
489 let mut w = XmlWriter::new();
490 w.raw("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n");
491 w.open_with(
492 "c:chartSpace",
493 &[
494 (
495 "xmlns:c",
496 "http://schemas.openxmlformats.org/drawingml/2006/chart",
497 ),
498 (
499 "xmlns:a",
500 "http://schemas.openxmlformats.org/drawingml/2006/main",
501 ),
502 (
503 "xmlns:r",
504 "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
505 ),
506 ],
507 );
508 w.open("c:chart");
509 if let Some(title) = &self.data.title {
511 w.open("c:title");
512 w.open("a:tx");
513 w.open("a:rich");
514 w.open("a:bodyPr");
515 w.close("a:bodyPr");
516 w.open("a:lstStyle");
517 w.close("a:lstStyle");
518 w.open("a:p");
519 w.open("a:pPr");
520 w.empty_with("a:defRPr", &[("sz", "1400"), ("b", "1")]);
521 w.close("a:pPr");
522 w.leaf("a:t", title.as_str());
523 w.close("a:p");
524 w.close("a:rich");
525 w.close("a:tx");
526 w.empty_with("c:overlay", &[("val", "0")]);
527 w.close("c:title");
528 }
529 w.open("c:plotArea");
531 w.open("c:layout");
532 w.close("c:layout");
533
534 let chart_elem = self.chart_type.chart_element();
535 w.open(chart_elem);
536 if matches!(self.chart_type, ChartType::Column | ChartType::Bar) {
537 w.empty_with("c:barDir", &[("val", self.chart_type.bar_dir())]);
538 w.empty_with("c:grouping", &[("val", "clustered")]);
539 }
540 if matches!(self.chart_type, ChartType::Area) {
542 w.empty_with("c:grouping", &[("val", "standard")]);
543 }
544 if matches!(self.chart_type, ChartType::Scatter) {
546 w.empty_with("c:scatterStyle", &[("val", "lineMarker")]);
547 }
548 if matches!(self.chart_type, ChartType::Radar) {
550 w.empty_with("c:radarStyle", &[("val", "marker")]);
551 }
552 if matches!(self.chart_type, ChartType::Bubble) {
554 w.empty_with("c:bubbleScale", &[("val", "100")]);
555 }
556 if matches!(self.chart_type, ChartType::Pie) {
558 w.empty_with("c:varyColors", &[("val", "1")]);
559 }
560
561 if let Some(dl) = &self.data.data_labels {
564 if !dl.is_empty() {
565 dl.write_xml(&mut w);
566 }
567 }
568
569 for (idx, s) in self.data.series.iter().enumerate() {
571 let idx_s = idx.to_string();
572 w.open("c:ser");
573 w.empty_with("c:idx", &[("val", idx_s.as_str())]);
574 w.empty_with("c:order", &[("val", idx_s.as_str())]);
575 w.open("c:tx");
577 w.open("c:strRef");
578 w.leaf("c:f", &format!("Sheet1!${}$1", col_letter(idx + 1)));
579 w.open("c:strCache");
580 w.open_with("c:pt", &[("idx", "0")]);
581 w.leaf("c:v", s.name.as_str());
582 w.close("c:pt");
583 w.close("c:strCache");
584 w.close("c:strRef");
585 w.close("c:tx");
586 if self.chart_type.is_xy_chart() {
587 w.open("c:xVal");
590 w.open("c:numRef");
591 w.leaf("c:f", "Sheet1!$A$2:$A$100");
592 w.open("c:numCache");
593 w.empty_with("c:formatCode", &[("val", "General")]);
594 if let Some(xs) = &s.x_values {
595 for (i, v) in xs.iter().enumerate() {
596 let i_s = i.to_string();
597 let v_s = format_f64(*v);
598 w.open_with("c:pt", &[("idx", i_s.as_str())]);
599 w.leaf("c:v", v_s.as_str());
600 w.close("c:pt");
601 }
602 }
603 w.close("c:numCache");
604 w.close("c:numRef");
605 w.close("c:xVal");
606 w.open("c:yVal");
608 w.open("c:numRef");
609 w.leaf(
610 "c:f",
611 &format!(
612 "Sheet1!${}$2:${}$100",
613 col_letter(idx + 1),
614 col_letter(idx + 1)
615 ),
616 );
617 w.open("c:numCache");
618 w.empty_with("c:formatCode", &[("val", "General")]);
619 for (i, v) in s.values.iter().enumerate() {
620 let i_s = i.to_string();
621 let v_s = format_f64(*v);
622 w.open_with("c:pt", &[("idx", i_s.as_str())]);
623 w.leaf("c:v", v_s.as_str());
624 w.close("c:pt");
625 }
626 w.close("c:numCache");
627 w.close("c:numRef");
628 w.close("c:yVal");
629 if self.chart_type.is_bubble() {
631 w.open("c:bubbleSize");
632 w.open("c:numRef");
633 w.leaf(
634 "c:f",
635 &format!(
636 "Sheet1!${}$2:${}$100",
637 col_letter(idx + 2),
638 col_letter(idx + 2)
639 ),
640 );
641 w.open("c:numCache");
642 w.empty_with("c:formatCode", &[("val", "General")]);
643 if let Some(sizes) = &s.bubble_sizes {
644 for (i, v) in sizes.iter().enumerate() {
645 let i_s = i.to_string();
646 let v_s = format_f64(*v);
647 w.open_with("c:pt", &[("idx", i_s.as_str())]);
648 w.leaf("c:v", v_s.as_str());
649 w.close("c:pt");
650 }
651 }
652 w.close("c:numCache");
653 w.close("c:numRef");
654 w.close("c:bubbleSize");
655 }
656 } else {
657 if !matches!(self.chart_type, ChartType::Pie) && !self.data.categories.is_empty() {
660 w.open("c:cat");
661 w.open("c:strRef");
662 w.leaf("c:f", "Sheet1!$A$2:$A$100");
663 w.open("c:strCache");
664 for (i, cat) in self.data.categories.iter().enumerate() {
665 let i_s = i.to_string();
666 w.open_with("c:pt", &[("idx", i_s.as_str())]);
667 w.leaf("c:v", cat.name.as_str());
668 w.close("c:pt");
669 }
670 w.close("c:strCache");
671 w.close("c:strRef");
672 w.close("c:cat");
673 } else if matches!(self.chart_type, ChartType::Pie)
674 && !self.data.categories.is_empty()
675 {
676 w.open("c:cat");
678 w.open("c:strRef");
679 w.leaf("c:f", "Sheet1!$A$2:$A$100");
680 w.open("c:strCache");
681 for (i, cat) in self.data.categories.iter().enumerate() {
682 let i_s = i.to_string();
683 w.open_with("c:pt", &[("idx", i_s.as_str())]);
684 w.leaf("c:v", cat.name.as_str());
685 w.close("c:pt");
686 }
687 w.close("c:strCache");
688 w.close("c:strRef");
689 w.close("c:cat");
690 }
691 w.open("c:val");
693 w.open("c:numRef");
694 w.leaf(
695 "c:f",
696 &format!(
697 "Sheet1!${}$2:${}$100",
698 col_letter(idx + 1),
699 col_letter(idx + 1)
700 ),
701 );
702 w.open("c:numCache");
703 w.empty_with("c:formatCode", &[("val", "General")]);
704 for (i, v) in s.values.iter().enumerate() {
705 let i_s = i.to_string();
706 let v_s = format_f64(*v);
707 w.open_with("c:pt", &[("idx", i_s.as_str())]);
708 w.leaf("c:v", v_s.as_str());
709 w.close("c:pt");
710 }
711 w.close("c:numCache");
712 w.close("c:numRef");
713 w.close("c:val");
714 }
715 if let Some(dl) = &s.data_labels {
718 if !dl.is_empty() {
719 dl.write_xml(&mut w);
720 }
721 }
722 w.close("c:ser");
723 }
724
725 let has_secondary = !self.chart_type.is_xy_chart()
733 && !matches!(self.chart_type, ChartType::Pie)
734 && self.data.series.iter().any(|s| s.secondary_axis);
735 if matches!(self.chart_type, ChartType::Pie) {
736 } else if self.chart_type.is_xy_chart() {
738 w.empty_with("c:axId", &[("val", "111111111")]);
740 w.empty_with("c:axId", &[("val", "222222222")]);
741 } else {
742 w.empty_with("c:axId", &[("val", "111111111")]);
744 w.empty_with("c:axId", &[("val", "222222222")]);
745 if has_secondary {
748 w.empty_with("c:axId", &[("val", "444444444")]);
749 }
750 }
751 w.close(chart_elem);
752
753 if !matches!(self.chart_type, ChartType::Pie) {
755 if self.chart_type.is_xy_chart() {
756 w.open("c:valAx");
759 w.empty_with("c:axId", &[("val", "111111111")]);
760 w.empty_with("c:scaling", &[("orientation", "minMax")]);
761 w.empty_with("c:delete", &[("val", "0")]);
762 w.empty_with("c:axPos", &[("val", "b")]);
763 w.empty_with("c:crossAx", &[("val", "222222222")]);
764 w.close("c:valAx");
765 w.open("c:valAx");
767 w.empty_with("c:axId", &[("val", "222222222")]);
768 w.empty_with("c:scaling", &[("orientation", "minMax")]);
769 w.empty_with("c:delete", &[("val", "0")]);
770 w.empty_with("c:axPos", &[("val", "l")]);
771 w.empty_with("c:crossAx", &[("val", "111111111")]);
772 w.close("c:valAx");
773 } else {
774 w.open("c:catAx");
777 w.empty_with("c:axId", &[("val", "111111111")]);
778 w.empty_with("c:scaling", &[("orientation", "minMax")]);
779 w.empty_with("c:delete", &[("val", "0")]);
780 w.empty_with("c:axPos", &[("val", "b")]);
781 w.empty_with("c:crossAx", &[("val", "222222222")]);
782 w.close("c:catAx");
783 w.open("c:valAx");
785 w.empty_with("c:axId", &[("val", "222222222")]);
786 w.empty_with("c:scaling", &[("orientation", "minMax")]);
787 w.empty_with("c:delete", &[("val", "0")]);
788 w.empty_with("c:axPos", &[("val", "l")]);
789 w.empty_with("c:crossAx", &[("val", "111111111")]);
790 w.close("c:valAx");
791 if has_secondary {
797 w.open("c:valAx");
798 w.empty_with("c:axId", &[("val", "444444444")]);
799 w.empty_with("c:scaling", &[("orientation", "minMax")]);
800 w.empty_with("c:delete", &[("val", "0")]);
801 w.empty_with("c:axPos", &[("val", "r")]);
802 w.empty_with("c:crossAx", &[("val", "111111111")]);
803 w.empty_with("c:crosses", &[("val", "max")]);
804 w.close("c:valAx");
805 }
806 }
807 }
808 w.close("c:plotArea");
809
810 w.open("c:legend");
812 w.empty_with("c:legendPos", &[("val", "r")]);
813 w.empty_with("c:overlay", &[("val", "0")]);
814 w.close("c:legend");
815
816 w.empty_with("c:plotVisOnly", &[("val", "1")]);
817 w.empty_with("c:dispBlanksAs", &[("val", "gap")]);
818 w.close("c:chart");
819
820 if let Some(ext_rid) = &self.external_data_rid {
829 w.open_with("c:externalData", &[("r:id", ext_rid.as_str())]);
830 w.empty_with("c:autoUpdate", &[("val", "0")]);
831 w.close("c:externalData");
832 }
833
834 w.close("c:chartSpace");
835 w.into_string()
836 }
837}
838
839fn col_letter(n: usize) -> String {
841 let mut s = String::new();
842 let mut n = n;
843 while n > 0 {
844 n -= 1;
845 let ch = (b'A' + (n % 26) as u8) as char;
846 s.insert(0, ch);
847 n /= 26;
848 }
849 s
850}
851
852fn format_f64(v: f64) -> String {
854 if v.is_nan() || v.is_infinite() {
855 return "0".to_string();
856 }
857 if v.fract() == 0.0 && v.abs() < 1e15 {
858 format!("{}", v as i64)
859 } else {
860 format!("{}", v)
861 }
862}
863
864pub fn parse_from_xml(xml: &str) -> crate::Result<Chart> {
906 use quick_xml::events::Event;
907 use quick_xml::reader::Reader;
908
909 let mut chart_type = ChartType::Column;
910 let mut title: Option<String> = None;
911 let mut series: Vec<ChartSeries> = Vec::new();
912 let mut categories: Vec<ChartCategory> = Vec::new();
913
914 let mut rd = Reader::from_str(xml);
915 rd.config_mut().trim_text(true);
916 let mut buf = Vec::new();
917 let mut in_chart_elem: Option<&'static str> = None;
919 let mut in_plot_area = false;
922 let mut bar_dir: Option<String> = None;
924 let mut in_title_text = false;
926 let mut cur_ser: Option<ChartSeries> = None;
928 let mut ser_field: Option<SerField> = None;
930 let mut in_cache = false;
932 let mut external_data_rid: Option<String> = None;
934 let mut cur_values: Vec<f64> = Vec::new();
936 let mut cur_strings: Vec<String> = Vec::new();
937
938 let mut chart_dl: Option<DataLabels> = None;
946 let mut series_dl: Option<DataLabels> = None;
947 let mut dl_target: Option<DlTarget> = None;
949 let mut has_secondary_axis = false;
952
953 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
955 enum SerField {
956 Name,
958 Cat,
960 Val,
962 XVal,
964 YVal,
966 BubbleSize,
968 }
969
970 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
972 enum DlTarget {
973 Chart,
975 Series,
977 }
978
979 fn chart_elem_for(local: &[u8]) -> Option<(&'static str, ChartType)> {
981 match local {
982 b"barChart" => Some(("barChart", ChartType::Column)), b"lineChart" => Some(("lineChart", ChartType::Line)),
984 b"pieChart" => Some(("pieChart", ChartType::Pie)),
985 b"scatterChart" => Some(("scatterChart", ChartType::Scatter)),
986 b"areaChart" => Some(("areaChart", ChartType::Area)),
987 b"radarChart" => Some(("radarChart", ChartType::Radar)),
988 b"bubbleChart" => Some(("bubbleChart", ChartType::Bubble)),
989 _ => None,
990 }
991 }
992
993 fn local_name_quick(name: &[u8]) -> &[u8] {
995 match name.iter().position(|&b| b == b':') {
996 Some(i) => &name[i + 1..],
997 None => name,
998 }
999 }
1000
1001 loop {
1002 match rd.read_event_into(&mut buf) {
1003 Ok(Event::Start(e)) => {
1004 let name = e.name();
1006 let local = local_name_quick(name.as_ref());
1007 if local == b"externalData" {
1012 for a in e.attributes().flatten() {
1013 let key = a.key.as_ref();
1018 if key == b"r:id" || key == b"id" || key.ends_with(b":id") {
1019 external_data_rid = Some(
1020 a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
1021 .unwrap_or_default()
1022 .to_string(),
1023 );
1024 }
1025 }
1026 }
1027 if local == b"plotArea" {
1029 in_plot_area = true;
1030 }
1031 if let Some((elem, ct)) = chart_elem_for(local) {
1033 in_chart_elem = Some(elem);
1034 chart_type = ct;
1035 }
1037 if local == b"barDir" && in_chart_elem.is_some() {
1039 for a in e.attributes().flatten() {
1040 if a.key.as_ref() == b"val" {
1041 let v = a
1042 .normalized_value(quick_xml::XmlVersion::Implicit1_0)
1043 .unwrap_or_default()
1044 .to_string();
1045 bar_dir = Some(v);
1046 }
1047 }
1048 }
1049 if local == b"t" {
1051 in_title_text = true;
1052 }
1053 if local == b"ser" && in_chart_elem.is_some() {
1055 cur_ser = Some(ChartSeries::default());
1056 series_dl = None;
1058 }
1059 if local == b"dLbls" {
1064 if cur_ser.is_some() {
1065 dl_target = Some(DlTarget::Series);
1066 series_dl = Some(DataLabels::default());
1067 } else if in_chart_elem.is_some() {
1068 dl_target = Some(DlTarget::Chart);
1069 chart_dl = Some(DataLabels::default());
1070 }
1071 }
1072 if cur_ser.is_some() {
1074 match local {
1075 b"tx" => ser_field = Some(SerField::Name),
1076 b"cat" => ser_field = Some(SerField::Cat),
1077 b"val" => ser_field = Some(SerField::Val),
1078 b"xVal" => ser_field = Some(SerField::XVal),
1079 b"yVal" => ser_field = Some(SerField::YVal),
1080 b"bubbleSize" => ser_field = Some(SerField::BubbleSize),
1081 _ => {}
1082 }
1083 }
1084 if matches!(local, b"numCache" | b"strCache") {
1086 in_cache = true;
1087 cur_values.clear();
1088 cur_strings.clear();
1089 }
1090 if local == b"crosses" && in_plot_area {
1099 for a in e.attributes().flatten() {
1100 if a.key.as_ref() == b"val" {
1101 let v = a
1102 .normalized_value(quick_xml::XmlVersion::Implicit1_0)
1103 .unwrap_or_default();
1104 if v == "max" {
1105 has_secondary_axis = true;
1106 }
1107 }
1108 }
1109 }
1110 }
1111 Ok(Event::Empty(e)) => {
1112 let name = e.name();
1114 let local = local_name_quick(name.as_ref());
1115 if local == b"barDir" && in_chart_elem.is_some() {
1117 for a in e.attributes().flatten() {
1118 if a.key.as_ref() == b"val" {
1119 let v = a
1120 .normalized_value(quick_xml::XmlVersion::Implicit1_0)
1121 .unwrap_or_default()
1122 .to_string();
1123 bar_dir = Some(v);
1124 }
1125 }
1126 }
1127 if local == b"v" && in_cache {
1130 if let Some(v) = e.attributes().flatten().find(|a| a.key.as_ref() == b"val") {
1131 let s = v
1132 .normalized_value(quick_xml::XmlVersion::Implicit1_0)
1133 .unwrap_or_default()
1134 .to_string();
1135 if let Ok(f) = s.parse::<f64>() {
1136 cur_values.push(f);
1137 }
1138 cur_strings.push(s);
1139 }
1140 }
1141 if let Some(target) = dl_target.as_ref() {
1146 let dl_buf: &mut Option<DataLabels> = if *target == DlTarget::Chart {
1148 &mut chart_dl
1149 } else {
1150 &mut series_dl
1151 };
1152 let dl = dl_buf.get_or_insert_with(DataLabels::default);
1153 match local {
1154 b"showVal" => {
1155 dl.show_val = parse_bool_val(&e);
1156 }
1157 b"showCatName" => {
1158 dl.show_cat_name = parse_bool_val(&e);
1159 }
1160 b"showSerName" => {
1161 dl.show_ser_name = parse_bool_val(&e);
1162 }
1163 b"showLegendKey" => {
1164 dl.show_legend_key = parse_bool_val(&e);
1165 }
1166 b"showPercent" => {
1167 dl.show_percent = parse_bool_val(&e);
1168 }
1169 b"showBubbleSize" => {
1170 dl.show_bubble_size = parse_bool_val(&e);
1171 }
1172 b"dLblPos" => {
1173 if let Some(v) = attr_val(&e, "val") {
1174 dl.position = LabelPosition::parse(&v);
1175 }
1176 }
1177 b"separator" => {
1178 if let Some(v) = attr_val(&e, "val") {
1179 dl.separator = Some(v);
1180 }
1181 }
1182 b"numFmt" => {
1183 if let Some(fc) = attr_val(&e, "formatCode") {
1184 dl.num_fmt = Some(fc);
1185 }
1186 }
1187 _ => {}
1188 }
1189 }
1190 if local == b"crosses" && in_plot_area {
1193 if let Some(v) = attr_val(&e, "val") {
1194 if v == "max" {
1195 has_secondary_axis = true;
1196 }
1197 }
1198 }
1199 }
1200 Ok(Event::Text(t)) => {
1201 if in_title_text && title.is_none() {
1203 let text_str = std::str::from_utf8(t.as_ref()).unwrap_or("");
1207 let text = quick_xml::escape::unescape(text_str)
1208 .unwrap_or_default()
1209 .to_string();
1210 if !text.is_empty() {
1211 title = Some(text);
1212 }
1213 }
1214 if in_cache {
1216 let text_str = std::str::from_utf8(t.as_ref()).unwrap_or("");
1217 let text = quick_xml::escape::unescape(text_str)
1218 .unwrap_or_default()
1219 .to_string();
1220 if !text.is_empty() {
1221 if let Ok(f) = text.parse::<f64>() {
1222 cur_values.push(f);
1223 }
1224 cur_strings.push(text);
1225 }
1226 }
1227 }
1228 Ok(Event::End(e)) => {
1229 let name = e.name();
1231 let local = local_name_quick(name.as_ref());
1232 if local == b"plotArea" {
1234 in_plot_area = false;
1235 }
1236 if let Some(elem) = in_chart_elem {
1238 if local == elem.as_bytes() {
1239 if elem == "barChart" {
1241 if let Some(dir) = &bar_dir {
1242 if dir == "bar" {
1243 chart_type = ChartType::Bar;
1244 }
1245 }
1246 }
1247 in_chart_elem = None;
1248 }
1249 }
1250 if local == b"t" {
1252 in_title_text = false;
1253 }
1254 if local == b"dLbls" {
1256 dl_target = None;
1257 }
1258 if matches!(local, b"numCache" | b"strCache") && cur_ser.is_some() {
1260 let field = ser_field.take();
1261 if let Some(f) = field {
1262 match f {
1263 SerField::Name => {
1264 if let Some(s) = cur_strings.first() {
1266 if let Some(s_obj) = cur_ser.as_mut() {
1267 s_obj.name = s.clone();
1268 }
1269 }
1270 ser_field = None;
1271 }
1272 SerField::Cat => {
1273 if categories.is_empty() {
1275 for s in &cur_strings {
1276 categories.push(ChartCategory::new(s.clone()));
1277 }
1278 }
1279 ser_field = None;
1280 }
1281 SerField::Val | SerField::YVal => {
1282 if let Some(s_obj) = cur_ser.as_mut() {
1283 s_obj.values = cur_values.clone();
1284 }
1285 ser_field = None;
1286 }
1287 SerField::XVal => {
1288 if let Some(s_obj) = cur_ser.as_mut() {
1289 s_obj.x_values = Some(cur_values.clone());
1290 }
1291 ser_field = None;
1292 }
1293 SerField::BubbleSize => {
1294 if let Some(s_obj) = cur_ser.as_mut() {
1295 s_obj.bubble_sizes = Some(cur_values.clone());
1296 }
1297 ser_field = None;
1298 }
1299 }
1300 }
1301 in_cache = false;
1302 cur_values.clear();
1303 cur_strings.clear();
1304 }
1305 if local == b"ser" && cur_ser.is_some() {
1307 if let Some(mut s_obj) = cur_ser.take() {
1308 if let Some(dl) = series_dl.take() {
1310 if !dl.is_empty() {
1311 s_obj.data_labels = Some(dl);
1312 }
1313 }
1314 series.push(s_obj);
1315 }
1316 ser_field = None;
1317 series_dl = None;
1319 if dl_target == Some(DlTarget::Series) {
1320 dl_target = None;
1321 }
1322 }
1323 }
1324 Ok(Event::Eof) => break,
1325 Err(e) => return Err(crate::Error::Xml(format!("chart parse_from_xml: {e}"))),
1326 _ => {}
1327 }
1328 buf.clear();
1329 }
1330
1331 if has_secondary_axis && !chart_type.is_xy_chart() && !matches!(chart_type, ChartType::Pie) {
1337 for s in series.iter_mut() {
1338 s.secondary_axis = true;
1339 }
1340 }
1341
1342 let data = ChartData {
1343 categories,
1344 series,
1345 title,
1346 data_labels: chart_dl.filter(|dl| !dl.is_empty()),
1347 };
1348 Ok(Chart {
1349 chart_type,
1350 data,
1351 rid: String::new(),
1352 external_data_rid,
1353 })
1354}
1355
1356fn parse_bool_val(e: &quick_xml::events::BytesStart<'_>) -> Option<bool> {
1361 for a in e.attributes().flatten() {
1362 if a.key.as_ref() == b"val" {
1363 let v = a
1364 .normalized_value(quick_xml::XmlVersion::Implicit1_0)
1365 .unwrap_or_default();
1366 return match v.as_ref() {
1367 "1" | "true" | "True" => Some(true),
1368 "0" | "false" | "False" => Some(false),
1369 _ => None,
1370 };
1371 }
1372 }
1373 None
1374}
1375
1376fn attr_val(e: &quick_xml::events::BytesStart<'_>, key: &str) -> Option<String> {
1380 let key_bytes = key.as_bytes();
1381 let suffix: Vec<u8> = std::iter::once(b':')
1383 .chain(key_bytes.iter().copied())
1384 .collect();
1385 for a in e.attributes().flatten() {
1386 let k = a.key.as_ref();
1387 if k == key_bytes || k.ends_with(&suffix) {
1388 return Some(
1389 a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
1390 .unwrap_or_default()
1391 .to_string(),
1392 );
1393 }
1394 }
1395 None
1396}
1397
1398#[cfg(test)]
1399mod tests {
1400 use super::*;
1401
1402 #[test]
1404 fn column_chart_minimal_xml() {
1405 let data = ChartData {
1406 categories: vec![ChartCategory::new("Q1"), ChartCategory::new("Q2")],
1407 series: vec![ChartSeries::new("Sales", vec![10.0, 20.0])],
1408 title: Some("Revenue".to_string()),
1409 data_labels: None,
1410 };
1411 let chart = Chart::new(ChartType::Column, data);
1412 let xml = chart.to_xml();
1413 assert!(xml.contains("<c:chartSpace"), "xml: {}", xml);
1414 assert!(xml.contains("<c:barChart>"), "xml: {}", xml);
1415 assert!(xml.contains("barDir"), "xml: {}", xml);
1416 assert!(xml.contains("val=\"col\""), "xml: {}", xml);
1417 assert!(xml.contains("<c:ser>"), "xml: {}", xml);
1418 assert!(xml.contains("<c:catAx>"), "xml: {}", xml);
1419 assert!(xml.contains("<c:valAx>"), "xml: {}", xml);
1420 assert!(xml.contains("Revenue"), "xml: {}", xml);
1421 }
1422
1423 #[test]
1425 fn pie_chart_no_axes() {
1426 let data = ChartData {
1427 categories: vec![ChartCategory::new("A"), ChartCategory::new("B")],
1428 series: vec![ChartSeries::new("X", vec![1.0, 2.0])],
1429 title: None,
1430 data_labels: None,
1431 };
1432 let xml = Chart::new(ChartType::Pie, data).to_xml();
1433 assert!(xml.contains("<c:pieChart>"), "xml: {}", xml);
1434 assert!(!xml.contains("<c:catAx>"), "xml: {}", xml);
1435 assert!(!xml.contains("<c:valAx>"), "xml: {}", xml);
1436 assert!(xml.contains("varyColors"), "xml: {}", xml);
1437 }
1438
1439 #[test]
1441 fn line_chart_uses_line_chart_element() {
1442 let data = ChartData {
1443 categories: vec![ChartCategory::new("X1"), ChartCategory::new("X2")],
1444 series: vec![ChartSeries::new("Y", vec![1.5, 2.5])],
1445 title: None,
1446 data_labels: None,
1447 };
1448 let xml = Chart::new(ChartType::Line, data).to_xml();
1449 assert!(xml.contains("<c:lineChart>"), "xml: {}", xml);
1450 assert!(!xml.contains("<c:barChart>"), "xml: {}", xml);
1451 assert!(xml.contains("1.5"), "xml: {}", xml);
1453 assert!(xml.contains("2.5"), "xml: {}", xml);
1454 }
1455
1456 #[test]
1458 fn scatter_chart_uses_scatter_elements() {
1459 let data = ChartData {
1460 categories: vec![], series: vec![ChartSeries::new_scatter(
1462 "Series1",
1463 vec![1.0, 2.0, 3.0], vec![2.0, 4.0, 6.0], )],
1466 title: Some("Scatter".to_string()),
1467 data_labels: None,
1468 };
1469 let xml = Chart::new(ChartType::Scatter, data).to_xml();
1470 assert!(xml.contains("<c:scatterChart>"), "xml: {}", xml);
1471 assert!(xml.contains("scatterStyle"), "xml: {}", xml);
1472 assert!(xml.contains("<c:xVal>"), "xml: {}", xml);
1473 assert!(xml.contains("<c:yVal>"), "xml: {}", xml);
1474 assert!(!xml.contains("<c:catAx>"), "xml: {}", xml);
1476 let val_ax_count = xml.matches("<c:valAx>").count();
1478 assert_eq!(
1479 val_ax_count, 2,
1480 "expected 2 valAx, got {}: {}",
1481 val_ax_count, xml
1482 );
1483 assert!(!xml.contains("<c:cat>"), "xml: {}", xml);
1485 assert!(xml.contains(">1<"), "xml: {}", xml);
1487 assert!(xml.contains(">3<"), "xml: {}", xml);
1488 }
1489
1490 #[test]
1492 fn area_chart_uses_area_elements() {
1493 let data = ChartData {
1494 categories: vec![ChartCategory::new("A"), ChartCategory::new("B")],
1495 series: vec![ChartSeries::new("S1", vec![10.0, 20.0])],
1496 title: None,
1497 data_labels: None,
1498 };
1499 let xml = Chart::new(ChartType::Area, data).to_xml();
1500 assert!(xml.contains("<c:areaChart>"), "xml: {}", xml);
1501 assert!(xml.contains("grouping"), "xml: {}", xml);
1502 assert!(xml.contains("val=\"standard\""), "xml: {}", xml);
1503 assert!(xml.contains("<c:catAx>"), "xml: {}", xml);
1504 assert!(xml.contains("<c:valAx>"), "xml: {}", xml);
1505 assert!(xml.contains("<c:cat>"), "xml: {}", xml);
1507 }
1508
1509 #[test]
1511 fn radar_chart_uses_radar_elements() {
1512 let data = ChartData {
1513 categories: vec![
1514 ChartCategory::new("速度"),
1515 ChartCategory::new("力量"),
1516 ChartCategory::new("技巧"),
1517 ],
1518 series: vec![ChartSeries::new("选手A", vec![8.0, 6.0, 9.0])],
1519 title: Some("Radar".to_string()),
1520 data_labels: None,
1521 };
1522 let xml = Chart::new(ChartType::Radar, data).to_xml();
1523 assert!(xml.contains("<c:radarChart>"), "xml: {}", xml);
1524 assert!(xml.contains("radarStyle"), "xml: {}", xml);
1525 assert!(xml.contains("val=\"marker\""), "xml: {}", xml);
1526 assert!(xml.contains("<c:catAx>"), "xml: {}", xml);
1528 assert!(xml.contains("<c:valAx>"), "xml: {}", xml);
1529 assert!(xml.contains("<c:cat>"), "xml: {}", xml);
1531 assert!(!xml.contains("<c:xVal>"), "xml: {}", xml);
1533 assert!(!xml.contains("<c:bubbleSize>"), "xml: {}", xml);
1534 }
1535
1536 #[test]
1538 fn bubble_chart_uses_bubble_elements() {
1539 let data = ChartData {
1540 categories: vec![],
1541 series: vec![ChartSeries::new_bubble(
1542 "S1",
1543 vec![1.0, 2.0, 3.0], vec![10.0, 20.0, 30.0], vec![5.0, 15.0, 25.0], )],
1547 title: Some("Bubble".to_string()),
1548 data_labels: None,
1549 };
1550 let xml = Chart::new(ChartType::Bubble, data).to_xml();
1551 assert!(xml.contains("<c:bubbleChart>"), "xml: {}", xml);
1552 assert!(xml.contains("bubbleScale"), "xml: {}", xml);
1553 assert!(xml.contains("val=\"100\""), "xml: {}", xml);
1554 assert!(xml.contains("<c:xVal>"), "xml: {}", xml);
1556 assert!(xml.contains("<c:yVal>"), "xml: {}", xml);
1557 assert!(xml.contains("<c:bubbleSize>"), "xml: {}", xml);
1558 assert!(!xml.contains("<c:catAx>"), "xml: {}", xml);
1560 assert!(xml.contains("<c:valAx>"), "xml: {}", xml);
1561 assert!(xml.contains(">5<"), "xml: {}", xml);
1563 assert!(xml.contains(">25<"), "xml: {}", xml);
1564 }
1565
1566 #[test]
1568 fn col_letter_basic() {
1569 assert_eq!(col_letter(1), "A");
1570 assert_eq!(col_letter(2), "B");
1571 assert_eq!(col_letter(26), "Z");
1572 assert_eq!(col_letter(27), "AA");
1573 assert_eq!(col_letter(28), "AB");
1574 }
1575
1576 #[test]
1578 fn format_f64_integer() {
1579 assert_eq!(format_f64(10.0), "10");
1580 assert_eq!(format_f64(1.5), "1.5");
1581 assert_eq!(format_f64(f64::NAN), "0");
1582 }
1583
1584 #[test]
1590 fn parse_column_chart_round_trip() {
1591 let original = Chart::new(
1592 ChartType::Column,
1593 ChartData {
1594 categories: vec![
1595 ChartCategory::new("Q1"),
1596 ChartCategory::new("Q2"),
1597 ChartCategory::new("Q3"),
1598 ],
1599 series: vec![ChartSeries::new("Sales", vec![10.0, 20.0, 30.0])],
1600 title: Some("Revenue".to_string()),
1601 data_labels: None,
1602 },
1603 );
1604 let xml = original.to_xml();
1605 let parsed = Chart::parse_from_xml(&xml).expect("parse column chart");
1606
1607 assert_eq!(parsed.chart_type, ChartType::Column);
1608 assert_eq!(parsed.data.title.as_deref(), Some("Revenue"));
1609 assert_eq!(parsed.data.categories.len(), 3);
1610 assert_eq!(parsed.data.categories[0].name, "Q1");
1611 assert_eq!(parsed.data.categories[2].name, "Q3");
1612 assert_eq!(parsed.data.series.len(), 1);
1613 assert_eq!(parsed.data.series[0].name, "Sales");
1614 assert_eq!(parsed.data.series[0].values, vec![10.0, 20.0, 30.0]);
1615 }
1616
1617 #[test]
1622 fn parse_bar_chart_distinguishes_bar_from_column() {
1623 let original = Chart::new(
1624 ChartType::Bar,
1625 ChartData {
1626 categories: vec![ChartCategory::new("A"), ChartCategory::new("B")],
1627 series: vec![ChartSeries::new("S1", vec![1.0, 2.0])],
1628 title: None,
1629 data_labels: None,
1630 },
1631 );
1632 let xml = original.to_xml();
1633 let parsed = Chart::parse_from_xml(&xml).expect("parse bar chart");
1634
1635 assert_eq!(parsed.chart_type, ChartType::Bar);
1637 assert_ne!(parsed.chart_type, ChartType::Column);
1638 assert_eq!(parsed.data.series.len(), 1);
1639 assert_eq!(parsed.data.series[0].values, vec![1.0, 2.0]);
1640 }
1641
1642 #[test]
1644 fn parse_line_chart_round_trip() {
1645 let original = Chart::new(
1646 ChartType::Line,
1647 ChartData {
1648 categories: vec![ChartCategory::new("X1"), ChartCategory::new("X2")],
1649 series: vec![ChartSeries::new("Y", vec![1.5, 2.5])],
1650 title: None,
1651 data_labels: None,
1652 },
1653 );
1654 let xml = original.to_xml();
1655 let parsed = Chart::parse_from_xml(&xml).expect("parse line chart");
1656
1657 assert_eq!(parsed.chart_type, ChartType::Line);
1658 assert_eq!(parsed.data.categories.len(), 2);
1659 assert_eq!(parsed.data.series[0].name, "Y");
1660 assert_eq!(parsed.data.series[0].values, vec![1.5, 2.5]);
1662 }
1663
1664 #[test]
1666 fn parse_pie_chart_round_trip() {
1667 let original = Chart::new(
1668 ChartType::Pie,
1669 ChartData {
1670 categories: vec![ChartCategory::new("A"), ChartCategory::new("B")],
1671 series: vec![ChartSeries::new("X", vec![1.0, 2.0])],
1672 title: None,
1673 data_labels: None,
1674 },
1675 );
1676 let xml = original.to_xml();
1677 let parsed = Chart::parse_from_xml(&xml).expect("parse pie chart");
1678
1679 assert_eq!(parsed.chart_type, ChartType::Pie);
1680 assert_eq!(parsed.data.categories.len(), 2);
1682 assert_eq!(parsed.data.categories[0].name, "A");
1683 assert_eq!(parsed.data.categories[1].name, "B");
1684 assert_eq!(parsed.data.series.len(), 1);
1685 assert_eq!(parsed.data.series[0].values, vec![1.0, 2.0]);
1686 }
1687
1688 #[test]
1692 fn parse_scatter_chart_round_trip() {
1693 let original = Chart::new(
1694 ChartType::Scatter,
1695 ChartData {
1696 categories: vec![],
1697 series: vec![ChartSeries::new_scatter(
1698 "Series1",
1699 vec![1.0, 2.0, 3.0], vec![2.0, 4.0, 6.0], )],
1702 title: Some("Scatter".to_string()),
1703 data_labels: None,
1704 },
1705 );
1706 let xml = original.to_xml();
1707 let parsed = Chart::parse_from_xml(&xml).expect("parse scatter chart");
1708
1709 assert_eq!(parsed.chart_type, ChartType::Scatter);
1710 assert_eq!(parsed.data.title.as_deref(), Some("Scatter"));
1711 assert_eq!(parsed.data.series.len(), 1);
1712 let s = &parsed.data.series[0];
1713 assert_eq!(s.name, "Series1");
1714 assert_eq!(s.values, vec![2.0, 4.0, 6.0]);
1716 assert_eq!(s.x_values.as_deref(), Some(&[1.0, 2.0, 3.0][..]));
1718 assert!(s.bubble_sizes.is_none());
1720 }
1721
1722 #[test]
1724 fn parse_bubble_chart_round_trip() {
1725 let original = Chart::new(
1726 ChartType::Bubble,
1727 ChartData {
1728 categories: vec![],
1729 series: vec![ChartSeries::new_bubble(
1730 "S1",
1731 vec![1.0, 2.0, 3.0], vec![10.0, 20.0, 30.0], vec![5.0, 15.0, 25.0], )],
1735 title: Some("Bubble".to_string()),
1736 data_labels: None,
1737 },
1738 );
1739 let xml = original.to_xml();
1740 let parsed = Chart::parse_from_xml(&xml).expect("parse bubble chart");
1741
1742 assert_eq!(parsed.chart_type, ChartType::Bubble);
1743 assert_eq!(parsed.data.series.len(), 1);
1744 let s = &parsed.data.series[0];
1745 assert_eq!(s.name, "S1");
1746 assert_eq!(s.values, vec![10.0, 20.0, 30.0]);
1747 assert_eq!(s.x_values.as_deref(), Some(&[1.0, 2.0, 3.0][..]));
1748 assert_eq!(s.bubble_sizes.as_deref(), Some(&[5.0, 15.0, 25.0][..]));
1749 }
1750
1751 #[test]
1753 fn parse_radar_chart_round_trip() {
1754 let original = Chart::new(
1755 ChartType::Radar,
1756 ChartData {
1757 categories: vec![
1758 ChartCategory::new("速度"),
1759 ChartCategory::new("力量"),
1760 ChartCategory::new("技巧"),
1761 ],
1762 series: vec![ChartSeries::new("选手A", vec![8.0, 6.0, 9.0])],
1763 title: Some("Radar".to_string()),
1764 data_labels: None,
1765 },
1766 );
1767 let xml = original.to_xml();
1768 let parsed = Chart::parse_from_xml(&xml).expect("parse radar chart");
1769
1770 assert_eq!(parsed.chart_type, ChartType::Radar);
1771 assert_eq!(parsed.data.categories.len(), 3);
1772 assert_eq!(parsed.data.categories[0].name, "速度");
1773 assert_eq!(parsed.data.series[0].values, vec![8.0, 6.0, 9.0]);
1774 }
1775
1776 #[test]
1778 fn parse_multi_series_column_chart() {
1779 let original = Chart::new(
1780 ChartType::Column,
1781 ChartData {
1782 categories: vec![ChartCategory::new("A"), ChartCategory::new("B")],
1783 series: vec![
1784 ChartSeries::new("S1", vec![1.0, 2.0]),
1785 ChartSeries::new("S2", vec![3.0, 4.0]),
1786 ChartSeries::new("S3", vec![5.0, 6.0]),
1787 ],
1788 title: None,
1789 data_labels: None,
1790 },
1791 );
1792 let xml = original.to_xml();
1793 let parsed = Chart::parse_from_xml(&xml).expect("parse multi-series chart");
1794
1795 assert_eq!(parsed.data.series.len(), 3);
1796 assert_eq!(parsed.data.series[0].name, "S1");
1797 assert_eq!(parsed.data.series[1].name, "S2");
1798 assert_eq!(parsed.data.series[2].name, "S3");
1799 assert_eq!(parsed.data.series[2].values, vec![5.0, 6.0]);
1800 }
1801
1802 #[test]
1804 fn parse_chart_without_title() {
1805 let original = Chart::new(
1806 ChartType::Column,
1807 ChartData {
1808 categories: vec![ChartCategory::new("A")],
1809 series: vec![ChartSeries::new("S", vec![1.0])],
1810 title: None,
1811 data_labels: None,
1812 },
1813 );
1814 let xml = original.to_xml();
1815 let parsed = Chart::parse_from_xml(&xml).expect("parse no-title chart");
1816
1817 assert!(parsed.data.title.is_none());
1818 }
1819
1820 #[test]
1824 fn parse_empty_chart_space_no_panic() {
1825 let xml = "<?xml version=\"1.0\"?>\
1826 <c:chartSpace xmlns:c=\"http://schemas.openxmlformats.org/drawingml/2006/chart\">\
1827 <c:chart></c:chart>\
1828 </c:chartSpace>";
1829 let parsed = Chart::parse_from_xml(xml).expect("parse empty chartSpace");
1830 assert_eq!(parsed.chart_type, ChartType::Column);
1832 assert!(parsed.data.series.is_empty());
1833 assert!(parsed.data.categories.is_empty());
1834 }
1835
1836 #[test]
1838 fn parse_malformed_xml_returns_error() {
1839 let xml = "<c:chartSpace><!-- unclosed comment <c:chart/></c:chartSpace>";
1843 let result = Chart::parse_from_xml(xml);
1844 assert!(result.is_err(), "malformed xml should error");
1845 }
1846
1847 #[test]
1850 fn to_xml_writes_external_data() {
1851 let mut chart = Chart::new(
1852 ChartType::Column,
1853 ChartData {
1854 categories: vec![ChartCategory::new("A")],
1855 series: vec![ChartSeries::new("S", vec![1.0])],
1856 title: None,
1857 data_labels: None,
1858 },
1859 );
1860 chart.external_data_rid = Some("rIdXlsx1".to_string());
1861 let xml = chart.to_xml();
1862 assert!(
1864 xml.contains(r#"<c:externalData r:id="rIdXlsx1">"#),
1865 "应输出 c:externalData 开标签"
1866 );
1867 assert!(
1868 xml.contains(r#"<c:autoUpdate val="0"/>"#),
1869 "应输出 autoUpdate=0"
1870 );
1871 assert!(xml.contains("</c:externalData>"), "应闭合 c:externalData");
1872 let pos_chart_close = xml.find("</c:chart>").expect("c:chart 关闭存在");
1874 let pos_ext = xml.find("<c:externalData").expect("c:externalData 存在");
1875 assert!(
1876 pos_chart_close < pos_ext,
1877 "c:externalData 必须在 </c:chart> 之后"
1878 );
1879 let pos_chart_space_close = xml.rfind("</c:chartSpace>").expect("c:chartSpace 关闭存在");
1880 assert!(
1881 pos_ext < pos_chart_space_close,
1882 "c:externalData 必须在 </c:chartSpace> 之前"
1883 );
1884 }
1885
1886 #[test]
1888 fn to_xml_omits_external_data_when_none() {
1889 let chart = Chart::new(
1890 ChartType::Line,
1891 ChartData {
1892 categories: vec![ChartCategory::new("A")],
1893 series: vec![ChartSeries::new("S", vec![1.0])],
1894 title: None,
1895 data_labels: None,
1896 },
1897 );
1898 let xml = chart.to_xml();
1899 assert!(
1900 !xml.contains("c:externalData"),
1901 "None 时不应输出 c:externalData"
1902 );
1903 }
1904
1905 #[test]
1907 fn parse_external_data_rid_round_trip() {
1908 let mut original = Chart::new(
1909 ChartType::Column,
1910 ChartData {
1911 categories: vec![ChartCategory::new("Q1"), ChartCategory::new("Q2")],
1912 series: vec![ChartSeries::new("Sales", vec![10.0, 20.0])],
1913 title: Some("Revenue".into()),
1914 data_labels: None,
1915 },
1916 );
1917 original.external_data_rid = Some("rIdXlsx1".into());
1918 let xml = original.to_xml();
1919 let parsed = Chart::parse_from_xml(&xml).expect("parse round-trip");
1920 assert_eq!(parsed.chart_type, ChartType::Column);
1921 assert_eq!(parsed.external_data_rid, Some("rIdXlsx1".to_string()));
1922 }
1923
1924 #[test]
1926 fn parse_external_data_rid_bare_id_form() {
1927 let xml = "<?xml version=\"1.0\"?>\
1928 <c:chartSpace xmlns:c=\"http://schemas.openxmlformats.org/drawingml/2006/chart\" \
1929 xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">\
1930 <c:chart><c:plotArea><c:barChart><c:barDir val=\"col\"/>\
1931 <c:ser><c:idx val=\"0\"/><c:order val=\"0\"/>\
1932 <c:val><c:numCache><c:formatCode>General</c:formatCode>\
1933 <c:ptCount val=\"1\"/><c:pt idx=\"0\"><c:v>1</c:v></c:pt>\
1934 </c:numCache></c:val>\
1935 </c:ser></c:barChart></c:plotArea></c:chart>\
1936 <c:externalData id=\"rIdBare\"><c:autoUpdate val=\"0\"/></c:externalData>\
1937 </c:chartSpace>";
1938 let parsed = Chart::parse_from_xml(xml).expect("parse bare id form");
1939 assert_eq!(parsed.external_data_rid, Some("rIdBare".to_string()));
1940 }
1941
1942 #[test]
1946 fn data_labels_show_values_constructor() {
1947 let dl = DataLabels::show_values();
1948 assert_eq!(dl.show_val, Some(true));
1949 assert_eq!(dl.show_cat_name, None);
1950 assert_eq!(dl.show_ser_name, None);
1951 assert!(!dl.is_empty());
1952 }
1953
1954 #[test]
1956 fn data_labels_show_percent_pie_constructor() {
1957 let dl = DataLabels::show_percent_pie();
1958 assert_eq!(dl.show_percent, Some(true));
1959 assert_eq!(dl.show_val, None);
1960 assert!(!dl.is_empty());
1961 }
1962
1963 #[test]
1965 fn data_labels_is_empty_for_default() {
1966 let dl = DataLabels::default();
1967 assert!(dl.is_empty());
1968 }
1969
1970 #[test]
1972 fn label_position_round_trip() {
1973 for pos in [
1974 LabelPosition::BestFit,
1975 LabelPosition::Above,
1976 LabelPosition::Below,
1977 LabelPosition::Center,
1978 LabelPosition::InsideEnd,
1979 LabelPosition::OutsideEnd,
1980 LabelPosition::Left,
1981 LabelPosition::Right,
1982 ] {
1983 let s = pos.as_str();
1984 let parsed = LabelPosition::parse(s).expect("known position should parse");
1986 assert_eq!(parsed, pos, "position round-trip failed for {:?}", pos);
1987 }
1988 assert!(LabelPosition::parse("unknown").is_none());
1990 }
1991
1992 #[test]
1996 fn parse_chart_level_data_labels_round_trip() {
1997 let dl = DataLabels {
1998 show_val: Some(true),
1999 show_cat_name: Some(true),
2000 show_ser_name: Some(false),
2001 show_legend_key: Some(false),
2002 show_percent: None,
2003 show_bubble_size: None,
2004 position: Some(LabelPosition::OutsideEnd),
2005 separator: Some(", ".to_string()),
2006 num_fmt: Some("0.00".to_string()),
2007 };
2008 let original = Chart::new(
2009 ChartType::Column,
2010 ChartData {
2011 categories: vec![ChartCategory::new("Q1"), ChartCategory::new("Q2")],
2012 series: vec![ChartSeries::new("Sales", vec![10.0, 20.0])],
2013 title: None,
2014 data_labels: Some(dl.clone()),
2015 },
2016 );
2017 let xml = original.to_xml();
2018 assert!(xml.contains("<c:dLbls>"), "xml: {}", xml);
2020 assert!(xml.contains(r#"<c:showVal val="1"/>"#), "xml: {}", xml);
2021 assert!(xml.contains(r#"<c:showCatName val="1"/>"#), "xml: {}", xml);
2022 assert!(xml.contains(r#"<c:showSerName val="0"/>"#), "xml: {}", xml);
2023 assert!(
2024 xml.contains(r#"<c:showLegendKey val="0"/>"#),
2025 "xml: {}",
2026 xml
2027 );
2028 assert!(xml.contains(r#"<c:dLblPos val="outEnd"/>"#), "xml: {}", xml);
2029 assert!(xml.contains(r#"<c:separator val=", "/>"#), "xml: {}", xml);
2030 assert!(xml.contains(r#"formatCode="0.00""#), "xml: {}", xml);
2031
2032 let parsed = Chart::parse_from_xml(&xml).expect("parse chart with dLbls");
2034 let parsed_dl = parsed
2035 .data
2036 .data_labels
2037 .expect("chart-level dLbls should parse");
2038 assert_eq!(parsed_dl.show_val, Some(true));
2039 assert_eq!(parsed_dl.show_cat_name, Some(true));
2040 assert_eq!(parsed_dl.show_ser_name, Some(false));
2041 assert_eq!(parsed_dl.show_legend_key, Some(false));
2042 assert_eq!(parsed_dl.show_percent, None);
2043 assert_eq!(parsed_dl.position, Some(LabelPosition::OutsideEnd));
2044 assert_eq!(parsed_dl.separator.as_deref(), Some(", "));
2045 assert_eq!(parsed_dl.num_fmt.as_deref(), Some("0.00"));
2046 }
2047
2048 #[test]
2050 fn parse_series_level_data_labels_round_trip() {
2051 let series_dl = DataLabels {
2052 show_val: Some(true),
2053 show_percent: Some(true),
2054 position: Some(LabelPosition::InsideEnd),
2055 ..Default::default()
2056 };
2057 let original = Chart::new(
2058 ChartType::Pie,
2059 ChartData {
2060 categories: vec![ChartCategory::new("A"), ChartCategory::new("B")],
2061 series: vec![{
2062 let mut s = ChartSeries::new("X", vec![1.0, 2.0]);
2063 s.data_labels = Some(series_dl.clone());
2064 s
2065 }],
2066 title: None,
2067 data_labels: None,
2068 },
2069 );
2070 let xml = original.to_xml();
2071 assert!(xml.contains("<c:dLbls>"), "xml: {}", xml);
2073 assert!(xml.contains(r#"<c:showVal val="1"/>"#), "xml: {}", xml);
2074 assert!(xml.contains(r#"<c:showPercent val="1"/>"#), "xml: {}", xml);
2075 assert!(xml.contains(r#"<c:dLblPos val="inEnd"/>"#), "xml: {}", xml);
2076
2077 let parsed = Chart::parse_from_xml(&xml).expect("parse pie with series dLbls");
2078 assert_eq!(parsed.chart_type, ChartType::Pie);
2079 assert!(parsed.data.data_labels.is_none());
2081 let s = &parsed.data.series[0];
2083 let parsed_dl = s.data_labels.as_ref().expect("series dLbls should parse");
2084 assert_eq!(parsed_dl.show_val, Some(true));
2085 assert_eq!(parsed_dl.show_percent, Some(true));
2086 assert_eq!(parsed_dl.position, Some(LabelPosition::InsideEnd));
2087 }
2088
2089 #[test]
2091 fn parse_empty_dlbls_is_ignored() {
2092 let original = Chart::new(
2094 ChartType::Column,
2095 ChartData {
2096 categories: vec![ChartCategory::new("A")],
2097 series: vec![ChartSeries::new("S", vec![1.0])],
2098 title: None,
2099 data_labels: Some(DataLabels::default()), },
2101 );
2102 let xml = original.to_xml();
2103 assert!(
2105 !xml.contains("<c:dLbls>"),
2106 "empty dLbls should not be written: {}",
2107 xml
2108 );
2109 let parsed = Chart::parse_from_xml(&xml).expect("parse");
2111 assert!(parsed.data.data_labels.is_none());
2112 }
2113
2114 #[test]
2118 fn to_xml_writes_secondary_axis() {
2119 let s2 = {
2120 let mut s = ChartSeries::new("Secondary", vec![1.0, 2.0]);
2121 s.secondary_axis = true;
2122 s
2123 };
2124 let original = Chart::new(
2125 ChartType::Column,
2126 ChartData {
2127 categories: vec![ChartCategory::new("Q1"), ChartCategory::new("Q2")],
2128 series: vec![s2],
2129 title: None,
2130 data_labels: None,
2131 },
2132 );
2133 let xml = original.to_xml();
2134 assert!(xml.contains(r#"<c:axId val="444444444"/>"#), "xml: {}", xml);
2136 assert!(xml.contains(r#"<c:crosses val="max"/>"#), "xml: {}", xml);
2138 let val_ax_count = xml.matches("<c:valAx>").count();
2140 assert_eq!(
2141 val_ax_count, 2,
2142 "expected 2 valAx (primary + secondary): {}",
2143 xml
2144 );
2145 assert!(xml.contains("<c:catAx>"), "xml: {}", xml);
2146 }
2147
2148 #[test]
2151 fn parse_secondary_axis_round_trip() {
2152 let original = Chart::new(
2153 ChartType::Column,
2154 ChartData {
2155 categories: vec![ChartCategory::new("Q1"), ChartCategory::new("Q2")],
2156 series: vec![{
2157 let mut s = ChartSeries::new("S", vec![10.0, 20.0]);
2158 s.secondary_axis = true;
2159 s
2160 }],
2161 title: None,
2162 data_labels: None,
2163 },
2164 );
2165 let xml = original.to_xml();
2166 let parsed = Chart::parse_from_xml(&xml).expect("parse secondary axis chart");
2167 assert_eq!(parsed.data.series.len(), 1);
2169 assert!(
2170 parsed.data.series[0].secondary_axis,
2171 "secondary_axis should be true after round-trip"
2172 );
2173 }
2174
2175 #[test]
2177 fn pie_chart_ignores_secondary_axis() {
2178 let original = Chart::new(
2179 ChartType::Pie,
2180 ChartData {
2181 categories: vec![ChartCategory::new("A"), ChartCategory::new("B")],
2182 series: vec![{
2183 let mut s = ChartSeries::new("X", vec![1.0, 2.0]);
2184 s.secondary_axis = true; s
2186 }],
2187 title: None,
2188 data_labels: None,
2189 },
2190 );
2191 let xml = original.to_xml();
2192 assert!(
2194 !xml.contains("444444444"),
2195 "pie should not write secondary axId: {}",
2196 xml
2197 );
2198 assert!(
2199 !xml.contains(r#"<c:crosses val="max"/>"#),
2200 "pie should not write crosses=max: {}",
2201 xml
2202 );
2203 }
2204
2205 #[test]
2207 fn scatter_chart_ignores_secondary_axis() {
2208 let mut s = ChartSeries::new_scatter("S", vec![1.0, 2.0], vec![3.0, 4.0]);
2209 s.secondary_axis = true; let original = Chart::new(
2211 ChartType::Scatter,
2212 ChartData {
2213 categories: vec![],
2214 series: vec![s],
2215 title: None,
2216 data_labels: None,
2217 },
2218 );
2219 let xml = original.to_xml();
2220 assert!(
2221 !xml.contains("444444444"),
2222 "scatter should not write secondary axId: {}",
2223 xml
2224 );
2225 assert!(
2226 !xml.contains(r#"<c:crosses val="max"/>"#),
2227 "scatter should not write crosses=max: {}",
2228 xml
2229 );
2230 }
2231}