Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/xlsxwriter/chart.py: 11%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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#
10import copy
11import re
12from typing import Any, Dict, Optional
13from warnings import warn
15from xlsxwriter import xmlwriter
16from xlsxwriter.chart_title import ChartTitle
17from xlsxwriter.color import Color, ColorTypes
18from xlsxwriter.shape import Shape
19from xlsxwriter.utility import (
20 _datetime_to_excel_datetime,
21 _supported_datetime,
22 quote_sheetname,
23 xl_range_formula,
24 xl_rowcol_to_cell,
25)
28class Chart(xmlwriter.XMLwriter):
29 """
30 A class for writing the Excel XLSX Chart file.
33 """
35 ###########################################################################
36 #
37 # Public API.
38 #
39 ###########################################################################
41 def __init__(self) -> None:
42 """
43 Constructor.
45 """
47 super().__init__()
49 self.subtype = None
50 self.sheet_type = 0x0200
51 self.orientation = 0x0
52 self.series = []
53 self.embedded = 0
54 self.id = -1
55 self.series_index = 0
56 self.style_id = 2
57 self.axis_ids = []
58 self.axis2_ids = []
59 self.cat_has_num_fmt = False
60 self.requires_category = False
61 self.legend = {}
62 self.cat_axis_position = "b"
63 self.val_axis_position = "l"
64 self.formula_ids = {}
65 self.formula_data = []
66 self.horiz_cat_axis = 0
67 self.horiz_val_axis = 1
68 self.protection = 0
69 self.chartarea = {}
70 self.plotarea = {}
71 self.x_axis = {}
72 self.y_axis = {}
73 self.y2_axis = {}
74 self.x2_axis = {}
75 self.chart_name = ""
76 self.show_blanks = "gap"
77 self.show_na_as_empty = False
78 self.show_hidden = False
79 self.show_crosses = True
80 self.width = 480
81 self.height = 288
82 self.x_scale = 1
83 self.y_scale = 1
84 self.x_offset = 0
85 self.y_offset = 0
86 self.table = None
87 self.cross_between = "between"
88 self.default_marker = None
89 self.series_gap_1 = None
90 self.series_gap_2 = None
91 self.series_overlap_1 = None
92 self.series_overlap_2 = None
93 self.drop_lines = None
94 self.hi_low_lines = None
95 self.up_down_bars = None
96 self.smooth_allowed = False
97 self.title = ChartTitle()
99 self.date_category = False
100 self.date_1904 = False
101 self.remove_timezone = False
102 self.label_positions = {}
103 self.label_position_default = ""
104 self.already_inserted = False
105 self.combined = None
106 self.is_secondary = False
107 self.warn_sheetname = True
108 self._set_default_properties()
109 self.fill = {}
111 def add_series(self, options: Optional[Dict[str, Any]] = None) -> None:
112 """
113 Add a data series to a chart.
115 Args:
116 options: A dictionary of chart series options.
118 Returns:
119 Nothing.
121 """
122 # Add a series and it's properties to a chart.
123 if options is None:
124 options = {}
126 # Check that the required input has been specified.
127 if "values" not in options:
128 warn("Must specify 'values' in add_series()")
129 return
131 if self.requires_category and "categories" not in options:
132 warn("Must specify 'categories' in add_series() for this chart type")
133 return
135 if len(self.series) == 255:
136 warn(
137 "The maximum number of series that can be added to an "
138 "Excel Chart is 255"
139 )
140 return
142 # Convert list into a formula string.
143 values = self._list_to_formula(options.get("values"))
144 categories = self._list_to_formula(options.get("categories"))
146 # Switch name and name_formula parameters if required.
147 name, name_formula = self._process_names(
148 options.get("name"), options.get("name_formula")
149 )
151 # Get an id for the data equivalent to the range formula.
152 cat_id = self._get_data_id(categories, options.get("categories_data"))
153 val_id = self._get_data_id(values, options.get("values_data"))
154 name_id = self._get_data_id(name_formula, options.get("name_data"))
156 # Set the line properties for the series.
157 line = Shape._get_line_properties(options)
159 # Set the fill properties for the series.
160 fill = Shape._get_fill_properties(options.get("fill"))
162 # Set the pattern fill properties for the series.
163 pattern = Shape._get_pattern_properties(options.get("pattern"))
165 # Set the gradient fill properties for the series.
166 gradient = Shape._get_gradient_properties(options.get("gradient"))
168 # Pattern fill overrides solid fill.
169 if pattern:
170 self.fill = None
172 # Gradient fill overrides the solid and pattern fill.
173 if gradient:
174 pattern = None
175 fill = None
177 # Set the marker properties for the series.
178 marker = self._get_marker_properties(options.get("marker"))
180 # Set the trendline properties for the series.
181 trendline = self._get_trendline_properties(options.get("trendline"))
183 # Set the line smooth property for the series.
184 smooth = options.get("smooth")
186 # Set the error bars properties for the series.
187 y_error_bars = self._get_error_bars_props(options.get("y_error_bars"))
188 x_error_bars = self._get_error_bars_props(options.get("x_error_bars"))
190 error_bars = {"x_error_bars": x_error_bars, "y_error_bars": y_error_bars}
192 # Set the point properties for the series.
193 points = self._get_points_properties(options.get("points"))
195 # Set the labels properties for the series.
196 labels = self._get_labels_properties(options.get("data_labels"))
198 # Set the "invert if negative" fill property.
199 invert_if_neg = options.get("invert_if_negative", False)
200 inverted_color = options.get("invert_if_negative_color")
202 if inverted_color:
203 inverted_color = Color._from_value(inverted_color)
205 # Set the secondary axis properties.
206 x2_axis = options.get("x2_axis")
207 y2_axis = options.get("y2_axis")
209 # Store secondary status for combined charts.
210 if x2_axis or y2_axis:
211 self.is_secondary = True
213 # Set the gap for Bar/Column charts.
214 if options.get("gap") is not None:
215 if y2_axis:
216 self.series_gap_2 = options["gap"]
217 else:
218 self.series_gap_1 = options["gap"]
220 # Set the overlap for Bar/Column charts.
221 if options.get("overlap"):
222 if y2_axis:
223 self.series_overlap_2 = options["overlap"]
224 else:
225 self.series_overlap_1 = options["overlap"]
227 # Add the user supplied data to the internal structures.
228 series = {
229 "values": values,
230 "categories": categories,
231 "name": name,
232 "name_formula": name_formula,
233 "name_id": name_id,
234 "val_data_id": val_id,
235 "cat_data_id": cat_id,
236 "line": line,
237 "fill": fill,
238 "pattern": pattern,
239 "gradient": gradient,
240 "marker": marker,
241 "trendline": trendline,
242 "labels": labels,
243 "invert_if_neg": invert_if_neg,
244 "inverted_color": inverted_color,
245 "x2_axis": x2_axis,
246 "y2_axis": y2_axis,
247 "points": points,
248 "error_bars": error_bars,
249 "smooth": smooth,
250 }
252 self.series.append(series)
254 def set_x_axis(self, options: Dict[str, Any]) -> None:
255 """
256 Set the chart X axis options.
258 Args:
259 options: A dictionary of axis options.
261 Returns:
262 Nothing.
264 """
265 axis = self._convert_axis_args(self.x_axis, options)
267 self.x_axis = axis
269 def set_y_axis(self, options: Dict[str, Any]) -> None:
270 """
271 Set the chart Y axis options.
273 Args:
274 options: A dictionary of axis options.
276 Returns:
277 Nothing.
279 """
280 axis = self._convert_axis_args(self.y_axis, options)
282 self.y_axis = axis
284 def set_x2_axis(self, options: Dict[str, Any]) -> None:
285 """
286 Set the chart secondary X axis options.
288 Args:
289 options: A dictionary of axis options.
291 Returns:
292 Nothing.
294 """
295 axis = self._convert_axis_args(self.x2_axis, options)
297 self.x2_axis = axis
299 def set_y2_axis(self, options: Dict[str, Any]) -> None:
300 """
301 Set the chart secondary Y axis options.
303 Args:
304 options: A dictionary of axis options.
306 Returns:
307 Nothing.
309 """
310 axis = self._convert_axis_args(self.y2_axis, options)
312 self.y2_axis = axis
314 def set_title(self, options: Optional[Dict[str, Any]] = None) -> None:
315 """
316 Set the chart title options.
318 Args:
319 options: A dictionary of chart title options.
321 Returns:
322 Nothing.
324 """
325 if options is None:
326 options = {}
328 name, name_formula = self._process_names(
329 options.get("name"), options.get("name_formula")
330 )
332 data_id = self._get_data_id(name_formula, options.get("data"))
334 # Update the main chart title.
335 self.title.name = name
336 self.title.formula = name_formula
337 self.title.data_id = data_id
339 # Set the font properties if present.
340 if options.get("font"):
341 self.title.font = self._convert_font_args(options.get("font"))
342 else:
343 # For backward/axis compatibility.
344 self.title.font = self._convert_font_args(options.get("name_font"))
346 # Set the line properties.
347 self.title.line = Shape._get_line_properties(options)
349 # Set the fill properties.
350 self.title.fill = Shape._get_fill_properties(options.get("fill"))
352 # Set the gradient properties.
353 self.title.gradient = Shape._get_gradient_properties(options.get("gradient"))
355 # Set the layout.
356 self.title.layout = self._get_layout_properties(options.get("layout"), True)
358 # Set the title overlay option.
359 self.title.overlay = options.get("overlay")
361 # Set the automatic title option.
362 self.title.hidden = options.get("none", False)
364 def set_legend(self, options: Dict[str, Any]) -> None:
365 """
366 Set the chart legend options.
368 Args:
369 options: A dictionary of chart legend options.
371 Returns:
372 Nothing.
373 """
374 # Convert the user defined properties to internal properties.
375 self.legend = self._get_legend_properties(options)
377 def set_plotarea(self, options: Dict[str, Any]) -> None:
378 """
379 Set the chart plot area options.
381 Args:
382 options: A dictionary of chart plot area options.
384 Returns:
385 Nothing.
386 """
387 # Convert the user defined properties to internal properties.
388 self.plotarea = self._get_area_properties(options)
390 def set_chartarea(self, options: Dict[str, Any]) -> None:
391 """
392 Set the chart area options.
394 Args:
395 options: A dictionary of chart area options.
397 Returns:
398 Nothing.
399 """
400 # Convert the user defined properties to internal properties.
401 self.chartarea = self._get_area_properties(options)
403 def set_style(self, style_id: int = 2) -> None:
404 """
405 Set the chart style type.
407 Args:
408 style_id: An int representing the chart style.
410 Returns:
411 Nothing.
412 """
413 # Set one of the 48 built-in Excel chart styles. The default is 2.
414 if style_id is None:
415 style_id = 2
417 if style_id < 1 or style_id > 48:
418 style_id = 2
420 self.style_id = style_id
422 def show_blanks_as(self, option: str) -> None:
423 """
424 Set the option for displaying blank data in a chart.
426 Args:
427 option: A string representing the display option.
429 Returns:
430 Nothing.
431 """
432 if not option:
433 return
435 valid_options = {
436 "gap": 1,
437 "zero": 1,
438 "span": 1,
439 }
441 if option not in valid_options:
442 warn(f"Unknown show_blanks_as() option '{option}'")
443 return
445 self.show_blanks = option
447 def show_na_as_empty_cell(self) -> None:
448 """
449 Display ``#N/A`` on charts as blank/empty cells.
451 Args:
452 None.
454 Returns:
455 Nothing.
456 """
457 self.show_na_as_empty = True
459 def show_hidden_data(self) -> None:
460 """
461 Display data on charts from hidden rows or columns.
463 Args:
464 None.
466 Returns:
467 Nothing.
468 """
469 self.show_hidden = True
471 def set_size(self, options: Optional[Dict[str, Any]] = None) -> None:
472 """
473 Set size or scale of the chart.
475 Args:
476 options: A dictionary of chart size options.
478 Returns:
479 Nothing.
480 """
481 if options is None:
482 options = {}
484 # Set dimensions or scale for the chart.
485 self.width = options.get("width", self.width)
486 self.height = options.get("height", self.height)
487 self.x_scale = options.get("x_scale", 1)
488 self.y_scale = options.get("y_scale", 1)
489 self.x_offset = options.get("x_offset", 0)
490 self.y_offset = options.get("y_offset", 0)
492 def set_table(self, options: Optional[Dict[str, Any]] = None) -> None:
493 """
494 Set properties for an axis data table.
496 Args:
497 options: A dictionary of axis table options.
499 Returns:
500 Nothing.
502 """
503 if options is None:
504 options = {}
506 table = {}
508 table["horizontal"] = options.get("horizontal", 1)
509 table["vertical"] = options.get("vertical", 1)
510 table["outline"] = options.get("outline", 1)
511 table["show_keys"] = options.get("show_keys", 0)
512 table["font"] = self._convert_font_args(options.get("font"))
514 self.table = table
516 def set_up_down_bars(self, options: Optional[Dict[str, Any]] = None) -> None:
517 """
518 Set properties for the chart up-down bars.
520 Args:
521 options: A dictionary of options.
523 Returns:
524 Nothing.
526 """
527 if options is None:
528 options = {}
530 # Defaults.
531 up_line = None
532 up_fill = None
533 down_line = None
534 down_fill = None
536 # Set properties for 'up' bar.
537 if options.get("up"):
538 up_line = Shape._get_line_properties(options["up"])
539 up_fill = Shape._get_fill_properties(options["up"]["fill"])
541 # Set properties for 'down' bar.
542 if options.get("down"):
543 down_line = Shape._get_line_properties(options["down"])
544 down_fill = Shape._get_fill_properties(options["down"]["fill"])
546 self.up_down_bars = {
547 "up": {
548 "line": up_line,
549 "fill": up_fill,
550 },
551 "down": {
552 "line": down_line,
553 "fill": down_fill,
554 },
555 }
557 def set_drop_lines(self, options: Optional[Dict[str, Any]] = None) -> None:
558 """
559 Set properties for the chart drop lines.
561 Args:
562 options: A dictionary of options.
564 Returns:
565 Nothing.
567 """
568 if options is None:
569 options = {}
571 line = Shape._get_line_properties(options)
572 fill = Shape._get_fill_properties(options.get("fill"))
574 # Set the pattern fill properties for the series.
575 pattern = Shape._get_pattern_properties(options.get("pattern"))
577 # Set the gradient fill properties for the series.
578 gradient = Shape._get_gradient_properties(options.get("gradient"))
580 # Pattern fill overrides solid fill.
581 if pattern:
582 self.fill = None
584 # Gradient fill overrides the solid and pattern fill.
585 if gradient:
586 pattern = None
587 fill = None
589 self.drop_lines = {
590 "line": line,
591 "fill": fill,
592 "pattern": pattern,
593 "gradient": gradient,
594 }
596 def set_high_low_lines(self, options: Optional[Dict[str, Any]] = None) -> None:
597 """
598 Set properties for the chart high-low lines.
600 Args:
601 options: A dictionary of options.
603 Returns:
604 Nothing.
606 """
607 if options is None:
608 options = {}
610 line = Shape._get_line_properties(options)
611 fill = Shape._get_fill_properties(options.get("fill"))
613 # Set the pattern fill properties for the series.
614 pattern = Shape._get_pattern_properties(options.get("pattern"))
616 # Set the gradient fill properties for the series.
617 gradient = Shape._get_gradient_properties(options.get("gradient"))
619 # Pattern fill overrides solid fill.
620 if pattern:
621 self.fill = None
623 # Gradient fill overrides the solid and pattern fill.
624 if gradient:
625 pattern = None
626 fill = None
628 self.hi_low_lines = {
629 "line": line,
630 "fill": fill,
631 "pattern": pattern,
632 "gradient": gradient,
633 }
635 def combine(self, chart: Optional["Chart"] = None) -> None:
636 """
637 Create a combination chart with a secondary chart.
639 Args:
640 chart: The secondary chart to combine with the primary chart.
642 Returns:
643 Nothing.
645 """
646 if chart is None:
647 return
649 self.combined = chart
651 ###########################################################################
652 #
653 # Private API.
654 #
655 ###########################################################################
657 def _assemble_xml_file(self) -> None:
658 # Assemble and write the XML file.
660 # Write the XML declaration.
661 self._xml_declaration()
663 # Write the c:chartSpace element.
664 self._write_chart_space()
666 # Write the c:lang element.
667 self._write_lang()
669 # Write the c:style element.
670 self._write_style()
672 # Write the c:protection element.
673 self._write_protection()
675 # Write the c:chart element.
676 self._write_chart()
678 # Write the c:spPr element for the chartarea formatting.
679 self._write_sp_pr(self.chartarea)
681 # Write the c:printSettings element.
682 if self.embedded:
683 self._write_print_settings()
685 # Close the worksheet tag.
686 self._xml_end_tag("c:chartSpace")
687 # Close the file.
688 self._xml_close()
690 def _convert_axis_args(self, axis, user_options):
691 # Convert user defined axis values into private hash values.
692 options = axis["defaults"].copy()
693 options.update(user_options)
695 axis = {
696 "defaults": axis["defaults"],
697 "reverse": options.get("reverse"),
698 "min": options.get("min"),
699 "max": options.get("max"),
700 "minor_unit": options.get("minor_unit"),
701 "major_unit": options.get("major_unit"),
702 "minor_unit_type": options.get("minor_unit_type"),
703 "major_unit_type": options.get("major_unit_type"),
704 "display_units": options.get("display_units"),
705 "log_base": options.get("log_base"),
706 "crossing": options.get("crossing"),
707 "position_axis": options.get("position_axis"),
708 "position": options.get("position"),
709 "label_position": options.get("label_position"),
710 "label_align": options.get("label_align"),
711 "num_format": options.get("num_format"),
712 "num_format_linked": options.get("num_format_linked"),
713 "interval_unit": options.get("interval_unit"),
714 "interval_tick": options.get("interval_tick"),
715 "text_axis": False,
716 "title": ChartTitle(),
717 }
719 axis["visible"] = options.get("visible", True)
721 # Convert the display units.
722 axis["display_units"] = self._get_display_units(axis["display_units"])
723 axis["display_units_visible"] = options.get("display_units_visible", True)
725 # Map major_gridlines properties.
726 if options.get("major_gridlines") and options["major_gridlines"]["visible"]:
727 axis["major_gridlines"] = self._get_gridline_properties(
728 options["major_gridlines"]
729 )
731 # Map minor_gridlines properties.
732 if options.get("minor_gridlines") and options["minor_gridlines"]["visible"]:
733 axis["minor_gridlines"] = self._get_gridline_properties(
734 options["minor_gridlines"]
735 )
737 # Only use the first letter of bottom, top, left or right.
738 if axis.get("position"):
739 axis["position"] = axis["position"].lower()[0]
741 # Set the position for a category axis on or between the tick marks.
742 if axis.get("position_axis"):
743 if axis["position_axis"] == "on_tick":
744 axis["position_axis"] = "midCat"
745 elif axis["position_axis"] == "between":
746 # Doesn't need to be modified.
747 pass
748 else:
749 # Otherwise use the default value.
750 axis["position_axis"] = None
752 # Set the category axis as a date axis.
753 if options.get("date_axis"):
754 self.date_category = True
756 # Set the category axis as a text axis.
757 if options.get("text_axis"):
758 self.date_category = False
759 axis["text_axis"] = True
761 # Convert datetime args if required.
762 if axis.get("min") and _supported_datetime(axis["min"]):
763 axis["min"] = _datetime_to_excel_datetime(
764 axis["min"], self.date_1904, self.remove_timezone
765 )
766 if axis.get("max") and _supported_datetime(axis["max"]):
767 axis["max"] = _datetime_to_excel_datetime(
768 axis["max"], self.date_1904, self.remove_timezone
769 )
770 if axis.get("crossing") and _supported_datetime(axis["crossing"]):
771 axis["crossing"] = _datetime_to_excel_datetime(
772 axis["crossing"], self.date_1904, self.remove_timezone
773 )
775 # Set the font properties if present.
776 axis["num_font"] = self._convert_font_args(options.get("num_font"))
778 # Set the line properties for the axis.
779 axis["line"] = Shape._get_line_properties(options)
781 # Set the fill properties for the axis.
782 axis["fill"] = Shape._get_fill_properties(options.get("fill"))
784 # Set the pattern fill properties for the series.
785 axis["pattern"] = Shape._get_pattern_properties(options.get("pattern"))
787 # Set the gradient fill properties for the series.
788 axis["gradient"] = Shape._get_gradient_properties(options.get("gradient"))
790 # Pattern fill overrides solid fill.
791 if axis.get("pattern"):
792 axis["fill"] = None
794 # Gradient fill overrides the solid and pattern fill.
795 if axis.get("gradient"):
796 axis["pattern"] = None
797 axis["fill"] = None
799 # Set the tick marker types.
800 axis["minor_tick_mark"] = self._get_tick_type(options.get("minor_tick_mark"))
801 axis["major_tick_mark"] = self._get_tick_type(options.get("major_tick_mark"))
803 # Check if the axis title is simple text or a formula.
804 name, name_formula = self._process_names(
805 options.get("name"), options.get("name_formula")
806 )
808 # Get an id for the data equivalent to the range formula.
809 data_id = self._get_data_id(name_formula, options.get("data"))
811 # Set the title properties.
812 axis["title"].name = name
813 axis["title"].formula = name_formula
814 axis["title"].data_id = data_id
815 axis["title"].font = self._convert_font_args(options.get("name_font"))
816 axis["title"].layout = self._get_layout_properties(
817 options.get("name_layout"), True
818 )
820 # Map the line and border properties for the axis title.
821 options["line"] = options.get("name_line")
822 options["border"] = options.get("name_border")
824 axis["title"].line = Shape._get_line_properties(options)
825 axis["title"].fill = Shape._get_fill_properties(options.get("name_fill"))
826 axis["title"].pattern = Shape._get_pattern_properties(
827 options.get("name_pattern")
828 )
829 axis["title"].gradient = Shape._get_gradient_properties(
830 options.get("name_gradient")
831 )
833 return axis
835 def _convert_font_args(self, options):
836 # Convert user defined font values into private dict values.
837 if not options:
838 return {}
840 font = {
841 "name": options.get("name"),
842 "color": options.get("color"),
843 "size": options.get("size"),
844 "bold": options.get("bold"),
845 "italic": options.get("italic"),
846 "underline": options.get("underline"),
847 "pitch_family": options.get("pitch_family"),
848 "charset": options.get("charset"),
849 "baseline": options.get("baseline", 0),
850 "rotation": options.get("rotation"),
851 }
853 # Convert font size units.
854 if font["size"]:
855 font["size"] = int(font["size"] * 100)
857 # Convert rotation into 60,000ths of a degree.
858 if font["rotation"]:
859 font["rotation"] = 60000 * int(font["rotation"])
861 if font.get("color"):
862 font["color"] = Color._from_value(font["color"])
864 return font
866 def _list_to_formula(self, data):
867 # Convert and list of row col values to a range formula.
869 # If it isn't an array ref it is probably a formula already.
870 if not isinstance(data, list):
871 # Check for unquoted sheetnames.
872 if data and " " in data and "'" not in data and self.warn_sheetname:
873 warn(
874 f"Sheetname in '{data}' contains spaces but isn't quoted. "
875 f"This may cause an error in Excel."
876 )
877 return data
879 formula = xl_range_formula(*data)
881 return formula
883 def _process_names(self, name, name_formula):
884 # Switch name and name_formula parameters if required.
886 if name is not None:
887 if isinstance(name, list):
888 # Convert a list of values into a name formula.
889 cell = xl_rowcol_to_cell(name[1], name[2], True, True)
890 name_formula = quote_sheetname(name[0]) + "!" + cell
891 name = ""
892 elif re.match(r"^=?[^!]+!\$?[A-Z]+\$?\d+", name):
893 # Name looks like a formula, use it to set name_formula.
894 name_formula = name
895 name = ""
897 return name, name_formula
899 def _get_data_type(self, data) -> str:
900 # Find the overall type of the data associated with a series.
902 # Check for no data in the series.
903 if data is None or len(data) == 0:
904 return "none"
906 if isinstance(data[0], list):
907 return "multi_str"
909 # Determine if data is numeric or strings.
910 for token in data:
911 if token is None:
912 continue
914 # Check for strings that would evaluate to float like
915 # '1.1_1' of ' 1'.
916 if isinstance(token, str) and re.search("[_ ]", token):
917 # Assume entire data series is string data.
918 return "str"
920 try:
921 float(token)
922 except ValueError:
923 # Not a number. Assume entire data series is string data.
924 return "str"
926 # The series data was all numeric.
927 return "num"
929 def _get_data_id(self, formula, data):
930 # Assign an id to a each unique series formula or title/axis formula.
931 # Repeated formulas such as for categories get the same id. If the
932 # series or title has user specified data associated with it then
933 # that is also stored. This data is used to populate cached Excel
934 # data when creating a chart. If there is no user defined data then
935 # it will be populated by the parent Workbook._add_chart_data().
937 # Ignore series without a range formula.
938 if not formula:
939 return None
941 # Strip the leading '=' from the formula.
942 if formula.startswith("="):
943 formula = formula.lstrip("=")
945 # Store the data id in a hash keyed by the formula and store the data
946 # in a separate array with the same id.
947 if formula not in self.formula_ids:
948 # Haven't seen this formula before.
949 formula_id = len(self.formula_data)
951 self.formula_data.append(data)
952 self.formula_ids[formula] = formula_id
953 else:
954 # Formula already seen. Return existing id.
955 formula_id = self.formula_ids[formula]
957 # Store user defined data if it isn't already there.
958 if self.formula_data[formula_id] is None:
959 self.formula_data[formula_id] = data
961 return formula_id
963 def _get_marker_properties(self, marker):
964 # Convert user marker properties to the structure required internally.
966 if not marker:
967 return None
969 # Copy the user defined properties since they will be modified.
970 marker = copy.deepcopy(marker)
972 types = {
973 "automatic": "automatic",
974 "none": "none",
975 "square": "square",
976 "diamond": "diamond",
977 "triangle": "triangle",
978 "x": "x",
979 "star": "star",
980 "dot": "dot",
981 "short_dash": "dot",
982 "dash": "dash",
983 "long_dash": "dash",
984 "circle": "circle",
985 "plus": "plus",
986 "picture": "picture",
987 }
989 # Check for valid types.
990 marker_type = marker.get("type")
992 if marker_type is not None:
993 if marker_type in types:
994 marker["type"] = types[marker_type]
995 else:
996 warn(f"Unknown marker type '{marker_type}")
997 return None
999 # Set the line properties for the marker.
1000 line = Shape._get_line_properties(marker)
1002 # Set the fill properties for the marker.
1003 fill = Shape._get_fill_properties(marker.get("fill"))
1005 # Set the pattern fill properties for the series.
1006 pattern = Shape._get_pattern_properties(marker.get("pattern"))
1008 # Set the gradient fill properties for the series.
1009 gradient = Shape._get_gradient_properties(marker.get("gradient"))
1011 # Pattern fill overrides solid fill.
1012 if pattern:
1013 self.fill = None
1015 # Gradient fill overrides the solid and pattern fill.
1016 if gradient:
1017 pattern = None
1018 fill = None
1020 marker["line"] = line
1021 marker["fill"] = fill
1022 marker["pattern"] = pattern
1023 marker["gradient"] = gradient
1025 return marker
1027 def _get_trendline_properties(self, trendline):
1028 # Convert user trendline properties to structure required internally.
1030 if not trendline:
1031 return None
1033 # Copy the user defined properties since they will be modified.
1034 trendline = copy.deepcopy(trendline)
1036 types = {
1037 "exponential": "exp",
1038 "linear": "linear",
1039 "log": "log",
1040 "moving_average": "movingAvg",
1041 "polynomial": "poly",
1042 "power": "power",
1043 }
1045 # Check the trendline type.
1046 trend_type = trendline.get("type")
1048 if trend_type in types:
1049 trendline["type"] = types[trend_type]
1050 else:
1051 warn(f"Unknown trendline type '{trend_type}'")
1052 return None
1054 # Set the line properties for the trendline.
1055 line = Shape._get_line_properties(trendline)
1057 # Set the fill properties for the trendline.
1058 fill = Shape._get_fill_properties(trendline.get("fill"))
1060 # Set the pattern fill properties for the trendline.
1061 pattern = Shape._get_pattern_properties(trendline.get("pattern"))
1063 # Set the gradient fill properties for the trendline.
1064 gradient = Shape._get_gradient_properties(trendline.get("gradient"))
1066 # Set the format properties for the trendline label.
1067 label = self._get_trendline_label_properties(trendline.get("label"))
1069 # Pattern fill overrides solid fill.
1070 if pattern:
1071 self.fill = None
1073 # Gradient fill overrides the solid and pattern fill.
1074 if gradient:
1075 pattern = None
1076 fill = None
1078 trendline["line"] = line
1079 trendline["fill"] = fill
1080 trendline["pattern"] = pattern
1081 trendline["gradient"] = gradient
1082 trendline["label"] = label
1084 return trendline
1086 def _get_trendline_label_properties(self, label):
1087 # Convert user trendline properties to structure required internally.
1089 if not label:
1090 return {}
1092 # Copy the user defined properties since they will be modified.
1093 label = copy.deepcopy(label)
1095 # Set the font properties if present.
1096 font = self._convert_font_args(label.get("font"))
1098 # Set the line properties for the label.
1099 line = Shape._get_line_properties(label)
1101 # Set the fill properties for the label.
1102 fill = Shape._get_fill_properties(label.get("fill"))
1104 # Set the pattern fill properties for the label.
1105 pattern = Shape._get_pattern_properties(label.get("pattern"))
1107 # Set the gradient fill properties for the label.
1108 gradient = Shape._get_gradient_properties(label.get("gradient"))
1110 # Pattern fill overrides solid fill.
1111 if pattern:
1112 self.fill = None
1114 # Gradient fill overrides the solid and pattern fill.
1115 if gradient:
1116 pattern = None
1117 fill = None
1119 label["font"] = font
1120 label["line"] = line
1121 label["fill"] = fill
1122 label["pattern"] = pattern
1123 label["gradient"] = gradient
1125 return label
1127 def _get_error_bars_props(self, options):
1128 # Convert user error bars properties to structure required internally.
1129 if not options:
1130 return {}
1132 # Default values.
1133 error_bars = {"type": "fixedVal", "value": 1, "endcap": 1, "direction": "both"}
1135 types = {
1136 "fixed": "fixedVal",
1137 "percentage": "percentage",
1138 "standard_deviation": "stdDev",
1139 "standard_error": "stdErr",
1140 "custom": "cust",
1141 }
1143 # Check the error bars type.
1144 error_type = options["type"]
1146 if error_type in types:
1147 error_bars["type"] = types[error_type]
1148 else:
1149 warn(f"Unknown error bars type '{error_type}")
1150 return {}
1152 # Set the value for error types that require it.
1153 if "value" in options:
1154 error_bars["value"] = options["value"]
1156 # Set the end-cap style.
1157 if "end_style" in options:
1158 error_bars["endcap"] = options["end_style"]
1160 # Set the error bar direction.
1161 if "direction" in options:
1162 if options["direction"] == "minus":
1163 error_bars["direction"] = "minus"
1164 elif options["direction"] == "plus":
1165 error_bars["direction"] = "plus"
1166 else:
1167 # Default to 'both'.
1168 pass
1170 # Set any custom values.
1171 error_bars["plus_values"] = options.get("plus_values")
1172 error_bars["minus_values"] = options.get("minus_values")
1173 error_bars["plus_data"] = options.get("plus_data")
1174 error_bars["minus_data"] = options.get("minus_data")
1176 # Set the line properties for the error bars.
1177 error_bars["line"] = Shape._get_line_properties(options)
1179 return error_bars
1181 def _get_gridline_properties(self, options):
1182 # Convert user gridline properties to structure required internally.
1184 # Set the visible property for the gridline.
1185 gridline = {"visible": options.get("visible")}
1187 # Set the line properties for the gridline.
1188 gridline["line"] = Shape._get_line_properties(options)
1190 return gridline
1192 def _get_labels_properties(self, labels):
1193 # Convert user labels properties to the structure required internally.
1195 if not labels:
1196 return None
1198 # Copy the user defined properties since they will be modified.
1199 labels = copy.deepcopy(labels)
1201 # Map user defined label positions to Excel positions.
1202 position = labels.get("position")
1204 if position:
1205 if position in self.label_positions:
1206 if position == self.label_position_default:
1207 labels["position"] = None
1208 else:
1209 labels["position"] = self.label_positions[position]
1210 else:
1211 warn(f"Unsupported label position '{position}' for this chart type")
1212 return None
1214 # Map the user defined label separator to the Excel separator.
1215 separator = labels.get("separator")
1216 separators = {
1217 ",": ", ",
1218 ";": "; ",
1219 ".": ". ",
1220 "\n": "\n",
1221 " ": " ",
1222 }
1224 if separator:
1225 if separator in separators:
1226 labels["separator"] = separators[separator]
1227 else:
1228 warn("Unsupported label separator")
1229 return None
1231 # Set the font properties if present.
1232 labels["font"] = self._convert_font_args(labels.get("font"))
1234 # Set the line properties for the labels.
1235 line = Shape._get_line_properties(labels)
1237 # Set the fill properties for the labels.
1238 fill = Shape._get_fill_properties(labels.get("fill"))
1240 # Set the pattern fill properties for the labels.
1241 pattern = Shape._get_pattern_properties(labels.get("pattern"))
1243 # Set the gradient fill properties for the labels.
1244 gradient = Shape._get_gradient_properties(labels.get("gradient"))
1246 # Pattern fill overrides solid fill.
1247 if pattern:
1248 self.fill = None
1250 # Gradient fill overrides the solid and pattern fill.
1251 if gradient:
1252 pattern = None
1253 fill = None
1255 labels["line"] = line
1256 labels["fill"] = fill
1257 labels["pattern"] = pattern
1258 labels["gradient"] = gradient
1260 if labels.get("custom"):
1261 for label in labels["custom"]:
1262 if label is None:
1263 continue
1265 value = label.get("value")
1266 if value and re.match(r"^=?[^!]+!\$?[A-Z]+\$?\d+", str(value)):
1267 label["formula"] = value
1269 formula = label.get("formula")
1270 if formula and formula.startswith("="):
1271 label["formula"] = formula.lstrip("=")
1273 data_id = self._get_data_id(formula, label.get("data"))
1274 label["data_id"] = data_id
1276 label["font"] = self._convert_font_args(label.get("font"))
1278 # Set the line properties for the label.
1279 line = Shape._get_line_properties(label)
1281 # Set the fill properties for the label.
1282 fill = Shape._get_fill_properties(label.get("fill"))
1284 # Set the pattern fill properties for the label.
1285 pattern = Shape._get_pattern_properties(label.get("pattern"))
1287 # Set the gradient fill properties for the label.
1288 gradient = Shape._get_gradient_properties(label.get("gradient"))
1290 # Pattern fill overrides solid fill.
1291 if pattern:
1292 self.fill = None
1294 # Gradient fill overrides the solid and pattern fill.
1295 if gradient:
1296 pattern = None
1297 fill = None
1299 # Map user defined label positions to Excel positions.
1300 position = label.get("position")
1302 if position:
1303 if position in self.label_positions:
1304 if position == self.label_position_default:
1305 label["position"] = None
1306 else:
1307 label["position"] = self.label_positions[position]
1308 else:
1309 warn(f"Unsupported label position '{position}' for chart type")
1310 return None
1312 label["line"] = line
1313 label["fill"] = fill
1314 label["pattern"] = pattern
1315 label["gradient"] = gradient
1317 return labels
1319 def _get_area_properties(self, options):
1320 # Convert user area properties to the structure required internally.
1321 area = {}
1323 # Set the line properties for the chartarea.
1324 line = Shape._get_line_properties(options)
1326 # Set the fill properties for the chartarea.
1327 fill = Shape._get_fill_properties(options.get("fill"))
1329 # Set the pattern fill properties for the series.
1330 pattern = Shape._get_pattern_properties(options.get("pattern"))
1332 # Set the gradient fill properties for the series.
1333 gradient = Shape._get_gradient_properties(options.get("gradient"))
1335 # Pattern fill overrides solid fill.
1336 if pattern:
1337 self.fill = None
1339 # Gradient fill overrides the solid and pattern fill.
1340 if gradient:
1341 pattern = None
1342 fill = None
1344 # Set the plotarea layout.
1345 layout = self._get_layout_properties(options.get("layout"), False)
1347 area["line"] = line
1348 area["fill"] = fill
1349 area["pattern"] = pattern
1350 area["layout"] = layout
1351 area["gradient"] = gradient
1353 return area
1355 def _get_legend_properties(self, options: Optional[Dict[str, Any]] = None):
1356 # Convert user legend properties to the structure required internally.
1357 legend = {}
1359 if options is None:
1360 options = {}
1362 legend["position"] = options.get("position", "right")
1363 legend["delete_series"] = options.get("delete_series")
1364 legend["font"] = self._convert_font_args(options.get("font"))
1365 legend["layout"] = self._get_layout_properties(options.get("layout"), False)
1367 # Turn off the legend.
1368 if options.get("none"):
1369 legend["position"] = "none"
1371 # Set the line properties for the legend.
1372 line = Shape._get_line_properties(options)
1374 # Set the fill properties for the legend.
1375 fill = Shape._get_fill_properties(options.get("fill"))
1377 # Set the pattern fill properties for the series.
1378 pattern = Shape._get_pattern_properties(options.get("pattern"))
1380 # Set the gradient fill properties for the series.
1381 gradient = Shape._get_gradient_properties(options.get("gradient"))
1383 # Pattern fill overrides solid fill.
1384 if pattern:
1385 self.fill = None
1387 # Gradient fill overrides the solid and pattern fill.
1388 if gradient:
1389 pattern = None
1390 fill = None
1392 # Set the legend layout.
1393 layout = self._get_layout_properties(options.get("layout"), False)
1395 legend["line"] = line
1396 legend["fill"] = fill
1397 legend["pattern"] = pattern
1398 legend["layout"] = layout
1399 legend["gradient"] = gradient
1401 return legend
1403 def _get_layout_properties(self, args, is_text):
1404 # Convert user defined layout properties to format used internally.
1405 layout = {}
1407 if not args:
1408 return {}
1410 if is_text:
1411 properties = ("x", "y")
1412 else:
1413 properties = ("x", "y", "width", "height")
1415 # Check for valid properties.
1416 for key in args.keys():
1417 if key not in properties:
1418 warn(f"Property '{key}' not supported in layout options")
1419 return {}
1421 # Set the layout properties.
1422 for prop in properties:
1423 if prop not in args.keys():
1424 warn(f"Property '{prop}' must be specified in layout options")
1425 return {}
1427 value = args[prop]
1429 try:
1430 float(value)
1431 except ValueError:
1432 warn(f"Property '{prop}' value '{value}' must be numeric in layout")
1433 return {}
1435 if value < 0 or value > 1:
1436 warn(
1437 f"Property '{prop}' value '{value}' must be in range "
1438 f"0 < x <= 1 in layout options"
1439 )
1440 return {}
1442 # Convert to the format used by Excel for easier testing
1443 layout[prop] = f"{value:.17g}"
1445 return layout
1447 def _get_points_properties(self, user_points):
1448 # Convert user points properties to structure required internally.
1449 points = []
1451 if not user_points:
1452 return []
1454 for user_point in user_points:
1455 point = {}
1457 if user_point is not None:
1458 # Set the line properties for the point.
1459 line = Shape._get_line_properties(user_point)
1461 # Set the fill properties for the chartarea.
1462 fill = Shape._get_fill_properties(user_point.get("fill"))
1464 # Set the pattern fill properties for the series.
1465 pattern = Shape._get_pattern_properties(user_point.get("pattern"))
1467 # Set the gradient fill properties for the series.
1468 gradient = Shape._get_gradient_properties(user_point.get("gradient"))
1470 # Pattern fill overrides solid fill.
1471 if pattern:
1472 self.fill = None
1474 # Gradient fill overrides the solid and pattern fill.
1475 if gradient:
1476 pattern = None
1477 fill = None
1479 point["line"] = line
1480 point["fill"] = fill
1481 point["pattern"] = pattern
1482 point["gradient"] = gradient
1484 points.append(point)
1486 return points
1488 def _has_formatting(self, element: dict) -> bool:
1489 # Check if a chart element has line, fill or gradient formatting.
1490 has_fill = element.get("fill") and element["fill"]["defined"]
1491 has_line = element.get("line") and element["line"]["defined"]
1492 has_pattern = element.get("pattern")
1493 has_gradient = element.get("gradient")
1495 return has_fill or has_line or has_pattern or has_gradient
1497 def _get_display_units(self, display_units):
1498 # Convert user defined display units to internal units.
1499 if not display_units:
1500 return None
1502 types = {
1503 "hundreds": "hundreds",
1504 "thousands": "thousands",
1505 "ten_thousands": "tenThousands",
1506 "hundred_thousands": "hundredThousands",
1507 "millions": "millions",
1508 "ten_millions": "tenMillions",
1509 "hundred_millions": "hundredMillions",
1510 "billions": "billions",
1511 "trillions": "trillions",
1512 }
1514 if display_units in types:
1515 display_units = types[display_units]
1516 else:
1517 warn(f"Unknown display_units type '{display_units}'")
1518 return None
1520 return display_units
1522 def _get_tick_type(self, tick_type):
1523 # Convert user defined display units to internal units.
1524 if not tick_type:
1525 return None
1527 types = {
1528 "outside": "out",
1529 "inside": "in",
1530 "none": "none",
1531 "cross": "cross",
1532 }
1534 if tick_type in types:
1535 tick_type = types[tick_type]
1536 else:
1537 warn(f"Unknown tick_type '{tick_type}'")
1538 return None
1540 return tick_type
1542 def _get_primary_axes_series(self):
1543 # Returns series which use the primary axes.
1544 primary_axes_series = []
1546 for series in self.series:
1547 if not series["y2_axis"]:
1548 primary_axes_series.append(series)
1550 return primary_axes_series
1552 def _get_secondary_axes_series(self):
1553 # Returns series which use the secondary axes.
1554 secondary_axes_series = []
1556 for series in self.series:
1557 if series["y2_axis"]:
1558 secondary_axes_series.append(series)
1560 return secondary_axes_series
1562 def _add_axis_ids(self, args) -> None:
1563 # Add unique ids for primary or secondary axes
1564 chart_id = 5001 + int(self.id)
1565 axis_count = 1 + len(self.axis2_ids) + len(self.axis_ids)
1567 id1 = f"{chart_id:04d}{axis_count:04d}"
1568 id2 = f"{chart_id:04d}{axis_count + 1:04d}"
1570 if args["primary_axes"]:
1571 self.axis_ids.append(id1)
1572 self.axis_ids.append(id2)
1574 if not args["primary_axes"]:
1575 self.axis2_ids.append(id1)
1576 self.axis2_ids.append(id2)
1578 def _set_default_properties(self) -> None:
1579 # Setup the default properties for a chart.
1581 self.x_axis["defaults"] = {
1582 "num_format": "General",
1583 "major_gridlines": {"visible": 0},
1584 }
1586 self.y_axis["defaults"] = {
1587 "num_format": "General",
1588 "major_gridlines": {"visible": 1},
1589 }
1591 self.x2_axis["defaults"] = {
1592 "num_format": "General",
1593 "label_position": "none",
1594 "crossing": "max",
1595 "visible": 0,
1596 }
1598 self.y2_axis["defaults"] = {
1599 "num_format": "General",
1600 "major_gridlines": {"visible": 0},
1601 "position": "right",
1602 "visible": 1,
1603 }
1605 self.set_x_axis({})
1606 self.set_y_axis({})
1608 self.set_x2_axis({})
1609 self.set_y2_axis({})
1611 ###########################################################################
1612 #
1613 # XML methods.
1614 #
1615 ###########################################################################
1617 def _write_chart_space(self) -> None:
1618 # Write the <c:chartSpace> element.
1619 schema = "http://schemas.openxmlformats.org/"
1620 xmlns_c = schema + "drawingml/2006/chart"
1621 xmlns_a = schema + "drawingml/2006/main"
1622 xmlns_r = schema + "officeDocument/2006/relationships"
1624 attributes = [
1625 ("xmlns:c", xmlns_c),
1626 ("xmlns:a", xmlns_a),
1627 ("xmlns:r", xmlns_r),
1628 ]
1630 self._xml_start_tag("c:chartSpace", attributes)
1632 def _write_lang(self) -> None:
1633 # Write the <c:lang> element.
1634 val = "en-US"
1636 attributes = [("val", val)]
1638 self._xml_empty_tag("c:lang", attributes)
1640 def _write_style(self) -> None:
1641 # Write the <c:style> element.
1642 style_id = self.style_id
1644 # Don't write an element for the default style, 2.
1645 if style_id == 2:
1646 return
1648 attributes = [("val", style_id)]
1650 self._xml_empty_tag("c:style", attributes)
1652 def _write_chart(self) -> None:
1653 # Write the <c:chart> element.
1654 self._xml_start_tag("c:chart")
1656 if self.title.is_hidden():
1657 # Turn off the title.
1658 self._write_c_auto_title_deleted()
1659 else:
1660 # Write the chart title elements.
1661 self._write_title(self.title)
1663 # Write the c:plotArea element.
1664 self._write_plot_area()
1666 # Write the c:legend element.
1667 self._write_legend()
1669 # Write the c:plotVisOnly element.
1670 self._write_plot_vis_only()
1672 # Write the c:dispBlanksAs element.
1673 self._write_disp_blanks_as()
1675 # Write the c:extLst element.
1676 if self.show_na_as_empty:
1677 self._write_c_ext_lst_display_na()
1679 self._xml_end_tag("c:chart")
1681 def _write_disp_blanks_as(self) -> None:
1682 # Write the <c:dispBlanksAs> element.
1683 val = self.show_blanks
1685 # Ignore the default value.
1686 if val == "gap":
1687 return
1689 attributes = [("val", val)]
1691 self._xml_empty_tag("c:dispBlanksAs", attributes)
1693 def _write_plot_area(self) -> None:
1694 # Write the <c:plotArea> element.
1695 self._xml_start_tag("c:plotArea")
1697 # Write the c:layout element.
1698 self._write_layout(self.plotarea.get("layout"), "plot")
1700 # Write subclass chart type elements for primary and secondary axes.
1701 self._write_chart_type({"primary_axes": True})
1702 self._write_chart_type({"primary_axes": False})
1704 # Configure a combined chart if present.
1705 second_chart = self.combined
1706 if second_chart:
1707 # Secondary axis has unique id otherwise use same as primary.
1708 if second_chart.is_secondary:
1709 second_chart.id = 1000 + self.id
1710 else:
1711 second_chart.id = self.id
1713 # Share the same filehandle for writing.
1714 second_chart.fh = self.fh
1716 # Share series index with primary chart.
1717 second_chart.series_index = self.series_index
1719 # Write the subclass chart type elements for combined chart.
1720 second_chart._write_chart_type({"primary_axes": True})
1721 second_chart._write_chart_type({"primary_axes": False})
1723 # Write the category and value elements for the primary axes.
1724 args = {"x_axis": self.x_axis, "y_axis": self.y_axis, "axis_ids": self.axis_ids}
1726 if self.date_category:
1727 self._write_date_axis(args)
1728 else:
1729 self._write_cat_axis(args)
1731 self._write_val_axis(args)
1733 # Write the category and value elements for the secondary axes.
1734 args = {
1735 "x_axis": self.x2_axis,
1736 "y_axis": self.y2_axis,
1737 "axis_ids": self.axis2_ids,
1738 }
1740 self._write_val_axis(args)
1742 # Write the secondary axis for the secondary chart.
1743 if second_chart and second_chart.is_secondary:
1744 args = {
1745 "x_axis": second_chart.x2_axis,
1746 "y_axis": second_chart.y2_axis,
1747 "axis_ids": second_chart.axis2_ids,
1748 }
1750 second_chart._write_val_axis(args)
1752 if self.date_category:
1753 self._write_date_axis(args)
1754 else:
1755 self._write_cat_axis(args)
1757 # Write the c:dTable element.
1758 self._write_d_table()
1760 # Write the c:spPr element for the plotarea formatting.
1761 self._write_sp_pr(self.plotarea)
1763 self._xml_end_tag("c:plotArea")
1765 def _write_layout(self, layout, layout_type) -> None:
1766 # Write the <c:layout> element.
1768 if not layout:
1769 # Automatic layout.
1770 self._xml_empty_tag("c:layout")
1771 else:
1772 # User defined manual layout.
1773 self._xml_start_tag("c:layout")
1774 self._write_manual_layout(layout, layout_type)
1775 self._xml_end_tag("c:layout")
1777 def _write_manual_layout(self, layout, layout_type) -> None:
1778 # Write the <c:manualLayout> element.
1779 self._xml_start_tag("c:manualLayout")
1781 # Plotarea has a layoutTarget element.
1782 if layout_type == "plot":
1783 self._xml_empty_tag("c:layoutTarget", [("val", "inner")])
1785 # Set the x, y positions.
1786 self._xml_empty_tag("c:xMode", [("val", "edge")])
1787 self._xml_empty_tag("c:yMode", [("val", "edge")])
1788 self._xml_empty_tag("c:x", [("val", layout["x"])])
1789 self._xml_empty_tag("c:y", [("val", layout["y"])])
1791 # For plotarea and legend set the width and height.
1792 if layout_type != "text":
1793 self._xml_empty_tag("c:w", [("val", layout["width"])])
1794 self._xml_empty_tag("c:h", [("val", layout["height"])])
1796 self._xml_end_tag("c:manualLayout")
1798 def _write_chart_type(self, args) -> None:
1799 # pylint: disable=unused-argument
1800 # Write the chart type element. This method should be overridden
1801 # by the subclasses.
1802 return
1804 def _write_grouping(self, val) -> None:
1805 # Write the <c:grouping> element.
1806 attributes = [("val", val)]
1808 self._xml_empty_tag("c:grouping", attributes)
1810 def _write_series(self, series) -> None:
1811 # Write the series elements.
1812 self._write_ser(series)
1814 def _write_ser(self, series) -> None:
1815 # Write the <c:ser> element.
1816 index = self.series_index
1817 self.series_index += 1
1819 self._xml_start_tag("c:ser")
1821 # Write the c:idx element.
1822 self._write_idx(index)
1824 # Write the c:order element.
1825 self._write_order(index)
1827 # Write the series name.
1828 self._write_series_name(series)
1830 # Write the c:spPr element.
1831 self._write_sp_pr(series)
1833 # Write the c:marker element.
1834 self._write_marker(series["marker"])
1836 # Write the c:invertIfNegative element.
1837 self._write_c_invert_if_negative(series["invert_if_neg"])
1839 # Write the c:dPt element.
1840 self._write_d_pt(series["points"])
1842 # Write the c:dLbls element.
1843 self._write_d_lbls(series["labels"])
1845 # Write the c:trendline element.
1846 self._write_trendline(series["trendline"])
1848 # Write the c:errBars element.
1849 self._write_error_bars(series["error_bars"])
1851 # Write the c:cat element.
1852 self._write_cat(series)
1854 # Write the c:val element.
1855 self._write_val(series)
1857 # Write the c:smooth element.
1858 if self.smooth_allowed:
1859 self._write_c_smooth(series["smooth"])
1861 # Write the c:extLst element.
1862 if series.get("inverted_color"):
1863 self._write_c_ext_lst_inverted_color(series["inverted_color"])
1865 self._xml_end_tag("c:ser")
1867 def _write_c_ext_lst_inverted_color(self, color: Color) -> None:
1868 # Write the <c:extLst> element for the inverted fill color.
1870 uri = "{6F2FDCE9-48DA-4B69-8628-5D25D57E5C99}"
1871 xmlns_c_14 = "http://schemas.microsoft.com/office/drawing/2007/8/2/chart"
1873 attributes1 = [
1874 ("uri", uri),
1875 ("xmlns:c14", xmlns_c_14),
1876 ]
1878 attributes2 = [("xmlns:c14", xmlns_c_14)]
1880 self._xml_start_tag("c:extLst")
1881 self._xml_start_tag("c:ext", attributes1)
1882 self._xml_start_tag("c14:invertSolidFillFmt")
1883 self._xml_start_tag("c14:spPr", attributes2)
1885 self._write_a_solid_fill({"color": color})
1887 self._xml_end_tag("c14:spPr")
1888 self._xml_end_tag("c14:invertSolidFillFmt")
1889 self._xml_end_tag("c:ext")
1890 self._xml_end_tag("c:extLst")
1892 def _write_c_ext_lst_display_na(self) -> None:
1893 # Write the <c:extLst> element for the display NA as empty cell option.
1895 uri = "{56B9EC1D-385E-4148-901F-78D8002777C0}"
1896 xmlns_c_16 = "http://schemas.microsoft.com/office/drawing/2017/03/chart"
1898 attributes1 = [
1899 ("uri", uri),
1900 ("xmlns:c16r3", xmlns_c_16),
1901 ]
1903 attributes2 = [("val", 1)]
1905 self._xml_start_tag("c:extLst")
1906 self._xml_start_tag("c:ext", attributes1)
1907 self._xml_start_tag("c16r3:dataDisplayOptions16")
1908 self._xml_empty_tag("c16r3:dispNaAsBlank", attributes2)
1909 self._xml_end_tag("c16r3:dataDisplayOptions16")
1910 self._xml_end_tag("c:ext")
1911 self._xml_end_tag("c:extLst")
1913 def _write_idx(self, val) -> None:
1914 # Write the <c:idx> element.
1916 attributes = [("val", val)]
1918 self._xml_empty_tag("c:idx", attributes)
1920 def _write_order(self, val) -> None:
1921 # Write the <c:order> element.
1923 attributes = [("val", val)]
1925 self._xml_empty_tag("c:order", attributes)
1927 def _write_series_name(self, series) -> None:
1928 # Write the series name.
1930 if series["name_formula"] is not None:
1931 self._write_tx_formula(series["name_formula"], series["name_id"])
1932 elif series["name"] is not None:
1933 self._write_tx_value(series["name"])
1935 def _write_c_smooth(self, smooth) -> None:
1936 # Write the <c:smooth> element.
1938 if smooth:
1939 self._xml_empty_tag("c:smooth", [("val", "1")])
1941 def _write_cat(self, series) -> None:
1942 # Write the <c:cat> element.
1943 formula = series["categories"]
1944 data_id = series["cat_data_id"]
1945 data = None
1947 if data_id is not None:
1948 data = self.formula_data[data_id]
1950 # Ignore <c:cat> elements for charts without category values.
1951 if not formula:
1952 return
1954 self._xml_start_tag("c:cat")
1956 # Check the type of cached data.
1957 cat_type = self._get_data_type(data)
1959 if cat_type == "str":
1960 self.cat_has_num_fmt = False
1961 # Write the c:numRef element.
1962 self._write_str_ref(formula, data, cat_type)
1964 elif cat_type == "multi_str":
1965 self.cat_has_num_fmt = False
1966 # Write the c:numRef element.
1967 self._write_multi_lvl_str_ref(formula, data)
1969 else:
1970 self.cat_has_num_fmt = True
1971 # Write the c:numRef element.
1972 self._write_num_ref(formula, data, cat_type)
1974 self._xml_end_tag("c:cat")
1976 def _write_val(self, series) -> None:
1977 # Write the <c:val> element.
1978 formula = series["values"]
1979 data_id = series["val_data_id"]
1980 data = self.formula_data[data_id]
1982 self._xml_start_tag("c:val")
1984 # Unlike Cat axes data should only be numeric.
1985 # Write the c:numRef element.
1986 self._write_num_ref(formula, data, "num")
1988 self._xml_end_tag("c:val")
1990 def _write_num_ref(self, formula, data, ref_type) -> None:
1991 # Write the <c:numRef> element.
1992 self._xml_start_tag("c:numRef")
1994 # Write the c:f element.
1995 self._write_series_formula(formula)
1997 if ref_type == "num":
1998 # Write the c:numCache element.
1999 self._write_num_cache(data)
2000 elif ref_type == "str":
2001 # Write the c:strCache element.
2002 self._write_str_cache(data)
2004 self._xml_end_tag("c:numRef")
2006 def _write_str_ref(self, formula, data, ref_type) -> None:
2007 # Write the <c:strRef> element.
2009 self._xml_start_tag("c:strRef")
2011 # Write the c:f element.
2012 self._write_series_formula(formula)
2014 if ref_type == "num":
2015 # Write the c:numCache element.
2016 self._write_num_cache(data)
2017 elif ref_type == "str":
2018 # Write the c:strCache element.
2019 self._write_str_cache(data)
2021 self._xml_end_tag("c:strRef")
2023 def _write_multi_lvl_str_ref(self, formula, data) -> None:
2024 # Write the <c:multiLvlStrRef> element.
2026 if not data:
2027 return
2029 self._xml_start_tag("c:multiLvlStrRef")
2031 # Write the c:f element.
2032 self._write_series_formula(formula)
2034 self._xml_start_tag("c:multiLvlStrCache")
2036 # Write the c:ptCount element.
2037 count = len(data[-1])
2038 self._write_pt_count(count)
2040 for cat_data in reversed(data):
2041 self._xml_start_tag("c:lvl")
2043 for i, point in enumerate(cat_data):
2044 # Write the c:pt element.
2045 self._write_pt(i, point)
2047 self._xml_end_tag("c:lvl")
2049 self._xml_end_tag("c:multiLvlStrCache")
2050 self._xml_end_tag("c:multiLvlStrRef")
2052 def _write_series_formula(self, formula) -> None:
2053 # Write the <c:f> element.
2055 # Strip the leading '=' from the formula.
2056 if formula.startswith("="):
2057 formula = formula.lstrip("=")
2059 self._xml_data_element("c:f", formula)
2061 def _write_axis_ids(self, args) -> None:
2062 # Write the <c:axId> elements for the primary or secondary axes.
2064 # Generate the axis ids.
2065 self._add_axis_ids(args)
2067 if args["primary_axes"]:
2068 # Write the axis ids for the primary axes.
2069 self._write_axis_id(self.axis_ids[0])
2070 self._write_axis_id(self.axis_ids[1])
2071 else:
2072 # Write the axis ids for the secondary axes.
2073 self._write_axis_id(self.axis2_ids[0])
2074 self._write_axis_id(self.axis2_ids[1])
2076 def _write_axis_id(self, val) -> None:
2077 # Write the <c:axId> element.
2079 attributes = [("val", val)]
2081 self._xml_empty_tag("c:axId", attributes)
2083 def _write_cat_axis(self, args) -> None:
2084 # Write the <c:catAx> element. Usually the X axis.
2085 x_axis = args["x_axis"]
2086 y_axis = args["y_axis"]
2087 axis_ids = args["axis_ids"]
2089 # If there are no axis_ids then we don't need to write this element.
2090 if axis_ids is None or not axis_ids:
2091 return
2093 position = self.cat_axis_position
2094 is_horizontal = self.horiz_cat_axis
2096 # Overwrite the default axis position with a user supplied value.
2097 if x_axis.get("position"):
2098 position = x_axis["position"]
2100 self._xml_start_tag("c:catAx")
2102 self._write_axis_id(axis_ids[0])
2104 # Write the c:scaling element.
2105 self._write_scaling(x_axis.get("reverse"), None, None, None)
2107 if not x_axis.get("visible"):
2108 self._write_delete(1)
2110 # Write the c:axPos element.
2111 self._write_axis_pos(position, y_axis.get("reverse"))
2113 # Write the c:majorGridlines element.
2114 self._write_major_gridlines(x_axis.get("major_gridlines"))
2116 # Write the c:minorGridlines element.
2117 self._write_minor_gridlines(x_axis.get("minor_gridlines"))
2119 # Write the axis title elements.
2120 self._write_title(x_axis["title"], is_horizontal)
2122 # Write the c:numFmt element.
2123 self._write_cat_number_format(x_axis)
2125 # Write the c:majorTickMark element.
2126 self._write_major_tick_mark(x_axis.get("major_tick_mark"))
2128 # Write the c:minorTickMark element.
2129 self._write_minor_tick_mark(x_axis.get("minor_tick_mark"))
2131 # Write the c:tickLblPos element.
2132 self._write_tick_label_pos(x_axis.get("label_position"))
2134 # Write the c:spPr element for the axis line.
2135 self._write_sp_pr(x_axis)
2137 # Write the axis font elements.
2138 self._write_axis_font(x_axis.get("num_font"))
2140 # Write the c:crossAx element.
2141 self._write_cross_axis(axis_ids[1])
2143 if self.show_crosses or x_axis.get("visible"):
2144 # Note, the category crossing comes from the value axis.
2145 if (
2146 y_axis.get("crossing") is None
2147 or y_axis.get("crossing") == "max"
2148 or y_axis["crossing"] == "min"
2149 ):
2150 # Write the c:crosses element.
2151 self._write_crosses(y_axis.get("crossing"))
2152 else:
2153 # Write the c:crossesAt element.
2154 self._write_c_crosses_at(y_axis.get("crossing"))
2156 # Write the c:auto element.
2157 if not x_axis.get("text_axis"):
2158 self._write_auto(1)
2160 # Write the c:labelAlign element.
2161 self._write_label_align(x_axis.get("label_align"))
2163 # Write the c:labelOffset element.
2164 self._write_label_offset(100)
2166 # Write the c:tickLblSkip element.
2167 self._write_c_tick_lbl_skip(x_axis.get("interval_unit"))
2169 # Write the c:tickMarkSkip element.
2170 self._write_c_tick_mark_skip(x_axis.get("interval_tick"))
2172 self._xml_end_tag("c:catAx")
2174 def _write_val_axis(self, args) -> None:
2175 # Write the <c:valAx> element. Usually the Y axis.
2176 x_axis = args["x_axis"]
2177 y_axis = args["y_axis"]
2178 axis_ids = args["axis_ids"]
2179 position = args.get("position", self.val_axis_position)
2180 is_horizontal = self.horiz_val_axis
2182 # If there are no axis_ids then we don't need to write this element.
2183 if axis_ids is None or not axis_ids:
2184 return
2186 # Overwrite the default axis position with a user supplied value.
2187 position = y_axis.get("position") or position
2189 self._xml_start_tag("c:valAx")
2191 self._write_axis_id(axis_ids[1])
2193 # Write the c:scaling element.
2194 self._write_scaling(
2195 y_axis.get("reverse"),
2196 y_axis.get("min"),
2197 y_axis.get("max"),
2198 y_axis.get("log_base"),
2199 )
2201 if not y_axis.get("visible"):
2202 self._write_delete(1)
2204 # Write the c:axPos element.
2205 self._write_axis_pos(position, x_axis.get("reverse"))
2207 # Write the c:majorGridlines element.
2208 self._write_major_gridlines(y_axis.get("major_gridlines"))
2210 # Write the c:minorGridlines element.
2211 self._write_minor_gridlines(y_axis.get("minor_gridlines"))
2213 # Write the axis title elements.
2214 self._write_title(y_axis["title"], is_horizontal)
2216 # Write the c:numberFormat element.
2217 self._write_number_format(y_axis)
2219 # Write the c:majorTickMark element.
2220 self._write_major_tick_mark(y_axis.get("major_tick_mark"))
2222 # Write the c:minorTickMark element.
2223 self._write_minor_tick_mark(y_axis.get("minor_tick_mark"))
2225 # Write the c:tickLblPos element.
2226 self._write_tick_label_pos(y_axis.get("label_position"))
2228 # Write the c:spPr element for the axis line.
2229 self._write_sp_pr(y_axis)
2231 # Write the axis font elements.
2232 self._write_axis_font(y_axis.get("num_font"))
2234 # Write the c:crossAx element.
2235 self._write_cross_axis(axis_ids[0])
2237 # Note, the category crossing comes from the value axis.
2238 if (
2239 x_axis.get("crossing") is None
2240 or x_axis["crossing"] == "max"
2241 or x_axis["crossing"] == "min"
2242 ):
2243 # Write the c:crosses element.
2244 self._write_crosses(x_axis.get("crossing"))
2245 else:
2246 # Write the c:crossesAt element.
2247 self._write_c_crosses_at(x_axis.get("crossing"))
2249 # Write the c:crossBetween element.
2250 self._write_cross_between(x_axis.get("position_axis"))
2252 # Write the c:majorUnit element.
2253 self._write_c_major_unit(y_axis.get("major_unit"))
2255 # Write the c:minorUnit element.
2256 self._write_c_minor_unit(y_axis.get("minor_unit"))
2258 # Write the c:dispUnits element.
2259 self._write_disp_units(
2260 y_axis.get("display_units"), y_axis.get("display_units_visible")
2261 )
2263 self._xml_end_tag("c:valAx")
2265 def _write_cat_val_axis(self, args) -> None:
2266 # Write the <c:valAx> element. This is for the second valAx
2267 # in scatter plots. Usually the X axis.
2268 x_axis = args["x_axis"]
2269 y_axis = args["y_axis"]
2270 axis_ids = args["axis_ids"]
2271 position = args["position"] or self.val_axis_position
2272 is_horizontal = self.horiz_val_axis
2274 # If there are no axis_ids then we don't need to write this element.
2275 if axis_ids is None or not axis_ids:
2276 return
2278 # Overwrite the default axis position with a user supplied value.
2279 position = x_axis.get("position") or position
2281 self._xml_start_tag("c:valAx")
2283 self._write_axis_id(axis_ids[0])
2285 # Write the c:scaling element.
2286 self._write_scaling(
2287 x_axis.get("reverse"),
2288 x_axis.get("min"),
2289 x_axis.get("max"),
2290 x_axis.get("log_base"),
2291 )
2293 if not x_axis.get("visible"):
2294 self._write_delete(1)
2296 # Write the c:axPos element.
2297 self._write_axis_pos(position, y_axis.get("reverse"))
2299 # Write the c:majorGridlines element.
2300 self._write_major_gridlines(x_axis.get("major_gridlines"))
2302 # Write the c:minorGridlines element.
2303 self._write_minor_gridlines(x_axis.get("minor_gridlines"))
2305 # Write the axis title elements.
2306 self._write_title(x_axis["title"], is_horizontal)
2308 # Write the c:numberFormat element.
2309 self._write_number_format(x_axis)
2311 # Write the c:majorTickMark element.
2312 self._write_major_tick_mark(x_axis.get("major_tick_mark"))
2314 # Write the c:minorTickMark element.
2315 self._write_minor_tick_mark(x_axis.get("minor_tick_mark"))
2317 # Write the c:tickLblPos element.
2318 self._write_tick_label_pos(x_axis.get("label_position"))
2320 # Write the c:spPr element for the axis line.
2321 self._write_sp_pr(x_axis)
2323 # Write the axis font elements.
2324 self._write_axis_font(x_axis.get("num_font"))
2326 # Write the c:crossAx element.
2327 self._write_cross_axis(axis_ids[1])
2329 # Note, the category crossing comes from the value axis.
2330 if (
2331 y_axis.get("crossing") is None
2332 or y_axis["crossing"] == "max"
2333 or y_axis["crossing"] == "min"
2334 ):
2335 # Write the c:crosses element.
2336 self._write_crosses(y_axis.get("crossing"))
2337 else:
2338 # Write the c:crossesAt element.
2339 self._write_c_crosses_at(y_axis.get("crossing"))
2341 # Write the c:crossBetween element.
2342 self._write_cross_between(y_axis.get("position_axis"))
2344 # Write the c:majorUnit element.
2345 self._write_c_major_unit(x_axis.get("major_unit"))
2347 # Write the c:minorUnit element.
2348 self._write_c_minor_unit(x_axis.get("minor_unit"))
2350 # Write the c:dispUnits element.
2351 self._write_disp_units(
2352 x_axis.get("display_units"), x_axis.get("display_units_visible")
2353 )
2355 self._xml_end_tag("c:valAx")
2357 def _write_date_axis(self, args) -> None:
2358 # Write the <c:dateAx> element. Usually the X axis.
2359 x_axis = args["x_axis"]
2360 y_axis = args["y_axis"]
2361 axis_ids = args["axis_ids"]
2363 # If there are no axis_ids then we don't need to write this element.
2364 if axis_ids is None or not axis_ids:
2365 return
2367 position = self.cat_axis_position
2369 # Overwrite the default axis position with a user supplied value.
2370 position = x_axis.get("position") or position
2372 self._xml_start_tag("c:dateAx")
2374 self._write_axis_id(axis_ids[0])
2376 # Write the c:scaling element.
2377 self._write_scaling(
2378 x_axis.get("reverse"),
2379 x_axis.get("min"),
2380 x_axis.get("max"),
2381 x_axis.get("log_base"),
2382 )
2384 if not x_axis.get("visible"):
2385 self._write_delete(1)
2387 # Write the c:axPos element.
2388 self._write_axis_pos(position, y_axis.get("reverse"))
2390 # Write the c:majorGridlines element.
2391 self._write_major_gridlines(x_axis.get("major_gridlines"))
2393 # Write the c:minorGridlines element.
2394 self._write_minor_gridlines(x_axis.get("minor_gridlines"))
2396 # Write the axis title elements.
2397 self._write_title(x_axis["title"])
2399 # Write the c:numFmt element.
2400 self._write_number_format(x_axis)
2402 # Write the c:majorTickMark element.
2403 self._write_major_tick_mark(x_axis.get("major_tick_mark"))
2405 # Write the c:minorTickMark element.
2406 self._write_minor_tick_mark(x_axis.get("minor_tick_mark"))
2408 # Write the c:tickLblPos element.
2409 self._write_tick_label_pos(x_axis.get("label_position"))
2411 # Write the c:spPr element for the axis line.
2412 self._write_sp_pr(x_axis)
2414 # Write the axis font elements.
2415 self._write_axis_font(x_axis.get("num_font"))
2417 # Write the c:crossAx element.
2418 self._write_cross_axis(axis_ids[1])
2420 if self.show_crosses or x_axis.get("visible"):
2421 # Note, the category crossing comes from the value axis.
2422 if (
2423 y_axis.get("crossing") is None
2424 or y_axis.get("crossing") == "max"
2425 or y_axis["crossing"] == "min"
2426 ):
2427 # Write the c:crosses element.
2428 self._write_crosses(y_axis.get("crossing"))
2429 else:
2430 # Write the c:crossesAt element.
2431 self._write_c_crosses_at(y_axis.get("crossing"))
2433 # Write the c:auto element.
2434 self._write_auto(1)
2436 # Write the c:labelOffset element.
2437 self._write_label_offset(100)
2439 # Write the c:tickLblSkip element.
2440 self._write_c_tick_lbl_skip(x_axis.get("interval_unit"))
2442 # Write the c:tickMarkSkip element.
2443 self._write_c_tick_mark_skip(x_axis.get("interval_tick"))
2445 # Write the c:majorUnit element.
2446 self._write_c_major_unit(x_axis.get("major_unit"))
2448 # Write the c:majorTimeUnit element.
2449 if x_axis.get("major_unit"):
2450 self._write_c_major_time_unit(x_axis["major_unit_type"])
2452 # Write the c:minorUnit element.
2453 self._write_c_minor_unit(x_axis.get("minor_unit"))
2455 # Write the c:minorTimeUnit element.
2456 if x_axis.get("minor_unit"):
2457 self._write_c_minor_time_unit(x_axis["minor_unit_type"])
2459 self._xml_end_tag("c:dateAx")
2461 def _write_scaling(self, reverse, min_val, max_val, log_base) -> None:
2462 # Write the <c:scaling> element.
2464 self._xml_start_tag("c:scaling")
2466 # Write the c:logBase element.
2467 self._write_c_log_base(log_base)
2469 # Write the c:orientation element.
2470 self._write_orientation(reverse)
2472 # Write the c:max element.
2473 self._write_c_max(max_val)
2475 # Write the c:min element.
2476 self._write_c_min(min_val)
2478 self._xml_end_tag("c:scaling")
2480 def _write_c_log_base(self, val) -> None:
2481 # Write the <c:logBase> element.
2483 if not val:
2484 return
2486 attributes = [("val", val)]
2488 self._xml_empty_tag("c:logBase", attributes)
2490 def _write_orientation(self, reverse) -> None:
2491 # Write the <c:orientation> element.
2492 val = "minMax"
2494 if reverse:
2495 val = "maxMin"
2497 attributes = [("val", val)]
2499 self._xml_empty_tag("c:orientation", attributes)
2501 def _write_c_max(self, max_val) -> None:
2502 # Write the <c:max> element.
2504 if max_val is None:
2505 return
2507 attributes = [("val", max_val)]
2509 self._xml_empty_tag("c:max", attributes)
2511 def _write_c_min(self, min_val) -> None:
2512 # Write the <c:min> element.
2514 if min_val is None:
2515 return
2517 attributes = [("val", min_val)]
2519 self._xml_empty_tag("c:min", attributes)
2521 def _write_axis_pos(self, val, reverse) -> None:
2522 # Write the <c:axPos> element.
2524 if reverse:
2525 if val == "l":
2526 val = "r"
2527 if val == "b":
2528 val = "t"
2530 attributes = [("val", val)]
2532 self._xml_empty_tag("c:axPos", attributes)
2534 def _write_number_format(self, axis) -> None:
2535 # Write the <c:numberFormat> element. Note: It is assumed that if
2536 # a user defined number format is supplied (i.e., non-default) then
2537 # the sourceLinked attribute is 0.
2538 # The user can override this if required.
2539 format_code = axis.get("num_format")
2540 source_linked = 1
2542 # Check if a user defined number format has been set.
2543 if format_code is not None and format_code != axis["defaults"]["num_format"]:
2544 source_linked = 0
2546 # User override of sourceLinked.
2547 if axis.get("num_format_linked"):
2548 source_linked = 1
2550 attributes = [
2551 ("formatCode", format_code),
2552 ("sourceLinked", source_linked),
2553 ]
2555 self._xml_empty_tag("c:numFmt", attributes)
2557 def _write_cat_number_format(self, axis) -> None:
2558 # Write the <c:numFmt> element. Special case handler for category
2559 # axes which don't always have a number format.
2560 format_code = axis.get("num_format")
2561 source_linked = 1
2562 default_format = 1
2564 # Check if a user defined number format has been set.
2565 if format_code is not None and format_code != axis["defaults"]["num_format"]:
2566 source_linked = 0
2567 default_format = 0
2569 # User override of sourceLinked.
2570 if axis.get("num_format_linked"):
2571 source_linked = 1
2573 # Skip if cat doesn't have a num format (unless it is non-default).
2574 if not self.cat_has_num_fmt and default_format:
2575 return
2577 attributes = [
2578 ("formatCode", format_code),
2579 ("sourceLinked", source_linked),
2580 ]
2582 self._xml_empty_tag("c:numFmt", attributes)
2584 def _write_data_label_number_format(self, format_code) -> None:
2585 # Write the <c:numberFormat> element for data labels.
2586 source_linked = 0
2588 attributes = [
2589 ("formatCode", format_code),
2590 ("sourceLinked", source_linked),
2591 ]
2593 self._xml_empty_tag("c:numFmt", attributes)
2595 def _write_major_tick_mark(self, val) -> None:
2596 # Write the <c:majorTickMark> element.
2598 if not val:
2599 return
2601 attributes = [("val", val)]
2603 self._xml_empty_tag("c:majorTickMark", attributes)
2605 def _write_minor_tick_mark(self, val) -> None:
2606 # Write the <c:minorTickMark> element.
2608 if not val:
2609 return
2611 attributes = [("val", val)]
2613 self._xml_empty_tag("c:minorTickMark", attributes)
2615 def _write_tick_label_pos(self, val=None) -> None:
2616 # Write the <c:tickLblPos> element.
2617 if val is None or val == "next_to":
2618 val = "nextTo"
2620 attributes = [("val", val)]
2622 self._xml_empty_tag("c:tickLblPos", attributes)
2624 def _write_cross_axis(self, val) -> None:
2625 # Write the <c:crossAx> element.
2627 attributes = [("val", val)]
2629 self._xml_empty_tag("c:crossAx", attributes)
2631 def _write_crosses(self, val=None) -> None:
2632 # Write the <c:crosses> element.
2633 if val is None:
2634 val = "autoZero"
2636 attributes = [("val", val)]
2638 self._xml_empty_tag("c:crosses", attributes)
2640 def _write_c_crosses_at(self, val) -> None:
2641 # Write the <c:crossesAt> element.
2643 attributes = [("val", val)]
2645 self._xml_empty_tag("c:crossesAt", attributes)
2647 def _write_auto(self, val) -> None:
2648 # Write the <c:auto> element.
2650 attributes = [("val", val)]
2652 self._xml_empty_tag("c:auto", attributes)
2654 def _write_label_align(self, val=None) -> None:
2655 # Write the <c:labelAlign> element.
2657 if val is None:
2658 val = "ctr"
2660 if val == "right":
2661 val = "r"
2663 if val == "left":
2664 val = "l"
2666 attributes = [("val", val)]
2668 self._xml_empty_tag("c:lblAlgn", attributes)
2670 def _write_label_offset(self, val) -> None:
2671 # Write the <c:labelOffset> element.
2673 attributes = [("val", val)]
2675 self._xml_empty_tag("c:lblOffset", attributes)
2677 def _write_c_tick_lbl_skip(self, val) -> None:
2678 # Write the <c:tickLblSkip> element.
2679 if val is None:
2680 return
2682 attributes = [("val", val)]
2684 self._xml_empty_tag("c:tickLblSkip", attributes)
2686 def _write_c_tick_mark_skip(self, val) -> None:
2687 # Write the <c:tickMarkSkip> element.
2688 if val is None:
2689 return
2691 attributes = [("val", val)]
2693 self._xml_empty_tag("c:tickMarkSkip", attributes)
2695 def _write_major_gridlines(self, gridlines) -> None:
2696 # Write the <c:majorGridlines> element.
2698 if not gridlines:
2699 return
2701 if not gridlines["visible"]:
2702 return
2704 if gridlines["line"]["defined"]:
2705 self._xml_start_tag("c:majorGridlines")
2707 # Write the c:spPr element.
2708 self._write_sp_pr(gridlines)
2710 self._xml_end_tag("c:majorGridlines")
2711 else:
2712 self._xml_empty_tag("c:majorGridlines")
2714 def _write_minor_gridlines(self, gridlines) -> None:
2715 # Write the <c:minorGridlines> element.
2717 if not gridlines:
2718 return
2720 if not gridlines["visible"]:
2721 return
2723 if gridlines["line"]["defined"]:
2724 self._xml_start_tag("c:minorGridlines")
2726 # Write the c:spPr element.
2727 self._write_sp_pr(gridlines)
2729 self._xml_end_tag("c:minorGridlines")
2730 else:
2731 self._xml_empty_tag("c:minorGridlines")
2733 def _write_cross_between(self, val) -> None:
2734 # Write the <c:crossBetween> element.
2735 if val is None:
2736 val = self.cross_between
2738 attributes = [("val", val)]
2740 self._xml_empty_tag("c:crossBetween", attributes)
2742 def _write_c_major_unit(self, val) -> None:
2743 # Write the <c:majorUnit> element.
2745 if not val:
2746 return
2748 attributes = [("val", val)]
2750 self._xml_empty_tag("c:majorUnit", attributes)
2752 def _write_c_minor_unit(self, val) -> None:
2753 # Write the <c:minorUnit> element.
2755 if not val:
2756 return
2758 attributes = [("val", val)]
2760 self._xml_empty_tag("c:minorUnit", attributes)
2762 def _write_c_major_time_unit(self, val=None) -> None:
2763 # Write the <c:majorTimeUnit> element.
2764 if val is None:
2765 val = "days"
2767 attributes = [("val", val)]
2769 self._xml_empty_tag("c:majorTimeUnit", attributes)
2771 def _write_c_minor_time_unit(self, val=None) -> None:
2772 # Write the <c:minorTimeUnit> element.
2773 if val is None:
2774 val = "days"
2776 attributes = [("val", val)]
2778 self._xml_empty_tag("c:minorTimeUnit", attributes)
2780 def _write_legend(self) -> None:
2781 # Write the <c:legend> element.
2782 legend = self.legend
2783 position = legend.get("position", "right")
2784 font = legend.get("font")
2785 delete_series = []
2786 overlay = 0
2788 if legend.get("delete_series") and isinstance(legend["delete_series"], list):
2789 delete_series = legend["delete_series"]
2791 if position.startswith("overlay_"):
2792 position = position.replace("overlay_", "")
2793 overlay = 1
2795 allowed = {
2796 "right": "r",
2797 "left": "l",
2798 "top": "t",
2799 "bottom": "b",
2800 "top_right": "tr",
2801 }
2803 if position == "none":
2804 return
2806 if position not in allowed:
2807 return
2809 position = allowed[position]
2811 self._xml_start_tag("c:legend")
2813 # Write the c:legendPos element.
2814 self._write_legend_pos(position)
2816 # Remove series labels from the legend.
2817 for index in delete_series:
2818 # Write the c:legendEntry element.
2819 self._write_legend_entry(index)
2821 # Write the c:layout element.
2822 self._write_layout(legend.get("layout"), "legend")
2824 # Write the c:overlay element.
2825 if overlay:
2826 self._write_overlay()
2828 if font:
2829 self._write_tx_pr(font)
2831 # Write the c:spPr element.
2832 self._write_sp_pr(legend)
2834 self._xml_end_tag("c:legend")
2836 def _write_legend_pos(self, val) -> None:
2837 # Write the <c:legendPos> element.
2839 attributes = [("val", val)]
2841 self._xml_empty_tag("c:legendPos", attributes)
2843 def _write_legend_entry(self, index) -> None:
2844 # Write the <c:legendEntry> element.
2846 self._xml_start_tag("c:legendEntry")
2848 # Write the c:idx element.
2849 self._write_idx(index)
2851 # Write the c:delete element.
2852 self._write_delete(1)
2854 self._xml_end_tag("c:legendEntry")
2856 def _write_overlay(self) -> None:
2857 # Write the <c:overlay> element.
2858 val = 1
2860 attributes = [("val", val)]
2862 self._xml_empty_tag("c:overlay", attributes)
2864 def _write_plot_vis_only(self) -> None:
2865 # Write the <c:plotVisOnly> element.
2866 val = 1
2868 # Ignore this element if we are plotting hidden data.
2869 if self.show_hidden:
2870 return
2872 attributes = [("val", val)]
2874 self._xml_empty_tag("c:plotVisOnly", attributes)
2876 def _write_print_settings(self) -> None:
2877 # Write the <c:printSettings> element.
2878 self._xml_start_tag("c:printSettings")
2880 # Write the c:headerFooter element.
2881 self._write_header_footer()
2883 # Write the c:pageMargins element.
2884 self._write_page_margins()
2886 # Write the c:pageSetup element.
2887 self._write_page_setup()
2889 self._xml_end_tag("c:printSettings")
2891 def _write_header_footer(self) -> None:
2892 # Write the <c:headerFooter> element.
2893 self._xml_empty_tag("c:headerFooter")
2895 def _write_page_margins(self) -> None:
2896 # Write the <c:pageMargins> element.
2897 bottom = 0.75
2898 left = 0.7
2899 right = 0.7
2900 top = 0.75
2901 header = 0.3
2902 footer = 0.3
2904 attributes = [
2905 ("b", bottom),
2906 ("l", left),
2907 ("r", right),
2908 ("t", top),
2909 ("header", header),
2910 ("footer", footer),
2911 ]
2913 self._xml_empty_tag("c:pageMargins", attributes)
2915 def _write_page_setup(self) -> None:
2916 # Write the <c:pageSetup> element.
2917 self._xml_empty_tag("c:pageSetup")
2919 def _write_c_auto_title_deleted(self) -> None:
2920 # Write the <c:autoTitleDeleted> element.
2921 self._xml_empty_tag("c:autoTitleDeleted", [("val", 1)])
2923 def _write_title(self, title: ChartTitle, is_horizontal: bool = False) -> None:
2924 # Write the <c:title> element for different title types.
2925 if title.has_name():
2926 self._write_title_rich(title, is_horizontal)
2927 elif title.has_formula():
2928 self._write_title_formula(title, is_horizontal)
2929 elif title.has_formatting():
2930 self._write_title_format_only(title)
2932 def _write_title_rich(self, title: ChartTitle, is_horizontal: bool = False) -> None:
2933 # Write the <c:title> element for a rich string.
2934 self._xml_start_tag("c:title")
2936 # Write the c:tx element.
2937 self._write_tx_rich(title.name, is_horizontal, title.font)
2939 # Write the c:layout element.
2940 self._write_layout(title.layout, "text")
2942 # Write the c:overlay element.
2943 if title.overlay:
2944 self._write_overlay()
2946 # Write the c:spPr element.
2947 self._write_sp_pr(title.get_formatting())
2949 self._xml_end_tag("c:title")
2951 def _write_title_formula(
2952 self, title: ChartTitle, is_horizontal: bool = False
2953 ) -> None:
2954 # Write the <c:title> element for a rich string.
2955 self._xml_start_tag("c:title")
2957 # Write the c:tx element.
2958 self._write_tx_formula(title.formula, title.data_id)
2960 # Write the c:layout element.
2961 self._write_layout(title.layout, "text")
2963 # Write the c:overlay element.
2964 if title.overlay:
2965 self._write_overlay()
2967 # Write the c:spPr element.
2968 self._write_sp_pr(title.get_formatting())
2970 # Write the c:txPr element.
2971 self._write_tx_pr(title.font, is_horizontal)
2973 self._xml_end_tag("c:title")
2975 def _write_title_format_only(self, title: ChartTitle) -> None:
2976 # Write the <c:title> element title with formatting and default name.
2977 self._xml_start_tag("c:title")
2979 # Write the c:layout element.
2980 self._write_layout(title.layout, "text")
2982 # Write the c:overlay element.
2983 if title.overlay:
2984 self._write_overlay()
2986 # Write the c:spPr element.
2987 self._write_sp_pr(title.get_formatting())
2989 self._xml_end_tag("c:title")
2991 def _write_tx_rich(self, title, is_horizontal, font) -> None:
2992 # Write the <c:tx> element.
2994 self._xml_start_tag("c:tx")
2996 # Write the c:rich element.
2997 self._write_rich(title, font, is_horizontal, ignore_rich_pr=False)
2999 self._xml_end_tag("c:tx")
3001 def _write_tx_value(self, title) -> None:
3002 # Write the <c:tx> element with a value such as for series names.
3004 self._xml_start_tag("c:tx")
3006 # Write the c:v element.
3007 self._write_v(title)
3009 self._xml_end_tag("c:tx")
3011 def _write_tx_formula(self, title, data_id) -> None:
3012 # Write the <c:tx> element.
3013 data = None
3015 if data_id is not None:
3016 data = self.formula_data[data_id]
3018 self._xml_start_tag("c:tx")
3020 # Write the c:strRef element.
3021 self._write_str_ref(title, data, "str")
3023 self._xml_end_tag("c:tx")
3025 def _write_rich(self, title, font, is_horizontal, ignore_rich_pr) -> None:
3026 # Write the <c:rich> element.
3028 if font and font.get("rotation") is not None:
3029 rotation = font["rotation"]
3030 else:
3031 rotation = None
3033 self._xml_start_tag("c:rich")
3035 # Write the a:bodyPr element.
3036 self._write_a_body_pr(rotation, is_horizontal)
3038 # Write the a:lstStyle element.
3039 self._write_a_lst_style()
3041 # Write the a:p element.
3042 self._write_a_p_rich(title, font, ignore_rich_pr)
3044 self._xml_end_tag("c:rich")
3046 def _write_a_body_pr(self, rotation, is_horizontal) -> None:
3047 # Write the <a:bodyPr> element.
3048 attributes = []
3050 if rotation is None and is_horizontal:
3051 rotation = -5400000
3053 if rotation is not None:
3054 if rotation == 16200000:
3055 # 270 deg/stacked angle.
3056 attributes.append(("rot", 0))
3057 attributes.append(("vert", "wordArtVert"))
3058 elif rotation == 16260000:
3059 # 271 deg/East Asian vertical.
3060 attributes.append(("rot", 0))
3061 attributes.append(("vert", "eaVert"))
3062 else:
3063 attributes.append(("rot", rotation))
3064 attributes.append(("vert", "horz"))
3066 self._xml_empty_tag("a:bodyPr", attributes)
3068 def _write_a_lst_style(self) -> None:
3069 # Write the <a:lstStyle> element.
3070 self._xml_empty_tag("a:lstStyle")
3072 def _write_a_p_rich(self, title, font, ignore_rich_pr) -> None:
3073 # Write the <a:p> element for rich string titles.
3075 self._xml_start_tag("a:p")
3077 # Write the a:pPr element.
3078 if not ignore_rich_pr:
3079 self._write_a_p_pr_rich(font)
3081 # Write the a:r element.
3082 self._write_a_r(title, font)
3084 self._xml_end_tag("a:p")
3086 def _write_a_p_formula(self, font) -> None:
3087 # Write the <a:p> element for formula titles.
3089 self._xml_start_tag("a:p")
3091 # Write the a:pPr element.
3092 self._write_a_p_pr_rich(font)
3094 # Write the a:endParaRPr element.
3095 self._write_a_end_para_rpr()
3097 self._xml_end_tag("a:p")
3099 def _write_a_p_pr_rich(self, font) -> None:
3100 # Write the <a:pPr> element for rich string titles.
3102 self._xml_start_tag("a:pPr")
3104 # Write the a:defRPr element.
3105 self._write_a_def_rpr(font)
3107 self._xml_end_tag("a:pPr")
3109 def _write_a_def_rpr(self, font) -> None:
3110 # Write the <a:defRPr> element.
3111 has_color = False
3113 style_attributes = Shape._get_font_style_attributes(font)
3114 latin_attributes = Shape._get_font_latin_attributes(font)
3116 if font and font.get("color"):
3117 has_color = True
3119 if latin_attributes or has_color:
3120 self._xml_start_tag("a:defRPr", style_attributes)
3122 if has_color:
3123 self._write_a_solid_fill({"color": font["color"]})
3125 if latin_attributes:
3126 self._write_a_latin(latin_attributes)
3128 self._xml_end_tag("a:defRPr")
3129 else:
3130 self._xml_empty_tag("a:defRPr", style_attributes)
3132 def _write_a_end_para_rpr(self) -> None:
3133 # Write the <a:endParaRPr> element.
3134 lang = "en-US"
3136 attributes = [("lang", lang)]
3138 self._xml_empty_tag("a:endParaRPr", attributes)
3140 def _write_a_r(self, title, font) -> None:
3141 # Write the <a:r> element.
3143 self._xml_start_tag("a:r")
3145 # Write the a:rPr element.
3146 self._write_a_r_pr(font)
3148 # Write the a:t element.
3149 self._write_a_t(title)
3151 self._xml_end_tag("a:r")
3153 def _write_a_r_pr(self, font) -> None:
3154 # Write the <a:rPr> element.
3155 has_color = False
3156 lang = "en-US"
3158 style_attributes = Shape._get_font_style_attributes(font)
3159 latin_attributes = Shape._get_font_latin_attributes(font)
3161 if font and font["color"]:
3162 has_color = True
3164 # Add the lang type to the attributes.
3165 style_attributes.insert(0, ("lang", lang))
3167 if latin_attributes or has_color:
3168 self._xml_start_tag("a:rPr", style_attributes)
3170 if has_color:
3171 self._write_a_solid_fill({"color": font["color"]})
3173 if latin_attributes:
3174 self._write_a_latin(latin_attributes)
3176 self._xml_end_tag("a:rPr")
3177 else:
3178 self._xml_empty_tag("a:rPr", style_attributes)
3180 def _write_a_t(self, title) -> None:
3181 # Write the <a:t> element.
3183 self._xml_data_element("a:t", title)
3185 def _write_tx_pr(self, font, is_horizontal=False) -> None:
3186 # Write the <c:txPr> element.
3188 if font and font.get("rotation") is not None:
3189 rotation = font["rotation"]
3190 else:
3191 rotation = None
3193 self._xml_start_tag("c:txPr")
3195 # Write the a:bodyPr element.
3196 self._write_a_body_pr(rotation, is_horizontal)
3198 # Write the a:lstStyle element.
3199 self._write_a_lst_style()
3201 # Write the a:p element.
3202 self._write_a_p_formula(font)
3204 self._xml_end_tag("c:txPr")
3206 def _write_marker(self, marker) -> None:
3207 # Write the <c:marker> element.
3208 if marker is None:
3209 marker = self.default_marker
3211 if not marker:
3212 return
3214 if marker["type"] == "automatic":
3215 return
3217 self._xml_start_tag("c:marker")
3219 # Write the c:symbol element.
3220 self._write_symbol(marker["type"])
3222 # Write the c:size element.
3223 if marker.get("size"):
3224 self._write_marker_size(marker["size"])
3226 # Write the c:spPr element.
3227 self._write_sp_pr(marker)
3229 self._xml_end_tag("c:marker")
3231 def _write_marker_size(self, val) -> None:
3232 # Write the <c:size> element.
3234 attributes = [("val", val)]
3236 self._xml_empty_tag("c:size", attributes)
3238 def _write_symbol(self, val) -> None:
3239 # Write the <c:symbol> element.
3241 attributes = [("val", val)]
3243 self._xml_empty_tag("c:symbol", attributes)
3245 def _write_sp_pr(self, chart_format: dict) -> None:
3246 # Write the <c:spPr> element.
3247 if not self._has_formatting(chart_format):
3248 return
3250 self._xml_start_tag("c:spPr")
3252 # Write the fill elements for solid charts such as pie and bar.
3253 if chart_format.get("fill") and chart_format["fill"]["defined"]:
3254 if "none" in chart_format["fill"]:
3255 # Write the a:noFill element.
3256 self._write_a_no_fill()
3257 else:
3258 # Write the a:solidFill element.
3259 self._write_a_solid_fill(chart_format["fill"])
3261 if chart_format.get("pattern"):
3262 # Write the a:gradFill element.
3263 self._write_a_patt_fill(chart_format["pattern"])
3265 if chart_format.get("gradient"):
3266 # Write the a:gradFill element.
3267 self._write_a_grad_fill(chart_format["gradient"])
3269 # Write the a:ln element.
3270 if chart_format.get("line") and chart_format["line"]["defined"]:
3271 self._write_a_ln(chart_format["line"])
3273 self._xml_end_tag("c:spPr")
3275 def _write_a_ln(self, line) -> None:
3276 # Write the <a:ln> element.
3277 attributes = []
3279 # Add the line width as an attribute.
3280 width = line.get("width")
3282 if width is not None:
3283 # Round width to nearest 0.25, like Excel.
3284 width = int((width + 0.125) * 4) / 4.0
3286 # Convert to internal units.
3287 width = int(0.5 + (12700 * width))
3289 attributes = [("w", width)]
3291 if line.get("none") or line.get("color") or line.get("dash_type"):
3292 self._xml_start_tag("a:ln", attributes)
3294 # Write the line fill.
3295 if "none" in line:
3296 # Write the a:noFill element.
3297 self._write_a_no_fill()
3298 elif "color" in line:
3299 # Write the a:solidFill element.
3300 self._write_a_solid_fill(line)
3302 # Write the line/dash type.
3303 line_type = line.get("dash_type")
3304 if line_type:
3305 # Write the a:prstDash element.
3306 self._write_a_prst_dash(line_type)
3308 self._xml_end_tag("a:ln")
3309 else:
3310 self._xml_empty_tag("a:ln", attributes)
3312 def _write_a_no_fill(self) -> None:
3313 # Write the <a:noFill> element.
3314 self._xml_empty_tag("a:noFill")
3316 def _write_a_solid_fill(self, fill) -> None:
3317 # Write the <a:solidFill> element.
3319 self._xml_start_tag("a:solidFill")
3321 if fill.get("color"):
3322 self._write_color(fill["color"], fill.get("transparency"))
3324 self._xml_end_tag("a:solidFill")
3326 def _write_color(self, color: Color, transparency=None) -> None:
3327 # Write the appropriate chart color element.
3329 if not color:
3330 return
3332 if color._is_automatic:
3333 # Write the a:sysClr element.
3334 self._write_a_sys_clr()
3335 elif color._type == ColorTypes.RGB:
3336 # Write the a:srgbClr element.
3337 self._write_a_srgb_clr(color, transparency)
3338 elif color._type == ColorTypes.THEME:
3339 self._write_a_scheme_clr(color, transparency)
3341 def _write_a_sys_clr(self) -> None:
3342 # Write the <a:sysClr> element.
3344 val = "window"
3345 last_clr = "FFFFFF"
3347 attributes = [
3348 ("val", val),
3349 ("lastClr", last_clr),
3350 ]
3352 self._xml_empty_tag("a:sysClr", attributes)
3354 def _write_a_srgb_clr(self, color: Color, transparency=None) -> None:
3355 # Write the <a:srgbClr> element.
3357 if not color:
3358 return
3360 attributes = [("val", color._rgb_hex_value())]
3362 if transparency:
3363 self._xml_start_tag("a:srgbClr", attributes)
3365 # Write the a:alpha element.
3366 self._write_a_alpha(transparency)
3368 self._xml_end_tag("a:srgbClr")
3369 else:
3370 self._xml_empty_tag("a:srgbClr", attributes)
3372 def _write_a_scheme_clr(self, color: Color, transparency=None) -> None:
3373 # Write the <a:schemeClr> element.
3374 scheme, lum_mod, lum_off = color._chart_scheme()
3375 attributes = [("val", scheme)]
3377 if lum_mod > 0 or lum_off > 0 or transparency:
3378 self._xml_start_tag("a:schemeClr", attributes)
3380 if lum_mod > 0:
3381 # Write the a:lumMod element.
3382 self._write_a_lum_mod(lum_mod)
3384 if lum_off > 0:
3385 # Write the a:lumOff element.
3386 self._write_a_lum_off(lum_off)
3388 if transparency:
3389 # Write the a:alpha element.
3390 self._write_a_alpha(transparency)
3392 self._xml_end_tag("a:schemeClr")
3393 else:
3394 self._xml_empty_tag("a:schemeClr", attributes)
3396 def _write_a_lum_mod(self, value: int) -> None:
3397 # Write the <a:lumMod> element.
3398 attributes = [("val", value)]
3400 self._xml_empty_tag("a:lumMod", attributes)
3402 def _write_a_lum_off(self, value: int) -> None:
3403 # Write the <a:lumOff> element.
3404 attributes = [("val", value)]
3406 self._xml_empty_tag("a:lumOff", attributes)
3408 def _write_a_alpha(self, val) -> None:
3409 # Write the <a:alpha> element.
3411 val = int((100 - int(val)) * 1000)
3413 attributes = [("val", val)]
3415 self._xml_empty_tag("a:alpha", attributes)
3417 def _write_a_prst_dash(self, val) -> None:
3418 # Write the <a:prstDash> element.
3420 attributes = [("val", val)]
3422 self._xml_empty_tag("a:prstDash", attributes)
3424 def _write_trendline(self, trendline) -> None:
3425 # Write the <c:trendline> element.
3427 if not trendline:
3428 return
3430 self._xml_start_tag("c:trendline")
3432 # Write the c:name element.
3433 self._write_name(trendline.get("name"))
3435 # Write the c:spPr element.
3436 self._write_sp_pr(trendline)
3438 # Write the c:trendlineType element.
3439 self._write_trendline_type(trendline["type"])
3441 # Write the c:order element for polynomial trendlines.
3442 if trendline["type"] == "poly":
3443 self._write_trendline_order(trendline.get("order"))
3445 # Write the c:period element for moving average trendlines.
3446 if trendline["type"] == "movingAvg":
3447 self._write_period(trendline.get("period"))
3449 # Write the c:forward element.
3450 self._write_forward(trendline.get("forward"))
3452 # Write the c:backward element.
3453 self._write_backward(trendline.get("backward"))
3455 if "intercept" in trendline:
3456 # Write the c:intercept element.
3457 self._write_c_intercept(trendline["intercept"])
3459 if trendline.get("display_r_squared"):
3460 # Write the c:dispRSqr element.
3461 self._write_c_disp_rsqr()
3463 if trendline.get("display_equation"):
3464 # Write the c:dispEq element.
3465 self._write_c_disp_eq()
3467 # Write the c:trendlineLbl element.
3468 self._write_c_trendline_lbl(trendline)
3470 self._xml_end_tag("c:trendline")
3472 def _write_trendline_type(self, val) -> None:
3473 # Write the <c:trendlineType> element.
3475 attributes = [("val", val)]
3477 self._xml_empty_tag("c:trendlineType", attributes)
3479 def _write_name(self, data) -> None:
3480 # Write the <c:name> element.
3482 if data is None:
3483 return
3485 self._xml_data_element("c:name", data)
3487 def _write_trendline_order(self, val) -> None:
3488 # Write the <c:order> element.
3489 val = max(val, 2)
3491 attributes = [("val", val)]
3493 self._xml_empty_tag("c:order", attributes)
3495 def _write_period(self, val) -> None:
3496 # Write the <c:period> element.
3497 val = max(val, 2)
3499 attributes = [("val", val)]
3501 self._xml_empty_tag("c:period", attributes)
3503 def _write_forward(self, val) -> None:
3504 # Write the <c:forward> element.
3506 if not val:
3507 return
3509 attributes = [("val", val)]
3511 self._xml_empty_tag("c:forward", attributes)
3513 def _write_backward(self, val) -> None:
3514 # Write the <c:backward> element.
3516 if not val:
3517 return
3519 attributes = [("val", val)]
3521 self._xml_empty_tag("c:backward", attributes)
3523 def _write_c_intercept(self, val) -> None:
3524 # Write the <c:intercept> element.
3525 attributes = [("val", val)]
3527 self._xml_empty_tag("c:intercept", attributes)
3529 def _write_c_disp_eq(self) -> None:
3530 # Write the <c:dispEq> element.
3531 attributes = [("val", 1)]
3533 self._xml_empty_tag("c:dispEq", attributes)
3535 def _write_c_disp_rsqr(self) -> None:
3536 # Write the <c:dispRSqr> element.
3537 attributes = [("val", 1)]
3539 self._xml_empty_tag("c:dispRSqr", attributes)
3541 def _write_c_trendline_lbl(self, trendline) -> None:
3542 # Write the <c:trendlineLbl> element.
3543 self._xml_start_tag("c:trendlineLbl")
3545 # Write the c:layout element.
3546 self._write_layout(None, None)
3548 # Write the c:numFmt element.
3549 self._write_trendline_num_fmt()
3551 # Write the c:spPr element.
3552 self._write_sp_pr(trendline["label"])
3554 # Write the data label font elements.
3555 if trendline["label"]:
3556 font = trendline["label"].get("font")
3557 if font:
3558 self._write_axis_font(font)
3560 self._xml_end_tag("c:trendlineLbl")
3562 def _write_trendline_num_fmt(self) -> None:
3563 # Write the <c:numFmt> element.
3564 attributes = [
3565 ("formatCode", "General"),
3566 ("sourceLinked", 0),
3567 ]
3569 self._xml_empty_tag("c:numFmt", attributes)
3571 def _write_hi_low_lines(self) -> None:
3572 # Write the <c:hiLowLines> element.
3573 hi_low_lines = self.hi_low_lines
3575 if hi_low_lines is None:
3576 return
3578 if "line" in hi_low_lines and hi_low_lines["line"]["defined"]:
3579 self._xml_start_tag("c:hiLowLines")
3581 # Write the c:spPr element.
3582 self._write_sp_pr(hi_low_lines)
3584 self._xml_end_tag("c:hiLowLines")
3585 else:
3586 self._xml_empty_tag("c:hiLowLines")
3588 def _write_drop_lines(self) -> None:
3589 # Write the <c:dropLines> element.
3590 drop_lines = self.drop_lines
3592 if drop_lines is None:
3593 return
3595 if drop_lines["line"]["defined"]:
3596 self._xml_start_tag("c:dropLines")
3598 # Write the c:spPr element.
3599 self._write_sp_pr(drop_lines)
3601 self._xml_end_tag("c:dropLines")
3602 else:
3603 self._xml_empty_tag("c:dropLines")
3605 def _write_overlap(self, val) -> None:
3606 # Write the <c:overlap> element.
3608 if val is None:
3609 return
3611 attributes = [("val", val)]
3613 self._xml_empty_tag("c:overlap", attributes)
3615 def _write_num_cache(self, data) -> None:
3616 # Write the <c:numCache> element.
3617 if data:
3618 count = len(data)
3619 else:
3620 count = 0
3622 self._xml_start_tag("c:numCache")
3624 # Write the c:formatCode element.
3625 self._write_format_code("General")
3627 # Write the c:ptCount element.
3628 self._write_pt_count(count)
3630 for i in range(count):
3631 token = data[i]
3633 if token is None:
3634 continue
3636 try:
3637 float(token)
3638 except ValueError:
3639 # Write non-numeric data as 0.
3640 token = 0
3642 # Write the c:pt element.
3643 self._write_pt(i, token)
3645 self._xml_end_tag("c:numCache")
3647 def _write_str_cache(self, data) -> None:
3648 # Write the <c:strCache> element.
3649 count = len(data)
3651 self._xml_start_tag("c:strCache")
3653 # Write the c:ptCount element.
3654 self._write_pt_count(count)
3656 for i in range(count):
3657 # Write the c:pt element.
3658 self._write_pt(i, data[i])
3660 self._xml_end_tag("c:strCache")
3662 def _write_format_code(self, data) -> None:
3663 # Write the <c:formatCode> element.
3665 self._xml_data_element("c:formatCode", data)
3667 def _write_pt_count(self, val) -> None:
3668 # Write the <c:ptCount> element.
3670 attributes = [("val", val)]
3672 self._xml_empty_tag("c:ptCount", attributes)
3674 def _write_pt(self, idx, value) -> None:
3675 # Write the <c:pt> element.
3677 if value is None:
3678 return
3680 attributes = [("idx", idx)]
3682 self._xml_start_tag("c:pt", attributes)
3684 # Write the c:v element.
3685 self._write_v(value)
3687 self._xml_end_tag("c:pt")
3689 def _write_v(self, data) -> None:
3690 # Write the <c:v> element.
3692 self._xml_data_element("c:v", data)
3694 def _write_protection(self) -> None:
3695 # Write the <c:protection> element.
3696 if not self.protection:
3697 return
3699 self._xml_empty_tag("c:protection")
3701 def _write_d_pt(self, points) -> None:
3702 # Write the <c:dPt> elements.
3703 index = -1
3705 if not points:
3706 return
3708 for point in points:
3709 index += 1
3710 if not point:
3711 continue
3713 self._write_d_pt_point(index, point)
3715 def _write_d_pt_point(self, index, point) -> None:
3716 # Write an individual <c:dPt> element.
3718 self._xml_start_tag("c:dPt")
3720 # Write the c:idx element.
3721 self._write_idx(index)
3723 # Write the c:spPr element.
3724 self._write_sp_pr(point)
3726 self._xml_end_tag("c:dPt")
3728 def _write_d_lbls(self, labels) -> None:
3729 # Write the <c:dLbls> element.
3731 if not labels:
3732 return
3734 self._xml_start_tag("c:dLbls")
3736 # Write the custom c:dLbl elements.
3737 if labels.get("custom"):
3738 self._write_custom_labels(labels, labels["custom"])
3740 # Write the c:numFmt element.
3741 if labels.get("num_format"):
3742 self._write_data_label_number_format(labels["num_format"])
3744 # Write the c:spPr element for the plotarea formatting.
3745 self._write_sp_pr(labels)
3747 # Write the data label font elements.
3748 if labels.get("font"):
3749 self._write_axis_font(labels["font"])
3751 # Write the c:dLblPos element.
3752 if labels.get("position"):
3753 self._write_d_lbl_pos(labels["position"])
3755 # Write the c:showLegendKey element.
3756 if labels.get("legend_key"):
3757 self._write_show_legend_key()
3759 # Write the c:showVal element.
3760 if labels.get("value"):
3761 self._write_show_val()
3763 # Write the c:showCatName element.
3764 if labels.get("category"):
3765 self._write_show_cat_name()
3767 # Write the c:showSerName element.
3768 if labels.get("series_name"):
3769 self._write_show_ser_name()
3771 # Write the c:showPercent element.
3772 if labels.get("percentage"):
3773 self._write_show_percent()
3775 # Write the c:separator element.
3776 if labels.get("separator"):
3777 self._write_separator(labels["separator"])
3779 # Write the c:showLeaderLines element.
3780 if labels.get("leader_lines"):
3781 self._write_show_leader_lines()
3783 self._xml_end_tag("c:dLbls")
3785 def _write_custom_labels(self, parent, labels) -> None:
3786 # Write the <c:showLegendKey> element.
3787 index = 0
3789 for label in labels:
3790 index += 1
3792 if label is None:
3793 continue
3795 use_custom_formatting = True
3797 self._xml_start_tag("c:dLbl")
3799 # Write the c:idx element.
3800 self._write_idx(index - 1)
3802 delete_label = label.get("delete")
3804 if delete_label:
3805 self._write_delete(1)
3807 elif label.get("formula") or label.get("value") or label.get("position"):
3809 # Write the c:layout element.
3810 self._write_layout(None, None)
3812 if label.get("formula"):
3813 self._write_custom_label_formula(label)
3814 elif label.get("value"):
3815 self._write_custom_label_str(label)
3816 # String values use spPr formatting.
3817 use_custom_formatting = False
3819 if use_custom_formatting:
3820 self._write_custom_label_format(label)
3822 if label.get("position"):
3823 self._write_d_lbl_pos(label["position"])
3824 elif parent.get("position"):
3825 self._write_d_lbl_pos(parent["position"])
3827 if parent.get("value"):
3828 self._write_show_val()
3830 if parent.get("category"):
3831 self._write_show_cat_name()
3833 if parent.get("series_name"):
3834 self._write_show_ser_name()
3836 else:
3837 self._write_custom_label_format(label)
3839 self._xml_end_tag("c:dLbl")
3841 def _write_custom_label_str(self, label) -> None:
3842 # Write parts of the <c:dLbl> element for strings.
3843 title = label.get("value")
3844 font = label.get("font")
3845 has_formatting = self._has_formatting(label)
3847 self._xml_start_tag("c:tx")
3849 # Write the c:rich element.
3850 self._write_rich(title, font, False, not has_formatting)
3852 self._xml_end_tag("c:tx")
3854 # Write the c:spPr element.
3855 self._write_sp_pr(label)
3857 def _write_custom_label_formula(self, label) -> None:
3858 # Write parts of the <c:dLbl> element for formulas.
3859 formula = label.get("formula")
3860 data_id = label.get("data_id")
3861 data = None
3863 if data_id is not None:
3864 data = self.formula_data[data_id]
3866 self._xml_start_tag("c:tx")
3868 # Write the c:strRef element.
3869 self._write_str_ref(formula, data, "str")
3871 self._xml_end_tag("c:tx")
3873 def _write_custom_label_format(self, label) -> None:
3874 # Write the formatting and font elements for the custom labels.
3875 font = label.get("font")
3876 has_formatting = self._has_formatting(label)
3878 if has_formatting:
3879 self._write_sp_pr(label)
3880 self._write_tx_pr(font)
3881 elif font:
3882 self._xml_empty_tag("c:spPr")
3883 self._write_tx_pr(font)
3885 def _write_show_legend_key(self) -> None:
3886 # Write the <c:showLegendKey> element.
3887 val = "1"
3889 attributes = [("val", val)]
3891 self._xml_empty_tag("c:showLegendKey", attributes)
3893 def _write_show_val(self) -> None:
3894 # Write the <c:showVal> element.
3895 val = 1
3897 attributes = [("val", val)]
3899 self._xml_empty_tag("c:showVal", attributes)
3901 def _write_show_cat_name(self) -> None:
3902 # Write the <c:showCatName> element.
3903 val = 1
3905 attributes = [("val", val)]
3907 self._xml_empty_tag("c:showCatName", attributes)
3909 def _write_show_ser_name(self) -> None:
3910 # Write the <c:showSerName> element.
3911 val = 1
3913 attributes = [("val", val)]
3915 self._xml_empty_tag("c:showSerName", attributes)
3917 def _write_show_percent(self) -> None:
3918 # Write the <c:showPercent> element.
3919 val = 1
3921 attributes = [("val", val)]
3923 self._xml_empty_tag("c:showPercent", attributes)
3925 def _write_separator(self, data) -> None:
3926 # Write the <c:separator> element.
3927 self._xml_data_element("c:separator", data)
3929 def _write_show_leader_lines(self) -> None:
3930 # Write the <c:showLeaderLines> element.
3931 #
3932 # This is different for Pie/Doughnut charts. Other chart types only
3933 # supported leader lines after Excel 2015 via an extension element.
3934 #
3935 uri = "{CE6537A1-D6FC-4f65-9D91-7224C49458BB}"
3936 xmlns_c_15 = "http://schemas.microsoft.com/office/drawing/2012/chart"
3938 attributes = [
3939 ("uri", uri),
3940 ("xmlns:c15", xmlns_c_15),
3941 ]
3943 self._xml_start_tag("c:extLst")
3944 self._xml_start_tag("c:ext", attributes)
3945 self._xml_empty_tag("c15:showLeaderLines", [("val", 1)])
3946 self._xml_end_tag("c:ext")
3947 self._xml_end_tag("c:extLst")
3949 def _write_d_lbl_pos(self, val) -> None:
3950 # Write the <c:dLblPos> element.
3952 attributes = [("val", val)]
3954 self._xml_empty_tag("c:dLblPos", attributes)
3956 def _write_delete(self, val) -> None:
3957 # Write the <c:delete> element.
3959 attributes = [("val", val)]
3961 self._xml_empty_tag("c:delete", attributes)
3963 def _write_c_invert_if_negative(self, invert) -> None:
3964 # Write the <c:invertIfNegative> element.
3965 val = 1
3967 if not invert:
3968 return
3970 attributes = [("val", val)]
3972 self._xml_empty_tag("c:invertIfNegative", attributes)
3974 def _write_axis_font(self, font) -> None:
3975 # Write the axis font elements.
3977 if not font:
3978 return
3980 self._xml_start_tag("c:txPr")
3981 self._write_a_body_pr(font.get("rotation"), None)
3982 self._write_a_lst_style()
3983 self._xml_start_tag("a:p")
3985 self._write_a_p_pr_rich(font)
3987 self._write_a_end_para_rpr()
3988 self._xml_end_tag("a:p")
3989 self._xml_end_tag("c:txPr")
3991 def _write_a_latin(self, attributes) -> None:
3992 # Write the <a:latin> element.
3993 self._xml_empty_tag("a:latin", attributes)
3995 def _write_d_table(self) -> None:
3996 # Write the <c:dTable> element.
3997 table = self.table
3999 if not table:
4000 return
4002 self._xml_start_tag("c:dTable")
4004 if table["horizontal"]:
4005 # Write the c:showHorzBorder element.
4006 self._write_show_horz_border()
4008 if table["vertical"]:
4009 # Write the c:showVertBorder element.
4010 self._write_show_vert_border()
4012 if table["outline"]:
4013 # Write the c:showOutline element.
4014 self._write_show_outline()
4016 if table["show_keys"]:
4017 # Write the c:showKeys element.
4018 self._write_show_keys()
4020 if table["font"]:
4021 # Write the table font.
4022 self._write_tx_pr(table["font"])
4024 self._xml_end_tag("c:dTable")
4026 def _write_show_horz_border(self) -> None:
4027 # Write the <c:showHorzBorder> element.
4028 attributes = [("val", 1)]
4030 self._xml_empty_tag("c:showHorzBorder", attributes)
4032 def _write_show_vert_border(self) -> None:
4033 # Write the <c:showVertBorder> element.
4034 attributes = [("val", 1)]
4036 self._xml_empty_tag("c:showVertBorder", attributes)
4038 def _write_show_outline(self) -> None:
4039 # Write the <c:showOutline> element.
4040 attributes = [("val", 1)]
4042 self._xml_empty_tag("c:showOutline", attributes)
4044 def _write_show_keys(self) -> None:
4045 # Write the <c:showKeys> element.
4046 attributes = [("val", 1)]
4048 self._xml_empty_tag("c:showKeys", attributes)
4050 def _write_error_bars(self, error_bars) -> None:
4051 # Write the X and Y error bars.
4053 if not error_bars:
4054 return
4056 if error_bars["x_error_bars"]:
4057 self._write_err_bars("x", error_bars["x_error_bars"])
4059 if error_bars["y_error_bars"]:
4060 self._write_err_bars("y", error_bars["y_error_bars"])
4062 def _write_err_bars(self, direction, error_bars) -> None:
4063 # Write the <c:errBars> element.
4065 if not error_bars:
4066 return
4068 self._xml_start_tag("c:errBars")
4070 # Write the c:errDir element.
4071 self._write_err_dir(direction)
4073 # Write the c:errBarType element.
4074 self._write_err_bar_type(error_bars["direction"])
4076 # Write the c:errValType element.
4077 self._write_err_val_type(error_bars["type"])
4079 if not error_bars["endcap"]:
4080 # Write the c:noEndCap element.
4081 self._write_no_end_cap()
4083 if error_bars["type"] == "stdErr":
4084 # Don't need to write a c:errValType tag.
4085 pass
4086 elif error_bars["type"] == "cust":
4087 # Write the custom error tags.
4088 self._write_custom_error(error_bars)
4089 else:
4090 # Write the c:val element.
4091 self._write_error_val(error_bars["value"])
4093 # Write the c:spPr element.
4094 self._write_sp_pr(error_bars)
4096 self._xml_end_tag("c:errBars")
4098 def _write_err_dir(self, val) -> None:
4099 # Write the <c:errDir> element.
4101 attributes = [("val", val)]
4103 self._xml_empty_tag("c:errDir", attributes)
4105 def _write_err_bar_type(self, val) -> None:
4106 # Write the <c:errBarType> element.
4108 attributes = [("val", val)]
4110 self._xml_empty_tag("c:errBarType", attributes)
4112 def _write_err_val_type(self, val) -> None:
4113 # Write the <c:errValType> element.
4115 attributes = [("val", val)]
4117 self._xml_empty_tag("c:errValType", attributes)
4119 def _write_no_end_cap(self) -> None:
4120 # Write the <c:noEndCap> element.
4121 attributes = [("val", 1)]
4123 self._xml_empty_tag("c:noEndCap", attributes)
4125 def _write_error_val(self, val) -> None:
4126 # Write the <c:val> element for error bars.
4128 attributes = [("val", val)]
4130 self._xml_empty_tag("c:val", attributes)
4132 def _write_custom_error(self, error_bars) -> None:
4133 # Write the custom error bars tags.
4135 if error_bars["plus_values"]:
4136 # Write the c:plus element.
4137 self._xml_start_tag("c:plus")
4139 if isinstance(error_bars["plus_values"], list):
4140 self._write_num_lit(error_bars["plus_values"])
4141 else:
4142 self._write_num_ref(
4143 error_bars["plus_values"], error_bars["plus_data"], "num"
4144 )
4145 self._xml_end_tag("c:plus")
4147 if error_bars["minus_values"]:
4148 # Write the c:minus element.
4149 self._xml_start_tag("c:minus")
4151 if isinstance(error_bars["minus_values"], list):
4152 self._write_num_lit(error_bars["minus_values"])
4153 else:
4154 self._write_num_ref(
4155 error_bars["minus_values"], error_bars["minus_data"], "num"
4156 )
4157 self._xml_end_tag("c:minus")
4159 def _write_num_lit(self, data) -> None:
4160 # Write the <c:numLit> element for literal number list elements.
4161 count = len(data)
4163 # Write the c:numLit element.
4164 self._xml_start_tag("c:numLit")
4166 # Write the c:formatCode element.
4167 self._write_format_code("General")
4169 # Write the c:ptCount element.
4170 self._write_pt_count(count)
4172 for i in range(count):
4173 token = data[i]
4175 if token is None:
4176 continue
4178 try:
4179 float(token)
4180 except ValueError:
4181 # Write non-numeric data as 0.
4182 token = 0
4184 # Write the c:pt element.
4185 self._write_pt(i, token)
4187 self._xml_end_tag("c:numLit")
4189 def _write_up_down_bars(self) -> None:
4190 # Write the <c:upDownBars> element.
4191 up_down_bars = self.up_down_bars
4193 if up_down_bars is None:
4194 return
4196 self._xml_start_tag("c:upDownBars")
4198 # Write the c:gapWidth element.
4199 self._write_gap_width(150)
4201 # Write the c:upBars element.
4202 self._write_up_bars(up_down_bars.get("up"))
4204 # Write the c:downBars element.
4205 self._write_down_bars(up_down_bars.get("down"))
4207 self._xml_end_tag("c:upDownBars")
4209 def _write_gap_width(self, val) -> None:
4210 # Write the <c:gapWidth> element.
4212 if val is None:
4213 return
4215 attributes = [("val", val)]
4217 self._xml_empty_tag("c:gapWidth", attributes)
4219 def _write_up_bars(self, bar_format) -> None:
4220 # Write the <c:upBars> element.
4222 if bar_format["line"] and bar_format["line"]["defined"]:
4223 self._xml_start_tag("c:upBars")
4225 # Write the c:spPr element.
4226 self._write_sp_pr(bar_format)
4228 self._xml_end_tag("c:upBars")
4229 else:
4230 self._xml_empty_tag("c:upBars")
4232 def _write_down_bars(self, bar_format) -> None:
4233 # Write the <c:downBars> element.
4235 if bar_format["line"] and bar_format["line"]["defined"]:
4236 self._xml_start_tag("c:downBars")
4238 # Write the c:spPr element.
4239 self._write_sp_pr(bar_format)
4241 self._xml_end_tag("c:downBars")
4242 else:
4243 self._xml_empty_tag("c:downBars")
4245 def _write_disp_units(self, units, display) -> None:
4246 # Write the <c:dispUnits> element.
4248 if not units:
4249 return
4251 attributes = [("val", units)]
4253 self._xml_start_tag("c:dispUnits")
4254 self._xml_empty_tag("c:builtInUnit", attributes)
4256 if display:
4257 self._xml_start_tag("c:dispUnitsLbl")
4258 self._xml_empty_tag("c:layout")
4259 self._xml_end_tag("c:dispUnitsLbl")
4261 self._xml_end_tag("c:dispUnits")
4263 def _write_a_grad_fill(self, gradient) -> None:
4264 # Write the <a:gradFill> element.
4266 attributes = [("flip", "none"), ("rotWithShape", "1")]
4268 if gradient["type"] == "linear":
4269 attributes = []
4271 self._xml_start_tag("a:gradFill", attributes)
4273 # Write the a:gsLst element.
4274 self._write_a_gs_lst(gradient)
4276 if gradient["type"] == "linear":
4277 # Write the a:lin element.
4278 self._write_a_lin(gradient["angle"])
4279 else:
4280 # Write the a:path element.
4281 self._write_a_path(gradient["type"])
4283 # Write the a:tileRect element.
4284 self._write_a_tile_rect(gradient["type"])
4286 self._xml_end_tag("a:gradFill")
4288 def _write_a_gs_lst(self, gradient) -> None:
4289 # Write the <a:gsLst> element.
4290 positions = gradient["positions"]
4291 colors = gradient["colors"]
4293 self._xml_start_tag("a:gsLst")
4295 for i, color in enumerate(colors):
4296 pos = int(positions[i] * 1000)
4297 attributes = [("pos", pos)]
4298 self._xml_start_tag("a:gs", attributes)
4300 self._write_color(color)
4302 self._xml_end_tag("a:gs")
4304 self._xml_end_tag("a:gsLst")
4306 def _write_a_lin(self, angle) -> None:
4307 # Write the <a:lin> element.
4309 angle = int(60000 * angle)
4311 attributes = [
4312 ("ang", angle),
4313 ("scaled", "0"),
4314 ]
4316 self._xml_empty_tag("a:lin", attributes)
4318 def _write_a_path(self, gradient_type) -> None:
4319 # Write the <a:path> element.
4321 attributes = [("path", gradient_type)]
4323 self._xml_start_tag("a:path", attributes)
4325 # Write the a:fillToRect element.
4326 self._write_a_fill_to_rect(gradient_type)
4328 self._xml_end_tag("a:path")
4330 def _write_a_fill_to_rect(self, gradient_type) -> None:
4331 # Write the <a:fillToRect> element.
4333 if gradient_type == "shape":
4334 attributes = [
4335 ("l", "50000"),
4336 ("t", "50000"),
4337 ("r", "50000"),
4338 ("b", "50000"),
4339 ]
4340 else:
4341 attributes = [
4342 ("l", "100000"),
4343 ("t", "100000"),
4344 ]
4346 self._xml_empty_tag("a:fillToRect", attributes)
4348 def _write_a_tile_rect(self, gradient_type) -> None:
4349 # Write the <a:tileRect> element.
4351 if gradient_type == "shape":
4352 attributes = []
4353 else:
4354 attributes = [
4355 ("r", "-100000"),
4356 ("b", "-100000"),
4357 ]
4359 self._xml_empty_tag("a:tileRect", attributes)
4361 def _write_a_patt_fill(self, pattern) -> None:
4362 # Write the <a:pattFill> element.
4364 attributes = [("prst", pattern["pattern"])]
4366 self._xml_start_tag("a:pattFill", attributes)
4368 # Write the a:fgClr element.
4369 self._write_a_fg_clr(pattern["fg_color"])
4371 # Write the a:bgClr element.
4372 self._write_a_bg_clr(pattern["bg_color"])
4374 self._xml_end_tag("a:pattFill")
4376 def _write_a_fg_clr(self, color: Color) -> None:
4377 # Write the <a:fgClr> element.
4378 self._xml_start_tag("a:fgClr")
4379 self._write_color(color)
4380 self._xml_end_tag("a:fgClr")
4382 def _write_a_bg_clr(self, color: Color) -> None:
4383 # Write the <a:bgClr> element.
4384 self._xml_start_tag("a:bgClr")
4385 self._write_color(color)
4386 self._xml_end_tag("a:bgClr")