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