1###############################################################################
2#
3# Chart - A class for writing the Excel XLSX Worksheet file.
4#
5# SPDX-License-Identifier: BSD-2-Clause
6#
7# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org
8#
9
10import copy
11import re
12from typing import Any, Dict, Optional
13from warnings import warn
14
15from xlsxwriter.color import Color, ColorTypes
16
17from . import xmlwriter
18from .chart_title import ChartTitle
19from .shape import Shape
20from .utility import (
21 _datetime_to_excel_datetime,
22 _supported_datetime,
23 quote_sheetname,
24 xl_range_formula,
25 xl_rowcol_to_cell,
26)
27
28
29class Chart(xmlwriter.XMLwriter):
30 """
31 A class for writing the Excel XLSX Chart file.
32
33
34 """
35
36 ###########################################################################
37 #
38 # Public API.
39 #
40 ###########################################################################
41
42 def __init__(self) -> None:
43 """
44 Constructor.
45
46 """
47
48 super().__init__()
49
50 self.subtype = None
51 self.sheet_type = 0x0200
52 self.orientation = 0x0
53 self.series = []
54 self.embedded = 0
55 self.id = -1
56 self.series_index = 0
57 self.style_id = 2
58 self.axis_ids = []
59 self.axis2_ids = []
60 self.cat_has_num_fmt = False
61 self.requires_category = False
62 self.legend = {}
63 self.cat_axis_position = "b"
64 self.val_axis_position = "l"
65 self.formula_ids = {}
66 self.formula_data = []
67 self.horiz_cat_axis = 0
68 self.horiz_val_axis = 1
69 self.protection = 0
70 self.chartarea = {}
71 self.plotarea = {}
72 self.x_axis = {}
73 self.y_axis = {}
74 self.y2_axis = {}
75 self.x2_axis = {}
76 self.chart_name = ""
77 self.show_blanks = "gap"
78 self.show_na_as_empty = False
79 self.show_hidden = False
80 self.show_crosses = True
81 self.width = 480
82 self.height = 288
83 self.x_scale = 1
84 self.y_scale = 1
85 self.x_offset = 0
86 self.y_offset = 0
87 self.table = None
88 self.cross_between = "between"
89 self.default_marker = None
90 self.series_gap_1 = None
91 self.series_gap_2 = None
92 self.series_overlap_1 = None
93 self.series_overlap_2 = None
94 self.drop_lines = None
95 self.hi_low_lines = None
96 self.up_down_bars = None
97 self.smooth_allowed = False
98 self.title = ChartTitle()
99
100 self.date_category = False
101 self.date_1904 = False
102 self.remove_timezone = False
103 self.label_positions = {}
104 self.label_position_default = ""
105 self.already_inserted = False
106 self.combined = None
107 self.is_secondary = False
108 self.warn_sheetname = True
109 self._set_default_properties()
110 self.fill = {}
111
112 def add_series(self, options: Optional[Dict[str, Any]] = None) -> None:
113 """
114 Add a data series to a chart.
115
116 Args:
117 options: A dictionary of chart series options.
118
119 Returns:
120 Nothing.
121
122 """
123 # Add a series and it's properties to a chart.
124 if options is None:
125 options = {}
126
127 # Check that the required input has been specified.
128 if "values" not in options:
129 warn("Must specify 'values' in add_series()")
130 return
131
132 if self.requires_category and "categories" not in options:
133 warn("Must specify 'categories' in add_series() for this chart type")
134 return
135
136 if len(self.series) == 255:
137 warn(
138 "The maximum number of series that can be added to an "
139 "Excel Chart is 255"
140 )
141 return
142
143 # Convert list into a formula string.
144 values = self._list_to_formula(options.get("values"))
145 categories = self._list_to_formula(options.get("categories"))
146
147 # Switch name and name_formula parameters if required.
148 name, name_formula = self._process_names(
149 options.get("name"), options.get("name_formula")
150 )
151
152 # Get an id for the data equivalent to the range formula.
153 cat_id = self._get_data_id(categories, options.get("categories_data"))
154 val_id = self._get_data_id(values, options.get("values_data"))
155 name_id = self._get_data_id(name_formula, options.get("name_data"))
156
157 # Set the line properties for the series.
158 line = Shape._get_line_properties(options)
159
160 # Set the fill properties for the series.
161 fill = Shape._get_fill_properties(options.get("fill"))
162
163 # Set the pattern fill properties for the series.
164 pattern = Shape._get_pattern_properties(options.get("pattern"))
165
166 # Set the gradient fill properties for the series.
167 gradient = Shape._get_gradient_properties(options.get("gradient"))
168
169 # Pattern fill overrides solid fill.
170 if pattern:
171 self.fill = None
172
173 # Gradient fill overrides the solid and pattern fill.
174 if gradient:
175 pattern = None
176 fill = None
177
178 # Set the marker properties for the series.
179 marker = self._get_marker_properties(options.get("marker"))
180
181 # Set the trendline properties for the series.
182 trendline = self._get_trendline_properties(options.get("trendline"))
183
184 # Set the line smooth property for the series.
185 smooth = options.get("smooth")
186
187 # Set the error bars properties for the series.
188 y_error_bars = self._get_error_bars_props(options.get("y_error_bars"))
189 x_error_bars = self._get_error_bars_props(options.get("x_error_bars"))
190
191 error_bars = {"x_error_bars": x_error_bars, "y_error_bars": y_error_bars}
192
193 # Set the point properties for the series.
194 points = self._get_points_properties(options.get("points"))
195
196 # Set the labels properties for the series.
197 labels = self._get_labels_properties(options.get("data_labels"))
198
199 # Set the "invert if negative" fill property.
200 invert_if_neg = options.get("invert_if_negative", False)
201 inverted_color = options.get("invert_if_negative_color")
202
203 if inverted_color:
204 inverted_color = Color._from_value(inverted_color)
205
206 # Set the secondary axis properties.
207 x2_axis = options.get("x2_axis")
208 y2_axis = options.get("y2_axis")
209
210 # Store secondary status for combined charts.
211 if x2_axis or y2_axis:
212 self.is_secondary = True
213
214 # Set the gap for Bar/Column charts.
215 if options.get("gap") is not None:
216 if y2_axis:
217 self.series_gap_2 = options["gap"]
218 else:
219 self.series_gap_1 = options["gap"]
220
221 # Set the overlap for Bar/Column charts.
222 if options.get("overlap"):
223 if y2_axis:
224 self.series_overlap_2 = options["overlap"]
225 else:
226 self.series_overlap_1 = options["overlap"]
227
228 # Add the user supplied data to the internal structures.
229 series = {
230 "values": values,
231 "categories": categories,
232 "name": name,
233 "name_formula": name_formula,
234 "name_id": name_id,
235 "val_data_id": val_id,
236 "cat_data_id": cat_id,
237 "line": line,
238 "fill": fill,
239 "pattern": pattern,
240 "gradient": gradient,
241 "marker": marker,
242 "trendline": trendline,
243 "labels": labels,
244 "invert_if_neg": invert_if_neg,
245 "inverted_color": inverted_color,
246 "x2_axis": x2_axis,
247 "y2_axis": y2_axis,
248 "points": points,
249 "error_bars": error_bars,
250 "smooth": smooth,
251 }
252
253 self.series.append(series)
254
255 def set_x_axis(self, options: Dict[str, Any]) -> None:
256 """
257 Set the chart X axis options.
258
259 Args:
260 options: A dictionary of axis options.
261
262 Returns:
263 Nothing.
264
265 """
266 axis = self._convert_axis_args(self.x_axis, options)
267
268 self.x_axis = axis
269
270 def set_y_axis(self, options: Dict[str, Any]) -> None:
271 """
272 Set the chart Y axis options.
273
274 Args:
275 options: A dictionary of axis options.
276
277 Returns:
278 Nothing.
279
280 """
281 axis = self._convert_axis_args(self.y_axis, options)
282
283 self.y_axis = axis
284
285 def set_x2_axis(self, options: Dict[str, Any]) -> None:
286 """
287 Set the chart secondary X axis options.
288
289 Args:
290 options: A dictionary of axis options.
291
292 Returns:
293 Nothing.
294
295 """
296 axis = self._convert_axis_args(self.x2_axis, options)
297
298 self.x2_axis = axis
299
300 def set_y2_axis(self, options: Dict[str, Any]) -> None:
301 """
302 Set the chart secondary Y axis options.
303
304 Args:
305 options: A dictionary of axis options.
306
307 Returns:
308 Nothing.
309
310 """
311 axis = self._convert_axis_args(self.y2_axis, options)
312
313 self.y2_axis = axis
314
315 def set_title(self, options: Optional[Dict[str, Any]] = None) -> None:
316 """
317 Set the chart title options.
318
319 Args:
320 options: A dictionary of chart title options.
321
322 Returns:
323 Nothing.
324
325 """
326 if options is None:
327 options = {}
328
329 name, name_formula = self._process_names(
330 options.get("name"), options.get("name_formula")
331 )
332
333 data_id = self._get_data_id(name_formula, options.get("data"))
334
335 # Update the main chart title.
336 self.title.name = name
337 self.title.formula = name_formula
338 self.title.data_id = data_id
339
340 # Set the font properties if present.
341 if options.get("font"):
342 self.title.font = self._convert_font_args(options.get("font"))
343 else:
344 # For backward/axis compatibility.
345 self.title.font = self._convert_font_args(options.get("name_font"))
346
347 # Set the line properties.
348 self.title.line = Shape._get_line_properties(options)
349
350 # Set the fill properties.
351 self.title.fill = Shape._get_fill_properties(options.get("fill"))
352
353 # Set the gradient properties.
354 self.title.gradient = Shape._get_gradient_properties(options.get("gradient"))
355
356 # Set the layout.
357 self.title.layout = self._get_layout_properties(options.get("layout"), True)
358
359 # Set the title overlay option.
360 self.title.overlay = options.get("overlay")
361
362 # Set the automatic title option.
363 self.title.hidden = options.get("none", False)
364
365 def set_legend(self, options: Dict[str, Any]) -> None:
366 """
367 Set the chart legend options.
368
369 Args:
370 options: A dictionary of chart legend options.
371
372 Returns:
373 Nothing.
374 """
375 # Convert the user defined properties to internal properties.
376 self.legend = self._get_legend_properties(options)
377
378 def set_plotarea(self, options: Dict[str, Any]) -> None:
379 """
380 Set the chart plot area options.
381
382 Args:
383 options: A dictionary of chart plot area options.
384
385 Returns:
386 Nothing.
387 """
388 # Convert the user defined properties to internal properties.
389 self.plotarea = self._get_area_properties(options)
390
391 def set_chartarea(self, options: Dict[str, Any]) -> None:
392 """
393 Set the chart area options.
394
395 Args:
396 options: A dictionary of chart area options.
397
398 Returns:
399 Nothing.
400 """
401 # Convert the user defined properties to internal properties.
402 self.chartarea = self._get_area_properties(options)
403
404 def set_style(self, style_id: int = 2) -> None:
405 """
406 Set the chart style type.
407
408 Args:
409 style_id: An int representing the chart style.
410
411 Returns:
412 Nothing.
413 """
414 # Set one of the 48 built-in Excel chart styles. The default is 2.
415 if style_id is None:
416 style_id = 2
417
418 if style_id < 1 or style_id > 48:
419 style_id = 2
420
421 self.style_id = style_id
422
423 def show_blanks_as(self, option: str) -> None:
424 """
425 Set the option for displaying blank data in a chart.
426
427 Args:
428 option: A string representing the display option.
429
430 Returns:
431 Nothing.
432 """
433 if not option:
434 return
435
436 valid_options = {
437 "gap": 1,
438 "zero": 1,
439 "span": 1,
440 }
441
442 if option not in valid_options:
443 warn(f"Unknown show_blanks_as() option '{option}'")
444 return
445
446 self.show_blanks = option
447
448 def show_na_as_empty_cell(self) -> None:
449 """
450 Display ``#N/A`` on charts as blank/empty cells.
451
452 Args:
453 None.
454
455 Returns:
456 Nothing.
457 """
458 self.show_na_as_empty = True
459
460 def show_hidden_data(self) -> None:
461 """
462 Display data on charts from hidden rows or columns.
463
464 Args:
465 None.
466
467 Returns:
468 Nothing.
469 """
470 self.show_hidden = True
471
472 def set_size(self, options: Optional[Dict[str, Any]] = None) -> None:
473 """
474 Set size or scale of the chart.
475
476 Args:
477 options: A dictionary of chart size options.
478
479 Returns:
480 Nothing.
481 """
482 if options is None:
483 options = {}
484
485 # Set dimensions or scale for the chart.
486 self.width = options.get("width", self.width)
487 self.height = options.get("height", self.height)
488 self.x_scale = options.get("x_scale", 1)
489 self.y_scale = options.get("y_scale", 1)
490 self.x_offset = options.get("x_offset", 0)
491 self.y_offset = options.get("y_offset", 0)
492
493 def set_table(self, options: Optional[Dict[str, Any]] = None) -> None:
494 """
495 Set properties for an axis data table.
496
497 Args:
498 options: A dictionary of axis table options.
499
500 Returns:
501 Nothing.
502
503 """
504 if options is None:
505 options = {}
506
507 table = {}
508
509 table["horizontal"] = options.get("horizontal", 1)
510 table["vertical"] = options.get("vertical", 1)
511 table["outline"] = options.get("outline", 1)
512 table["show_keys"] = options.get("show_keys", 0)
513 table["font"] = self._convert_font_args(options.get("font"))
514
515 self.table = table
516
517 def set_up_down_bars(self, options: Optional[Dict[str, Any]] = None) -> None:
518 """
519 Set properties for the chart up-down bars.
520
521 Args:
522 options: A dictionary of options.
523
524 Returns:
525 Nothing.
526
527 """
528 if options is None:
529 options = {}
530
531 # Defaults.
532 up_line = None
533 up_fill = None
534 down_line = None
535 down_fill = None
536
537 # Set properties for 'up' bar.
538 if options.get("up"):
539 up_line = Shape._get_line_properties(options["up"])
540 up_fill = Shape._get_fill_properties(options["up"]["fill"])
541
542 # Set properties for 'down' bar.
543 if options.get("down"):
544 down_line = Shape._get_line_properties(options["down"])
545 down_fill = Shape._get_fill_properties(options["down"]["fill"])
546
547 self.up_down_bars = {
548 "up": {
549 "line": up_line,
550 "fill": up_fill,
551 },
552 "down": {
553 "line": down_line,
554 "fill": down_fill,
555 },
556 }
557
558 def set_drop_lines(self, options: Optional[Dict[str, Any]] = None) -> None:
559 """
560 Set properties for the chart drop lines.
561
562 Args:
563 options: A dictionary of options.
564
565 Returns:
566 Nothing.
567
568 """
569 if options is None:
570 options = {}
571
572 line = Shape._get_line_properties(options)
573 fill = Shape._get_fill_properties(options.get("fill"))
574
575 # Set the pattern fill properties for the series.
576 pattern = Shape._get_pattern_properties(options.get("pattern"))
577
578 # Set the gradient fill properties for the series.
579 gradient = Shape._get_gradient_properties(options.get("gradient"))
580
581 # Pattern fill overrides solid fill.
582 if pattern:
583 self.fill = None
584
585 # Gradient fill overrides the solid and pattern fill.
586 if gradient:
587 pattern = None
588 fill = None
589
590 self.drop_lines = {
591 "line": line,
592 "fill": fill,
593 "pattern": pattern,
594 "gradient": gradient,
595 }
596
597 def set_high_low_lines(self, options: Optional[Dict[str, Any]] = None) -> None:
598 """
599 Set properties for the chart high-low lines.
600
601 Args:
602 options: A dictionary of options.
603
604 Returns:
605 Nothing.
606
607 """
608 if options is None:
609 options = {}
610
611 line = Shape._get_line_properties(options)
612 fill = Shape._get_fill_properties(options.get("fill"))
613
614 # Set the pattern fill properties for the series.
615 pattern = Shape._get_pattern_properties(options.get("pattern"))
616
617 # Set the gradient fill properties for the series.
618 gradient = Shape._get_gradient_properties(options.get("gradient"))
619
620 # Pattern fill overrides solid fill.
621 if pattern:
622 self.fill = None
623
624 # Gradient fill overrides the solid and pattern fill.
625 if gradient:
626 pattern = None
627 fill = None
628
629 self.hi_low_lines = {
630 "line": line,
631 "fill": fill,
632 "pattern": pattern,
633 "gradient": gradient,
634 }
635
636 def combine(self, chart: Optional["Chart"] = None) -> None:
637 """
638 Create a combination chart with a secondary chart.
639
640 Args:
641 chart: The secondary chart to combine with the primary chart.
642
643 Returns:
644 Nothing.
645
646 """
647 if chart is None:
648 return
649
650 self.combined = chart
651
652 ###########################################################################
653 #
654 # Private API.
655 #
656 ###########################################################################
657
658 def _assemble_xml_file(self) -> None:
659 # Assemble and write the XML file.
660
661 # Write the XML declaration.
662 self._xml_declaration()
663
664 # Write the c:chartSpace element.
665 self._write_chart_space()
666
667 # Write the c:lang element.
668 self._write_lang()
669
670 # Write the c:style element.
671 self._write_style()
672
673 # Write the c:protection element.
674 self._write_protection()
675
676 # Write the c:chart element.
677 self._write_chart()
678
679 # Write the c:spPr element for the chartarea formatting.
680 self._write_sp_pr(self.chartarea)
681
682 # Write the c:printSettings element.
683 if self.embedded:
684 self._write_print_settings()
685
686 # Close the worksheet tag.
687 self._xml_end_tag("c:chartSpace")
688 # Close the file.
689 self._xml_close()
690
691 def _convert_axis_args(self, axis, user_options):
692 # Convert user defined axis values into private hash values.
693 options = axis["defaults"].copy()
694 options.update(user_options)
695
696 axis = {
697 "defaults": axis["defaults"],
698 "reverse": options.get("reverse"),
699 "min": options.get("min"),
700 "max": options.get("max"),
701 "minor_unit": options.get("minor_unit"),
702 "major_unit": options.get("major_unit"),
703 "minor_unit_type": options.get("minor_unit_type"),
704 "major_unit_type": options.get("major_unit_type"),
705 "display_units": options.get("display_units"),
706 "log_base": options.get("log_base"),
707 "crossing": options.get("crossing"),
708 "position_axis": options.get("position_axis"),
709 "position": options.get("position"),
710 "label_position": options.get("label_position"),
711 "label_align": options.get("label_align"),
712 "num_format": options.get("num_format"),
713 "num_format_linked": options.get("num_format_linked"),
714 "interval_unit": options.get("interval_unit"),
715 "interval_tick": options.get("interval_tick"),
716 "text_axis": False,
717 "title": ChartTitle(),
718 }
719
720 axis["visible"] = options.get("visible", True)
721
722 # Convert the display units.
723 axis["display_units"] = self._get_display_units(axis["display_units"])
724 axis["display_units_visible"] = options.get("display_units_visible", True)
725
726 # Map major_gridlines properties.
727 if options.get("major_gridlines") and options["major_gridlines"]["visible"]:
728 axis["major_gridlines"] = self._get_gridline_properties(
729 options["major_gridlines"]
730 )
731
732 # Map minor_gridlines properties.
733 if options.get("minor_gridlines") and options["minor_gridlines"]["visible"]:
734 axis["minor_gridlines"] = self._get_gridline_properties(
735 options["minor_gridlines"]
736 )
737
738 # Only use the first letter of bottom, top, left or right.
739 if axis.get("position"):
740 axis["position"] = axis["position"].lower()[0]
741
742 # Set the position for a category axis on or between the tick marks.
743 if axis.get("position_axis"):
744 if axis["position_axis"] == "on_tick":
745 axis["position_axis"] = "midCat"
746 elif axis["position_axis"] == "between":
747 # Doesn't need to be modified.
748 pass
749 else:
750 # Otherwise use the default value.
751 axis["position_axis"] = None
752
753 # Set the category axis as a date axis.
754 if options.get("date_axis"):
755 self.date_category = True
756
757 # Set the category axis as a text axis.
758 if options.get("text_axis"):
759 self.date_category = False
760 axis["text_axis"] = True
761
762 # Convert datetime args if required.
763 if axis.get("min") and _supported_datetime(axis["min"]):
764 axis["min"] = _datetime_to_excel_datetime(
765 axis["min"], self.date_1904, self.remove_timezone
766 )
767 if axis.get("max") and _supported_datetime(axis["max"]):
768 axis["max"] = _datetime_to_excel_datetime(
769 axis["max"], self.date_1904, self.remove_timezone
770 )
771 if axis.get("crossing") and _supported_datetime(axis["crossing"]):
772 axis["crossing"] = _datetime_to_excel_datetime(
773 axis["crossing"], self.date_1904, self.remove_timezone
774 )
775
776 # Set the font properties if present.
777 axis["num_font"] = self._convert_font_args(options.get("num_font"))
778
779 # Set the line properties for the axis.
780 axis["line"] = Shape._get_line_properties(options)
781
782 # Set the fill properties for the axis.
783 axis["fill"] = Shape._get_fill_properties(options.get("fill"))
784
785 # Set the pattern fill properties for the series.
786 axis["pattern"] = Shape._get_pattern_properties(options.get("pattern"))
787
788 # Set the gradient fill properties for the series.
789 axis["gradient"] = Shape._get_gradient_properties(options.get("gradient"))
790
791 # Pattern fill overrides solid fill.
792 if axis.get("pattern"):
793 axis["fill"] = None
794
795 # Gradient fill overrides the solid and pattern fill.
796 if axis.get("gradient"):
797 axis["pattern"] = None
798 axis["fill"] = None
799
800 # Set the tick marker types.
801 axis["minor_tick_mark"] = self._get_tick_type(options.get("minor_tick_mark"))
802 axis["major_tick_mark"] = self._get_tick_type(options.get("major_tick_mark"))
803
804 # Check if the axis title is simple text or a formula.
805 name, name_formula = self._process_names(
806 options.get("name"), options.get("name_formula")
807 )
808
809 # Get an id for the data equivalent to the range formula.
810 data_id = self._get_data_id(name_formula, options.get("data"))
811
812 # Set the title properties.
813 axis["title"].name = name
814 axis["title"].formula = name_formula
815 axis["title"].data_id = data_id
816 axis["title"].font = self._convert_font_args(options.get("name_font"))
817 axis["title"].layout = self._get_layout_properties(
818 options.get("name_layout"), True
819 )
820
821 # Map the line and border properties for the axis title.
822 options["line"] = options.get("name_line")
823 options["border"] = options.get("name_border")
824
825 axis["title"].line = Shape._get_line_properties(options)
826 axis["title"].fill = Shape._get_fill_properties(options.get("name_fill"))
827 axis["title"].pattern = Shape._get_pattern_properties(
828 options.get("name_pattern")
829 )
830 axis["title"].gradient = Shape._get_gradient_properties(
831 options.get("name_gradient")
832 )
833
834 return axis
835
836 def _convert_font_args(self, options):
837 # Convert user defined font values into private dict values.
838 if not options:
839 return {}
840
841 font = {
842 "name": options.get("name"),
843 "color": options.get("color"),
844 "size": options.get("size"),
845 "bold": options.get("bold"),
846 "italic": options.get("italic"),
847 "underline": options.get("underline"),
848 "pitch_family": options.get("pitch_family"),
849 "charset": options.get("charset"),
850 "baseline": options.get("baseline", 0),
851 "rotation": options.get("rotation"),
852 }
853
854 # Convert font size units.
855 if font["size"]:
856 font["size"] = int(font["size"] * 100)
857
858 # Convert rotation into 60,000ths of a degree.
859 if font["rotation"]:
860 font["rotation"] = 60000 * int(font["rotation"])
861
862 if font.get("color"):
863 font["color"] = Color._from_value(font["color"])
864
865 return font
866
867 def _list_to_formula(self, data):
868 # Convert and list of row col values to a range formula.
869
870 # If it isn't an array ref it is probably a formula already.
871 if not isinstance(data, list):
872 # Check for unquoted sheetnames.
873 if data and " " in data and "'" not in data and self.warn_sheetname:
874 warn(
875 f"Sheetname in '{data}' contains spaces but isn't quoted. "
876 f"This may cause an error in Excel."
877 )
878 return data
879
880 formula = xl_range_formula(*data)
881
882 return formula
883
884 def _process_names(self, name, name_formula):
885 # Switch name and name_formula parameters if required.
886
887 if name is not None:
888 if isinstance(name, list):
889 # Convert a list of values into a name formula.
890 cell = xl_rowcol_to_cell(name[1], name[2], True, True)
891 name_formula = quote_sheetname(name[0]) + "!" + cell
892 name = ""
893 elif re.match(r"^=?[^!]+!\$?[A-Z]+\$?\d+", name):
894 # Name looks like a formula, use it to set name_formula.
895 name_formula = name
896 name = ""
897
898 return name, name_formula
899
900 def _get_data_type(self, data) -> str:
901 # Find the overall type of the data associated with a series.
902
903 # Check for no data in the series.
904 if data is None or len(data) == 0:
905 return "none"
906
907 if isinstance(data[0], list):
908 return "multi_str"
909
910 # Determine if data is numeric or strings.
911 for token in data:
912 if token is None:
913 continue
914
915 # Check for strings that would evaluate to float like
916 # '1.1_1' of ' 1'.
917 if isinstance(token, str) and re.search("[_ ]", token):
918 # Assume entire data series is string data.
919 return "str"
920
921 try:
922 float(token)
923 except ValueError:
924 # Not a number. Assume entire data series is string data.
925 return "str"
926
927 # The series data was all numeric.
928 return "num"
929
930 def _get_data_id(self, formula, data):
931 # Assign an id to a each unique series formula or title/axis formula.
932 # Repeated formulas such as for categories get the same id. If the
933 # series or title has user specified data associated with it then
934 # that is also stored. This data is used to populate cached Excel
935 # data when creating a chart. If there is no user defined data then
936 # it will be populated by the parent Workbook._add_chart_data().
937
938 # Ignore series without a range formula.
939 if not formula:
940 return None
941
942 # Strip the leading '=' from the formula.
943 if formula.startswith("="):
944 formula = formula.lstrip("=")
945
946 # Store the data id in a hash keyed by the formula and store the data
947 # in a separate array with the same id.
948 if formula not in self.formula_ids:
949 # Haven't seen this formula before.
950 formula_id = len(self.formula_data)
951
952 self.formula_data.append(data)
953 self.formula_ids[formula] = formula_id
954 else:
955 # Formula already seen. Return existing id.
956 formula_id = self.formula_ids[formula]
957
958 # Store user defined data if it isn't already there.
959 if self.formula_data[formula_id] is None:
960 self.formula_data[formula_id] = data
961
962 return formula_id
963
964 def _get_marker_properties(self, marker):
965 # Convert user marker properties to the structure required internally.
966
967 if not marker:
968 return None
969
970 # Copy the user defined properties since they will be modified.
971 marker = copy.deepcopy(marker)
972
973 types = {
974 "automatic": "automatic",
975 "none": "none",
976 "square": "square",
977 "diamond": "diamond",
978 "triangle": "triangle",
979 "x": "x",
980 "star": "star",
981 "dot": "dot",
982 "short_dash": "dot",
983 "dash": "dash",
984 "long_dash": "dash",
985 "circle": "circle",
986 "plus": "plus",
987 "picture": "picture",
988 }
989
990 # Check for valid types.
991 marker_type = marker.get("type")
992
993 if marker_type is not None:
994 if marker_type in types:
995 marker["type"] = types[marker_type]
996 else:
997 warn(f"Unknown marker type '{marker_type}")
998 return None
999
1000 # Set the line properties for the marker.
1001 line = Shape._get_line_properties(marker)
1002
1003 # Set the fill properties for the marker.
1004 fill = Shape._get_fill_properties(marker.get("fill"))
1005
1006 # Set the pattern fill properties for the series.
1007 pattern = Shape._get_pattern_properties(marker.get("pattern"))
1008
1009 # Set the gradient fill properties for the series.
1010 gradient = Shape._get_gradient_properties(marker.get("gradient"))
1011
1012 # Pattern fill overrides solid fill.
1013 if pattern:
1014 self.fill = None
1015
1016 # Gradient fill overrides the solid and pattern fill.
1017 if gradient:
1018 pattern = None
1019 fill = None
1020
1021 marker["line"] = line
1022 marker["fill"] = fill
1023 marker["pattern"] = pattern
1024 marker["gradient"] = gradient
1025
1026 return marker
1027
1028 def _get_trendline_properties(self, trendline):
1029 # Convert user trendline properties to structure required internally.
1030
1031 if not trendline:
1032 return None
1033
1034 # Copy the user defined properties since they will be modified.
1035 trendline = copy.deepcopy(trendline)
1036
1037 types = {
1038 "exponential": "exp",
1039 "linear": "linear",
1040 "log": "log",
1041 "moving_average": "movingAvg",
1042 "polynomial": "poly",
1043 "power": "power",
1044 }
1045
1046 # Check the trendline type.
1047 trend_type = trendline.get("type")
1048
1049 if trend_type in types:
1050 trendline["type"] = types[trend_type]
1051 else:
1052 warn(f"Unknown trendline type '{trend_type}'")
1053 return None
1054
1055 # Set the line properties for the trendline.
1056 line = Shape._get_line_properties(trendline)
1057
1058 # Set the fill properties for the trendline.
1059 fill = Shape._get_fill_properties(trendline.get("fill"))
1060
1061 # Set the pattern fill properties for the trendline.
1062 pattern = Shape._get_pattern_properties(trendline.get("pattern"))
1063
1064 # Set the gradient fill properties for the trendline.
1065 gradient = Shape._get_gradient_properties(trendline.get("gradient"))
1066
1067 # Set the format properties for the trendline label.
1068 label = self._get_trendline_label_properties(trendline.get("label"))
1069
1070 # Pattern fill overrides solid fill.
1071 if pattern:
1072 self.fill = None
1073
1074 # Gradient fill overrides the solid and pattern fill.
1075 if gradient:
1076 pattern = None
1077 fill = None
1078
1079 trendline["line"] = line
1080 trendline["fill"] = fill
1081 trendline["pattern"] = pattern
1082 trendline["gradient"] = gradient
1083 trendline["label"] = label
1084
1085 return trendline
1086
1087 def _get_trendline_label_properties(self, label):
1088 # Convert user trendline properties to structure required internally.
1089
1090 if not label:
1091 return {}
1092
1093 # Copy the user defined properties since they will be modified.
1094 label = copy.deepcopy(label)
1095
1096 # Set the font properties if present.
1097 font = self._convert_font_args(label.get("font"))
1098
1099 # Set the line properties for the label.
1100 line = Shape._get_line_properties(label)
1101
1102 # Set the fill properties for the label.
1103 fill = Shape._get_fill_properties(label.get("fill"))
1104
1105 # Set the pattern fill properties for the label.
1106 pattern = Shape._get_pattern_properties(label.get("pattern"))
1107
1108 # Set the gradient fill properties for the label.
1109 gradient = Shape._get_gradient_properties(label.get("gradient"))
1110
1111 # Pattern fill overrides solid fill.
1112 if pattern:
1113 self.fill = None
1114
1115 # Gradient fill overrides the solid and pattern fill.
1116 if gradient:
1117 pattern = None
1118 fill = None
1119
1120 label["font"] = font
1121 label["line"] = line
1122 label["fill"] = fill
1123 label["pattern"] = pattern
1124 label["gradient"] = gradient
1125
1126 return label
1127
1128 def _get_error_bars_props(self, options):
1129 # Convert user error bars properties to structure required internally.
1130 if not options:
1131 return {}
1132
1133 # Default values.
1134 error_bars = {"type": "fixedVal", "value": 1, "endcap": 1, "direction": "both"}
1135
1136 types = {
1137 "fixed": "fixedVal",
1138 "percentage": "percentage",
1139 "standard_deviation": "stdDev",
1140 "standard_error": "stdErr",
1141 "custom": "cust",
1142 }
1143
1144 # Check the error bars type.
1145 error_type = options["type"]
1146
1147 if error_type in types:
1148 error_bars["type"] = types[error_type]
1149 else:
1150 warn(f"Unknown error bars type '{error_type}")
1151 return {}
1152
1153 # Set the value for error types that require it.
1154 if "value" in options:
1155 error_bars["value"] = options["value"]
1156
1157 # Set the end-cap style.
1158 if "end_style" in options:
1159 error_bars["endcap"] = options["end_style"]
1160
1161 # Set the error bar direction.
1162 if "direction" in options:
1163 if options["direction"] == "minus":
1164 error_bars["direction"] = "minus"
1165 elif options["direction"] == "plus":
1166 error_bars["direction"] = "plus"
1167 else:
1168 # Default to 'both'.
1169 pass
1170
1171 # Set any custom values.
1172 error_bars["plus_values"] = options.get("plus_values")
1173 error_bars["minus_values"] = options.get("minus_values")
1174 error_bars["plus_data"] = options.get("plus_data")
1175 error_bars["minus_data"] = options.get("minus_data")
1176
1177 # Set the line properties for the error bars.
1178 error_bars["line"] = Shape._get_line_properties(options)
1179
1180 return error_bars
1181
1182 def _get_gridline_properties(self, options):
1183 # Convert user gridline properties to structure required internally.
1184
1185 # Set the visible property for the gridline.
1186 gridline = {"visible": options.get("visible")}
1187
1188 # Set the line properties for the gridline.
1189 gridline["line"] = Shape._get_line_properties(options)
1190
1191 return gridline
1192
1193 def _get_labels_properties(self, labels):
1194 # Convert user labels properties to the structure required internally.
1195
1196 if not labels:
1197 return None
1198
1199 # Copy the user defined properties since they will be modified.
1200 labels = copy.deepcopy(labels)
1201
1202 # Map user defined label positions to Excel positions.
1203 position = labels.get("position")
1204
1205 if position:
1206 if position in self.label_positions:
1207 if position == self.label_position_default:
1208 labels["position"] = None
1209 else:
1210 labels["position"] = self.label_positions[position]
1211 else:
1212 warn(f"Unsupported label position '{position}' for this chart type")
1213 return None
1214
1215 # Map the user defined label separator to the Excel separator.
1216 separator = labels.get("separator")
1217 separators = {
1218 ",": ", ",
1219 ";": "; ",
1220 ".": ". ",
1221 "\n": "\n",
1222 " ": " ",
1223 }
1224
1225 if separator:
1226 if separator in separators:
1227 labels["separator"] = separators[separator]
1228 else:
1229 warn("Unsupported label separator")
1230 return None
1231
1232 # Set the font properties if present.
1233 labels["font"] = self._convert_font_args(labels.get("font"))
1234
1235 # Set the line properties for the labels.
1236 line = Shape._get_line_properties(labels)
1237
1238 # Set the fill properties for the labels.
1239 fill = Shape._get_fill_properties(labels.get("fill"))
1240
1241 # Set the pattern fill properties for the labels.
1242 pattern = Shape._get_pattern_properties(labels.get("pattern"))
1243
1244 # Set the gradient fill properties for the labels.
1245 gradient = Shape._get_gradient_properties(labels.get("gradient"))
1246
1247 # Pattern fill overrides solid fill.
1248 if pattern:
1249 self.fill = None
1250
1251 # Gradient fill overrides the solid and pattern fill.
1252 if gradient:
1253 pattern = None
1254 fill = None
1255
1256 labels["line"] = line
1257 labels["fill"] = fill
1258 labels["pattern"] = pattern
1259 labels["gradient"] = gradient
1260
1261 if labels.get("custom"):
1262 for label in labels["custom"]:
1263 if label is None:
1264 continue
1265
1266 value = label.get("value")
1267 if value and re.match(r"^=?[^!]+!\$?[A-Z]+\$?\d+", str(value)):
1268 label["formula"] = value
1269
1270 formula = label.get("formula")
1271 if formula and formula.startswith("="):
1272 label["formula"] = formula.lstrip("=")
1273
1274 data_id = self._get_data_id(formula, label.get("data"))
1275 label["data_id"] = data_id
1276
1277 label["font"] = self._convert_font_args(label.get("font"))
1278
1279 # Set the line properties for the label.
1280 line = Shape._get_line_properties(label)
1281
1282 # Set the fill properties for the label.
1283 fill = Shape._get_fill_properties(label.get("fill"))
1284
1285 # Set the pattern fill properties for the label.
1286 pattern = Shape._get_pattern_properties(label.get("pattern"))
1287
1288 # Set the gradient fill properties for the label.
1289 gradient = Shape._get_gradient_properties(label.get("gradient"))
1290
1291 # Pattern fill overrides solid fill.
1292 if pattern:
1293 self.fill = None
1294
1295 # Gradient fill overrides the solid and pattern fill.
1296 if gradient:
1297 pattern = None
1298 fill = None
1299
1300 # Map user defined label positions to Excel positions.
1301 position = label.get("position")
1302
1303 if position:
1304 if position in self.label_positions:
1305 if position == self.label_position_default:
1306 label["position"] = None
1307 else:
1308 label["position"] = self.label_positions[position]
1309 else:
1310 warn(f"Unsupported label position '{position}' for chart type")
1311 return None
1312
1313 label["line"] = line
1314 label["fill"] = fill
1315 label["pattern"] = pattern
1316 label["gradient"] = gradient
1317
1318 return labels
1319
1320 def _get_area_properties(self, options):
1321 # Convert user area properties to the structure required internally.
1322 area = {}
1323
1324 # Set the line properties for the chartarea.
1325 line = Shape._get_line_properties(options)
1326
1327 # Set the fill properties for the chartarea.
1328 fill = Shape._get_fill_properties(options.get("fill"))
1329
1330 # Set the pattern fill properties for the series.
1331 pattern = Shape._get_pattern_properties(options.get("pattern"))
1332
1333 # Set the gradient fill properties for the series.
1334 gradient = Shape._get_gradient_properties(options.get("gradient"))
1335
1336 # Pattern fill overrides solid fill.
1337 if pattern:
1338 self.fill = None
1339
1340 # Gradient fill overrides the solid and pattern fill.
1341 if gradient:
1342 pattern = None
1343 fill = None
1344
1345 # Set the plotarea layout.
1346 layout = self._get_layout_properties(options.get("layout"), False)
1347
1348 area["line"] = line
1349 area["fill"] = fill
1350 area["pattern"] = pattern
1351 area["layout"] = layout
1352 area["gradient"] = gradient
1353
1354 return area
1355
1356 def _get_legend_properties(self, options: Optional[Dict[str, Any]] = None):
1357 # Convert user legend properties to the structure required internally.
1358 legend = {}
1359
1360 if options is None:
1361 options = {}
1362
1363 legend["position"] = options.get("position", "right")
1364 legend["delete_series"] = options.get("delete_series")
1365 legend["font"] = self._convert_font_args(options.get("font"))
1366 legend["layout"] = self._get_layout_properties(options.get("layout"), False)
1367
1368 # Turn off the legend.
1369 if options.get("none"):
1370 legend["position"] = "none"
1371
1372 # Set the line properties for the legend.
1373 line = Shape._get_line_properties(options)
1374
1375 # Set the fill properties for the legend.
1376 fill = Shape._get_fill_properties(options.get("fill"))
1377
1378 # Set the pattern fill properties for the series.
1379 pattern = Shape._get_pattern_properties(options.get("pattern"))
1380
1381 # Set the gradient fill properties for the series.
1382 gradient = Shape._get_gradient_properties(options.get("gradient"))
1383
1384 # Pattern fill overrides solid fill.
1385 if pattern:
1386 self.fill = None
1387
1388 # Gradient fill overrides the solid and pattern fill.
1389 if gradient:
1390 pattern = None
1391 fill = None
1392
1393 # Set the legend layout.
1394 layout = self._get_layout_properties(options.get("layout"), False)
1395
1396 legend["line"] = line
1397 legend["fill"] = fill
1398 legend["pattern"] = pattern
1399 legend["layout"] = layout
1400 legend["gradient"] = gradient
1401
1402 return legend
1403
1404 def _get_layout_properties(self, args, is_text):
1405 # Convert user defined layout properties to format used internally.
1406 layout = {}
1407
1408 if not args:
1409 return {}
1410
1411 if is_text:
1412 properties = ("x", "y")
1413 else:
1414 properties = ("x", "y", "width", "height")
1415
1416 # Check for valid properties.
1417 for key in args.keys():
1418 if key not in properties:
1419 warn(f"Property '{key}' not supported in layout options")
1420 return {}
1421
1422 # Set the layout properties.
1423 for prop in properties:
1424 if prop not in args.keys():
1425 warn(f"Property '{prop}' must be specified in layout options")
1426 return {}
1427
1428 value = args[prop]
1429
1430 try:
1431 float(value)
1432 except ValueError:
1433 warn(f"Property '{prop}' value '{value}' must be numeric in layout")
1434 return {}
1435
1436 if value < 0 or value > 1:
1437 warn(
1438 f"Property '{prop}' value '{value}' must be in range "
1439 f"0 < x <= 1 in layout options"
1440 )
1441 return {}
1442
1443 # Convert to the format used by Excel for easier testing
1444 layout[prop] = f"{value:.17g}"
1445
1446 return layout
1447
1448 def _get_points_properties(self, user_points):
1449 # Convert user points properties to structure required internally.
1450 points = []
1451
1452 if not user_points:
1453 return []
1454
1455 for user_point in user_points:
1456 point = {}
1457
1458 if user_point is not None:
1459 # Set the line properties for the point.
1460 line = Shape._get_line_properties(user_point)
1461
1462 # Set the fill properties for the chartarea.
1463 fill = Shape._get_fill_properties(user_point.get("fill"))
1464
1465 # Set the pattern fill properties for the series.
1466 pattern = Shape._get_pattern_properties(user_point.get("pattern"))
1467
1468 # Set the gradient fill properties for the series.
1469 gradient = Shape._get_gradient_properties(user_point.get("gradient"))
1470
1471 # Pattern fill overrides solid fill.
1472 if pattern:
1473 self.fill = None
1474
1475 # Gradient fill overrides the solid and pattern fill.
1476 if gradient:
1477 pattern = None
1478 fill = None
1479
1480 point["line"] = line
1481 point["fill"] = fill
1482 point["pattern"] = pattern
1483 point["gradient"] = gradient
1484
1485 points.append(point)
1486
1487 return points
1488
1489 def _has_formatting(self, element: dict) -> bool:
1490 # Check if a chart element has line, fill or gradient formatting.
1491 has_fill = element.get("fill") and element["fill"]["defined"]
1492 has_line = element.get("line") and element["line"]["defined"]
1493 has_pattern = element.get("pattern")
1494 has_gradient = element.get("gradient")
1495
1496 return has_fill or has_line or has_pattern or has_gradient
1497
1498 def _get_display_units(self, display_units):
1499 # Convert user defined display units to internal units.
1500 if not display_units:
1501 return None
1502
1503 types = {
1504 "hundreds": "hundreds",
1505 "thousands": "thousands",
1506 "ten_thousands": "tenThousands",
1507 "hundred_thousands": "hundredThousands",
1508 "millions": "millions",
1509 "ten_millions": "tenMillions",
1510 "hundred_millions": "hundredMillions",
1511 "billions": "billions",
1512 "trillions": "trillions",
1513 }
1514
1515 if display_units in types:
1516 display_units = types[display_units]
1517 else:
1518 warn(f"Unknown display_units type '{display_units}'")
1519 return None
1520
1521 return display_units
1522
1523 def _get_tick_type(self, tick_type):
1524 # Convert user defined display units to internal units.
1525 if not tick_type:
1526 return None
1527
1528 types = {
1529 "outside": "out",
1530 "inside": "in",
1531 "none": "none",
1532 "cross": "cross",
1533 }
1534
1535 if tick_type in types:
1536 tick_type = types[tick_type]
1537 else:
1538 warn(f"Unknown tick_type '{tick_type}'")
1539 return None
1540
1541 return tick_type
1542
1543 def _get_primary_axes_series(self):
1544 # Returns series which use the primary axes.
1545 primary_axes_series = []
1546
1547 for series in self.series:
1548 if not series["y2_axis"]:
1549 primary_axes_series.append(series)
1550
1551 return primary_axes_series
1552
1553 def _get_secondary_axes_series(self):
1554 # Returns series which use the secondary axes.
1555 secondary_axes_series = []
1556
1557 for series in self.series:
1558 if series["y2_axis"]:
1559 secondary_axes_series.append(series)
1560
1561 return secondary_axes_series
1562
1563 def _add_axis_ids(self, args) -> None:
1564 # Add unique ids for primary or secondary axes
1565 chart_id = 5001 + int(self.id)
1566 axis_count = 1 + len(self.axis2_ids) + len(self.axis_ids)
1567
1568 id1 = f"{chart_id:04d}{axis_count:04d}"
1569 id2 = f"{chart_id:04d}{axis_count + 1:04d}"
1570
1571 if args["primary_axes"]:
1572 self.axis_ids.append(id1)
1573 self.axis_ids.append(id2)
1574
1575 if not args["primary_axes"]:
1576 self.axis2_ids.append(id1)
1577 self.axis2_ids.append(id2)
1578
1579 def _set_default_properties(self) -> None:
1580 # Setup the default properties for a chart.
1581
1582 self.x_axis["defaults"] = {
1583 "num_format": "General",
1584 "major_gridlines": {"visible": 0},
1585 }
1586
1587 self.y_axis["defaults"] = {
1588 "num_format": "General",
1589 "major_gridlines": {"visible": 1},
1590 }
1591
1592 self.x2_axis["defaults"] = {
1593 "num_format": "General",
1594 "label_position": "none",
1595 "crossing": "max",
1596 "visible": 0,
1597 }
1598
1599 self.y2_axis["defaults"] = {
1600 "num_format": "General",
1601 "major_gridlines": {"visible": 0},
1602 "position": "right",
1603 "visible": 1,
1604 }
1605
1606 self.set_x_axis({})
1607 self.set_y_axis({})
1608
1609 self.set_x2_axis({})
1610 self.set_y2_axis({})
1611
1612 ###########################################################################
1613 #
1614 # XML methods.
1615 #
1616 ###########################################################################
1617
1618 def _write_chart_space(self) -> None:
1619 # Write the <c:chartSpace> element.
1620 schema = "http://schemas.openxmlformats.org/"
1621 xmlns_c = schema + "drawingml/2006/chart"
1622 xmlns_a = schema + "drawingml/2006/main"
1623 xmlns_r = schema + "officeDocument/2006/relationships"
1624
1625 attributes = [
1626 ("xmlns:c", xmlns_c),
1627 ("xmlns:a", xmlns_a),
1628 ("xmlns:r", xmlns_r),
1629 ]
1630
1631 self._xml_start_tag("c:chartSpace", attributes)
1632
1633 def _write_lang(self) -> None:
1634 # Write the <c:lang> element.
1635 val = "en-US"
1636
1637 attributes = [("val", val)]
1638
1639 self._xml_empty_tag("c:lang", attributes)
1640
1641 def _write_style(self) -> None:
1642 # Write the <c:style> element.
1643 style_id = self.style_id
1644
1645 # Don't write an element for the default style, 2.
1646 if style_id == 2:
1647 return
1648
1649 attributes = [("val", style_id)]
1650
1651 self._xml_empty_tag("c:style", attributes)
1652
1653 def _write_chart(self) -> None:
1654 # Write the <c:chart> element.
1655 self._xml_start_tag("c:chart")
1656
1657 if self.title.is_hidden():
1658 # Turn off the title.
1659 self._write_c_auto_title_deleted()
1660 else:
1661 # Write the chart title elements.
1662 self._write_title(self.title)
1663
1664 # Write the c:plotArea element.
1665 self._write_plot_area()
1666
1667 # Write the c:legend element.
1668 self._write_legend()
1669
1670 # Write the c:plotVisOnly element.
1671 self._write_plot_vis_only()
1672
1673 # Write the c:dispBlanksAs element.
1674 self._write_disp_blanks_as()
1675
1676 # Write the c:extLst element.
1677 if self.show_na_as_empty:
1678 self._write_c_ext_lst_display_na()
1679
1680 self._xml_end_tag("c:chart")
1681
1682 def _write_disp_blanks_as(self) -> None:
1683 # Write the <c:dispBlanksAs> element.
1684 val = self.show_blanks
1685
1686 # Ignore the default value.
1687 if val == "gap":
1688 return
1689
1690 attributes = [("val", val)]
1691
1692 self._xml_empty_tag("c:dispBlanksAs", attributes)
1693
1694 def _write_plot_area(self) -> None:
1695 # Write the <c:plotArea> element.
1696 self._xml_start_tag("c:plotArea")
1697
1698 # Write the c:layout element.
1699 self._write_layout(self.plotarea.get("layout"), "plot")
1700
1701 # Write subclass chart type elements for primary and secondary axes.
1702 self._write_chart_type({"primary_axes": True})
1703 self._write_chart_type({"primary_axes": False})
1704
1705 # Configure a combined chart if present.
1706 second_chart = self.combined
1707 if second_chart:
1708 # Secondary axis has unique id otherwise use same as primary.
1709 if second_chart.is_secondary:
1710 second_chart.id = 1000 + self.id
1711 else:
1712 second_chart.id = self.id
1713
1714 # Share the same filehandle for writing.
1715 second_chart.fh = self.fh
1716
1717 # Share series index with primary chart.
1718 second_chart.series_index = self.series_index
1719
1720 # Write the subclass chart type elements for combined chart.
1721 second_chart._write_chart_type({"primary_axes": True})
1722 second_chart._write_chart_type({"primary_axes": False})
1723
1724 # Write the category and value elements for the primary axes.
1725 args = {"x_axis": self.x_axis, "y_axis": self.y_axis, "axis_ids": self.axis_ids}
1726
1727 if self.date_category:
1728 self._write_date_axis(args)
1729 else:
1730 self._write_cat_axis(args)
1731
1732 self._write_val_axis(args)
1733
1734 # Write the category and value elements for the secondary axes.
1735 args = {
1736 "x_axis": self.x2_axis,
1737 "y_axis": self.y2_axis,
1738 "axis_ids": self.axis2_ids,
1739 }
1740
1741 self._write_val_axis(args)
1742
1743 # Write the secondary axis for the secondary chart.
1744 if second_chart and second_chart.is_secondary:
1745 args = {
1746 "x_axis": second_chart.x2_axis,
1747 "y_axis": second_chart.y2_axis,
1748 "axis_ids": second_chart.axis2_ids,
1749 }
1750
1751 second_chart._write_val_axis(args)
1752
1753 if self.date_category:
1754 self._write_date_axis(args)
1755 else:
1756 self._write_cat_axis(args)
1757
1758 # Write the c:dTable element.
1759 self._write_d_table()
1760
1761 # Write the c:spPr element for the plotarea formatting.
1762 self._write_sp_pr(self.plotarea)
1763
1764 self._xml_end_tag("c:plotArea")
1765
1766 def _write_layout(self, layout, layout_type) -> None:
1767 # Write the <c:layout> element.
1768
1769 if not layout:
1770 # Automatic layout.
1771 self._xml_empty_tag("c:layout")
1772 else:
1773 # User defined manual layout.
1774 self._xml_start_tag("c:layout")
1775 self._write_manual_layout(layout, layout_type)
1776 self._xml_end_tag("c:layout")
1777
1778 def _write_manual_layout(self, layout, layout_type) -> None:
1779 # Write the <c:manualLayout> element.
1780 self._xml_start_tag("c:manualLayout")
1781
1782 # Plotarea has a layoutTarget element.
1783 if layout_type == "plot":
1784 self._xml_empty_tag("c:layoutTarget", [("val", "inner")])
1785
1786 # Set the x, y positions.
1787 self._xml_empty_tag("c:xMode", [("val", "edge")])
1788 self._xml_empty_tag("c:yMode", [("val", "edge")])
1789 self._xml_empty_tag("c:x", [("val", layout["x"])])
1790 self._xml_empty_tag("c:y", [("val", layout["y"])])
1791
1792 # For plotarea and legend set the width and height.
1793 if layout_type != "text":
1794 self._xml_empty_tag("c:w", [("val", layout["width"])])
1795 self._xml_empty_tag("c:h", [("val", layout["height"])])
1796
1797 self._xml_end_tag("c:manualLayout")
1798
1799 def _write_chart_type(self, args) -> None:
1800 # pylint: disable=unused-argument
1801 # Write the chart type element. This method should be overridden
1802 # by the subclasses.
1803 return
1804
1805 def _write_grouping(self, val) -> None:
1806 # Write the <c:grouping> element.
1807 attributes = [("val", val)]
1808
1809 self._xml_empty_tag("c:grouping", attributes)
1810
1811 def _write_series(self, series) -> None:
1812 # Write the series elements.
1813 self._write_ser(series)
1814
1815 def _write_ser(self, series) -> None:
1816 # Write the <c:ser> element.
1817 index = self.series_index
1818 self.series_index += 1
1819
1820 self._xml_start_tag("c:ser")
1821
1822 # Write the c:idx element.
1823 self._write_idx(index)
1824
1825 # Write the c:order element.
1826 self._write_order(index)
1827
1828 # Write the series name.
1829 self._write_series_name(series)
1830
1831 # Write the c:spPr element.
1832 self._write_sp_pr(series)
1833
1834 # Write the c:marker element.
1835 self._write_marker(series["marker"])
1836
1837 # Write the c:invertIfNegative element.
1838 self._write_c_invert_if_negative(series["invert_if_neg"])
1839
1840 # Write the c:dPt element.
1841 self._write_d_pt(series["points"])
1842
1843 # Write the c:dLbls element.
1844 self._write_d_lbls(series["labels"])
1845
1846 # Write the c:trendline element.
1847 self._write_trendline(series["trendline"])
1848
1849 # Write the c:errBars element.
1850 self._write_error_bars(series["error_bars"])
1851
1852 # Write the c:cat element.
1853 self._write_cat(series)
1854
1855 # Write the c:val element.
1856 self._write_val(series)
1857
1858 # Write the c:smooth element.
1859 if self.smooth_allowed:
1860 self._write_c_smooth(series["smooth"])
1861
1862 # Write the c:extLst element.
1863 if series.get("inverted_color"):
1864 self._write_c_ext_lst_inverted_color(series["inverted_color"])
1865
1866 self._xml_end_tag("c:ser")
1867
1868 def _write_c_ext_lst_inverted_color(self, color: Color) -> None:
1869 # Write the <c:extLst> element for the inverted fill color.
1870
1871 uri = "{6F2FDCE9-48DA-4B69-8628-5D25D57E5C99}"
1872 xmlns_c_14 = "http://schemas.microsoft.com/office/drawing/2007/8/2/chart"
1873
1874 attributes1 = [
1875 ("uri", uri),
1876 ("xmlns:c14", xmlns_c_14),
1877 ]
1878
1879 attributes2 = [("xmlns:c14", xmlns_c_14)]
1880
1881 self._xml_start_tag("c:extLst")
1882 self._xml_start_tag("c:ext", attributes1)
1883 self._xml_start_tag("c14:invertSolidFillFmt")
1884 self._xml_start_tag("c14:spPr", attributes2)
1885
1886 self._write_a_solid_fill({"color": color})
1887
1888 self._xml_end_tag("c14:spPr")
1889 self._xml_end_tag("c14:invertSolidFillFmt")
1890 self._xml_end_tag("c:ext")
1891 self._xml_end_tag("c:extLst")
1892
1893 def _write_c_ext_lst_display_na(self) -> None:
1894 # Write the <c:extLst> element for the display NA as empty cell option.
1895
1896 uri = "{56B9EC1D-385E-4148-901F-78D8002777C0}"
1897 xmlns_c_16 = "http://schemas.microsoft.com/office/drawing/2017/03/chart"
1898
1899 attributes1 = [
1900 ("uri", uri),
1901 ("xmlns:c16r3", xmlns_c_16),
1902 ]
1903
1904 attributes2 = [("val", 1)]
1905
1906 self._xml_start_tag("c:extLst")
1907 self._xml_start_tag("c:ext", attributes1)
1908 self._xml_start_tag("c16r3:dataDisplayOptions16")
1909 self._xml_empty_tag("c16r3:dispNaAsBlank", attributes2)
1910 self._xml_end_tag("c16r3:dataDisplayOptions16")
1911 self._xml_end_tag("c:ext")
1912 self._xml_end_tag("c:extLst")
1913
1914 def _write_idx(self, val) -> None:
1915 # Write the <c:idx> element.
1916
1917 attributes = [("val", val)]
1918
1919 self._xml_empty_tag("c:idx", attributes)
1920
1921 def _write_order(self, val) -> None:
1922 # Write the <c:order> element.
1923
1924 attributes = [("val", val)]
1925
1926 self._xml_empty_tag("c:order", attributes)
1927
1928 def _write_series_name(self, series) -> None:
1929 # Write the series name.
1930
1931 if series["name_formula"] is not None:
1932 self._write_tx_formula(series["name_formula"], series["name_id"])
1933 elif series["name"] is not None:
1934 self._write_tx_value(series["name"])
1935
1936 def _write_c_smooth(self, smooth) -> None:
1937 # Write the <c:smooth> element.
1938
1939 if smooth:
1940 self._xml_empty_tag("c:smooth", [("val", "1")])
1941
1942 def _write_cat(self, series) -> None:
1943 # Write the <c:cat> element.
1944 formula = series["categories"]
1945 data_id = series["cat_data_id"]
1946 data = None
1947
1948 if data_id is not None:
1949 data = self.formula_data[data_id]
1950
1951 # Ignore <c:cat> elements for charts without category values.
1952 if not formula:
1953 return
1954
1955 self._xml_start_tag("c:cat")
1956
1957 # Check the type of cached data.
1958 cat_type = self._get_data_type(data)
1959
1960 if cat_type == "str":
1961 self.cat_has_num_fmt = False
1962 # Write the c:numRef element.
1963 self._write_str_ref(formula, data, cat_type)
1964
1965 elif cat_type == "multi_str":
1966 self.cat_has_num_fmt = False
1967 # Write the c:numRef element.
1968 self._write_multi_lvl_str_ref(formula, data)
1969
1970 else:
1971 self.cat_has_num_fmt = True
1972 # Write the c:numRef element.
1973 self._write_num_ref(formula, data, cat_type)
1974
1975 self._xml_end_tag("c:cat")
1976
1977 def _write_val(self, series) -> None:
1978 # Write the <c:val> element.
1979 formula = series["values"]
1980 data_id = series["val_data_id"]
1981 data = self.formula_data[data_id]
1982
1983 self._xml_start_tag("c:val")
1984
1985 # Unlike Cat axes data should only be numeric.
1986 # Write the c:numRef element.
1987 self._write_num_ref(formula, data, "num")
1988
1989 self._xml_end_tag("c:val")
1990
1991 def _write_num_ref(self, formula, data, ref_type) -> None:
1992 # Write the <c:numRef> element.
1993 self._xml_start_tag("c:numRef")
1994
1995 # Write the c:f element.
1996 self._write_series_formula(formula)
1997
1998 if ref_type == "num":
1999 # Write the c:numCache element.
2000 self._write_num_cache(data)
2001 elif ref_type == "str":
2002 # Write the c:strCache element.
2003 self._write_str_cache(data)
2004
2005 self._xml_end_tag("c:numRef")
2006
2007 def _write_str_ref(self, formula, data, ref_type) -> None:
2008 # Write the <c:strRef> element.
2009
2010 self._xml_start_tag("c:strRef")
2011
2012 # Write the c:f element.
2013 self._write_series_formula(formula)
2014
2015 if ref_type == "num":
2016 # Write the c:numCache element.
2017 self._write_num_cache(data)
2018 elif ref_type == "str":
2019 # Write the c:strCache element.
2020 self._write_str_cache(data)
2021
2022 self._xml_end_tag("c:strRef")
2023
2024 def _write_multi_lvl_str_ref(self, formula, data) -> None:
2025 # Write the <c:multiLvlStrRef> element.
2026
2027 if not data:
2028 return
2029
2030 self._xml_start_tag("c:multiLvlStrRef")
2031
2032 # Write the c:f element.
2033 self._write_series_formula(formula)
2034
2035 self._xml_start_tag("c:multiLvlStrCache")
2036
2037 # Write the c:ptCount element.
2038 count = len(data[-1])
2039 self._write_pt_count(count)
2040
2041 for cat_data in reversed(data):
2042 self._xml_start_tag("c:lvl")
2043
2044 for i, point in enumerate(cat_data):
2045 # Write the c:pt element.
2046 self._write_pt(i, point)
2047
2048 self._xml_end_tag("c:lvl")
2049
2050 self._xml_end_tag("c:multiLvlStrCache")
2051 self._xml_end_tag("c:multiLvlStrRef")
2052
2053 def _write_series_formula(self, formula) -> None:
2054 # Write the <c:f> element.
2055
2056 # Strip the leading '=' from the formula.
2057 if formula.startswith("="):
2058 formula = formula.lstrip("=")
2059
2060 self._xml_data_element("c:f", formula)
2061
2062 def _write_axis_ids(self, args) -> None:
2063 # Write the <c:axId> elements for the primary or secondary axes.
2064
2065 # Generate the axis ids.
2066 self._add_axis_ids(args)
2067
2068 if args["primary_axes"]:
2069 # Write the axis ids for the primary axes.
2070 self._write_axis_id(self.axis_ids[0])
2071 self._write_axis_id(self.axis_ids[1])
2072 else:
2073 # Write the axis ids for the secondary axes.
2074 self._write_axis_id(self.axis2_ids[0])
2075 self._write_axis_id(self.axis2_ids[1])
2076
2077 def _write_axis_id(self, val) -> None:
2078 # Write the <c:axId> element.
2079
2080 attributes = [("val", val)]
2081
2082 self._xml_empty_tag("c:axId", attributes)
2083
2084 def _write_cat_axis(self, args) -> None:
2085 # Write the <c:catAx> element. Usually the X axis.
2086 x_axis = args["x_axis"]
2087 y_axis = args["y_axis"]
2088 axis_ids = args["axis_ids"]
2089
2090 # If there are no axis_ids then we don't need to write this element.
2091 if axis_ids is None or not axis_ids:
2092 return
2093
2094 position = self.cat_axis_position
2095 is_horizontal = self.horiz_cat_axis
2096
2097 # Overwrite the default axis position with a user supplied value.
2098 if x_axis.get("position"):
2099 position = x_axis["position"]
2100
2101 self._xml_start_tag("c:catAx")
2102
2103 self._write_axis_id(axis_ids[0])
2104
2105 # Write the c:scaling element.
2106 self._write_scaling(x_axis.get("reverse"), None, None, None)
2107
2108 if not x_axis.get("visible"):
2109 self._write_delete(1)
2110
2111 # Write the c:axPos element.
2112 self._write_axis_pos(position, y_axis.get("reverse"))
2113
2114 # Write the c:majorGridlines element.
2115 self._write_major_gridlines(x_axis.get("major_gridlines"))
2116
2117 # Write the c:minorGridlines element.
2118 self._write_minor_gridlines(x_axis.get("minor_gridlines"))
2119
2120 # Write the axis title elements.
2121 self._write_title(x_axis["title"], is_horizontal)
2122
2123 # Write the c:numFmt element.
2124 self._write_cat_number_format(x_axis)
2125
2126 # Write the c:majorTickMark element.
2127 self._write_major_tick_mark(x_axis.get("major_tick_mark"))
2128
2129 # Write the c:minorTickMark element.
2130 self._write_minor_tick_mark(x_axis.get("minor_tick_mark"))
2131
2132 # Write the c:tickLblPos element.
2133 self._write_tick_label_pos(x_axis.get("label_position"))
2134
2135 # Write the c:spPr element for the axis line.
2136 self._write_sp_pr(x_axis)
2137
2138 # Write the axis font elements.
2139 self._write_axis_font(x_axis.get("num_font"))
2140
2141 # Write the c:crossAx element.
2142 self._write_cross_axis(axis_ids[1])
2143
2144 if self.show_crosses or x_axis.get("visible"):
2145 # Note, the category crossing comes from the value axis.
2146 if (
2147 y_axis.get("crossing") is None
2148 or y_axis.get("crossing") == "max"
2149 or y_axis["crossing"] == "min"
2150 ):
2151 # Write the c:crosses element.
2152 self._write_crosses(y_axis.get("crossing"))
2153 else:
2154 # Write the c:crossesAt element.
2155 self._write_c_crosses_at(y_axis.get("crossing"))
2156
2157 # Write the c:auto element.
2158 if not x_axis.get("text_axis"):
2159 self._write_auto(1)
2160
2161 # Write the c:labelAlign element.
2162 self._write_label_align(x_axis.get("label_align"))
2163
2164 # Write the c:labelOffset element.
2165 self._write_label_offset(100)
2166
2167 # Write the c:tickLblSkip element.
2168 self._write_c_tick_lbl_skip(x_axis.get("interval_unit"))
2169
2170 # Write the c:tickMarkSkip element.
2171 self._write_c_tick_mark_skip(x_axis.get("interval_tick"))
2172
2173 self._xml_end_tag("c:catAx")
2174
2175 def _write_val_axis(self, args) -> None:
2176 # Write the <c:valAx> element. Usually the Y axis.
2177 x_axis = args["x_axis"]
2178 y_axis = args["y_axis"]
2179 axis_ids = args["axis_ids"]
2180 position = args.get("position", self.val_axis_position)
2181 is_horizontal = self.horiz_val_axis
2182
2183 # If there are no axis_ids then we don't need to write this element.
2184 if axis_ids is None or not axis_ids:
2185 return
2186
2187 # Overwrite the default axis position with a user supplied value.
2188 position = y_axis.get("position") or position
2189
2190 self._xml_start_tag("c:valAx")
2191
2192 self._write_axis_id(axis_ids[1])
2193
2194 # Write the c:scaling element.
2195 self._write_scaling(
2196 y_axis.get("reverse"),
2197 y_axis.get("min"),
2198 y_axis.get("max"),
2199 y_axis.get("log_base"),
2200 )
2201
2202 if not y_axis.get("visible"):
2203 self._write_delete(1)
2204
2205 # Write the c:axPos element.
2206 self._write_axis_pos(position, x_axis.get("reverse"))
2207
2208 # Write the c:majorGridlines element.
2209 self._write_major_gridlines(y_axis.get("major_gridlines"))
2210
2211 # Write the c:minorGridlines element.
2212 self._write_minor_gridlines(y_axis.get("minor_gridlines"))
2213
2214 # Write the axis title elements.
2215 self._write_title(y_axis["title"], is_horizontal)
2216
2217 # Write the c:numberFormat element.
2218 self._write_number_format(y_axis)
2219
2220 # Write the c:majorTickMark element.
2221 self._write_major_tick_mark(y_axis.get("major_tick_mark"))
2222
2223 # Write the c:minorTickMark element.
2224 self._write_minor_tick_mark(y_axis.get("minor_tick_mark"))
2225
2226 # Write the c:tickLblPos element.
2227 self._write_tick_label_pos(y_axis.get("label_position"))
2228
2229 # Write the c:spPr element for the axis line.
2230 self._write_sp_pr(y_axis)
2231
2232 # Write the axis font elements.
2233 self._write_axis_font(y_axis.get("num_font"))
2234
2235 # Write the c:crossAx element.
2236 self._write_cross_axis(axis_ids[0])
2237
2238 # Note, the category crossing comes from the value axis.
2239 if (
2240 x_axis.get("crossing") is None
2241 or x_axis["crossing"] == "max"
2242 or x_axis["crossing"] == "min"
2243 ):
2244 # Write the c:crosses element.
2245 self._write_crosses(x_axis.get("crossing"))
2246 else:
2247 # Write the c:crossesAt element.
2248 self._write_c_crosses_at(x_axis.get("crossing"))
2249
2250 # Write the c:crossBetween element.
2251 self._write_cross_between(x_axis.get("position_axis"))
2252
2253 # Write the c:majorUnit element.
2254 self._write_c_major_unit(y_axis.get("major_unit"))
2255
2256 # Write the c:minorUnit element.
2257 self._write_c_minor_unit(y_axis.get("minor_unit"))
2258
2259 # Write the c:dispUnits element.
2260 self._write_disp_units(
2261 y_axis.get("display_units"), y_axis.get("display_units_visible")
2262 )
2263
2264 self._xml_end_tag("c:valAx")
2265
2266 def _write_cat_val_axis(self, args) -> None:
2267 # Write the <c:valAx> element. This is for the second valAx
2268 # in scatter plots. Usually the X axis.
2269 x_axis = args["x_axis"]
2270 y_axis = args["y_axis"]
2271 axis_ids = args["axis_ids"]
2272 position = args["position"] or self.val_axis_position
2273 is_horizontal = self.horiz_val_axis
2274
2275 # If there are no axis_ids then we don't need to write this element.
2276 if axis_ids is None or not axis_ids:
2277 return
2278
2279 # Overwrite the default axis position with a user supplied value.
2280 position = x_axis.get("position") or position
2281
2282 self._xml_start_tag("c:valAx")
2283
2284 self._write_axis_id(axis_ids[0])
2285
2286 # Write the c:scaling element.
2287 self._write_scaling(
2288 x_axis.get("reverse"),
2289 x_axis.get("min"),
2290 x_axis.get("max"),
2291 x_axis.get("log_base"),
2292 )
2293
2294 if not x_axis.get("visible"):
2295 self._write_delete(1)
2296
2297 # Write the c:axPos element.
2298 self._write_axis_pos(position, y_axis.get("reverse"))
2299
2300 # Write the c:majorGridlines element.
2301 self._write_major_gridlines(x_axis.get("major_gridlines"))
2302
2303 # Write the c:minorGridlines element.
2304 self._write_minor_gridlines(x_axis.get("minor_gridlines"))
2305
2306 # Write the axis title elements.
2307 self._write_title(x_axis["title"], is_horizontal)
2308
2309 # Write the c:numberFormat element.
2310 self._write_number_format(x_axis)
2311
2312 # Write the c:majorTickMark element.
2313 self._write_major_tick_mark(x_axis.get("major_tick_mark"))
2314
2315 # Write the c:minorTickMark element.
2316 self._write_minor_tick_mark(x_axis.get("minor_tick_mark"))
2317
2318 # Write the c:tickLblPos element.
2319 self._write_tick_label_pos(x_axis.get("label_position"))
2320
2321 # Write the c:spPr element for the axis line.
2322 self._write_sp_pr(x_axis)
2323
2324 # Write the axis font elements.
2325 self._write_axis_font(x_axis.get("num_font"))
2326
2327 # Write the c:crossAx element.
2328 self._write_cross_axis(axis_ids[1])
2329
2330 # Note, the category crossing comes from the value axis.
2331 if (
2332 y_axis.get("crossing") is None
2333 or y_axis["crossing"] == "max"
2334 or y_axis["crossing"] == "min"
2335 ):
2336 # Write the c:crosses element.
2337 self._write_crosses(y_axis.get("crossing"))
2338 else:
2339 # Write the c:crossesAt element.
2340 self._write_c_crosses_at(y_axis.get("crossing"))
2341
2342 # Write the c:crossBetween element.
2343 self._write_cross_between(y_axis.get("position_axis"))
2344
2345 # Write the c:majorUnit element.
2346 self._write_c_major_unit(x_axis.get("major_unit"))
2347
2348 # Write the c:minorUnit element.
2349 self._write_c_minor_unit(x_axis.get("minor_unit"))
2350
2351 # Write the c:dispUnits element.
2352 self._write_disp_units(
2353 x_axis.get("display_units"), x_axis.get("display_units_visible")
2354 )
2355
2356 self._xml_end_tag("c:valAx")
2357
2358 def _write_date_axis(self, args) -> None:
2359 # Write the <c:dateAx> element. Usually the X axis.
2360 x_axis = args["x_axis"]
2361 y_axis = args["y_axis"]
2362 axis_ids = args["axis_ids"]
2363
2364 # If there are no axis_ids then we don't need to write this element.
2365 if axis_ids is None or not axis_ids:
2366 return
2367
2368 position = self.cat_axis_position
2369
2370 # Overwrite the default axis position with a user supplied value.
2371 position = x_axis.get("position") or position
2372
2373 self._xml_start_tag("c:dateAx")
2374
2375 self._write_axis_id(axis_ids[0])
2376
2377 # Write the c:scaling element.
2378 self._write_scaling(
2379 x_axis.get("reverse"),
2380 x_axis.get("min"),
2381 x_axis.get("max"),
2382 x_axis.get("log_base"),
2383 )
2384
2385 if not x_axis.get("visible"):
2386 self._write_delete(1)
2387
2388 # Write the c:axPos element.
2389 self._write_axis_pos(position, y_axis.get("reverse"))
2390
2391 # Write the c:majorGridlines element.
2392 self._write_major_gridlines(x_axis.get("major_gridlines"))
2393
2394 # Write the c:minorGridlines element.
2395 self._write_minor_gridlines(x_axis.get("minor_gridlines"))
2396
2397 # Write the axis title elements.
2398 self._write_title(x_axis["title"])
2399
2400 # Write the c:numFmt element.
2401 self._write_number_format(x_axis)
2402
2403 # Write the c:majorTickMark element.
2404 self._write_major_tick_mark(x_axis.get("major_tick_mark"))
2405
2406 # Write the c:minorTickMark element.
2407 self._write_minor_tick_mark(x_axis.get("minor_tick_mark"))
2408
2409 # Write the c:tickLblPos element.
2410 self._write_tick_label_pos(x_axis.get("label_position"))
2411
2412 # Write the c:spPr element for the axis line.
2413 self._write_sp_pr(x_axis)
2414
2415 # Write the axis font elements.
2416 self._write_axis_font(x_axis.get("num_font"))
2417
2418 # Write the c:crossAx element.
2419 self._write_cross_axis(axis_ids[1])
2420
2421 if self.show_crosses or x_axis.get("visible"):
2422 # Note, the category crossing comes from the value axis.
2423 if (
2424 y_axis.get("crossing") is None
2425 or y_axis.get("crossing") == "max"
2426 or y_axis["crossing"] == "min"
2427 ):
2428 # Write the c:crosses element.
2429 self._write_crosses(y_axis.get("crossing"))
2430 else:
2431 # Write the c:crossesAt element.
2432 self._write_c_crosses_at(y_axis.get("crossing"))
2433
2434 # Write the c:auto element.
2435 self._write_auto(1)
2436
2437 # Write the c:labelOffset element.
2438 self._write_label_offset(100)
2439
2440 # Write the c:tickLblSkip element.
2441 self._write_c_tick_lbl_skip(x_axis.get("interval_unit"))
2442
2443 # Write the c:tickMarkSkip element.
2444 self._write_c_tick_mark_skip(x_axis.get("interval_tick"))
2445
2446 # Write the c:majorUnit element.
2447 self._write_c_major_unit(x_axis.get("major_unit"))
2448
2449 # Write the c:majorTimeUnit element.
2450 if x_axis.get("major_unit"):
2451 self._write_c_major_time_unit(x_axis["major_unit_type"])
2452
2453 # Write the c:minorUnit element.
2454 self._write_c_minor_unit(x_axis.get("minor_unit"))
2455
2456 # Write the c:minorTimeUnit element.
2457 if x_axis.get("minor_unit"):
2458 self._write_c_minor_time_unit(x_axis["minor_unit_type"])
2459
2460 self._xml_end_tag("c:dateAx")
2461
2462 def _write_scaling(self, reverse, min_val, max_val, log_base) -> None:
2463 # Write the <c:scaling> element.
2464
2465 self._xml_start_tag("c:scaling")
2466
2467 # Write the c:logBase element.
2468 self._write_c_log_base(log_base)
2469
2470 # Write the c:orientation element.
2471 self._write_orientation(reverse)
2472
2473 # Write the c:max element.
2474 self._write_c_max(max_val)
2475
2476 # Write the c:min element.
2477 self._write_c_min(min_val)
2478
2479 self._xml_end_tag("c:scaling")
2480
2481 def _write_c_log_base(self, val) -> None:
2482 # Write the <c:logBase> element.
2483
2484 if not val:
2485 return
2486
2487 attributes = [("val", val)]
2488
2489 self._xml_empty_tag("c:logBase", attributes)
2490
2491 def _write_orientation(self, reverse) -> None:
2492 # Write the <c:orientation> element.
2493 val = "minMax"
2494
2495 if reverse:
2496 val = "maxMin"
2497
2498 attributes = [("val", val)]
2499
2500 self._xml_empty_tag("c:orientation", attributes)
2501
2502 def _write_c_max(self, max_val) -> None:
2503 # Write the <c:max> element.
2504
2505 if max_val is None:
2506 return
2507
2508 attributes = [("val", max_val)]
2509
2510 self._xml_empty_tag("c:max", attributes)
2511
2512 def _write_c_min(self, min_val) -> None:
2513 # Write the <c:min> element.
2514
2515 if min_val is None:
2516 return
2517
2518 attributes = [("val", min_val)]
2519
2520 self._xml_empty_tag("c:min", attributes)
2521
2522 def _write_axis_pos(self, val, reverse) -> None:
2523 # Write the <c:axPos> element.
2524
2525 if reverse:
2526 if val == "l":
2527 val = "r"
2528 if val == "b":
2529 val = "t"
2530
2531 attributes = [("val", val)]
2532
2533 self._xml_empty_tag("c:axPos", attributes)
2534
2535 def _write_number_format(self, axis) -> None:
2536 # Write the <c:numberFormat> element. Note: It is assumed that if
2537 # a user defined number format is supplied (i.e., non-default) then
2538 # the sourceLinked attribute is 0.
2539 # The user can override this if required.
2540 format_code = axis.get("num_format")
2541 source_linked = 1
2542
2543 # Check if a user defined number format has been set.
2544 if format_code is not None and format_code != axis["defaults"]["num_format"]:
2545 source_linked = 0
2546
2547 # User override of sourceLinked.
2548 if axis.get("num_format_linked"):
2549 source_linked = 1
2550
2551 attributes = [
2552 ("formatCode", format_code),
2553 ("sourceLinked", source_linked),
2554 ]
2555
2556 self._xml_empty_tag("c:numFmt", attributes)
2557
2558 def _write_cat_number_format(self, axis) -> None:
2559 # Write the <c:numFmt> element. Special case handler for category
2560 # axes which don't always have a number format.
2561 format_code = axis.get("num_format")
2562 source_linked = 1
2563 default_format = 1
2564
2565 # Check if a user defined number format has been set.
2566 if format_code is not None and format_code != axis["defaults"]["num_format"]:
2567 source_linked = 0
2568 default_format = 0
2569
2570 # User override of sourceLinked.
2571 if axis.get("num_format_linked"):
2572 source_linked = 1
2573
2574 # Skip if cat doesn't have a num format (unless it is non-default).
2575 if not self.cat_has_num_fmt and default_format:
2576 return
2577
2578 attributes = [
2579 ("formatCode", format_code),
2580 ("sourceLinked", source_linked),
2581 ]
2582
2583 self._xml_empty_tag("c:numFmt", attributes)
2584
2585 def _write_data_label_number_format(self, format_code) -> None:
2586 # Write the <c:numberFormat> element for data labels.
2587 source_linked = 0
2588
2589 attributes = [
2590 ("formatCode", format_code),
2591 ("sourceLinked", source_linked),
2592 ]
2593
2594 self._xml_empty_tag("c:numFmt", attributes)
2595
2596 def _write_major_tick_mark(self, val) -> None:
2597 # Write the <c:majorTickMark> element.
2598
2599 if not val:
2600 return
2601
2602 attributes = [("val", val)]
2603
2604 self._xml_empty_tag("c:majorTickMark", attributes)
2605
2606 def _write_minor_tick_mark(self, val) -> None:
2607 # Write the <c:minorTickMark> element.
2608
2609 if not val:
2610 return
2611
2612 attributes = [("val", val)]
2613
2614 self._xml_empty_tag("c:minorTickMark", attributes)
2615
2616 def _write_tick_label_pos(self, val=None) -> None:
2617 # Write the <c:tickLblPos> element.
2618 if val is None or val == "next_to":
2619 val = "nextTo"
2620
2621 attributes = [("val", val)]
2622
2623 self._xml_empty_tag("c:tickLblPos", attributes)
2624
2625 def _write_cross_axis(self, val) -> None:
2626 # Write the <c:crossAx> element.
2627
2628 attributes = [("val", val)]
2629
2630 self._xml_empty_tag("c:crossAx", attributes)
2631
2632 def _write_crosses(self, val=None) -> None:
2633 # Write the <c:crosses> element.
2634 if val is None:
2635 val = "autoZero"
2636
2637 attributes = [("val", val)]
2638
2639 self._xml_empty_tag("c:crosses", attributes)
2640
2641 def _write_c_crosses_at(self, val) -> None:
2642 # Write the <c:crossesAt> element.
2643
2644 attributes = [("val", val)]
2645
2646 self._xml_empty_tag("c:crossesAt", attributes)
2647
2648 def _write_auto(self, val) -> None:
2649 # Write the <c:auto> element.
2650
2651 attributes = [("val", val)]
2652
2653 self._xml_empty_tag("c:auto", attributes)
2654
2655 def _write_label_align(self, val=None) -> None:
2656 # Write the <c:labelAlign> element.
2657
2658 if val is None:
2659 val = "ctr"
2660
2661 if val == "right":
2662 val = "r"
2663
2664 if val == "left":
2665 val = "l"
2666
2667 attributes = [("val", val)]
2668
2669 self._xml_empty_tag("c:lblAlgn", attributes)
2670
2671 def _write_label_offset(self, val) -> None:
2672 # Write the <c:labelOffset> element.
2673
2674 attributes = [("val", val)]
2675
2676 self._xml_empty_tag("c:lblOffset", attributes)
2677
2678 def _write_c_tick_lbl_skip(self, val) -> None:
2679 # Write the <c:tickLblSkip> element.
2680 if val is None:
2681 return
2682
2683 attributes = [("val", val)]
2684
2685 self._xml_empty_tag("c:tickLblSkip", attributes)
2686
2687 def _write_c_tick_mark_skip(self, val) -> None:
2688 # Write the <c:tickMarkSkip> element.
2689 if val is None:
2690 return
2691
2692 attributes = [("val", val)]
2693
2694 self._xml_empty_tag("c:tickMarkSkip", attributes)
2695
2696 def _write_major_gridlines(self, gridlines) -> None:
2697 # Write the <c:majorGridlines> element.
2698
2699 if not gridlines:
2700 return
2701
2702 if not gridlines["visible"]:
2703 return
2704
2705 if gridlines["line"]["defined"]:
2706 self._xml_start_tag("c:majorGridlines")
2707
2708 # Write the c:spPr element.
2709 self._write_sp_pr(gridlines)
2710
2711 self._xml_end_tag("c:majorGridlines")
2712 else:
2713 self._xml_empty_tag("c:majorGridlines")
2714
2715 def _write_minor_gridlines(self, gridlines) -> None:
2716 # Write the <c:minorGridlines> element.
2717
2718 if not gridlines:
2719 return
2720
2721 if not gridlines["visible"]:
2722 return
2723
2724 if gridlines["line"]["defined"]:
2725 self._xml_start_tag("c:minorGridlines")
2726
2727 # Write the c:spPr element.
2728 self._write_sp_pr(gridlines)
2729
2730 self._xml_end_tag("c:minorGridlines")
2731 else:
2732 self._xml_empty_tag("c:minorGridlines")
2733
2734 def _write_cross_between(self, val) -> None:
2735 # Write the <c:crossBetween> element.
2736 if val is None:
2737 val = self.cross_between
2738
2739 attributes = [("val", val)]
2740
2741 self._xml_empty_tag("c:crossBetween", attributes)
2742
2743 def _write_c_major_unit(self, val) -> None:
2744 # Write the <c:majorUnit> element.
2745
2746 if not val:
2747 return
2748
2749 attributes = [("val", val)]
2750
2751 self._xml_empty_tag("c:majorUnit", attributes)
2752
2753 def _write_c_minor_unit(self, val) -> None:
2754 # Write the <c:minorUnit> element.
2755
2756 if not val:
2757 return
2758
2759 attributes = [("val", val)]
2760
2761 self._xml_empty_tag("c:minorUnit", attributes)
2762
2763 def _write_c_major_time_unit(self, val=None) -> None:
2764 # Write the <c:majorTimeUnit> element.
2765 if val is None:
2766 val = "days"
2767
2768 attributes = [("val", val)]
2769
2770 self._xml_empty_tag("c:majorTimeUnit", attributes)
2771
2772 def _write_c_minor_time_unit(self, val=None) -> None:
2773 # Write the <c:minorTimeUnit> element.
2774 if val is None:
2775 val = "days"
2776
2777 attributes = [("val", val)]
2778
2779 self._xml_empty_tag("c:minorTimeUnit", attributes)
2780
2781 def _write_legend(self) -> None:
2782 # Write the <c:legend> element.
2783 legend = self.legend
2784 position = legend.get("position", "right")
2785 font = legend.get("font")
2786 delete_series = []
2787 overlay = 0
2788
2789 if legend.get("delete_series") and isinstance(legend["delete_series"], list):
2790 delete_series = legend["delete_series"]
2791
2792 if position.startswith("overlay_"):
2793 position = position.replace("overlay_", "")
2794 overlay = 1
2795
2796 allowed = {
2797 "right": "r",
2798 "left": "l",
2799 "top": "t",
2800 "bottom": "b",
2801 "top_right": "tr",
2802 }
2803
2804 if position == "none":
2805 return
2806
2807 if position not in allowed:
2808 return
2809
2810 position = allowed[position]
2811
2812 self._xml_start_tag("c:legend")
2813
2814 # Write the c:legendPos element.
2815 self._write_legend_pos(position)
2816
2817 # Remove series labels from the legend.
2818 for index in delete_series:
2819 # Write the c:legendEntry element.
2820 self._write_legend_entry(index)
2821
2822 # Write the c:layout element.
2823 self._write_layout(legend.get("layout"), "legend")
2824
2825 # Write the c:overlay element.
2826 if overlay:
2827 self._write_overlay()
2828
2829 if font:
2830 self._write_tx_pr(font)
2831
2832 # Write the c:spPr element.
2833 self._write_sp_pr(legend)
2834
2835 self._xml_end_tag("c:legend")
2836
2837 def _write_legend_pos(self, val) -> None:
2838 # Write the <c:legendPos> element.
2839
2840 attributes = [("val", val)]
2841
2842 self._xml_empty_tag("c:legendPos", attributes)
2843
2844 def _write_legend_entry(self, index) -> None:
2845 # Write the <c:legendEntry> element.
2846
2847 self._xml_start_tag("c:legendEntry")
2848
2849 # Write the c:idx element.
2850 self._write_idx(index)
2851
2852 # Write the c:delete element.
2853 self._write_delete(1)
2854
2855 self._xml_end_tag("c:legendEntry")
2856
2857 def _write_overlay(self) -> None:
2858 # Write the <c:overlay> element.
2859 val = 1
2860
2861 attributes = [("val", val)]
2862
2863 self._xml_empty_tag("c:overlay", attributes)
2864
2865 def _write_plot_vis_only(self) -> None:
2866 # Write the <c:plotVisOnly> element.
2867 val = 1
2868
2869 # Ignore this element if we are plotting hidden data.
2870 if self.show_hidden:
2871 return
2872
2873 attributes = [("val", val)]
2874
2875 self._xml_empty_tag("c:plotVisOnly", attributes)
2876
2877 def _write_print_settings(self) -> None:
2878 # Write the <c:printSettings> element.
2879 self._xml_start_tag("c:printSettings")
2880
2881 # Write the c:headerFooter element.
2882 self._write_header_footer()
2883
2884 # Write the c:pageMargins element.
2885 self._write_page_margins()
2886
2887 # Write the c:pageSetup element.
2888 self._write_page_setup()
2889
2890 self._xml_end_tag("c:printSettings")
2891
2892 def _write_header_footer(self) -> None:
2893 # Write the <c:headerFooter> element.
2894 self._xml_empty_tag("c:headerFooter")
2895
2896 def _write_page_margins(self) -> None:
2897 # Write the <c:pageMargins> element.
2898 bottom = 0.75
2899 left = 0.7
2900 right = 0.7
2901 top = 0.75
2902 header = 0.3
2903 footer = 0.3
2904
2905 attributes = [
2906 ("b", bottom),
2907 ("l", left),
2908 ("r", right),
2909 ("t", top),
2910 ("header", header),
2911 ("footer", footer),
2912 ]
2913
2914 self._xml_empty_tag("c:pageMargins", attributes)
2915
2916 def _write_page_setup(self) -> None:
2917 # Write the <c:pageSetup> element.
2918 self._xml_empty_tag("c:pageSetup")
2919
2920 def _write_c_auto_title_deleted(self) -> None:
2921 # Write the <c:autoTitleDeleted> element.
2922 self._xml_empty_tag("c:autoTitleDeleted", [("val", 1)])
2923
2924 def _write_title(self, title: ChartTitle, is_horizontal: bool = False) -> None:
2925 # Write the <c:title> element for different title types.
2926 if title.has_name():
2927 self._write_title_rich(title, is_horizontal)
2928 elif title.has_formula():
2929 self._write_title_formula(title, is_horizontal)
2930 elif title.has_formatting():
2931 self._write_title_format_only(title)
2932
2933 def _write_title_rich(self, title: ChartTitle, is_horizontal: bool = False) -> None:
2934 # Write the <c:title> element for a rich string.
2935 self._xml_start_tag("c:title")
2936
2937 # Write the c:tx element.
2938 self._write_tx_rich(title.name, is_horizontal, title.font)
2939
2940 # Write the c:layout element.
2941 self._write_layout(title.layout, "text")
2942
2943 # Write the c:overlay element.
2944 if title.overlay:
2945 self._write_overlay()
2946
2947 # Write the c:spPr element.
2948 self._write_sp_pr(title.get_formatting())
2949
2950 self._xml_end_tag("c:title")
2951
2952 def _write_title_formula(
2953 self, title: ChartTitle, is_horizontal: bool = False
2954 ) -> None:
2955 # Write the <c:title> element for a rich string.
2956 self._xml_start_tag("c:title")
2957
2958 # Write the c:tx element.
2959 self._write_tx_formula(title.formula, title.data_id)
2960
2961 # Write the c:layout element.
2962 self._write_layout(title.layout, "text")
2963
2964 # Write the c:overlay element.
2965 if title.overlay:
2966 self._write_overlay()
2967
2968 # Write the c:spPr element.
2969 self._write_sp_pr(title.get_formatting())
2970
2971 # Write the c:txPr element.
2972 self._write_tx_pr(title.font, is_horizontal)
2973
2974 self._xml_end_tag("c:title")
2975
2976 def _write_title_format_only(self, title: ChartTitle) -> None:
2977 # Write the <c:title> element title with formatting and default name.
2978 self._xml_start_tag("c:title")
2979
2980 # Write the c:layout element.
2981 self._write_layout(title.layout, "text")
2982
2983 # Write the c:overlay element.
2984 if title.overlay:
2985 self._write_overlay()
2986
2987 # Write the c:spPr element.
2988 self._write_sp_pr(title.get_formatting())
2989
2990 self._xml_end_tag("c:title")
2991
2992 def _write_tx_rich(self, title, is_horizontal, font) -> None:
2993 # Write the <c:tx> element.
2994
2995 self._xml_start_tag("c:tx")
2996
2997 # Write the c:rich element.
2998 self._write_rich(title, font, is_horizontal, ignore_rich_pr=False)
2999
3000 self._xml_end_tag("c:tx")
3001
3002 def _write_tx_value(self, title) -> None:
3003 # Write the <c:tx> element with a value such as for series names.
3004
3005 self._xml_start_tag("c:tx")
3006
3007 # Write the c:v element.
3008 self._write_v(title)
3009
3010 self._xml_end_tag("c:tx")
3011
3012 def _write_tx_formula(self, title, data_id) -> None:
3013 # Write the <c:tx> element.
3014 data = None
3015
3016 if data_id is not None:
3017 data = self.formula_data[data_id]
3018
3019 self._xml_start_tag("c:tx")
3020
3021 # Write the c:strRef element.
3022 self._write_str_ref(title, data, "str")
3023
3024 self._xml_end_tag("c:tx")
3025
3026 def _write_rich(self, title, font, is_horizontal, ignore_rich_pr) -> None:
3027 # Write the <c:rich> element.
3028
3029 if font and font.get("rotation") is not None:
3030 rotation = font["rotation"]
3031 else:
3032 rotation = None
3033
3034 self._xml_start_tag("c:rich")
3035
3036 # Write the a:bodyPr element.
3037 self._write_a_body_pr(rotation, is_horizontal)
3038
3039 # Write the a:lstStyle element.
3040 self._write_a_lst_style()
3041
3042 # Write the a:p element.
3043 self._write_a_p_rich(title, font, ignore_rich_pr)
3044
3045 self._xml_end_tag("c:rich")
3046
3047 def _write_a_body_pr(self, rotation, is_horizontal) -> None:
3048 # Write the <a:bodyPr> element.
3049 attributes = []
3050
3051 if rotation is None and is_horizontal:
3052 rotation = -5400000
3053
3054 if rotation is not None:
3055 if rotation == 16200000:
3056 # 270 deg/stacked angle.
3057 attributes.append(("rot", 0))
3058 attributes.append(("vert", "wordArtVert"))
3059 elif rotation == 16260000:
3060 # 271 deg/East Asian vertical.
3061 attributes.append(("rot", 0))
3062 attributes.append(("vert", "eaVert"))
3063 else:
3064 attributes.append(("rot", rotation))
3065 attributes.append(("vert", "horz"))
3066
3067 self._xml_empty_tag("a:bodyPr", attributes)
3068
3069 def _write_a_lst_style(self) -> None:
3070 # Write the <a:lstStyle> element.
3071 self._xml_empty_tag("a:lstStyle")
3072
3073 def _write_a_p_rich(self, title, font, ignore_rich_pr) -> None:
3074 # Write the <a:p> element for rich string titles.
3075
3076 self._xml_start_tag("a:p")
3077
3078 # Write the a:pPr element.
3079 if not ignore_rich_pr:
3080 self._write_a_p_pr_rich(font)
3081
3082 # Write the a:r element.
3083 self._write_a_r(title, font)
3084
3085 self._xml_end_tag("a:p")
3086
3087 def _write_a_p_formula(self, font) -> None:
3088 # Write the <a:p> element for formula titles.
3089
3090 self._xml_start_tag("a:p")
3091
3092 # Write the a:pPr element.
3093 self._write_a_p_pr_rich(font)
3094
3095 # Write the a:endParaRPr element.
3096 self._write_a_end_para_rpr()
3097
3098 self._xml_end_tag("a:p")
3099
3100 def _write_a_p_pr_rich(self, font) -> None:
3101 # Write the <a:pPr> element for rich string titles.
3102
3103 self._xml_start_tag("a:pPr")
3104
3105 # Write the a:defRPr element.
3106 self._write_a_def_rpr(font)
3107
3108 self._xml_end_tag("a:pPr")
3109
3110 def _write_a_def_rpr(self, font) -> None:
3111 # Write the <a:defRPr> element.
3112 has_color = False
3113
3114 style_attributes = Shape._get_font_style_attributes(font)
3115 latin_attributes = Shape._get_font_latin_attributes(font)
3116
3117 if font and font.get("color"):
3118 has_color = True
3119
3120 if latin_attributes or has_color:
3121 self._xml_start_tag("a:defRPr", style_attributes)
3122
3123 if has_color:
3124 self._write_a_solid_fill({"color": font["color"]})
3125
3126 if latin_attributes:
3127 self._write_a_latin(latin_attributes)
3128
3129 self._xml_end_tag("a:defRPr")
3130 else:
3131 self._xml_empty_tag("a:defRPr", style_attributes)
3132
3133 def _write_a_end_para_rpr(self) -> None:
3134 # Write the <a:endParaRPr> element.
3135 lang = "en-US"
3136
3137 attributes = [("lang", lang)]
3138
3139 self._xml_empty_tag("a:endParaRPr", attributes)
3140
3141 def _write_a_r(self, title, font) -> None:
3142 # Write the <a:r> element.
3143
3144 self._xml_start_tag("a:r")
3145
3146 # Write the a:rPr element.
3147 self._write_a_r_pr(font)
3148
3149 # Write the a:t element.
3150 self._write_a_t(title)
3151
3152 self._xml_end_tag("a:r")
3153
3154 def _write_a_r_pr(self, font) -> None:
3155 # Write the <a:rPr> element.
3156 has_color = False
3157 lang = "en-US"
3158
3159 style_attributes = Shape._get_font_style_attributes(font)
3160 latin_attributes = Shape._get_font_latin_attributes(font)
3161
3162 if font and font["color"]:
3163 has_color = True
3164
3165 # Add the lang type to the attributes.
3166 style_attributes.insert(0, ("lang", lang))
3167
3168 if latin_attributes or has_color:
3169 self._xml_start_tag("a:rPr", style_attributes)
3170
3171 if has_color:
3172 self._write_a_solid_fill({"color": font["color"]})
3173
3174 if latin_attributes:
3175 self._write_a_latin(latin_attributes)
3176
3177 self._xml_end_tag("a:rPr")
3178 else:
3179 self._xml_empty_tag("a:rPr", style_attributes)
3180
3181 def _write_a_t(self, title) -> None:
3182 # Write the <a:t> element.
3183
3184 self._xml_data_element("a:t", title)
3185
3186 def _write_tx_pr(self, font, is_horizontal=False) -> None:
3187 # Write the <c:txPr> element.
3188
3189 if font and font.get("rotation") is not None:
3190 rotation = font["rotation"]
3191 else:
3192 rotation = None
3193
3194 self._xml_start_tag("c:txPr")
3195
3196 # Write the a:bodyPr element.
3197 self._write_a_body_pr(rotation, is_horizontal)
3198
3199 # Write the a:lstStyle element.
3200 self._write_a_lst_style()
3201
3202 # Write the a:p element.
3203 self._write_a_p_formula(font)
3204
3205 self._xml_end_tag("c:txPr")
3206
3207 def _write_marker(self, marker) -> None:
3208 # Write the <c:marker> element.
3209 if marker is None:
3210 marker = self.default_marker
3211
3212 if not marker:
3213 return
3214
3215 if marker["type"] == "automatic":
3216 return
3217
3218 self._xml_start_tag("c:marker")
3219
3220 # Write the c:symbol element.
3221 self._write_symbol(marker["type"])
3222
3223 # Write the c:size element.
3224 if marker.get("size"):
3225 self._write_marker_size(marker["size"])
3226
3227 # Write the c:spPr element.
3228 self._write_sp_pr(marker)
3229
3230 self._xml_end_tag("c:marker")
3231
3232 def _write_marker_size(self, val) -> None:
3233 # Write the <c:size> element.
3234
3235 attributes = [("val", val)]
3236
3237 self._xml_empty_tag("c:size", attributes)
3238
3239 def _write_symbol(self, val) -> None:
3240 # Write the <c:symbol> element.
3241
3242 attributes = [("val", val)]
3243
3244 self._xml_empty_tag("c:symbol", attributes)
3245
3246 def _write_sp_pr(self, chart_format: dict) -> None:
3247 # Write the <c:spPr> element.
3248 if not self._has_formatting(chart_format):
3249 return
3250
3251 self._xml_start_tag("c:spPr")
3252
3253 # Write the fill elements for solid charts such as pie and bar.
3254 if chart_format.get("fill") and chart_format["fill"]["defined"]:
3255 if "none" in chart_format["fill"]:
3256 # Write the a:noFill element.
3257 self._write_a_no_fill()
3258 else:
3259 # Write the a:solidFill element.
3260 self._write_a_solid_fill(chart_format["fill"])
3261
3262 if chart_format.get("pattern"):
3263 # Write the a:gradFill element.
3264 self._write_a_patt_fill(chart_format["pattern"])
3265
3266 if chart_format.get("gradient"):
3267 # Write the a:gradFill element.
3268 self._write_a_grad_fill(chart_format["gradient"])
3269
3270 # Write the a:ln element.
3271 if chart_format.get("line") and chart_format["line"]["defined"]:
3272 self._write_a_ln(chart_format["line"])
3273
3274 self._xml_end_tag("c:spPr")
3275
3276 def _write_a_ln(self, line) -> None:
3277 # Write the <a:ln> element.
3278 attributes = []
3279
3280 # Add the line width as an attribute.
3281 width = line.get("width")
3282
3283 if width is not None:
3284 # Round width to nearest 0.25, like Excel.
3285 width = int((width + 0.125) * 4) / 4.0
3286
3287 # Convert to internal units.
3288 width = int(0.5 + (12700 * width))
3289
3290 attributes = [("w", width)]
3291
3292 if line.get("none") or line.get("color") or line.get("dash_type"):
3293 self._xml_start_tag("a:ln", attributes)
3294
3295 # Write the line fill.
3296 if "none" in line:
3297 # Write the a:noFill element.
3298 self._write_a_no_fill()
3299 elif "color" in line:
3300 # Write the a:solidFill element.
3301 self._write_a_solid_fill(line)
3302
3303 # Write the line/dash type.
3304 line_type = line.get("dash_type")
3305 if line_type:
3306 # Write the a:prstDash element.
3307 self._write_a_prst_dash(line_type)
3308
3309 self._xml_end_tag("a:ln")
3310 else:
3311 self._xml_empty_tag("a:ln", attributes)
3312
3313 def _write_a_no_fill(self) -> None:
3314 # Write the <a:noFill> element.
3315 self._xml_empty_tag("a:noFill")
3316
3317 def _write_a_solid_fill(self, fill) -> None:
3318 # Write the <a:solidFill> element.
3319
3320 self._xml_start_tag("a:solidFill")
3321
3322 if fill.get("color"):
3323 self._write_color(fill["color"], fill.get("transparency"))
3324
3325 self._xml_end_tag("a:solidFill")
3326
3327 def _write_color(self, color: Color, transparency=None) -> None:
3328 # Write the appropriate chart color element.
3329
3330 if not color:
3331 return
3332
3333 if color._is_automatic:
3334 # Write the a:sysClr element.
3335 self._write_a_sys_clr()
3336 elif color._type == ColorTypes.RGB:
3337 # Write the a:srgbClr element.
3338 self._write_a_srgb_clr(color, transparency)
3339 elif color._type == ColorTypes.THEME:
3340 self._write_a_scheme_clr(color, transparency)
3341
3342 def _write_a_sys_clr(self) -> None:
3343 # Write the <a:sysClr> element.
3344
3345 val = "window"
3346 last_clr = "FFFFFF"
3347
3348 attributes = [
3349 ("val", val),
3350 ("lastClr", last_clr),
3351 ]
3352
3353 self._xml_empty_tag("a:sysClr", attributes)
3354
3355 def _write_a_srgb_clr(self, color: Color, transparency=None) -> None:
3356 # Write the <a:srgbClr> element.
3357
3358 if not color:
3359 return
3360
3361 attributes = [("val", color._rgb_hex_value())]
3362
3363 if transparency:
3364 self._xml_start_tag("a:srgbClr", attributes)
3365
3366 # Write the a:alpha element.
3367 self._write_a_alpha(transparency)
3368
3369 self._xml_end_tag("a:srgbClr")
3370 else:
3371 self._xml_empty_tag("a:srgbClr", attributes)
3372
3373 def _write_a_scheme_clr(self, color: Color, transparency=None) -> None:
3374 # Write the <a:schemeClr> element.
3375 scheme, lum_mod, lum_off = color._chart_scheme()
3376 attributes = [("val", scheme)]
3377
3378 if lum_mod > 0 or lum_off > 0 or transparency:
3379 self._xml_start_tag("a:schemeClr", attributes)
3380
3381 if lum_mod > 0:
3382 # Write the a:lumMod element.
3383 self._write_a_lum_mod(lum_mod)
3384
3385 if lum_off > 0:
3386 # Write the a:lumOff element.
3387 self._write_a_lum_off(lum_off)
3388
3389 if transparency:
3390 # Write the a:alpha element.
3391 self._write_a_alpha(transparency)
3392
3393 self._xml_end_tag("a:schemeClr")
3394 else:
3395 self._xml_empty_tag("a:schemeClr", attributes)
3396
3397 def _write_a_lum_mod(self, value: int) -> None:
3398 # Write the <a:lumMod> element.
3399 attributes = [("val", value)]
3400
3401 self._xml_empty_tag("a:lumMod", attributes)
3402
3403 def _write_a_lum_off(self, value: int) -> None:
3404 # Write the <a:lumOff> element.
3405 attributes = [("val", value)]
3406
3407 self._xml_empty_tag("a:lumOff", attributes)
3408
3409 def _write_a_alpha(self, val) -> None:
3410 # Write the <a:alpha> element.
3411
3412 val = int((100 - int(val)) * 1000)
3413
3414 attributes = [("val", val)]
3415
3416 self._xml_empty_tag("a:alpha", attributes)
3417
3418 def _write_a_prst_dash(self, val) -> None:
3419 # Write the <a:prstDash> element.
3420
3421 attributes = [("val", val)]
3422
3423 self._xml_empty_tag("a:prstDash", attributes)
3424
3425 def _write_trendline(self, trendline) -> None:
3426 # Write the <c:trendline> element.
3427
3428 if not trendline:
3429 return
3430
3431 self._xml_start_tag("c:trendline")
3432
3433 # Write the c:name element.
3434 self._write_name(trendline.get("name"))
3435
3436 # Write the c:spPr element.
3437 self._write_sp_pr(trendline)
3438
3439 # Write the c:trendlineType element.
3440 self._write_trendline_type(trendline["type"])
3441
3442 # Write the c:order element for polynomial trendlines.
3443 if trendline["type"] == "poly":
3444 self._write_trendline_order(trendline.get("order"))
3445
3446 # Write the c:period element for moving average trendlines.
3447 if trendline["type"] == "movingAvg":
3448 self._write_period(trendline.get("period"))
3449
3450 # Write the c:forward element.
3451 self._write_forward(trendline.get("forward"))
3452
3453 # Write the c:backward element.
3454 self._write_backward(trendline.get("backward"))
3455
3456 if "intercept" in trendline:
3457 # Write the c:intercept element.
3458 self._write_c_intercept(trendline["intercept"])
3459
3460 if trendline.get("display_r_squared"):
3461 # Write the c:dispRSqr element.
3462 self._write_c_disp_rsqr()
3463
3464 if trendline.get("display_equation"):
3465 # Write the c:dispEq element.
3466 self._write_c_disp_eq()
3467
3468 # Write the c:trendlineLbl element.
3469 self._write_c_trendline_lbl(trendline)
3470
3471 self._xml_end_tag("c:trendline")
3472
3473 def _write_trendline_type(self, val) -> None:
3474 # Write the <c:trendlineType> element.
3475
3476 attributes = [("val", val)]
3477
3478 self._xml_empty_tag("c:trendlineType", attributes)
3479
3480 def _write_name(self, data) -> None:
3481 # Write the <c:name> element.
3482
3483 if data is None:
3484 return
3485
3486 self._xml_data_element("c:name", data)
3487
3488 def _write_trendline_order(self, val) -> None:
3489 # Write the <c:order> element.
3490 val = max(val, 2)
3491
3492 attributes = [("val", val)]
3493
3494 self._xml_empty_tag("c:order", attributes)
3495
3496 def _write_period(self, val) -> None:
3497 # Write the <c:period> element.
3498 val = max(val, 2)
3499
3500 attributes = [("val", val)]
3501
3502 self._xml_empty_tag("c:period", attributes)
3503
3504 def _write_forward(self, val) -> None:
3505 # Write the <c:forward> element.
3506
3507 if not val:
3508 return
3509
3510 attributes = [("val", val)]
3511
3512 self._xml_empty_tag("c:forward", attributes)
3513
3514 def _write_backward(self, val) -> None:
3515 # Write the <c:backward> element.
3516
3517 if not val:
3518 return
3519
3520 attributes = [("val", val)]
3521
3522 self._xml_empty_tag("c:backward", attributes)
3523
3524 def _write_c_intercept(self, val) -> None:
3525 # Write the <c:intercept> element.
3526 attributes = [("val", val)]
3527
3528 self._xml_empty_tag("c:intercept", attributes)
3529
3530 def _write_c_disp_eq(self) -> None:
3531 # Write the <c:dispEq> element.
3532 attributes = [("val", 1)]
3533
3534 self._xml_empty_tag("c:dispEq", attributes)
3535
3536 def _write_c_disp_rsqr(self) -> None:
3537 # Write the <c:dispRSqr> element.
3538 attributes = [("val", 1)]
3539
3540 self._xml_empty_tag("c:dispRSqr", attributes)
3541
3542 def _write_c_trendline_lbl(self, trendline) -> None:
3543 # Write the <c:trendlineLbl> element.
3544 self._xml_start_tag("c:trendlineLbl")
3545
3546 # Write the c:layout element.
3547 self._write_layout(None, None)
3548
3549 # Write the c:numFmt element.
3550 self._write_trendline_num_fmt()
3551
3552 # Write the c:spPr element.
3553 self._write_sp_pr(trendline["label"])
3554
3555 # Write the data label font elements.
3556 if trendline["label"]:
3557 font = trendline["label"].get("font")
3558 if font:
3559 self._write_axis_font(font)
3560
3561 self._xml_end_tag("c:trendlineLbl")
3562
3563 def _write_trendline_num_fmt(self) -> None:
3564 # Write the <c:numFmt> element.
3565 attributes = [
3566 ("formatCode", "General"),
3567 ("sourceLinked", 0),
3568 ]
3569
3570 self._xml_empty_tag("c:numFmt", attributes)
3571
3572 def _write_hi_low_lines(self) -> None:
3573 # Write the <c:hiLowLines> element.
3574 hi_low_lines = self.hi_low_lines
3575
3576 if hi_low_lines is None:
3577 return
3578
3579 if "line" in hi_low_lines and hi_low_lines["line"]["defined"]:
3580 self._xml_start_tag("c:hiLowLines")
3581
3582 # Write the c:spPr element.
3583 self._write_sp_pr(hi_low_lines)
3584
3585 self._xml_end_tag("c:hiLowLines")
3586 else:
3587 self._xml_empty_tag("c:hiLowLines")
3588
3589 def _write_drop_lines(self) -> None:
3590 # Write the <c:dropLines> element.
3591 drop_lines = self.drop_lines
3592
3593 if drop_lines is None:
3594 return
3595
3596 if drop_lines["line"]["defined"]:
3597 self._xml_start_tag("c:dropLines")
3598
3599 # Write the c:spPr element.
3600 self._write_sp_pr(drop_lines)
3601
3602 self._xml_end_tag("c:dropLines")
3603 else:
3604 self._xml_empty_tag("c:dropLines")
3605
3606 def _write_overlap(self, val) -> None:
3607 # Write the <c:overlap> element.
3608
3609 if val is None:
3610 return
3611
3612 attributes = [("val", val)]
3613
3614 self._xml_empty_tag("c:overlap", attributes)
3615
3616 def _write_num_cache(self, data) -> None:
3617 # Write the <c:numCache> element.
3618 if data:
3619 count = len(data)
3620 else:
3621 count = 0
3622
3623 self._xml_start_tag("c:numCache")
3624
3625 # Write the c:formatCode element.
3626 self._write_format_code("General")
3627
3628 # Write the c:ptCount element.
3629 self._write_pt_count(count)
3630
3631 for i in range(count):
3632 token = data[i]
3633
3634 if token is None:
3635 continue
3636
3637 try:
3638 float(token)
3639 except ValueError:
3640 # Write non-numeric data as 0.
3641 token = 0
3642
3643 # Write the c:pt element.
3644 self._write_pt(i, token)
3645
3646 self._xml_end_tag("c:numCache")
3647
3648 def _write_str_cache(self, data) -> None:
3649 # Write the <c:strCache> element.
3650 count = len(data)
3651
3652 self._xml_start_tag("c:strCache")
3653
3654 # Write the c:ptCount element.
3655 self._write_pt_count(count)
3656
3657 for i in range(count):
3658 # Write the c:pt element.
3659 self._write_pt(i, data[i])
3660
3661 self._xml_end_tag("c:strCache")
3662
3663 def _write_format_code(self, data) -> None:
3664 # Write the <c:formatCode> element.
3665
3666 self._xml_data_element("c:formatCode", data)
3667
3668 def _write_pt_count(self, val) -> None:
3669 # Write the <c:ptCount> element.
3670
3671 attributes = [("val", val)]
3672
3673 self._xml_empty_tag("c:ptCount", attributes)
3674
3675 def _write_pt(self, idx, value) -> None:
3676 # Write the <c:pt> element.
3677
3678 if value is None:
3679 return
3680
3681 attributes = [("idx", idx)]
3682
3683 self._xml_start_tag("c:pt", attributes)
3684
3685 # Write the c:v element.
3686 self._write_v(value)
3687
3688 self._xml_end_tag("c:pt")
3689
3690 def _write_v(self, data) -> None:
3691 # Write the <c:v> element.
3692
3693 self._xml_data_element("c:v", data)
3694
3695 def _write_protection(self) -> None:
3696 # Write the <c:protection> element.
3697 if not self.protection:
3698 return
3699
3700 self._xml_empty_tag("c:protection")
3701
3702 def _write_d_pt(self, points) -> None:
3703 # Write the <c:dPt> elements.
3704 index = -1
3705
3706 if not points:
3707 return
3708
3709 for point in points:
3710 index += 1
3711 if not point:
3712 continue
3713
3714 self._write_d_pt_point(index, point)
3715
3716 def _write_d_pt_point(self, index, point) -> None:
3717 # Write an individual <c:dPt> element.
3718
3719 self._xml_start_tag("c:dPt")
3720
3721 # Write the c:idx element.
3722 self._write_idx(index)
3723
3724 # Write the c:spPr element.
3725 self._write_sp_pr(point)
3726
3727 self._xml_end_tag("c:dPt")
3728
3729 def _write_d_lbls(self, labels) -> None:
3730 # Write the <c:dLbls> element.
3731
3732 if not labels:
3733 return
3734
3735 self._xml_start_tag("c:dLbls")
3736
3737 # Write the custom c:dLbl elements.
3738 if labels.get("custom"):
3739 self._write_custom_labels(labels, labels["custom"])
3740
3741 # Write the c:numFmt element.
3742 if labels.get("num_format"):
3743 self._write_data_label_number_format(labels["num_format"])
3744
3745 # Write the c:spPr element for the plotarea formatting.
3746 self._write_sp_pr(labels)
3747
3748 # Write the data label font elements.
3749 if labels.get("font"):
3750 self._write_axis_font(labels["font"])
3751
3752 # Write the c:dLblPos element.
3753 if labels.get("position"):
3754 self._write_d_lbl_pos(labels["position"])
3755
3756 # Write the c:showLegendKey element.
3757 if labels.get("legend_key"):
3758 self._write_show_legend_key()
3759
3760 # Write the c:showVal element.
3761 if labels.get("value"):
3762 self._write_show_val()
3763
3764 # Write the c:showCatName element.
3765 if labels.get("category"):
3766 self._write_show_cat_name()
3767
3768 # Write the c:showSerName element.
3769 if labels.get("series_name"):
3770 self._write_show_ser_name()
3771
3772 # Write the c:showPercent element.
3773 if labels.get("percentage"):
3774 self._write_show_percent()
3775
3776 # Write the c:separator element.
3777 if labels.get("separator"):
3778 self._write_separator(labels["separator"])
3779
3780 # Write the c:showLeaderLines element.
3781 if labels.get("leader_lines"):
3782 self._write_show_leader_lines()
3783
3784 self._xml_end_tag("c:dLbls")
3785
3786 def _write_custom_labels(self, parent, labels) -> None:
3787 # Write the <c:showLegendKey> element.
3788 index = 0
3789
3790 for label in labels:
3791 index += 1
3792
3793 if label is None:
3794 continue
3795
3796 use_custom_formatting = True
3797
3798 self._xml_start_tag("c:dLbl")
3799
3800 # Write the c:idx element.
3801 self._write_idx(index - 1)
3802
3803 delete_label = label.get("delete")
3804
3805 if delete_label:
3806 self._write_delete(1)
3807
3808 elif label.get("formula") or label.get("value") or label.get("position"):
3809
3810 # Write the c:layout element.
3811 self._write_layout(None, None)
3812
3813 if label.get("formula"):
3814 self._write_custom_label_formula(label)
3815 elif label.get("value"):
3816 self._write_custom_label_str(label)
3817 # String values use spPr formatting.
3818 use_custom_formatting = False
3819
3820 if use_custom_formatting:
3821 self._write_custom_label_format(label)
3822
3823 if label.get("position"):
3824 self._write_d_lbl_pos(label["position"])
3825 elif parent.get("position"):
3826 self._write_d_lbl_pos(parent["position"])
3827
3828 if parent.get("value"):
3829 self._write_show_val()
3830
3831 if parent.get("category"):
3832 self._write_show_cat_name()
3833
3834 if parent.get("series_name"):
3835 self._write_show_ser_name()
3836
3837 else:
3838 self._write_custom_label_format(label)
3839
3840 self._xml_end_tag("c:dLbl")
3841
3842 def _write_custom_label_str(self, label) -> None:
3843 # Write parts of the <c:dLbl> element for strings.
3844 title = label.get("value")
3845 font = label.get("font")
3846 has_formatting = self._has_formatting(label)
3847
3848 self._xml_start_tag("c:tx")
3849
3850 # Write the c:rich element.
3851 self._write_rich(title, font, False, not has_formatting)
3852
3853 self._xml_end_tag("c:tx")
3854
3855 # Write the c:spPr element.
3856 self._write_sp_pr(label)
3857
3858 def _write_custom_label_formula(self, label) -> None:
3859 # Write parts of the <c:dLbl> element for formulas.
3860 formula = label.get("formula")
3861 data_id = label.get("data_id")
3862 data = None
3863
3864 if data_id is not None:
3865 data = self.formula_data[data_id]
3866
3867 self._xml_start_tag("c:tx")
3868
3869 # Write the c:strRef element.
3870 self._write_str_ref(formula, data, "str")
3871
3872 self._xml_end_tag("c:tx")
3873
3874 def _write_custom_label_format(self, label) -> None:
3875 # Write the formatting and font elements for the custom labels.
3876 font = label.get("font")
3877 has_formatting = self._has_formatting(label)
3878
3879 if has_formatting:
3880 self._write_sp_pr(label)
3881 self._write_tx_pr(font)
3882 elif font:
3883 self._xml_empty_tag("c:spPr")
3884 self._write_tx_pr(font)
3885
3886 def _write_show_legend_key(self) -> None:
3887 # Write the <c:showLegendKey> element.
3888 val = "1"
3889
3890 attributes = [("val", val)]
3891
3892 self._xml_empty_tag("c:showLegendKey", attributes)
3893
3894 def _write_show_val(self) -> None:
3895 # Write the <c:showVal> element.
3896 val = 1
3897
3898 attributes = [("val", val)]
3899
3900 self._xml_empty_tag("c:showVal", attributes)
3901
3902 def _write_show_cat_name(self) -> None:
3903 # Write the <c:showCatName> element.
3904 val = 1
3905
3906 attributes = [("val", val)]
3907
3908 self._xml_empty_tag("c:showCatName", attributes)
3909
3910 def _write_show_ser_name(self) -> None:
3911 # Write the <c:showSerName> element.
3912 val = 1
3913
3914 attributes = [("val", val)]
3915
3916 self._xml_empty_tag("c:showSerName", attributes)
3917
3918 def _write_show_percent(self) -> None:
3919 # Write the <c:showPercent> element.
3920 val = 1
3921
3922 attributes = [("val", val)]
3923
3924 self._xml_empty_tag("c:showPercent", attributes)
3925
3926 def _write_separator(self, data) -> None:
3927 # Write the <c:separator> element.
3928 self._xml_data_element("c:separator", data)
3929
3930 def _write_show_leader_lines(self) -> None:
3931 # Write the <c:showLeaderLines> element.
3932 #
3933 # This is different for Pie/Doughnut charts. Other chart types only
3934 # supported leader lines after Excel 2015 via an extension element.
3935 #
3936 uri = "{CE6537A1-D6FC-4f65-9D91-7224C49458BB}"
3937 xmlns_c_15 = "http://schemas.microsoft.com/office/drawing/2012/chart"
3938
3939 attributes = [
3940 ("uri", uri),
3941 ("xmlns:c15", xmlns_c_15),
3942 ]
3943
3944 self._xml_start_tag("c:extLst")
3945 self._xml_start_tag("c:ext", attributes)
3946 self._xml_empty_tag("c15:showLeaderLines", [("val", 1)])
3947 self._xml_end_tag("c:ext")
3948 self._xml_end_tag("c:extLst")
3949
3950 def _write_d_lbl_pos(self, val) -> None:
3951 # Write the <c:dLblPos> element.
3952
3953 attributes = [("val", val)]
3954
3955 self._xml_empty_tag("c:dLblPos", attributes)
3956
3957 def _write_delete(self, val) -> None:
3958 # Write the <c:delete> element.
3959
3960 attributes = [("val", val)]
3961
3962 self._xml_empty_tag("c:delete", attributes)
3963
3964 def _write_c_invert_if_negative(self, invert) -> None:
3965 # Write the <c:invertIfNegative> element.
3966 val = 1
3967
3968 if not invert:
3969 return
3970
3971 attributes = [("val", val)]
3972
3973 self._xml_empty_tag("c:invertIfNegative", attributes)
3974
3975 def _write_axis_font(self, font) -> None:
3976 # Write the axis font elements.
3977
3978 if not font:
3979 return
3980
3981 self._xml_start_tag("c:txPr")
3982 self._write_a_body_pr(font.get("rotation"), None)
3983 self._write_a_lst_style()
3984 self._xml_start_tag("a:p")
3985
3986 self._write_a_p_pr_rich(font)
3987
3988 self._write_a_end_para_rpr()
3989 self._xml_end_tag("a:p")
3990 self._xml_end_tag("c:txPr")
3991
3992 def _write_a_latin(self, attributes) -> None:
3993 # Write the <a:latin> element.
3994 self._xml_empty_tag("a:latin", attributes)
3995
3996 def _write_d_table(self) -> None:
3997 # Write the <c:dTable> element.
3998 table = self.table
3999
4000 if not table:
4001 return
4002
4003 self._xml_start_tag("c:dTable")
4004
4005 if table["horizontal"]:
4006 # Write the c:showHorzBorder element.
4007 self._write_show_horz_border()
4008
4009 if table["vertical"]:
4010 # Write the c:showVertBorder element.
4011 self._write_show_vert_border()
4012
4013 if table["outline"]:
4014 # Write the c:showOutline element.
4015 self._write_show_outline()
4016
4017 if table["show_keys"]:
4018 # Write the c:showKeys element.
4019 self._write_show_keys()
4020
4021 if table["font"]:
4022 # Write the table font.
4023 self._write_tx_pr(table["font"])
4024
4025 self._xml_end_tag("c:dTable")
4026
4027 def _write_show_horz_border(self) -> None:
4028 # Write the <c:showHorzBorder> element.
4029 attributes = [("val", 1)]
4030
4031 self._xml_empty_tag("c:showHorzBorder", attributes)
4032
4033 def _write_show_vert_border(self) -> None:
4034 # Write the <c:showVertBorder> element.
4035 attributes = [("val", 1)]
4036
4037 self._xml_empty_tag("c:showVertBorder", attributes)
4038
4039 def _write_show_outline(self) -> None:
4040 # Write the <c:showOutline> element.
4041 attributes = [("val", 1)]
4042
4043 self._xml_empty_tag("c:showOutline", attributes)
4044
4045 def _write_show_keys(self) -> None:
4046 # Write the <c:showKeys> element.
4047 attributes = [("val", 1)]
4048
4049 self._xml_empty_tag("c:showKeys", attributes)
4050
4051 def _write_error_bars(self, error_bars) -> None:
4052 # Write the X and Y error bars.
4053
4054 if not error_bars:
4055 return
4056
4057 if error_bars["x_error_bars"]:
4058 self._write_err_bars("x", error_bars["x_error_bars"])
4059
4060 if error_bars["y_error_bars"]:
4061 self._write_err_bars("y", error_bars["y_error_bars"])
4062
4063 def _write_err_bars(self, direction, error_bars) -> None:
4064 # Write the <c:errBars> element.
4065
4066 if not error_bars:
4067 return
4068
4069 self._xml_start_tag("c:errBars")
4070
4071 # Write the c:errDir element.
4072 self._write_err_dir(direction)
4073
4074 # Write the c:errBarType element.
4075 self._write_err_bar_type(error_bars["direction"])
4076
4077 # Write the c:errValType element.
4078 self._write_err_val_type(error_bars["type"])
4079
4080 if not error_bars["endcap"]:
4081 # Write the c:noEndCap element.
4082 self._write_no_end_cap()
4083
4084 if error_bars["type"] == "stdErr":
4085 # Don't need to write a c:errValType tag.
4086 pass
4087 elif error_bars["type"] == "cust":
4088 # Write the custom error tags.
4089 self._write_custom_error(error_bars)
4090 else:
4091 # Write the c:val element.
4092 self._write_error_val(error_bars["value"])
4093
4094 # Write the c:spPr element.
4095 self._write_sp_pr(error_bars)
4096
4097 self._xml_end_tag("c:errBars")
4098
4099 def _write_err_dir(self, val) -> None:
4100 # Write the <c:errDir> element.
4101
4102 attributes = [("val", val)]
4103
4104 self._xml_empty_tag("c:errDir", attributes)
4105
4106 def _write_err_bar_type(self, val) -> None:
4107 # Write the <c:errBarType> element.
4108
4109 attributes = [("val", val)]
4110
4111 self._xml_empty_tag("c:errBarType", attributes)
4112
4113 def _write_err_val_type(self, val) -> None:
4114 # Write the <c:errValType> element.
4115
4116 attributes = [("val", val)]
4117
4118 self._xml_empty_tag("c:errValType", attributes)
4119
4120 def _write_no_end_cap(self) -> None:
4121 # Write the <c:noEndCap> element.
4122 attributes = [("val", 1)]
4123
4124 self._xml_empty_tag("c:noEndCap", attributes)
4125
4126 def _write_error_val(self, val) -> None:
4127 # Write the <c:val> element for error bars.
4128
4129 attributes = [("val", val)]
4130
4131 self._xml_empty_tag("c:val", attributes)
4132
4133 def _write_custom_error(self, error_bars) -> None:
4134 # Write the custom error bars tags.
4135
4136 if error_bars["plus_values"]:
4137 # Write the c:plus element.
4138 self._xml_start_tag("c:plus")
4139
4140 if isinstance(error_bars["plus_values"], list):
4141 self._write_num_lit(error_bars["plus_values"])
4142 else:
4143 self._write_num_ref(
4144 error_bars["plus_values"], error_bars["plus_data"], "num"
4145 )
4146 self._xml_end_tag("c:plus")
4147
4148 if error_bars["minus_values"]:
4149 # Write the c:minus element.
4150 self._xml_start_tag("c:minus")
4151
4152 if isinstance(error_bars["minus_values"], list):
4153 self._write_num_lit(error_bars["minus_values"])
4154 else:
4155 self._write_num_ref(
4156 error_bars["minus_values"], error_bars["minus_data"], "num"
4157 )
4158 self._xml_end_tag("c:minus")
4159
4160 def _write_num_lit(self, data) -> None:
4161 # Write the <c:numLit> element for literal number list elements.
4162 count = len(data)
4163
4164 # Write the c:numLit element.
4165 self._xml_start_tag("c:numLit")
4166
4167 # Write the c:formatCode element.
4168 self._write_format_code("General")
4169
4170 # Write the c:ptCount element.
4171 self._write_pt_count(count)
4172
4173 for i in range(count):
4174 token = data[i]
4175
4176 if token is None:
4177 continue
4178
4179 try:
4180 float(token)
4181 except ValueError:
4182 # Write non-numeric data as 0.
4183 token = 0
4184
4185 # Write the c:pt element.
4186 self._write_pt(i, token)
4187
4188 self._xml_end_tag("c:numLit")
4189
4190 def _write_up_down_bars(self) -> None:
4191 # Write the <c:upDownBars> element.
4192 up_down_bars = self.up_down_bars
4193
4194 if up_down_bars is None:
4195 return
4196
4197 self._xml_start_tag("c:upDownBars")
4198
4199 # Write the c:gapWidth element.
4200 self._write_gap_width(150)
4201
4202 # Write the c:upBars element.
4203 self._write_up_bars(up_down_bars.get("up"))
4204
4205 # Write the c:downBars element.
4206 self._write_down_bars(up_down_bars.get("down"))
4207
4208 self._xml_end_tag("c:upDownBars")
4209
4210 def _write_gap_width(self, val) -> None:
4211 # Write the <c:gapWidth> element.
4212
4213 if val is None:
4214 return
4215
4216 attributes = [("val", val)]
4217
4218 self._xml_empty_tag("c:gapWidth", attributes)
4219
4220 def _write_up_bars(self, bar_format) -> None:
4221 # Write the <c:upBars> element.
4222
4223 if bar_format["line"] and bar_format["line"]["defined"]:
4224 self._xml_start_tag("c:upBars")
4225
4226 # Write the c:spPr element.
4227 self._write_sp_pr(bar_format)
4228
4229 self._xml_end_tag("c:upBars")
4230 else:
4231 self._xml_empty_tag("c:upBars")
4232
4233 def _write_down_bars(self, bar_format) -> None:
4234 # Write the <c:downBars> element.
4235
4236 if bar_format["line"] and bar_format["line"]["defined"]:
4237 self._xml_start_tag("c:downBars")
4238
4239 # Write the c:spPr element.
4240 self._write_sp_pr(bar_format)
4241
4242 self._xml_end_tag("c:downBars")
4243 else:
4244 self._xml_empty_tag("c:downBars")
4245
4246 def _write_disp_units(self, units, display) -> None:
4247 # Write the <c:dispUnits> element.
4248
4249 if not units:
4250 return
4251
4252 attributes = [("val", units)]
4253
4254 self._xml_start_tag("c:dispUnits")
4255 self._xml_empty_tag("c:builtInUnit", attributes)
4256
4257 if display:
4258 self._xml_start_tag("c:dispUnitsLbl")
4259 self._xml_empty_tag("c:layout")
4260 self._xml_end_tag("c:dispUnitsLbl")
4261
4262 self._xml_end_tag("c:dispUnits")
4263
4264 def _write_a_grad_fill(self, gradient) -> None:
4265 # Write the <a:gradFill> element.
4266
4267 attributes = [("flip", "none"), ("rotWithShape", "1")]
4268
4269 if gradient["type"] == "linear":
4270 attributes = []
4271
4272 self._xml_start_tag("a:gradFill", attributes)
4273
4274 # Write the a:gsLst element.
4275 self._write_a_gs_lst(gradient)
4276
4277 if gradient["type"] == "linear":
4278 # Write the a:lin element.
4279 self._write_a_lin(gradient["angle"])
4280 else:
4281 # Write the a:path element.
4282 self._write_a_path(gradient["type"])
4283
4284 # Write the a:tileRect element.
4285 self._write_a_tile_rect(gradient["type"])
4286
4287 self._xml_end_tag("a:gradFill")
4288
4289 def _write_a_gs_lst(self, gradient) -> None:
4290 # Write the <a:gsLst> element.
4291 positions = gradient["positions"]
4292 colors = gradient["colors"]
4293
4294 self._xml_start_tag("a:gsLst")
4295
4296 for i, color in enumerate(colors):
4297 pos = int(positions[i] * 1000)
4298 attributes = [("pos", pos)]
4299 self._xml_start_tag("a:gs", attributes)
4300
4301 self._write_color(color)
4302
4303 self._xml_end_tag("a:gs")
4304
4305 self._xml_end_tag("a:gsLst")
4306
4307 def _write_a_lin(self, angle) -> None:
4308 # Write the <a:lin> element.
4309
4310 angle = int(60000 * angle)
4311
4312 attributes = [
4313 ("ang", angle),
4314 ("scaled", "0"),
4315 ]
4316
4317 self._xml_empty_tag("a:lin", attributes)
4318
4319 def _write_a_path(self, gradient_type) -> None:
4320 # Write the <a:path> element.
4321
4322 attributes = [("path", gradient_type)]
4323
4324 self._xml_start_tag("a:path", attributes)
4325
4326 # Write the a:fillToRect element.
4327 self._write_a_fill_to_rect(gradient_type)
4328
4329 self._xml_end_tag("a:path")
4330
4331 def _write_a_fill_to_rect(self, gradient_type) -> None:
4332 # Write the <a:fillToRect> element.
4333
4334 if gradient_type == "shape":
4335 attributes = [
4336 ("l", "50000"),
4337 ("t", "50000"),
4338 ("r", "50000"),
4339 ("b", "50000"),
4340 ]
4341 else:
4342 attributes = [
4343 ("l", "100000"),
4344 ("t", "100000"),
4345 ]
4346
4347 self._xml_empty_tag("a:fillToRect", attributes)
4348
4349 def _write_a_tile_rect(self, gradient_type) -> None:
4350 # Write the <a:tileRect> element.
4351
4352 if gradient_type == "shape":
4353 attributes = []
4354 else:
4355 attributes = [
4356 ("r", "-100000"),
4357 ("b", "-100000"),
4358 ]
4359
4360 self._xml_empty_tag("a:tileRect", attributes)
4361
4362 def _write_a_patt_fill(self, pattern) -> None:
4363 # Write the <a:pattFill> element.
4364
4365 attributes = [("prst", pattern["pattern"])]
4366
4367 self._xml_start_tag("a:pattFill", attributes)
4368
4369 # Write the a:fgClr element.
4370 self._write_a_fg_clr(pattern["fg_color"])
4371
4372 # Write the a:bgClr element.
4373 self._write_a_bg_clr(pattern["bg_color"])
4374
4375 self._xml_end_tag("a:pattFill")
4376
4377 def _write_a_fg_clr(self, color: Color) -> None:
4378 # Write the <a:fgClr> element.
4379 self._xml_start_tag("a:fgClr")
4380 self._write_color(color)
4381 self._xml_end_tag("a:fgClr")
4382
4383 def _write_a_bg_clr(self, color: Color) -> None:
4384 # Write the <a:bgClr> element.
4385 self._xml_start_tag("a:bgClr")
4386 self._write_color(color)
4387 self._xml_end_tag("a:bgClr")