############################################################################### # # Chart - A class for writing the Excel XLSX Worksheet file. # # SPDX-License-Identifier: BSD-2-Clause # # Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org # import copy import re from warnings import warn from . import xmlwriter from .shape import Shape from .utility import ( _datetime_to_excel_datetime, _get_rgb_color, _supported_datetime, quote_sheetname, xl_range_formula, xl_rowcol_to_cell, ) class Chart(xmlwriter.XMLwriter): """ A class for writing the Excel XLSX Chart file. """ ########################################################################### # # Public API. # ########################################################################### def __init__(self): """ Constructor. """ super().__init__() self.subtype = None self.sheet_type = 0x0200 self.orientation = 0x0 self.series = [] self.embedded = 0 self.id = -1 self.series_index = 0 self.style_id = 2 self.axis_ids = [] self.axis2_ids = [] self.cat_has_num_fmt = 0 self.requires_category = False self.legend = {} self.cat_axis_position = "b" self.val_axis_position = "l" self.formula_ids = {} self.formula_data = [] self.horiz_cat_axis = 0 self.horiz_val_axis = 1 self.protection = 0 self.chartarea = {} self.plotarea = {} self.x_axis = {} self.y_axis = {} self.y2_axis = {} self.x2_axis = {} self.chart_name = "" self.show_blanks = "gap" self.show_na_as_empty = False self.show_hidden = False self.show_crosses = True self.width = 480 self.height = 288 self.x_scale = 1 self.y_scale = 1 self.x_offset = 0 self.y_offset = 0 self.table = None self.cross_between = "between" self.default_marker = None self.series_gap_1 = None self.series_gap_2 = None self.series_overlap_1 = None self.series_overlap_2 = None self.drop_lines = None self.hi_low_lines = None self.up_down_bars = None self.smooth_allowed = False self.title_font = None self.title_name = None self.title_formula = None self.title_data_id = None self.title_layout = None self.title_overlay = None self.title_none = False self.date_category = False self.date_1904 = False self.remove_timezone = False self.label_positions = {} self.label_position_default = "" self.already_inserted = False self.combined = None self.is_secondary = False self.warn_sheetname = True self._set_default_properties() self.fill = {} def add_series(self, options=None): """ Add a data series to a chart. Args: options: A dictionary of chart series options. Returns: Nothing. """ # Add a series and it's properties to a chart. if options is None: options = {} # Check that the required input has been specified. if "values" not in options: warn("Must specify 'values' in add_series()") return if self.requires_category and "categories" not in options: warn("Must specify 'categories' in add_series() for this chart type") return if len(self.series) == 255: warn( "The maximum number of series that can be added to an " "Excel Chart is 255" ) return # Convert list into a formula string. values = self._list_to_formula(options.get("values")) categories = self._list_to_formula(options.get("categories")) # Switch name and name_formula parameters if required. name, name_formula = self._process_names( options.get("name"), options.get("name_formula") ) # Get an id for the data equivalent to the range formula. cat_id = self._get_data_id(categories, options.get("categories_data")) val_id = self._get_data_id(values, options.get("values_data")) name_id = self._get_data_id(name_formula, options.get("name_data")) # Set the line properties for the series. line = Shape._get_line_properties(options.get("line")) # Allow 'border' as a synonym for 'line' in bar/column style charts. if options.get("border"): line = Shape._get_line_properties(options["border"]) # Set the fill properties for the series. fill = Shape._get_fill_properties(options.get("fill")) # Set the pattern fill properties for the series. pattern = Shape._get_pattern_properties(options.get("pattern")) # Set the gradient fill properties for the series. gradient = Shape._get_gradient_properties(options.get("gradient")) # Pattern fill overrides solid fill. if pattern: self.fill = None # Gradient fill overrides the solid and pattern fill. if gradient: pattern = None fill = None # Set the marker properties for the series. marker = self._get_marker_properties(options.get("marker")) # Set the trendline properties for the series. trendline = self._get_trendline_properties(options.get("trendline")) # Set the line smooth property for the series. smooth = options.get("smooth") # Set the error bars properties for the series. y_error_bars = self._get_error_bars_props(options.get("y_error_bars")) x_error_bars = self._get_error_bars_props(options.get("x_error_bars")) error_bars = {"x_error_bars": x_error_bars, "y_error_bars": y_error_bars} # Set the point properties for the series. points = self._get_points_properties(options.get("points")) # Set the labels properties for the series. labels = self._get_labels_properties(options.get("data_labels")) # Set the "invert if negative" fill property. invert_if_neg = options.get("invert_if_negative", False) inverted_color = options.get("invert_if_negative_color", False) # Set the secondary axis properties. x2_axis = options.get("x2_axis") y2_axis = options.get("y2_axis") # Store secondary status for combined charts. if x2_axis or y2_axis: self.is_secondary = True # Set the gap for Bar/Column charts. if options.get("gap") is not None: if y2_axis: self.series_gap_2 = options["gap"] else: self.series_gap_1 = options["gap"] # Set the overlap for Bar/Column charts. if options.get("overlap"): if y2_axis: self.series_overlap_2 = options["overlap"] else: self.series_overlap_1 = options["overlap"] # Add the user supplied data to the internal structures. series = { "values": values, "categories": categories, "name": name, "name_formula": name_formula, "name_id": name_id, "val_data_id": val_id, "cat_data_id": cat_id, "line": line, "fill": fill, "pattern": pattern, "gradient": gradient, "marker": marker, "trendline": trendline, "labels": labels, "invert_if_neg": invert_if_neg, "inverted_color": inverted_color, "x2_axis": x2_axis, "y2_axis": y2_axis, "points": points, "error_bars": error_bars, "smooth": smooth, } self.series.append(series) def set_x_axis(self, options): """ Set the chart X axis options. Args: options: A dictionary of axis options. Returns: Nothing. """ axis = self._convert_axis_args(self.x_axis, options) self.x_axis = axis def set_y_axis(self, options): """ Set the chart Y axis options. Args: options: A dictionary of axis options. Returns: Nothing. """ axis = self._convert_axis_args(self.y_axis, options) self.y_axis = axis def set_x2_axis(self, options): """ Set the chart secondary X axis options. Args: options: A dictionary of axis options. Returns: Nothing. """ axis = self._convert_axis_args(self.x2_axis, options) self.x2_axis = axis def set_y2_axis(self, options): """ Set the chart secondary Y axis options. Args: options: A dictionary of axis options. Returns: Nothing. """ axis = self._convert_axis_args(self.y2_axis, options) self.y2_axis = axis def set_title(self, options=None): """ Set the chart title options. Args: options: A dictionary of chart title options. Returns: Nothing. """ if options is None: options = {} name, name_formula = self._process_names( options.get("name"), options.get("name_formula") ) data_id = self._get_data_id(name_formula, options.get("data")) self.title_name = name self.title_formula = name_formula self.title_data_id = data_id # Set the font properties if present. self.title_font = self._convert_font_args(options.get("name_font")) # Set the axis name layout. self.title_layout = self._get_layout_properties(options.get("layout"), True) # Set the title overlay option. self.title_overlay = options.get("overlay") # Set the automatic title option. self.title_none = options.get("none") def set_legend(self, options): """ Set the chart legend options. Args: options: A dictionary of chart legend options. Returns: Nothing. """ # Convert the user defined properties to internal properties. self.legend = self._get_legend_properties(options) def set_plotarea(self, options): """ Set the chart plot area options. Args: options: A dictionary of chart plot area options. Returns: Nothing. """ # Convert the user defined properties to internal properties. self.plotarea = self._get_area_properties(options) def set_chartarea(self, options): """ Set the chart area options. Args: options: A dictionary of chart area options. Returns: Nothing. """ # Convert the user defined properties to internal properties. self.chartarea = self._get_area_properties(options) def set_style(self, style_id): """ Set the chart style type. Args: style_id: An int representing the chart style. Returns: Nothing. """ # Set one of the 48 built-in Excel chart styles. The default is 2. if style_id is None: style_id = 2 if style_id < 1 or style_id > 48: style_id = 2 self.style_id = style_id def show_blanks_as(self, option): """ Set the option for displaying blank data in a chart. Args: option: A string representing the display option. Returns: Nothing. """ if not option: return valid_options = { "gap": 1, "zero": 1, "span": 1, } if option not in valid_options: warn(f"Unknown show_blanks_as() option '{option}'") return self.show_blanks = option def show_na_as_empty_cell(self): """ Display ``#N/A`` on charts as blank/empty cells. Args: None. Returns: Nothing. """ self.show_na_as_empty = True def show_hidden_data(self): """ Display data on charts from hidden rows or columns. Args: None. Returns: Nothing. """ self.show_hidden = True def set_size(self, options=None): """ Set size or scale of the chart. Args: options: A dictionary of chart size options. Returns: Nothing. """ if options is None: options = {} # Set dimensions or scale for the chart. self.width = options.get("width", self.width) self.height = options.get("height", self.height) self.x_scale = options.get("x_scale", 1) self.y_scale = options.get("y_scale", 1) self.x_offset = options.get("x_offset", 0) self.y_offset = options.get("y_offset", 0) def set_table(self, options=None): """ Set properties for an axis data table. Args: options: A dictionary of axis table options. Returns: Nothing. """ if options is None: options = {} table = {} table["horizontal"] = options.get("horizontal", 1) table["vertical"] = options.get("vertical", 1) table["outline"] = options.get("outline", 1) table["show_keys"] = options.get("show_keys", 0) table["font"] = self._convert_font_args(options.get("font")) self.table = table def set_up_down_bars(self, options=None): """ Set properties for the chart up-down bars. Args: options: A dictionary of options. Returns: Nothing. """ if options is None: options = {} # Defaults. up_line = None up_fill = None down_line = None down_fill = None # Set properties for 'up' bar. if options.get("up"): if "border" in options["up"]: # Map border to line. up_line = Shape._get_line_properties(options["up"]["border"]) if "line" in options["up"]: up_line = Shape._get_line_properties(options["up"]["line"]) if "fill" in options["up"]: up_fill = Shape._get_fill_properties(options["up"]["fill"]) # Set properties for 'down' bar. if options.get("down"): if "border" in options["down"]: # Map border to line. down_line = Shape._get_line_properties(options["down"]["border"]) if "line" in options["down"]: down_line = Shape._get_line_properties(options["down"]["line"]) if "fill" in options["down"]: down_fill = Shape._get_fill_properties(options["down"]["fill"]) self.up_down_bars = { "up": { "line": up_line, "fill": up_fill, }, "down": { "line": down_line, "fill": down_fill, }, } def set_drop_lines(self, options=None): """ Set properties for the chart drop lines. Args: options: A dictionary of options. Returns: Nothing. """ if options is None: options = {} line = Shape._get_line_properties(options.get("line")) fill = Shape._get_fill_properties(options.get("fill")) # Set the pattern fill properties for the series. pattern = Shape._get_pattern_properties(options.get("pattern")) # Set the gradient fill properties for the series. gradient = Shape._get_gradient_properties(options.get("gradient")) # Pattern fill overrides solid fill. if pattern: self.fill = None # Gradient fill overrides the solid and pattern fill. if gradient: pattern = None fill = None self.drop_lines = { "line": line, "fill": fill, "pattern": pattern, "gradient": gradient, } def set_high_low_lines(self, options=None): """ Set properties for the chart high-low lines. Args: options: A dictionary of options. Returns: Nothing. """ if options is None: options = {} line = Shape._get_line_properties(options.get("line")) fill = Shape._get_fill_properties(options.get("fill")) # Set the pattern fill properties for the series. pattern = Shape._get_pattern_properties(options.get("pattern")) # Set the gradient fill properties for the series. gradient = Shape._get_gradient_properties(options.get("gradient")) # Pattern fill overrides solid fill. if pattern: self.fill = None # Gradient fill overrides the solid and pattern fill. if gradient: pattern = None fill = None self.hi_low_lines = { "line": line, "fill": fill, "pattern": pattern, "gradient": gradient, } def combine(self, chart=None): """ Create a combination chart with a secondary chart. Args: chart: The secondary chart to combine with the primary chart. Returns: Nothing. """ if chart is None: return self.combined = chart ########################################################################### # # Private API. # ########################################################################### def _assemble_xml_file(self): # Assemble and write the XML file. # Write the XML declaration. self._xml_declaration() # Write the c:chartSpace element. self._write_chart_space() # Write the c:lang element. self._write_lang() # Write the c:style element. self._write_style() # Write the c:protection element. self._write_protection() # Write the c:chart element. self._write_chart() # Write the c:spPr element for the chartarea formatting. self._write_sp_pr(self.chartarea) # Write the c:printSettings element. if self.embedded: self._write_print_settings() # Close the worksheet tag. self._xml_end_tag("c:chartSpace") # Close the file. self._xml_close() def _convert_axis_args(self, axis, user_options): # Convert user defined axis values into private hash values. options = axis["defaults"].copy() options.update(user_options) name, name_formula = self._process_names( options.get("name"), options.get("name_formula") ) data_id = self._get_data_id(name_formula, options.get("data")) axis = { "defaults": axis["defaults"], "name": name, "formula": name_formula, "data_id": data_id, "reverse": options.get("reverse"), "min": options.get("min"), "max": options.get("max"), "minor_unit": options.get("minor_unit"), "major_unit": options.get("major_unit"), "minor_unit_type": options.get("minor_unit_type"), "major_unit_type": options.get("major_unit_type"), "display_units": options.get("display_units"), "log_base": options.get("log_base"), "crossing": options.get("crossing"), "position_axis": options.get("position_axis"), "position": options.get("position"), "label_position": options.get("label_position"), "label_align": options.get("label_align"), "num_format": options.get("num_format"), "num_format_linked": options.get("num_format_linked"), "interval_unit": options.get("interval_unit"), "interval_tick": options.get("interval_tick"), "text_axis": False, } axis["visible"] = options.get("visible", True) # Convert the display units. axis["display_units"] = self._get_display_units(axis["display_units"]) axis["display_units_visible"] = options.get("display_units_visible", True) # Map major_gridlines properties. if options.get("major_gridlines") and options["major_gridlines"]["visible"]: axis["major_gridlines"] = self._get_gridline_properties( options["major_gridlines"] ) # Map minor_gridlines properties. if options.get("minor_gridlines") and options["minor_gridlines"]["visible"]: axis["minor_gridlines"] = self._get_gridline_properties( options["minor_gridlines"] ) # Only use the first letter of bottom, top, left or right. if axis.get("position"): axis["position"] = axis["position"].lower()[0] # Set the position for a category axis on or between the tick marks. if axis.get("position_axis"): if axis["position_axis"] == "on_tick": axis["position_axis"] = "midCat" elif axis["position_axis"] == "between": # Doesn't need to be modified. pass else: # Otherwise use the default value. axis["position_axis"] = None # Set the category axis as a date axis. if options.get("date_axis"): self.date_category = True # Set the category axis as a text axis. if options.get("text_axis"): self.date_category = False axis["text_axis"] = True # Convert datetime args if required. if axis.get("min") and _supported_datetime(axis["min"]): axis["min"] = _datetime_to_excel_datetime( axis["min"], self.date_1904, self.remove_timezone ) if axis.get("max") and _supported_datetime(axis["max"]): axis["max"] = _datetime_to_excel_datetime( axis["max"], self.date_1904, self.remove_timezone ) if axis.get("crossing") and _supported_datetime(axis["crossing"]): axis["crossing"] = _datetime_to_excel_datetime( axis["crossing"], self.date_1904, self.remove_timezone ) # Set the font properties if present. axis["num_font"] = self._convert_font_args(options.get("num_font")) axis["name_font"] = self._convert_font_args(options.get("name_font")) # Set the axis name layout. axis["name_layout"] = self._get_layout_properties( options.get("name_layout"), True ) # Set the line properties for the axis. axis["line"] = Shape._get_line_properties(options.get("line")) # Set the fill properties for the axis. axis["fill"] = Shape._get_fill_properties(options.get("fill")) # Set the pattern fill properties for the series. axis["pattern"] = Shape._get_pattern_properties(options.get("pattern")) # Set the gradient fill properties for the series. axis["gradient"] = Shape._get_gradient_properties(options.get("gradient")) # Pattern fill overrides solid fill. if axis.get("pattern"): axis["fill"] = None # Gradient fill overrides the solid and pattern fill. if axis.get("gradient"): axis["pattern"] = None axis["fill"] = None # Set the tick marker types. axis["minor_tick_mark"] = self._get_tick_type(options.get("minor_tick_mark")) axis["major_tick_mark"] = self._get_tick_type(options.get("major_tick_mark")) return axis def _convert_font_args(self, options): # Convert user defined font values into private dict values. if not options: return {} font = { "name": options.get("name"), "color": options.get("color"), "size": options.get("size"), "bold": options.get("bold"), "italic": options.get("italic"), "underline": options.get("underline"), "pitch_family": options.get("pitch_family"), "charset": options.get("charset"), "baseline": options.get("baseline", 0), "rotation": options.get("rotation"), } # Convert font size units. if font["size"]: font["size"] = int(font["size"] * 100) # Convert rotation into 60,000ths of a degree. if font["rotation"]: font["rotation"] = 60000 * int(font["rotation"]) return font def _list_to_formula(self, data): # Convert and list of row col values to a range formula. # If it isn't an array ref it is probably a formula already. if not isinstance(data, list): # Check for unquoted sheetnames. if data and " " in data and "'" not in data and self.warn_sheetname: warn( f"Sheetname in '{data}' contains spaces but isn't quoted. " f"This may cause an error in Excel." ) return data formula = xl_range_formula(*data) return formula def _process_names(self, name, name_formula): # Switch name and name_formula parameters if required. if name is not None: if isinstance(name, list): # Convert a list of values into a name formula. cell = xl_rowcol_to_cell(name[1], name[2], True, True) name_formula = quote_sheetname(name[0]) + "!" + cell name = "" elif re.match(r"^=?[^!]+!\$?[A-Z]+\$?\d+", name): # Name looks like a formula, use it to set name_formula. name_formula = name name = "" return name, name_formula def _get_data_type(self, data): # Find the overall type of the data associated with a series. # Check for no data in the series. if data is None or len(data) == 0: return "none" if isinstance(data[0], list): return "multi_str" # Determine if data is numeric or strings. for token in data: if token is None: continue # Check for strings that would evaluate to float like # '1.1_1' of ' 1'. if isinstance(token, str) and re.search("[_ ]", token): # Assume entire data series is string data. return "str" try: float(token) except ValueError: # Not a number. Assume entire data series is string data. return "str" # The series data was all numeric. return "num" def _get_data_id(self, formula, data): # Assign an id to a each unique series formula or title/axis formula. # Repeated formulas such as for categories get the same id. If the # series or title has user specified data associated with it then # that is also stored. This data is used to populate cached Excel # data when creating a chart. If there is no user defined data then # it will be populated by the parent Workbook._add_chart_data(). # Ignore series without a range formula. if not formula: return None # Strip the leading '=' from the formula. if formula.startswith("="): formula = formula.lstrip("=") # Store the data id in a hash keyed by the formula and store the data # in a separate array with the same id. if formula not in self.formula_ids: # Haven't seen this formula before. formula_id = len(self.formula_data) self.formula_data.append(data) self.formula_ids[formula] = formula_id else: # Formula already seen. Return existing id. formula_id = self.formula_ids[formula] # Store user defined data if it isn't already there. if self.formula_data[formula_id] is None: self.formula_data[formula_id] = data return formula_id def _get_marker_properties(self, marker): # Convert user marker properties to the structure required internally. if not marker: return None # Copy the user defined properties since they will be modified. marker = copy.deepcopy(marker) types = { "automatic": "automatic", "none": "none", "square": "square", "diamond": "diamond", "triangle": "triangle", "x": "x", "star": "star", "dot": "dot", "short_dash": "dot", "dash": "dash", "long_dash": "dash", "circle": "circle", "plus": "plus", "picture": "picture", } # Check for valid types. marker_type = marker.get("type") if marker_type is not None: if marker_type in types: marker["type"] = types[marker_type] else: warn(f"Unknown marker type '{marker_type}") return None # Set the line properties for the marker. line = Shape._get_line_properties(marker.get("line")) # Allow 'border' as a synonym for 'line'. if "border" in marker: line = Shape._get_line_properties(marker["border"]) # Set the fill properties for the marker. fill = Shape._get_fill_properties(marker.get("fill")) # Set the pattern fill properties for the series. pattern = Shape._get_pattern_properties(marker.get("pattern")) # Set the gradient fill properties for the series. gradient = Shape._get_gradient_properties(marker.get("gradient")) # Pattern fill overrides solid fill. if pattern: self.fill = None # Gradient fill overrides the solid and pattern fill. if gradient: pattern = None fill = None marker["line"] = line marker["fill"] = fill marker["pattern"] = pattern marker["gradient"] = gradient return marker def _get_trendline_properties(self, trendline): # Convert user trendline properties to structure required internally. if not trendline: return None # Copy the user defined properties since they will be modified. trendline = copy.deepcopy(trendline) types = { "exponential": "exp", "linear": "linear", "log": "log", "moving_average": "movingAvg", "polynomial": "poly", "power": "power", } # Check the trendline type. trend_type = trendline.get("type") if trend_type in types: trendline["type"] = types[trend_type] else: warn(f"Unknown trendline type '{trend_type}'") return None # Set the line properties for the trendline. line = Shape._get_line_properties(trendline.get("line")) # Allow 'border' as a synonym for 'line'. if "border" in trendline: line = Shape._get_line_properties(trendline["border"]) # Set the fill properties for the trendline. fill = Shape._get_fill_properties(trendline.get("fill")) # Set the pattern fill properties for the trendline. pattern = Shape._get_pattern_properties(trendline.get("pattern")) # Set the gradient fill properties for the trendline. gradient = Shape._get_gradient_properties(trendline.get("gradient")) # Set the format properties for the trendline label. label = self._get_trendline_label_properties(trendline.get("label")) # Pattern fill overrides solid fill. if pattern: self.fill = None # Gradient fill overrides the solid and pattern fill. if gradient: pattern = None fill = None trendline["line"] = line trendline["fill"] = fill trendline["pattern"] = pattern trendline["gradient"] = gradient trendline["label"] = label return trendline def _get_trendline_label_properties(self, label): # Convert user trendline properties to structure required internally. if not label: return {} # Copy the user defined properties since they will be modified. label = copy.deepcopy(label) # Set the font properties if present. font = self._convert_font_args(label.get("font")) # Set the line properties for the label. line = Shape._get_line_properties(label.get("line")) # Allow 'border' as a synonym for 'line'. if "border" in label: line = Shape._get_line_properties(label["border"]) # Set the fill properties for the label. fill = Shape._get_fill_properties(label.get("fill")) # Set the pattern fill properties for the label. pattern = Shape._get_pattern_properties(label.get("pattern")) # Set the gradient fill properties for the label. gradient = Shape._get_gradient_properties(label.get("gradient")) # Pattern fill overrides solid fill. if pattern: self.fill = None # Gradient fill overrides the solid and pattern fill. if gradient: pattern = None fill = None label["font"] = font label["line"] = line label["fill"] = fill label["pattern"] = pattern label["gradient"] = gradient return label def _get_error_bars_props(self, options): # Convert user error bars properties to structure required internally. if not options: return {} # Default values. error_bars = {"type": "fixedVal", "value": 1, "endcap": 1, "direction": "both"} types = { "fixed": "fixedVal", "percentage": "percentage", "standard_deviation": "stdDev", "standard_error": "stdErr", "custom": "cust", } # Check the error bars type. error_type = options["type"] if error_type in types: error_bars["type"] = types[error_type] else: warn(f"Unknown error bars type '{error_type}") return {} # Set the value for error types that require it. if "value" in options: error_bars["value"] = options["value"] # Set the end-cap style. if "end_style" in options: error_bars["endcap"] = options["end_style"] # Set the error bar direction. if "direction" in options: if options["direction"] == "minus": error_bars["direction"] = "minus" elif options["direction"] == "plus": error_bars["direction"] = "plus" else: # Default to 'both'. pass # Set any custom values. error_bars["plus_values"] = options.get("plus_values") error_bars["minus_values"] = options.get("minus_values") error_bars["plus_data"] = options.get("plus_data") error_bars["minus_data"] = options.get("minus_data") # Set the line properties for the error bars. error_bars["line"] = Shape._get_line_properties(options.get("line")) return error_bars def _get_gridline_properties(self, options): # Convert user gridline properties to structure required internally. # Set the visible property for the gridline. gridline = {"visible": options.get("visible")} # Set the line properties for the gridline. gridline["line"] = Shape._get_line_properties(options.get("line")) return gridline def _get_labels_properties(self, labels): # Convert user labels properties to the structure required internally. if not labels: return None # Copy the user defined properties since they will be modified. labels = copy.deepcopy(labels) # Map user defined label positions to Excel positions. position = labels.get("position") if position: if position in self.label_positions: if position == self.label_position_default: labels["position"] = None else: labels["position"] = self.label_positions[position] else: warn(f"Unsupported label position '{position}' for this chart type") return None # Map the user defined label separator to the Excel separator. separator = labels.get("separator") separators = { ",": ", ", ";": "; ", ".": ". ", "\n": "\n", " ": " ", } if separator: if separator in separators: labels["separator"] = separators[separator] else: warn("Unsupported label separator") return None # Set the font properties if present. labels["font"] = self._convert_font_args(labels.get("font")) # Set the line properties for the labels. line = Shape._get_line_properties(labels.get("line")) # Allow 'border' as a synonym for 'line'. if "border" in labels: line = Shape._get_line_properties(labels["border"]) # Set the fill properties for the labels. fill = Shape._get_fill_properties(labels.get("fill")) # Set the pattern fill properties for the labels. pattern = Shape._get_pattern_properties(labels.get("pattern")) # Set the gradient fill properties for the labels. gradient = Shape._get_gradient_properties(labels.get("gradient")) # Pattern fill overrides solid fill. if pattern: self.fill = None # Gradient fill overrides the solid and pattern fill. if gradient: pattern = None fill = None labels["line"] = line labels["fill"] = fill labels["pattern"] = pattern labels["gradient"] = gradient if labels.get("custom"): for label in labels["custom"]: if label is None: continue value = label.get("value") if value and re.match(r"^=?[^!]+!\$?[A-Z]+\$?\d+", str(value)): label["formula"] = value formula = label.get("formula") if formula and formula.startswith("="): label["formula"] = formula.lstrip("=") data_id = self._get_data_id(formula, label.get("data")) label["data_id"] = data_id label["font"] = self._convert_font_args(label.get("font")) # Set the line properties for the label. line = Shape._get_line_properties(label.get("line")) # Allow 'border' as a synonym for 'line'. if "border" in label: line = Shape._get_line_properties(label["border"]) # Set the fill properties for the label. fill = Shape._get_fill_properties(label.get("fill")) # Set the pattern fill properties for the label. pattern = Shape._get_pattern_properties(label.get("pattern")) # Set the gradient fill properties for the label. gradient = Shape._get_gradient_properties(label.get("gradient")) # Pattern fill overrides solid fill. if pattern: self.fill = None # Gradient fill overrides the solid and pattern fill. if gradient: pattern = None fill = None label["line"] = line label["fill"] = fill label["pattern"] = pattern label["gradient"] = gradient return labels def _get_area_properties(self, options): # Convert user area properties to the structure required internally. area = {} # Set the line properties for the chartarea. line = Shape._get_line_properties(options.get("line")) # Allow 'border' as a synonym for 'line'. if options.get("border"): line = Shape._get_line_properties(options["border"]) # Set the fill properties for the chartarea. fill = Shape._get_fill_properties(options.get("fill")) # Set the pattern fill properties for the series. pattern = Shape._get_pattern_properties(options.get("pattern")) # Set the gradient fill properties for the series. gradient = Shape._get_gradient_properties(options.get("gradient")) # Pattern fill overrides solid fill. if pattern: self.fill = None # Gradient fill overrides the solid and pattern fill. if gradient: pattern = None fill = None # Set the plotarea layout. layout = self._get_layout_properties(options.get("layout"), False) area["line"] = line area["fill"] = fill area["pattern"] = pattern area["layout"] = layout area["gradient"] = gradient return area def _get_legend_properties(self, options=None): # Convert user legend properties to the structure required internally. legend = {} if options is None: options = {} legend["position"] = options.get("position", "right") legend["delete_series"] = options.get("delete_series") legend["font"] = self._convert_font_args(options.get("font")) legend["layout"] = self._get_layout_properties(options.get("layout"), False) # Turn off the legend. if options.get("none"): legend["position"] = "none" # Set the line properties for the legend. line = Shape._get_line_properties(options.get("line")) # Allow 'border' as a synonym for 'line'. if options.get("border"): line = Shape._get_line_properties(options["border"]) # Set the fill properties for the legend. fill = Shape._get_fill_properties(options.get("fill")) # Set the pattern fill properties for the series. pattern = Shape._get_pattern_properties(options.get("pattern")) # Set the gradient fill properties for the series. gradient = Shape._get_gradient_properties(options.get("gradient")) # Pattern fill overrides solid fill. if pattern: self.fill = None # Gradient fill overrides the solid and pattern fill. if gradient: pattern = None fill = None # Set the legend layout. layout = self._get_layout_properties(options.get("layout"), False) legend["line"] = line legend["fill"] = fill legend["pattern"] = pattern legend["layout"] = layout legend["gradient"] = gradient return legend def _get_layout_properties(self, args, is_text): # Convert user defined layout properties to format used internally. layout = {} if not args: return {} if is_text: properties = ("x", "y") else: properties = ("x", "y", "width", "height") # Check for valid properties. for key in args.keys(): if key not in properties: warn(f"Property '{key}' not supported in layout options") return {} # Set the layout properties. for prop in properties: if prop not in args.keys(): warn(f"Property '{prop}' must be specified in layout options") return {} value = args[prop] try: float(value) except ValueError: warn(f"Property '{prop}' value '{value}' must be numeric in layout") return {} if value < 0 or value > 1: warn( f"Property '{prop}' value '{value}' must be in range " f"0 < x <= 1 in layout options" ) return {} # Convert to the format used by Excel for easier testing layout[prop] = f"{value:.17g}" return layout def _get_points_properties(self, user_points): # Convert user points properties to structure required internally. points = [] if not user_points: return [] for user_point in user_points: point = {} if user_point is not None: # Set the line properties for the point. line = Shape._get_line_properties(user_point.get("line")) # Allow 'border' as a synonym for 'line'. if "border" in user_point: line = Shape._get_line_properties(user_point["border"]) # Set the fill properties for the chartarea. fill = Shape._get_fill_properties(user_point.get("fill")) # Set the pattern fill properties for the series. pattern = Shape._get_pattern_properties(user_point.get("pattern")) # Set the gradient fill properties for the series. gradient = Shape._get_gradient_properties(user_point.get("gradient")) # Pattern fill overrides solid fill. if pattern: self.fill = None # Gradient fill overrides the solid and pattern fill. if gradient: pattern = None fill = None point["line"] = line point["fill"] = fill point["pattern"] = pattern point["gradient"] = gradient points.append(point) return points def _has_fill_formatting(self, element): # Check if a chart element has line, fill or gradient formatting. has_fill = False has_line = False has_pattern = element.get("pattern") has_gradient = element.get("gradient") if element.get("fill") and element["fill"]["defined"]: has_fill = True if element.get("line") and element["line"]["defined"]: has_line = True return has_fill or has_line or has_pattern or has_gradient def _get_display_units(self, display_units): # Convert user defined display units to internal units. if not display_units: return None types = { "hundreds": "hundreds", "thousands": "thousands", "ten_thousands": "tenThousands", "hundred_thousands": "hundredThousands", "millions": "millions", "ten_millions": "tenMillions", "hundred_millions": "hundredMillions", "billions": "billions", "trillions": "trillions", } if display_units in types: display_units = types[display_units] else: warn(f"Unknown display_units type '{display_units}'") return None return display_units def _get_tick_type(self, tick_type): # Convert user defined display units to internal units. if not tick_type: return None types = { "outside": "out", "inside": "in", "none": "none", "cross": "cross", } if tick_type in types: tick_type = types[tick_type] else: warn(f"Unknown tick_type '{tick_type}'") return None return tick_type def _get_primary_axes_series(self): # Returns series which use the primary axes. primary_axes_series = [] for series in self.series: if not series["y2_axis"]: primary_axes_series.append(series) return primary_axes_series def _get_secondary_axes_series(self): # Returns series which use the secondary axes. secondary_axes_series = [] for series in self.series: if series["y2_axis"]: secondary_axes_series.append(series) return secondary_axes_series def _add_axis_ids(self, args): # Add unique ids for primary or secondary axes chart_id = 5001 + int(self.id) axis_count = 1 + len(self.axis2_ids) + len(self.axis_ids) id1 = f"{chart_id:04d}{axis_count:04d}" id2 = f"{chart_id:04d}{axis_count + 1:04d}" if args["primary_axes"]: self.axis_ids.append(id1) self.axis_ids.append(id2) if not args["primary_axes"]: self.axis2_ids.append(id1) self.axis2_ids.append(id2) def _set_default_properties(self): # Setup the default properties for a chart. self.x_axis["defaults"] = { "num_format": "General", "major_gridlines": {"visible": 0}, } self.y_axis["defaults"] = { "num_format": "General", "major_gridlines": {"visible": 1}, } self.x2_axis["defaults"] = { "num_format": "General", "label_position": "none", "crossing": "max", "visible": 0, } self.y2_axis["defaults"] = { "num_format": "General", "major_gridlines": {"visible": 0}, "position": "right", "visible": 1, } self.set_x_axis({}) self.set_y_axis({}) self.set_x2_axis({}) self.set_y2_axis({}) ########################################################################### # # XML methods. # ########################################################################### def _write_chart_space(self): # Write the element. schema = "http://schemas.openxmlformats.org/" xmlns_c = schema + "drawingml/2006/chart" xmlns_a = schema + "drawingml/2006/main" xmlns_r = schema + "officeDocument/2006/relationships" attributes = [ ("xmlns:c", xmlns_c), ("xmlns:a", xmlns_a), ("xmlns:r", xmlns_r), ] self._xml_start_tag("c:chartSpace", attributes) def _write_lang(self): # Write the element. val = "en-US" attributes = [("val", val)] self._xml_empty_tag("c:lang", attributes) def _write_style(self): # Write the element. style_id = self.style_id # Don't write an element for the default style, 2. if style_id == 2: return attributes = [("val", style_id)] self._xml_empty_tag("c:style", attributes) def _write_chart(self): # Write the element. self._xml_start_tag("c:chart") if self.title_none: # Turn off the title. self._write_c_auto_title_deleted() else: # Write the chart title elements. if self.title_formula is not None: self._write_title_formula( self.title_formula, self.title_data_id, None, self.title_font, self.title_layout, self.title_overlay, ) elif self.title_name is not None: self._write_title_rich( self.title_name, None, self.title_font, self.title_layout, self.title_overlay, ) # Write the c:plotArea element. self._write_plot_area() # Write the c:legend element. self._write_legend() # Write the c:plotVisOnly element. self._write_plot_vis_only() # Write the c:dispBlanksAs element. self._write_disp_blanks_as() # Write the c:extLst element. if self.show_na_as_empty: self._write_c_ext_lst_display_na() self._xml_end_tag("c:chart") def _write_disp_blanks_as(self): # Write the element. val = self.show_blanks # Ignore the default value. if val == "gap": return attributes = [("val", val)] self._xml_empty_tag("c:dispBlanksAs", attributes) def _write_plot_area(self): # Write the element. self._xml_start_tag("c:plotArea") # Write the c:layout element. self._write_layout(self.plotarea.get("layout"), "plot") # Write subclass chart type elements for primary and secondary axes. self._write_chart_type({"primary_axes": True}) self._write_chart_type({"primary_axes": False}) # Configure a combined chart if present. second_chart = self.combined if second_chart: # Secondary axis has unique id otherwise use same as primary. if second_chart.is_secondary: second_chart.id = 1000 + self.id else: second_chart.id = self.id # Share the same filehandle for writing. second_chart.fh = self.fh # Share series index with primary chart. second_chart.series_index = self.series_index # Write the subclass chart type elements for combined chart. second_chart._write_chart_type({"primary_axes": True}) second_chart._write_chart_type({"primary_axes": False}) # Write the category and value elements for the primary axes. args = {"x_axis": self.x_axis, "y_axis": self.y_axis, "axis_ids": self.axis_ids} if self.date_category: self._write_date_axis(args) else: self._write_cat_axis(args) self._write_val_axis(args) # Write the category and value elements for the secondary axes. args = { "x_axis": self.x2_axis, "y_axis": self.y2_axis, "axis_ids": self.axis2_ids, } self._write_val_axis(args) # Write the secondary axis for the secondary chart. if second_chart and second_chart.is_secondary: args = { "x_axis": second_chart.x2_axis, "y_axis": second_chart.y2_axis, "axis_ids": second_chart.axis2_ids, } second_chart._write_val_axis(args) if self.date_category: self._write_date_axis(args) else: self._write_cat_axis(args) # Write the c:dTable element. self._write_d_table() # Write the c:spPr element for the plotarea formatting. self._write_sp_pr(self.plotarea) self._xml_end_tag("c:plotArea") def _write_layout(self, layout, layout_type): # Write the element. if not layout: # Automatic layout. self._xml_empty_tag("c:layout") else: # User defined manual layout. self._xml_start_tag("c:layout") self._write_manual_layout(layout, layout_type) self._xml_end_tag("c:layout") def _write_manual_layout(self, layout, layout_type): # Write the element. self._xml_start_tag("c:manualLayout") # Plotarea has a layoutTarget element. if layout_type == "plot": self._xml_empty_tag("c:layoutTarget", [("val", "inner")]) # Set the x, y positions. self._xml_empty_tag("c:xMode", [("val", "edge")]) self._xml_empty_tag("c:yMode", [("val", "edge")]) self._xml_empty_tag("c:x", [("val", layout["x"])]) self._xml_empty_tag("c:y", [("val", layout["y"])]) # For plotarea and legend set the width and height. if layout_type != "text": self._xml_empty_tag("c:w", [("val", layout["width"])]) self._xml_empty_tag("c:h", [("val", layout["height"])]) self._xml_end_tag("c:manualLayout") def _write_chart_type(self, args): # pylint: disable=unused-argument # Write the chart type element. This method should be overridden # by the subclasses. return def _write_grouping(self, val): # Write the element. attributes = [("val", val)] self._xml_empty_tag("c:grouping", attributes) def _write_series(self, series): # Write the series elements. self._write_ser(series) def _write_ser(self, series): # Write the element. index = self.series_index self.series_index += 1 self._xml_start_tag("c:ser") # Write the c:idx element. self._write_idx(index) # Write the c:order element. self._write_order(index) # Write the series name. self._write_series_name(series) # Write the c:spPr element. self._write_sp_pr(series) # Write the c:marker element. self._write_marker(series["marker"]) # Write the c:invertIfNegative element. self._write_c_invert_if_negative(series["invert_if_neg"]) # Write the c:dPt element. self._write_d_pt(series["points"]) # Write the c:dLbls element. self._write_d_lbls(series["labels"]) # Write the c:trendline element. self._write_trendline(series["trendline"]) # Write the c:errBars element. self._write_error_bars(series["error_bars"]) # Write the c:cat element. self._write_cat(series) # Write the c:val element. self._write_val(series) # Write the c:smooth element. if self.smooth_allowed: self._write_c_smooth(series["smooth"]) # Write the c:extLst element. if series.get("inverted_color"): self._write_c_ext_lst_inverted_color(series["inverted_color"]) self._xml_end_tag("c:ser") def _write_c_ext_lst_inverted_color(self, color): # Write the element for the inverted fill color. uri = "{6F2FDCE9-48DA-4B69-8628-5D25D57E5C99}" xmlns_c_14 = "http://schemas.microsoft.com/office/drawing/2007/8/2/chart" attributes1 = [ ("uri", uri), ("xmlns:c14", xmlns_c_14), ] attributes2 = [("xmlns:c14", xmlns_c_14)] self._xml_start_tag("c:extLst") self._xml_start_tag("c:ext", attributes1) self._xml_start_tag("c14:invertSolidFillFmt") self._xml_start_tag("c14:spPr", attributes2) self._write_a_solid_fill({"color": color}) self._xml_end_tag("c14:spPr") self._xml_end_tag("c14:invertSolidFillFmt") self._xml_end_tag("c:ext") self._xml_end_tag("c:extLst") def _write_c_ext_lst_display_na(self): # Write the element for the display NA as empty cell option. uri = "{56B9EC1D-385E-4148-901F-78D8002777C0}" xmlns_c_16 = "http://schemas.microsoft.com/office/drawing/2017/03/chart" attributes1 = [ ("uri", uri), ("xmlns:c16r3", xmlns_c_16), ] attributes2 = [("val", 1)] self._xml_start_tag("c:extLst") self._xml_start_tag("c:ext", attributes1) self._xml_start_tag("c16r3:dataDisplayOptions16") self._xml_empty_tag("c16r3:dispNaAsBlank", attributes2) self._xml_end_tag("c16r3:dataDisplayOptions16") self._xml_end_tag("c:ext") self._xml_end_tag("c:extLst") def _write_idx(self, val): # Write the element. attributes = [("val", val)] self._xml_empty_tag("c:idx", attributes) def _write_order(self, val): # Write the element. attributes = [("val", val)] self._xml_empty_tag("c:order", attributes) def _write_series_name(self, series): # Write the series name. if series["name_formula"] is not None: self._write_tx_formula(series["name_formula"], series["name_id"]) elif series["name"] is not None: self._write_tx_value(series["name"]) def _write_c_smooth(self, smooth): # Write the element. if smooth: self._xml_empty_tag("c:smooth", [("val", "1")]) def _write_cat(self, series): # Write the element. formula = series["categories"] data_id = series["cat_data_id"] data = None if data_id is not None: data = self.formula_data[data_id] # Ignore elements for charts without category values. if not formula: return self._xml_start_tag("c:cat") # Check the type of cached data. cat_type = self._get_data_type(data) if cat_type == "str": self.cat_has_num_fmt = 0 # Write the c:numRef element. self._write_str_ref(formula, data, cat_type) elif cat_type == "multi_str": self.cat_has_num_fmt = 0 # Write the c:numRef element. self._write_multi_lvl_str_ref(formula, data) else: self.cat_has_num_fmt = 1 # Write the c:numRef element. self._write_num_ref(formula, data, cat_type) self._xml_end_tag("c:cat") def _write_val(self, series): # Write the element. formula = series["values"] data_id = series["val_data_id"] data = self.formula_data[data_id] self._xml_start_tag("c:val") # Unlike Cat axes data should only be numeric. # Write the c:numRef element. self._write_num_ref(formula, data, "num") self._xml_end_tag("c:val") def _write_num_ref(self, formula, data, ref_type): # Write the element. self._xml_start_tag("c:numRef") # Write the c:f element. self._write_series_formula(formula) if ref_type == "num": # Write the c:numCache element. self._write_num_cache(data) elif ref_type == "str": # Write the c:strCache element. self._write_str_cache(data) self._xml_end_tag("c:numRef") def _write_str_ref(self, formula, data, ref_type): # Write the element. self._xml_start_tag("c:strRef") # Write the c:f element. self._write_series_formula(formula) if ref_type == "num": # Write the c:numCache element. self._write_num_cache(data) elif ref_type == "str": # Write the c:strCache element. self._write_str_cache(data) self._xml_end_tag("c:strRef") def _write_multi_lvl_str_ref(self, formula, data): # Write the element. if not data: return self._xml_start_tag("c:multiLvlStrRef") # Write the c:f element. self._write_series_formula(formula) self._xml_start_tag("c:multiLvlStrCache") # Write the c:ptCount element. count = len(data[-1]) self._write_pt_count(count) for cat_data in reversed(data): self._xml_start_tag("c:lvl") for i, point in enumerate(cat_data): # Write the c:pt element. self._write_pt(i, point) self._xml_end_tag("c:lvl") self._xml_end_tag("c:multiLvlStrCache") self._xml_end_tag("c:multiLvlStrRef") def _write_series_formula(self, formula): # Write the element. # Strip the leading '=' from the formula. if formula.startswith("="): formula = formula.lstrip("=") self._xml_data_element("c:f", formula) def _write_axis_ids(self, args): # Write the elements for the primary or secondary axes. # Generate the axis ids. self._add_axis_ids(args) if args["primary_axes"]: # Write the axis ids for the primary axes. self._write_axis_id(self.axis_ids[0]) self._write_axis_id(self.axis_ids[1]) else: # Write the axis ids for the secondary axes. self._write_axis_id(self.axis2_ids[0]) self._write_axis_id(self.axis2_ids[1]) def _write_axis_id(self, val): # Write the element. attributes = [("val", val)] self._xml_empty_tag("c:axId", attributes) def _write_cat_axis(self, args): # Write the element. Usually the X axis. x_axis = args["x_axis"] y_axis = args["y_axis"] axis_ids = args["axis_ids"] # If there are no axis_ids then we don't need to write this element. if axis_ids is None or not axis_ids: return position = self.cat_axis_position is_y_axis = self.horiz_cat_axis # Overwrite the default axis position with a user supplied value. if x_axis.get("position"): position = x_axis["position"] self._xml_start_tag("c:catAx") self._write_axis_id(axis_ids[0]) # Write the c:scaling element. self._write_scaling(x_axis.get("reverse"), None, None, None) if not x_axis.get("visible"): self._write_delete(1) # Write the c:axPos element. self._write_axis_pos(position, y_axis.get("reverse")) # Write the c:majorGridlines element. self._write_major_gridlines(x_axis.get("major_gridlines")) # Write the c:minorGridlines element. self._write_minor_gridlines(x_axis.get("minor_gridlines")) # Write the axis title elements. if x_axis["formula"] is not None: self._write_title_formula( x_axis["formula"], x_axis["data_id"], is_y_axis, x_axis["name_font"], x_axis["name_layout"], ) elif x_axis["name"] is not None: self._write_title_rich( x_axis["name"], is_y_axis, x_axis["name_font"], x_axis["name_layout"] ) # Write the c:numFmt element. self._write_cat_number_format(x_axis) # Write the c:majorTickMark element. self._write_major_tick_mark(x_axis.get("major_tick_mark")) # Write the c:minorTickMark element. self._write_minor_tick_mark(x_axis.get("minor_tick_mark")) # Write the c:tickLblPos element. self._write_tick_label_pos(x_axis.get("label_position")) # Write the c:spPr element for the axis line. self._write_sp_pr(x_axis) # Write the axis font elements. self._write_axis_font(x_axis.get("num_font")) # Write the c:crossAx element. self._write_cross_axis(axis_ids[1]) if self.show_crosses or x_axis.get("visible"): # Note, the category crossing comes from the value axis. if ( y_axis.get("crossing") is None or y_axis.get("crossing") == "max" or y_axis["crossing"] == "min" ): # Write the c:crosses element. self._write_crosses(y_axis.get("crossing")) else: # Write the c:crossesAt element. self._write_c_crosses_at(y_axis.get("crossing")) # Write the c:auto element. if not x_axis.get("text_axis"): self._write_auto(1) # Write the c:labelAlign element. self._write_label_align(x_axis.get("label_align")) # Write the c:labelOffset element. self._write_label_offset(100) # Write the c:tickLblSkip element. self._write_c_tick_lbl_skip(x_axis.get("interval_unit")) # Write the c:tickMarkSkip element. self._write_c_tick_mark_skip(x_axis.get("interval_tick")) self._xml_end_tag("c:catAx") def _write_val_axis(self, args): # Write the element. Usually the Y axis. x_axis = args["x_axis"] y_axis = args["y_axis"] axis_ids = args["axis_ids"] position = args.get("position", self.val_axis_position) is_y_axis = self.horiz_val_axis # If there are no axis_ids then we don't need to write this element. if axis_ids is None or not axis_ids: return # Overwrite the default axis position with a user supplied value. position = y_axis.get("position") or position self._xml_start_tag("c:valAx") self._write_axis_id(axis_ids[1]) # Write the c:scaling element. self._write_scaling( y_axis.get("reverse"), y_axis.get("min"), y_axis.get("max"), y_axis.get("log_base"), ) if not y_axis.get("visible"): self._write_delete(1) # Write the c:axPos element. self._write_axis_pos(position, x_axis.get("reverse")) # Write the c:majorGridlines element. self._write_major_gridlines(y_axis.get("major_gridlines")) # Write the c:minorGridlines element. self._write_minor_gridlines(y_axis.get("minor_gridlines")) # Write the axis title elements. if y_axis["formula"] is not None: self._write_title_formula( y_axis["formula"], y_axis["data_id"], is_y_axis, y_axis["name_font"], y_axis["name_layout"], ) elif y_axis["name"] is not None: self._write_title_rich( y_axis["name"], is_y_axis, y_axis.get("name_font"), y_axis.get("name_layout"), ) # Write the c:numberFormat element. self._write_number_format(y_axis) # Write the c:majorTickMark element. self._write_major_tick_mark(y_axis.get("major_tick_mark")) # Write the c:minorTickMark element. self._write_minor_tick_mark(y_axis.get("minor_tick_mark")) # Write the c:tickLblPos element. self._write_tick_label_pos(y_axis.get("label_position")) # Write the c:spPr element for the axis line. self._write_sp_pr(y_axis) # Write the axis font elements. self._write_axis_font(y_axis.get("num_font")) # Write the c:crossAx element. self._write_cross_axis(axis_ids[0]) # Note, the category crossing comes from the value axis. if ( x_axis.get("crossing") is None or x_axis["crossing"] == "max" or x_axis["crossing"] == "min" ): # Write the c:crosses element. self._write_crosses(x_axis.get("crossing")) else: # Write the c:crossesAt element. self._write_c_crosses_at(x_axis.get("crossing")) # Write the c:crossBetween element. self._write_cross_between(x_axis.get("position_axis")) # Write the c:majorUnit element. self._write_c_major_unit(y_axis.get("major_unit")) # Write the c:minorUnit element. self._write_c_minor_unit(y_axis.get("minor_unit")) # Write the c:dispUnits element. self._write_disp_units( y_axis.get("display_units"), y_axis.get("display_units_visible") ) self._xml_end_tag("c:valAx") def _write_cat_val_axis(self, args): # Write the element. This is for the second valAx # in scatter plots. Usually the X axis. x_axis = args["x_axis"] y_axis = args["y_axis"] axis_ids = args["axis_ids"] position = args["position"] or self.val_axis_position is_y_axis = self.horiz_val_axis # If there are no axis_ids then we don't need to write this element. if axis_ids is None or not axis_ids: return # Overwrite the default axis position with a user supplied value. position = x_axis.get("position") or position self._xml_start_tag("c:valAx") self._write_axis_id(axis_ids[0]) # Write the c:scaling element. self._write_scaling( x_axis.get("reverse"), x_axis.get("min"), x_axis.get("max"), x_axis.get("log_base"), ) if not x_axis.get("visible"): self._write_delete(1) # Write the c:axPos element. self._write_axis_pos(position, y_axis.get("reverse")) # Write the c:majorGridlines element. self._write_major_gridlines(x_axis.get("major_gridlines")) # Write the c:minorGridlines element. self._write_minor_gridlines(x_axis.get("minor_gridlines")) # Write the axis title elements. if x_axis["formula"] is not None: self._write_title_formula( x_axis["formula"], x_axis["data_id"], is_y_axis, x_axis["name_font"], x_axis["name_layout"], ) elif x_axis["name"] is not None: self._write_title_rich( x_axis["name"], is_y_axis, x_axis["name_font"], x_axis["name_layout"] ) # Write the c:numberFormat element. self._write_number_format(x_axis) # Write the c:majorTickMark element. self._write_major_tick_mark(x_axis.get("major_tick_mark")) # Write the c:minorTickMark element. self._write_minor_tick_mark(x_axis.get("minor_tick_mark")) # Write the c:tickLblPos element. self._write_tick_label_pos(x_axis.get("label_position")) # Write the c:spPr element for the axis line. self._write_sp_pr(x_axis) # Write the axis font elements. self._write_axis_font(x_axis.get("num_font")) # Write the c:crossAx element. self._write_cross_axis(axis_ids[1]) # Note, the category crossing comes from the value axis. if ( y_axis.get("crossing") is None or y_axis["crossing"] == "max" or y_axis["crossing"] == "min" ): # Write the c:crosses element. self._write_crosses(y_axis.get("crossing")) else: # Write the c:crossesAt element. self._write_c_crosses_at(y_axis.get("crossing")) # Write the c:crossBetween element. self._write_cross_between(y_axis.get("position_axis")) # Write the c:majorUnit element. self._write_c_major_unit(x_axis.get("major_unit")) # Write the c:minorUnit element. self._write_c_minor_unit(x_axis.get("minor_unit")) # Write the c:dispUnits element. self._write_disp_units( x_axis.get("display_units"), x_axis.get("display_units_visible") ) self._xml_end_tag("c:valAx") def _write_date_axis(self, args): # Write the element. Usually the X axis. x_axis = args["x_axis"] y_axis = args["y_axis"] axis_ids = args["axis_ids"] # If there are no axis_ids then we don't need to write this element. if axis_ids is None or not axis_ids: return position = self.cat_axis_position # Overwrite the default axis position with a user supplied value. position = x_axis.get("position") or position self._xml_start_tag("c:dateAx") self._write_axis_id(axis_ids[0]) # Write the c:scaling element. self._write_scaling( x_axis.get("reverse"), x_axis.get("min"), x_axis.get("max"), x_axis.get("log_base"), ) if not x_axis.get("visible"): self._write_delete(1) # Write the c:axPos element. self._write_axis_pos(position, y_axis.get("reverse")) # Write the c:majorGridlines element. self._write_major_gridlines(x_axis.get("major_gridlines")) # Write the c:minorGridlines element. self._write_minor_gridlines(x_axis.get("minor_gridlines")) # Write the axis title elements. if x_axis["formula"] is not None: self._write_title_formula( x_axis["formula"], x_axis["data_id"], None, x_axis["name_font"], x_axis["name_layout"], ) elif x_axis["name"] is not None: self._write_title_rich( x_axis["name"], None, x_axis["name_font"], x_axis["name_layout"] ) # Write the c:numFmt element. self._write_number_format(x_axis) # Write the c:majorTickMark element. self._write_major_tick_mark(x_axis.get("major_tick_mark")) # Write the c:minorTickMark element. self._write_minor_tick_mark(x_axis.get("minor_tick_mark")) # Write the c:tickLblPos element. self._write_tick_label_pos(x_axis.get("label_position")) # Write the c:spPr element for the axis line. self._write_sp_pr(x_axis) # Write the axis font elements. self._write_axis_font(x_axis.get("num_font")) # Write the c:crossAx element. self._write_cross_axis(axis_ids[1]) if self.show_crosses or x_axis.get("visible"): # Note, the category crossing comes from the value axis. if ( y_axis.get("crossing") is None or y_axis.get("crossing") == "max" or y_axis["crossing"] == "min" ): # Write the c:crosses element. self._write_crosses(y_axis.get("crossing")) else: # Write the c:crossesAt element. self._write_c_crosses_at(y_axis.get("crossing")) # Write the c:auto element. self._write_auto(1) # Write the c:labelOffset element. self._write_label_offset(100) # Write the c:tickLblSkip element. self._write_c_tick_lbl_skip(x_axis.get("interval_unit")) # Write the c:tickMarkSkip element. self._write_c_tick_mark_skip(x_axis.get("interval_tick")) # Write the c:majorUnit element. self._write_c_major_unit(x_axis.get("major_unit")) # Write the c:majorTimeUnit element. if x_axis.get("major_unit"): self._write_c_major_time_unit(x_axis["major_unit_type"]) # Write the c:minorUnit element. self._write_c_minor_unit(x_axis.get("minor_unit")) # Write the c:minorTimeUnit element. if x_axis.get("minor_unit"): self._write_c_minor_time_unit(x_axis["minor_unit_type"]) self._xml_end_tag("c:dateAx") def _write_scaling(self, reverse, min_val, max_val, log_base): # Write the element. self._xml_start_tag("c:scaling") # Write the c:logBase element. self._write_c_log_base(log_base) # Write the c:orientation element. self._write_orientation(reverse) # Write the c:max element. self._write_c_max(max_val) # Write the c:min element. self._write_c_min(min_val) self._xml_end_tag("c:scaling") def _write_c_log_base(self, val): # Write the element. if not val: return attributes = [("val", val)] self._xml_empty_tag("c:logBase", attributes) def _write_orientation(self, reverse): # Write the element. val = "minMax" if reverse: val = "maxMin" attributes = [("val", val)] self._xml_empty_tag("c:orientation", attributes) def _write_c_max(self, max_val): # Write the element. if max_val is None: return attributes = [("val", max_val)] self._xml_empty_tag("c:max", attributes) def _write_c_min(self, min_val): # Write the element. if min_val is None: return attributes = [("val", min_val)] self._xml_empty_tag("c:min", attributes) def _write_axis_pos(self, val, reverse): # Write the element. if reverse: if val == "l": val = "r" if val == "b": val = "t" attributes = [("val", val)] self._xml_empty_tag("c:axPos", attributes) def _write_number_format(self, axis): # Write the element. Note: It is assumed that if # a user defined number format is supplied (i.e., non-default) then # the sourceLinked attribute is 0. # The user can override this if required. format_code = axis.get("num_format") source_linked = 1 # Check if a user defined number format has been set. if format_code is not None and format_code != axis["defaults"]["num_format"]: source_linked = 0 # User override of sourceLinked. if axis.get("num_format_linked"): source_linked = 1 attributes = [ ("formatCode", format_code), ("sourceLinked", source_linked), ] self._xml_empty_tag("c:numFmt", attributes) def _write_cat_number_format(self, axis): # Write the element. Special case handler for category # axes which don't always have a number format. format_code = axis.get("num_format") source_linked = 1 default_format = 1 # Check if a user defined number format has been set. if format_code is not None and format_code != axis["defaults"]["num_format"]: source_linked = 0 default_format = 0 # User override of sourceLinked. if axis.get("num_format_linked"): source_linked = 1 # Skip if cat doesn't have a num format (unless it is non-default). if not self.cat_has_num_fmt and default_format: return attributes = [ ("formatCode", format_code), ("sourceLinked", source_linked), ] self._xml_empty_tag("c:numFmt", attributes) def _write_data_label_number_format(self, format_code): # Write the element for data labels. source_linked = 0 attributes = [ ("formatCode", format_code), ("sourceLinked", source_linked), ] self._xml_empty_tag("c:numFmt", attributes) def _write_major_tick_mark(self, val): # Write the element. if not val: return attributes = [("val", val)] self._xml_empty_tag("c:majorTickMark", attributes) def _write_minor_tick_mark(self, val): # Write the element. if not val: return attributes = [("val", val)] self._xml_empty_tag("c:minorTickMark", attributes) def _write_tick_label_pos(self, val=None): # Write the element. if val is None or val == "next_to": val = "nextTo" attributes = [("val", val)] self._xml_empty_tag("c:tickLblPos", attributes) def _write_cross_axis(self, val): # Write the element. attributes = [("val", val)] self._xml_empty_tag("c:crossAx", attributes) def _write_crosses(self, val=None): # Write the element. if val is None: val = "autoZero" attributes = [("val", val)] self._xml_empty_tag("c:crosses", attributes) def _write_c_crosses_at(self, val): # Write the element. attributes = [("val", val)] self._xml_empty_tag("c:crossesAt", attributes) def _write_auto(self, val): # Write the element. attributes = [("val", val)] self._xml_empty_tag("c:auto", attributes) def _write_label_align(self, val=None): # Write the element. if val is None: val = "ctr" if val == "right": val = "r" if val == "left": val = "l" attributes = [("val", val)] self._xml_empty_tag("c:lblAlgn", attributes) def _write_label_offset(self, val): # Write the element. attributes = [("val", val)] self._xml_empty_tag("c:lblOffset", attributes) def _write_c_tick_lbl_skip(self, val): # Write the element. if val is None: return attributes = [("val", val)] self._xml_empty_tag("c:tickLblSkip", attributes) def _write_c_tick_mark_skip(self, val): # Write the element. if val is None: return attributes = [("val", val)] self._xml_empty_tag("c:tickMarkSkip", attributes) def _write_major_gridlines(self, gridlines): # Write the element. if not gridlines: return if not gridlines["visible"]: return if gridlines["line"]["defined"]: self._xml_start_tag("c:majorGridlines") # Write the c:spPr element. self._write_sp_pr(gridlines) self._xml_end_tag("c:majorGridlines") else: self._xml_empty_tag("c:majorGridlines") def _write_minor_gridlines(self, gridlines): # Write the element. if not gridlines: return if not gridlines["visible"]: return if gridlines["line"]["defined"]: self._xml_start_tag("c:minorGridlines") # Write the c:spPr element. self._write_sp_pr(gridlines) self._xml_end_tag("c:minorGridlines") else: self._xml_empty_tag("c:minorGridlines") def _write_cross_between(self, val): # Write the element. if val is None: val = self.cross_between attributes = [("val", val)] self._xml_empty_tag("c:crossBetween", attributes) def _write_c_major_unit(self, val): # Write the element. if not val: return attributes = [("val", val)] self._xml_empty_tag("c:majorUnit", attributes) def _write_c_minor_unit(self, val): # Write the element. if not val: return attributes = [("val", val)] self._xml_empty_tag("c:minorUnit", attributes) def _write_c_major_time_unit(self, val=None): # Write the element. if val is None: val = "days" attributes = [("val", val)] self._xml_empty_tag("c:majorTimeUnit", attributes) def _write_c_minor_time_unit(self, val=None): # Write the element. if val is None: val = "days" attributes = [("val", val)] self._xml_empty_tag("c:minorTimeUnit", attributes) def _write_legend(self): # Write the element. legend = self.legend position = legend.get("position", "right") font = legend.get("font") delete_series = [] overlay = 0 if legend.get("delete_series") and isinstance(legend["delete_series"], list): delete_series = legend["delete_series"] if position.startswith("overlay_"): position = position.replace("overlay_", "") overlay = 1 allowed = { "right": "r", "left": "l", "top": "t", "bottom": "b", "top_right": "tr", } if position == "none": return if position not in allowed: return position = allowed[position] self._xml_start_tag("c:legend") # Write the c:legendPos element. self._write_legend_pos(position) # Remove series labels from the legend. for index in delete_series: # Write the c:legendEntry element. self._write_legend_entry(index) # Write the c:layout element. self._write_layout(legend.get("layout"), "legend") # Write the c:overlay element. if overlay: self._write_overlay() if font: self._write_tx_pr(font) # Write the c:spPr element. self._write_sp_pr(legend) self._xml_end_tag("c:legend") def _write_legend_pos(self, val): # Write the element. attributes = [("val", val)] self._xml_empty_tag("c:legendPos", attributes) def _write_legend_entry(self, index): # Write the element. self._xml_start_tag("c:legendEntry") # Write the c:idx element. self._write_idx(index) # Write the c:delete element. self._write_delete(1) self._xml_end_tag("c:legendEntry") def _write_overlay(self): # Write the element. val = 1 attributes = [("val", val)] self._xml_empty_tag("c:overlay", attributes) def _write_plot_vis_only(self): # Write the element. val = 1 # Ignore this element if we are plotting hidden data. if self.show_hidden: return attributes = [("val", val)] self._xml_empty_tag("c:plotVisOnly", attributes) def _write_print_settings(self): # Write the element. self._xml_start_tag("c:printSettings") # Write the c:headerFooter element. self._write_header_footer() # Write the c:pageMargins element. self._write_page_margins() # Write the c:pageSetup element. self._write_page_setup() self._xml_end_tag("c:printSettings") def _write_header_footer(self): # Write the element. self._xml_empty_tag("c:headerFooter") def _write_page_margins(self): # Write the element. bottom = 0.75 left = 0.7 right = 0.7 top = 0.75 header = 0.3 footer = 0.3 attributes = [ ("b", bottom), ("l", left), ("r", right), ("t", top), ("header", header), ("footer", footer), ] self._xml_empty_tag("c:pageMargins", attributes) def _write_page_setup(self): # Write the element. self._xml_empty_tag("c:pageSetup") def _write_c_auto_title_deleted(self): # Write the element. self._xml_empty_tag("c:autoTitleDeleted", [("val", 1)]) def _write_title_rich(self, title, is_y_axis, font, layout, overlay=False): # Write the element for a rich string. self._xml_start_tag("c:title") # Write the c:tx element. self._write_tx_rich(title, is_y_axis, font) # Write the c:layout element. self._write_layout(layout, "text") # Write the c:overlay element. if overlay: self._write_overlay() self._xml_end_tag("c:title") def _write_title_formula( self, title, data_id, is_y_axis, font, layout, overlay=False ): # Write the element for a rich string. self._xml_start_tag("c:title") # Write the c:tx element. self._write_tx_formula(title, data_id) # Write the c:layout element. self._write_layout(layout, "text") # Write the c:overlay element. if overlay: self._write_overlay() # Write the c:txPr element. self._write_tx_pr(font, is_y_axis) self._xml_end_tag("c:title") def _write_tx_rich(self, title, is_y_axis, font): # Write the element. self._xml_start_tag("c:tx") # Write the c:rich element. self._write_rich(title, font, is_y_axis, ignore_rich_pr=False) self._xml_end_tag("c:tx") def _write_tx_value(self, title): # Write the element with a value such as for series names. self._xml_start_tag("c:tx") # Write the c:v element. self._write_v(title) self._xml_end_tag("c:tx") def _write_tx_formula(self, title, data_id): # Write the element. data = None if data_id is not None: data = self.formula_data[data_id] self._xml_start_tag("c:tx") # Write the c:strRef element. self._write_str_ref(title, data, "str") self._xml_end_tag("c:tx") def _write_rich(self, title, font, is_y_axis, ignore_rich_pr): # Write the element. if font and font.get("rotation") is not None: rotation = font["rotation"] else: rotation = None self._xml_start_tag("c:rich") # Write the a:bodyPr element. self._write_a_body_pr(rotation, is_y_axis) # Write the a:lstStyle element. self._write_a_lst_style() # Write the a:p element. self._write_a_p_rich(title, font, ignore_rich_pr) self._xml_end_tag("c:rich") def _write_a_body_pr(self, rotation, is_y_axis): # Write the element. attributes = [] if rotation is None and is_y_axis: rotation = -5400000 if rotation is not None: if rotation == 16200000: # 270 deg/stacked angle. attributes.append(("rot", 0)) attributes.append(("vert", "wordArtVert")) elif rotation == 16260000: # 271 deg/East Asian vertical. attributes.append(("rot", 0)) attributes.append(("vert", "eaVert")) else: attributes.append(("rot", rotation)) attributes.append(("vert", "horz")) self._xml_empty_tag("a:bodyPr", attributes) def _write_a_lst_style(self): # Write the element. self._xml_empty_tag("a:lstStyle") def _write_a_p_rich(self, title, font, ignore_rich_pr): # Write the element for rich string titles. self._xml_start_tag("a:p") # Write the a:pPr element. if not ignore_rich_pr: self._write_a_p_pr_rich(font) # Write the a:r element. self._write_a_r(title, font) self._xml_end_tag("a:p") def _write_a_p_formula(self, font): # Write the element for formula titles. self._xml_start_tag("a:p") # Write the a:pPr element. self._write_a_p_pr_rich(font) # Write the a:endParaRPr element. self._write_a_end_para_rpr() self._xml_end_tag("a:p") def _write_a_p_pr_rich(self, font): # Write the element for rich string titles. self._xml_start_tag("a:pPr") # Write the a:defRPr element. self._write_a_def_rpr(font) self._xml_end_tag("a:pPr") def _write_a_def_rpr(self, font): # Write the element. has_color = 0 style_attributes = Shape._get_font_style_attributes(font) latin_attributes = Shape._get_font_latin_attributes(font) if font and font.get("color") is not None: has_color = 1 if latin_attributes or has_color: self._xml_start_tag("a:defRPr", style_attributes) if has_color: self._write_a_solid_fill({"color": font["color"]}) if latin_attributes: self._write_a_latin(latin_attributes) self._xml_end_tag("a:defRPr") else: self._xml_empty_tag("a:defRPr", style_attributes) def _write_a_end_para_rpr(self): # Write the element. lang = "en-US" attributes = [("lang", lang)] self._xml_empty_tag("a:endParaRPr", attributes) def _write_a_r(self, title, font): # Write the element. self._xml_start_tag("a:r") # Write the a:rPr element. self._write_a_r_pr(font) # Write the a:t element. self._write_a_t(title) self._xml_end_tag("a:r") def _write_a_r_pr(self, font): # Write the element. has_color = 0 lang = "en-US" style_attributes = Shape._get_font_style_attributes(font) latin_attributes = Shape._get_font_latin_attributes(font) if font and font["color"] is not None: has_color = 1 # Add the lang type to the attributes. style_attributes.insert(0, ("lang", lang)) if latin_attributes or has_color: self._xml_start_tag("a:rPr", style_attributes) if has_color: self._write_a_solid_fill({"color": font["color"]}) if latin_attributes: self._write_a_latin(latin_attributes) self._xml_end_tag("a:rPr") else: self._xml_empty_tag("a:rPr", style_attributes) def _write_a_t(self, title): # Write the element. self._xml_data_element("a:t", title) def _write_tx_pr(self, font, is_y_axis=False): # Write the element. if font and font.get("rotation") is not None: rotation = font["rotation"] else: rotation = None self._xml_start_tag("c:txPr") # Write the a:bodyPr element. self._write_a_body_pr(rotation, is_y_axis) # Write the a:lstStyle element. self._write_a_lst_style() # Write the a:p element. self._write_a_p_formula(font) self._xml_end_tag("c:txPr") def _write_marker(self, marker): # Write the element. if marker is None: marker = self.default_marker if not marker: return if marker["type"] == "automatic": return self._xml_start_tag("c:marker") # Write the c:symbol element. self._write_symbol(marker["type"]) # Write the c:size element. if marker.get("size"): self._write_marker_size(marker["size"]) # Write the c:spPr element. self._write_sp_pr(marker) self._xml_end_tag("c:marker") def _write_marker_size(self, val): # Write the element. attributes = [("val", val)] self._xml_empty_tag("c:size", attributes) def _write_symbol(self, val): # Write the element. attributes = [("val", val)] self._xml_empty_tag("c:symbol", attributes) def _write_sp_pr(self, series): # Write the element. if not self._has_fill_formatting(series): return self._xml_start_tag("c:spPr") # Write the fill elements for solid charts such as pie and bar. if series.get("fill") and series["fill"]["defined"]: if "none" in series["fill"]: # Write the a:noFill element. self._write_a_no_fill() else: # Write the a:solidFill element. self._write_a_solid_fill(series["fill"]) if series.get("pattern"): # Write the a:gradFill element. self._write_a_patt_fill(series["pattern"]) if series.get("gradient"): # Write the a:gradFill element. self._write_a_grad_fill(series["gradient"]) # Write the a:ln element. if series.get("line") and series["line"]["defined"]: self._write_a_ln(series["line"]) self._xml_end_tag("c:spPr") def _write_a_ln(self, line): # Write the element. attributes = [] # Add the line width as an attribute. width = line.get("width") if width is not None: # Round width to nearest 0.25, like Excel. width = int((width + 0.125) * 4) / 4.0 # Convert to internal units. width = int(0.5 + (12700 * width)) attributes = [("w", width)] if line.get("none") or line.get("color") or line.get("dash_type"): self._xml_start_tag("a:ln", attributes) # Write the line fill. if "none" in line: # Write the a:noFill element. self._write_a_no_fill() elif "color" in line: # Write the a:solidFill element. self._write_a_solid_fill(line) # Write the line/dash type. line_type = line.get("dash_type") if line_type: # Write the a:prstDash element. self._write_a_prst_dash(line_type) self._xml_end_tag("a:ln") else: self._xml_empty_tag("a:ln", attributes) def _write_a_no_fill(self): # Write the element. self._xml_empty_tag("a:noFill") def _write_a_solid_fill(self, fill): # Write the element. self._xml_start_tag("a:solidFill") if "color" in fill: color = _get_rgb_color(fill["color"]) transparency = fill.get("transparency") # Write the a:srgbClr element. self._write_a_srgb_clr(color, transparency) self._xml_end_tag("a:solidFill") def _write_a_srgb_clr(self, val, transparency=None): # Write the element. attributes = [("val", val)] if transparency: self._xml_start_tag("a:srgbClr", attributes) # Write the a:alpha element. self._write_a_alpha(transparency) self._xml_end_tag("a:srgbClr") else: self._xml_empty_tag("a:srgbClr", attributes) def _write_a_alpha(self, val): # Write the element. val = int((100 - int(val)) * 1000) attributes = [("val", val)] self._xml_empty_tag("a:alpha", attributes) def _write_a_prst_dash(self, val): # Write the element. attributes = [("val", val)] self._xml_empty_tag("a:prstDash", attributes) def _write_trendline(self, trendline): # Write the element. if not trendline: return self._xml_start_tag("c:trendline") # Write the c:name element. self._write_name(trendline.get("name")) # Write the c:spPr element. self._write_sp_pr(trendline) # Write the c:trendlineType element. self._write_trendline_type(trendline["type"]) # Write the c:order element for polynomial trendlines. if trendline["type"] == "poly": self._write_trendline_order(trendline.get("order")) # Write the c:period element for moving average trendlines. if trendline["type"] == "movingAvg": self._write_period(trendline.get("period")) # Write the c:forward element. self._write_forward(trendline.get("forward")) # Write the c:backward element. self._write_backward(trendline.get("backward")) if "intercept" in trendline: # Write the c:intercept element. self._write_c_intercept(trendline["intercept"]) if trendline.get("display_r_squared"): # Write the c:dispRSqr element. self._write_c_disp_rsqr() if trendline.get("display_equation"): # Write the c:dispEq element. self._write_c_disp_eq() # Write the c:trendlineLbl element. self._write_c_trendline_lbl(trendline) self._xml_end_tag("c:trendline") def _write_trendline_type(self, val): # Write the element. attributes = [("val", val)] self._xml_empty_tag("c:trendlineType", attributes) def _write_name(self, data): # Write the element. if data is None: return self._xml_data_element("c:name", data) def _write_trendline_order(self, val): # Write the element. val = max(val, 2) attributes = [("val", val)] self._xml_empty_tag("c:order", attributes) def _write_period(self, val): # Write the element. val = max(val, 2) attributes = [("val", val)] self._xml_empty_tag("c:period", attributes) def _write_forward(self, val): # Write the element. if not val: return attributes = [("val", val)] self._xml_empty_tag("c:forward", attributes) def _write_backward(self, val): # Write the element. if not val: return attributes = [("val", val)] self._xml_empty_tag("c:backward", attributes) def _write_c_intercept(self, val): # Write the element. attributes = [("val", val)] self._xml_empty_tag("c:intercept", attributes) def _write_c_disp_eq(self): # Write the element. attributes = [("val", 1)] self._xml_empty_tag("c:dispEq", attributes) def _write_c_disp_rsqr(self): # Write the element. attributes = [("val", 1)] self._xml_empty_tag("c:dispRSqr", attributes) def _write_c_trendline_lbl(self, trendline): # Write the element. self._xml_start_tag("c:trendlineLbl") # Write the c:layout element. self._write_layout(None, None) # Write the c:numFmt element. self._write_trendline_num_fmt() # Write the c:spPr element. self._write_sp_pr(trendline["label"]) # Write the data label font elements. if trendline["label"]: font = trendline["label"].get("font") if font: self._write_axis_font(font) self._xml_end_tag("c:trendlineLbl") def _write_trendline_num_fmt(self): # Write the element. attributes = [ ("formatCode", "General"), ("sourceLinked", 0), ] self._xml_empty_tag("c:numFmt", attributes) def _write_hi_low_lines(self): # Write the element. hi_low_lines = self.hi_low_lines if hi_low_lines is None: return if "line" in hi_low_lines and hi_low_lines["line"]["defined"]: self._xml_start_tag("c:hiLowLines") # Write the c:spPr element. self._write_sp_pr(hi_low_lines) self._xml_end_tag("c:hiLowLines") else: self._xml_empty_tag("c:hiLowLines") def _write_drop_lines(self): # Write the element. drop_lines = self.drop_lines if drop_lines is None: return if drop_lines["line"]["defined"]: self._xml_start_tag("c:dropLines") # Write the c:spPr element. self._write_sp_pr(drop_lines) self._xml_end_tag("c:dropLines") else: self._xml_empty_tag("c:dropLines") def _write_overlap(self, val): # Write the element. if val is None: return attributes = [("val", val)] self._xml_empty_tag("c:overlap", attributes) def _write_num_cache(self, data): # Write the element. if data: count = len(data) else: count = 0 self._xml_start_tag("c:numCache") # Write the c:formatCode element. self._write_format_code("General") # Write the c:ptCount element. self._write_pt_count(count) for i in range(count): token = data[i] if token is None: continue try: float(token) except ValueError: # Write non-numeric data as 0. token = 0 # Write the c:pt element. self._write_pt(i, token) self._xml_end_tag("c:numCache") def _write_str_cache(self, data): # Write the element. count = len(data) self._xml_start_tag("c:strCache") # Write the c:ptCount element. self._write_pt_count(count) for i in range(count): # Write the c:pt element. self._write_pt(i, data[i]) self._xml_end_tag("c:strCache") def _write_format_code(self, data): # Write the element. self._xml_data_element("c:formatCode", data) def _write_pt_count(self, val): # Write the element. attributes = [("val", val)] self._xml_empty_tag("c:ptCount", attributes) def _write_pt(self, idx, value): # Write the element. if value is None: return attributes = [("idx", idx)] self._xml_start_tag("c:pt", attributes) # Write the c:v element. self._write_v(value) self._xml_end_tag("c:pt") def _write_v(self, data): # Write the element. self._xml_data_element("c:v", data) def _write_protection(self): # Write the element. if not self.protection: return self._xml_empty_tag("c:protection") def _write_d_pt(self, points): # Write the elements. index = -1 if not points: return for point in points: index += 1 if not point: continue self._write_d_pt_point(index, point) def _write_d_pt_point(self, index, point): # Write an individual element. self._xml_start_tag("c:dPt") # Write the c:idx element. self._write_idx(index) # Write the c:spPr element. self._write_sp_pr(point) self._xml_end_tag("c:dPt") def _write_d_lbls(self, labels): # Write the element. if not labels: return self._xml_start_tag("c:dLbls") # Write the custom c:dLbl elements. if labels.get("custom"): self._write_custom_labels(labels, labels["custom"]) # Write the c:numFmt element. if labels.get("num_format"): self._write_data_label_number_format(labels["num_format"]) # Write the c:spPr element for the plotarea formatting. self._write_sp_pr(labels) # Write the data label font elements. if labels.get("font"): self._write_axis_font(labels["font"]) # Write the c:dLblPos element. if labels.get("position"): self._write_d_lbl_pos(labels["position"]) # Write the c:showLegendKey element. if labels.get("legend_key"): self._write_show_legend_key() # Write the c:showVal element. if labels.get("value"): self._write_show_val() # Write the c:showCatName element. if labels.get("category"): self._write_show_cat_name() # Write the c:showSerName element. if labels.get("series_name"): self._write_show_ser_name() # Write the c:showPercent element. if labels.get("percentage"): self._write_show_percent() # Write the c:separator element. if labels.get("separator"): self._write_separator(labels["separator"]) # Write the c:showLeaderLines element. if labels.get("leader_lines"): self._write_show_leader_lines() self._xml_end_tag("c:dLbls") def _write_custom_labels(self, parent, labels): # Write the element. index = 0 for label in labels: index += 1 if label is None: continue self._xml_start_tag("c:dLbl") # Write the c:idx element. self._write_idx(index - 1) delete_label = label.get("delete") if delete_label: self._write_delete(1) elif label.get("formula"): self._write_custom_label_formula(label) if parent.get("position"): self._write_d_lbl_pos(parent["position"]) if parent.get("value"): self._write_show_val() if parent.get("category"): self._write_show_cat_name() if parent.get("series_name"): self._write_show_ser_name() elif label.get("value"): self._write_custom_label_str(label) if parent.get("position"): self._write_d_lbl_pos(parent["position"]) if parent.get("value"): self._write_show_val() if parent.get("category"): self._write_show_cat_name() if parent.get("series_name"): self._write_show_ser_name() else: self._write_custom_label_format_only(label) self._xml_end_tag("c:dLbl") def _write_custom_label_str(self, label): # Write parts of the element for strings. title = label.get("value") font = label.get("font") has_formatting = self._has_fill_formatting(label) # Write the c:layout element. self._write_layout(None, None) self._xml_start_tag("c:tx") # Write the c:rich element. self._write_rich(title, font, False, not has_formatting) self._xml_end_tag("c:tx") # Write the c:spPr element. self._write_sp_pr(label) def _write_custom_label_formula(self, label): # Write parts of the element for formulas. formula = label.get("formula") data_id = label.get("data_id") data = None if data_id is not None: data = self.formula_data[data_id] # Write the c:layout element. self._write_layout(None, None) self._xml_start_tag("c:tx") # Write the c:strRef element. self._write_str_ref(formula, data, "str") self._xml_end_tag("c:tx") # Write the data label formatting, if any. self._write_custom_label_format_only(label) def _write_custom_label_format_only(self, label): # Write parts of the labels with changed formatting. font = label.get("font") has_formatting = self._has_fill_formatting(label) if has_formatting: self._write_sp_pr(label) self._write_tx_pr(font) elif font: self._xml_empty_tag("c:spPr") self._write_tx_pr(font) def _write_show_legend_key(self): # Write the element. val = "1" attributes = [("val", val)] self._xml_empty_tag("c:showLegendKey", attributes) def _write_show_val(self): # Write the element. val = 1 attributes = [("val", val)] self._xml_empty_tag("c:showVal", attributes) def _write_show_cat_name(self): # Write the element. val = 1 attributes = [("val", val)] self._xml_empty_tag("c:showCatName", attributes) def _write_show_ser_name(self): # Write the element. val = 1 attributes = [("val", val)] self._xml_empty_tag("c:showSerName", attributes) def _write_show_percent(self): # Write the element. val = 1 attributes = [("val", val)] self._xml_empty_tag("c:showPercent", attributes) def _write_separator(self, data): # Write the element. self._xml_data_element("c:separator", data) def _write_show_leader_lines(self): # Write the element. # # This is different for Pie/Doughnut charts. Other chart types only # supported leader lines after Excel 2015 via an extension element. # uri = "{CE6537A1-D6FC-4f65-9D91-7224C49458BB}" xmlns_c_15 = "http://schemas.microsoft.com/office/drawing/2012/chart" attributes = [ ("uri", uri), ("xmlns:c15", xmlns_c_15), ] self._xml_start_tag("c:extLst") self._xml_start_tag("c:ext", attributes) self._xml_empty_tag("c15:showLeaderLines", [("val", 1)]) self._xml_end_tag("c:ext") self._xml_end_tag("c:extLst") def _write_d_lbl_pos(self, val): # Write the element. attributes = [("val", val)] self._xml_empty_tag("c:dLblPos", attributes) def _write_delete(self, val): # Write the element. attributes = [("val", val)] self._xml_empty_tag("c:delete", attributes) def _write_c_invert_if_negative(self, invert): # Write the element. val = 1 if not invert: return attributes = [("val", val)] self._xml_empty_tag("c:invertIfNegative", attributes) def _write_axis_font(self, font): # Write the axis font elements. if not font: return self._xml_start_tag("c:txPr") self._write_a_body_pr(font.get("rotation"), None) self._write_a_lst_style() self._xml_start_tag("a:p") self._write_a_p_pr_rich(font) self._write_a_end_para_rpr() self._xml_end_tag("a:p") self._xml_end_tag("c:txPr") def _write_a_latin(self, attributes): # Write the element. self._xml_empty_tag("a:latin", attributes) def _write_d_table(self): # Write the element. table = self.table if not table: return self._xml_start_tag("c:dTable") if table["horizontal"]: # Write the c:showHorzBorder element. self._write_show_horz_border() if table["vertical"]: # Write the c:showVertBorder element. self._write_show_vert_border() if table["outline"]: # Write the c:showOutline element. self._write_show_outline() if table["show_keys"]: # Write the c:showKeys element. self._write_show_keys() if table["font"]: # Write the table font. self._write_tx_pr(table["font"]) self._xml_end_tag("c:dTable") def _write_show_horz_border(self): # Write the element. attributes = [("val", 1)] self._xml_empty_tag("c:showHorzBorder", attributes) def _write_show_vert_border(self): # Write the element. attributes = [("val", 1)] self._xml_empty_tag("c:showVertBorder", attributes) def _write_show_outline(self): # Write the element. attributes = [("val", 1)] self._xml_empty_tag("c:showOutline", attributes) def _write_show_keys(self): # Write the element. attributes = [("val", 1)] self._xml_empty_tag("c:showKeys", attributes) def _write_error_bars(self, error_bars): # Write the X and Y error bars. if not error_bars: return if error_bars["x_error_bars"]: self._write_err_bars("x", error_bars["x_error_bars"]) if error_bars["y_error_bars"]: self._write_err_bars("y", error_bars["y_error_bars"]) def _write_err_bars(self, direction, error_bars): # Write the element. if not error_bars: return self._xml_start_tag("c:errBars") # Write the c:errDir element. self._write_err_dir(direction) # Write the c:errBarType element. self._write_err_bar_type(error_bars["direction"]) # Write the c:errValType element. self._write_err_val_type(error_bars["type"]) if not error_bars["endcap"]: # Write the c:noEndCap element. self._write_no_end_cap() if error_bars["type"] == "stdErr": # Don't need to write a c:errValType tag. pass elif error_bars["type"] == "cust": # Write the custom error tags. self._write_custom_error(error_bars) else: # Write the c:val element. self._write_error_val(error_bars["value"]) # Write the c:spPr element. self._write_sp_pr(error_bars) self._xml_end_tag("c:errBars") def _write_err_dir(self, val): # Write the element. attributes = [("val", val)] self._xml_empty_tag("c:errDir", attributes) def _write_err_bar_type(self, val): # Write the element. attributes = [("val", val)] self._xml_empty_tag("c:errBarType", attributes) def _write_err_val_type(self, val): # Write the element. attributes = [("val", val)] self._xml_empty_tag("c:errValType", attributes) def _write_no_end_cap(self): # Write the element. attributes = [("val", 1)] self._xml_empty_tag("c:noEndCap", attributes) def _write_error_val(self, val): # Write the element for error bars. attributes = [("val", val)] self._xml_empty_tag("c:val", attributes) def _write_custom_error(self, error_bars): # Write the custom error bars tags. if error_bars["plus_values"]: # Write the c:plus element. self._xml_start_tag("c:plus") if isinstance(error_bars["plus_values"], list): self._write_num_lit(error_bars["plus_values"]) else: self._write_num_ref( error_bars["plus_values"], error_bars["plus_data"], "num" ) self._xml_end_tag("c:plus") if error_bars["minus_values"]: # Write the c:minus element. self._xml_start_tag("c:minus") if isinstance(error_bars["minus_values"], list): self._write_num_lit(error_bars["minus_values"]) else: self._write_num_ref( error_bars["minus_values"], error_bars["minus_data"], "num" ) self._xml_end_tag("c:minus") def _write_num_lit(self, data): # Write the element for literal number list elements. count = len(data) # Write the c:numLit element. self._xml_start_tag("c:numLit") # Write the c:formatCode element. self._write_format_code("General") # Write the c:ptCount element. self._write_pt_count(count) for i in range(count): token = data[i] if token is None: continue try: float(token) except ValueError: # Write non-numeric data as 0. token = 0 # Write the c:pt element. self._write_pt(i, token) self._xml_end_tag("c:numLit") def _write_up_down_bars(self): # Write the element. up_down_bars = self.up_down_bars if up_down_bars is None: return self._xml_start_tag("c:upDownBars") # Write the c:gapWidth element. self._write_gap_width(150) # Write the c:upBars element. self._write_up_bars(up_down_bars.get("up")) # Write the c:downBars element. self._write_down_bars(up_down_bars.get("down")) self._xml_end_tag("c:upDownBars") def _write_gap_width(self, val): # Write the element. if val is None: return attributes = [("val", val)] self._xml_empty_tag("c:gapWidth", attributes) def _write_up_bars(self, bar_format): # Write the element. if bar_format["line"] and bar_format["line"]["defined"]: self._xml_start_tag("c:upBars") # Write the c:spPr element. self._write_sp_pr(bar_format) self._xml_end_tag("c:upBars") else: self._xml_empty_tag("c:upBars") def _write_down_bars(self, bar_format): # Write the element. if bar_format["line"] and bar_format["line"]["defined"]: self._xml_start_tag("c:downBars") # Write the c:spPr element. self._write_sp_pr(bar_format) self._xml_end_tag("c:downBars") else: self._xml_empty_tag("c:downBars") def _write_disp_units(self, units, display): # Write the element. if not units: return attributes = [("val", units)] self._xml_start_tag("c:dispUnits") self._xml_empty_tag("c:builtInUnit", attributes) if display: self._xml_start_tag("c:dispUnitsLbl") self._xml_empty_tag("c:layout") self._xml_end_tag("c:dispUnitsLbl") self._xml_end_tag("c:dispUnits") def _write_a_grad_fill(self, gradient): # Write the element. attributes = [("flip", "none"), ("rotWithShape", "1")] if gradient["type"] == "linear": attributes = [] self._xml_start_tag("a:gradFill", attributes) # Write the a:gsLst element. self._write_a_gs_lst(gradient) if gradient["type"] == "linear": # Write the a:lin element. self._write_a_lin(gradient["angle"]) else: # Write the a:path element. self._write_a_path(gradient["type"]) # Write the a:tileRect element. self._write_a_tile_rect(gradient["type"]) self._xml_end_tag("a:gradFill") def _write_a_gs_lst(self, gradient): # Write the element. positions = gradient["positions"] colors = gradient["colors"] self._xml_start_tag("a:gsLst") for i, color in enumerate(colors): pos = int(positions[i] * 1000) attributes = [("pos", pos)] self._xml_start_tag("a:gs", attributes) # Write the a:srgbClr element. color = _get_rgb_color(color) self._write_a_srgb_clr(color) self._xml_end_tag("a:gs") self._xml_end_tag("a:gsLst") def _write_a_lin(self, angle): # Write the element. angle = int(60000 * angle) attributes = [ ("ang", angle), ("scaled", "0"), ] self._xml_empty_tag("a:lin", attributes) def _write_a_path(self, gradient_type): # Write the element. attributes = [("path", gradient_type)] self._xml_start_tag("a:path", attributes) # Write the a:fillToRect element. self._write_a_fill_to_rect(gradient_type) self._xml_end_tag("a:path") def _write_a_fill_to_rect(self, gradient_type): # Write the element. if gradient_type == "shape": attributes = [ ("l", "50000"), ("t", "50000"), ("r", "50000"), ("b", "50000"), ] else: attributes = [ ("l", "100000"), ("t", "100000"), ] self._xml_empty_tag("a:fillToRect", attributes) def _write_a_tile_rect(self, gradient_type): # Write the element. if gradient_type == "shape": attributes = [] else: attributes = [ ("r", "-100000"), ("b", "-100000"), ] self._xml_empty_tag("a:tileRect", attributes) def _write_a_patt_fill(self, pattern): # Write the element. attributes = [("prst", pattern["pattern"])] self._xml_start_tag("a:pattFill", attributes) # Write the a:fgClr element. self._write_a_fg_clr(pattern["fg_color"]) # Write the a:bgClr element. self._write_a_bg_clr(pattern["bg_color"]) self._xml_end_tag("a:pattFill") def _write_a_fg_clr(self, color): # Write the element. color = _get_rgb_color(color) self._xml_start_tag("a:fgClr") # Write the a:srgbClr element. self._write_a_srgb_clr(color) self._xml_end_tag("a:fgClr") def _write_a_bg_clr(self, color): # Write the element. color = _get_rgb_color(color) self._xml_start_tag("a:bgClr") # Write the a:srgbClr element. self._write_a_srgb_clr(color) self._xml_end_tag("a:bgClr")