Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/xlsxwriter/chart.py: 11%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1###############################################################################
2#
3# Chart - A class for writing the Excel XLSX Worksheet file.
4#
5# SPDX-License-Identifier: BSD-2-Clause
6# Copyright 2013-2024, John McNamara, jmcnamara@cpan.org
7#
8import re
9import copy
10from warnings import warn
12from .shape import Shape
13from . import xmlwriter
14from .utility import get_rgb_color
15from .utility import xl_rowcol_to_cell
16from .utility import xl_range_formula
17from .utility import supported_datetime
18from .utility import datetime_to_excel_datetime
19from .utility import quote_sheetname
22class Chart(xmlwriter.XMLwriter):
23 """
24 A class for writing the Excel XLSX Chart file.
27 """
29 ###########################################################################
30 #
31 # Public API.
32 #
33 ###########################################################################
35 def __init__(self, options=None):
36 """
37 Constructor.
39 """
41 super(Chart, self).__init__()
43 self.subtype = None
44 self.sheet_type = 0x0200
45 self.orientation = 0x0
46 self.series = []
47 self.embedded = 0
48 self.id = -1
49 self.series_index = 0
50 self.style_id = 2
51 self.axis_ids = []
52 self.axis2_ids = []
53 self.cat_has_num_fmt = 0
54 self.requires_category = False
55 self.legend = {}
56 self.cat_axis_position = "b"
57 self.val_axis_position = "l"
58 self.formula_ids = {}
59 self.formula_data = []
60 self.horiz_cat_axis = 0
61 self.horiz_val_axis = 1
62 self.protection = 0
63 self.chartarea = {}
64 self.plotarea = {}
65 self.x_axis = {}
66 self.y_axis = {}
67 self.y2_axis = {}
68 self.x2_axis = {}
69 self.chart_name = ""
70 self.show_blanks = "gap"
71 self.show_na_as_empty = False
72 self.show_hidden = False
73 self.show_crosses = True
74 self.width = 480
75 self.height = 288
76 self.x_scale = 1
77 self.y_scale = 1
78 self.x_offset = 0
79 self.y_offset = 0
80 self.table = None
81 self.cross_between = "between"
82 self.default_marker = None
83 self.series_gap_1 = None
84 self.series_gap_2 = None
85 self.series_overlap_1 = None
86 self.series_overlap_2 = None
87 self.drop_lines = None
88 self.hi_low_lines = None
89 self.up_down_bars = None
90 self.smooth_allowed = False
91 self.title_font = None
92 self.title_name = None
93 self.title_formula = None
94 self.title_data_id = None
95 self.title_layout = None
96 self.title_overlay = None
97 self.title_none = False
98 self.date_category = False
99 self.date_1904 = False
100 self.remove_timezone = False
101 self.label_positions = {}
102 self.label_position_default = ""
103 self.already_inserted = False
104 self.combined = None
105 self.is_secondary = False
106 self.warn_sheetname = True
107 self._set_default_properties()
109 def add_series(self, options=None):
110 """
111 Add a data series to a chart.
113 Args:
114 options: A dictionary of chart series options.
116 Returns:
117 Nothing.
119 """
120 # Add a series and it's properties to a chart.
121 if options is None:
122 options = {}
124 # Check that the required input has been specified.
125 if "values" not in options:
126 warn("Must specify 'values' in add_series()")
127 return
129 if self.requires_category and "categories" not in options:
130 warn("Must specify 'categories' in add_series() for this chart type")
131 return
133 if len(self.series) == 255:
134 warn(
135 "The maximum number of series that can be added to an "
136 "Excel Chart is 255"
137 )
138 return
140 # Convert list into a formula string.
141 values = self._list_to_formula(options.get("values"))
142 categories = self._list_to_formula(options.get("categories"))
144 # Switch name and name_formula parameters if required.
145 name, name_formula = self._process_names(
146 options.get("name"), options.get("name_formula")
147 )
149 # Get an id for the data equivalent to the range formula.
150 cat_id = self._get_data_id(categories, options.get("categories_data"))
151 val_id = self._get_data_id(values, options.get("values_data"))
152 name_id = self._get_data_id(name_formula, options.get("name_data"))
154 # Set the line properties for the series.
155 line = Shape._get_line_properties(options.get("line"))
157 # Allow 'border' as a synonym for 'line' in bar/column style charts.
158 if options.get("border"):
159 line = Shape._get_line_properties(options["border"])
161 # Set the fill properties for the series.
162 fill = Shape._get_fill_properties(options.get("fill"))
164 # Set the pattern fill properties for the series.
165 pattern = Shape._get_pattern_properties(options.get("pattern"))
167 # Set the gradient fill properties for the series.
168 gradient = Shape._get_gradient_properties(options.get("gradient"))
170 # Pattern fill overrides solid fill.
171 if pattern:
172 self.fill = None
174 # Gradient fill overrides the solid and pattern fill.
175 if gradient:
176 pattern = None
177 fill = None
179 # Set the marker properties for the series.
180 marker = self._get_marker_properties(options.get("marker"))
182 # Set the trendline properties for the series.
183 trendline = self._get_trendline_properties(options.get("trendline"))
185 # Set the line smooth property for the series.
186 smooth = options.get("smooth")
188 # Set the error bars properties for the series.
189 y_error_bars = self._get_error_bars_props(options.get("y_error_bars"))
190 x_error_bars = self._get_error_bars_props(options.get("x_error_bars"))
192 error_bars = {"x_error_bars": x_error_bars, "y_error_bars": y_error_bars}
194 # Set the point properties for the series.
195 points = self._get_points_properties(options.get("points"))
197 # Set the labels properties for the series.
198 labels = self._get_labels_properties(options.get("data_labels"))
200 # Set the "invert if negative" fill property.
201 invert_if_neg = options.get("invert_if_negative", False)
202 inverted_color = options.get("invert_if_negative_color", False)
204 # Set the secondary axis properties.
205 x2_axis = options.get("x2_axis")
206 y2_axis = options.get("y2_axis")
208 # Store secondary status for combined charts.
209 if x2_axis or y2_axis:
210 self.is_secondary = True
212 # Set the gap for Bar/Column charts.
213 if options.get("gap") is not None:
214 if y2_axis:
215 self.series_gap_2 = options["gap"]
216 else:
217 self.series_gap_1 = options["gap"]
219 # Set the overlap for Bar/Column charts.
220 if options.get("overlap"):
221 if y2_axis:
222 self.series_overlap_2 = options["overlap"]
223 else:
224 self.series_overlap_1 = options["overlap"]
226 # Add the user supplied data to the internal structures.
227 series = {
228 "values": values,
229 "categories": categories,
230 "name": name,
231 "name_formula": name_formula,
232 "name_id": name_id,
233 "val_data_id": val_id,
234 "cat_data_id": cat_id,
235 "line": line,
236 "fill": fill,
237 "pattern": pattern,
238 "gradient": gradient,
239 "marker": marker,
240 "trendline": trendline,
241 "labels": labels,
242 "invert_if_neg": invert_if_neg,
243 "inverted_color": inverted_color,
244 "x2_axis": x2_axis,
245 "y2_axis": y2_axis,
246 "points": points,
247 "error_bars": error_bars,
248 "smooth": smooth,
249 }
251 self.series.append(series)
253 def set_x_axis(self, options):
254 """
255 Set the chart X axis options.
257 Args:
258 options: A dictionary of axis options.
260 Returns:
261 Nothing.
263 """
264 axis = self._convert_axis_args(self.x_axis, options)
266 self.x_axis = axis
268 def set_y_axis(self, options):
269 """
270 Set the chart Y axis options.
272 Args:
273 options: A dictionary of axis options.
275 Returns:
276 Nothing.
278 """
279 axis = self._convert_axis_args(self.y_axis, options)
281 self.y_axis = axis
283 def set_x2_axis(self, options):
284 """
285 Set the chart secondary X axis options.
287 Args:
288 options: A dictionary of axis options.
290 Returns:
291 Nothing.
293 """
294 axis = self._convert_axis_args(self.x2_axis, options)
296 self.x2_axis = axis
298 def set_y2_axis(self, options):
299 """
300 Set the chart secondary Y axis options.
302 Args:
303 options: A dictionary of axis options.
305 Returns:
306 Nothing.
308 """
309 axis = self._convert_axis_args(self.y2_axis, options)
311 self.y2_axis = axis
313 def set_title(self, options=None):
314 """
315 Set the chart title options.
317 Args:
318 options: A dictionary of chart title options.
320 Returns:
321 Nothing.
323 """
324 if options is None:
325 options = {}
327 name, name_formula = self._process_names(
328 options.get("name"), options.get("name_formula")
329 )
331 data_id = self._get_data_id(name_formula, options.get("data"))
333 self.title_name = name
334 self.title_formula = name_formula
335 self.title_data_id = data_id
337 # Set the font properties if present.
338 self.title_font = self._convert_font_args(options.get("name_font"))
340 # Set the axis name layout.
341 self.title_layout = self._get_layout_properties(options.get("layout"), True)
342 # Set the title overlay option.
343 self.title_overlay = options.get("overlay")
345 # Set the automatic title option.
346 self.title_none = options.get("none")
348 def set_legend(self, options):
349 """
350 Set the chart legend options.
352 Args:
353 options: A dictionary of chart legend options.
355 Returns:
356 Nothing.
357 """
358 # Convert the user defined properties to internal properties.
359 self.legend = self._get_legend_properties(options)
361 def set_plotarea(self, options):
362 """
363 Set the chart plot area options.
365 Args:
366 options: A dictionary of chart plot area options.
368 Returns:
369 Nothing.
370 """
371 # Convert the user defined properties to internal properties.
372 self.plotarea = self._get_area_properties(options)
374 def set_chartarea(self, options):
375 """
376 Set the chart area options.
378 Args:
379 options: A dictionary of chart area options.
381 Returns:
382 Nothing.
383 """
384 # Convert the user defined properties to internal properties.
385 self.chartarea = self._get_area_properties(options)
387 def set_style(self, style_id):
388 """
389 Set the chart style type.
391 Args:
392 style_id: An int representing the chart style.
394 Returns:
395 Nothing.
396 """
397 # Set one of the 48 built-in Excel chart styles. The default is 2.
398 if style_id is None:
399 style_id = 2
401 if style_id < 1 or style_id > 48:
402 style_id = 2
404 self.style_id = style_id
406 def show_blanks_as(self, option):
407 """
408 Set the option for displaying blank data in a chart.
410 Args:
411 option: A string representing the display option.
413 Returns:
414 Nothing.
415 """
416 if not option:
417 return
419 valid_options = {
420 "gap": 1,
421 "zero": 1,
422 "span": 1,
423 }
425 if option not in valid_options:
426 warn("Unknown show_blanks_as() option '%s'" % option)
427 return
429 self.show_blanks = option
431 def show_na_as_empty_cell(self):
432 """
433 Display ``#N/A`` on charts as blank/empty cells.
435 Args:
436 None.
438 Returns:
439 Nothing.
440 """
441 self.show_na_as_empty = True
443 def show_hidden_data(self):
444 """
445 Display data on charts from hidden rows or columns.
447 Args:
448 None.
450 Returns:
451 Nothing.
452 """
453 self.show_hidden = True
455 def set_size(self, options=None):
456 """
457 Set size or scale of the chart.
459 Args:
460 options: A dictionary of chart size options.
462 Returns:
463 Nothing.
464 """
465 if options is None:
466 options = {}
468 # Set dimensions or scale for the chart.
469 self.width = options.get("width", self.width)
470 self.height = options.get("height", self.height)
471 self.x_scale = options.get("x_scale", 1)
472 self.y_scale = options.get("y_scale", 1)
473 self.x_offset = options.get("x_offset", 0)
474 self.y_offset = options.get("y_offset", 0)
476 def set_table(self, options=None):
477 """
478 Set properties for an axis data table.
480 Args:
481 options: A dictionary of axis table options.
483 Returns:
484 Nothing.
486 """
487 if options is None:
488 options = {}
490 table = {}
492 table["horizontal"] = options.get("horizontal", 1)
493 table["vertical"] = options.get("vertical", 1)
494 table["outline"] = options.get("outline", 1)
495 table["show_keys"] = options.get("show_keys", 0)
496 table["font"] = self._convert_font_args(options.get("font"))
498 self.table = table
500 def set_up_down_bars(self, options=None):
501 """
502 Set properties for the chart up-down bars.
504 Args:
505 options: A dictionary of options.
507 Returns:
508 Nothing.
510 """
511 if options is None:
512 options = {}
514 # Defaults.
515 up_line = None
516 up_fill = None
517 down_line = None
518 down_fill = None
520 # Set properties for 'up' bar.
521 if options.get("up"):
522 if "border" in options["up"]:
523 # Map border to line.
524 up_line = Shape._get_line_properties(options["up"]["border"])
526 if "line" in options["up"]:
527 up_line = Shape._get_line_properties(options["up"]["line"])
529 if "fill" in options["up"]:
530 up_fill = Shape._get_fill_properties(options["up"]["fill"])
532 # Set properties for 'down' bar.
533 if options.get("down"):
534 if "border" in options["down"]:
535 # Map border to line.
536 down_line = Shape._get_line_properties(options["down"]["border"])
538 if "line" in options["down"]:
539 down_line = Shape._get_line_properties(options["down"]["line"])
541 if "fill" in options["down"]:
542 down_fill = Shape._get_fill_properties(options["down"]["fill"])
544 self.up_down_bars = {
545 "up": {
546 "line": up_line,
547 "fill": up_fill,
548 },
549 "down": {
550 "line": down_line,
551 "fill": down_fill,
552 },
553 }
555 def set_drop_lines(self, options=None):
556 """
557 Set properties for the chart drop lines.
559 Args:
560 options: A dictionary of options.
562 Returns:
563 Nothing.
565 """
566 if options is None:
567 options = {}
569 line = Shape._get_line_properties(options.get("line"))
570 fill = Shape._get_fill_properties(options.get("fill"))
572 # Set the pattern fill properties for the series.
573 pattern = Shape._get_pattern_properties(options.get("pattern"))
575 # Set the gradient fill properties for the series.
576 gradient = Shape._get_gradient_properties(options.get("gradient"))
578 # Pattern fill overrides solid fill.
579 if pattern:
580 self.fill = None
582 # Gradient fill overrides the solid and pattern fill.
583 if gradient:
584 pattern = None
585 fill = None
587 self.drop_lines = {
588 "line": line,
589 "fill": fill,
590 "pattern": pattern,
591 "gradient": gradient,
592 }
594 def set_high_low_lines(self, options=None):
595 """
596 Set properties for the chart high-low lines.
598 Args:
599 options: A dictionary of options.
601 Returns:
602 Nothing.
604 """
605 if options is None:
606 options = {}
608 line = Shape._get_line_properties(options.get("line"))
609 fill = Shape._get_fill_properties(options.get("fill"))
611 # Set the pattern fill properties for the series.
612 pattern = Shape._get_pattern_properties(options.get("pattern"))
614 # Set the gradient fill properties for the series.
615 gradient = Shape._get_gradient_properties(options.get("gradient"))
617 # Pattern fill overrides solid fill.
618 if pattern:
619 self.fill = None
621 # Gradient fill overrides the solid and pattern fill.
622 if gradient:
623 pattern = None
624 fill = None
626 self.hi_low_lines = {
627 "line": line,
628 "fill": fill,
629 "pattern": pattern,
630 "gradient": gradient,
631 }
633 def combine(self, chart=None):
634 """
635 Create a combination chart with a secondary chart.
637 Args:
638 chart: The secondary chart to combine with the primary chart.
640 Returns:
641 Nothing.
643 """
644 if chart is None:
645 return
647 self.combined = chart
649 ###########################################################################
650 #
651 # Private API.
652 #
653 ###########################################################################
655 def _assemble_xml_file(self):
656 # Assemble and write the XML file.
658 # Write the XML declaration.
659 self._xml_declaration()
661 # Write the c:chartSpace element.
662 self._write_chart_space()
664 # Write the c:lang element.
665 self._write_lang()
667 # Write the c:style element.
668 self._write_style()
670 # Write the c:protection element.
671 self._write_protection()
673 # Write the c:chart element.
674 self._write_chart()
676 # Write the c:spPr element for the chartarea formatting.
677 self._write_sp_pr(self.chartarea)
679 # Write the c:printSettings element.
680 if self.embedded:
681 self._write_print_settings()
683 # Close the worksheet tag.
684 self._xml_end_tag("c:chartSpace")
685 # Close the file.
686 self._xml_close()
688 def _convert_axis_args(self, axis, user_options):
689 # Convert user defined axis values into private hash values.
690 options = axis["defaults"].copy()
691 options.update(user_options)
693 name, name_formula = self._process_names(
694 options.get("name"), options.get("name_formula")
695 )
697 data_id = self._get_data_id(name_formula, options.get("data"))
699 axis = {
700 "defaults": axis["defaults"],
701 "name": name,
702 "formula": name_formula,
703 "data_id": data_id,
704 "reverse": options.get("reverse"),
705 "min": options.get("min"),
706 "max": options.get("max"),
707 "minor_unit": options.get("minor_unit"),
708 "major_unit": options.get("major_unit"),
709 "minor_unit_type": options.get("minor_unit_type"),
710 "major_unit_type": options.get("major_unit_type"),
711 "display_units": options.get("display_units"),
712 "log_base": options.get("log_base"),
713 "crossing": options.get("crossing"),
714 "position_axis": options.get("position_axis"),
715 "position": options.get("position"),
716 "label_position": options.get("label_position"),
717 "label_align": options.get("label_align"),
718 "num_format": options.get("num_format"),
719 "num_format_linked": options.get("num_format_linked"),
720 "interval_unit": options.get("interval_unit"),
721 "interval_tick": options.get("interval_tick"),
722 "text_axis": False,
723 }
725 axis["visible"] = options.get("visible", True)
727 # Convert the display units.
728 axis["display_units"] = self._get_display_units(axis["display_units"])
729 axis["display_units_visible"] = options.get("display_units_visible", True)
731 # Map major_gridlines properties.
732 if options.get("major_gridlines") and options["major_gridlines"]["visible"]:
733 axis["major_gridlines"] = self._get_gridline_properties(
734 options["major_gridlines"]
735 )
737 # Map minor_gridlines properties.
738 if options.get("minor_gridlines") and options["minor_gridlines"]["visible"]:
739 axis["minor_gridlines"] = self._get_gridline_properties(
740 options["minor_gridlines"]
741 )
743 # Only use the first letter of bottom, top, left or right.
744 if axis.get("position"):
745 axis["position"] = axis["position"].lower()[0]
747 # Set the position for a category axis on or between the tick marks.
748 if axis.get("position_axis"):
749 if axis["position_axis"] == "on_tick":
750 axis["position_axis"] = "midCat"
751 elif axis["position_axis"] == "between":
752 # Doesn't need to be modified.
753 pass
754 else:
755 # Otherwise use the default value.
756 axis["position_axis"] = None
758 # Set the category axis as a date axis.
759 if options.get("date_axis"):
760 self.date_category = True
762 # Set the category axis as a text axis.
763 if options.get("text_axis"):
764 self.date_category = False
765 axis["text_axis"] = True
767 # Convert datetime args if required.
768 if axis.get("min") and supported_datetime(axis["min"]):
769 axis["min"] = datetime_to_excel_datetime(
770 axis["min"], self.date_1904, self.remove_timezone
771 )
772 if axis.get("max") and supported_datetime(axis["max"]):
773 axis["max"] = datetime_to_excel_datetime(
774 axis["max"], self.date_1904, self.remove_timezone
775 )
776 if axis.get("crossing") and supported_datetime(axis["crossing"]):
777 axis["crossing"] = datetime_to_excel_datetime(
778 axis["crossing"], self.date_1904, self.remove_timezone
779 )
781 # Set the font properties if present.
782 axis["num_font"] = self._convert_font_args(options.get("num_font"))
783 axis["name_font"] = self._convert_font_args(options.get("name_font"))
785 # Set the axis name layout.
786 axis["name_layout"] = self._get_layout_properties(
787 options.get("name_layout"), True
788 )
790 # Set the line properties for the axis.
791 axis["line"] = Shape._get_line_properties(options.get("line"))
793 # Set the fill properties for the axis.
794 axis["fill"] = Shape._get_fill_properties(options.get("fill"))
796 # Set the pattern fill properties for the series.
797 axis["pattern"] = Shape._get_pattern_properties(options.get("pattern"))
799 # Set the gradient fill properties for the series.
800 axis["gradient"] = Shape._get_gradient_properties(options.get("gradient"))
802 # Pattern fill overrides solid fill.
803 if axis.get("pattern"):
804 axis["fill"] = None
806 # Gradient fill overrides the solid and pattern fill.
807 if axis.get("gradient"):
808 axis["pattern"] = None
809 axis["fill"] = None
811 # Set the tick marker types.
812 axis["minor_tick_mark"] = self._get_tick_type(options.get("minor_tick_mark"))
813 axis["major_tick_mark"] = self._get_tick_type(options.get("major_tick_mark"))
815 return axis
817 def _convert_font_args(self, options):
818 # Convert user defined font values into private dict values.
819 if not options:
820 return
822 font = {
823 "name": options.get("name"),
824 "color": options.get("color"),
825 "size": options.get("size"),
826 "bold": options.get("bold"),
827 "italic": options.get("italic"),
828 "underline": options.get("underline"),
829 "pitch_family": options.get("pitch_family"),
830 "charset": options.get("charset"),
831 "baseline": options.get("baseline", 0),
832 "rotation": options.get("rotation"),
833 }
835 # Convert font size units.
836 if font["size"]:
837 font["size"] = int(font["size"] * 100)
839 # Convert rotation into 60,000ths of a degree.
840 if font["rotation"]:
841 font["rotation"] = 60000 * int(font["rotation"])
843 return font
845 def _list_to_formula(self, data):
846 # Convert and list of row col values to a range formula.
848 # If it isn't an array ref it is probably a formula already.
849 if not isinstance(data, list):
850 # Check for unquoted sheetnames.
851 if data and " " in data and "'" not in data and self.warn_sheetname:
852 warn(
853 "Sheetname in '%s' contains spaces but isn't quoted. "
854 "This may cause errors in Excel." % data
855 )
856 return data
858 formula = xl_range_formula(*data)
860 return formula
862 def _process_names(self, name, name_formula):
863 # Switch name and name_formula parameters if required.
865 if name is not None:
866 if isinstance(name, list):
867 # Convert a list of values into a name formula.
868 cell = xl_rowcol_to_cell(name[1], name[2], True, True)
869 name_formula = quote_sheetname(name[0]) + "!" + cell
870 name = ""
871 elif re.match(r"^=?[^!]+!\$?[A-Z]+\$?\d+", name):
872 # Name looks like a formula, use it to set name_formula.
873 name_formula = name
874 name = ""
876 return name, name_formula
878 def _get_data_type(self, data):
879 # Find the overall type of the data associated with a series.
881 # Check for no data in the series.
882 if data is None or len(data) == 0:
883 return "none"
885 if isinstance(data[0], list):
886 return "multi_str"
888 # Determine if data is numeric or strings.
889 for token in data:
890 if token is None:
891 continue
893 # Check for strings that would evaluate to float like
894 # '1.1_1' of ' 1'.
895 if isinstance(token, str) and re.search("[_ ]", token):
896 # Assume entire data series is string data.
897 return "str"
899 try:
900 float(token)
901 except ValueError:
902 # Not a number. Assume entire data series is string data.
903 return "str"
905 # The series data was all numeric.
906 return "num"
908 def _get_data_id(self, formula, data):
909 # Assign an id to a each unique series formula or title/axis formula.
910 # Repeated formulas such as for categories get the same id. If the
911 # series or title has user specified data associated with it then
912 # that is also stored. This data is used to populate cached Excel
913 # data when creating a chart. If there is no user defined data then
914 # it will be populated by the parent Workbook._add_chart_data().
916 # Ignore series without a range formula.
917 if not formula:
918 return
920 # Strip the leading '=' from the formula.
921 if formula.startswith("="):
922 formula = formula.lstrip("=")
924 # Store the data id in a hash keyed by the formula and store the data
925 # in a separate array with the same id.
926 if formula not in self.formula_ids:
927 # Haven't seen this formula before.
928 formula_id = len(self.formula_data)
930 self.formula_data.append(data)
931 self.formula_ids[formula] = formula_id
932 else:
933 # Formula already seen. Return existing id.
934 formula_id = self.formula_ids[formula]
936 # Store user defined data if it isn't already there.
937 if self.formula_data[formula_id] is None:
938 self.formula_data[formula_id] = data
940 return formula_id
942 def _get_marker_properties(self, marker):
943 # Convert user marker properties to the structure required internally.
945 if not marker:
946 return
948 # Copy the user defined properties since they will be modified.
949 marker = copy.deepcopy(marker)
951 types = {
952 "automatic": "automatic",
953 "none": "none",
954 "square": "square",
955 "diamond": "diamond",
956 "triangle": "triangle",
957 "x": "x",
958 "star": "star",
959 "dot": "dot",
960 "short_dash": "dot",
961 "dash": "dash",
962 "long_dash": "dash",
963 "circle": "circle",
964 "plus": "plus",
965 "picture": "picture",
966 }
968 # Check for valid types.
969 marker_type = marker.get("type")
971 if marker_type is not None:
972 if marker_type in types:
973 marker["type"] = types[marker_type]
974 else:
975 warn("Unknown marker type '%s" % marker_type)
976 return
978 # Set the line properties for the marker.
979 line = Shape._get_line_properties(marker.get("line"))
981 # Allow 'border' as a synonym for 'line'.
982 if "border" in marker:
983 line = Shape._get_line_properties(marker["border"])
985 # Set the fill properties for the marker.
986 fill = Shape._get_fill_properties(marker.get("fill"))
988 # Set the pattern fill properties for the series.
989 pattern = Shape._get_pattern_properties(marker.get("pattern"))
991 # Set the gradient fill properties for the series.
992 gradient = Shape._get_gradient_properties(marker.get("gradient"))
994 # Pattern fill overrides solid fill.
995 if pattern:
996 self.fill = None
998 # Gradient fill overrides the solid and pattern fill.
999 if gradient:
1000 pattern = None
1001 fill = None
1003 marker["line"] = line
1004 marker["fill"] = fill
1005 marker["pattern"] = pattern
1006 marker["gradient"] = gradient
1008 return marker
1010 def _get_trendline_properties(self, trendline):
1011 # Convert user trendline properties to structure required internally.
1013 if not trendline:
1014 return
1016 # Copy the user defined properties since they will be modified.
1017 trendline = copy.deepcopy(trendline)
1019 types = {
1020 "exponential": "exp",
1021 "linear": "linear",
1022 "log": "log",
1023 "moving_average": "movingAvg",
1024 "polynomial": "poly",
1025 "power": "power",
1026 }
1028 # Check the trendline type.
1029 trend_type = trendline.get("type")
1031 if trend_type in types:
1032 trendline["type"] = types[trend_type]
1033 else:
1034 warn("Unknown trendline type '%s'" % trend_type)
1035 return
1037 # Set the line properties for the trendline.
1038 line = Shape._get_line_properties(trendline.get("line"))
1040 # Allow 'border' as a synonym for 'line'.
1041 if "border" in trendline:
1042 line = Shape._get_line_properties(trendline["border"])
1044 # Set the fill properties for the trendline.
1045 fill = Shape._get_fill_properties(trendline.get("fill"))
1047 # Set the pattern fill properties for the trendline.
1048 pattern = Shape._get_pattern_properties(trendline.get("pattern"))
1050 # Set the gradient fill properties for the trendline.
1051 gradient = Shape._get_gradient_properties(trendline.get("gradient"))
1053 # Set the format properties for the trendline label.
1054 label = self._get_trendline_label_properties(trendline.get("label"))
1056 # Pattern fill overrides solid fill.
1057 if pattern:
1058 self.fill = None
1060 # Gradient fill overrides the solid and pattern fill.
1061 if gradient:
1062 pattern = None
1063 fill = None
1065 trendline["line"] = line
1066 trendline["fill"] = fill
1067 trendline["pattern"] = pattern
1068 trendline["gradient"] = gradient
1069 trendline["label"] = label
1071 return trendline
1073 def _get_trendline_label_properties(self, label):
1074 # Convert user trendline properties to structure required internally.
1076 if not label:
1077 return {}
1079 # Copy the user defined properties since they will be modified.
1080 label = copy.deepcopy(label)
1082 # Set the font properties if present.
1083 font = self._convert_font_args(label.get("font"))
1085 # Set the line properties for the label.
1086 line = Shape._get_line_properties(label.get("line"))
1088 # Allow 'border' as a synonym for 'line'.
1089 if "border" in label:
1090 line = Shape._get_line_properties(label["border"])
1092 # Set the fill properties for the label.
1093 fill = Shape._get_fill_properties(label.get("fill"))
1095 # Set the pattern fill properties for the label.
1096 pattern = Shape._get_pattern_properties(label.get("pattern"))
1098 # Set the gradient fill properties for the label.
1099 gradient = Shape._get_gradient_properties(label.get("gradient"))
1101 # Pattern fill overrides solid fill.
1102 if pattern:
1103 self.fill = None
1105 # Gradient fill overrides the solid and pattern fill.
1106 if gradient:
1107 pattern = None
1108 fill = None
1110 label["font"] = font
1111 label["line"] = line
1112 label["fill"] = fill
1113 label["pattern"] = pattern
1114 label["gradient"] = gradient
1116 return label
1118 def _get_error_bars_props(self, options):
1119 # Convert user error bars properties to structure required internally.
1120 if not options:
1121 return
1123 # Default values.
1124 error_bars = {"type": "fixedVal", "value": 1, "endcap": 1, "direction": "both"}
1126 types = {
1127 "fixed": "fixedVal",
1128 "percentage": "percentage",
1129 "standard_deviation": "stdDev",
1130 "standard_error": "stdErr",
1131 "custom": "cust",
1132 }
1134 # Check the error bars type.
1135 error_type = options["type"]
1137 if error_type in types:
1138 error_bars["type"] = types[error_type]
1139 else:
1140 warn("Unknown error bars type '%s" % error_type)
1141 return
1143 # Set the value for error types that require it.
1144 if "value" in options:
1145 error_bars["value"] = options["value"]
1147 # Set the end-cap style.
1148 if "end_style" in options:
1149 error_bars["endcap"] = options["end_style"]
1151 # Set the error bar direction.
1152 if "direction" in options:
1153 if options["direction"] == "minus":
1154 error_bars["direction"] = "minus"
1155 elif options["direction"] == "plus":
1156 error_bars["direction"] = "plus"
1157 else:
1158 # Default to 'both'.
1159 pass
1161 # Set any custom values.
1162 error_bars["plus_values"] = options.get("plus_values")
1163 error_bars["minus_values"] = options.get("minus_values")
1164 error_bars["plus_data"] = options.get("plus_data")
1165 error_bars["minus_data"] = options.get("minus_data")
1167 # Set the line properties for the error bars.
1168 error_bars["line"] = Shape._get_line_properties(options.get("line"))
1170 return error_bars
1172 def _get_gridline_properties(self, options):
1173 # Convert user gridline properties to structure required internally.
1175 # Set the visible property for the gridline.
1176 gridline = {"visible": options.get("visible")}
1178 # Set the line properties for the gridline.
1179 gridline["line"] = Shape._get_line_properties(options.get("line"))
1181 return gridline
1183 def _get_labels_properties(self, labels):
1184 # Convert user labels properties to the structure required internally.
1186 if not labels:
1187 return None
1189 # Copy the user defined properties since they will be modified.
1190 labels = copy.deepcopy(labels)
1192 # Map user defined label positions to Excel positions.
1193 position = labels.get("position")
1195 if position:
1196 if position in self.label_positions:
1197 if position == self.label_position_default:
1198 labels["position"] = None
1199 else:
1200 labels["position"] = self.label_positions[position]
1201 else:
1202 warn("Unsupported label position '%s' for this chart type" % position)
1203 return
1205 # Map the user defined label separator to the Excel separator.
1206 separator = labels.get("separator")
1207 separators = {
1208 ",": ", ",
1209 ";": "; ",
1210 ".": ". ",
1211 "\n": "\n",
1212 " ": " ",
1213 }
1215 if separator:
1216 if separator in separators:
1217 labels["separator"] = separators[separator]
1218 else:
1219 warn("Unsupported label separator")
1220 return
1222 # Set the font properties if present.
1223 labels["font"] = self._convert_font_args(labels.get("font"))
1225 # Set the line properties for the labels.
1226 line = Shape._get_line_properties(labels.get("line"))
1228 # Allow 'border' as a synonym for 'line'.
1229 if "border" in labels:
1230 line = Shape._get_line_properties(labels["border"])
1232 # Set the fill properties for the labels.
1233 fill = Shape._get_fill_properties(labels.get("fill"))
1235 # Set the pattern fill properties for the labels.
1236 pattern = Shape._get_pattern_properties(labels.get("pattern"))
1238 # Set the gradient fill properties for the labels.
1239 gradient = Shape._get_gradient_properties(labels.get("gradient"))
1241 # Pattern fill overrides solid fill.
1242 if pattern:
1243 self.fill = None
1245 # Gradient fill overrides the solid and pattern fill.
1246 if gradient:
1247 pattern = None
1248 fill = None
1250 labels["line"] = line
1251 labels["fill"] = fill
1252 labels["pattern"] = pattern
1253 labels["gradient"] = gradient
1255 if labels.get("custom"):
1256 for label in labels["custom"]:
1257 if label is None:
1258 continue
1260 value = label.get("value")
1261 if value and re.match(r"^=?[^!]+!\$?[A-Z]+\$?\d+", str(value)):
1262 label["formula"] = value
1264 formula = label.get("formula")
1265 if formula and formula.startswith("="):
1266 label["formula"] = formula.lstrip("=")
1268 data_id = self._get_data_id(formula, label.get("data"))
1269 label["data_id"] = data_id
1271 label["font"] = self._convert_font_args(label.get("font"))
1273 # Set the line properties for the label.
1274 line = Shape._get_line_properties(label.get("line"))
1276 # Allow 'border' as a synonym for 'line'.
1277 if "border" in label:
1278 line = Shape._get_line_properties(label["border"])
1280 # Set the fill properties for the label.
1281 fill = Shape._get_fill_properties(label.get("fill"))
1283 # Set the pattern fill properties for the label.
1284 pattern = Shape._get_pattern_properties(label.get("pattern"))
1286 # Set the gradient fill properties for the label.
1287 gradient = Shape._get_gradient_properties(label.get("gradient"))
1289 # Pattern fill overrides solid fill.
1290 if pattern:
1291 self.fill = None
1293 # Gradient fill overrides the solid and pattern fill.
1294 if gradient:
1295 pattern = None
1296 fill = None
1298 label["line"] = line
1299 label["fill"] = fill
1300 label["pattern"] = pattern
1301 label["gradient"] = gradient
1303 return labels
1305 def _get_area_properties(self, options):
1306 # Convert user area properties to the structure required internally.
1307 area = {}
1309 # Set the line properties for the chartarea.
1310 line = Shape._get_line_properties(options.get("line"))
1312 # Allow 'border' as a synonym for 'line'.
1313 if options.get("border"):
1314 line = Shape._get_line_properties(options["border"])
1316 # Set the fill properties for the chartarea.
1317 fill = Shape._get_fill_properties(options.get("fill"))
1319 # Set the pattern fill properties for the series.
1320 pattern = Shape._get_pattern_properties(options.get("pattern"))
1322 # Set the gradient fill properties for the series.
1323 gradient = Shape._get_gradient_properties(options.get("gradient"))
1325 # Pattern fill overrides solid fill.
1326 if pattern:
1327 self.fill = None
1329 # Gradient fill overrides the solid and pattern fill.
1330 if gradient:
1331 pattern = None
1332 fill = None
1334 # Set the plotarea layout.
1335 layout = self._get_layout_properties(options.get("layout"), False)
1337 area["line"] = line
1338 area["fill"] = fill
1339 area["pattern"] = pattern
1340 area["layout"] = layout
1341 area["gradient"] = gradient
1343 return area
1345 def _get_legend_properties(self, options=None):
1346 # Convert user legend properties to the structure required internally.
1347 legend = {}
1349 if options is None:
1350 options = {}
1352 legend["position"] = options.get("position", "right")
1353 legend["delete_series"] = options.get("delete_series")
1354 legend["font"] = self._convert_font_args(options.get("font"))
1355 legend["layout"] = self._get_layout_properties(options.get("layout"), False)
1357 # Turn off the legend.
1358 if options.get("none"):
1359 legend["position"] = "none"
1361 # Set the line properties for the legend.
1362 line = Shape._get_line_properties(options.get("line"))
1364 # Allow 'border' as a synonym for 'line'.
1365 if options.get("border"):
1366 line = Shape._get_line_properties(options["border"])
1368 # Set the fill properties for the legend.
1369 fill = Shape._get_fill_properties(options.get("fill"))
1371 # Set the pattern fill properties for the series.
1372 pattern = Shape._get_pattern_properties(options.get("pattern"))
1374 # Set the gradient fill properties for the series.
1375 gradient = Shape._get_gradient_properties(options.get("gradient"))
1377 # Pattern fill overrides solid fill.
1378 if pattern:
1379 self.fill = None
1381 # Gradient fill overrides the solid and pattern fill.
1382 if gradient:
1383 pattern = None
1384 fill = None
1386 # Set the legend layout.
1387 layout = self._get_layout_properties(options.get("layout"), False)
1389 legend["line"] = line
1390 legend["fill"] = fill
1391 legend["pattern"] = pattern
1392 legend["layout"] = layout
1393 legend["gradient"] = gradient
1395 return legend
1397 def _get_layout_properties(self, args, is_text):
1398 # Convert user defined layout properties to format used internally.
1399 layout = {}
1401 if not args:
1402 return
1404 if is_text:
1405 properties = ("x", "y")
1406 else:
1407 properties = ("x", "y", "width", "height")
1409 # Check for valid properties.
1410 for key in args.keys():
1411 if key not in properties:
1412 warn("Property '%s' allowed not in layout options" % key)
1413 return
1415 # Set the layout properties.
1416 for prop in properties:
1417 if prop not in args.keys():
1418 warn("Property '%s' must be specified in layout options" % prop)
1419 return
1421 value = args[prop]
1423 try:
1424 float(value)
1425 except ValueError:
1426 warn(
1427 "Property '%s' value '%s' must be numeric in layout" % (prop, value)
1428 )
1429 return
1431 if value < 0 or value > 1:
1432 warn(
1433 "Property '%s' value '%s' must be in range "
1434 "0 < x <= 1 in layout options" % (prop, value)
1435 )
1436 return
1438 # Convert to the format used by Excel for easier testing
1439 layout[prop] = "%.17g" % value
1441 return layout
1443 def _get_points_properties(self, user_points):
1444 # Convert user points properties to structure required internally.
1445 points = []
1447 if not user_points:
1448 return
1450 for user_point in user_points:
1451 point = {}
1453 if user_point is not None:
1454 # Set the line properties for the point.
1455 line = Shape._get_line_properties(user_point.get("line"))
1457 # Allow 'border' as a synonym for 'line'.
1458 if "border" in user_point:
1459 line = Shape._get_line_properties(user_point["border"])
1461 # Set the fill properties for the chartarea.
1462 fill = Shape._get_fill_properties(user_point.get("fill"))
1464 # Set the pattern fill properties for the series.
1465 pattern = Shape._get_pattern_properties(user_point.get("pattern"))
1467 # Set the gradient fill properties for the series.
1468 gradient = Shape._get_gradient_properties(user_point.get("gradient"))
1470 # Pattern fill overrides solid fill.
1471 if pattern:
1472 self.fill = None
1474 # Gradient fill overrides the solid and pattern fill.
1475 if gradient:
1476 pattern = None
1477 fill = None
1479 point["line"] = line
1480 point["fill"] = fill
1481 point["pattern"] = pattern
1482 point["gradient"] = gradient
1484 points.append(point)
1486 return points
1488 def _has_fill_formatting(self, element):
1489 # Check if a chart element has line, fill or gradient formatting.
1490 has_fill = False
1491 has_line = False
1492 has_pattern = element.get("pattern")
1493 has_gradient = element.get("gradient")
1495 if element.get("fill") and element["fill"]["defined"]:
1496 has_fill = True
1498 if element.get("line") and element["line"]["defined"]:
1499 has_line = True
1501 if not has_fill and not has_line and not has_pattern and not has_gradient:
1502 return False
1503 else:
1504 return True
1506 def _get_display_units(self, display_units):
1507 # Convert user defined display units to internal units.
1508 if not display_units:
1509 return
1511 types = {
1512 "hundreds": "hundreds",
1513 "thousands": "thousands",
1514 "ten_thousands": "tenThousands",
1515 "hundred_thousands": "hundredThousands",
1516 "millions": "millions",
1517 "ten_millions": "tenMillions",
1518 "hundred_millions": "hundredMillions",
1519 "billions": "billions",
1520 "trillions": "trillions",
1521 }
1523 if display_units in types:
1524 display_units = types[display_units]
1525 else:
1526 warn("Unknown display_units type '%s'" % display_units)
1527 return
1529 return display_units
1531 def _get_tick_type(self, tick_type):
1532 # Convert user defined display units to internal units.
1533 if not tick_type:
1534 return
1536 types = {
1537 "outside": "out",
1538 "inside": "in",
1539 "none": "none",
1540 "cross": "cross",
1541 }
1543 if tick_type in types:
1544 tick_type = types[tick_type]
1545 else:
1546 warn("Unknown tick_type '%s'" % tick_type)
1547 return
1549 return tick_type
1551 def _get_primary_axes_series(self):
1552 # Returns series which use the primary axes.
1553 primary_axes_series = []
1555 for series in self.series:
1556 if not series["y2_axis"]:
1557 primary_axes_series.append(series)
1559 return primary_axes_series
1561 def _get_secondary_axes_series(self):
1562 # Returns series which use the secondary axes.
1563 secondary_axes_series = []
1565 for series in self.series:
1566 if series["y2_axis"]:
1567 secondary_axes_series.append(series)
1569 return secondary_axes_series
1571 def _add_axis_ids(self, args):
1572 # Add unique ids for primary or secondary axes
1573 chart_id = 5001 + int(self.id)
1574 axis_count = 1 + len(self.axis2_ids) + len(self.axis_ids)
1576 id1 = "%04d%04d" % (chart_id, axis_count)
1577 id2 = "%04d%04d" % (chart_id, axis_count + 1)
1579 if args["primary_axes"]:
1580 self.axis_ids.append(id1)
1581 self.axis_ids.append(id2)
1583 if not args["primary_axes"]:
1584 self.axis2_ids.append(id1)
1585 self.axis2_ids.append(id2)
1587 def _set_default_properties(self):
1588 # Setup the default properties for a chart.
1590 self.x_axis["defaults"] = {
1591 "num_format": "General",
1592 "major_gridlines": {"visible": 0},
1593 }
1595 self.y_axis["defaults"] = {
1596 "num_format": "General",
1597 "major_gridlines": {"visible": 1},
1598 }
1600 self.x2_axis["defaults"] = {
1601 "num_format": "General",
1602 "label_position": "none",
1603 "crossing": "max",
1604 "visible": 0,
1605 }
1607 self.y2_axis["defaults"] = {
1608 "num_format": "General",
1609 "major_gridlines": {"visible": 0},
1610 "position": "right",
1611 "visible": 1,
1612 }
1614 self.set_x_axis({})
1615 self.set_y_axis({})
1617 self.set_x2_axis({})
1618 self.set_y2_axis({})
1620 ###########################################################################
1621 #
1622 # XML methods.
1623 #
1624 ###########################################################################
1626 def _write_chart_space(self):
1627 # Write the <c:chartSpace> element.
1628 schema = "http://schemas.openxmlformats.org/"
1629 xmlns_c = schema + "drawingml/2006/chart"
1630 xmlns_a = schema + "drawingml/2006/main"
1631 xmlns_r = schema + "officeDocument/2006/relationships"
1633 attributes = [
1634 ("xmlns:c", xmlns_c),
1635 ("xmlns:a", xmlns_a),
1636 ("xmlns:r", xmlns_r),
1637 ]
1639 self._xml_start_tag("c:chartSpace", attributes)
1641 def _write_lang(self):
1642 # Write the <c:lang> element.
1643 val = "en-US"
1645 attributes = [("val", val)]
1647 self._xml_empty_tag("c:lang", attributes)
1649 def _write_style(self):
1650 # Write the <c:style> element.
1651 style_id = self.style_id
1653 # Don't write an element for the default style, 2.
1654 if style_id == 2:
1655 return
1657 attributes = [("val", style_id)]
1659 self._xml_empty_tag("c:style", attributes)
1661 def _write_chart(self):
1662 # Write the <c:chart> element.
1663 self._xml_start_tag("c:chart")
1665 if self.title_none:
1666 # Turn off the title.
1667 self._write_c_auto_title_deleted()
1668 else:
1669 # Write the chart title elements.
1670 if self.title_formula is not None:
1671 self._write_title_formula(
1672 self.title_formula,
1673 self.title_data_id,
1674 None,
1675 self.title_font,
1676 self.title_layout,
1677 self.title_overlay,
1678 )
1679 elif self.title_name is not None:
1680 self._write_title_rich(
1681 self.title_name,
1682 None,
1683 self.title_font,
1684 self.title_layout,
1685 self.title_overlay,
1686 )
1688 # Write the c:plotArea element.
1689 self._write_plot_area()
1691 # Write the c:legend element.
1692 self._write_legend()
1694 # Write the c:plotVisOnly element.
1695 self._write_plot_vis_only()
1697 # Write the c:dispBlanksAs element.
1698 self._write_disp_blanks_as()
1700 # Write the c:extLst element.
1701 if self.show_na_as_empty:
1702 self._write_c_ext_lst_display_na()
1704 self._xml_end_tag("c:chart")
1706 def _write_disp_blanks_as(self):
1707 # Write the <c:dispBlanksAs> element.
1708 val = self.show_blanks
1710 # Ignore the default value.
1711 if val == "gap":
1712 return
1714 attributes = [("val", val)]
1716 self._xml_empty_tag("c:dispBlanksAs", attributes)
1718 def _write_plot_area(self):
1719 # Write the <c:plotArea> element.
1720 self._xml_start_tag("c:plotArea")
1722 # Write the c:layout element.
1723 self._write_layout(self.plotarea.get("layout"), "plot")
1725 # Write subclass chart type elements for primary and secondary axes.
1726 self._write_chart_type({"primary_axes": True})
1727 self._write_chart_type({"primary_axes": False})
1729 # Configure a combined chart if present.
1730 second_chart = self.combined
1731 if second_chart:
1732 # Secondary axis has unique id otherwise use same as primary.
1733 if second_chart.is_secondary:
1734 second_chart.id = 1000 + self.id
1735 else:
1736 second_chart.id = self.id
1738 # Share the same filehandle for writing.
1739 second_chart.fh = self.fh
1741 # Share series index with primary chart.
1742 second_chart.series_index = self.series_index
1744 # Write the subclass chart type elements for combined chart.
1745 second_chart._write_chart_type({"primary_axes": True})
1746 second_chart._write_chart_type({"primary_axes": False})
1748 # Write the category and value elements for the primary axes.
1749 args = {"x_axis": self.x_axis, "y_axis": self.y_axis, "axis_ids": self.axis_ids}
1751 if self.date_category:
1752 self._write_date_axis(args)
1753 else:
1754 self._write_cat_axis(args)
1756 self._write_val_axis(args)
1758 # Write the category and value elements for the secondary axes.
1759 args = {
1760 "x_axis": self.x2_axis,
1761 "y_axis": self.y2_axis,
1762 "axis_ids": self.axis2_ids,
1763 }
1765 self._write_val_axis(args)
1767 # Write the secondary axis for the secondary chart.
1768 if second_chart and second_chart.is_secondary:
1769 args = {
1770 "x_axis": second_chart.x2_axis,
1771 "y_axis": second_chart.y2_axis,
1772 "axis_ids": second_chart.axis2_ids,
1773 }
1775 second_chart._write_val_axis(args)
1777 if self.date_category:
1778 self._write_date_axis(args)
1779 else:
1780 self._write_cat_axis(args)
1782 # Write the c:dTable element.
1783 self._write_d_table()
1785 # Write the c:spPr element for the plotarea formatting.
1786 self._write_sp_pr(self.plotarea)
1788 self._xml_end_tag("c:plotArea")
1790 def _write_layout(self, layout, layout_type):
1791 # Write the <c:layout> element.
1793 if not layout:
1794 # Automatic layout.
1795 self._xml_empty_tag("c:layout")
1796 else:
1797 # User defined manual layout.
1798 self._xml_start_tag("c:layout")
1799 self._write_manual_layout(layout, layout_type)
1800 self._xml_end_tag("c:layout")
1802 def _write_manual_layout(self, layout, layout_type):
1803 # Write the <c:manualLayout> element.
1804 self._xml_start_tag("c:manualLayout")
1806 # Plotarea has a layoutTarget element.
1807 if layout_type == "plot":
1808 self._xml_empty_tag("c:layoutTarget", [("val", "inner")])
1810 # Set the x, y positions.
1811 self._xml_empty_tag("c:xMode", [("val", "edge")])
1812 self._xml_empty_tag("c:yMode", [("val", "edge")])
1813 self._xml_empty_tag("c:x", [("val", layout["x"])])
1814 self._xml_empty_tag("c:y", [("val", layout["y"])])
1816 # For plotarea and legend set the width and height.
1817 if layout_type != "text":
1818 self._xml_empty_tag("c:w", [("val", layout["width"])])
1819 self._xml_empty_tag("c:h", [("val", layout["height"])])
1821 self._xml_end_tag("c:manualLayout")
1823 def _write_chart_type(self, options):
1824 # Write the chart type element. This method should be overridden
1825 # by the subclasses.
1826 return
1828 def _write_grouping(self, val):
1829 # Write the <c:grouping> element.
1830 attributes = [("val", val)]
1832 self._xml_empty_tag("c:grouping", attributes)
1834 def _write_series(self, series):
1835 # Write the series elements.
1836 self._write_ser(series)
1838 def _write_ser(self, series):
1839 # Write the <c:ser> element.
1840 index = self.series_index
1841 self.series_index += 1
1843 self._xml_start_tag("c:ser")
1845 # Write the c:idx element.
1846 self._write_idx(index)
1848 # Write the c:order element.
1849 self._write_order(index)
1851 # Write the series name.
1852 self._write_series_name(series)
1854 # Write the c:spPr element.
1855 self._write_sp_pr(series)
1857 # Write the c:marker element.
1858 self._write_marker(series["marker"])
1860 # Write the c:invertIfNegative element.
1861 self._write_c_invert_if_negative(series["invert_if_neg"])
1863 # Write the c:dPt element.
1864 self._write_d_pt(series["points"])
1866 # Write the c:dLbls element.
1867 self._write_d_lbls(series["labels"])
1869 # Write the c:trendline element.
1870 self._write_trendline(series["trendline"])
1872 # Write the c:errBars element.
1873 self._write_error_bars(series["error_bars"])
1875 # Write the c:cat element.
1876 self._write_cat(series)
1878 # Write the c:val element.
1879 self._write_val(series)
1881 # Write the c:smooth element.
1882 if self.smooth_allowed:
1883 self._write_c_smooth(series["smooth"])
1885 # Write the c:extLst element.
1886 if series.get("inverted_color"):
1887 self._write_c_ext_lst_inverted_color(series["inverted_color"])
1889 self._xml_end_tag("c:ser")
1891 def _write_c_ext_lst_inverted_color(self, color):
1892 # Write the <c:extLst> element for the inverted fill color.
1894 uri = "{6F2FDCE9-48DA-4B69-8628-5D25D57E5C99}"
1895 xmlns_c_14 = "http://schemas.microsoft.com/office/drawing/2007/8/2/chart"
1897 attributes1 = [
1898 ("uri", uri),
1899 ("xmlns:c14", xmlns_c_14),
1900 ]
1902 attributes2 = [("xmlns:c14", xmlns_c_14)]
1904 self._xml_start_tag("c:extLst")
1905 self._xml_start_tag("c:ext", attributes1)
1906 self._xml_start_tag("c14:invertSolidFillFmt")
1907 self._xml_start_tag("c14:spPr", attributes2)
1909 self._write_a_solid_fill({"color": color})
1911 self._xml_end_tag("c14:spPr")
1912 self._xml_end_tag("c14:invertSolidFillFmt")
1913 self._xml_end_tag("c:ext")
1914 self._xml_end_tag("c:extLst")
1916 def _write_c_ext_lst_display_na(self):
1917 # Write the <c:extLst> element for the display NA as empty cell option.
1919 uri = "{56B9EC1D-385E-4148-901F-78D8002777C0}"
1920 xmlns_c_16 = "http://schemas.microsoft.com/office/drawing/2017/03/chart"
1922 attributes1 = [
1923 ("uri", uri),
1924 ("xmlns:c16r3", xmlns_c_16),
1925 ]
1927 attributes2 = [("val", 1)]
1929 self._xml_start_tag("c:extLst")
1930 self._xml_start_tag("c:ext", attributes1)
1931 self._xml_start_tag("c16r3:dataDisplayOptions16")
1932 self._xml_empty_tag("c16r3:dispNaAsBlank", attributes2)
1933 self._xml_end_tag("c16r3:dataDisplayOptions16")
1934 self._xml_end_tag("c:ext")
1935 self._xml_end_tag("c:extLst")
1937 def _write_idx(self, val):
1938 # Write the <c:idx> element.
1940 attributes = [("val", val)]
1942 self._xml_empty_tag("c:idx", attributes)
1944 def _write_order(self, val):
1945 # Write the <c:order> element.
1947 attributes = [("val", val)]
1949 self._xml_empty_tag("c:order", attributes)
1951 def _write_series_name(self, series):
1952 # Write the series name.
1954 if series["name_formula"] is not None:
1955 self._write_tx_formula(series["name_formula"], series["name_id"])
1956 elif series["name"] is not None:
1957 self._write_tx_value(series["name"])
1959 def _write_c_smooth(self, smooth):
1960 # Write the <c:smooth> element.
1962 if smooth:
1963 self._xml_empty_tag("c:smooth", [("val", "1")])
1965 def _write_cat(self, series):
1966 # Write the <c:cat> element.
1967 formula = series["categories"]
1968 data_id = series["cat_data_id"]
1969 data = None
1971 if data_id is not None:
1972 data = self.formula_data[data_id]
1974 # Ignore <c:cat> elements for charts without category values.
1975 if not formula:
1976 return
1978 self._xml_start_tag("c:cat")
1980 # Check the type of cached data.
1981 cat_type = self._get_data_type(data)
1983 if cat_type == "str":
1984 self.cat_has_num_fmt = 0
1985 # Write the c:numRef element.
1986 self._write_str_ref(formula, data, cat_type)
1988 elif cat_type == "multi_str":
1989 self.cat_has_num_fmt = 0
1990 # Write the c:numRef element.
1991 self._write_multi_lvl_str_ref(formula, data)
1993 else:
1994 self.cat_has_num_fmt = 1
1995 # Write the c:numRef element.
1996 self._write_num_ref(formula, data, cat_type)
1998 self._xml_end_tag("c:cat")
2000 def _write_val(self, series):
2001 # Write the <c:val> element.
2002 formula = series["values"]
2003 data_id = series["val_data_id"]
2004 data = self.formula_data[data_id]
2006 self._xml_start_tag("c:val")
2008 # Unlike Cat axes data should only be numeric.
2009 # Write the c:numRef element.
2010 self._write_num_ref(formula, data, "num")
2012 self._xml_end_tag("c:val")
2014 def _write_num_ref(self, formula, data, ref_type):
2015 # Write the <c:numRef> element.
2016 self._xml_start_tag("c:numRef")
2018 # Write the c:f element.
2019 self._write_series_formula(formula)
2021 if ref_type == "num":
2022 # Write the c:numCache element.
2023 self._write_num_cache(data)
2024 elif ref_type == "str":
2025 # Write the c:strCache element.
2026 self._write_str_cache(data)
2028 self._xml_end_tag("c:numRef")
2030 def _write_str_ref(self, formula, data, ref_type):
2031 # Write the <c:strRef> element.
2033 self._xml_start_tag("c:strRef")
2035 # Write the c:f element.
2036 self._write_series_formula(formula)
2038 if ref_type == "num":
2039 # Write the c:numCache element.
2040 self._write_num_cache(data)
2041 elif ref_type == "str":
2042 # Write the c:strCache element.
2043 self._write_str_cache(data)
2045 self._xml_end_tag("c:strRef")
2047 def _write_multi_lvl_str_ref(self, formula, data):
2048 # Write the <c:multiLvlStrRef> element.
2050 if not data:
2051 return
2053 self._xml_start_tag("c:multiLvlStrRef")
2055 # Write the c:f element.
2056 self._write_series_formula(formula)
2058 self._xml_start_tag("c:multiLvlStrCache")
2060 # Write the c:ptCount element.
2061 count = len(data[-1])
2062 self._write_pt_count(count)
2064 for cat_data in reversed(data):
2065 self._xml_start_tag("c:lvl")
2067 for i, point in enumerate(cat_data):
2068 # Write the c:pt element.
2069 self._write_pt(i, cat_data[i])
2071 self._xml_end_tag("c:lvl")
2073 self._xml_end_tag("c:multiLvlStrCache")
2074 self._xml_end_tag("c:multiLvlStrRef")
2076 def _write_series_formula(self, formula):
2077 # Write the <c:f> element.
2079 # Strip the leading '=' from the formula.
2080 if formula.startswith("="):
2081 formula = formula.lstrip("=")
2083 self._xml_data_element("c:f", formula)
2085 def _write_axis_ids(self, args):
2086 # Write the <c:axId> elements for the primary or secondary axes.
2088 # Generate the axis ids.
2089 self._add_axis_ids(args)
2091 if args["primary_axes"]:
2092 # Write the axis ids for the primary axes.
2093 self._write_axis_id(self.axis_ids[0])
2094 self._write_axis_id(self.axis_ids[1])
2095 else:
2096 # Write the axis ids for the secondary axes.
2097 self._write_axis_id(self.axis2_ids[0])
2098 self._write_axis_id(self.axis2_ids[1])
2100 def _write_axis_id(self, val):
2101 # Write the <c:axId> element.
2103 attributes = [("val", val)]
2105 self._xml_empty_tag("c:axId", attributes)
2107 def _write_cat_axis(self, args):
2108 # Write the <c:catAx> element. Usually the X axis.
2109 x_axis = args["x_axis"]
2110 y_axis = args["y_axis"]
2111 axis_ids = args["axis_ids"]
2113 # If there are no axis_ids then we don't need to write this element.
2114 if axis_ids is None or not len(axis_ids):
2115 return
2117 position = self.cat_axis_position
2118 is_y_axis = self.horiz_cat_axis
2120 # Overwrite the default axis position with a user supplied value.
2121 if x_axis.get("position"):
2122 position = x_axis["position"]
2124 self._xml_start_tag("c:catAx")
2126 self._write_axis_id(axis_ids[0])
2128 # Write the c:scaling element.
2129 self._write_scaling(x_axis.get("reverse"), None, None, None)
2131 if not x_axis.get("visible"):
2132 self._write_delete(1)
2134 # Write the c:axPos element.
2135 self._write_axis_pos(position, y_axis.get("reverse"))
2137 # Write the c:majorGridlines element.
2138 self._write_major_gridlines(x_axis.get("major_gridlines"))
2140 # Write the c:minorGridlines element.
2141 self._write_minor_gridlines(x_axis.get("minor_gridlines"))
2143 # Write the axis title elements.
2144 if x_axis["formula"] is not None:
2145 self._write_title_formula(
2146 x_axis["formula"],
2147 x_axis["data_id"],
2148 is_y_axis,
2149 x_axis["name_font"],
2150 x_axis["name_layout"],
2151 )
2152 elif x_axis["name"] is not None:
2153 self._write_title_rich(
2154 x_axis["name"], is_y_axis, x_axis["name_font"], x_axis["name_layout"]
2155 )
2157 # Write the c:numFmt element.
2158 self._write_cat_number_format(x_axis)
2160 # Write the c:majorTickMark element.
2161 self._write_major_tick_mark(x_axis.get("major_tick_mark"))
2163 # Write the c:minorTickMark element.
2164 self._write_minor_tick_mark(x_axis.get("minor_tick_mark"))
2166 # Write the c:tickLblPos element.
2167 self._write_tick_label_pos(x_axis.get("label_position"))
2169 # Write the c:spPr element for the axis line.
2170 self._write_sp_pr(x_axis)
2172 # Write the axis font elements.
2173 self._write_axis_font(x_axis.get("num_font"))
2175 # Write the c:crossAx element.
2176 self._write_cross_axis(axis_ids[1])
2178 if self.show_crosses or x_axis.get("visible"):
2179 # Note, the category crossing comes from the value axis.
2180 if (
2181 y_axis.get("crossing") is None
2182 or y_axis.get("crossing") == "max"
2183 or y_axis["crossing"] == "min"
2184 ):
2185 # Write the c:crosses element.
2186 self._write_crosses(y_axis.get("crossing"))
2187 else:
2188 # Write the c:crossesAt element.
2189 self._write_c_crosses_at(y_axis.get("crossing"))
2191 # Write the c:auto element.
2192 if not x_axis.get("text_axis"):
2193 self._write_auto(1)
2195 # Write the c:labelAlign element.
2196 self._write_label_align(x_axis.get("label_align"))
2198 # Write the c:labelOffset element.
2199 self._write_label_offset(100)
2201 # Write the c:tickLblSkip element.
2202 self._write_c_tick_lbl_skip(x_axis.get("interval_unit"))
2204 # Write the c:tickMarkSkip element.
2205 self._write_c_tick_mark_skip(x_axis.get("interval_tick"))
2207 self._xml_end_tag("c:catAx")
2209 def _write_val_axis(self, args):
2210 # Write the <c:valAx> element. Usually the Y axis.
2211 x_axis = args["x_axis"]
2212 y_axis = args["y_axis"]
2213 axis_ids = args["axis_ids"]
2214 position = args.get("position", self.val_axis_position)
2215 is_y_axis = self.horiz_val_axis
2217 # If there are no axis_ids then we don't need to write this element.
2218 if axis_ids is None or not len(axis_ids):
2219 return
2221 # Overwrite the default axis position with a user supplied value.
2222 position = y_axis.get("position") or position
2224 self._xml_start_tag("c:valAx")
2226 self._write_axis_id(axis_ids[1])
2228 # Write the c:scaling element.
2229 self._write_scaling(
2230 y_axis.get("reverse"),
2231 y_axis.get("min"),
2232 y_axis.get("max"),
2233 y_axis.get("log_base"),
2234 )
2236 if not y_axis.get("visible"):
2237 self._write_delete(1)
2239 # Write the c:axPos element.
2240 self._write_axis_pos(position, x_axis.get("reverse"))
2242 # Write the c:majorGridlines element.
2243 self._write_major_gridlines(y_axis.get("major_gridlines"))
2245 # Write the c:minorGridlines element.
2246 self._write_minor_gridlines(y_axis.get("minor_gridlines"))
2248 # Write the axis title elements.
2249 if y_axis["formula"] is not None:
2250 self._write_title_formula(
2251 y_axis["formula"],
2252 y_axis["data_id"],
2253 is_y_axis,
2254 y_axis["name_font"],
2255 y_axis["name_layout"],
2256 )
2257 elif y_axis["name"] is not None:
2258 self._write_title_rich(
2259 y_axis["name"],
2260 is_y_axis,
2261 y_axis.get("name_font"),
2262 y_axis.get("name_layout"),
2263 )
2265 # Write the c:numberFormat element.
2266 self._write_number_format(y_axis)
2268 # Write the c:majorTickMark element.
2269 self._write_major_tick_mark(y_axis.get("major_tick_mark"))
2271 # Write the c:minorTickMark element.
2272 self._write_minor_tick_mark(y_axis.get("minor_tick_mark"))
2274 # Write the c:tickLblPos element.
2275 self._write_tick_label_pos(y_axis.get("label_position"))
2277 # Write the c:spPr element for the axis line.
2278 self._write_sp_pr(y_axis)
2280 # Write the axis font elements.
2281 self._write_axis_font(y_axis.get("num_font"))
2283 # Write the c:crossAx element.
2284 self._write_cross_axis(axis_ids[0])
2286 # Note, the category crossing comes from the value axis.
2287 if (
2288 x_axis.get("crossing") is None
2289 or x_axis["crossing"] == "max"
2290 or x_axis["crossing"] == "min"
2291 ):
2292 # Write the c:crosses element.
2293 self._write_crosses(x_axis.get("crossing"))
2294 else:
2295 # Write the c:crossesAt element.
2296 self._write_c_crosses_at(x_axis.get("crossing"))
2298 # Write the c:crossBetween element.
2299 self._write_cross_between(x_axis.get("position_axis"))
2301 # Write the c:majorUnit element.
2302 self._write_c_major_unit(y_axis.get("major_unit"))
2304 # Write the c:minorUnit element.
2305 self._write_c_minor_unit(y_axis.get("minor_unit"))
2307 # Write the c:dispUnits element.
2308 self._write_disp_units(
2309 y_axis.get("display_units"), y_axis.get("display_units_visible")
2310 )
2312 self._xml_end_tag("c:valAx")
2314 def _write_cat_val_axis(self, args):
2315 # Write the <c:valAx> element. This is for the second valAx
2316 # in scatter plots. Usually the X axis.
2317 x_axis = args["x_axis"]
2318 y_axis = args["y_axis"]
2319 axis_ids = args["axis_ids"]
2320 position = args["position"] or self.val_axis_position
2321 is_y_axis = self.horiz_val_axis
2323 # If there are no axis_ids then we don't need to write this element.
2324 if axis_ids is None or not len(axis_ids):
2325 return
2327 # Overwrite the default axis position with a user supplied value.
2328 position = x_axis.get("position") or position
2330 self._xml_start_tag("c:valAx")
2332 self._write_axis_id(axis_ids[0])
2334 # Write the c:scaling element.
2335 self._write_scaling(
2336 x_axis.get("reverse"),
2337 x_axis.get("min"),
2338 x_axis.get("max"),
2339 x_axis.get("log_base"),
2340 )
2342 if not x_axis.get("visible"):
2343 self._write_delete(1)
2345 # Write the c:axPos element.
2346 self._write_axis_pos(position, y_axis.get("reverse"))
2348 # Write the c:majorGridlines element.
2349 self._write_major_gridlines(x_axis.get("major_gridlines"))
2351 # Write the c:minorGridlines element.
2352 self._write_minor_gridlines(x_axis.get("minor_gridlines"))
2354 # Write the axis title elements.
2355 if x_axis["formula"] is not None:
2356 self._write_title_formula(
2357 x_axis["formula"],
2358 x_axis["data_id"],
2359 is_y_axis,
2360 x_axis["name_font"],
2361 x_axis["name_layout"],
2362 )
2363 elif x_axis["name"] is not None:
2364 self._write_title_rich(
2365 x_axis["name"], is_y_axis, x_axis["name_font"], x_axis["name_layout"]
2366 )
2368 # Write the c:numberFormat element.
2369 self._write_number_format(x_axis)
2371 # Write the c:majorTickMark element.
2372 self._write_major_tick_mark(x_axis.get("major_tick_mark"))
2374 # Write the c:minorTickMark element.
2375 self._write_minor_tick_mark(x_axis.get("minor_tick_mark"))
2377 # Write the c:tickLblPos element.
2378 self._write_tick_label_pos(x_axis.get("label_position"))
2380 # Write the c:spPr element for the axis line.
2381 self._write_sp_pr(x_axis)
2383 # Write the axis font elements.
2384 self._write_axis_font(x_axis.get("num_font"))
2386 # Write the c:crossAx element.
2387 self._write_cross_axis(axis_ids[1])
2389 # Note, the category crossing comes from the value axis.
2390 if (
2391 y_axis.get("crossing") is None
2392 or y_axis["crossing"] == "max"
2393 or y_axis["crossing"] == "min"
2394 ):
2395 # Write the c:crosses element.
2396 self._write_crosses(y_axis.get("crossing"))
2397 else:
2398 # Write the c:crossesAt element.
2399 self._write_c_crosses_at(y_axis.get("crossing"))
2401 # Write the c:crossBetween element.
2402 self._write_cross_between(y_axis.get("position_axis"))
2404 # Write the c:majorUnit element.
2405 self._write_c_major_unit(x_axis.get("major_unit"))
2407 # Write the c:minorUnit element.
2408 self._write_c_minor_unit(x_axis.get("minor_unit"))
2410 # Write the c:dispUnits element.
2411 self._write_disp_units(
2412 x_axis.get("display_units"), x_axis.get("display_units_visible")
2413 )
2415 self._xml_end_tag("c:valAx")
2417 def _write_date_axis(self, args):
2418 # Write the <c:dateAx> element. Usually the X axis.
2419 x_axis = args["x_axis"]
2420 y_axis = args["y_axis"]
2421 axis_ids = args["axis_ids"]
2423 # If there are no axis_ids then we don't need to write this element.
2424 if axis_ids is None or not len(axis_ids):
2425 return
2427 position = self.cat_axis_position
2429 # Overwrite the default axis position with a user supplied value.
2430 position = x_axis.get("position") or position
2432 self._xml_start_tag("c:dateAx")
2434 self._write_axis_id(axis_ids[0])
2436 # Write the c:scaling element.
2437 self._write_scaling(
2438 x_axis.get("reverse"),
2439 x_axis.get("min"),
2440 x_axis.get("max"),
2441 x_axis.get("log_base"),
2442 )
2444 if not x_axis.get("visible"):
2445 self._write_delete(1)
2447 # Write the c:axPos element.
2448 self._write_axis_pos(position, y_axis.get("reverse"))
2450 # Write the c:majorGridlines element.
2451 self._write_major_gridlines(x_axis.get("major_gridlines"))
2453 # Write the c:minorGridlines element.
2454 self._write_minor_gridlines(x_axis.get("minor_gridlines"))
2456 # Write the axis title elements.
2457 if x_axis["formula"] is not None:
2458 self._write_title_formula(
2459 x_axis["formula"],
2460 x_axis["data_id"],
2461 None,
2462 x_axis["name_font"],
2463 x_axis["name_layout"],
2464 )
2465 elif x_axis["name"] is not None:
2466 self._write_title_rich(
2467 x_axis["name"], None, x_axis["name_font"], x_axis["name_layout"]
2468 )
2470 # Write the c:numFmt element.
2471 self._write_number_format(x_axis)
2473 # Write the c:majorTickMark element.
2474 self._write_major_tick_mark(x_axis.get("major_tick_mark"))
2476 # Write the c:minorTickMark element.
2477 self._write_minor_tick_mark(x_axis.get("minor_tick_mark"))
2479 # Write the c:tickLblPos element.
2480 self._write_tick_label_pos(x_axis.get("label_position"))
2482 # Write the c:spPr element for the axis line.
2483 self._write_sp_pr(x_axis)
2485 # Write the axis font elements.
2486 self._write_axis_font(x_axis.get("num_font"))
2488 # Write the c:crossAx element.
2489 self._write_cross_axis(axis_ids[1])
2491 if self.show_crosses or x_axis.get("visible"):
2492 # Note, the category crossing comes from the value axis.
2493 if (
2494 y_axis.get("crossing") is None
2495 or y_axis.get("crossing") == "max"
2496 or y_axis["crossing"] == "min"
2497 ):
2498 # Write the c:crosses element.
2499 self._write_crosses(y_axis.get("crossing"))
2500 else:
2501 # Write the c:crossesAt element.
2502 self._write_c_crosses_at(y_axis.get("crossing"))
2504 # Write the c:auto element.
2505 self._write_auto(1)
2507 # Write the c:labelOffset element.
2508 self._write_label_offset(100)
2510 # Write the c:tickLblSkip element.
2511 self._write_c_tick_lbl_skip(x_axis.get("interval_unit"))
2513 # Write the c:tickMarkSkip element.
2514 self._write_c_tick_mark_skip(x_axis.get("interval_tick"))
2516 # Write the c:majorUnit element.
2517 self._write_c_major_unit(x_axis.get("major_unit"))
2519 # Write the c:majorTimeUnit element.
2520 if x_axis.get("major_unit"):
2521 self._write_c_major_time_unit(x_axis["major_unit_type"])
2523 # Write the c:minorUnit element.
2524 self._write_c_minor_unit(x_axis.get("minor_unit"))
2526 # Write the c:minorTimeUnit element.
2527 if x_axis.get("minor_unit"):
2528 self._write_c_minor_time_unit(x_axis["minor_unit_type"])
2530 self._xml_end_tag("c:dateAx")
2532 def _write_scaling(self, reverse, min_val, max_val, log_base):
2533 # Write the <c:scaling> element.
2535 self._xml_start_tag("c:scaling")
2537 # Write the c:logBase element.
2538 self._write_c_log_base(log_base)
2540 # Write the c:orientation element.
2541 self._write_orientation(reverse)
2543 # Write the c:max element.
2544 self._write_c_max(max_val)
2546 # Write the c:min element.
2547 self._write_c_min(min_val)
2549 self._xml_end_tag("c:scaling")
2551 def _write_c_log_base(self, val):
2552 # Write the <c:logBase> element.
2554 if not val:
2555 return
2557 attributes = [("val", val)]
2559 self._xml_empty_tag("c:logBase", attributes)
2561 def _write_orientation(self, reverse):
2562 # Write the <c:orientation> element.
2563 val = "minMax"
2565 if reverse:
2566 val = "maxMin"
2568 attributes = [("val", val)]
2570 self._xml_empty_tag("c:orientation", attributes)
2572 def _write_c_max(self, max_val):
2573 # Write the <c:max> element.
2575 if max_val is None:
2576 return
2578 attributes = [("val", max_val)]
2580 self._xml_empty_tag("c:max", attributes)
2582 def _write_c_min(self, min_val):
2583 # Write the <c:min> element.
2585 if min_val is None:
2586 return
2588 attributes = [("val", min_val)]
2590 self._xml_empty_tag("c:min", attributes)
2592 def _write_axis_pos(self, val, reverse):
2593 # Write the <c:axPos> element.
2595 if reverse:
2596 if val == "l":
2597 val = "r"
2598 if val == "b":
2599 val = "t"
2601 attributes = [("val", val)]
2603 self._xml_empty_tag("c:axPos", attributes)
2605 def _write_number_format(self, axis):
2606 # Write the <c:numberFormat> element. Note: It is assumed that if
2607 # a user defined number format is supplied (i.e., non-default) then
2608 # the sourceLinked attribute is 0.
2609 # The user can override this if required.
2610 format_code = axis.get("num_format")
2611 source_linked = 1
2613 # Check if a user defined number format has been set.
2614 if format_code is not None and format_code != axis["defaults"]["num_format"]:
2615 source_linked = 0
2617 # User override of sourceLinked.
2618 if axis.get("num_format_linked"):
2619 source_linked = 1
2621 attributes = [
2622 ("formatCode", format_code),
2623 ("sourceLinked", source_linked),
2624 ]
2626 self._xml_empty_tag("c:numFmt", attributes)
2628 def _write_cat_number_format(self, axis):
2629 # Write the <c:numFmt> element. Special case handler for category
2630 # axes which don't always have a number format.
2631 format_code = axis.get("num_format")
2632 source_linked = 1
2633 default_format = 1
2635 # Check if a user defined number format has been set.
2636 if format_code is not None and format_code != axis["defaults"]["num_format"]:
2637 source_linked = 0
2638 default_format = 0
2640 # User override of sourceLinked.
2641 if axis.get("num_format_linked"):
2642 source_linked = 1
2644 # Skip if cat doesn't have a num format (unless it is non-default).
2645 if not self.cat_has_num_fmt and default_format:
2646 return
2648 attributes = [
2649 ("formatCode", format_code),
2650 ("sourceLinked", source_linked),
2651 ]
2653 self._xml_empty_tag("c:numFmt", attributes)
2655 def _write_data_label_number_format(self, format_code):
2656 # Write the <c:numberFormat> element for data labels.
2657 source_linked = 0
2659 attributes = [
2660 ("formatCode", format_code),
2661 ("sourceLinked", source_linked),
2662 ]
2664 self._xml_empty_tag("c:numFmt", attributes)
2666 def _write_major_tick_mark(self, val):
2667 # Write the <c:majorTickMark> element.
2669 if not val:
2670 return
2672 attributes = [("val", val)]
2674 self._xml_empty_tag("c:majorTickMark", attributes)
2676 def _write_minor_tick_mark(self, val):
2677 # Write the <c:minorTickMark> element.
2679 if not val:
2680 return
2682 attributes = [("val", val)]
2684 self._xml_empty_tag("c:minorTickMark", attributes)
2686 def _write_tick_label_pos(self, val=None):
2687 # Write the <c:tickLblPos> element.
2688 if val is None or val == "next_to":
2689 val = "nextTo"
2691 attributes = [("val", val)]
2693 self._xml_empty_tag("c:tickLblPos", attributes)
2695 def _write_cross_axis(self, val):
2696 # Write the <c:crossAx> element.
2698 attributes = [("val", val)]
2700 self._xml_empty_tag("c:crossAx", attributes)
2702 def _write_crosses(self, val=None):
2703 # Write the <c:crosses> element.
2704 if val is None:
2705 val = "autoZero"
2707 attributes = [("val", val)]
2709 self._xml_empty_tag("c:crosses", attributes)
2711 def _write_c_crosses_at(self, val):
2712 # Write the <c:crossesAt> element.
2714 attributes = [("val", val)]
2716 self._xml_empty_tag("c:crossesAt", attributes)
2718 def _write_auto(self, val):
2719 # Write the <c:auto> element.
2721 attributes = [("val", val)]
2723 self._xml_empty_tag("c:auto", attributes)
2725 def _write_label_align(self, val=None):
2726 # Write the <c:labelAlign> element.
2728 if val is None:
2729 val = "ctr"
2731 if val == "right":
2732 val = "r"
2734 if val == "left":
2735 val = "l"
2737 attributes = [("val", val)]
2739 self._xml_empty_tag("c:lblAlgn", attributes)
2741 def _write_label_offset(self, val):
2742 # Write the <c:labelOffset> element.
2744 attributes = [("val", val)]
2746 self._xml_empty_tag("c:lblOffset", attributes)
2748 def _write_c_tick_lbl_skip(self, val):
2749 # Write the <c:tickLblSkip> element.
2750 if val is None:
2751 return
2753 attributes = [("val", val)]
2755 self._xml_empty_tag("c:tickLblSkip", attributes)
2757 def _write_c_tick_mark_skip(self, val):
2758 # Write the <c:tickMarkSkip> element.
2759 if val is None:
2760 return
2762 attributes = [("val", val)]
2764 self._xml_empty_tag("c:tickMarkSkip", attributes)
2766 def _write_major_gridlines(self, gridlines):
2767 # Write the <c:majorGridlines> element.
2769 if not gridlines:
2770 return
2772 if not gridlines["visible"]:
2773 return
2775 if gridlines["line"]["defined"]:
2776 self._xml_start_tag("c:majorGridlines")
2778 # Write the c:spPr element.
2779 self._write_sp_pr(gridlines)
2781 self._xml_end_tag("c:majorGridlines")
2782 else:
2783 self._xml_empty_tag("c:majorGridlines")
2785 def _write_minor_gridlines(self, gridlines):
2786 # Write the <c:minorGridlines> element.
2788 if not gridlines:
2789 return
2791 if not gridlines["visible"]:
2792 return
2794 if gridlines["line"]["defined"]:
2795 self._xml_start_tag("c:minorGridlines")
2797 # Write the c:spPr element.
2798 self._write_sp_pr(gridlines)
2800 self._xml_end_tag("c:minorGridlines")
2801 else:
2802 self._xml_empty_tag("c:minorGridlines")
2804 def _write_cross_between(self, val):
2805 # Write the <c:crossBetween> element.
2806 if val is None:
2807 val = self.cross_between
2809 attributes = [("val", val)]
2811 self._xml_empty_tag("c:crossBetween", attributes)
2813 def _write_c_major_unit(self, val):
2814 # Write the <c:majorUnit> element.
2816 if not val:
2817 return
2819 attributes = [("val", val)]
2821 self._xml_empty_tag("c:majorUnit", attributes)
2823 def _write_c_minor_unit(self, val):
2824 # Write the <c:minorUnit> element.
2826 if not val:
2827 return
2829 attributes = [("val", val)]
2831 self._xml_empty_tag("c:minorUnit", attributes)
2833 def _write_c_major_time_unit(self, val=None):
2834 # Write the <c:majorTimeUnit> element.
2835 if val is None:
2836 val = "days"
2838 attributes = [("val", val)]
2840 self._xml_empty_tag("c:majorTimeUnit", attributes)
2842 def _write_c_minor_time_unit(self, val=None):
2843 # Write the <c:minorTimeUnit> element.
2844 if val is None:
2845 val = "days"
2847 attributes = [("val", val)]
2849 self._xml_empty_tag("c:minorTimeUnit", attributes)
2851 def _write_legend(self):
2852 # Write the <c:legend> element.
2853 legend = self.legend
2854 position = legend.get("position", "right")
2855 font = legend.get("font")
2856 delete_series = []
2857 overlay = 0
2859 if legend.get("delete_series") and isinstance(legend["delete_series"], list):
2860 delete_series = legend["delete_series"]
2862 if position.startswith("overlay_"):
2863 position = position.replace("overlay_", "")
2864 overlay = 1
2866 allowed = {
2867 "right": "r",
2868 "left": "l",
2869 "top": "t",
2870 "bottom": "b",
2871 "top_right": "tr",
2872 }
2874 if position == "none":
2875 return
2877 if position not in allowed:
2878 return
2880 position = allowed[position]
2882 self._xml_start_tag("c:legend")
2884 # Write the c:legendPos element.
2885 self._write_legend_pos(position)
2887 # Remove series labels from the legend.
2888 for index in delete_series:
2889 # Write the c:legendEntry element.
2890 self._write_legend_entry(index)
2892 # Write the c:layout element.
2893 self._write_layout(legend.get("layout"), "legend")
2895 # Write the c:overlay element.
2896 if overlay:
2897 self._write_overlay()
2899 if font:
2900 self._write_tx_pr(font)
2902 # Write the c:spPr element.
2903 self._write_sp_pr(legend)
2905 self._xml_end_tag("c:legend")
2907 def _write_legend_pos(self, val):
2908 # Write the <c:legendPos> element.
2910 attributes = [("val", val)]
2912 self._xml_empty_tag("c:legendPos", attributes)
2914 def _write_legend_entry(self, index):
2915 # Write the <c:legendEntry> element.
2917 self._xml_start_tag("c:legendEntry")
2919 # Write the c:idx element.
2920 self._write_idx(index)
2922 # Write the c:delete element.
2923 self._write_delete(1)
2925 self._xml_end_tag("c:legendEntry")
2927 def _write_overlay(self):
2928 # Write the <c:overlay> element.
2929 val = 1
2931 attributes = [("val", val)]
2933 self._xml_empty_tag("c:overlay", attributes)
2935 def _write_plot_vis_only(self):
2936 # Write the <c:plotVisOnly> element.
2937 val = 1
2939 # Ignore this element if we are plotting hidden data.
2940 if self.show_hidden:
2941 return
2943 attributes = [("val", val)]
2945 self._xml_empty_tag("c:plotVisOnly", attributes)
2947 def _write_print_settings(self):
2948 # Write the <c:printSettings> element.
2949 self._xml_start_tag("c:printSettings")
2951 # Write the c:headerFooter element.
2952 self._write_header_footer()
2954 # Write the c:pageMargins element.
2955 self._write_page_margins()
2957 # Write the c:pageSetup element.
2958 self._write_page_setup()
2960 self._xml_end_tag("c:printSettings")
2962 def _write_header_footer(self):
2963 # Write the <c:headerFooter> element.
2964 self._xml_empty_tag("c:headerFooter")
2966 def _write_page_margins(self):
2967 # Write the <c:pageMargins> element.
2968 bottom = 0.75
2969 left = 0.7
2970 right = 0.7
2971 top = 0.75
2972 header = 0.3
2973 footer = 0.3
2975 attributes = [
2976 ("b", bottom),
2977 ("l", left),
2978 ("r", right),
2979 ("t", top),
2980 ("header", header),
2981 ("footer", footer),
2982 ]
2984 self._xml_empty_tag("c:pageMargins", attributes)
2986 def _write_page_setup(self):
2987 # Write the <c:pageSetup> element.
2988 self._xml_empty_tag("c:pageSetup")
2990 def _write_c_auto_title_deleted(self):
2991 # Write the <c:autoTitleDeleted> element.
2992 self._xml_empty_tag("c:autoTitleDeleted", [("val", 1)])
2994 def _write_title_rich(self, title, is_y_axis, font, layout, overlay=False):
2995 # Write the <c:title> element for a rich string.
2997 self._xml_start_tag("c:title")
2999 # Write the c:tx element.
3000 self._write_tx_rich(title, is_y_axis, font)
3002 # Write the c:layout element.
3003 self._write_layout(layout, "text")
3005 # Write the c:overlay element.
3006 if overlay:
3007 self._write_overlay()
3009 self._xml_end_tag("c:title")
3011 def _write_title_formula(
3012 self, title, data_id, is_y_axis, font, layout, overlay=False
3013 ):
3014 # Write the <c:title> element for a rich string.
3016 self._xml_start_tag("c:title")
3018 # Write the c:tx element.
3019 self._write_tx_formula(title, data_id)
3021 # Write the c:layout element.
3022 self._write_layout(layout, "text")
3024 # Write the c:overlay element.
3025 if overlay:
3026 self._write_overlay()
3028 # Write the c:txPr element.
3029 self._write_tx_pr(font, is_y_axis)
3031 self._xml_end_tag("c:title")
3033 def _write_tx_rich(self, title, is_y_axis, font):
3034 # Write the <c:tx> element.
3036 self._xml_start_tag("c:tx")
3038 # Write the c:rich element.
3039 self._write_rich(title, font, is_y_axis, ignore_rich_pr=False)
3041 self._xml_end_tag("c:tx")
3043 def _write_tx_value(self, title):
3044 # Write the <c:tx> element with a value such as for series names.
3046 self._xml_start_tag("c:tx")
3048 # Write the c:v element.
3049 self._write_v(title)
3051 self._xml_end_tag("c:tx")
3053 def _write_tx_formula(self, title, data_id):
3054 # Write the <c:tx> element.
3055 data = None
3057 if data_id is not None:
3058 data = self.formula_data[data_id]
3060 self._xml_start_tag("c:tx")
3062 # Write the c:strRef element.
3063 self._write_str_ref(title, data, "str")
3065 self._xml_end_tag("c:tx")
3067 def _write_rich(self, title, font, is_y_axis, ignore_rich_pr):
3068 # Write the <c:rich> element.
3070 if font and font.get("rotation") is not None:
3071 rotation = font["rotation"]
3072 else:
3073 rotation = None
3075 self._xml_start_tag("c:rich")
3077 # Write the a:bodyPr element.
3078 self._write_a_body_pr(rotation, is_y_axis)
3080 # Write the a:lstStyle element.
3081 self._write_a_lst_style()
3083 # Write the a:p element.
3084 self._write_a_p_rich(title, font, ignore_rich_pr)
3086 self._xml_end_tag("c:rich")
3088 def _write_a_body_pr(self, rotation, is_y_axis):
3089 # Write the <a:bodyPr> element.
3090 attributes = []
3092 if rotation is None and is_y_axis:
3093 rotation = -5400000
3095 if rotation is not None:
3096 if rotation == 16200000:
3097 # 270 deg/stacked angle.
3098 attributes.append(("rot", 0))
3099 attributes.append(("vert", "wordArtVert"))
3100 elif rotation == 16260000:
3101 # 271 deg/East Asian vertical.
3102 attributes.append(("rot", 0))
3103 attributes.append(("vert", "eaVert"))
3104 else:
3105 attributes.append(("rot", rotation))
3106 attributes.append(("vert", "horz"))
3108 self._xml_empty_tag("a:bodyPr", attributes)
3110 def _write_a_lst_style(self):
3111 # Write the <a:lstStyle> element.
3112 self._xml_empty_tag("a:lstStyle")
3114 def _write_a_p_rich(self, title, font, ignore_rich_pr):
3115 # Write the <a:p> element for rich string titles.
3117 self._xml_start_tag("a:p")
3119 # Write the a:pPr element.
3120 if not ignore_rich_pr:
3121 self._write_a_p_pr_rich(font)
3123 # Write the a:r element.
3124 self._write_a_r(title, font)
3126 self._xml_end_tag("a:p")
3128 def _write_a_p_formula(self, font):
3129 # Write the <a:p> element for formula titles.
3131 self._xml_start_tag("a:p")
3133 # Write the a:pPr element.
3134 self._write_a_p_pr_rich(font)
3136 # Write the a:endParaRPr element.
3137 self._write_a_end_para_rpr()
3139 self._xml_end_tag("a:p")
3141 def _write_a_p_pr_rich(self, font):
3142 # Write the <a:pPr> element for rich string titles.
3144 self._xml_start_tag("a:pPr")
3146 # Write the a:defRPr element.
3147 self._write_a_def_rpr(font)
3149 self._xml_end_tag("a:pPr")
3151 def _write_a_def_rpr(self, font):
3152 # Write the <a:defRPr> element.
3153 has_color = 0
3155 style_attributes = Shape._get_font_style_attributes(font)
3156 latin_attributes = Shape._get_font_latin_attributes(font)
3158 if font and font.get("color") is not None:
3159 has_color = 1
3161 if latin_attributes or has_color:
3162 self._xml_start_tag("a:defRPr", style_attributes)
3164 if has_color:
3165 self._write_a_solid_fill({"color": font["color"]})
3167 if latin_attributes:
3168 self._write_a_latin(latin_attributes)
3170 self._xml_end_tag("a:defRPr")
3171 else:
3172 self._xml_empty_tag("a:defRPr", style_attributes)
3174 def _write_a_end_para_rpr(self):
3175 # Write the <a:endParaRPr> element.
3176 lang = "en-US"
3178 attributes = [("lang", lang)]
3180 self._xml_empty_tag("a:endParaRPr", attributes)
3182 def _write_a_r(self, title, font):
3183 # Write the <a:r> element.
3185 self._xml_start_tag("a:r")
3187 # Write the a:rPr element.
3188 self._write_a_r_pr(font)
3190 # Write the a:t element.
3191 self._write_a_t(title)
3193 self._xml_end_tag("a:r")
3195 def _write_a_r_pr(self, font):
3196 # Write the <a:rPr> element.
3197 has_color = 0
3198 lang = "en-US"
3200 style_attributes = Shape._get_font_style_attributes(font)
3201 latin_attributes = Shape._get_font_latin_attributes(font)
3203 if font and font["color"] is not None:
3204 has_color = 1
3206 # Add the lang type to the attributes.
3207 style_attributes.insert(0, ("lang", lang))
3209 if latin_attributes or has_color:
3210 self._xml_start_tag("a:rPr", style_attributes)
3212 if has_color:
3213 self._write_a_solid_fill({"color": font["color"]})
3215 if latin_attributes:
3216 self._write_a_latin(latin_attributes)
3218 self._xml_end_tag("a:rPr")
3219 else:
3220 self._xml_empty_tag("a:rPr", style_attributes)
3222 def _write_a_t(self, title):
3223 # Write the <a:t> element.
3225 self._xml_data_element("a:t", title)
3227 def _write_tx_pr(self, font, is_y_axis=False):
3228 # Write the <c:txPr> element.
3230 if font and font.get("rotation") is not None:
3231 rotation = font["rotation"]
3232 else:
3233 rotation = None
3235 self._xml_start_tag("c:txPr")
3237 # Write the a:bodyPr element.
3238 self._write_a_body_pr(rotation, is_y_axis)
3240 # Write the a:lstStyle element.
3241 self._write_a_lst_style()
3243 # Write the a:p element.
3244 self._write_a_p_formula(font)
3246 self._xml_end_tag("c:txPr")
3248 def _write_marker(self, marker):
3249 # Write the <c:marker> element.
3250 if marker is None:
3251 marker = self.default_marker
3253 if not marker:
3254 return
3256 if marker["type"] == "automatic":
3257 return
3259 self._xml_start_tag("c:marker")
3261 # Write the c:symbol element.
3262 self._write_symbol(marker["type"])
3264 # Write the c:size element.
3265 if marker.get("size"):
3266 self._write_marker_size(marker["size"])
3268 # Write the c:spPr element.
3269 self._write_sp_pr(marker)
3271 self._xml_end_tag("c:marker")
3273 def _write_marker_size(self, val):
3274 # Write the <c:size> element.
3276 attributes = [("val", val)]
3278 self._xml_empty_tag("c:size", attributes)
3280 def _write_symbol(self, val):
3281 # Write the <c:symbol> element.
3283 attributes = [("val", val)]
3285 self._xml_empty_tag("c:symbol", attributes)
3287 def _write_sp_pr(self, series):
3288 # Write the <c:spPr> element.
3290 if not self._has_fill_formatting(series):
3291 return
3293 self._xml_start_tag("c:spPr")
3295 # Write the fill elements for solid charts such as pie and bar.
3296 if series.get("fill") and series["fill"]["defined"]:
3297 if "none" in series["fill"]:
3298 # Write the a:noFill element.
3299 self._write_a_no_fill()
3300 else:
3301 # Write the a:solidFill element.
3302 self._write_a_solid_fill(series["fill"])
3304 if series.get("pattern"):
3305 # Write the a:gradFill element.
3306 self._write_a_patt_fill(series["pattern"])
3308 if series.get("gradient"):
3309 # Write the a:gradFill element.
3310 self._write_a_grad_fill(series["gradient"])
3312 # Write the a:ln element.
3313 if series.get("line") and series["line"]["defined"]:
3314 self._write_a_ln(series["line"])
3316 self._xml_end_tag("c:spPr")
3318 def _write_a_ln(self, line):
3319 # Write the <a:ln> element.
3320 attributes = []
3322 # Add the line width as an attribute.
3323 width = line.get("width")
3325 if width is not None:
3326 # Round width to nearest 0.25, like Excel.
3327 width = int((width + 0.125) * 4) / 4.0
3329 # Convert to internal units.
3330 width = int(0.5 + (12700 * width))
3332 attributes = [("w", width)]
3334 if line.get("none") or line.get("color") or line.get("dash_type"):
3335 self._xml_start_tag("a:ln", attributes)
3337 # Write the line fill.
3338 if "none" in line:
3339 # Write the a:noFill element.
3340 self._write_a_no_fill()
3341 elif "color" in line:
3342 # Write the a:solidFill element.
3343 self._write_a_solid_fill(line)
3345 # Write the line/dash type.
3346 line_type = line.get("dash_type")
3347 if line_type:
3348 # Write the a:prstDash element.
3349 self._write_a_prst_dash(line_type)
3351 self._xml_end_tag("a:ln")
3352 else:
3353 self._xml_empty_tag("a:ln", attributes)
3355 def _write_a_no_fill(self):
3356 # Write the <a:noFill> element.
3357 self._xml_empty_tag("a:noFill")
3359 def _write_a_solid_fill(self, fill):
3360 # Write the <a:solidFill> element.
3362 self._xml_start_tag("a:solidFill")
3364 if "color" in fill:
3365 color = get_rgb_color(fill["color"])
3366 transparency = fill.get("transparency")
3367 # Write the a:srgbClr element.
3368 self._write_a_srgb_clr(color, transparency)
3370 self._xml_end_tag("a:solidFill")
3372 def _write_a_srgb_clr(self, val, transparency=None):
3373 # Write the <a:srgbClr> element.
3374 attributes = [("val", val)]
3376 if transparency:
3377 self._xml_start_tag("a:srgbClr", attributes)
3379 # Write the a:alpha element.
3380 self._write_a_alpha(transparency)
3382 self._xml_end_tag("a:srgbClr")
3383 else:
3384 self._xml_empty_tag("a:srgbClr", attributes)
3386 def _write_a_alpha(self, val):
3387 # Write the <a:alpha> element.
3389 val = int((100 - int(val)) * 1000)
3391 attributes = [("val", val)]
3393 self._xml_empty_tag("a:alpha", attributes)
3395 def _write_a_prst_dash(self, val):
3396 # Write the <a:prstDash> element.
3398 attributes = [("val", val)]
3400 self._xml_empty_tag("a:prstDash", attributes)
3402 def _write_trendline(self, trendline):
3403 # Write the <c:trendline> element.
3405 if not trendline:
3406 return
3408 self._xml_start_tag("c:trendline")
3410 # Write the c:name element.
3411 self._write_name(trendline.get("name"))
3413 # Write the c:spPr element.
3414 self._write_sp_pr(trendline)
3416 # Write the c:trendlineType element.
3417 self._write_trendline_type(trendline["type"])
3419 # Write the c:order element for polynomial trendlines.
3420 if trendline["type"] == "poly":
3421 self._write_trendline_order(trendline.get("order"))
3423 # Write the c:period element for moving average trendlines.
3424 if trendline["type"] == "movingAvg":
3425 self._write_period(trendline.get("period"))
3427 # Write the c:forward element.
3428 self._write_forward(trendline.get("forward"))
3430 # Write the c:backward element.
3431 self._write_backward(trendline.get("backward"))
3433 if "intercept" in trendline:
3434 # Write the c:intercept element.
3435 self._write_c_intercept(trendline["intercept"])
3437 if trendline.get("display_r_squared"):
3438 # Write the c:dispRSqr element.
3439 self._write_c_disp_rsqr()
3441 if trendline.get("display_equation"):
3442 # Write the c:dispEq element.
3443 self._write_c_disp_eq()
3445 # Write the c:trendlineLbl element.
3446 self._write_c_trendline_lbl(trendline)
3448 self._xml_end_tag("c:trendline")
3450 def _write_trendline_type(self, val):
3451 # Write the <c:trendlineType> element.
3453 attributes = [("val", val)]
3455 self._xml_empty_tag("c:trendlineType", attributes)
3457 def _write_name(self, data):
3458 # Write the <c:name> element.
3460 if data is None:
3461 return
3463 self._xml_data_element("c:name", data)
3465 def _write_trendline_order(self, val):
3466 # Write the <c:order> element.
3467 if val < 2:
3468 val = 2
3470 attributes = [("val", val)]
3472 self._xml_empty_tag("c:order", attributes)
3474 def _write_period(self, val):
3475 # Write the <c:period> element.
3476 if val < 2:
3477 val = 2
3479 attributes = [("val", val)]
3481 self._xml_empty_tag("c:period", attributes)
3483 def _write_forward(self, val):
3484 # Write the <c:forward> element.
3486 if not val:
3487 return
3489 attributes = [("val", val)]
3491 self._xml_empty_tag("c:forward", attributes)
3493 def _write_backward(self, val):
3494 # Write the <c:backward> element.
3496 if not val:
3497 return
3499 attributes = [("val", val)]
3501 self._xml_empty_tag("c:backward", attributes)
3503 def _write_c_intercept(self, val):
3504 # Write the <c:intercept> element.
3505 attributes = [("val", val)]
3507 self._xml_empty_tag("c:intercept", attributes)
3509 def _write_c_disp_eq(self):
3510 # Write the <c:dispEq> element.
3511 attributes = [("val", 1)]
3513 self._xml_empty_tag("c:dispEq", attributes)
3515 def _write_c_disp_rsqr(self):
3516 # Write the <c:dispRSqr> element.
3517 attributes = [("val", 1)]
3519 self._xml_empty_tag("c:dispRSqr", attributes)
3521 def _write_c_trendline_lbl(self, trendline):
3522 # Write the <c:trendlineLbl> element.
3523 self._xml_start_tag("c:trendlineLbl")
3525 # Write the c:layout element.
3526 self._write_layout(None, None)
3528 # Write the c:numFmt element.
3529 self._write_trendline_num_fmt()
3531 # Write the c:spPr element.
3532 self._write_sp_pr(trendline["label"])
3534 # Write the data label font elements.
3535 if trendline["label"]:
3536 font = trendline["label"].get("font")
3537 if font:
3538 self._write_axis_font(font)
3540 self._xml_end_tag("c:trendlineLbl")
3542 def _write_trendline_num_fmt(self):
3543 # Write the <c:numFmt> element.
3544 attributes = [
3545 ("formatCode", "General"),
3546 ("sourceLinked", 0),
3547 ]
3549 self._xml_empty_tag("c:numFmt", attributes)
3551 def _write_hi_low_lines(self):
3552 # Write the <c:hiLowLines> element.
3553 hi_low_lines = self.hi_low_lines
3555 if hi_low_lines is None:
3556 return
3558 if "line" in hi_low_lines and hi_low_lines["line"]["defined"]:
3559 self._xml_start_tag("c:hiLowLines")
3561 # Write the c:spPr element.
3562 self._write_sp_pr(hi_low_lines)
3564 self._xml_end_tag("c:hiLowLines")
3565 else:
3566 self._xml_empty_tag("c:hiLowLines")
3568 def _write_drop_lines(self):
3569 # Write the <c:dropLines> element.
3570 drop_lines = self.drop_lines
3572 if drop_lines is None:
3573 return
3575 if drop_lines["line"]["defined"]:
3576 self._xml_start_tag("c:dropLines")
3578 # Write the c:spPr element.
3579 self._write_sp_pr(drop_lines)
3581 self._xml_end_tag("c:dropLines")
3582 else:
3583 self._xml_empty_tag("c:dropLines")
3585 def _write_overlap(self, val):
3586 # Write the <c:overlap> element.
3588 if val is None:
3589 return
3591 attributes = [("val", val)]
3593 self._xml_empty_tag("c:overlap", attributes)
3595 def _write_num_cache(self, data):
3596 # Write the <c:numCache> element.
3597 if data:
3598 count = len(data)
3599 else:
3600 count = 0
3602 self._xml_start_tag("c:numCache")
3604 # Write the c:formatCode element.
3605 self._write_format_code("General")
3607 # Write the c:ptCount element.
3608 self._write_pt_count(count)
3610 for i in range(count):
3611 token = data[i]
3613 if token is None:
3614 continue
3616 try:
3617 float(token)
3618 except ValueError:
3619 # Write non-numeric data as 0.
3620 token = 0
3622 # Write the c:pt element.
3623 self._write_pt(i, token)
3625 self._xml_end_tag("c:numCache")
3627 def _write_str_cache(self, data):
3628 # Write the <c:strCache> element.
3629 count = len(data)
3631 self._xml_start_tag("c:strCache")
3633 # Write the c:ptCount element.
3634 self._write_pt_count(count)
3636 for i in range(count):
3637 # Write the c:pt element.
3638 self._write_pt(i, data[i])
3640 self._xml_end_tag("c:strCache")
3642 def _write_format_code(self, data):
3643 # Write the <c:formatCode> element.
3645 self._xml_data_element("c:formatCode", data)
3647 def _write_pt_count(self, val):
3648 # Write the <c:ptCount> element.
3650 attributes = [("val", val)]
3652 self._xml_empty_tag("c:ptCount", attributes)
3654 def _write_pt(self, idx, value):
3655 # Write the <c:pt> element.
3657 if value is None:
3658 return
3660 attributes = [("idx", idx)]
3662 self._xml_start_tag("c:pt", attributes)
3664 # Write the c:v element.
3665 self._write_v(value)
3667 self._xml_end_tag("c:pt")
3669 def _write_v(self, data):
3670 # Write the <c:v> element.
3672 self._xml_data_element("c:v", data)
3674 def _write_protection(self):
3675 # Write the <c:protection> element.
3676 if not self.protection:
3677 return
3679 self._xml_empty_tag("c:protection")
3681 def _write_d_pt(self, points):
3682 # Write the <c:dPt> elements.
3683 index = -1
3685 if not points:
3686 return
3688 for point in points:
3689 index += 1
3690 if not point:
3691 continue
3693 self._write_d_pt_point(index, point)
3695 def _write_d_pt_point(self, index, point):
3696 # Write an individual <c:dPt> element.
3698 self._xml_start_tag("c:dPt")
3700 # Write the c:idx element.
3701 self._write_idx(index)
3703 # Write the c:spPr element.
3704 self._write_sp_pr(point)
3706 self._xml_end_tag("c:dPt")
3708 def _write_d_lbls(self, labels):
3709 # Write the <c:dLbls> element.
3711 if not labels:
3712 return
3714 self._xml_start_tag("c:dLbls")
3716 # Write the custom c:dLbl elements.
3717 if labels.get("custom"):
3718 self._write_custom_labels(labels, labels["custom"])
3720 # Write the c:numFmt element.
3721 if labels.get("num_format"):
3722 self._write_data_label_number_format(labels["num_format"])
3724 # Write the c:spPr element for the plotarea formatting.
3725 self._write_sp_pr(labels)
3727 # Write the data label font elements.
3728 if labels.get("font"):
3729 self._write_axis_font(labels["font"])
3731 # Write the c:dLblPos element.
3732 if labels.get("position"):
3733 self._write_d_lbl_pos(labels["position"])
3735 # Write the c:showLegendKey element.
3736 if labels.get("legend_key"):
3737 self._write_show_legend_key()
3739 # Write the c:showVal element.
3740 if labels.get("value"):
3741 self._write_show_val()
3743 # Write the c:showCatName element.
3744 if labels.get("category"):
3745 self._write_show_cat_name()
3747 # Write the c:showSerName element.
3748 if labels.get("series_name"):
3749 self._write_show_ser_name()
3751 # Write the c:showPercent element.
3752 if labels.get("percentage"):
3753 self._write_show_percent()
3755 # Write the c:separator element.
3756 if labels.get("separator"):
3757 self._write_separator(labels["separator"])
3759 # Write the c:showLeaderLines element.
3760 if labels.get("leader_lines"):
3761 self._write_show_leader_lines()
3763 self._xml_end_tag("c:dLbls")
3765 def _write_custom_labels(self, parent, labels):
3766 # Write the <c:showLegendKey> element.
3767 index = 0
3769 for label in labels:
3770 index += 1
3772 if label is None:
3773 continue
3775 self._xml_start_tag("c:dLbl")
3777 # Write the c:idx element.
3778 self._write_idx(index - 1)
3780 delete_label = label.get("delete")
3782 if delete_label:
3783 self._write_delete(1)
3785 elif label.get("formula"):
3786 self._write_custom_label_formula(label)
3788 if parent.get("position"):
3789 self._write_d_lbl_pos(parent["position"])
3791 if parent.get("value"):
3792 self._write_show_val()
3793 if parent.get("category"):
3794 self._write_show_cat_name()
3795 if parent.get("series_name"):
3796 self._write_show_ser_name()
3798 elif label.get("value"):
3799 self._write_custom_label_str(label)
3801 if parent.get("position"):
3802 self._write_d_lbl_pos(parent["position"])
3804 if parent.get("value"):
3805 self._write_show_val()
3806 if parent.get("category"):
3807 self._write_show_cat_name()
3808 if parent.get("series_name"):
3809 self._write_show_ser_name()
3810 else:
3811 self._write_custom_label_format_only(label)
3813 self._xml_end_tag("c:dLbl")
3815 def _write_custom_label_str(self, label):
3816 # Write parts of the <c:dLbl> element for strings.
3817 title = label.get("value")
3818 font = label.get("font")
3819 has_formatting = self._has_fill_formatting(label)
3821 # Write the c:layout element.
3822 self._write_layout(None, None)
3824 self._xml_start_tag("c:tx")
3826 # Write the c:rich element.
3827 self._write_rich(title, font, False, not has_formatting)
3829 self._xml_end_tag("c:tx")
3831 # Write the c:spPr element.
3832 self._write_sp_pr(label)
3834 def _write_custom_label_formula(self, label):
3835 # Write parts of the <c:dLbl> element for formulas.
3836 formula = label.get("formula")
3837 data_id = label.get("data_id")
3838 data = None
3840 if data_id is not None:
3841 data = self.formula_data[data_id]
3843 # Write the c:layout element.
3844 self._write_layout(None, None)
3846 self._xml_start_tag("c:tx")
3848 # Write the c:strRef element.
3849 self._write_str_ref(formula, data, "str")
3851 self._xml_end_tag("c:tx")
3853 # Write the data label formatting, if any.
3854 self._write_custom_label_format_only(label)
3856 def _write_custom_label_format_only(self, label):
3857 # Write parts of the <c:dLbl> labels with changed formatting.
3858 font = label.get("font")
3859 has_formatting = self._has_fill_formatting(label)
3861 if has_formatting:
3862 self._write_sp_pr(label)
3863 self._write_tx_pr(font)
3864 elif font:
3865 self._xml_empty_tag("c:spPr")
3866 self._write_tx_pr(font)
3868 def _write_show_legend_key(self):
3869 # Write the <c:showLegendKey> element.
3870 val = "1"
3872 attributes = [("val", val)]
3874 self._xml_empty_tag("c:showLegendKey", attributes)
3876 def _write_show_val(self):
3877 # Write the <c:showVal> element.
3878 val = 1
3880 attributes = [("val", val)]
3882 self._xml_empty_tag("c:showVal", attributes)
3884 def _write_show_cat_name(self):
3885 # Write the <c:showCatName> element.
3886 val = 1
3888 attributes = [("val", val)]
3890 self._xml_empty_tag("c:showCatName", attributes)
3892 def _write_show_ser_name(self):
3893 # Write the <c:showSerName> element.
3894 val = 1
3896 attributes = [("val", val)]
3898 self._xml_empty_tag("c:showSerName", attributes)
3900 def _write_show_percent(self):
3901 # Write the <c:showPercent> element.
3902 val = 1
3904 attributes = [("val", val)]
3906 self._xml_empty_tag("c:showPercent", attributes)
3908 def _write_separator(self, data):
3909 # Write the <c:separator> element.
3910 self._xml_data_element("c:separator", data)
3912 def _write_show_leader_lines(self):
3913 # Write the <c:showLeaderLines> element.
3914 #
3915 # This is different for Pie/Doughnut charts. Other chart types only
3916 # supported leader lines after Excel 2015 via an extension element.
3917 #
3918 uri = "{CE6537A1-D6FC-4f65-9D91-7224C49458BB}"
3919 xmlns_c_15 = "http://schemas.microsoft.com/office/drawing/2012/chart"
3921 attributes = [
3922 ("uri", uri),
3923 ("xmlns:c15", xmlns_c_15),
3924 ]
3926 self._xml_start_tag("c:extLst")
3927 self._xml_start_tag("c:ext", attributes)
3928 self._xml_empty_tag("c15:showLeaderLines", [("val", 1)])
3929 self._xml_end_tag("c:ext")
3930 self._xml_end_tag("c:extLst")
3932 def _write_d_lbl_pos(self, val):
3933 # Write the <c:dLblPos> element.
3935 attributes = [("val", val)]
3937 self._xml_empty_tag("c:dLblPos", attributes)
3939 def _write_delete(self, val):
3940 # Write the <c:delete> element.
3942 attributes = [("val", val)]
3944 self._xml_empty_tag("c:delete", attributes)
3946 def _write_c_invert_if_negative(self, invert):
3947 # Write the <c:invertIfNegative> element.
3948 val = 1
3950 if not invert:
3951 return
3953 attributes = [("val", val)]
3955 self._xml_empty_tag("c:invertIfNegative", attributes)
3957 def _write_axis_font(self, font):
3958 # Write the axis font elements.
3960 if not font:
3961 return
3963 self._xml_start_tag("c:txPr")
3964 self._write_a_body_pr(font.get("rotation"), None)
3965 self._write_a_lst_style()
3966 self._xml_start_tag("a:p")
3968 self._write_a_p_pr_rich(font)
3970 self._write_a_end_para_rpr()
3971 self._xml_end_tag("a:p")
3972 self._xml_end_tag("c:txPr")
3974 def _write_a_latin(self, attributes):
3975 # Write the <a:latin> element.
3976 self._xml_empty_tag("a:latin", attributes)
3978 def _write_d_table(self):
3979 # Write the <c:dTable> element.
3980 table = self.table
3982 if not table:
3983 return
3985 self._xml_start_tag("c:dTable")
3987 if table["horizontal"]:
3988 # Write the c:showHorzBorder element.
3989 self._write_show_horz_border()
3991 if table["vertical"]:
3992 # Write the c:showVertBorder element.
3993 self._write_show_vert_border()
3995 if table["outline"]:
3996 # Write the c:showOutline element.
3997 self._write_show_outline()
3999 if table["show_keys"]:
4000 # Write the c:showKeys element.
4001 self._write_show_keys()
4003 if table["font"]:
4004 # Write the table font.
4005 self._write_tx_pr(table["font"])
4007 self._xml_end_tag("c:dTable")
4009 def _write_show_horz_border(self):
4010 # Write the <c:showHorzBorder> element.
4011 attributes = [("val", 1)]
4013 self._xml_empty_tag("c:showHorzBorder", attributes)
4015 def _write_show_vert_border(self):
4016 # Write the <c:showVertBorder> element.
4017 attributes = [("val", 1)]
4019 self._xml_empty_tag("c:showVertBorder", attributes)
4021 def _write_show_outline(self):
4022 # Write the <c:showOutline> element.
4023 attributes = [("val", 1)]
4025 self._xml_empty_tag("c:showOutline", attributes)
4027 def _write_show_keys(self):
4028 # Write the <c:showKeys> element.
4029 attributes = [("val", 1)]
4031 self._xml_empty_tag("c:showKeys", attributes)
4033 def _write_error_bars(self, error_bars):
4034 # Write the X and Y error bars.
4036 if not error_bars:
4037 return
4039 if error_bars["x_error_bars"]:
4040 self._write_err_bars("x", error_bars["x_error_bars"])
4042 if error_bars["y_error_bars"]:
4043 self._write_err_bars("y", error_bars["y_error_bars"])
4045 def _write_err_bars(self, direction, error_bars):
4046 # Write the <c:errBars> element.
4048 if not error_bars:
4049 return
4051 self._xml_start_tag("c:errBars")
4053 # Write the c:errDir element.
4054 self._write_err_dir(direction)
4056 # Write the c:errBarType element.
4057 self._write_err_bar_type(error_bars["direction"])
4059 # Write the c:errValType element.
4060 self._write_err_val_type(error_bars["type"])
4062 if not error_bars["endcap"]:
4063 # Write the c:noEndCap element.
4064 self._write_no_end_cap()
4066 if error_bars["type"] == "stdErr":
4067 # Don't need to write a c:errValType tag.
4068 pass
4069 elif error_bars["type"] == "cust":
4070 # Write the custom error tags.
4071 self._write_custom_error(error_bars)
4072 else:
4073 # Write the c:val element.
4074 self._write_error_val(error_bars["value"])
4076 # Write the c:spPr element.
4077 self._write_sp_pr(error_bars)
4079 self._xml_end_tag("c:errBars")
4081 def _write_err_dir(self, val):
4082 # Write the <c:errDir> element.
4084 attributes = [("val", val)]
4086 self._xml_empty_tag("c:errDir", attributes)
4088 def _write_err_bar_type(self, val):
4089 # Write the <c:errBarType> element.
4091 attributes = [("val", val)]
4093 self._xml_empty_tag("c:errBarType", attributes)
4095 def _write_err_val_type(self, val):
4096 # Write the <c:errValType> element.
4098 attributes = [("val", val)]
4100 self._xml_empty_tag("c:errValType", attributes)
4102 def _write_no_end_cap(self):
4103 # Write the <c:noEndCap> element.
4104 attributes = [("val", 1)]
4106 self._xml_empty_tag("c:noEndCap", attributes)
4108 def _write_error_val(self, val):
4109 # Write the <c:val> element for error bars.
4111 attributes = [("val", val)]
4113 self._xml_empty_tag("c:val", attributes)
4115 def _write_custom_error(self, error_bars):
4116 # Write the custom error bars tags.
4118 if error_bars["plus_values"]:
4119 # Write the c:plus element.
4120 self._xml_start_tag("c:plus")
4122 if isinstance(error_bars["plus_values"], list):
4123 self._write_num_lit(error_bars["plus_values"])
4124 else:
4125 self._write_num_ref(
4126 error_bars["plus_values"], error_bars["plus_data"], "num"
4127 )
4128 self._xml_end_tag("c:plus")
4130 if error_bars["minus_values"]:
4131 # Write the c:minus element.
4132 self._xml_start_tag("c:minus")
4134 if isinstance(error_bars["minus_values"], list):
4135 self._write_num_lit(error_bars["minus_values"])
4136 else:
4137 self._write_num_ref(
4138 error_bars["minus_values"], error_bars["minus_data"], "num"
4139 )
4140 self._xml_end_tag("c:minus")
4142 def _write_num_lit(self, data):
4143 # Write the <c:numLit> element for literal number list elements.
4144 count = len(data)
4146 # Write the c:numLit element.
4147 self._xml_start_tag("c:numLit")
4149 # Write the c:formatCode element.
4150 self._write_format_code("General")
4152 # Write the c:ptCount element.
4153 self._write_pt_count(count)
4155 for i in range(count):
4156 token = data[i]
4158 if token is None:
4159 continue
4161 try:
4162 float(token)
4163 except ValueError:
4164 # Write non-numeric data as 0.
4165 token = 0
4167 # Write the c:pt element.
4168 self._write_pt(i, token)
4170 self._xml_end_tag("c:numLit")
4172 def _write_up_down_bars(self):
4173 # Write the <c:upDownBars> element.
4174 up_down_bars = self.up_down_bars
4176 if up_down_bars is None:
4177 return
4179 self._xml_start_tag("c:upDownBars")
4181 # Write the c:gapWidth element.
4182 self._write_gap_width(150)
4184 # Write the c:upBars element.
4185 self._write_up_bars(up_down_bars.get("up"))
4187 # Write the c:downBars element.
4188 self._write_down_bars(up_down_bars.get("down"))
4190 self._xml_end_tag("c:upDownBars")
4192 def _write_gap_width(self, val):
4193 # Write the <c:gapWidth> element.
4195 if val is None:
4196 return
4198 attributes = [("val", val)]
4200 self._xml_empty_tag("c:gapWidth", attributes)
4202 def _write_up_bars(self, bar_format):
4203 # Write the <c:upBars> element.
4205 if bar_format["line"] and bar_format["line"]["defined"]:
4206 self._xml_start_tag("c:upBars")
4208 # Write the c:spPr element.
4209 self._write_sp_pr(bar_format)
4211 self._xml_end_tag("c:upBars")
4212 else:
4213 self._xml_empty_tag("c:upBars")
4215 def _write_down_bars(self, bar_format):
4216 # Write the <c:downBars> element.
4218 if bar_format["line"] and bar_format["line"]["defined"]:
4219 self._xml_start_tag("c:downBars")
4221 # Write the c:spPr element.
4222 self._write_sp_pr(bar_format)
4224 self._xml_end_tag("c:downBars")
4225 else:
4226 self._xml_empty_tag("c:downBars")
4228 def _write_disp_units(self, units, display):
4229 # Write the <c:dispUnits> element.
4231 if not units:
4232 return
4234 attributes = [("val", units)]
4236 self._xml_start_tag("c:dispUnits")
4237 self._xml_empty_tag("c:builtInUnit", attributes)
4239 if display:
4240 self._xml_start_tag("c:dispUnitsLbl")
4241 self._xml_empty_tag("c:layout")
4242 self._xml_end_tag("c:dispUnitsLbl")
4244 self._xml_end_tag("c:dispUnits")
4246 def _write_a_grad_fill(self, gradient):
4247 # Write the <a:gradFill> element.
4249 attributes = [("flip", "none"), ("rotWithShape", "1")]
4251 if gradient["type"] == "linear":
4252 attributes = []
4254 self._xml_start_tag("a:gradFill", attributes)
4256 # Write the a:gsLst element.
4257 self._write_a_gs_lst(gradient)
4259 if gradient["type"] == "linear":
4260 # Write the a:lin element.
4261 self._write_a_lin(gradient["angle"])
4262 else:
4263 # Write the a:path element.
4264 self._write_a_path(gradient["type"])
4266 # Write the a:tileRect element.
4267 self._write_a_tile_rect(gradient["type"])
4269 self._xml_end_tag("a:gradFill")
4271 def _write_a_gs_lst(self, gradient):
4272 # Write the <a:gsLst> element.
4273 positions = gradient["positions"]
4274 colors = gradient["colors"]
4276 self._xml_start_tag("a:gsLst")
4278 for i in range(len(colors)):
4279 pos = int(positions[i] * 1000)
4280 attributes = [("pos", pos)]
4281 self._xml_start_tag("a:gs", attributes)
4283 # Write the a:srgbClr element.
4284 color = get_rgb_color(colors[i])
4285 self._write_a_srgb_clr(color)
4287 self._xml_end_tag("a:gs")
4289 self._xml_end_tag("a:gsLst")
4291 def _write_a_lin(self, angle):
4292 # Write the <a:lin> element.
4294 angle = int(60000 * angle)
4296 attributes = [
4297 ("ang", angle),
4298 ("scaled", "0"),
4299 ]
4301 self._xml_empty_tag("a:lin", attributes)
4303 def _write_a_path(self, gradient_type):
4304 # Write the <a:path> element.
4306 attributes = [("path", gradient_type)]
4308 self._xml_start_tag("a:path", attributes)
4310 # Write the a:fillToRect element.
4311 self._write_a_fill_to_rect(gradient_type)
4313 self._xml_end_tag("a:path")
4315 def _write_a_fill_to_rect(self, gradient_type):
4316 # Write the <a:fillToRect> element.
4318 if gradient_type == "shape":
4319 attributes = [
4320 ("l", "50000"),
4321 ("t", "50000"),
4322 ("r", "50000"),
4323 ("b", "50000"),
4324 ]
4325 else:
4326 attributes = [
4327 ("l", "100000"),
4328 ("t", "100000"),
4329 ]
4331 self._xml_empty_tag("a:fillToRect", attributes)
4333 def _write_a_tile_rect(self, gradient_type):
4334 # Write the <a:tileRect> element.
4336 if gradient_type == "shape":
4337 attributes = []
4338 else:
4339 attributes = [
4340 ("r", "-100000"),
4341 ("b", "-100000"),
4342 ]
4344 self._xml_empty_tag("a:tileRect", attributes)
4346 def _write_a_patt_fill(self, pattern):
4347 # Write the <a:pattFill> element.
4349 attributes = [("prst", pattern["pattern"])]
4351 self._xml_start_tag("a:pattFill", attributes)
4353 # Write the a:fgClr element.
4354 self._write_a_fg_clr(pattern["fg_color"])
4356 # Write the a:bgClr element.
4357 self._write_a_bg_clr(pattern["bg_color"])
4359 self._xml_end_tag("a:pattFill")
4361 def _write_a_fg_clr(self, color):
4362 # Write the <a:fgClr> element.
4364 color = get_rgb_color(color)
4366 self._xml_start_tag("a:fgClr")
4368 # Write the a:srgbClr element.
4369 self._write_a_srgb_clr(color)
4371 self._xml_end_tag("a:fgClr")
4373 def _write_a_bg_clr(self, color):
4374 # Write the <a:bgClr> element.
4376 color = get_rgb_color(color)
4378 self._xml_start_tag("a:bgClr")
4380 # Write the a:srgbClr element.
4381 self._write_a_srgb_clr(color)
4383 self._xml_end_tag("a:bgClr")