4383 lines
127 KiB
Python
4383 lines
127 KiB
Python
###############################################################################
|
|
#
|
|
# 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 <c:chartSpace> 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 <c:lang> element.
|
|
val = "en-US"
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:lang", attributes)
|
|
|
|
def _write_style(self):
|
|
# Write the <c:style> 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 <c:chart> 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 <c:dispBlanksAs> 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 <c:plotArea> 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 <c:layout> 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 <c:manualLayout> 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 <c:grouping> 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 <c:ser> 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 <c:extLst> 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 <c:extLst> 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 <c:idx> element.
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:idx", attributes)
|
|
|
|
def _write_order(self, val):
|
|
# Write the <c:order> 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 <c:smooth> element.
|
|
|
|
if smooth:
|
|
self._xml_empty_tag("c:smooth", [("val", "1")])
|
|
|
|
def _write_cat(self, series):
|
|
# Write the <c:cat> 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 <c:cat> 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 <c:val> 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 <c:numRef> 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 <c:strRef> 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 <c:multiLvlStrRef> 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 <c:f> 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 <c:axId> 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 <c:axId> element.
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:axId", attributes)
|
|
|
|
def _write_cat_axis(self, args):
|
|
# Write the <c:catAx> 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 <c:valAx> 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 <c:valAx> 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 <c:dateAx> 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 <c:scaling> 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 <c:logBase> element.
|
|
|
|
if not val:
|
|
return
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:logBase", attributes)
|
|
|
|
def _write_orientation(self, reverse):
|
|
# Write the <c:orientation> 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 <c:max> 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 <c:min> 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 <c:axPos> 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 <c:numberFormat> 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 <c:numFmt> 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 <c:numberFormat> 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 <c:majorTickMark> element.
|
|
|
|
if not val:
|
|
return
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:majorTickMark", attributes)
|
|
|
|
def _write_minor_tick_mark(self, val):
|
|
# Write the <c:minorTickMark> 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 <c:tickLblPos> 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 <c:crossAx> element.
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:crossAx", attributes)
|
|
|
|
def _write_crosses(self, val=None):
|
|
# Write the <c:crosses> 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 <c:crossesAt> element.
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:crossesAt", attributes)
|
|
|
|
def _write_auto(self, val):
|
|
# Write the <c:auto> element.
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:auto", attributes)
|
|
|
|
def _write_label_align(self, val=None):
|
|
# Write the <c:labelAlign> 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 <c:labelOffset> element.
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:lblOffset", attributes)
|
|
|
|
def _write_c_tick_lbl_skip(self, val):
|
|
# Write the <c:tickLblSkip> 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 <c:tickMarkSkip> element.
|
|
if val is None:
|
|
return
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:tickMarkSkip", attributes)
|
|
|
|
def _write_major_gridlines(self, gridlines):
|
|
# Write the <c:majorGridlines> 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 <c:minorGridlines> 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 <c:crossBetween> 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 <c:majorUnit> element.
|
|
|
|
if not val:
|
|
return
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:majorUnit", attributes)
|
|
|
|
def _write_c_minor_unit(self, val):
|
|
# Write the <c:minorUnit> 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 <c:majorTimeUnit> 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 <c:minorTimeUnit> element.
|
|
if val is None:
|
|
val = "days"
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:minorTimeUnit", attributes)
|
|
|
|
def _write_legend(self):
|
|
# Write the <c:legend> 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 <c:legendPos> element.
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:legendPos", attributes)
|
|
|
|
def _write_legend_entry(self, index):
|
|
# Write the <c:legendEntry> 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 <c:overlay> element.
|
|
val = 1
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:overlay", attributes)
|
|
|
|
def _write_plot_vis_only(self):
|
|
# Write the <c:plotVisOnly> 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 <c:printSettings> 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 <c:headerFooter> element.
|
|
self._xml_empty_tag("c:headerFooter")
|
|
|
|
def _write_page_margins(self):
|
|
# Write the <c:pageMargins> 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 <c:pageSetup> element.
|
|
self._xml_empty_tag("c:pageSetup")
|
|
|
|
def _write_c_auto_title_deleted(self):
|
|
# Write the <c:autoTitleDeleted> element.
|
|
self._xml_empty_tag("c:autoTitleDeleted", [("val", 1)])
|
|
|
|
def _write_title_rich(self, title, is_y_axis, font, layout, overlay=False):
|
|
# Write the <c:title> 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 <c:title> 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 <c:tx> 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 <c:tx> 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 <c:tx> 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 <c:rich> 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 <a:bodyPr> 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 <a:lstStyle> element.
|
|
self._xml_empty_tag("a:lstStyle")
|
|
|
|
def _write_a_p_rich(self, title, font, ignore_rich_pr):
|
|
# Write the <a:p> 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 <a:p> 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 <a:pPr> 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 <a:defRPr> 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 <a:endParaRPr> element.
|
|
lang = "en-US"
|
|
|
|
attributes = [("lang", lang)]
|
|
|
|
self._xml_empty_tag("a:endParaRPr", attributes)
|
|
|
|
def _write_a_r(self, title, font):
|
|
# Write the <a:r> 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 <a:rPr> 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 <a:t> element.
|
|
|
|
self._xml_data_element("a:t", title)
|
|
|
|
def _write_tx_pr(self, font, is_y_axis=False):
|
|
# Write the <c:txPr> 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 <c:marker> 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 <c:size> element.
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:size", attributes)
|
|
|
|
def _write_symbol(self, val):
|
|
# Write the <c:symbol> element.
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:symbol", attributes)
|
|
|
|
def _write_sp_pr(self, series):
|
|
# Write the <c:spPr> 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 <a:ln> 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 <a:noFill> element.
|
|
self._xml_empty_tag("a:noFill")
|
|
|
|
def _write_a_solid_fill(self, fill):
|
|
# Write the <a:solidFill> 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 <a:srgbClr> 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 <a:alpha> 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 <a:prstDash> element.
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("a:prstDash", attributes)
|
|
|
|
def _write_trendline(self, trendline):
|
|
# Write the <c:trendline> 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 <c:trendlineType> element.
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:trendlineType", attributes)
|
|
|
|
def _write_name(self, data):
|
|
# Write the <c:name> element.
|
|
|
|
if data is None:
|
|
return
|
|
|
|
self._xml_data_element("c:name", data)
|
|
|
|
def _write_trendline_order(self, val):
|
|
# Write the <c:order> element.
|
|
val = max(val, 2)
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:order", attributes)
|
|
|
|
def _write_period(self, val):
|
|
# Write the <c:period> element.
|
|
val = max(val, 2)
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:period", attributes)
|
|
|
|
def _write_forward(self, val):
|
|
# Write the <c:forward> element.
|
|
|
|
if not val:
|
|
return
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:forward", attributes)
|
|
|
|
def _write_backward(self, val):
|
|
# Write the <c:backward> element.
|
|
|
|
if not val:
|
|
return
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:backward", attributes)
|
|
|
|
def _write_c_intercept(self, val):
|
|
# Write the <c:intercept> element.
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:intercept", attributes)
|
|
|
|
def _write_c_disp_eq(self):
|
|
# Write the <c:dispEq> element.
|
|
attributes = [("val", 1)]
|
|
|
|
self._xml_empty_tag("c:dispEq", attributes)
|
|
|
|
def _write_c_disp_rsqr(self):
|
|
# Write the <c:dispRSqr> element.
|
|
attributes = [("val", 1)]
|
|
|
|
self._xml_empty_tag("c:dispRSqr", attributes)
|
|
|
|
def _write_c_trendline_lbl(self, trendline):
|
|
# Write the <c:trendlineLbl> 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 <c:numFmt> element.
|
|
attributes = [
|
|
("formatCode", "General"),
|
|
("sourceLinked", 0),
|
|
]
|
|
|
|
self._xml_empty_tag("c:numFmt", attributes)
|
|
|
|
def _write_hi_low_lines(self):
|
|
# Write the <c:hiLowLines> 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 <c:dropLines> 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 <c:overlap> element.
|
|
|
|
if val is None:
|
|
return
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:overlap", attributes)
|
|
|
|
def _write_num_cache(self, data):
|
|
# Write the <c:numCache> 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 <c:strCache> 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 <c:formatCode> element.
|
|
|
|
self._xml_data_element("c:formatCode", data)
|
|
|
|
def _write_pt_count(self, val):
|
|
# Write the <c:ptCount> element.
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:ptCount", attributes)
|
|
|
|
def _write_pt(self, idx, value):
|
|
# Write the <c:pt> 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 <c:v> element.
|
|
|
|
self._xml_data_element("c:v", data)
|
|
|
|
def _write_protection(self):
|
|
# Write the <c:protection> element.
|
|
if not self.protection:
|
|
return
|
|
|
|
self._xml_empty_tag("c:protection")
|
|
|
|
def _write_d_pt(self, points):
|
|
# Write the <c:dPt> 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 <c:dPt> 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 <c:dLbls> 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 <c:showLegendKey> 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 <c:dLbl> 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 <c:dLbl> 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 <c:dLbl> 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 <c:showLegendKey> element.
|
|
val = "1"
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:showLegendKey", attributes)
|
|
|
|
def _write_show_val(self):
|
|
# Write the <c:showVal> element.
|
|
val = 1
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:showVal", attributes)
|
|
|
|
def _write_show_cat_name(self):
|
|
# Write the <c:showCatName> element.
|
|
val = 1
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:showCatName", attributes)
|
|
|
|
def _write_show_ser_name(self):
|
|
# Write the <c:showSerName> element.
|
|
val = 1
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:showSerName", attributes)
|
|
|
|
def _write_show_percent(self):
|
|
# Write the <c:showPercent> element.
|
|
val = 1
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:showPercent", attributes)
|
|
|
|
def _write_separator(self, data):
|
|
# Write the <c:separator> element.
|
|
self._xml_data_element("c:separator", data)
|
|
|
|
def _write_show_leader_lines(self):
|
|
# Write the <c:showLeaderLines> 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 <c:dLblPos> element.
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:dLblPos", attributes)
|
|
|
|
def _write_delete(self, val):
|
|
# Write the <c:delete> element.
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:delete", attributes)
|
|
|
|
def _write_c_invert_if_negative(self, invert):
|
|
# Write the <c:invertIfNegative> 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 <a:latin> element.
|
|
self._xml_empty_tag("a:latin", attributes)
|
|
|
|
def _write_d_table(self):
|
|
# Write the <c:dTable> 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 <c:showHorzBorder> element.
|
|
attributes = [("val", 1)]
|
|
|
|
self._xml_empty_tag("c:showHorzBorder", attributes)
|
|
|
|
def _write_show_vert_border(self):
|
|
# Write the <c:showVertBorder> element.
|
|
attributes = [("val", 1)]
|
|
|
|
self._xml_empty_tag("c:showVertBorder", attributes)
|
|
|
|
def _write_show_outline(self):
|
|
# Write the <c:showOutline> element.
|
|
attributes = [("val", 1)]
|
|
|
|
self._xml_empty_tag("c:showOutline", attributes)
|
|
|
|
def _write_show_keys(self):
|
|
# Write the <c:showKeys> 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 <c:errBars> 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 <c:errDir> element.
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:errDir", attributes)
|
|
|
|
def _write_err_bar_type(self, val):
|
|
# Write the <c:errBarType> element.
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:errBarType", attributes)
|
|
|
|
def _write_err_val_type(self, val):
|
|
# Write the <c:errValType> element.
|
|
|
|
attributes = [("val", val)]
|
|
|
|
self._xml_empty_tag("c:errValType", attributes)
|
|
|
|
def _write_no_end_cap(self):
|
|
# Write the <c:noEndCap> element.
|
|
attributes = [("val", 1)]
|
|
|
|
self._xml_empty_tag("c:noEndCap", attributes)
|
|
|
|
def _write_error_val(self, val):
|
|
# Write the <c:val> 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 <c:numLit> 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 <c:upDownBars> 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 <c:gapWidth> 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 <c:upBars> 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 <c:downBars> 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 <c:dispUnits> 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 <a:gradFill> 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 <a:gsLst> 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 <a:lin> 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 <a:path> 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 <a:fillToRect> 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 <a:tileRect> 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 <a:pattFill> 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 <a:fgClr> 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 <a:bgClr> 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")
|