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.orientation = 0x0
51 self.series = []
52 self.embedded = 0
53 self.id = -1
54 self.series_index = 0
55 self.style_id = 2
56 self.axis_ids = []
57 self.axis2_ids = []
58 self.cat_has_num_fmt = False
59 self.requires_category = False
60 self.legend = {}
61 self.cat_axis_position = "b"
62 self.val_axis_position = "l"
63 self.formula_ids = {}
64 self.formula_data = []
65 self.horiz_cat_axis = 0
66 self.horiz_val_axis = 1
67 self.protection = 0
68 self.chartarea = {}
69 self.plotarea = {}
70 self.x_axis = {}
71 self.y_axis = {}
72 self.y2_axis = {}
73 self.x2_axis = {}
74 self.chart_name = ""
75 self.show_blanks = "gap"
76 self.show_na_as_empty = False
77 self.show_hidden = False
78 self.show_crosses = True
79 self.width = 480
80 self.height = 288
81 self.x_scale = 1
82 self.y_scale = 1
83 self.x_offset = 0
84 self.y_offset = 0
85 self.table = None
86 self.cross_between = "between"
87 self.default_marker = None
88 self.series_gap_1 = None
89 self.series_gap_2 = None
90 self.series_overlap_1 = None
91 self.series_overlap_2 = None
92 self.drop_lines = None
93 self.hi_low_lines = None
94 self.up_down_bars = None
95 self.smooth_allowed = False
96 self.title = ChartTitle()
98 self.date_category = False
99 self.date_1904 = False
100 self.remove_timezone = False
101 self.label_positions = {}
102 self.label_position_default = ""
103 self.already_inserted = False
104 self.combined = None
105 self.is_secondary = False
106 self.warn_sheetname = True
107 self._set_default_properties()
108 self.fill = {}
110 def add_series(self, options: Optional[Dict[str, Any]] = None) -> None:
111 """
112 Add a data series to a chart.
114 Args:
115 options: A dictionary of chart series options.
117 Returns:
118 Nothing.
120 """
121 # Add a series and it's properties to a chart.
122 if options is None:
123 options = {}
125 # Check that the required input has been specified.
126 if "values" not in options:
127 warn("Must specify 'values' in add_series()")
128 return
130 if self.requires_category and "categories" not in options:
131 warn("Must specify 'categories' in add_series() for this chart type")
132 return
134 if len(self.series) == 255:
135 warn(
136 "The maximum number of series that can be added to an "
137 "Excel Chart is 255"
138 )
139 return
141 # Convert list into a formula string.
142 values = self._list_to_formula(options.get("values"))
143 categories = self._list_to_formula(options.get("categories"))
145 # Switch name and name_formula parameters if required.
146 name, name_formula = self._process_names(
147 options.get("name"), options.get("name_formula")
148 )
150 # Get an id for the data equivalent to the range formula.
151 cat_id = self._get_data_id(categories, options.get("categories_data"))
152 val_id = self._get_data_id(values, options.get("values_data"))
153 name_id = self._get_data_id(name_formula, options.get("name_data"))
155 # Set the line properties for the series.
156 line = Shape._get_line_properties(options)
158 # Set the fill properties for the series.
159 fill = Shape._get_fill_properties(options.get("fill"))
161 # Set the pattern fill properties for the series.
162 pattern = Shape._get_pattern_properties(options.get("pattern"))
164 # Set the gradient fill properties for the series.
165 gradient = Shape._get_gradient_properties(options.get("gradient"))
167 # Pattern fill overrides solid fill.
168 if pattern:
169 self.fill = None
171 # Gradient fill overrides the solid and pattern fill.
172 if gradient:
173 pattern = None
174 fill = None
176 # Set the marker properties for the series.
177 marker = self._get_marker_properties(options.get("marker"))
179 # Set the trendline properties for the series.
180 trendline = self._get_trendline_properties(options.get("trendline"))
182 # Set the line smooth property for the series.
183 smooth = options.get("smooth")
185 # Set the error bars properties for the series.
186 y_error_bars = self._get_error_bars_props(options.get("y_error_bars"))
187 x_error_bars = self._get_error_bars_props(options.get("x_error_bars"))
189 error_bars = {"x_error_bars": x_error_bars, "y_error_bars": y_error_bars}
191 # Set the point properties for the series.
192 points = self._get_points_properties(options.get("points"))
194 # Set the labels properties for the series.
195 labels = self._get_labels_properties(options.get("data_labels"))
197 # Set the "invert if negative" fill property.
198 invert_if_neg = options.get("invert_if_negative", False)
199 inverted_color = options.get("invert_if_negative_color")
201 if inverted_color:
202 inverted_color = Color._from_value(inverted_color)
204 # Set the secondary axis properties.
205 x2_axis = options.get("x2_axis")
206 y2_axis = options.get("y2_axis")
208 # Store secondary status for combined charts.
209 if x2_axis or y2_axis:
210 self.is_secondary = True
212 # Set the gap for Bar/Column charts.
213 if options.get("gap") is not None:
214 if y2_axis:
215 self.series_gap_2 = options["gap"]
216 else:
217 self.series_gap_1 = options["gap"]
219 # Set the overlap for Bar/Column charts.
220 if options.get("overlap"):
221 if y2_axis:
222 self.series_overlap_2 = options["overlap"]
223 else:
224 self.series_overlap_1 = options["overlap"]
226 # Add the user supplied data to the internal structures.
227 series = {
228 "values": values,
229 "categories": categories,
230 "name": name,
231 "name_formula": name_formula,
232 "name_id": name_id,
233 "val_data_id": val_id,
234 "cat_data_id": cat_id,
235 "line": line,
236 "fill": fill,
237 "pattern": pattern,
238 "gradient": gradient,
239 "marker": marker,
240 "trendline": trendline,
241 "labels": labels,
242 "invert_if_neg": invert_if_neg,
243 "inverted_color": inverted_color,
244 "x2_axis": x2_axis,
245 "y2_axis": y2_axis,
246 "points": points,
247 "error_bars": error_bars,
248 "smooth": smooth,
249 }
251 self.series.append(series)
253 def set_x_axis(self, options: Dict[str, Any]) -> None:
254 """
255 Set the chart X axis options.
257 Args:
258 options: A dictionary of axis options.
260 Returns:
261 Nothing.
263 """
264 axis = self._convert_axis_args(self.x_axis, options)
266 self.x_axis = axis
268 def set_y_axis(self, options: Dict[str, Any]) -> None:
269 """
270 Set the chart Y axis options.
272 Args:
273 options: A dictionary of axis options.
275 Returns:
276 Nothing.
278 """
279 axis = self._convert_axis_args(self.y_axis, options)
281 self.y_axis = axis
283 def set_x2_axis(self, options: Dict[str, Any]) -> None:
284 """
285 Set the chart secondary X axis options.
287 Args:
288 options: A dictionary of axis options.
290 Returns:
291 Nothing.
293 """
294 axis = self._convert_axis_args(self.x2_axis, options)
296 self.x2_axis = axis
298 def set_y2_axis(self, options: Dict[str, Any]) -> None:
299 """
300 Set the chart secondary Y axis options.
302 Args:
303 options: A dictionary of axis options.
305 Returns:
306 Nothing.
308 """
309 axis = self._convert_axis_args(self.y2_axis, options)
311 self.y2_axis = axis
313 def set_title(self, options: Optional[Dict[str, Any]] = None) -> None:
314 """
315 Set the chart title options.
317 Args:
318 options: A dictionary of chart title options.
320 Returns:
321 Nothing.
323 """
324 if options is None:
325 options = {}
327 name, name_formula = self._process_names(
328 options.get("name"), options.get("name_formula")
329 )
331 data_id = self._get_data_id(name_formula, options.get("data"))
333 # Update the main chart title.
334 self.title.name = name
335 self.title.formula = name_formula
336 self.title.data_id = data_id
338 # Set the font properties if present.
339 if options.get("font"):
340 self.title.font = self._convert_font_args(options.get("font"))
341 else:
342 # For backward/axis compatibility.
343 self.title.font = self._convert_font_args(options.get("name_font"))
345 # Set the line properties.
346 self.title.line = Shape._get_line_properties(options)
348 # Set the fill properties.
349 self.title.fill = Shape._get_fill_properties(options.get("fill"))
351 # Set the gradient properties.
352 self.title.gradient = Shape._get_gradient_properties(options.get("gradient"))
354 # Set the layout.
355 self.title.layout = self._get_layout_properties(options.get("layout"), True)
357 # Set the title overlay option.
358 self.title.overlay = options.get("overlay")
360 # Set the automatic title option.
361 self.title.hidden = options.get("none", False)
363 def set_legend(self, options: Dict[str, Any]) -> None:
364 """
365 Set the chart legend options.
367 Args:
368 options: A dictionary of chart legend options.
370 Returns:
371 Nothing.
372 """
373 # Convert the user defined properties to internal properties.
374 self.legend = self._get_legend_properties(options)
376 def set_plotarea(self, options: Dict[str, Any]) -> None:
377 """
378 Set the chart plot area options.
380 Args:
381 options: A dictionary of chart plot area options.
383 Returns:
384 Nothing.
385 """
386 # Convert the user defined properties to internal properties.
387 self.plotarea = self._get_area_properties(options)
389 def set_chartarea(self, options: Dict[str, Any]) -> None:
390 """
391 Set the chart area options.
393 Args:
394 options: A dictionary of chart area options.
396 Returns:
397 Nothing.
398 """
399 # Convert the user defined properties to internal properties.
400 self.chartarea = self._get_area_properties(options)
402 def set_style(self, style_id: int = 2) -> None:
403 """
404 Set the chart style type.
406 Args:
407 style_id: An int representing the chart style.
409 Returns:
410 Nothing.
411 """
412 # Set one of the 48 built-in Excel chart styles. The default is 2.
413 if style_id is None:
414 style_id = 2
416 if style_id < 1 or style_id > 48:
417 style_id = 2
419 self.style_id = style_id
421 def show_blanks_as(self, option: str) -> None:
422 """
423 Set the option for displaying blank data in a chart.
425 Args:
426 option: A string representing the display option.
428 Returns:
429 Nothing.
430 """
431 if not option:
432 return
434 valid_options = {
435 "gap": 1,
436 "zero": 1,
437 "span": 1,
438 }
440 if option not in valid_options:
441 warn(f"Unknown show_blanks_as() option '{option}'")
442 return
444 self.show_blanks = option
446 def show_na_as_empty_cell(self) -> None:
447 """
448 Display ``#N/A`` on charts as blank/empty cells.
450 Args:
451 None.
453 Returns:
454 Nothing.
455 """
456 self.show_na_as_empty = True
458 def show_hidden_data(self) -> None:
459 """
460 Display data on charts from hidden rows or columns.
462 Args:
463 None.
465 Returns:
466 Nothing.
467 """
468 self.show_hidden = True
470 def set_size(self, options: Optional[Dict[str, Any]] = None) -> None:
471 """
472 Set size or scale of the chart.
474 Args:
475 options: A dictionary of chart size options.
477 Returns:
478 Nothing.
479 """
480 if options is None:
481 options = {}
483 # Set dimensions or scale for the chart.
484 self.width = options.get("width", self.width)
485 self.height = options.get("height", self.height)
486 self.x_scale = options.get("x_scale", 1)
487 self.y_scale = options.get("y_scale", 1)
488 self.x_offset = options.get("x_offset", 0)
489 self.y_offset = options.get("y_offset", 0)
491 def set_table(self, options: Optional[Dict[str, Any]] = None) -> None:
492 """
493 Set properties for an axis data table.
495 Args:
496 options: A dictionary of axis table options.
498 Returns:
499 Nothing.
501 """
502 if options is None:
503 options = {}
505 table = {}
507 table["horizontal"] = options.get("horizontal", 1)
508 table["vertical"] = options.get("vertical", 1)
509 table["outline"] = options.get("outline", 1)
510 table["show_keys"] = options.get("show_keys", 0)
511 table["font"] = self._convert_font_args(options.get("font"))
513 self.table = table
515 def set_up_down_bars(self, options: Optional[Dict[str, Any]] = None) -> None:
516 """
517 Set properties for the chart up-down bars.
519 Args:
520 options: A dictionary of options.
522 Returns:
523 Nothing.
525 """
526 if options is None:
527 options = {}
529 # Defaults.
530 up_line = None
531 up_fill = None
532 down_line = None
533 down_fill = None
535 # Set properties for 'up' bar.
536 if options.get("up"):
537 up_line = Shape._get_line_properties(options["up"])
538 up_fill = Shape._get_fill_properties(options["up"]["fill"])
540 # Set properties for 'down' bar.
541 if options.get("down"):
542 down_line = Shape._get_line_properties(options["down"])
543 down_fill = Shape._get_fill_properties(options["down"]["fill"])
545 self.up_down_bars = {
546 "up": {
547 "line": up_line,
548 "fill": up_fill,
549 },
550 "down": {
551 "line": down_line,
552 "fill": down_fill,
553 },
554 }
556 def set_drop_lines(self, options: Optional[Dict[str, Any]] = None) -> None:
557 """
558 Set properties for the chart drop lines.
560 Args:
561 options: A dictionary of options.
563 Returns:
564 Nothing.
566 """
567 if options is None:
568 options = {}
570 line = Shape._get_line_properties(options)
571 fill = Shape._get_fill_properties(options.get("fill"))
573 # Set the pattern fill properties for the series.
574 pattern = Shape._get_pattern_properties(options.get("pattern"))
576 # Set the gradient fill properties for the series.
577 gradient = Shape._get_gradient_properties(options.get("gradient"))
579 # Pattern fill overrides solid fill.
580 if pattern:
581 self.fill = None
583 # Gradient fill overrides the solid and pattern fill.
584 if gradient:
585 pattern = None
586 fill = None
588 self.drop_lines = {
589 "line": line,
590 "fill": fill,
591 "pattern": pattern,
592 "gradient": gradient,
593 }
595 def set_high_low_lines(self, options: Optional[Dict[str, Any]] = None) -> None:
596 """
597 Set properties for the chart high-low lines.
599 Args:
600 options: A dictionary of options.
602 Returns:
603 Nothing.
605 """
606 if options is None:
607 options = {}
609 line = Shape._get_line_properties(options)
610 fill = Shape._get_fill_properties(options.get("fill"))
612 # Set the pattern fill properties for the series.
613 pattern = Shape._get_pattern_properties(options.get("pattern"))
615 # Set the gradient fill properties for the series.
616 gradient = Shape._get_gradient_properties(options.get("gradient"))
618 # Pattern fill overrides solid fill.
619 if pattern:
620 self.fill = None
622 # Gradient fill overrides the solid and pattern fill.
623 if gradient:
624 pattern = None
625 fill = None
627 self.hi_low_lines = {
628 "line": line,
629 "fill": fill,
630 "pattern": pattern,
631 "gradient": gradient,
632 }
634 def combine(self, chart: Optional["Chart"] = None) -> None:
635 """
636 Create a combination chart with a secondary chart.
638 Args:
639 chart: The secondary chart to combine with the primary chart.
641 Returns:
642 Nothing.
644 """
645 if chart is None:
646 return
648 self.combined = chart
650 ###########################################################################
651 #
652 # Private API.
653 #
654 ###########################################################################
656 def _assemble_xml_file(self) -> None:
657 # Assemble and write the XML file.
659 # Write the XML declaration.
660 self._xml_declaration()
662 # Write the c:chartSpace element.
663 self._write_chart_space()
665 # Write the c:lang element.
666 self._write_lang()
668 # Write the c:style element.
669 self._write_style()
671 # Write the c:protection element.
672 self._write_protection()
674 # Write the c:chart element.
675 self._write_chart()
677 # Write the c:spPr element for the chartarea formatting.
678 self._write_sp_pr(self.chartarea)
680 # Write the c:printSettings element.
681 if self.embedded:
682 self._write_print_settings()
684 # Close the worksheet tag.
685 self._xml_end_tag("c:chartSpace")
686 # Close the file.
687 self._xml_close()
689 def _convert_axis_args(self, axis, user_options):
690 # Convert user defined axis values into private hash values.
691 options = axis["defaults"].copy()
692 options.update(user_options)
694 axis = {
695 "defaults": axis["defaults"],
696 "reverse": options.get("reverse"),
697 "min": options.get("min"),
698 "max": options.get("max"),
699 "minor_unit": options.get("minor_unit"),
700 "major_unit": options.get("major_unit"),
701 "minor_unit_type": options.get("minor_unit_type"),
702 "major_unit_type": options.get("major_unit_type"),
703 "display_units": options.get("display_units"),
704 "log_base": options.get("log_base"),
705 "crossing": options.get("crossing"),
706 "position_axis": options.get("position_axis"),
707 "position": options.get("position"),
708 "label_position": options.get("label_position"),
709 "label_align": options.get("label_align"),
710 "num_format": options.get("num_format"),
711 "num_format_linked": options.get("num_format_linked"),
712 "interval_unit": options.get("interval_unit"),
713 "interval_tick": options.get("interval_tick"),
714 "text_axis": False,
715 "title": ChartTitle(),
716 }
718 axis["visible"] = options.get("visible", True)
720 # Convert the display units.
721 axis["display_units"] = self._get_display_units(axis["display_units"])
722 axis["display_units_visible"] = options.get("display_units_visible", True)
724 # Map major_gridlines properties.
725 if options.get("major_gridlines") and options["major_gridlines"]["visible"]:
726 axis["major_gridlines"] = self._get_gridline_properties(
727 options["major_gridlines"]
728 )
730 # Map minor_gridlines properties.
731 if options.get("minor_gridlines") and options["minor_gridlines"]["visible"]:
732 axis["minor_gridlines"] = self._get_gridline_properties(
733 options["minor_gridlines"]
734 )
736 # Only use the first letter of bottom, top, left or right.
737 if axis.get("position"):
738 axis["position"] = axis["position"].lower()[0]
740 # Set the position for a category axis on or between the tick marks.
741 if axis.get("position_axis"):
742 if axis["position_axis"] == "on_tick":
743 axis["position_axis"] = "midCat"
744 elif axis["position_axis"] == "between":
745 # Doesn't need to be modified.
746 pass
747 else:
748 # Otherwise use the default value.
749 axis["position_axis"] = None
751 # Set the category axis as a date axis.
752 if options.get("date_axis"):
753 self.date_category = True
755 # Set the category axis as a text axis.
756 if options.get("text_axis"):
757 self.date_category = False
758 axis["text_axis"] = True
760 # Convert datetime args if required.
761 if axis.get("min") and _supported_datetime(axis["min"]):
762 axis["min"] = _datetime_to_excel_datetime(
763 axis["min"], self.date_1904, self.remove_timezone
764 )
765 if axis.get("max") and _supported_datetime(axis["max"]):
766 axis["max"] = _datetime_to_excel_datetime(
767 axis["max"], self.date_1904, self.remove_timezone
768 )
769 if axis.get("crossing") and _supported_datetime(axis["crossing"]):
770 axis["crossing"] = _datetime_to_excel_datetime(
771 axis["crossing"], self.date_1904, self.remove_timezone
772 )
774 # Set the font properties if present.
775 axis["num_font"] = self._convert_font_args(options.get("num_font"))
777 # Set the line properties for the axis.
778 axis["line"] = Shape._get_line_properties(options)
780 # Set the fill properties for the axis.
781 axis["fill"] = Shape._get_fill_properties(options.get("fill"))
783 # Set the pattern fill properties for the series.
784 axis["pattern"] = Shape._get_pattern_properties(options.get("pattern"))
786 # Set the gradient fill properties for the series.
787 axis["gradient"] = Shape._get_gradient_properties(options.get("gradient"))
789 # Pattern fill overrides solid fill.
790 if axis.get("pattern"):
791 axis["fill"] = None
793 # Gradient fill overrides the solid and pattern fill.
794 if axis.get("gradient"):
795 axis["pattern"] = None
796 axis["fill"] = None
798 # Set the tick marker types.
799 axis["minor_tick_mark"] = self._get_tick_type(options.get("minor_tick_mark"))
800 axis["major_tick_mark"] = self._get_tick_type(options.get("major_tick_mark"))
802 # Check if the axis title is simple text or a formula.
803 name, name_formula = self._process_names(
804 options.get("name"), options.get("name_formula")
805 )
807 # Get an id for the data equivalent to the range formula.
808 data_id = self._get_data_id(name_formula, options.get("data"))
810 # Set the title properties.
811 axis["title"].name = name
812 axis["title"].formula = name_formula
813 axis["title"].data_id = data_id
814 axis["title"].font = self._convert_font_args(options.get("name_font"))
815 axis["title"].layout = self._get_layout_properties(
816 options.get("name_layout"), True
817 )
819 # Map the line and border properties for the axis title.
820 options["line"] = options.get("name_line")
821 options["border"] = options.get("name_border")
823 axis["title"].line = Shape._get_line_properties(options)
824 axis["title"].fill = Shape._get_fill_properties(options.get("name_fill"))
825 axis["title"].pattern = Shape._get_pattern_properties(
826 options.get("name_pattern")
827 )
828 axis["title"].gradient = Shape._get_gradient_properties(
829 options.get("name_gradient")
830 )
832 return axis
834 def _convert_font_args(self, options):
835 # Convert user defined font values into private dict values.
836 if not options:
837 return {}
839 font = {
840 "name": options.get("name"),
841 "color": options.get("color"),
842 "size": options.get("size"),
843 "bold": options.get("bold"),
844 "italic": options.get("italic"),
845 "underline": options.get("underline"),
846 "pitch_family": options.get("pitch_family"),
847 "charset": options.get("charset"),
848 "baseline": options.get("baseline", 0),
849 "rotation": options.get("rotation"),
850 }
852 # Convert font size units.
853 if font["size"]:
854 font["size"] = int(font["size"] * 100)
856 # Convert rotation into 60,000ths of a degree.
857 if font["rotation"]:
858 font["rotation"] = 60000 * int(font["rotation"])
860 if font.get("color"):
861 font["color"] = Color._from_value(font["color"])
863 return font
865 def _list_to_formula(self, data):
866 # Convert and list of row col values to a range formula.
868 # If it isn't an array ref it is probably a formula already.
869 if not isinstance(data, list):
870 # Check for unquoted sheetnames.
871 if data and " " in data and "'" not in data and self.warn_sheetname:
872 warn(
873 f"Sheetname in '{data}' contains spaces but isn't quoted. "
874 f"This may cause an error in Excel."
875 )
876 return data
878 formula = xl_range_formula(*data)
880 return formula
882 def _process_names(self, name, name_formula):
883 # Switch name and name_formula parameters if required.
885 if name is not None:
886 if isinstance(name, list):
887 # Convert a list of values into a name formula.
888 cell = xl_rowcol_to_cell(name[1], name[2], True, True)
889 name_formula = quote_sheetname(name[0]) + "!" + cell
890 name = ""
891 elif re.match(r"^=?[^!]+!\$?[A-Z]+\$?\d+", name):
892 # Name looks like a formula, use it to set name_formula.
893 name_formula = name
894 name = ""
896 return name, name_formula
898 def _get_data_type(self, data) -> str:
899 # Find the overall type of the data associated with a series.
901 # Check for no data in the series.
902 if data is None or len(data) == 0:
903 return "none"
905 if isinstance(data[0], list):
906 return "multi_str"
908 # Determine if data is numeric or strings.
909 for token in data:
910 if token is None:
911 continue
913 # Check for strings that would evaluate to float like
914 # '1.1_1' of ' 1'.
915 if isinstance(token, str) and re.search("[_ ]", token):
916 # Assume entire data series is string data.
917 return "str"
919 try:
920 float(token)
921 except ValueError:
922 # Not a number. Assume entire data series is string data.
923 return "str"
925 # The series data was all numeric.
926 return "num"
928 def _get_data_id(self, formula, data):
929 # Assign an id to a each unique series formula or title/axis formula.
930 # Repeated formulas such as for categories get the same id. If the
931 # series or title has user specified data associated with it then
932 # that is also stored. This data is used to populate cached Excel
933 # data when creating a chart. If there is no user defined data then
934 # it will be populated by the parent Workbook._add_chart_data().
936 # Ignore series without a range formula.
937 if not formula:
938 return None
940 # Strip the leading '=' from the formula.
941 if formula.startswith("="):
942 formula = formula.lstrip("=")
944 # Store the data id in a hash keyed by the formula and store the data
945 # in a separate array with the same id.
946 if formula not in self.formula_ids:
947 # Haven't seen this formula before.
948 formula_id = len(self.formula_data)
950 self.formula_data.append(data)
951 self.formula_ids[formula] = formula_id
952 else:
953 # Formula already seen. Return existing id.
954 formula_id = self.formula_ids[formula]
956 # Store user defined data if it isn't already there.
957 if self.formula_data[formula_id] is None:
958 self.formula_data[formula_id] = data
960 return formula_id
962 def _get_marker_properties(self, marker):
963 # Convert user marker properties to the structure required internally.
965 if not marker:
966 return None
968 # Copy the user defined properties since they will be modified.
969 marker = copy.deepcopy(marker)
971 types = {
972 "automatic": "automatic",
973 "none": "none",
974 "square": "square",
975 "diamond": "diamond",
976 "triangle": "triangle",
977 "x": "x",
978 "star": "star",
979 "dot": "dot",
980 "short_dash": "dot",
981 "dash": "dash",
982 "long_dash": "dash",
983 "circle": "circle",
984 "plus": "plus",
985 "picture": "picture",
986 }
988 # Check for valid types.
989 marker_type = marker.get("type")
991 if marker_type is not None:
992 if marker_type in types:
993 marker["type"] = types[marker_type]
994 else:
995 warn(f"Unknown marker type '{marker_type}")
996 return None
998 # Set the line properties for the marker.
999 line = Shape._get_line_properties(marker)
1001 # Set the fill properties for the marker.
1002 fill = Shape._get_fill_properties(marker.get("fill"))
1004 # Set the pattern fill properties for the series.
1005 pattern = Shape._get_pattern_properties(marker.get("pattern"))
1007 # Set the gradient fill properties for the series.
1008 gradient = Shape._get_gradient_properties(marker.get("gradient"))
1010 # Pattern fill overrides solid fill.
1011 if pattern:
1012 self.fill = None
1014 # Gradient fill overrides the solid and pattern fill.
1015 if gradient:
1016 pattern = None
1017 fill = None
1019 marker["line"] = line
1020 marker["fill"] = fill
1021 marker["pattern"] = pattern
1022 marker["gradient"] = gradient
1024 return marker
1026 def _get_trendline_properties(self, trendline):
1027 # Convert user trendline properties to structure required internally.
1029 if not trendline:
1030 return None
1032 # Copy the user defined properties since they will be modified.
1033 trendline = copy.deepcopy(trendline)
1035 types = {
1036 "exponential": "exp",
1037 "linear": "linear",
1038 "log": "log",
1039 "moving_average": "movingAvg",
1040 "polynomial": "poly",
1041 "power": "power",
1042 }
1044 # Check the trendline type.
1045 trend_type = trendline.get("type")
1047 if trend_type in types:
1048 trendline["type"] = types[trend_type]
1049 else:
1050 warn(f"Unknown trendline type '{trend_type}'")
1051 return None
1053 # Set the line properties for the trendline.
1054 line = Shape._get_line_properties(trendline)
1056 # Set the fill properties for the trendline.
1057 fill = Shape._get_fill_properties(trendline.get("fill"))
1059 # Set the pattern fill properties for the trendline.
1060 pattern = Shape._get_pattern_properties(trendline.get("pattern"))
1062 # Set the gradient fill properties for the trendline.
1063 gradient = Shape._get_gradient_properties(trendline.get("gradient"))
1065 # Set the format properties for the trendline label.
1066 label = self._get_trendline_label_properties(trendline.get("label"))
1068 # Pattern fill overrides solid fill.
1069 if pattern:
1070 self.fill = None
1072 # Gradient fill overrides the solid and pattern fill.
1073 if gradient:
1074 pattern = None
1075 fill = None
1077 trendline["line"] = line
1078 trendline["fill"] = fill
1079 trendline["pattern"] = pattern
1080 trendline["gradient"] = gradient
1081 trendline["label"] = label
1083 return trendline
1085 def _get_trendline_label_properties(self, label):
1086 # Convert user trendline properties to structure required internally.
1088 if not label:
1089 return {}
1091 # Copy the user defined properties since they will be modified.
1092 label = copy.deepcopy(label)
1094 # Set the font properties if present.
1095 font = self._convert_font_args(label.get("font"))
1097 # Set the line properties for the label.
1098 line = Shape._get_line_properties(label)
1100 # Set the fill properties for the label.
1101 fill = Shape._get_fill_properties(label.get("fill"))
1103 # Set the pattern fill properties for the label.
1104 pattern = Shape._get_pattern_properties(label.get("pattern"))
1106 # Set the gradient fill properties for the label.
1107 gradient = Shape._get_gradient_properties(label.get("gradient"))
1109 # Pattern fill overrides solid fill.
1110 if pattern:
1111 self.fill = None
1113 # Gradient fill overrides the solid and pattern fill.
1114 if gradient:
1115 pattern = None
1116 fill = None
1118 label["font"] = font
1119 label["line"] = line
1120 label["fill"] = fill
1121 label["pattern"] = pattern
1122 label["gradient"] = gradient
1124 return label
1126 def _get_error_bars_props(self, options):
1127 # Convert user error bars properties to structure required internally.
1128 if not options:
1129 return {}
1131 # Default values.
1132 error_bars = {"type": "fixedVal", "value": 1, "endcap": 1, "direction": "both"}
1134 types = {
1135 "fixed": "fixedVal",
1136 "percentage": "percentage",
1137 "standard_deviation": "stdDev",
1138 "standard_error": "stdErr",
1139 "custom": "cust",
1140 }
1142 # Check the error bars type.
1143 error_type = options["type"]
1145 if error_type in types:
1146 error_bars["type"] = types[error_type]
1147 else:
1148 warn(f"Unknown error bars type '{error_type}")
1149 return {}
1151 # Set the value for error types that require it.
1152 if "value" in options:
1153 error_bars["value"] = options["value"]
1155 # Set the end-cap style.
1156 if "end_style" in options:
1157 error_bars["endcap"] = options["end_style"]
1159 # Set the error bar direction.
1160 if "direction" in options:
1161 if options["direction"] == "minus":
1162 error_bars["direction"] = "minus"
1163 elif options["direction"] == "plus":
1164 error_bars["direction"] = "plus"
1165 else:
1166 # Default to 'both'.
1167 pass
1169 # Set any custom values.
1170 error_bars["plus_values"] = options.get("plus_values")
1171 error_bars["minus_values"] = options.get("minus_values")
1172 error_bars["plus_data"] = options.get("plus_data")
1173 error_bars["minus_data"] = options.get("minus_data")
1175 # Set the line properties for the error bars.
1176 error_bars["line"] = Shape._get_line_properties(options)
1178 return error_bars
1180 def _get_gridline_properties(self, options):
1181 # Convert user gridline properties to structure required internally.
1183 # Set the visible property for the gridline.
1184 gridline = {"visible": options.get("visible")}
1186 # Set the line properties for the gridline.
1187 gridline["line"] = Shape._get_line_properties(options)
1189 return gridline
1191 def _get_labels_properties(self, labels):
1192 # Convert user labels properties to the structure required internally.
1194 if not labels:
1195 return None
1197 # Copy the user defined properties since they will be modified.
1198 labels = copy.deepcopy(labels)
1200 # Map user defined label positions to Excel positions.
1201 position = labels.get("position")
1203 if position:
1204 if position in self.label_positions:
1205 if position == self.label_position_default:
1206 labels["position"] = None
1207 else:
1208 labels["position"] = self.label_positions[position]
1209 else:
1210 warn(f"Unsupported label position '{position}' for this chart type")
1211 return None
1213 # Map the user defined label separator to the Excel separator.
1214 separator = labels.get("separator")
1215 separators = {
1216 ",": ", ",
1217 ";": "; ",
1218 ".": ". ",
1219 "\n": "\n",
1220 " ": " ",
1221 }
1223 if separator:
1224 if separator in separators:
1225 labels["separator"] = separators[separator]
1226 else:
1227 warn("Unsupported label separator")
1228 return None
1230 # Set the font properties if present.
1231 labels["font"] = self._convert_font_args(labels.get("font"))
1233 # Set the line properties for the labels.
1234 line = Shape._get_line_properties(labels)
1236 # Set the fill properties for the labels.
1237 fill = Shape._get_fill_properties(labels.get("fill"))
1239 # Set the pattern fill properties for the labels.
1240 pattern = Shape._get_pattern_properties(labels.get("pattern"))
1242 # Set the gradient fill properties for the labels.
1243 gradient = Shape._get_gradient_properties(labels.get("gradient"))
1245 # Pattern fill overrides solid fill.
1246 if pattern:
1247 self.fill = None
1249 # Gradient fill overrides the solid and pattern fill.
1250 if gradient:
1251 pattern = None
1252 fill = None
1254 labels["line"] = line
1255 labels["fill"] = fill
1256 labels["pattern"] = pattern
1257 labels["gradient"] = gradient
1259 if labels.get("custom"):
1260 for label in labels["custom"]:
1261 if label is None:
1262 continue
1264 value = label.get("value")
1265 if value and re.match(r"^=?[^!]+!\$?[A-Z]+\$?\d+", str(value)):
1266 label["formula"] = value
1268 formula = label.get("formula")
1269 if formula and formula.startswith("="):
1270 label["formula"] = formula.lstrip("=")
1272 data_id = self._get_data_id(formula, label.get("data"))
1273 label["data_id"] = data_id
1275 label["font"] = self._convert_font_args(label.get("font"))
1277 # Set the line properties for the label.
1278 line = Shape._get_line_properties(label)
1280 # Set the fill properties for the label.
1281 fill = Shape._get_fill_properties(label.get("fill"))
1283 # Set the pattern fill properties for the label.
1284 pattern = Shape._get_pattern_properties(label.get("pattern"))
1286 # Set the gradient fill properties for the label.
1287 gradient = Shape._get_gradient_properties(label.get("gradient"))
1289 # Pattern fill overrides solid fill.
1290 if pattern:
1291 self.fill = None
1293 # Gradient fill overrides the solid and pattern fill.
1294 if gradient:
1295 pattern = None
1296 fill = None
1298 # Map user defined label positions to Excel positions.
1299 position = label.get("position")
1301 if position:
1302 if position in self.label_positions:
1303 if position == self.label_position_default:
1304 label["position"] = None
1305 else:
1306 label["position"] = self.label_positions[position]
1307 else:
1308 warn(f"Unsupported label position '{position}' for chart type")
1309 return None
1311 label["line"] = line
1312 label["fill"] = fill
1313 label["pattern"] = pattern
1314 label["gradient"] = gradient
1316 return labels
1318 def _get_area_properties(self, options):
1319 # Convert user area properties to the structure required internally.
1320 area = {}
1322 # Set the line properties for the chartarea.
1323 line = Shape._get_line_properties(options)
1325 # Set the fill properties for the chartarea.
1326 fill = Shape._get_fill_properties(options.get("fill"))
1328 # Set the pattern fill properties for the series.
1329 pattern = Shape._get_pattern_properties(options.get("pattern"))
1331 # Set the gradient fill properties for the series.
1332 gradient = Shape._get_gradient_properties(options.get("gradient"))
1334 # Pattern fill overrides solid fill.
1335 if pattern:
1336 self.fill = None
1338 # Gradient fill overrides the solid and pattern fill.
1339 if gradient:
1340 pattern = None
1341 fill = None
1343 # Set the plotarea layout.
1344 layout = self._get_layout_properties(options.get("layout"), False)
1346 area["line"] = line
1347 area["fill"] = fill
1348 area["pattern"] = pattern
1349 area["layout"] = layout
1350 area["gradient"] = gradient
1352 return area
1354 def _get_legend_properties(self, options: Optional[Dict[str, Any]] = None):
1355 # Convert user legend properties to the structure required internally.
1356 legend = {}
1358 if options is None:
1359 options = {}
1361 legend["position"] = options.get("position", "right")
1362 legend["delete_series"] = options.get("delete_series")
1363 legend["font"] = self._convert_font_args(options.get("font"))
1364 legend["layout"] = self._get_layout_properties(options.get("layout"), False)
1366 # Turn off the legend.
1367 if options.get("none"):
1368 legend["position"] = "none"
1370 # Set the line properties for the legend.
1371 line = Shape._get_line_properties(options)
1373 # Set the fill properties for the legend.
1374 fill = Shape._get_fill_properties(options.get("fill"))
1376 # Set the pattern fill properties for the series.
1377 pattern = Shape._get_pattern_properties(options.get("pattern"))
1379 # Set the gradient fill properties for the series.
1380 gradient = Shape._get_gradient_properties(options.get("gradient"))
1382 # Pattern fill overrides solid fill.
1383 if pattern:
1384 self.fill = None
1386 # Gradient fill overrides the solid and pattern fill.
1387 if gradient:
1388 pattern = None
1389 fill = None
1391 # Set the legend layout.
1392 layout = self._get_layout_properties(options.get("layout"), False)
1394 legend["line"] = line
1395 legend["fill"] = fill
1396 legend["pattern"] = pattern
1397 legend["layout"] = layout
1398 legend["gradient"] = gradient
1400 return legend
1402 def _get_layout_properties(self, args, is_text):
1403 # Convert user defined layout properties to format used internally.
1404 layout = {}
1406 if not args:
1407 return {}
1409 if is_text:
1410 properties = ("x", "y")
1411 else:
1412 properties = ("x", "y", "width", "height")
1414 # Check for valid properties.
1415 for key in args.keys():
1416 if key not in properties:
1417 warn(f"Property '{key}' not supported in layout options")
1418 return {}
1420 # Set the layout properties.
1421 for prop in properties:
1422 if prop not in args.keys():
1423 warn(f"Property '{prop}' must be specified in layout options")
1424 return {}
1426 value = args[prop]
1428 try:
1429 float(value)
1430 except ValueError:
1431 warn(f"Property '{prop}' value '{value}' must be numeric in layout")
1432 return {}
1434 if value < 0 or value > 1:
1435 warn(
1436 f"Property '{prop}' value '{value}' must be in range "
1437 f"0 < x <= 1 in layout options"
1438 )
1439 return {}
1441 # Convert to the format used by Excel for easier testing
1442 layout[prop] = f"{value:.17g}"
1444 return layout
1446 def _get_points_properties(self, user_points):
1447 # Convert user points properties to structure required internally.
1448 points = []
1450 if not user_points:
1451 return []
1453 for user_point in user_points:
1454 point = {}
1456 if user_point is not None:
1457 # Set the line properties for the point.
1458 line = Shape._get_line_properties(user_point)
1460 # Set the fill properties for the chartarea.
1461 fill = Shape._get_fill_properties(user_point.get("fill"))
1463 # Set the pattern fill properties for the series.
1464 pattern = Shape._get_pattern_properties(user_point.get("pattern"))
1466 # Set the gradient fill properties for the series.
1467 gradient = Shape._get_gradient_properties(user_point.get("gradient"))
1469 # Pattern fill overrides solid fill.
1470 if pattern:
1471 self.fill = None
1473 # Gradient fill overrides the solid and pattern fill.
1474 if gradient:
1475 pattern = None
1476 fill = None
1478 point["line"] = line
1479 point["fill"] = fill
1480 point["pattern"] = pattern
1481 point["gradient"] = gradient
1483 points.append(point)
1485 return points
1487 def _has_formatting(self, element: dict) -> bool:
1488 # Check if a chart element has line, fill or gradient formatting.
1489 has_fill = element.get("fill") and element["fill"]["defined"]
1490 has_line = element.get("line") and element["line"]["defined"]
1491 has_pattern = element.get("pattern")
1492 has_gradient = element.get("gradient")
1494 return has_fill or has_line or has_pattern or has_gradient
1496 def _get_display_units(self, display_units):
1497 # Convert user defined display units to internal units.
1498 if not display_units:
1499 return None
1501 types = {
1502 "hundreds": "hundreds",
1503 "thousands": "thousands",
1504 "ten_thousands": "tenThousands",
1505 "hundred_thousands": "hundredThousands",
1506 "millions": "millions",
1507 "ten_millions": "tenMillions",
1508 "hundred_millions": "hundredMillions",
1509 "billions": "billions",
1510 "trillions": "trillions",
1511 }
1513 if display_units in types:
1514 display_units = types[display_units]
1515 else:
1516 warn(f"Unknown display_units type '{display_units}'")
1517 return None
1519 return display_units
1521 def _get_tick_type(self, tick_type):
1522 # Convert user defined display units to internal units.
1523 if not tick_type:
1524 return None
1526 types = {
1527 "outside": "out",
1528 "inside": "in",
1529 "none": "none",
1530 "cross": "cross",
1531 }
1533 if tick_type in types:
1534 tick_type = types[tick_type]
1535 else:
1536 warn(f"Unknown tick_type '{tick_type}'")
1537 return None
1539 return tick_type
1541 def _get_primary_axes_series(self):
1542 # Returns series which use the primary axes.
1543 primary_axes_series = []
1545 for series in self.series:
1546 if not series["y2_axis"]:
1547 primary_axes_series.append(series)
1549 return primary_axes_series
1551 def _get_secondary_axes_series(self):
1552 # Returns series which use the secondary axes.
1553 secondary_axes_series = []
1555 for series in self.series:
1556 if series["y2_axis"]:
1557 secondary_axes_series.append(series)
1559 return secondary_axes_series
1561 def _add_axis_ids(self, args) -> None:
1562 # Add unique ids for primary or secondary axes
1563 chart_id = 5001 + int(self.id)
1564 axis_count = 1 + len(self.axis2_ids) + len(self.axis_ids)
1566 id1 = f"{chart_id:04d}{axis_count:04d}"
1567 id2 = f"{chart_id:04d}{axis_count + 1:04d}"
1569 if args["primary_axes"]:
1570 self.axis_ids.append(id1)
1571 self.axis_ids.append(id2)
1573 if not args["primary_axes"]:
1574 self.axis2_ids.append(id1)
1575 self.axis2_ids.append(id2)
1577 def _set_default_properties(self) -> None:
1578 # Setup the default properties for a chart.
1580 self.x_axis["defaults"] = {
1581 "num_format": "General",
1582 "major_gridlines": {"visible": 0},
1583 }
1585 self.y_axis["defaults"] = {
1586 "num_format": "General",
1587 "major_gridlines": {"visible": 1},
1588 }
1590 self.x2_axis["defaults"] = {
1591 "num_format": "General",
1592 "label_position": "none",
1593 "crossing": "max",
1594 "visible": 0,
1595 }
1597 self.y2_axis["defaults"] = {
1598 "num_format": "General",
1599 "major_gridlines": {"visible": 0},
1600 "position": "right",
1601 "visible": 1,
1602 }
1604 self.set_x_axis({})
1605 self.set_y_axis({})
1607 self.set_x2_axis({})
1608 self.set_y2_axis({})
1610 ###########################################################################
1611 #
1612 # XML methods.
1613 #
1614 ###########################################################################
1616 def _write_chart_space(self) -> None:
1617 # Write the <c:chartSpace> element.
1618 schema = "http://schemas.openxmlformats.org/"
1619 xmlns_c = schema + "drawingml/2006/chart"
1620 xmlns_a = schema + "drawingml/2006/main"
1621 xmlns_r = schema + "officeDocument/2006/relationships"
1623 attributes = [
1624 ("xmlns:c", xmlns_c),
1625 ("xmlns:a", xmlns_a),
1626 ("xmlns:r", xmlns_r),
1627 ]
1629 self._xml_start_tag("c:chartSpace", attributes)
1631 def _write_lang(self) -> None:
1632 # Write the <c:lang> element.
1633 val = "en-US"
1635 attributes = [("val", val)]
1637 self._xml_empty_tag("c:lang", attributes)
1639 def _write_style(self) -> None:
1640 # Write the <c:style> element.
1641 style_id = self.style_id
1643 # Don't write an element for the default style, 2.
1644 if style_id == 2:
1645 return
1647 attributes = [("val", style_id)]
1649 self._xml_empty_tag("c:style", attributes)
1651 def _write_chart(self) -> None:
1652 # Write the <c:chart> element.
1653 self._xml_start_tag("c:chart")
1655 if self.title.is_hidden():
1656 # Turn off the title.
1657 self._write_c_auto_title_deleted()
1658 else:
1659 # Write the chart title elements.
1660 self._write_title(self.title)
1662 # Write the c:plotArea element.
1663 self._write_plot_area()
1665 # Write the c:legend element.
1666 self._write_legend()
1668 # Write the c:plotVisOnly element.
1669 self._write_plot_vis_only()
1671 # Write the c:dispBlanksAs element.
1672 self._write_disp_blanks_as()
1674 # Write the c:extLst element.
1675 if self.show_na_as_empty:
1676 self._write_c_ext_lst_display_na()
1678 self._xml_end_tag("c:chart")
1680 def _write_disp_blanks_as(self) -> None:
1681 # Write the <c:dispBlanksAs> element.
1682 val = self.show_blanks
1684 # Ignore the default value.
1685 if val == "gap":
1686 return
1688 attributes = [("val", val)]
1690 self._xml_empty_tag("c:dispBlanksAs", attributes)
1692 def _write_plot_area(self) -> None:
1693 # Write the <c:plotArea> element.
1694 self._xml_start_tag("c:plotArea")
1696 # Write the c:layout element.
1697 self._write_layout(self.plotarea.get("layout"), "plot")
1699 # Write subclass chart type elements for primary and secondary axes.
1700 self._write_chart_type({"primary_axes": True})
1701 self._write_chart_type({"primary_axes": False})
1703 # Configure a combined chart if present.
1704 second_chart = self.combined
1705 if second_chart:
1706 # Secondary axis has unique id otherwise use same as primary.
1707 if second_chart.is_secondary:
1708 second_chart.id = 1000 + self.id
1709 else:
1710 second_chart.id = self.id
1712 # Share the same filehandle for writing.
1713 second_chart.fh = self.fh
1715 # Share series index with primary chart.
1716 second_chart.series_index = self.series_index
1718 # Write the subclass chart type elements for combined chart.
1719 second_chart._write_chart_type({"primary_axes": True})
1720 second_chart._write_chart_type({"primary_axes": False})
1722 # Write the category and value elements for the primary axes.
1723 args = {"x_axis": self.x_axis, "y_axis": self.y_axis, "axis_ids": self.axis_ids}
1725 if self.date_category:
1726 self._write_date_axis(args)
1727 else:
1728 self._write_cat_axis(args)
1730 self._write_val_axis(args)
1732 # Write the category and value elements for the secondary axes.
1733 args = {
1734 "x_axis": self.x2_axis,
1735 "y_axis": self.y2_axis,
1736 "axis_ids": self.axis2_ids,
1737 }
1739 self._write_val_axis(args)
1741 # Write the secondary axis for the secondary chart.
1742 if second_chart and second_chart.is_secondary:
1743 args = {
1744 "x_axis": second_chart.x2_axis,
1745 "y_axis": second_chart.y2_axis,
1746 "axis_ids": second_chart.axis2_ids,
1747 }
1749 second_chart._write_val_axis(args)
1751 if self.date_category:
1752 self._write_date_axis(args)
1753 else:
1754 self._write_cat_axis(args)
1756 # Write the c:dTable element.
1757 self._write_d_table()
1759 # Write the c:spPr element for the plotarea formatting.
1760 self._write_sp_pr(self.plotarea)
1762 self._xml_end_tag("c:plotArea")
1764 def _write_layout(self, layout, layout_type) -> None:
1765 # Write the <c:layout> element.
1767 if not layout:
1768 # Automatic layout.
1769 self._xml_empty_tag("c:layout")
1770 else:
1771 # User defined manual layout.
1772 self._xml_start_tag("c:layout")
1773 self._write_manual_layout(layout, layout_type)
1774 self._xml_end_tag("c:layout")
1776 def _write_manual_layout(self, layout, layout_type) -> None:
1777 # Write the <c:manualLayout> element.
1778 self._xml_start_tag("c:manualLayout")
1780 # Plotarea has a layoutTarget element.
1781 if layout_type == "plot":
1782 self._xml_empty_tag("c:layoutTarget", [("val", "inner")])
1784 # Set the x, y positions.
1785 self._xml_empty_tag("c:xMode", [("val", "edge")])
1786 self._xml_empty_tag("c:yMode", [("val", "edge")])
1787 self._xml_empty_tag("c:x", [("val", layout["x"])])
1788 self._xml_empty_tag("c:y", [("val", layout["y"])])
1790 # For plotarea and legend set the width and height.
1791 if layout_type != "text":
1792 self._xml_empty_tag("c:w", [("val", layout["width"])])
1793 self._xml_empty_tag("c:h", [("val", layout["height"])])
1795 self._xml_end_tag("c:manualLayout")
1797 def _write_chart_type(self, args) -> None:
1798 # pylint: disable=unused-argument
1799 # Write the chart type element. This method should be overridden
1800 # by the subclasses.
1801 return
1803 def _write_grouping(self, val) -> None:
1804 # Write the <c:grouping> element.
1805 attributes = [("val", val)]
1807 self._xml_empty_tag("c:grouping", attributes)
1809 def _write_series(self, series) -> None:
1810 # Write the series elements.
1811 self._write_ser(series)
1813 def _write_ser(self, series) -> None:
1814 # Write the <c:ser> element.
1815 index = self.series_index
1816 self.series_index += 1
1818 self._xml_start_tag("c:ser")
1820 # Write the c:idx element.
1821 self._write_idx(index)
1823 # Write the c:order element.
1824 self._write_order(index)
1826 # Write the series name.
1827 self._write_series_name(series)
1829 # Write the c:spPr element.
1830 self._write_sp_pr(series)
1832 # Write the c:marker element.
1833 self._write_marker(series["marker"])
1835 # Write the c:invertIfNegative element.
1836 self._write_c_invert_if_negative(series["invert_if_neg"])
1838 # Write the c:dPt element.
1839 self._write_d_pt(series["points"])
1841 # Write the c:dLbls element.
1842 self._write_d_lbls(series["labels"])
1844 # Write the c:trendline element.
1845 self._write_trendline(series["trendline"])
1847 # Write the c:errBars element.
1848 self._write_error_bars(series["error_bars"])
1850 # Write the c:cat element.
1851 self._write_cat(series)
1853 # Write the c:val element.
1854 self._write_val(series)
1856 # Write the c:smooth element.
1857 if self.smooth_allowed:
1858 self._write_c_smooth(series["smooth"])
1860 # Write the c:extLst element.
1861 if series.get("inverted_color"):
1862 self._write_c_ext_lst_inverted_color(series["inverted_color"])
1864 self._xml_end_tag("c:ser")
1866 def _write_c_ext_lst_inverted_color(self, color: Color) -> None:
1867 # Write the <c:extLst> element for the inverted fill color.
1869 uri = "{6F2FDCE9-48DA-4B69-8628-5D25D57E5C99}"
1870 xmlns_c_14 = "http://schemas.microsoft.com/office/drawing/2007/8/2/chart"
1872 attributes1 = [
1873 ("uri", uri),
1874 ("xmlns:c14", xmlns_c_14),
1875 ]
1877 attributes2 = [("xmlns:c14", xmlns_c_14)]
1879 self._xml_start_tag("c:extLst")
1880 self._xml_start_tag("c:ext", attributes1)
1881 self._xml_start_tag("c14:invertSolidFillFmt")
1882 self._xml_start_tag("c14:spPr", attributes2)
1884 self._write_a_solid_fill({"color": color})
1886 self._xml_end_tag("c14:spPr")
1887 self._xml_end_tag("c14:invertSolidFillFmt")
1888 self._xml_end_tag("c:ext")
1889 self._xml_end_tag("c:extLst")
1891 def _write_c_ext_lst_display_na(self) -> None:
1892 # Write the <c:extLst> element for the display NA as empty cell option.
1894 uri = "{56B9EC1D-385E-4148-901F-78D8002777C0}"
1895 xmlns_c_16 = "http://schemas.microsoft.com/office/drawing/2017/03/chart"
1897 attributes1 = [
1898 ("uri", uri),
1899 ("xmlns:c16r3", xmlns_c_16),
1900 ]
1902 attributes2 = [("val", 1)]
1904 self._xml_start_tag("c:extLst")
1905 self._xml_start_tag("c:ext", attributes1)
1906 self._xml_start_tag("c16r3:dataDisplayOptions16")
1907 self._xml_empty_tag("c16r3:dispNaAsBlank", attributes2)
1908 self._xml_end_tag("c16r3:dataDisplayOptions16")
1909 self._xml_end_tag("c:ext")
1910 self._xml_end_tag("c:extLst")
1912 def _write_idx(self, val) -> None:
1913 # Write the <c:idx> element.
1915 attributes = [("val", val)]
1917 self._xml_empty_tag("c:idx", attributes)
1919 def _write_order(self, val) -> None:
1920 # Write the <c:order> element.
1922 attributes = [("val", val)]
1924 self._xml_empty_tag("c:order", attributes)
1926 def _write_series_name(self, series) -> None:
1927 # Write the series name.
1929 if series["name_formula"] is not None:
1930 self._write_tx_formula(series["name_formula"], series["name_id"])
1931 elif series["name"] is not None:
1932 self._write_tx_value(series["name"])
1934 def _write_c_smooth(self, smooth) -> None:
1935 # Write the <c:smooth> element.
1937 if smooth:
1938 self._xml_empty_tag("c:smooth", [("val", "1")])
1940 def _write_cat(self, series) -> None:
1941 # Write the <c:cat> element.
1942 formula = series["categories"]
1943 data_id = series["cat_data_id"]
1944 data = None
1946 if data_id is not None:
1947 data = self.formula_data[data_id]
1949 # Ignore <c:cat> elements for charts without category values.
1950 if not formula:
1951 return
1953 self._xml_start_tag("c:cat")
1955 # Check the type of cached data.
1956 cat_type = self._get_data_type(data)
1958 if cat_type == "str":
1959 self.cat_has_num_fmt = False
1960 # Write the c:numRef element.
1961 self._write_str_ref(formula, data, cat_type)
1963 elif cat_type == "multi_str":
1964 self.cat_has_num_fmt = False
1965 # Write the c:numRef element.
1966 self._write_multi_lvl_str_ref(formula, data)
1968 else:
1969 self.cat_has_num_fmt = True
1970 # Write the c:numRef element.
1971 self._write_num_ref(formula, data, cat_type)
1973 self._xml_end_tag("c:cat")
1975 def _write_val(self, series) -> None:
1976 # Write the <c:val> element.
1977 formula = series["values"]
1978 data_id = series["val_data_id"]
1979 data = self.formula_data[data_id]
1981 self._xml_start_tag("c:val")
1983 # Unlike Cat axes data should only be numeric.
1984 # Write the c:numRef element.
1985 self._write_num_ref(formula, data, "num")
1987 self._xml_end_tag("c:val")
1989 def _write_num_ref(self, formula, data, ref_type) -> None:
1990 # Write the <c:numRef> element.
1991 self._xml_start_tag("c:numRef")
1993 # Write the c:f element.
1994 self._write_series_formula(formula)
1996 if ref_type == "num":
1997 # Write the c:numCache element.
1998 self._write_num_cache(data)
1999 elif ref_type == "str":
2000 # Write the c:strCache element.
2001 self._write_str_cache(data)
2003 self._xml_end_tag("c:numRef")
2005 def _write_str_ref(self, formula, data, ref_type) -> None:
2006 # Write the <c:strRef> element.
2008 self._xml_start_tag("c:strRef")
2010 # Write the c:f element.
2011 self._write_series_formula(formula)
2013 if ref_type == "num":
2014 # Write the c:numCache element.
2015 self._write_num_cache(data)
2016 elif ref_type == "str":
2017 # Write the c:strCache element.
2018 self._write_str_cache(data)
2020 self._xml_end_tag("c:strRef")
2022 def _write_multi_lvl_str_ref(self, formula, data) -> None:
2023 # Write the <c:multiLvlStrRef> element.
2025 if not data:
2026 return
2028 self._xml_start_tag("c:multiLvlStrRef")
2030 # Write the c:f element.
2031 self._write_series_formula(formula)
2033 self._xml_start_tag("c:multiLvlStrCache")
2035 # Write the c:ptCount element.
2036 count = len(data[-1])
2037 self._write_pt_count(count)
2039 for cat_data in reversed(data):
2040 self._xml_start_tag("c:lvl")
2042 for i, point in enumerate(cat_data):
2043 # Write the c:pt element.
2044 self._write_pt(i, point)
2046 self._xml_end_tag("c:lvl")
2048 self._xml_end_tag("c:multiLvlStrCache")
2049 self._xml_end_tag("c:multiLvlStrRef")
2051 def _write_series_formula(self, formula) -> None:
2052 # Write the <c:f> element.
2054 # Strip the leading '=' from the formula.
2055 if formula.startswith("="):
2056 formula = formula.lstrip("=")
2058 self._xml_data_element("c:f", formula)
2060 def _write_axis_ids(self, args) -> None:
2061 # Write the <c:axId> elements for the primary or secondary axes.
2063 # Generate the axis ids.
2064 self._add_axis_ids(args)
2066 if args["primary_axes"]:
2067 # Write the axis ids for the primary axes.
2068 self._write_axis_id(self.axis_ids[0])
2069 self._write_axis_id(self.axis_ids[1])
2070 else:
2071 # Write the axis ids for the secondary axes.
2072 self._write_axis_id(self.axis2_ids[0])
2073 self._write_axis_id(self.axis2_ids[1])
2075 def _write_axis_id(self, val) -> None:
2076 # Write the <c:axId> element.
2078 attributes = [("val", val)]
2080 self._xml_empty_tag("c:axId", attributes)
2082 def _write_cat_axis(self, args) -> None:
2083 # Write the <c:catAx> element. Usually the X axis.
2084 x_axis = args["x_axis"]
2085 y_axis = args["y_axis"]
2086 axis_ids = args["axis_ids"]
2088 # If there are no axis_ids then we don't need to write this element.
2089 if axis_ids is None or not axis_ids:
2090 return
2092 position = self.cat_axis_position
2093 is_horizontal = self.horiz_cat_axis
2095 # Overwrite the default axis position with a user supplied value.
2096 if x_axis.get("position"):
2097 position = x_axis["position"]
2099 self._xml_start_tag("c:catAx")
2101 self._write_axis_id(axis_ids[0])
2103 # Write the c:scaling element.
2104 self._write_scaling(x_axis.get("reverse"), None, None, None)
2106 if not x_axis.get("visible"):
2107 self._write_delete(1)
2109 # Write the c:axPos element.
2110 self._write_axis_pos(position, y_axis.get("reverse"))
2112 # Write the c:majorGridlines element.
2113 self._write_major_gridlines(x_axis.get("major_gridlines"))
2115 # Write the c:minorGridlines element.
2116 self._write_minor_gridlines(x_axis.get("minor_gridlines"))
2118 # Write the axis title elements.
2119 self._write_title(x_axis["title"], is_horizontal)
2121 # Write the c:numFmt element.
2122 self._write_cat_number_format(x_axis)
2124 # Write the c:majorTickMark element.
2125 self._write_major_tick_mark(x_axis.get("major_tick_mark"))
2127 # Write the c:minorTickMark element.
2128 self._write_minor_tick_mark(x_axis.get("minor_tick_mark"))
2130 # Write the c:tickLblPos element.
2131 self._write_tick_label_pos(x_axis.get("label_position"))
2133 # Write the c:spPr element for the axis line.
2134 self._write_sp_pr(x_axis)
2136 # Write the axis font elements.
2137 self._write_axis_font(x_axis.get("num_font"))
2139 # Write the c:crossAx element.
2140 self._write_cross_axis(axis_ids[1])
2142 if self.show_crosses or x_axis.get("visible"):
2143 # Note, the category crossing comes from the value axis.
2144 if (
2145 y_axis.get("crossing") is None
2146 or y_axis.get("crossing") == "max"
2147 or y_axis["crossing"] == "min"
2148 ):
2149 # Write the c:crosses element.
2150 self._write_crosses(y_axis.get("crossing"))
2151 else:
2152 # Write the c:crossesAt element.
2153 self._write_c_crosses_at(y_axis.get("crossing"))
2155 # Write the c:auto element.
2156 if not x_axis.get("text_axis"):
2157 self._write_auto(1)
2159 # Write the c:labelAlign element.
2160 self._write_label_align(x_axis.get("label_align"))
2162 # Write the c:labelOffset element.
2163 self._write_label_offset(100)
2165 # Write the c:tickLblSkip element.
2166 self._write_c_tick_lbl_skip(x_axis.get("interval_unit"))
2168 # Write the c:tickMarkSkip element.
2169 self._write_c_tick_mark_skip(x_axis.get("interval_tick"))
2171 self._xml_end_tag("c:catAx")
2173 def _write_val_axis(self, args) -> None:
2174 # Write the <c:valAx> element. Usually the Y axis.
2175 x_axis = args["x_axis"]
2176 y_axis = args["y_axis"]
2177 axis_ids = args["axis_ids"]
2178 position = args.get("position", self.val_axis_position)
2179 is_horizontal = self.horiz_val_axis
2181 # If there are no axis_ids then we don't need to write this element.
2182 if axis_ids is None or not axis_ids:
2183 return
2185 # Overwrite the default axis position with a user supplied value.
2186 position = y_axis.get("position") or position
2188 self._xml_start_tag("c:valAx")
2190 self._write_axis_id(axis_ids[1])
2192 # Write the c:scaling element.
2193 self._write_scaling(
2194 y_axis.get("reverse"),
2195 y_axis.get("min"),
2196 y_axis.get("max"),
2197 y_axis.get("log_base"),
2198 )
2200 if not y_axis.get("visible"):
2201 self._write_delete(1)
2203 # Write the c:axPos element.
2204 self._write_axis_pos(position, x_axis.get("reverse"))
2206 # Write the c:majorGridlines element.
2207 self._write_major_gridlines(y_axis.get("major_gridlines"))
2209 # Write the c:minorGridlines element.
2210 self._write_minor_gridlines(y_axis.get("minor_gridlines"))
2212 # Write the axis title elements.
2213 self._write_title(y_axis["title"], is_horizontal)
2215 # Write the c:numberFormat element.
2216 self._write_number_format(y_axis)
2218 # Write the c:majorTickMark element.
2219 self._write_major_tick_mark(y_axis.get("major_tick_mark"))
2221 # Write the c:minorTickMark element.
2222 self._write_minor_tick_mark(y_axis.get("minor_tick_mark"))
2224 # Write the c:tickLblPos element.
2225 self._write_tick_label_pos(y_axis.get("label_position"))
2227 # Write the c:spPr element for the axis line.
2228 self._write_sp_pr(y_axis)
2230 # Write the axis font elements.
2231 self._write_axis_font(y_axis.get("num_font"))
2233 # Write the c:crossAx element.
2234 self._write_cross_axis(axis_ids[0])
2236 # Note, the category crossing comes from the value axis.
2237 if (
2238 x_axis.get("crossing") is None
2239 or x_axis["crossing"] == "max"
2240 or x_axis["crossing"] == "min"
2241 ):
2242 # Write the c:crosses element.
2243 self._write_crosses(x_axis.get("crossing"))
2244 else:
2245 # Write the c:crossesAt element.
2246 self._write_c_crosses_at(x_axis.get("crossing"))
2248 # Write the c:crossBetween element.
2249 self._write_cross_between(x_axis.get("position_axis"))
2251 # Write the c:majorUnit element.
2252 self._write_c_major_unit(y_axis.get("major_unit"))
2254 # Write the c:minorUnit element.
2255 self._write_c_minor_unit(y_axis.get("minor_unit"))
2257 # Write the c:dispUnits element.
2258 self._write_disp_units(
2259 y_axis.get("display_units"), y_axis.get("display_units_visible")
2260 )
2262 self._xml_end_tag("c:valAx")
2264 def _write_cat_val_axis(self, args) -> None:
2265 # Write the <c:valAx> element. This is for the second valAx
2266 # in scatter plots. Usually the X axis.
2267 x_axis = args["x_axis"]
2268 y_axis = args["y_axis"]
2269 axis_ids = args["axis_ids"]
2270 position = args["position"] or self.val_axis_position
2271 is_horizontal = self.horiz_val_axis
2273 # If there are no axis_ids then we don't need to write this element.
2274 if axis_ids is None or not axis_ids:
2275 return
2277 # Overwrite the default axis position with a user supplied value.
2278 position = x_axis.get("position") or position
2280 self._xml_start_tag("c:valAx")
2282 self._write_axis_id(axis_ids[0])
2284 # Write the c:scaling element.
2285 self._write_scaling(
2286 x_axis.get("reverse"),
2287 x_axis.get("min"),
2288 x_axis.get("max"),
2289 x_axis.get("log_base"),
2290 )
2292 if not x_axis.get("visible"):
2293 self._write_delete(1)
2295 # Write the c:axPos element.
2296 self._write_axis_pos(position, y_axis.get("reverse"))
2298 # Write the c:majorGridlines element.
2299 self._write_major_gridlines(x_axis.get("major_gridlines"))
2301 # Write the c:minorGridlines element.
2302 self._write_minor_gridlines(x_axis.get("minor_gridlines"))
2304 # Write the axis title elements.
2305 self._write_title(x_axis["title"], is_horizontal)
2307 # Write the c:numberFormat element.
2308 self._write_number_format(x_axis)
2310 # Write the c:majorTickMark element.
2311 self._write_major_tick_mark(x_axis.get("major_tick_mark"))
2313 # Write the c:minorTickMark element.
2314 self._write_minor_tick_mark(x_axis.get("minor_tick_mark"))
2316 # Write the c:tickLblPos element.
2317 self._write_tick_label_pos(x_axis.get("label_position"))
2319 # Write the c:spPr element for the axis line.
2320 self._write_sp_pr(x_axis)
2322 # Write the axis font elements.
2323 self._write_axis_font(x_axis.get("num_font"))
2325 # Write the c:crossAx element.
2326 self._write_cross_axis(axis_ids[1])
2328 # Note, the category crossing comes from the value axis.
2329 if (
2330 y_axis.get("crossing") is None
2331 or y_axis["crossing"] == "max"
2332 or y_axis["crossing"] == "min"
2333 ):
2334 # Write the c:crosses element.
2335 self._write_crosses(y_axis.get("crossing"))
2336 else:
2337 # Write the c:crossesAt element.
2338 self._write_c_crosses_at(y_axis.get("crossing"))
2340 # Write the c:crossBetween element.
2341 self._write_cross_between(y_axis.get("position_axis"))
2343 # Write the c:majorUnit element.
2344 self._write_c_major_unit(x_axis.get("major_unit"))
2346 # Write the c:minorUnit element.
2347 self._write_c_minor_unit(x_axis.get("minor_unit"))
2349 # Write the c:dispUnits element.
2350 self._write_disp_units(
2351 x_axis.get("display_units"), x_axis.get("display_units_visible")
2352 )
2354 self._xml_end_tag("c:valAx")
2356 def _write_date_axis(self, args) -> None:
2357 # Write the <c:dateAx> element. Usually the X axis.
2358 x_axis = args["x_axis"]
2359 y_axis = args["y_axis"]
2360 axis_ids = args["axis_ids"]
2362 # If there are no axis_ids then we don't need to write this element.
2363 if axis_ids is None or not axis_ids:
2364 return
2366 position = self.cat_axis_position
2368 # Overwrite the default axis position with a user supplied value.
2369 position = x_axis.get("position") or position
2371 self._xml_start_tag("c:dateAx")
2373 self._write_axis_id(axis_ids[0])
2375 # Write the c:scaling element.
2376 self._write_scaling(
2377 x_axis.get("reverse"),
2378 x_axis.get("min"),
2379 x_axis.get("max"),
2380 x_axis.get("log_base"),
2381 )
2383 if not x_axis.get("visible"):
2384 self._write_delete(1)
2386 # Write the c:axPos element.
2387 self._write_axis_pos(position, y_axis.get("reverse"))
2389 # Write the c:majorGridlines element.
2390 self._write_major_gridlines(x_axis.get("major_gridlines"))
2392 # Write the c:minorGridlines element.
2393 self._write_minor_gridlines(x_axis.get("minor_gridlines"))
2395 # Write the axis title elements.
2396 self._write_title(x_axis["title"])
2398 # Write the c:numFmt element.
2399 self._write_number_format(x_axis)
2401 # Write the c:majorTickMark element.
2402 self._write_major_tick_mark(x_axis.get("major_tick_mark"))
2404 # Write the c:minorTickMark element.
2405 self._write_minor_tick_mark(x_axis.get("minor_tick_mark"))
2407 # Write the c:tickLblPos element.
2408 self._write_tick_label_pos(x_axis.get("label_position"))
2410 # Write the c:spPr element for the axis line.
2411 self._write_sp_pr(x_axis)
2413 # Write the axis font elements.
2414 self._write_axis_font(x_axis.get("num_font"))
2416 # Write the c:crossAx element.
2417 self._write_cross_axis(axis_ids[1])
2419 if self.show_crosses or x_axis.get("visible"):
2420 # Note, the category crossing comes from the value axis.
2421 if (
2422 y_axis.get("crossing") is None
2423 or y_axis.get("crossing") == "max"
2424 or y_axis["crossing"] == "min"
2425 ):
2426 # Write the c:crosses element.
2427 self._write_crosses(y_axis.get("crossing"))
2428 else:
2429 # Write the c:crossesAt element.
2430 self._write_c_crosses_at(y_axis.get("crossing"))
2432 # Write the c:auto element.
2433 self._write_auto(1)
2435 # Write the c:labelOffset element.
2436 self._write_label_offset(100)
2438 # Write the c:tickLblSkip element.
2439 self._write_c_tick_lbl_skip(x_axis.get("interval_unit"))
2441 # Write the c:tickMarkSkip element.
2442 self._write_c_tick_mark_skip(x_axis.get("interval_tick"))
2444 # Write the c:majorUnit element.
2445 self._write_c_major_unit(x_axis.get("major_unit"))
2447 # Write the c:majorTimeUnit element.
2448 if x_axis.get("major_unit"):
2449 self._write_c_major_time_unit(x_axis["major_unit_type"])
2451 # Write the c:minorUnit element.
2452 self._write_c_minor_unit(x_axis.get("minor_unit"))
2454 # Write the c:minorTimeUnit element.
2455 if x_axis.get("minor_unit"):
2456 self._write_c_minor_time_unit(x_axis["minor_unit_type"])
2458 self._xml_end_tag("c:dateAx")
2460 def _write_scaling(self, reverse, min_val, max_val, log_base) -> None:
2461 # Write the <c:scaling> element.
2463 self._xml_start_tag("c:scaling")
2465 # Write the c:logBase element.
2466 self._write_c_log_base(log_base)
2468 # Write the c:orientation element.
2469 self._write_orientation(reverse)
2471 # Write the c:max element.
2472 self._write_c_max(max_val)
2474 # Write the c:min element.
2475 self._write_c_min(min_val)
2477 self._xml_end_tag("c:scaling")
2479 def _write_c_log_base(self, val) -> None:
2480 # Write the <c:logBase> element.
2482 if not val:
2483 return
2485 attributes = [("val", val)]
2487 self._xml_empty_tag("c:logBase", attributes)
2489 def _write_orientation(self, reverse) -> None:
2490 # Write the <c:orientation> element.
2491 val = "minMax"
2493 if reverse:
2494 val = "maxMin"
2496 attributes = [("val", val)]
2498 self._xml_empty_tag("c:orientation", attributes)
2500 def _write_c_max(self, max_val) -> None:
2501 # Write the <c:max> element.
2503 if max_val is None:
2504 return
2506 attributes = [("val", max_val)]
2508 self._xml_empty_tag("c:max", attributes)
2510 def _write_c_min(self, min_val) -> None:
2511 # Write the <c:min> element.
2513 if min_val is None:
2514 return
2516 attributes = [("val", min_val)]
2518 self._xml_empty_tag("c:min", attributes)
2520 def _write_axis_pos(self, val, reverse) -> None:
2521 # Write the <c:axPos> element.
2523 if reverse:
2524 if val == "l":
2525 val = "r"
2526 if val == "b":
2527 val = "t"
2529 attributes = [("val", val)]
2531 self._xml_empty_tag("c:axPos", attributes)
2533 def _write_number_format(self, axis) -> None:
2534 # Write the <c:numberFormat> element. Note: It is assumed that if
2535 # a user defined number format is supplied (i.e., non-default) then
2536 # the sourceLinked attribute is 0.
2537 # The user can override this if required.
2538 format_code = axis.get("num_format")
2539 source_linked = 1
2541 # Check if a user defined number format has been set.
2542 if format_code is not None and format_code != axis["defaults"]["num_format"]:
2543 source_linked = 0
2545 # User override of sourceLinked.
2546 if axis.get("num_format_linked"):
2547 source_linked = 1
2549 attributes = [
2550 ("formatCode", format_code),
2551 ("sourceLinked", source_linked),
2552 ]
2554 self._xml_empty_tag("c:numFmt", attributes)
2556 def _write_cat_number_format(self, axis) -> None:
2557 # Write the <c:numFmt> element. Special case handler for category
2558 # axes which don't always have a number format.
2559 format_code = axis.get("num_format")
2560 source_linked = 1
2561 default_format = 1
2563 # Check if a user defined number format has been set.
2564 if format_code is not None and format_code != axis["defaults"]["num_format"]:
2565 source_linked = 0
2566 default_format = 0
2568 # User override of sourceLinked.
2569 if axis.get("num_format_linked"):
2570 source_linked = 1
2572 # Skip if cat doesn't have a num format (unless it is non-default).
2573 if not self.cat_has_num_fmt and default_format:
2574 return
2576 attributes = [
2577 ("formatCode", format_code),
2578 ("sourceLinked", source_linked),
2579 ]
2581 self._xml_empty_tag("c:numFmt", attributes)
2583 def _write_data_label_number_format(self, format_code) -> None:
2584 # Write the <c:numberFormat> element for data labels.
2585 source_linked = 0
2587 attributes = [
2588 ("formatCode", format_code),
2589 ("sourceLinked", source_linked),
2590 ]
2592 self._xml_empty_tag("c:numFmt", attributes)
2594 def _write_major_tick_mark(self, val) -> None:
2595 # Write the <c:majorTickMark> element.
2597 if not val:
2598 return
2600 attributes = [("val", val)]
2602 self._xml_empty_tag("c:majorTickMark", attributes)
2604 def _write_minor_tick_mark(self, val) -> None:
2605 # Write the <c:minorTickMark> element.
2607 if not val:
2608 return
2610 attributes = [("val", val)]
2612 self._xml_empty_tag("c:minorTickMark", attributes)
2614 def _write_tick_label_pos(self, val=None) -> None:
2615 # Write the <c:tickLblPos> element.
2616 if val is None or val == "next_to":
2617 val = "nextTo"
2619 attributes = [("val", val)]
2621 self._xml_empty_tag("c:tickLblPos", attributes)
2623 def _write_cross_axis(self, val) -> None:
2624 # Write the <c:crossAx> element.
2626 attributes = [("val", val)]
2628 self._xml_empty_tag("c:crossAx", attributes)
2630 def _write_crosses(self, val=None) -> None:
2631 # Write the <c:crosses> element.
2632 if val is None:
2633 val = "autoZero"
2635 attributes = [("val", val)]
2637 self._xml_empty_tag("c:crosses", attributes)
2639 def _write_c_crosses_at(self, val) -> None:
2640 # Write the <c:crossesAt> element.
2642 attributes = [("val", val)]
2644 self._xml_empty_tag("c:crossesAt", attributes)
2646 def _write_auto(self, val) -> None:
2647 # Write the <c:auto> element.
2649 attributes = [("val", val)]
2651 self._xml_empty_tag("c:auto", attributes)
2653 def _write_label_align(self, val=None) -> None:
2654 # Write the <c:labelAlign> element.
2656 if val is None:
2657 val = "ctr"
2659 if val == "right":
2660 val = "r"
2662 if val == "left":
2663 val = "l"
2665 attributes = [("val", val)]
2667 self._xml_empty_tag("c:lblAlgn", attributes)
2669 def _write_label_offset(self, val) -> None:
2670 # Write the <c:labelOffset> element.
2672 attributes = [("val", val)]
2674 self._xml_empty_tag("c:lblOffset", attributes)
2676 def _write_c_tick_lbl_skip(self, val) -> None:
2677 # Write the <c:tickLblSkip> element.
2678 if val is None:
2679 return
2681 attributes = [("val", val)]
2683 self._xml_empty_tag("c:tickLblSkip", attributes)
2685 def _write_c_tick_mark_skip(self, val) -> None:
2686 # Write the <c:tickMarkSkip> element.
2687 if val is None:
2688 return
2690 attributes = [("val", val)]
2692 self._xml_empty_tag("c:tickMarkSkip", attributes)
2694 def _write_major_gridlines(self, gridlines) -> None:
2695 # Write the <c:majorGridlines> element.
2697 if not gridlines:
2698 return
2700 if not gridlines["visible"]:
2701 return
2703 if gridlines["line"]["defined"]:
2704 self._xml_start_tag("c:majorGridlines")
2706 # Write the c:spPr element.
2707 self._write_sp_pr(gridlines)
2709 self._xml_end_tag("c:majorGridlines")
2710 else:
2711 self._xml_empty_tag("c:majorGridlines")
2713 def _write_minor_gridlines(self, gridlines) -> None:
2714 # Write the <c:minorGridlines> element.
2716 if not gridlines:
2717 return
2719 if not gridlines["visible"]:
2720 return
2722 if gridlines["line"]["defined"]:
2723 self._xml_start_tag("c:minorGridlines")
2725 # Write the c:spPr element.
2726 self._write_sp_pr(gridlines)
2728 self._xml_end_tag("c:minorGridlines")
2729 else:
2730 self._xml_empty_tag("c:minorGridlines")
2732 def _write_cross_between(self, val) -> None:
2733 # Write the <c:crossBetween> element.
2734 if val is None:
2735 val = self.cross_between
2737 attributes = [("val", val)]
2739 self._xml_empty_tag("c:crossBetween", attributes)
2741 def _write_c_major_unit(self, val) -> None:
2742 # Write the <c:majorUnit> element.
2744 if not val:
2745 return
2747 attributes = [("val", val)]
2749 self._xml_empty_tag("c:majorUnit", attributes)
2751 def _write_c_minor_unit(self, val) -> None:
2752 # Write the <c:minorUnit> element.
2754 if not val:
2755 return
2757 attributes = [("val", val)]
2759 self._xml_empty_tag("c:minorUnit", attributes)
2761 def _write_c_major_time_unit(self, val=None) -> None:
2762 # Write the <c:majorTimeUnit> element.
2763 if val is None:
2764 val = "days"
2766 attributes = [("val", val)]
2768 self._xml_empty_tag("c:majorTimeUnit", attributes)
2770 def _write_c_minor_time_unit(self, val=None) -> None:
2771 # Write the <c:minorTimeUnit> element.
2772 if val is None:
2773 val = "days"
2775 attributes = [("val", val)]
2777 self._xml_empty_tag("c:minorTimeUnit", attributes)
2779 def _write_legend(self) -> None:
2780 # Write the <c:legend> element.
2781 legend = self.legend
2782 position = legend.get("position", "right")
2783 font = legend.get("font")
2784 delete_series = []
2785 overlay = 0
2787 if legend.get("delete_series") and isinstance(legend["delete_series"], list):
2788 delete_series = legend["delete_series"]
2790 if position.startswith("overlay_"):
2791 position = position.replace("overlay_", "")
2792 overlay = 1
2794 allowed = {
2795 "right": "r",
2796 "left": "l",
2797 "top": "t",
2798 "bottom": "b",
2799 "top_right": "tr",
2800 }
2802 if position == "none":
2803 return
2805 if position not in allowed:
2806 return
2808 position = allowed[position]
2810 self._xml_start_tag("c:legend")
2812 # Write the c:legendPos element.
2813 self._write_legend_pos(position)
2815 # Remove series labels from the legend.
2816 for index in delete_series:
2817 # Write the c:legendEntry element.
2818 self._write_legend_entry(index)
2820 # Write the c:layout element.
2821 self._write_layout(legend.get("layout"), "legend")
2823 # Write the c:overlay element.
2824 if overlay:
2825 self._write_overlay()
2827 if font:
2828 self._write_tx_pr(font)
2830 # Write the c:spPr element.
2831 self._write_sp_pr(legend)
2833 self._xml_end_tag("c:legend")
2835 def _write_legend_pos(self, val) -> None:
2836 # Write the <c:legendPos> element.
2838 attributes = [("val", val)]
2840 self._xml_empty_tag("c:legendPos", attributes)
2842 def _write_legend_entry(self, index) -> None:
2843 # Write the <c:legendEntry> element.
2845 self._xml_start_tag("c:legendEntry")
2847 # Write the c:idx element.
2848 self._write_idx(index)
2850 # Write the c:delete element.
2851 self._write_delete(1)
2853 self._xml_end_tag("c:legendEntry")
2855 def _write_overlay(self) -> None:
2856 # Write the <c:overlay> element.
2857 val = 1
2859 attributes = [("val", val)]
2861 self._xml_empty_tag("c:overlay", attributes)
2863 def _write_plot_vis_only(self) -> None:
2864 # Write the <c:plotVisOnly> element.
2865 val = 1
2867 # Ignore this element if we are plotting hidden data.
2868 if self.show_hidden:
2869 return
2871 attributes = [("val", val)]
2873 self._xml_empty_tag("c:plotVisOnly", attributes)
2875 def _write_print_settings(self) -> None:
2876 # Write the <c:printSettings> element.
2877 self._xml_start_tag("c:printSettings")
2879 # Write the c:headerFooter element.
2880 self._write_header_footer()
2882 # Write the c:pageMargins element.
2883 self._write_page_margins()
2885 # Write the c:pageSetup element.
2886 self._write_page_setup()
2888 self._xml_end_tag("c:printSettings")
2890 def _write_header_footer(self) -> None:
2891 # Write the <c:headerFooter> element.
2892 self._xml_empty_tag("c:headerFooter")
2894 def _write_page_margins(self) -> None:
2895 # Write the <c:pageMargins> element.
2896 bottom = 0.75
2897 left = 0.7
2898 right = 0.7
2899 top = 0.75
2900 header = 0.3
2901 footer = 0.3
2903 attributes = [
2904 ("b", bottom),
2905 ("l", left),
2906 ("r", right),
2907 ("t", top),
2908 ("header", header),
2909 ("footer", footer),
2910 ]
2912 self._xml_empty_tag("c:pageMargins", attributes)
2914 def _write_page_setup(self) -> None:
2915 # Write the <c:pageSetup> element.
2916 self._xml_empty_tag("c:pageSetup")
2918 def _write_c_auto_title_deleted(self) -> None:
2919 # Write the <c:autoTitleDeleted> element.
2920 self._xml_empty_tag("c:autoTitleDeleted", [("val", 1)])
2922 def _write_title(self, title: ChartTitle, is_horizontal: bool = False) -> None:
2923 # Write the <c:title> element for different title types.
2924 if title.has_name():
2925 self._write_title_rich(title, is_horizontal)
2926 elif title.has_formula():
2927 self._write_title_formula(title, is_horizontal)
2928 elif title.has_formatting():
2929 self._write_title_format_only(title)
2931 def _write_title_rich(self, title: ChartTitle, is_horizontal: bool = False) -> None:
2932 # Write the <c:title> element for a rich string.
2933 self._xml_start_tag("c:title")
2935 # Write the c:tx element.
2936 self._write_tx_rich(title.name, is_horizontal, title.font)
2938 # Write the c:layout element.
2939 self._write_layout(title.layout, "text")
2941 # Write the c:overlay element.
2942 if title.overlay:
2943 self._write_overlay()
2945 # Write the c:spPr element.
2946 self._write_sp_pr(title.get_formatting())
2948 self._xml_end_tag("c:title")
2950 def _write_title_formula(
2951 self, title: ChartTitle, is_horizontal: bool = False
2952 ) -> None:
2953 # Write the <c:title> element for a rich string.
2954 self._xml_start_tag("c:title")
2956 # Write the c:tx element.
2957 self._write_tx_formula(title.formula, title.data_id)
2959 # Write the c:layout element.
2960 self._write_layout(title.layout, "text")
2962 # Write the c:overlay element.
2963 if title.overlay:
2964 self._write_overlay()
2966 # Write the c:spPr element.
2967 self._write_sp_pr(title.get_formatting())
2969 # Write the c:txPr element.
2970 self._write_tx_pr(title.font, is_horizontal)
2972 self._xml_end_tag("c:title")
2974 def _write_title_format_only(self, title: ChartTitle) -> None:
2975 # Write the <c:title> element title with formatting and default name.
2976 self._xml_start_tag("c:title")
2978 # Write the c:layout element.
2979 self._write_layout(title.layout, "text")
2981 # Write the c:overlay element.
2982 if title.overlay:
2983 self._write_overlay()
2985 # Write the c:spPr element.
2986 self._write_sp_pr(title.get_formatting())
2988 self._xml_end_tag("c:title")
2990 def _write_tx_rich(self, title, is_horizontal, font) -> None:
2991 # Write the <c:tx> element.
2993 self._xml_start_tag("c:tx")
2995 # Write the c:rich element.
2996 self._write_rich(title, font, is_horizontal, ignore_rich_pr=False)
2998 self._xml_end_tag("c:tx")
3000 def _write_tx_value(self, title) -> None:
3001 # Write the <c:tx> element with a value such as for series names.
3003 self._xml_start_tag("c:tx")
3005 # Write the c:v element.
3006 self._write_v(title)
3008 self._xml_end_tag("c:tx")
3010 def _write_tx_formula(self, title, data_id) -> None:
3011 # Write the <c:tx> element.
3012 data = None
3014 if data_id is not None:
3015 data = self.formula_data[data_id]
3017 self._xml_start_tag("c:tx")
3019 # Write the c:strRef element.
3020 self._write_str_ref(title, data, "str")
3022 self._xml_end_tag("c:tx")
3024 def _write_rich(self, title, font, is_horizontal, ignore_rich_pr) -> None:
3025 # Write the <c:rich> element.
3027 if font and font.get("rotation") is not None:
3028 rotation = font["rotation"]
3029 else:
3030 rotation = None
3032 self._xml_start_tag("c:rich")
3034 # Write the a:bodyPr element.
3035 self._write_a_body_pr(rotation, is_horizontal)
3037 # Write the a:lstStyle element.
3038 self._write_a_lst_style()
3040 # Write the a:p element.
3041 self._write_a_p_rich(title, font, ignore_rich_pr)
3043 self._xml_end_tag("c:rich")
3045 def _write_a_body_pr(self, rotation, is_horizontal) -> None:
3046 # Write the <a:bodyPr> element.
3047 attributes = []
3049 if rotation is None and is_horizontal:
3050 rotation = -5400000
3052 if rotation is not None:
3053 if rotation == 16200000:
3054 # 270 deg/stacked angle.
3055 attributes.append(("rot", 0))
3056 attributes.append(("vert", "wordArtVert"))
3057 elif rotation == 16260000:
3058 # 271 deg/East Asian vertical.
3059 attributes.append(("rot", 0))
3060 attributes.append(("vert", "eaVert"))
3061 else:
3062 attributes.append(("rot", rotation))
3063 attributes.append(("vert", "horz"))
3065 self._xml_empty_tag("a:bodyPr", attributes)
3067 def _write_a_lst_style(self) -> None:
3068 # Write the <a:lstStyle> element.
3069 self._xml_empty_tag("a:lstStyle")
3071 def _write_a_p_rich(self, title, font, ignore_rich_pr) -> None:
3072 # Write the <a:p> element for rich string titles.
3074 self._xml_start_tag("a:p")
3076 # Write the a:pPr element.
3077 if not ignore_rich_pr:
3078 self._write_a_p_pr_rich(font)
3080 # Write the a:r element.
3081 self._write_a_r(title, font)
3083 self._xml_end_tag("a:p")
3085 def _write_a_p_formula(self, font) -> None:
3086 # Write the <a:p> element for formula titles.
3088 self._xml_start_tag("a:p")
3090 # Write the a:pPr element.
3091 self._write_a_p_pr_rich(font)
3093 # Write the a:endParaRPr element.
3094 self._write_a_end_para_rpr()
3096 self._xml_end_tag("a:p")
3098 def _write_a_p_pr_rich(self, font) -> None:
3099 # Write the <a:pPr> element for rich string titles.
3101 self._xml_start_tag("a:pPr")
3103 # Write the a:defRPr element.
3104 self._write_a_def_rpr(font)
3106 self._xml_end_tag("a:pPr")
3108 def _write_a_def_rpr(self, font) -> None:
3109 # Write the <a:defRPr> element.
3110 has_color = False
3112 style_attributes = Shape._get_font_style_attributes(font)
3113 latin_attributes = Shape._get_font_latin_attributes(font)
3115 if font and font.get("color"):
3116 has_color = True
3118 if latin_attributes or has_color:
3119 self._xml_start_tag("a:defRPr", style_attributes)
3121 if has_color:
3122 self._write_a_solid_fill({"color": font["color"]})
3124 if latin_attributes:
3125 self._write_a_latin(latin_attributes)
3127 self._xml_end_tag("a:defRPr")
3128 else:
3129 self._xml_empty_tag("a:defRPr", style_attributes)
3131 def _write_a_end_para_rpr(self) -> None:
3132 # Write the <a:endParaRPr> element.
3133 lang = "en-US"
3135 attributes = [("lang", lang)]
3137 self._xml_empty_tag("a:endParaRPr", attributes)
3139 def _write_a_r(self, title, font) -> None:
3140 # Write the <a:r> element.
3142 self._xml_start_tag("a:r")
3144 # Write the a:rPr element.
3145 self._write_a_r_pr(font)
3147 # Write the a:t element.
3148 self._write_a_t(title)
3150 self._xml_end_tag("a:r")
3152 def _write_a_r_pr(self, font) -> None:
3153 # Write the <a:rPr> element.
3154 has_color = False
3155 lang = "en-US"
3157 style_attributes = Shape._get_font_style_attributes(font)
3158 latin_attributes = Shape._get_font_latin_attributes(font)
3160 if font and font["color"]:
3161 has_color = True
3163 # Add the lang type to the attributes.
3164 style_attributes.insert(0, ("lang", lang))
3166 if latin_attributes or has_color:
3167 self._xml_start_tag("a:rPr", style_attributes)
3169 if has_color:
3170 self._write_a_solid_fill({"color": font["color"]})
3172 if latin_attributes:
3173 self._write_a_latin(latin_attributes)
3175 self._xml_end_tag("a:rPr")
3176 else:
3177 self._xml_empty_tag("a:rPr", style_attributes)
3179 def _write_a_t(self, title) -> None:
3180 # Write the <a:t> element.
3182 self._xml_data_element("a:t", title)
3184 def _write_tx_pr(self, font, is_horizontal=False) -> None:
3185 # Write the <c:txPr> element.
3187 if font and font.get("rotation") is not None:
3188 rotation = font["rotation"]
3189 else:
3190 rotation = None
3192 self._xml_start_tag("c:txPr")
3194 # Write the a:bodyPr element.
3195 self._write_a_body_pr(rotation, is_horizontal)
3197 # Write the a:lstStyle element.
3198 self._write_a_lst_style()
3200 # Write the a:p element.
3201 self._write_a_p_formula(font)
3203 self._xml_end_tag("c:txPr")
3205 def _write_marker(self, marker) -> None:
3206 # Write the <c:marker> element.
3207 if marker is None:
3208 marker = self.default_marker
3210 if not marker:
3211 return
3213 if marker["type"] == "automatic":
3214 return
3216 self._xml_start_tag("c:marker")
3218 # Write the c:symbol element.
3219 self._write_symbol(marker["type"])
3221 # Write the c:size element.
3222 if marker.get("size"):
3223 self._write_marker_size(marker["size"])
3225 # Write the c:spPr element.
3226 self._write_sp_pr(marker)
3228 self._xml_end_tag("c:marker")
3230 def _write_marker_size(self, val) -> None:
3231 # Write the <c:size> element.
3233 attributes = [("val", val)]
3235 self._xml_empty_tag("c:size", attributes)
3237 def _write_symbol(self, val) -> None:
3238 # Write the <c:symbol> element.
3240 attributes = [("val", val)]
3242 self._xml_empty_tag("c:symbol", attributes)
3244 def _write_sp_pr(self, chart_format: dict) -> None:
3245 # Write the <c:spPr> element.
3246 if not self._has_formatting(chart_format):
3247 return
3249 self._xml_start_tag("c:spPr")
3251 # Write the fill elements for solid charts such as pie and bar.
3252 if chart_format.get("fill") and chart_format["fill"]["defined"]:
3253 if "none" in chart_format["fill"]:
3254 # Write the a:noFill element.
3255 self._write_a_no_fill()
3256 else:
3257 # Write the a:solidFill element.
3258 self._write_a_solid_fill(chart_format["fill"])
3260 if chart_format.get("pattern"):
3261 # Write the a:gradFill element.
3262 self._write_a_patt_fill(chart_format["pattern"])
3264 if chart_format.get("gradient"):
3265 # Write the a:gradFill element.
3266 self._write_a_grad_fill(chart_format["gradient"])
3268 # Write the a:ln element.
3269 if chart_format.get("line") and chart_format["line"]["defined"]:
3270 self._write_a_ln(chart_format["line"])
3272 self._xml_end_tag("c:spPr")
3274 def _write_a_ln(self, line) -> None:
3275 # Write the <a:ln> element.
3276 attributes = []
3278 # Add the line width as an attribute.
3279 width = line.get("width")
3281 if width is not None:
3282 # Round width to nearest 0.25, like Excel.
3283 width = int((width + 0.125) * 4) / 4.0
3285 # Convert to internal units.
3286 width = int(0.5 + (12700 * width))
3288 attributes = [("w", width)]
3290 if line.get("none") or line.get("color") or line.get("dash_type"):
3291 self._xml_start_tag("a:ln", attributes)
3293 # Write the line fill.
3294 if "none" in line:
3295 # Write the a:noFill element.
3296 self._write_a_no_fill()
3297 elif "color" in line:
3298 # Write the a:solidFill element.
3299 self._write_a_solid_fill(line)
3301 # Write the line/dash type.
3302 line_type = line.get("dash_type")
3303 if line_type:
3304 # Write the a:prstDash element.
3305 self._write_a_prst_dash(line_type)
3307 self._xml_end_tag("a:ln")
3308 else:
3309 self._xml_empty_tag("a:ln", attributes)
3311 def _write_a_no_fill(self) -> None:
3312 # Write the <a:noFill> element.
3313 self._xml_empty_tag("a:noFill")
3315 def _write_a_solid_fill(self, fill) -> None:
3316 # Write the <a:solidFill> element.
3318 self._xml_start_tag("a:solidFill")
3320 if fill.get("color"):
3321 self._write_color(fill["color"], fill.get("transparency"))
3323 self._xml_end_tag("a:solidFill")
3325 def _write_color(self, color: Color, transparency=None) -> None:
3326 # Write the appropriate chart color element.
3328 if not color:
3329 return
3331 if color._is_automatic:
3332 # Write the a:sysClr element.
3333 self._write_a_sys_clr()
3334 elif color._type == ColorTypes.RGB:
3335 # Write the a:srgbClr element.
3336 self._write_a_srgb_clr(color, transparency)
3337 elif color._type == ColorTypes.THEME:
3338 self._write_a_scheme_clr(color, transparency)
3340 def _write_a_sys_clr(self) -> None:
3341 # Write the <a:sysClr> element.
3343 val = "window"
3344 last_clr = "FFFFFF"
3346 attributes = [
3347 ("val", val),
3348 ("lastClr", last_clr),
3349 ]
3351 self._xml_empty_tag("a:sysClr", attributes)
3353 def _write_a_srgb_clr(self, color: Color, transparency=None) -> None:
3354 # Write the <a:srgbClr> element.
3356 if not color:
3357 return
3359 attributes = [("val", color._rgb_hex_value())]
3361 if transparency:
3362 self._xml_start_tag("a:srgbClr", attributes)
3364 # Write the a:alpha element.
3365 self._write_a_alpha(transparency)
3367 self._xml_end_tag("a:srgbClr")
3368 else:
3369 self._xml_empty_tag("a:srgbClr", attributes)
3371 def _write_a_scheme_clr(self, color: Color, transparency=None) -> None:
3372 # Write the <a:schemeClr> element.
3373 scheme, lum_mod, lum_off = color._chart_scheme()
3374 attributes = [("val", scheme)]
3376 if lum_mod > 0 or lum_off > 0 or transparency:
3377 self._xml_start_tag("a:schemeClr", attributes)
3379 if lum_mod > 0:
3380 # Write the a:lumMod element.
3381 self._write_a_lum_mod(lum_mod)
3383 if lum_off > 0:
3384 # Write the a:lumOff element.
3385 self._write_a_lum_off(lum_off)
3387 if transparency:
3388 # Write the a:alpha element.
3389 self._write_a_alpha(transparency)
3391 self._xml_end_tag("a:schemeClr")
3392 else:
3393 self._xml_empty_tag("a:schemeClr", attributes)
3395 def _write_a_lum_mod(self, value: int) -> None:
3396 # Write the <a:lumMod> element.
3397 attributes = [("val", value)]
3399 self._xml_empty_tag("a:lumMod", attributes)
3401 def _write_a_lum_off(self, value: int) -> None:
3402 # Write the <a:lumOff> element.
3403 attributes = [("val", value)]
3405 self._xml_empty_tag("a:lumOff", attributes)
3407 def _write_a_alpha(self, val) -> None:
3408 # Write the <a:alpha> element.
3410 val = int((100 - int(val)) * 1000)
3412 attributes = [("val", val)]
3414 self._xml_empty_tag("a:alpha", attributes)
3416 def _write_a_prst_dash(self, val) -> None:
3417 # Write the <a:prstDash> element.
3419 attributes = [("val", val)]
3421 self._xml_empty_tag("a:prstDash", attributes)
3423 def _write_trendline(self, trendline) -> None:
3424 # Write the <c:trendline> element.
3426 if not trendline:
3427 return
3429 self._xml_start_tag("c:trendline")
3431 # Write the c:name element.
3432 self._write_name(trendline.get("name"))
3434 # Write the c:spPr element.
3435 self._write_sp_pr(trendline)
3437 # Write the c:trendlineType element.
3438 self._write_trendline_type(trendline["type"])
3440 # Write the c:order element for polynomial trendlines.
3441 if trendline["type"] == "poly":
3442 self._write_trendline_order(trendline.get("order"))
3444 # Write the c:period element for moving average trendlines.
3445 if trendline["type"] == "movingAvg":
3446 self._write_period(trendline.get("period"))
3448 # Write the c:forward element.
3449 self._write_forward(trendline.get("forward"))
3451 # Write the c:backward element.
3452 self._write_backward(trendline.get("backward"))
3454 if "intercept" in trendline:
3455 # Write the c:intercept element.
3456 self._write_c_intercept(trendline["intercept"])
3458 if trendline.get("display_r_squared"):
3459 # Write the c:dispRSqr element.
3460 self._write_c_disp_rsqr()
3462 if trendline.get("display_equation"):
3463 # Write the c:dispEq element.
3464 self._write_c_disp_eq()
3466 # Write the c:trendlineLbl element.
3467 self._write_c_trendline_lbl(trendline)
3469 self._xml_end_tag("c:trendline")
3471 def _write_trendline_type(self, val) -> None:
3472 # Write the <c:trendlineType> element.
3474 attributes = [("val", val)]
3476 self._xml_empty_tag("c:trendlineType", attributes)
3478 def _write_name(self, data) -> None:
3479 # Write the <c:name> element.
3481 if data is None:
3482 return
3484 self._xml_data_element("c:name", data)
3486 def _write_trendline_order(self, val) -> None:
3487 # Write the <c:order> element.
3488 val = max(val, 2)
3490 attributes = [("val", val)]
3492 self._xml_empty_tag("c:order", attributes)
3494 def _write_period(self, val) -> None:
3495 # Write the <c:period> element.
3496 val = max(val, 2)
3498 attributes = [("val", val)]
3500 self._xml_empty_tag("c:period", attributes)
3502 def _write_forward(self, val) -> None:
3503 # Write the <c:forward> element.
3505 if not val:
3506 return
3508 attributes = [("val", val)]
3510 self._xml_empty_tag("c:forward", attributes)
3512 def _write_backward(self, val) -> None:
3513 # Write the <c:backward> element.
3515 if not val:
3516 return
3518 attributes = [("val", val)]
3520 self._xml_empty_tag("c:backward", attributes)
3522 def _write_c_intercept(self, val) -> None:
3523 # Write the <c:intercept> element.
3524 attributes = [("val", val)]
3526 self._xml_empty_tag("c:intercept", attributes)
3528 def _write_c_disp_eq(self) -> None:
3529 # Write the <c:dispEq> element.
3530 attributes = [("val", 1)]
3532 self._xml_empty_tag("c:dispEq", attributes)
3534 def _write_c_disp_rsqr(self) -> None:
3535 # Write the <c:dispRSqr> element.
3536 attributes = [("val", 1)]
3538 self._xml_empty_tag("c:dispRSqr", attributes)
3540 def _write_c_trendline_lbl(self, trendline) -> None:
3541 # Write the <c:trendlineLbl> element.
3542 self._xml_start_tag("c:trendlineLbl")
3544 # Write the c:layout element.
3545 self._write_layout(None, None)
3547 # Write the c:numFmt element.
3548 self._write_trendline_num_fmt()
3550 # Write the c:spPr element.
3551 self._write_sp_pr(trendline["label"])
3553 # Write the data label font elements.
3554 if trendline["label"]:
3555 font = trendline["label"].get("font")
3556 if font:
3557 self._write_axis_font(font)
3559 self._xml_end_tag("c:trendlineLbl")
3561 def _write_trendline_num_fmt(self) -> None:
3562 # Write the <c:numFmt> element.
3563 attributes = [
3564 ("formatCode", "General"),
3565 ("sourceLinked", 0),
3566 ]
3568 self._xml_empty_tag("c:numFmt", attributes)
3570 def _write_hi_low_lines(self) -> None:
3571 # Write the <c:hiLowLines> element.
3572 hi_low_lines = self.hi_low_lines
3574 if hi_low_lines is None:
3575 return
3577 if "line" in hi_low_lines and hi_low_lines["line"]["defined"]:
3578 self._xml_start_tag("c:hiLowLines")
3580 # Write the c:spPr element.
3581 self._write_sp_pr(hi_low_lines)
3583 self._xml_end_tag("c:hiLowLines")
3584 else:
3585 self._xml_empty_tag("c:hiLowLines")
3587 def _write_drop_lines(self) -> None:
3588 # Write the <c:dropLines> element.
3589 drop_lines = self.drop_lines
3591 if drop_lines is None:
3592 return
3594 if drop_lines["line"]["defined"]:
3595 self._xml_start_tag("c:dropLines")
3597 # Write the c:spPr element.
3598 self._write_sp_pr(drop_lines)
3600 self._xml_end_tag("c:dropLines")
3601 else:
3602 self._xml_empty_tag("c:dropLines")
3604 def _write_overlap(self, val) -> None:
3605 # Write the <c:overlap> element.
3607 if val is None:
3608 return
3610 attributes = [("val", val)]
3612 self._xml_empty_tag("c:overlap", attributes)
3614 def _write_num_cache(self, data) -> None:
3615 # Write the <c:numCache> element.
3616 if data:
3617 count = len(data)
3618 else:
3619 count = 0
3621 self._xml_start_tag("c:numCache")
3623 # Write the c:formatCode element.
3624 self._write_format_code("General")
3626 # Write the c:ptCount element.
3627 self._write_pt_count(count)
3629 for i in range(count):
3630 token = data[i]
3632 if token is None:
3633 continue
3635 try:
3636 float(token)
3637 except ValueError:
3638 # Write non-numeric data as 0.
3639 token = 0
3641 # Write the c:pt element.
3642 self._write_pt(i, token)
3644 self._xml_end_tag("c:numCache")
3646 def _write_str_cache(self, data) -> None:
3647 # Write the <c:strCache> element.
3648 count = len(data)
3650 self._xml_start_tag("c:strCache")
3652 # Write the c:ptCount element.
3653 self._write_pt_count(count)
3655 for i in range(count):
3656 # Write the c:pt element.
3657 self._write_pt(i, data[i])
3659 self._xml_end_tag("c:strCache")
3661 def _write_format_code(self, data) -> None:
3662 # Write the <c:formatCode> element.
3664 self._xml_data_element("c:formatCode", data)
3666 def _write_pt_count(self, val) -> None:
3667 # Write the <c:ptCount> element.
3669 attributes = [("val", val)]
3671 self._xml_empty_tag("c:ptCount", attributes)
3673 def _write_pt(self, idx, value) -> None:
3674 # Write the <c:pt> element.
3676 if value is None:
3677 return
3679 attributes = [("idx", idx)]
3681 self._xml_start_tag("c:pt", attributes)
3683 # Write the c:v element.
3684 self._write_v(value)
3686 self._xml_end_tag("c:pt")
3688 def _write_v(self, data) -> None:
3689 # Write the <c:v> element.
3691 self._xml_data_element("c:v", data)
3693 def _write_protection(self) -> None:
3694 # Write the <c:protection> element.
3695 if not self.protection:
3696 return
3698 self._xml_empty_tag("c:protection")
3700 def _write_d_pt(self, points) -> None:
3701 # Write the <c:dPt> elements.
3702 index = -1
3704 if not points:
3705 return
3707 for point in points:
3708 index += 1
3709 if not point:
3710 continue
3712 self._write_d_pt_point(index, point)
3714 def _write_d_pt_point(self, index, point) -> None:
3715 # Write an individual <c:dPt> element.
3717 self._xml_start_tag("c:dPt")
3719 # Write the c:idx element.
3720 self._write_idx(index)
3722 # Write the c:spPr element.
3723 self._write_sp_pr(point)
3725 self._xml_end_tag("c:dPt")
3727 def _write_d_lbls(self, labels) -> None:
3728 # Write the <c:dLbls> element.
3730 if not labels:
3731 return
3733 self._xml_start_tag("c:dLbls")
3735 # Write the custom c:dLbl elements.
3736 if labels.get("custom"):
3737 self._write_custom_labels(labels, labels["custom"])
3739 # Write the c:numFmt element.
3740 if labels.get("num_format"):
3741 self._write_data_label_number_format(labels["num_format"])
3743 # Write the c:spPr element for the plotarea formatting.
3744 self._write_sp_pr(labels)
3746 # Write the data label font elements.
3747 if labels.get("font"):
3748 self._write_axis_font(labels["font"])
3750 # Write the c:dLblPos element.
3751 if labels.get("position"):
3752 self._write_d_lbl_pos(labels["position"])
3754 # Write the c:showLegendKey element.
3755 if labels.get("legend_key"):
3756 self._write_show_legend_key()
3758 # Write the c:showVal element.
3759 if labels.get("value"):
3760 self._write_show_val()
3762 # Write the c:showCatName element.
3763 if labels.get("category"):
3764 self._write_show_cat_name()
3766 # Write the c:showSerName element.
3767 if labels.get("series_name"):
3768 self._write_show_ser_name()
3770 # Write the c:showPercent element.
3771 if labels.get("percentage"):
3772 self._write_show_percent()
3774 # Write the c:separator element.
3775 if labels.get("separator"):
3776 self._write_separator(labels["separator"])
3778 # Write the c:showLeaderLines element.
3779 if labels.get("leader_lines"):
3780 self._write_show_leader_lines()
3782 self._xml_end_tag("c:dLbls")
3784 def _write_custom_labels(self, parent, labels) -> None:
3785 # Write the <c:showLegendKey> element.
3786 index = 0
3788 for label in labels:
3789 index += 1
3791 if label is None:
3792 continue
3794 use_custom_formatting = True
3796 self._xml_start_tag("c:dLbl")
3798 # Write the c:idx element.
3799 self._write_idx(index - 1)
3801 delete_label = label.get("delete")
3803 if delete_label:
3804 self._write_delete(1)
3806 elif label.get("formula") or label.get("value") or label.get("position"):
3808 # Write the c:layout element.
3809 self._write_layout(None, None)
3811 if label.get("formula"):
3812 self._write_custom_label_formula(label)
3813 elif label.get("value"):
3814 self._write_custom_label_str(label)
3815 # String values use spPr formatting.
3816 use_custom_formatting = False
3818 if use_custom_formatting:
3819 self._write_custom_label_format(label)
3821 if label.get("position"):
3822 self._write_d_lbl_pos(label["position"])
3823 elif parent.get("position"):
3824 self._write_d_lbl_pos(parent["position"])
3826 if parent.get("value"):
3827 self._write_show_val()
3829 if parent.get("category"):
3830 self._write_show_cat_name()
3832 if parent.get("series_name"):
3833 self._write_show_ser_name()
3835 else:
3836 self._write_custom_label_format(label)
3838 self._xml_end_tag("c:dLbl")
3840 def _write_custom_label_str(self, label) -> None:
3841 # Write parts of the <c:dLbl> element for strings.
3842 title = label.get("value")
3843 font = label.get("font")
3844 has_formatting = self._has_formatting(label)
3846 self._xml_start_tag("c:tx")
3848 # Write the c:rich element.
3849 self._write_rich(title, font, False, not has_formatting)
3851 self._xml_end_tag("c:tx")
3853 # Write the c:spPr element.
3854 self._write_sp_pr(label)
3856 def _write_custom_label_formula(self, label) -> None:
3857 # Write parts of the <c:dLbl> element for formulas.
3858 formula = label.get("formula")
3859 data_id = label.get("data_id")
3860 data = None
3862 if data_id is not None:
3863 data = self.formula_data[data_id]
3865 self._xml_start_tag("c:tx")
3867 # Write the c:strRef element.
3868 self._write_str_ref(formula, data, "str")
3870 self._xml_end_tag("c:tx")
3872 def _write_custom_label_format(self, label) -> None:
3873 # Write the formatting and font elements for the custom labels.
3874 font = label.get("font")
3875 has_formatting = self._has_formatting(label)
3877 if has_formatting:
3878 self._write_sp_pr(label)
3879 self._write_tx_pr(font)
3880 elif font:
3881 self._xml_empty_tag("c:spPr")
3882 self._write_tx_pr(font)
3884 def _write_show_legend_key(self) -> None:
3885 # Write the <c:showLegendKey> element.
3886 val = "1"
3888 attributes = [("val", val)]
3890 self._xml_empty_tag("c:showLegendKey", attributes)
3892 def _write_show_val(self) -> None:
3893 # Write the <c:showVal> element.
3894 val = 1
3896 attributes = [("val", val)]
3898 self._xml_empty_tag("c:showVal", attributes)
3900 def _write_show_cat_name(self) -> None:
3901 # Write the <c:showCatName> element.
3902 val = 1
3904 attributes = [("val", val)]
3906 self._xml_empty_tag("c:showCatName", attributes)
3908 def _write_show_ser_name(self) -> None:
3909 # Write the <c:showSerName> element.
3910 val = 1
3912 attributes = [("val", val)]
3914 self._xml_empty_tag("c:showSerName", attributes)
3916 def _write_show_percent(self) -> None:
3917 # Write the <c:showPercent> element.
3918 val = 1
3920 attributes = [("val", val)]
3922 self._xml_empty_tag("c:showPercent", attributes)
3924 def _write_separator(self, data) -> None:
3925 # Write the <c:separator> element.
3926 self._xml_data_element("c:separator", data)
3928 def _write_show_leader_lines(self) -> None:
3929 # Write the <c:showLeaderLines> element.
3930 #
3931 # This is different for Pie/Doughnut charts. Other chart types only
3932 # supported leader lines after Excel 2015 via an extension element.
3933 #
3934 uri = "{CE6537A1-D6FC-4f65-9D91-7224C49458BB}"
3935 xmlns_c_15 = "http://schemas.microsoft.com/office/drawing/2012/chart"
3937 attributes = [
3938 ("uri", uri),
3939 ("xmlns:c15", xmlns_c_15),
3940 ]
3942 self._xml_start_tag("c:extLst")
3943 self._xml_start_tag("c:ext", attributes)
3944 self._xml_empty_tag("c15:showLeaderLines", [("val", 1)])
3945 self._xml_end_tag("c:ext")
3946 self._xml_end_tag("c:extLst")
3948 def _write_d_lbl_pos(self, val) -> None:
3949 # Write the <c:dLblPos> element.
3951 attributes = [("val", val)]
3953 self._xml_empty_tag("c:dLblPos", attributes)
3955 def _write_delete(self, val) -> None:
3956 # Write the <c:delete> element.
3958 attributes = [("val", val)]
3960 self._xml_empty_tag("c:delete", attributes)
3962 def _write_c_invert_if_negative(self, invert) -> None:
3963 # Write the <c:invertIfNegative> element.
3964 val = 1
3966 if not invert:
3967 return
3969 attributes = [("val", val)]
3971 self._xml_empty_tag("c:invertIfNegative", attributes)
3973 def _write_axis_font(self, font) -> None:
3974 # Write the axis font elements.
3976 if not font:
3977 return
3979 self._xml_start_tag("c:txPr")
3980 self._write_a_body_pr(font.get("rotation"), None)
3981 self._write_a_lst_style()
3982 self._xml_start_tag("a:p")
3984 self._write_a_p_pr_rich(font)
3986 self._write_a_end_para_rpr()
3987 self._xml_end_tag("a:p")
3988 self._xml_end_tag("c:txPr")
3990 def _write_a_latin(self, attributes) -> None:
3991 # Write the <a:latin> element.
3992 self._xml_empty_tag("a:latin", attributes)
3994 def _write_d_table(self) -> None:
3995 # Write the <c:dTable> element.
3996 table = self.table
3998 if not table:
3999 return
4001 self._xml_start_tag("c:dTable")
4003 if table["horizontal"]:
4004 # Write the c:showHorzBorder element.
4005 self._write_show_horz_border()
4007 if table["vertical"]:
4008 # Write the c:showVertBorder element.
4009 self._write_show_vert_border()
4011 if table["outline"]:
4012 # Write the c:showOutline element.
4013 self._write_show_outline()
4015 if table["show_keys"]:
4016 # Write the c:showKeys element.
4017 self._write_show_keys()
4019 if table["font"]:
4020 # Write the table font.
4021 self._write_tx_pr(table["font"])
4023 self._xml_end_tag("c:dTable")
4025 def _write_show_horz_border(self) -> None:
4026 # Write the <c:showHorzBorder> element.
4027 attributes = [("val", 1)]
4029 self._xml_empty_tag("c:showHorzBorder", attributes)
4031 def _write_show_vert_border(self) -> None:
4032 # Write the <c:showVertBorder> element.
4033 attributes = [("val", 1)]
4035 self._xml_empty_tag("c:showVertBorder", attributes)
4037 def _write_show_outline(self) -> None:
4038 # Write the <c:showOutline> element.
4039 attributes = [("val", 1)]
4041 self._xml_empty_tag("c:showOutline", attributes)
4043 def _write_show_keys(self) -> None:
4044 # Write the <c:showKeys> element.
4045 attributes = [("val", 1)]
4047 self._xml_empty_tag("c:showKeys", attributes)
4049 def _write_error_bars(self, error_bars) -> None:
4050 # Write the X and Y error bars.
4052 if not error_bars:
4053 return
4055 if error_bars["x_error_bars"]:
4056 self._write_err_bars("x", error_bars["x_error_bars"])
4058 if error_bars["y_error_bars"]:
4059 self._write_err_bars("y", error_bars["y_error_bars"])
4061 def _write_err_bars(self, direction, error_bars) -> None:
4062 # Write the <c:errBars> element.
4064 if not error_bars:
4065 return
4067 self._xml_start_tag("c:errBars")
4069 # Write the c:errDir element.
4070 self._write_err_dir(direction)
4072 # Write the c:errBarType element.
4073 self._write_err_bar_type(error_bars["direction"])
4075 # Write the c:errValType element.
4076 self._write_err_val_type(error_bars["type"])
4078 if not error_bars["endcap"]:
4079 # Write the c:noEndCap element.
4080 self._write_no_end_cap()
4082 if error_bars["type"] == "stdErr":
4083 # Don't need to write a c:errValType tag.
4084 pass
4085 elif error_bars["type"] == "cust":
4086 # Write the custom error tags.
4087 self._write_custom_error(error_bars)
4088 else:
4089 # Write the c:val element.
4090 self._write_error_val(error_bars["value"])
4092 # Write the c:spPr element.
4093 self._write_sp_pr(error_bars)
4095 self._xml_end_tag("c:errBars")
4097 def _write_err_dir(self, val) -> None:
4098 # Write the <c:errDir> element.
4100 attributes = [("val", val)]
4102 self._xml_empty_tag("c:errDir", attributes)
4104 def _write_err_bar_type(self, val) -> None:
4105 # Write the <c:errBarType> element.
4107 attributes = [("val", val)]
4109 self._xml_empty_tag("c:errBarType", attributes)
4111 def _write_err_val_type(self, val) -> None:
4112 # Write the <c:errValType> element.
4114 attributes = [("val", val)]
4116 self._xml_empty_tag("c:errValType", attributes)
4118 def _write_no_end_cap(self) -> None:
4119 # Write the <c:noEndCap> element.
4120 attributes = [("val", 1)]
4122 self._xml_empty_tag("c:noEndCap", attributes)
4124 def _write_error_val(self, val) -> None:
4125 # Write the <c:val> element for error bars.
4127 attributes = [("val", val)]
4129 self._xml_empty_tag("c:val", attributes)
4131 def _write_custom_error(self, error_bars) -> None:
4132 # Write the custom error bars tags.
4134 if error_bars["plus_values"]:
4135 # Write the c:plus element.
4136 self._xml_start_tag("c:plus")
4138 if isinstance(error_bars["plus_values"], list):
4139 self._write_num_lit(error_bars["plus_values"])
4140 else:
4141 self._write_num_ref(
4142 error_bars["plus_values"], error_bars["plus_data"], "num"
4143 )
4144 self._xml_end_tag("c:plus")
4146 if error_bars["minus_values"]:
4147 # Write the c:minus element.
4148 self._xml_start_tag("c:minus")
4150 if isinstance(error_bars["minus_values"], list):
4151 self._write_num_lit(error_bars["minus_values"])
4152 else:
4153 self._write_num_ref(
4154 error_bars["minus_values"], error_bars["minus_data"], "num"
4155 )
4156 self._xml_end_tag("c:minus")
4158 def _write_num_lit(self, data) -> None:
4159 # Write the <c:numLit> element for literal number list elements.
4160 count = len(data)
4162 # Write the c:numLit element.
4163 self._xml_start_tag("c:numLit")
4165 # Write the c:formatCode element.
4166 self._write_format_code("General")
4168 # Write the c:ptCount element.
4169 self._write_pt_count(count)
4171 for i in range(count):
4172 token = data[i]
4174 if token is None:
4175 continue
4177 try:
4178 float(token)
4179 except ValueError:
4180 # Write non-numeric data as 0.
4181 token = 0
4183 # Write the c:pt element.
4184 self._write_pt(i, token)
4186 self._xml_end_tag("c:numLit")
4188 def _write_up_down_bars(self) -> None:
4189 # Write the <c:upDownBars> element.
4190 up_down_bars = self.up_down_bars
4192 if up_down_bars is None:
4193 return
4195 self._xml_start_tag("c:upDownBars")
4197 # Write the c:gapWidth element.
4198 self._write_gap_width(150)
4200 # Write the c:upBars element.
4201 self._write_up_bars(up_down_bars.get("up"))
4203 # Write the c:downBars element.
4204 self._write_down_bars(up_down_bars.get("down"))
4206 self._xml_end_tag("c:upDownBars")
4208 def _write_gap_width(self, val) -> None:
4209 # Write the <c:gapWidth> element.
4211 if val is None:
4212 return
4214 attributes = [("val", val)]
4216 self._xml_empty_tag("c:gapWidth", attributes)
4218 def _write_up_bars(self, bar_format) -> None:
4219 # Write the <c:upBars> element.
4221 if bar_format["line"] and bar_format["line"]["defined"]:
4222 self._xml_start_tag("c:upBars")
4224 # Write the c:spPr element.
4225 self._write_sp_pr(bar_format)
4227 self._xml_end_tag("c:upBars")
4228 else:
4229 self._xml_empty_tag("c:upBars")
4231 def _write_down_bars(self, bar_format) -> None:
4232 # Write the <c:downBars> element.
4234 if bar_format["line"] and bar_format["line"]["defined"]:
4235 self._xml_start_tag("c:downBars")
4237 # Write the c:spPr element.
4238 self._write_sp_pr(bar_format)
4240 self._xml_end_tag("c:downBars")
4241 else:
4242 self._xml_empty_tag("c:downBars")
4244 def _write_disp_units(self, units, display) -> None:
4245 # Write the <c:dispUnits> element.
4247 if not units:
4248 return
4250 attributes = [("val", units)]
4252 self._xml_start_tag("c:dispUnits")
4253 self._xml_empty_tag("c:builtInUnit", attributes)
4255 if display:
4256 self._xml_start_tag("c:dispUnitsLbl")
4257 self._xml_empty_tag("c:layout")
4258 self._xml_end_tag("c:dispUnitsLbl")
4260 self._xml_end_tag("c:dispUnits")
4262 def _write_a_grad_fill(self, gradient) -> None:
4263 # Write the <a:gradFill> element.
4265 attributes = [("flip", "none"), ("rotWithShape", "1")]
4267 if gradient["type"] == "linear":
4268 attributes = []
4270 self._xml_start_tag("a:gradFill", attributes)
4272 # Write the a:gsLst element.
4273 self._write_a_gs_lst(gradient)
4275 if gradient["type"] == "linear":
4276 # Write the a:lin element.
4277 self._write_a_lin(gradient["angle"])
4278 else:
4279 # Write the a:path element.
4280 self._write_a_path(gradient["type"])
4282 # Write the a:tileRect element.
4283 self._write_a_tile_rect(gradient["type"])
4285 self._xml_end_tag("a:gradFill")
4287 def _write_a_gs_lst(self, gradient) -> None:
4288 # Write the <a:gsLst> element.
4289 positions = gradient["positions"]
4290 colors = gradient["colors"]
4292 self._xml_start_tag("a:gsLst")
4294 for i, color in enumerate(colors):
4295 pos = int(positions[i] * 1000)
4296 attributes = [("pos", pos)]
4297 self._xml_start_tag("a:gs", attributes)
4299 self._write_color(color)
4301 self._xml_end_tag("a:gs")
4303 self._xml_end_tag("a:gsLst")
4305 def _write_a_lin(self, angle) -> None:
4306 # Write the <a:lin> element.
4308 angle = int(60000 * angle)
4310 attributes = [
4311 ("ang", angle),
4312 ("scaled", "0"),
4313 ]
4315 self._xml_empty_tag("a:lin", attributes)
4317 def _write_a_path(self, gradient_type) -> None:
4318 # Write the <a:path> element.
4320 attributes = [("path", gradient_type)]
4322 self._xml_start_tag("a:path", attributes)
4324 # Write the a:fillToRect element.
4325 self._write_a_fill_to_rect(gradient_type)
4327 self._xml_end_tag("a:path")
4329 def _write_a_fill_to_rect(self, gradient_type) -> None:
4330 # Write the <a:fillToRect> element.
4332 if gradient_type == "shape":
4333 attributes = [
4334 ("l", "50000"),
4335 ("t", "50000"),
4336 ("r", "50000"),
4337 ("b", "50000"),
4338 ]
4339 else:
4340 attributes = [
4341 ("l", "100000"),
4342 ("t", "100000"),
4343 ]
4345 self._xml_empty_tag("a:fillToRect", attributes)
4347 def _write_a_tile_rect(self, gradient_type) -> None:
4348 # Write the <a:tileRect> element.
4350 if gradient_type == "shape":
4351 attributes = []
4352 else:
4353 attributes = [
4354 ("r", "-100000"),
4355 ("b", "-100000"),
4356 ]
4358 self._xml_empty_tag("a:tileRect", attributes)
4360 def _write_a_patt_fill(self, pattern) -> None:
4361 # Write the <a:pattFill> element.
4363 attributes = [("prst", pattern["pattern"])]
4365 self._xml_start_tag("a:pattFill", attributes)
4367 # Write the a:fgClr element.
4368 self._write_a_fg_clr(pattern["fg_color"])
4370 # Write the a:bgClr element.
4371 self._write_a_bg_clr(pattern["bg_color"])
4373 self._xml_end_tag("a:pattFill")
4375 def _write_a_fg_clr(self, color: Color) -> None:
4376 # Write the <a:fgClr> element.
4377 self._xml_start_tag("a:fgClr")
4378 self._write_color(color)
4379 self._xml_end_tag("a:fgClr")
4381 def _write_a_bg_clr(self, color: Color) -> None:
4382 # Write the <a:bgClr> element.
4383 self._xml_start_tag("a:bgClr")
4384 self._write_color(color)
4385 self._xml_end_tag("a:bgClr")