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