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