diff options
| author | S. Solomon Darnell | 2025-03-28 21:52:21 -0500 |
|---|---|---|
| committer | S. Solomon Darnell | 2025-03-28 21:52:21 -0500 |
| commit | 4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch) | |
| tree | ee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/pptx/chart | |
| parent | cc961e04ba734dd72309fb548a2f97d67d578813 (diff) | |
| download | gn-ai-master.tar.gz | |
Diffstat (limited to '.venv/lib/python3.12/site-packages/pptx/chart')
13 files changed, 5187 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/__init__.py b/.venv/lib/python3.12/site-packages/pptx/chart/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/chart/__init__.py diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/axis.py b/.venv/lib/python3.12/site-packages/pptx/chart/axis.py new file mode 100644 index 00000000..a9b87703 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/chart/axis.py @@ -0,0 +1,523 @@ +"""Axis-related chart objects.""" + +from __future__ import annotations + +from pptx.dml.chtfmt import ChartFormat +from pptx.enum.chart import ( + XL_AXIS_CROSSES, + XL_CATEGORY_TYPE, + XL_TICK_LABEL_POSITION, + XL_TICK_MARK, +) +from pptx.oxml.ns import qn +from pptx.oxml.simpletypes import ST_Orientation +from pptx.shared import ElementProxy +from pptx.text.text import Font, TextFrame +from pptx.util import lazyproperty + + +class _BaseAxis(object): + """Base class for chart axis objects. All axis objects share these properties.""" + + def __init__(self, xAx): + super(_BaseAxis, self).__init__() + self._element = xAx # axis element, c:catAx or c:valAx + self._xAx = xAx + + @property + def axis_title(self): + """An |AxisTitle| object providing access to title properties. + + Calling this property is destructive in the sense that it adds an + axis title element (`c:title`) to the axis XML if one is not already + present. Use :attr:`has_title` to test for presence of axis title + non-destructively. + """ + return AxisTitle(self._element.get_or_add_title()) + + @lazyproperty + def format(self): + """ + The |ChartFormat| object providing access to the shape formatting + properties of this axis, such as its line color and fill. + """ + return ChartFormat(self._element) + + @property + def has_major_gridlines(self): + """ + Read/write boolean value specifying whether this axis has gridlines + at its major tick mark locations. Assigning |True| to this property + causes major gridlines to be displayed. Assigning |False| causes them + to be removed. + """ + if self._element.majorGridlines is None: + return False + return True + + @has_major_gridlines.setter + def has_major_gridlines(self, value): + if bool(value) is True: + self._element.get_or_add_majorGridlines() + else: + self._element._remove_majorGridlines() + + @property + def has_minor_gridlines(self): + """ + Read/write boolean value specifying whether this axis has gridlines + at its minor tick mark locations. Assigning |True| to this property + causes minor gridlines to be displayed. Assigning |False| causes them + to be removed. + """ + if self._element.minorGridlines is None: + return False + return True + + @has_minor_gridlines.setter + def has_minor_gridlines(self, value): + if bool(value) is True: + self._element.get_or_add_minorGridlines() + else: + self._element._remove_minorGridlines() + + @property + def has_title(self): + """Read/write boolean specifying whether this axis has a title. + + |True| if this axis has a title, |False| otherwise. Assigning |True| + causes an axis title to be added if not already present. Assigning + |False| causes any existing title to be deleted. + """ + if self._element.title is None: + return False + return True + + @has_title.setter + def has_title(self, value): + if bool(value) is True: + self._element.get_or_add_title() + else: + self._element._remove_title() + + @lazyproperty + def major_gridlines(self): + """ + The |MajorGridlines| object representing the major gridlines for + this axis. + """ + return MajorGridlines(self._element) + + @property + def major_tick_mark(self): + """ + Read/write :ref:`XlTickMark` value specifying the type of major tick + mark to display on this axis. + """ + majorTickMark = self._element.majorTickMark + if majorTickMark is None: + return XL_TICK_MARK.CROSS + return majorTickMark.val + + @major_tick_mark.setter + def major_tick_mark(self, value): + self._element._remove_majorTickMark() + if value is XL_TICK_MARK.CROSS: + return + self._element._add_majorTickMark(val=value) + + @property + def maximum_scale(self): + """ + Read/write float value specifying the upper limit of the value range + for this axis, the number at the top or right of the vertical or + horizontal value scale, respectively. The value |None| indicates the + upper limit should be determined automatically based on the range of + data point values associated with the axis. + """ + return self._element.scaling.maximum + + @maximum_scale.setter + def maximum_scale(self, value): + scaling = self._element.scaling + scaling.maximum = value + + @property + def minimum_scale(self): + """ + Read/write float value specifying lower limit of value range, the + number at the bottom or left of the value scale. |None| if no minimum + scale has been set. The value |None| indicates the lower limit should + be determined automatically based on the range of data point values + associated with the axis. + """ + return self._element.scaling.minimum + + @minimum_scale.setter + def minimum_scale(self, value): + scaling = self._element.scaling + scaling.minimum = value + + @property + def minor_tick_mark(self): + """ + Read/write :ref:`XlTickMark` value specifying the type of minor tick + mark for this axis. + """ + minorTickMark = self._element.minorTickMark + if minorTickMark is None: + return XL_TICK_MARK.CROSS + return minorTickMark.val + + @minor_tick_mark.setter + def minor_tick_mark(self, value): + self._element._remove_minorTickMark() + if value is XL_TICK_MARK.CROSS: + return + self._element._add_minorTickMark(val=value) + + @property + def reverse_order(self): + """Read/write bool value specifying whether to reverse plotting order for axis. + + For a category axis, this reverses the order in which the categories are + displayed. This may be desired, for example, on a (horizontal) bar-chart where + by default the first category appears at the bottom. Since we read from + top-to-bottom, many viewers may find it most natural for the first category to + appear on top. + + For a value axis, it reverses the direction of increasing value from + bottom-to-top to top-to-bottom. + """ + return self._element.orientation == ST_Orientation.MAX_MIN + + @reverse_order.setter + def reverse_order(self, value): + self._element.orientation = ( + ST_Orientation.MAX_MIN if bool(value) is True else ST_Orientation.MIN_MAX + ) + + @lazyproperty + def tick_labels(self): + """ + The |TickLabels| instance providing access to axis tick label + formatting properties. Tick labels are the numbers appearing on + a value axis or the category names appearing on a category axis. + """ + return TickLabels(self._element) + + @property + def tick_label_position(self): + """ + Read/write :ref:`XlTickLabelPosition` value specifying where the tick + labels for this axis should appear. + """ + tickLblPos = self._element.tickLblPos + if tickLblPos is None: + return XL_TICK_LABEL_POSITION.NEXT_TO_AXIS + if tickLblPos.val is None: + return XL_TICK_LABEL_POSITION.NEXT_TO_AXIS + return tickLblPos.val + + @tick_label_position.setter + def tick_label_position(self, value): + tickLblPos = self._element.get_or_add_tickLblPos() + tickLblPos.val = value + + @property + def visible(self): + """ + Read/write. |True| if axis is visible, |False| otherwise. + """ + delete = self._element.delete_ + if delete is None: + return False + return False if delete.val else True + + @visible.setter + def visible(self, value): + if value not in (True, False): + raise ValueError("assigned value must be True or False, got: %s" % value) + delete = self._element.get_or_add_delete_() + delete.val = not value + + +class AxisTitle(ElementProxy): + """Provides properties for manipulating axis title.""" + + def __init__(self, title): + super(AxisTitle, self).__init__(title) + self._title = title + + @lazyproperty + def format(self): + """|ChartFormat| object providing access to shape formatting. + + Return the |ChartFormat| object providing shape formatting properties + for this axis title, such as its line color and fill. + """ + return ChartFormat(self._element) + + @property + def has_text_frame(self): + """Read/write Boolean specifying presence of a text frame. + + Return |True| if this axis title has a text frame, and |False| + otherwise. Assigning |True| causes a text frame to be added if not + already present. Assigning |False| causes any existing text frame to + be removed along with any text contained in the text frame. + """ + if self._title.tx_rich is None: + return False + return True + + @has_text_frame.setter + def has_text_frame(self, value): + if bool(value) is True: + self._title.get_or_add_tx_rich() + else: + self._title._remove_tx() + + @property + def text_frame(self): + """|TextFrame| instance for this axis title. + + Return a |TextFrame| instance allowing read/write access to the text + of this axis title and its text formatting properties. Accessing this + property is destructive as it adds a new text frame if not already + present. + """ + rich = self._title.get_or_add_tx_rich() + return TextFrame(rich, self) + + +class CategoryAxis(_BaseAxis): + """A category axis of a chart.""" + + @property + def category_type(self): + """ + A member of :ref:`XlCategoryType` specifying the scale type of this + axis. Unconditionally ``CATEGORY_SCALE`` for a |CategoryAxis| object. + """ + return XL_CATEGORY_TYPE.CATEGORY_SCALE + + +class DateAxis(_BaseAxis): + """A category axis with dates as its category labels. + + This axis-type has some special display behaviors such as making length of equal + periods equal and normalizing month start dates despite unequal month lengths. + """ + + @property + def category_type(self): + """ + A member of :ref:`XlCategoryType` specifying the scale type of this + axis. Unconditionally ``TIME_SCALE`` for a |DateAxis| object. + """ + return XL_CATEGORY_TYPE.TIME_SCALE + + +class MajorGridlines(ElementProxy): + """Provides access to the properties of the major gridlines appearing on an axis.""" + + def __init__(self, xAx): + super(MajorGridlines, self).__init__(xAx) + self._xAx = xAx # axis element, catAx or valAx + + @lazyproperty + def format(self): + """ + The |ChartFormat| object providing access to the shape formatting + properties of this data point, such as line and fill. + """ + majorGridlines = self._xAx.get_or_add_majorGridlines() + return ChartFormat(majorGridlines) + + +class TickLabels(object): + """A service class providing access to formatting of axis tick mark labels.""" + + def __init__(self, xAx_elm): + super(TickLabels, self).__init__() + self._element = xAx_elm + + @lazyproperty + def font(self): + """ + The |Font| object that provides access to the text properties for + these tick labels, such as bold, italic, etc. + """ + defRPr = self._element.defRPr + font = Font(defRPr) + return font + + @property + def number_format(self): + """ + Read/write string (e.g. "$#,##0.00") specifying the format for the + numbers on this axis. The syntax for these strings is the same as it + appears in the PowerPoint or Excel UI. Returns 'General' if no number + format has been set. Note that this format string has no effect on + rendered tick labels when :meth:`number_format_is_linked` is |True|. + Assigning a format string to this property automatically sets + :meth:`number_format_is_linked` to |False|. + """ + numFmt = self._element.numFmt + if numFmt is None: + return "General" + return numFmt.formatCode + + @number_format.setter + def number_format(self, value): + numFmt = self._element.get_or_add_numFmt() + numFmt.formatCode = value + self.number_format_is_linked = False + + @property + def number_format_is_linked(self): + """ + Read/write boolean specifying whether number formatting should be + taken from the source spreadsheet rather than the value of + :meth:`number_format`. + """ + numFmt = self._element.numFmt + if numFmt is None: + return False + souceLinked = numFmt.sourceLinked + if souceLinked is None: + return True + return numFmt.sourceLinked + + @number_format_is_linked.setter + def number_format_is_linked(self, value): + numFmt = self._element.get_or_add_numFmt() + numFmt.sourceLinked = value + + @property + def offset(self): + """ + Read/write int value in range 0-1000 specifying the spacing between + the tick mark labels and the axis as a percentange of the default + value. 100 if no label offset setting is present. + """ + lblOffset = self._element.lblOffset + if lblOffset is None: + return 100 + return lblOffset.val + + @offset.setter + def offset(self, value): + if self._element.tag != qn("c:catAx"): + raise ValueError("only a category axis has an offset") + self._element._remove_lblOffset() + if value == 100: + return + lblOffset = self._element._add_lblOffset() + lblOffset.val = value + + +class ValueAxis(_BaseAxis): + """An axis having continuous (as opposed to discrete) values. + + The vertical axis is generally a value axis, however both axes of an XY-type chart + are value axes. + """ + + @property + def crosses(self): + """ + Member of :ref:`XlAxisCrosses` enumeration specifying the point on + this axis where the other axis crosses, such as auto/zero, minimum, + or maximum. Returns `XL_AXIS_CROSSES.CUSTOM` when a specific numeric + crossing point (e.g. 1.5) is defined. + """ + crosses = self._cross_xAx.crosses + if crosses is None: + return XL_AXIS_CROSSES.CUSTOM + return crosses.val + + @crosses.setter + def crosses(self, value): + cross_xAx = self._cross_xAx + if value == XL_AXIS_CROSSES.CUSTOM: + if cross_xAx.crossesAt is not None: + return + cross_xAx._remove_crosses() + cross_xAx._remove_crossesAt() + if value == XL_AXIS_CROSSES.CUSTOM: + cross_xAx._add_crossesAt(val=0.0) + else: + cross_xAx._add_crosses(val=value) + + @property + def crosses_at(self): + """ + Numeric value on this axis at which the perpendicular axis crosses. + Returns |None| if no crossing value is set. + """ + crossesAt = self._cross_xAx.crossesAt + if crossesAt is None: + return None + return crossesAt.val + + @crosses_at.setter + def crosses_at(self, value): + cross_xAx = self._cross_xAx + cross_xAx._remove_crosses() + cross_xAx._remove_crossesAt() + if value is None: + return + cross_xAx._add_crossesAt(val=value) + + @property + def major_unit(self): + """ + The float number of units between major tick marks on this value + axis. |None| corresponds to the 'Auto' setting in the UI, and + specifies the value should be calculated by PowerPoint based on the + underlying chart data. + """ + majorUnit = self._element.majorUnit + if majorUnit is None: + return None + return majorUnit.val + + @major_unit.setter + def major_unit(self, value): + self._element._remove_majorUnit() + if value is None: + return + self._element._add_majorUnit(val=value) + + @property + def minor_unit(self): + """ + The float number of units between minor tick marks on this value + axis. |None| corresponds to the 'Auto' setting in the UI, and + specifies the value should be calculated by PowerPoint based on the + underlying chart data. + """ + minorUnit = self._element.minorUnit + if minorUnit is None: + return None + return minorUnit.val + + @minor_unit.setter + def minor_unit(self, value): + self._element._remove_minorUnit() + if value is None: + return + self._element._add_minorUnit(val=value) + + @property + def _cross_xAx(self): + """ + The axis element in the same group (primary/secondary) that crosses + this axis. + """ + crossAx_id = self._element.crossAx.val + expr = '(../c:catAx | ../c:valAx | ../c:dateAx)/c:axId[@val="%d"]' % crossAx_id + cross_axId = self._element.xpath(expr)[0] + return cross_axId.getparent() diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/category.py b/.venv/lib/python3.12/site-packages/pptx/chart/category.py new file mode 100644 index 00000000..2c28aff5 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/chart/category.py @@ -0,0 +1,200 @@ +"""Category-related objects. + +The |category.Categories| object is returned by ``Plot.categories`` and contains zero or +more |category.Category| objects, each representing one of the category labels +associated with the plot. Categories can be hierarchical, so there are members allowing +discovery of the depth of that hierarchy and providing means to navigate it. +""" + +from __future__ import annotations + +from collections.abc import Sequence + + +class Categories(Sequence): + """ + A sequence of |category.Category| objects, each representing a category + label on the chart. Provides properties for dealing with hierarchical + categories. + """ + + def __init__(self, xChart): + super(Categories, self).__init__() + self._xChart = xChart + + def __getitem__(self, idx): + pt = self._xChart.cat_pts[idx] + return Category(pt, idx) + + def __iter__(self): + cat_pts = self._xChart.cat_pts + for idx, pt in enumerate(cat_pts): + yield Category(pt, idx) + + def __len__(self): + # a category can be "null", meaning the Excel cell for it is empty. + # In this case, there is no c:pt element for it. The "empty" category + # will, however, be accounted for in c:cat//c:ptCount/@val, which + # reflects the true length of the categories collection. + return self._xChart.cat_pt_count + + @property + def depth(self): + """ + Return an integer representing the number of hierarchical levels in + this category collection. Returns 1 for non-hierarchical categories + and 0 if no categories are present (generally meaning no series are + present). + """ + cat = self._xChart.cat + if cat is None: + return 0 + if cat.multiLvlStrRef is None: + return 1 + return len(cat.lvls) + + @property + def flattened_labels(self): + """ + Return a sequence of tuples, each containing the flattened hierarchy + of category labels for a leaf category. Each tuple is in parent -> + child order, e.g. ``('US', 'CA', 'San Francisco')``, with the leaf + category appearing last. If this categories collection is + non-hierarchical, each tuple will contain only a leaf category label. + If the plot has no series (and therefore no categories), an empty + tuple is returned. + """ + cat = self._xChart.cat + if cat is None: + return () + + if cat.multiLvlStrRef is None: + return tuple([(category.label,) for category in self]) + + return tuple( + [ + tuple([category.label for category in reversed(flat_cat)]) + for flat_cat in self._iter_flattened_categories() + ] + ) + + @property + def levels(self): + """ + Return a sequence of |CategoryLevel| objects representing the + hierarchy of this category collection. The sequence is empty when the + category collection is not hierarchical, that is, contains only + leaf-level categories. The levels are ordered from the leaf level to + the root level; so the first level will contain the same categories + as this category collection. + """ + cat = self._xChart.cat + if cat is None: + return [] + return [CategoryLevel(lvl) for lvl in cat.lvls] + + def _iter_flattened_categories(self): + """ + Generate a ``tuple`` object for each leaf category in this + collection, containing the leaf category followed by its "parent" + categories, e.g. ``('San Francisco', 'CA', 'USA'). Each tuple will be + the same length as the number of levels (excepting certain edge + cases which I believe always indicate a chart construction error). + """ + levels = self.levels + if not levels: + return + leaf_level, remaining_levels = levels[0], levels[1:] + for category in leaf_level: + yield self._parentage((category,), remaining_levels) + + def _parentage(self, categories, levels): + """ + Return a tuple formed by recursively concatenating *categories* with + its next ancestor from *levels*. The idx value of the first category + in *categories* determines parentage in all levels. The returned + sequence is in child -> parent order. A parent category is the + Category object in a next level having the maximum idx value not + exceeding that of the leaf category. + """ + # exhausting levels is the expected recursion termination condition + if not levels: + return tuple(categories) + + # guard against edge case where next level is present but empty. That + # situation is not prohibited for some reason. + if not levels[0]: + return tuple(categories) + + parent_level, remaining_levels = levels[0], levels[1:] + leaf_node = categories[0] + + # Make the first parent the default. A possible edge case is where no + # parent is defined for one or more leading values, e.g. idx > 0 for + # the first parent. + parent = parent_level[0] + for category in parent_level: + if category.idx > leaf_node.idx: + break + parent = category + + extended_categories = tuple(categories) + (parent,) + return self._parentage(extended_categories, remaining_levels) + + +class Category(str): + """ + An extension of `str` that provides the category label as its string + value, and additional attributes representing other aspects of the + category. + """ + + def __new__(cls, pt, *args): + category_label = "" if pt is None else pt.v.text + return str.__new__(cls, category_label) + + def __init__(self, pt, idx=None): + """ + *idx* is a required attribute of a c:pt element, but must be + specified when pt is None, as when a "placeholder" category is + created to represent a missing c:pt element. + """ + self._element = self._pt = pt + self._idx = idx + + @property + def idx(self): + """ + Return an integer representing the index reference of this category. + For a leaf node, the index identifies the category. For a parent (or + other ancestor) category, the index specifies the first leaf category + that ancestor encloses. + """ + if self._pt is None: + return self._idx + return self._pt.idx + + @property + def label(self): + """ + Return the label of this category as a string. + """ + return str(self) + + +class CategoryLevel(Sequence): + """ + A sequence of |category.Category| objects representing a single level in + a hierarchical category collection. This object is only used when the + categories are hierarchical, meaning they have more than one level and + higher level categories group those at lower levels. + """ + + def __init__(self, lvl): + self._element = self._lvl = lvl + + def __getitem__(self, offset): + return Category(self._lvl.pt_lst[offset]) + + def __len__(self): + return len(self._lvl.pt_lst) diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/chart.py b/.venv/lib/python3.12/site-packages/pptx/chart/chart.py new file mode 100644 index 00000000..d73aa933 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/chart/chart.py @@ -0,0 +1,280 @@ +"""Chart-related objects such as Chart and ChartTitle.""" + +from __future__ import annotations + +from collections.abc import Sequence + +from pptx.chart.axis import CategoryAxis, DateAxis, ValueAxis +from pptx.chart.legend import Legend +from pptx.chart.plot import PlotFactory, PlotTypeInspector +from pptx.chart.series import SeriesCollection +from pptx.chart.xmlwriter import SeriesXmlRewriterFactory +from pptx.dml.chtfmt import ChartFormat +from pptx.shared import ElementProxy, PartElementProxy +from pptx.text.text import Font, TextFrame +from pptx.util import lazyproperty + + +class Chart(PartElementProxy): + """A chart object.""" + + def __init__(self, chartSpace, chart_part): + super(Chart, self).__init__(chartSpace, chart_part) + self._chartSpace = chartSpace + + @property + def category_axis(self): + """ + The category axis of this chart. In the case of an XY or Bubble + chart, this is the X axis. Raises |ValueError| if no category + axis is defined (as is the case for a pie chart, for example). + """ + catAx_lst = self._chartSpace.catAx_lst + if catAx_lst: + return CategoryAxis(catAx_lst[0]) + + dateAx_lst = self._chartSpace.dateAx_lst + if dateAx_lst: + return DateAxis(dateAx_lst[0]) + + valAx_lst = self._chartSpace.valAx_lst + if valAx_lst: + return ValueAxis(valAx_lst[0]) + + raise ValueError("chart has no category axis") + + @property + def chart_style(self): + """ + Read/write integer index of chart style used to format this chart. + Range is from 1 to 48. Value is |None| if no explicit style has been + assigned, in which case the default chart style is used. Assigning + |None| causes any explicit setting to be removed. The integer index + corresponds to the style's position in the chart style gallery in the + PowerPoint UI. + """ + style = self._chartSpace.style + if style is None: + return None + return style.val + + @chart_style.setter + def chart_style(self, value): + self._chartSpace._remove_style() + if value is None: + return + self._chartSpace._add_style(val=value) + + @property + def chart_title(self): + """A |ChartTitle| object providing access to title properties. + + Calling this property is destructive in the sense it adds a chart + title element (`c:title`) to the chart XML if one is not already + present. Use :attr:`has_title` to test for presence of a chart title + non-destructively. + """ + return ChartTitle(self._element.get_or_add_title()) + + @property + def chart_type(self): + """Member of :ref:`XlChartType` enumeration specifying type of this chart. + + If the chart has two plots, for example, a line plot overlayed on a bar plot, + the type reported is for the first (back-most) plot. Read-only. + """ + first_plot = self.plots[0] + return PlotTypeInspector.chart_type(first_plot) + + @lazyproperty + def font(self): + """Font object controlling text format defaults for this chart.""" + defRPr = self._chartSpace.get_or_add_txPr().p_lst[0].get_or_add_pPr().get_or_add_defRPr() + return Font(defRPr) + + @property + def has_legend(self): + """ + Read/write boolean, |True| if the chart has a legend. Assigning + |True| causes a legend to be added to the chart if it doesn't already + have one. Assigning False removes any existing legend definition + along with any existing legend settings. + """ + return self._chartSpace.chart.has_legend + + @has_legend.setter + def has_legend(self, value): + self._chartSpace.chart.has_legend = bool(value) + + @property + def has_title(self): + """Read/write boolean, specifying whether this chart has a title. + + Assigning |True| causes a title to be added if not already present. + Assigning |False| removes any existing title along with its text and + settings. + """ + title = self._chartSpace.chart.title + if title is None: + return False + return True + + @has_title.setter + def has_title(self, value): + chart = self._chartSpace.chart + if bool(value) is False: + chart._remove_title() + autoTitleDeleted = chart.get_or_add_autoTitleDeleted() + autoTitleDeleted.val = True + return + chart.get_or_add_title() + + @property + def legend(self): + """ + A |Legend| object providing access to the properties of the legend + for this chart. + """ + legend_elm = self._chartSpace.chart.legend + if legend_elm is None: + return None + return Legend(legend_elm) + + @lazyproperty + def plots(self): + """ + The sequence of plots in this chart. A plot, called a *chart group* + in the Microsoft API, is a distinct sequence of one or more series + depicted in a particular charting type. For example, a chart having + a series plotted as a line overlaid on three series plotted as + columns would have two plots; the first corresponding to the three + column series and the second to the line series. Plots are sequenced + in the order drawn, i.e. back-most to front-most. Supports *len()*, + membership (e.g. ``p in plots``), iteration, slicing, and indexed + access (e.g. ``plot = plots[i]``). + """ + plotArea = self._chartSpace.chart.plotArea + return _Plots(plotArea, self) + + def replace_data(self, chart_data): + """ + Use the categories and series values in the |ChartData| object + *chart_data* to replace those in the XML and Excel worksheet for this + chart. + """ + rewriter = SeriesXmlRewriterFactory(self.chart_type, chart_data) + rewriter.replace_series_data(self._chartSpace) + self._workbook.update_from_xlsx_blob(chart_data.xlsx_blob) + + @lazyproperty + def series(self): + """ + A |SeriesCollection| object containing all the series in this + chart. When the chart has multiple plots, all the series for the + first plot appear before all those for the second, and so on. Series + within a plot have an explicit ordering and appear in that sequence. + """ + return SeriesCollection(self._chartSpace.plotArea) + + @property + def value_axis(self): + """ + The |ValueAxis| object providing access to properties of the value + axis of this chart. Raises |ValueError| if the chart has no value + axis. + """ + valAx_lst = self._chartSpace.valAx_lst + if not valAx_lst: + raise ValueError("chart has no value axis") + + idx = 1 if len(valAx_lst) > 1 else 0 + return ValueAxis(valAx_lst[idx]) + + @property + def _workbook(self): + """ + The |ChartWorkbook| object providing access to the Excel source data + for this chart. + """ + return self.part.chart_workbook + + +class ChartTitle(ElementProxy): + """Provides properties for manipulating a chart title.""" + + # This shares functionality with AxisTitle, which could be factored out + # into a base class, perhaps pptx.chart.shared.BaseTitle. I suspect they + # actually differ in certain fuller behaviors, but at present they're + # essentially identical. + + def __init__(self, title): + super(ChartTitle, self).__init__(title) + self._title = title + + @lazyproperty + def format(self): + """|ChartFormat| object providing access to line and fill formatting. + + Return the |ChartFormat| object providing shape formatting properties + for this chart title, such as its line color and fill. + """ + return ChartFormat(self._title) + + @property + def has_text_frame(self): + """Read/write Boolean specifying whether this title has a text frame. + + Return |True| if this chart title has a text frame, and |False| + otherwise. Assigning |True| causes a text frame to be added if not + already present. Assigning |False| causes any existing text frame to + be removed along with its text and formatting. + """ + if self._title.tx_rich is None: + return False + return True + + @has_text_frame.setter + def has_text_frame(self, value): + if bool(value) is False: + self._title._remove_tx() + return + self._title.get_or_add_tx_rich() + + @property + def text_frame(self): + """|TextFrame| instance for this chart title. + + Return a |TextFrame| instance allowing read/write access to the text + of this chart title and its text formatting properties. Accessing this + property is destructive in the sense it adds a text frame if one is + not present. Use :attr:`has_text_frame` to test for the presence of + a text frame non-destructively. + """ + rich = self._title.get_or_add_tx_rich() + return TextFrame(rich, self) + + +class _Plots(Sequence): + """ + The sequence of plots in a chart, such as a bar plot or a line plot. Most + charts have only a single plot. The concept is necessary when two chart + types are displayed in a single set of axes, like a bar plot with + a superimposed line plot. + """ + + def __init__(self, plotArea, chart): + super(_Plots, self).__init__() + self._plotArea = plotArea + self._chart = chart + + def __getitem__(self, index): + xCharts = self._plotArea.xCharts + if isinstance(index, slice): + plots = [PlotFactory(xChart, self._chart) for xChart in xCharts] + return plots[index] + else: + xChart = xCharts[index] + return PlotFactory(xChart, self._chart) + + def __len__(self): + return len(self._plotArea.xCharts) diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/data.py b/.venv/lib/python3.12/site-packages/pptx/chart/data.py new file mode 100644 index 00000000..ec6a61f3 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/chart/data.py @@ -0,0 +1,864 @@ +"""ChartData and related objects.""" + +from __future__ import annotations + +import datetime +from collections.abc import Sequence +from numbers import Number + +from pptx.chart.xlsx import ( + BubbleWorkbookWriter, + CategoryWorkbookWriter, + XyWorkbookWriter, +) +from pptx.chart.xmlwriter import ChartXmlWriter +from pptx.util import lazyproperty + + +class _BaseChartData(Sequence): + """Base class providing common members for chart data objects. + + A chart data object serves as a proxy for the chart data table that will be written to an + Excel worksheet; operating as a sequence of series as well as providing access to chart-level + attributes. A chart data object is used as a parameter in :meth:`shapes.add_chart` and + :meth:`Chart.replace_data`. The data structure varies between major chart categories such as + category charts and XY charts. + """ + + def __init__(self, number_format="General"): + super(_BaseChartData, self).__init__() + self._number_format = number_format + self._series = [] + + def __getitem__(self, index): + return self._series.__getitem__(index) + + def __len__(self): + return self._series.__len__() + + def append(self, series): + return self._series.append(series) + + def data_point_offset(self, series): + """ + The total integer number of data points appearing in the series of + this chart that are prior to *series* in this sequence. + """ + count = 0 + for this_series in self: + if series is this_series: + return count + count += len(this_series) + raise ValueError("series not in chart data object") + + @property + def number_format(self): + """ + The formatting template string, e.g. '#,##0.0', that determines how + X and Y values are formatted in this chart and in the Excel + spreadsheet. A number format specified on a series will override this + value for that series. Likewise, a distinct number format can be + specified for a particular data point within a series. + """ + return self._number_format + + def series_index(self, series): + """ + Return the integer index of *series* in this sequence. + """ + for idx, s in enumerate(self): + if series is s: + return idx + raise ValueError("series not in chart data object") + + def series_name_ref(self, series): + """ + Return the Excel worksheet reference to the cell containing the name + for *series*. + """ + return self._workbook_writer.series_name_ref(series) + + def x_values_ref(self, series): + """ + The Excel worksheet reference to the X values for *series* (not + including the column label). + """ + return self._workbook_writer.x_values_ref(series) + + @property + def xlsx_blob(self): + """ + Return a blob containing an Excel workbook file populated with the + contents of this chart data object. + """ + return self._workbook_writer.xlsx_blob + + def xml_bytes(self, chart_type): + """ + Return a blob containing the XML for a chart of *chart_type* + containing the series in this chart data object, as bytes suitable + for writing directly to a file. + """ + return self._xml(chart_type).encode("utf-8") + + def y_values_ref(self, series): + """ + The Excel worksheet reference to the Y values for *series* (not + including the column label). + """ + return self._workbook_writer.y_values_ref(series) + + @property + def _workbook_writer(self): + """ + The worksheet writer object to which layout and writing of the Excel + worksheet for this chart will be delegated. + """ + raise NotImplementedError("must be implemented by all subclasses") + + def _xml(self, chart_type): + """ + Return (as unicode text) the XML for a chart of *chart_type* + populated with the values in this chart data object. The XML is + a complete XML document, including an XML declaration specifying + UTF-8 encoding. + """ + return ChartXmlWriter(chart_type, self).xml + + +class _BaseSeriesData(Sequence): + """ + Base class providing common members for series data objects. A series + data object serves as proxy for a series data column in the Excel + worksheet. It operates as a sequence of data points, as well as providing + access to series-level attributes like the series label. + """ + + def __init__(self, chart_data, name, number_format): + self._chart_data = chart_data + self._name = name + self._number_format = number_format + self._data_points = [] + + def __getitem__(self, index): + return self._data_points.__getitem__(index) + + def __len__(self): + return self._data_points.__len__() + + def append(self, data_point): + return self._data_points.append(data_point) + + @property + def data_point_offset(self): + """ + The integer count of data points that appear in all chart series + prior to this one. + """ + return self._chart_data.data_point_offset(self) + + @property + def index(self): + """ + Zero-based integer indicating the sequence position of this series in + its chart. For example, the second of three series would return `1`. + """ + return self._chart_data.series_index(self) + + @property + def name(self): + """ + The name of this series, e.g. 'Series 1'. This name is used as the + column heading for the y-values of this series and may also appear in + the chart legend and perhaps other chart locations. + """ + return self._name if self._name is not None else "" + + @property + def name_ref(self): + """ + The Excel worksheet reference to the cell containing the name for + this series. + """ + return self._chart_data.series_name_ref(self) + + @property + def number_format(self): + """ + The formatting template string that determines how a number in this + series is formatted, both in the chart and in the Excel spreadsheet; + for example '#,##0.0'. If not specified for this series, it is + inherited from the parent chart data object. + """ + number_format = self._number_format + if number_format is None: + return self._chart_data.number_format + return number_format + + @property + def x_values(self): + """ + A sequence containing the X value of each datapoint in this series, + in data point order. + """ + return [dp.x for dp in self._data_points] + + @property + def x_values_ref(self): + """ + The Excel worksheet reference to the X values for this chart (not + including the column heading). + """ + return self._chart_data.x_values_ref(self) + + @property + def y_values(self): + """ + A sequence containing the Y value of each datapoint in this series, + in data point order. + """ + return [dp.y for dp in self._data_points] + + @property + def y_values_ref(self): + """ + The Excel worksheet reference to the Y values for this chart (not + including the column heading). + """ + return self._chart_data.y_values_ref(self) + + +class _BaseDataPoint(object): + """ + Base class providing common members for data point objects. + """ + + def __init__(self, series_data, number_format): + super(_BaseDataPoint, self).__init__() + self._series_data = series_data + self._number_format = number_format + + @property + def number_format(self): + """ + The formatting template string that determines how the value of this + data point is formatted, both in the chart and in the Excel + spreadsheet; for example '#,##0.0'. If not specified for this data + point, it is inherited from the parent series data object. + """ + number_format = self._number_format + if number_format is None: + return self._series_data.number_format + return number_format + + +class CategoryChartData(_BaseChartData): + """ + Accumulates data specifying the categories and series values for a chart + and acts as a proxy for the chart data table that will be written to an + Excel worksheet. Used as a parameter in :meth:`shapes.add_chart` and + :meth:`Chart.replace_data`. + + This object is suitable for use with category charts, i.e. all those + having a discrete set of label values (categories) as the range of their + independent variable (X-axis) values. Unlike the ChartData types for + charts supporting a continuous range of independent variable values (such + as XyChartData), CategoryChartData has a single collection of category + (X) values and each data point in its series specifies only the Y value. + The corresponding X value is inferred by its position in the sequence. + """ + + def add_category(self, label): + """ + Return a newly created |data.Category| object having *label* and + appended to the end of the category collection for this chart. + *label* can be a string, a number, a datetime.date, or + datetime.datetime object. All category labels in a chart must be the + same type. All category labels in a chart having multi-level + categories must be strings. + """ + return self.categories.add_category(label) + + def add_series(self, name, values=(), number_format=None): + """ + Add a series to this data set entitled *name* and having the data + points specified by *values*, an iterable of numeric values. + *number_format* specifies how the series values will be displayed, + and may be a string, e.g. '#,##0' corresponding to an Excel number + format. + """ + series_data = CategorySeriesData(self, name, number_format) + self.append(series_data) + for value in values: + series_data.add_data_point(value) + return series_data + + @property + def categories(self): + """|data.Categories| object providing access to category-object hierarchy. + + Assigning an iterable of category labels (strings, numbers, or dates) replaces + the |data.Categories| object with a new one containing a category for each label + in the sequence. + + Creating a chart from chart data having date categories will cause the chart to + have a |DateAxis| for its category axis. + """ + if not getattr(self, "_categories", False): + self._categories = Categories() + return self._categories + + @categories.setter + def categories(self, category_labels): + categories = Categories() + for label in category_labels: + categories.add_category(label) + self._categories = categories + + @property + def categories_ref(self): + """ + The Excel worksheet reference to the categories for this chart (not + including the column heading). + """ + return self._workbook_writer.categories_ref + + def values_ref(self, series): + """ + The Excel worksheet reference to the values for *series* (not + including the column heading). + """ + return self._workbook_writer.values_ref(series) + + @lazyproperty + def _workbook_writer(self): + """ + The worksheet writer object to which layout and writing of the Excel + worksheet for this chart will be delegated. + """ + return CategoryWorkbookWriter(self) + + +class Categories(Sequence): + """ + A sequence of |data.Category| objects, also having certain hierarchical + graph behaviors for support of multi-level (nested) categories. + """ + + def __init__(self): + super(Categories, self).__init__() + self._categories = [] + self._number_format = None + + def __getitem__(self, idx): + return self._categories.__getitem__(idx) + + def __len__(self): + """ + Return the count of the highest level of category in this sequence. + If it contains hierarchical (multi-level) categories, this number + will differ from :attr:`category_count`, which is the number of leaf + nodes. + """ + return self._categories.__len__() + + def add_category(self, label): + """ + Return a newly created |data.Category| object having *label* and + appended to the end of this category sequence. *label* can be + a string, a number, a datetime.date, or datetime.datetime object. All + category labels in a chart must be the same type. All category labels + in a chart having multi-level categories must be strings. + + Creating a chart from chart data having date categories will cause + the chart to have a |DateAxis| for its category axis. + """ + category = Category(label, self) + self._categories.append(category) + return category + + @property + def are_dates(self): + """ + Return |True| if the first category in this collection has a date + label (as opposed to str or numeric). A date label is one of type + datetime.date or datetime.datetime. Returns |False| otherwise, + including when this category collection is empty. It also returns + False when this category collection is hierarchical, because + hierarchical categories can only be written as string labels. + """ + if self.depth != 1: + return False + first_cat_label = self[0].label + date_types = (datetime.date, datetime.datetime) + if isinstance(first_cat_label, date_types): + return True + return False + + @property + def are_numeric(self): + """ + Return |True| if the first category in this collection has a numeric + label (as opposed to a string label), including if that value is + a datetime.date or datetime.datetime object (as those are converted + to integers for storage in Excel). Returns |False| otherwise, + including when this category collection is empty. It also returns + False when this category collection is hierarchical, because + hierarchical categories can only be written as string labels. + """ + if self.depth != 1: + return False + # This method only tests the first category. The categories must + # be of uniform type, and if they're not, there will be problems + # later in the process, but it's not this method's job to validate + # the caller's input. + first_cat_label = self[0].label + numeric_types = (Number, datetime.date, datetime.datetime) + if isinstance(first_cat_label, numeric_types): + return True + return False + + @property + def depth(self): + """ + The number of hierarchy levels in this category graph. Returns 0 if + it contains no categories. + """ + categories = self._categories + if not categories: + return 0 + first_depth = categories[0].depth + for category in categories[1:]: + if category.depth != first_depth: + raise ValueError("category depth not uniform") + return first_depth + + def index(self, category): + """ + The offset of *category* in the overall sequence of leaf categories. + A non-leaf category gets the index of its first sub-category. + """ + index = 0 + for this_category in self._categories: + if category is this_category: + return index + index += this_category.leaf_count + raise ValueError("category not in top-level categories") + + @property + def leaf_count(self): + """ + The number of leaf-level categories in this hierarchy. The return + value is the same as that of `len()` only when the hierarchy is + single level. + """ + return sum(c.leaf_count for c in self._categories) + + @property + def levels(self): + """ + A generator of (idx, label) sequences representing the category + hierarchy from the bottom up. The first level contains all leaf + categories, and each subsequent is the next level up. + """ + + def levels(categories): + # yield all lower levels + sub_categories = [sc for c in categories for sc in c.sub_categories] + if sub_categories: + for level in levels(sub_categories): + yield level + # yield this level + yield [(cat.idx, cat.label) for cat in categories] + + for level in levels(self): + yield level + + @property + def number_format(self): + """ + Read/write. Return a string representing the number format used in + Excel to format these category values, e.g. '0.0' or 'mm/dd/yyyy'. + This string is only relevant when the categories are numeric or date + type, although it returns 'General' without error when the categories + are string labels. Assigning |None| causes the default number format + to be used, based on the type of the category labels. + """ + GENERAL = "General" + + # defined value takes precedence + if self._number_format is not None: + return self._number_format + + # multi-level (should) always be string labels + # zero depth means empty in which case we can't tell anyway + if self.depth != 1: + return GENERAL + + # everything except dates gets 'General' + first_cat_label = self[0].label + if isinstance(first_cat_label, (datetime.date, datetime.datetime)): + return r"yyyy\-mm\-dd" + return GENERAL + + @number_format.setter + def number_format(self, value): + self._number_format = value + + +class Category(object): + """ + A chart category, primarily having a label to be displayed on the + category axis, but also able to be configured in a hierarchy for support + of multi-level category charts. + """ + + def __init__(self, label, parent): + super(Category, self).__init__() + self._label = label + self._parent = parent + self._sub_categories = [] + + def add_sub_category(self, label): + """ + Return a newly created |data.Category| object having *label* and + appended to the end of the sub-category sequence for this category. + """ + category = Category(label, self) + self._sub_categories.append(category) + return category + + @property + def depth(self): + """ + The number of hierarchy levels rooted at this category node. Returns + 1 if this category has no sub-categories. + """ + sub_categories = self._sub_categories + if not sub_categories: + return 1 + first_depth = sub_categories[0].depth + for category in sub_categories[1:]: + if category.depth != first_depth: + raise ValueError("category depth not uniform") + return first_depth + 1 + + @property + def idx(self): + """ + The offset of this category in the overall sequence of leaf + categories. A non-leaf category gets the index of its first + sub-category. + """ + return self._parent.index(self) + + def index(self, sub_category): + """ + The offset of *sub_category* in the overall sequence of leaf + categories. + """ + index = self._parent.index(self) + for this_sub_category in self._sub_categories: + if sub_category is this_sub_category: + return index + index += this_sub_category.leaf_count + raise ValueError("sub_category not in this category") + + @property + def leaf_count(self): + """ + The number of leaf category nodes under this category. Returns + 1 if this category has no sub-categories. + """ + if not self._sub_categories: + return 1 + return sum(category.leaf_count for category in self._sub_categories) + + @property + def label(self): + """ + The value that appears on the axis for this category. The label can + be a string, a number, or a datetime.date or datetime.datetime + object. + """ + return self._label if self._label is not None else "" + + def numeric_str_val(self, date_1904=False): + """ + The string representation of the numeric (or date) label of this + category, suitable for use in the XML `c:pt` element for this + category. The optional *date_1904* parameter specifies the epoch used + for calculating Excel date numbers. + """ + label = self._label + if isinstance(label, (datetime.date, datetime.datetime)): + return "%.1f" % self._excel_date_number(date_1904) + return str(self._label) + + @property + def sub_categories(self): + """ + The sequence of child categories for this category. + """ + return self._sub_categories + + def _excel_date_number(self, date_1904): + """ + Return an integer representing the date label of this category as the + number of days since January 1, 1900 (or 1904 if date_1904 is + |True|). + """ + date, label = datetime.date, self._label + # -- get date from label in type-independent-ish way + date_ = date(label.year, label.month, label.day) + epoch = date(1904, 1, 1) if date_1904 else date(1899, 12, 31) + delta = date_ - epoch + excel_day_number = delta.days + + # -- adjust for Excel mistaking 1900 for a leap year -- + if not date_1904 and excel_day_number > 59: + excel_day_number += 1 + + return excel_day_number + + +class ChartData(CategoryChartData): + """ + |ChartData| is simply an alias for |CategoryChartData| and may be removed + in a future release. All new development should use |CategoryChartData| + for creating or replacing the data in chart types other than XY and + Bubble. + """ + + +class CategorySeriesData(_BaseSeriesData): + """ + The data specific to a particular category chart series. It provides + access to the series label, the series data points, and an optional + number format to be applied to each data point not having a specified + number format. + """ + + def add_data_point(self, value, number_format=None): + """ + Return a CategoryDataPoint object newly created with value *value*, + an optional *number_format*, and appended to this sequence. + """ + data_point = CategoryDataPoint(self, value, number_format) + self.append(data_point) + return data_point + + @property + def categories(self): + """ + The |data.Categories| object that provides access to the category + objects for this series. + """ + return self._chart_data.categories + + @property + def categories_ref(self): + """ + The Excel worksheet reference to the categories for this chart (not + including the column heading). + """ + return self._chart_data.categories_ref + + @property + def values(self): + """ + A sequence containing the (Y) value of each datapoint in this series, + in data point order. + """ + return [dp.value for dp in self._data_points] + + @property + def values_ref(self): + """ + The Excel worksheet reference to the (Y) values for this series (not + including the column heading). + """ + return self._chart_data.values_ref(self) + + +class XyChartData(_BaseChartData): + """ + A specialized ChartData object suitable for use with an XY (aka. scatter) + chart. Unlike ChartData, it has no category sequence. Rather, each data + point of each series specifies both an X and a Y value. + """ + + def add_series(self, name, number_format=None): + """ + Return an |XySeriesData| object newly created and added at the end of + this sequence, identified by *name* and values formatted with + *number_format*. + """ + series_data = XySeriesData(self, name, number_format) + self.append(series_data) + return series_data + + @lazyproperty + def _workbook_writer(self): + """ + The worksheet writer object to which layout and writing of the Excel + worksheet for this chart will be delegated. + """ + return XyWorkbookWriter(self) + + +class BubbleChartData(XyChartData): + """ + A specialized ChartData object suitable for use with a bubble chart. + A bubble chart is essentially an XY chart where the markers are scaled to + provide a third quantitative dimension to the exhibit. + """ + + def add_series(self, name, number_format=None): + """ + Return a |BubbleSeriesData| object newly created and added at the end + of this sequence, and having series named *name* and values formatted + with *number_format*. + """ + series_data = BubbleSeriesData(self, name, number_format) + self.append(series_data) + return series_data + + def bubble_sizes_ref(self, series): + """ + The Excel worksheet reference for the range containing the bubble + sizes for *series*. + """ + return self._workbook_writer.bubble_sizes_ref(series) + + @lazyproperty + def _workbook_writer(self): + """ + The worksheet writer object to which layout and writing of the Excel + worksheet for this chart will be delegated. + """ + return BubbleWorkbookWriter(self) + + +class XySeriesData(_BaseSeriesData): + """ + The data specific to a particular XY chart series. It provides access to + the series label, the series data points, and an optional number format + to be applied to each data point not having a specified number format. + + The sequence of data points in an XY series is significant; lines are + plotted following the sequence of points, even if that causes a line + segment to "travel backward" (implying a multi-valued function). The data + points are not automatically sorted into increasing order by X value. + """ + + def add_data_point(self, x, y, number_format=None): + """ + Return an XyDataPoint object newly created with values *x* and *y*, + and appended to this sequence. + """ + data_point = XyDataPoint(self, x, y, number_format) + self.append(data_point) + return data_point + + +class BubbleSeriesData(XySeriesData): + """ + The data specific to a particular Bubble chart series. It provides access + to the series label, the series data points, and an optional number + format to be applied to each data point not having a specified number + format. + + The sequence of data points in a bubble chart series is maintained + throughout the chart building process because a data point has no unique + identifier and can only be retrieved by index. + """ + + def add_data_point(self, x, y, size, number_format=None): + """ + Append a new BubbleDataPoint object having the values *x*, *y*, and + *size*. The optional *number_format* is used to format the Y value. + If not provided, the number format is inherited from the series data. + """ + data_point = BubbleDataPoint(self, x, y, size, number_format) + self.append(data_point) + return data_point + + @property + def bubble_sizes(self): + """ + A sequence containing the bubble size for each datapoint in this + series, in data point order. + """ + return [dp.bubble_size for dp in self._data_points] + + @property + def bubble_sizes_ref(self): + """ + The Excel worksheet reference for the range containing the bubble + sizes for this series. + """ + return self._chart_data.bubble_sizes_ref(self) + + +class CategoryDataPoint(_BaseDataPoint): + """ + A data point in a category chart series. Provides access to the value of + the datapoint and the number format with which it should appear in the + Excel file. + """ + + def __init__(self, series_data, value, number_format): + super(CategoryDataPoint, self).__init__(series_data, number_format) + self._value = value + + @property + def value(self): + """ + The (Y) value for this category data point. + """ + return self._value + + +class XyDataPoint(_BaseDataPoint): + """ + A data point in an XY chart series. Provides access to the x and y values + of the datapoint. + """ + + def __init__(self, series_data, x, y, number_format): + super(XyDataPoint, self).__init__(series_data, number_format) + self._x = x + self._y = y + + @property + def x(self): + """ + The X value for this XY data point. + """ + return self._x + + @property + def y(self): + """ + The Y value for this XY data point. + """ + return self._y + + +class BubbleDataPoint(XyDataPoint): + """ + A data point in a bubble chart series. Provides access to the x, y, and + size values of the datapoint. + """ + + def __init__(self, series_data, x, y, size, number_format): + super(BubbleDataPoint, self).__init__(series_data, x, y, number_format) + self._size = size + + @property + def bubble_size(self): + """ + The value representing the size of the bubble for this data point. + """ + return self._size diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/datalabel.py b/.venv/lib/python3.12/site-packages/pptx/chart/datalabel.py new file mode 100644 index 00000000..af7cdf5c --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/chart/datalabel.py @@ -0,0 +1,288 @@ +"""Data label-related objects.""" + +from __future__ import annotations + +from pptx.text.text import Font, TextFrame +from pptx.util import lazyproperty + + +class DataLabels(object): + """Provides access to properties of data labels for a plot or a series. + + This is not a collection and does not provide access to individual data + labels. Access to individual labels is via the |Point| object. The + properties this object provides control formatting of *all* the data + labels in its scope. + """ + + def __init__(self, dLbls): + super(DataLabels, self).__init__() + self._element = dLbls + + @lazyproperty + def font(self): + """ + The |Font| object that provides access to the text properties for + these data labels, such as bold, italic, etc. + """ + defRPr = self._element.defRPr + font = Font(defRPr) + return font + + @property + def number_format(self): + """ + Read/write string specifying the format for the numbers on this set + of data labels. Returns 'General' if no number format has been set. + Note that this format string has no effect on rendered data labels + when :meth:`number_format_is_linked` is |True|. Assigning a format + string to this property automatically sets + :meth:`number_format_is_linked` to |False|. + """ + numFmt = self._element.numFmt + if numFmt is None: + return "General" + return numFmt.formatCode + + @number_format.setter + def number_format(self, value): + self._element.get_or_add_numFmt().formatCode = value + self.number_format_is_linked = False + + @property + def number_format_is_linked(self): + """ + Read/write boolean specifying whether number formatting should be + taken from the source spreadsheet rather than the value of + :meth:`number_format`. + """ + numFmt = self._element.numFmt + if numFmt is None: + return True + souceLinked = numFmt.sourceLinked + if souceLinked is None: + return True + return numFmt.sourceLinked + + @number_format_is_linked.setter + def number_format_is_linked(self, value): + numFmt = self._element.get_or_add_numFmt() + numFmt.sourceLinked = value + + @property + def position(self): + """ + Read/write :ref:`XlDataLabelPosition` enumeration value specifying + the position of the data labels with respect to their data point, or + |None| if no position is specified. Assigning |None| causes + PowerPoint to choose the default position, which varies by chart + type. + """ + dLblPos = self._element.dLblPos + if dLblPos is None: + return None + return dLblPos.val + + @position.setter + def position(self, value): + if value is None: + self._element._remove_dLblPos() + return + self._element.get_or_add_dLblPos().val = value + + @property + def show_category_name(self): + """Read/write. True when name of category should appear in label.""" + return self._element.get_or_add_showCatName().val + + @show_category_name.setter + def show_category_name(self, value): + self._element.get_or_add_showCatName().val = bool(value) + + @property + def show_legend_key(self): + """Read/write. True when data label displays legend-color swatch.""" + return self._element.get_or_add_showLegendKey().val + + @show_legend_key.setter + def show_legend_key(self, value): + self._element.get_or_add_showLegendKey().val = bool(value) + + @property + def show_percentage(self): + """Read/write. True when data label displays percentage. + + This option is not operative on all chart types. Percentage appears + on polar charts such as pie and donut. + """ + return self._element.get_or_add_showPercent().val + + @show_percentage.setter + def show_percentage(self, value): + self._element.get_or_add_showPercent().val = bool(value) + + @property + def show_series_name(self): + """Read/write. True when data label displays series name.""" + return self._element.get_or_add_showSerName().val + + @show_series_name.setter + def show_series_name(self, value): + self._element.get_or_add_showSerName().val = bool(value) + + @property + def show_value(self): + """Read/write. True when label displays numeric value of datapoint.""" + return self._element.get_or_add_showVal().val + + @show_value.setter + def show_value(self, value): + self._element.get_or_add_showVal().val = bool(value) + + +class DataLabel(object): + """ + The data label associated with an individual data point. + """ + + def __init__(self, ser, idx): + super(DataLabel, self).__init__() + self._ser = self._element = ser + self._idx = idx + + @lazyproperty + def font(self): + """The |Font| object providing text formatting for this data label. + + This font object is used to customize the appearance of automatically + inserted text, such as the data point value. The font applies to the + entire data label. More granular control of the appearance of custom + data label text is controlled by a font object on runs in the text + frame. + """ + txPr = self._get_or_add_txPr() + text_frame = TextFrame(txPr, self) + paragraph = text_frame.paragraphs[0] + return paragraph.font + + @property + def has_text_frame(self): + """ + Return |True| if this data label has a text frame (implying it has + custom data label text), and |False| otherwise. Assigning |True| + causes a text frame to be added if not already present. Assigning + |False| causes any existing text frame to be removed along with any + text contained in the text frame. + """ + dLbl = self._dLbl + if dLbl is None: + return False + if dLbl.xpath("c:tx/c:rich"): + return True + return False + + @has_text_frame.setter + def has_text_frame(self, value): + if bool(value) is True: + self._get_or_add_tx_rich() + else: + self._remove_tx_rich() + + @property + def position(self): + """ + Read/write :ref:`XlDataLabelPosition` member specifying the position + of this data label with respect to its data point, or |None| if no + position is specified. Assigning |None| causes PowerPoint to choose + the default position, which varies by chart type. + """ + dLbl = self._dLbl + if dLbl is None: + return None + dLblPos = dLbl.dLblPos + if dLblPos is None: + return None + return dLblPos.val + + @position.setter + def position(self, value): + if value is None: + dLbl = self._dLbl + if dLbl is None: + return + dLbl._remove_dLblPos() + return + dLbl = self._get_or_add_dLbl() + dLbl.get_or_add_dLblPos().val = value + + @property + def text_frame(self): + """ + |TextFrame| instance for this data label, containing the text of the + data label and providing access to its text formatting properties. + """ + rich = self._get_or_add_rich() + return TextFrame(rich, self) + + @property + def _dLbl(self): + """ + Return the |CT_DLbl| instance referring specifically to this + individual data label (having the same index value), or |None| if not + present. + """ + return self._ser.get_dLbl(self._idx) + + def _get_or_add_dLbl(self): + """ + The ``CT_DLbl`` instance referring specifically to this individual + data label, newly created if not yet present in the XML. + """ + return self._ser.get_or_add_dLbl(self._idx) + + def _get_or_add_rich(self): + """ + Return the `c:rich` element representing the text frame for this data + label, newly created with its ancestors if not present. + """ + dLbl = self._get_or_add_dLbl() + + # having a c:spPr or c:txPr when a c:tx is present causes the "can't + # save" bug on bubble charts. Remove c:spPr and c:txPr when present. + dLbl._remove_spPr() + dLbl._remove_txPr() + + return dLbl.get_or_add_rich() + + def _get_or_add_tx_rich(self): + """ + Return the `c:tx` element for this data label, with its `c:rich` + child and descendants, newly created if not yet present. + """ + dLbl = self._get_or_add_dLbl() + + # having a c:spPr or c:txPr when a c:tx is present causes the "can't + # save" bug on bubble charts. Remove c:spPr and c:txPr when present. + dLbl._remove_spPr() + dLbl._remove_txPr() + + return dLbl.get_or_add_tx_rich() + + def _get_or_add_txPr(self): + """Return the `c:txPr` element for this data label. + + The `c:txPr` element and its parent `c:dLbl` element are created if + not yet present. + """ + dLbl = self._get_or_add_dLbl() + return dLbl.get_or_add_txPr() + + def _remove_tx_rich(self): + """ + Remove any `c:tx/c:rich` child of the `c:dLbl` element for this data + label. Do nothing if that element is not present. + """ + dLbl = self._dLbl + if dLbl is None: + return + dLbl.remove_tx_rich() diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/legend.py b/.venv/lib/python3.12/site-packages/pptx/chart/legend.py new file mode 100644 index 00000000..9bc64dbf --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/chart/legend.py @@ -0,0 +1,79 @@ +"""Legend of a chart.""" + +from __future__ import annotations + +from pptx.enum.chart import XL_LEGEND_POSITION +from pptx.text.text import Font +from pptx.util import lazyproperty + + +class Legend(object): + """ + Represents the legend in a chart. A chart can have at most one legend. + """ + + def __init__(self, legend_elm): + super(Legend, self).__init__() + self._element = legend_elm + + @lazyproperty + def font(self): + """ + The |Font| object that provides access to the text properties for + this legend, such as bold, italic, etc. + """ + defRPr = self._element.defRPr + font = Font(defRPr) + return font + + @property + def horz_offset(self): + """ + Adjustment of the x position of the legend from its default. + Expressed as a float between -1.0 and 1.0 representing a fraction of + the chart width. Negative values move the legend left, positive + values move it to the right. |None| if no setting is specified. + """ + return self._element.horz_offset + + @horz_offset.setter + def horz_offset(self, value): + self._element.horz_offset = value + + @property + def include_in_layout(self): + """|True| if legend should be located inside plot area. + + Read/write boolean specifying whether legend should be placed inside + the plot area. In many cases this will cause it to be superimposed on + the chart itself. Assigning |None| to this property causes any + `c:overlay` element to be removed, which is interpreted the same as + |True|. This use case should rarely be required and assigning + a boolean value is recommended. + """ + overlay = self._element.overlay + if overlay is None: + return True + return overlay.val + + @include_in_layout.setter + def include_in_layout(self, value): + if value is None: + self._element._remove_overlay() + return + self._element.get_or_add_overlay().val = bool(value) + + @property + def position(self): + """ + Read/write :ref:`XlLegendPosition` enumeration value specifying the + general region of the chart in which to place the legend. + """ + legendPos = self._element.legendPos + if legendPos is None: + return XL_LEGEND_POSITION.RIGHT + return legendPos.val + + @position.setter + def position(self, position): + self._element.get_or_add_legendPos().val = position diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/marker.py b/.venv/lib/python3.12/site-packages/pptx/chart/marker.py new file mode 100644 index 00000000..cd4b7f02 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/chart/marker.py @@ -0,0 +1,70 @@ +"""Marker-related objects. + +Only the line-type charts Line, XY, and Radar have markers. +""" + +from __future__ import annotations + +from pptx.dml.chtfmt import ChartFormat +from pptx.shared import ElementProxy +from pptx.util import lazyproperty + + +class Marker(ElementProxy): + """ + Represents a data point marker, such as a diamond or circle, on + a line-type chart. + """ + + @lazyproperty + def format(self): + """ + The |ChartFormat| instance for this marker, providing access to shape + properties such as fill and line. + """ + marker = self._element.get_or_add_marker() + return ChartFormat(marker) + + @property + def size(self): + """ + An integer between 2 and 72 inclusive indicating the size of this + marker in points. A value of |None| indicates no explicit value is + set and the size is inherited from a higher-level setting or the + PowerPoint default (which may be 9). Assigning |None| removes any + explicitly assigned size, causing this value to be inherited. + """ + marker = self._element.marker + if marker is None: + return None + return marker.size_val + + @size.setter + def size(self, value): + marker = self._element.get_or_add_marker() + marker._remove_size() + if value is None: + return + size = marker._add_size() + size.val = value + + @property + def style(self): + """ + A member of the :ref:`XlMarkerStyle` enumeration indicating the shape + of this marker. Returns |None| if no explicit style has been set, + which corresponds to the "Automatic" option in the PowerPoint UI. + """ + marker = self._element.marker + if marker is None: + return None + return marker.symbol_val + + @style.setter + def style(self, value): + marker = self._element.get_or_add_marker() + marker._remove_symbol() + if value is None: + return + symbol = marker._add_symbol() + symbol.val = value diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/plot.py b/.venv/lib/python3.12/site-packages/pptx/chart/plot.py new file mode 100644 index 00000000..6e723585 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/chart/plot.py @@ -0,0 +1,412 @@ +"""Plot-related objects. + +A plot is known as a chart group in the MS API. A chart can have more than one plot overlayed on +each other, such as a line plot layered over a bar plot. +""" + +from __future__ import annotations + +from pptx.chart.category import Categories +from pptx.chart.datalabel import DataLabels +from pptx.chart.series import SeriesCollection +from pptx.enum.chart import XL_CHART_TYPE as XL +from pptx.oxml.ns import qn +from pptx.oxml.simpletypes import ST_BarDir, ST_Grouping +from pptx.util import lazyproperty + + +class _BasePlot(object): + """ + A distinct plot that appears in the plot area of a chart. A chart may + have more than one plot, in which case they appear as superimposed + layers, such as a line plot appearing on top of a bar chart. + """ + + def __init__(self, xChart, chart): + super(_BasePlot, self).__init__() + self._element = xChart + self._chart = chart + + @lazyproperty + def categories(self): + """ + Returns a |category.Categories| sequence object containing + a |category.Category| object for each of the category labels + associated with this plot. The |category.Category| class derives from + ``str``, so the returned value can be treated as a simple sequence of + strings for the common case where all you need is the labels in the + order they appear on the chart. |category.Categories| provides + additional properties for dealing with hierarchical categories when + required. + """ + return Categories(self._element) + + @property + def chart(self): + """ + The |Chart| object containing this plot. + """ + return self._chart + + @property + def data_labels(self): + """ + |DataLabels| instance providing properties and methods on the + collection of data labels associated with this plot. + """ + dLbls = self._element.dLbls + if dLbls is None: + raise ValueError("plot has no data labels, set has_data_labels = True first") + return DataLabels(dLbls) + + @property + def has_data_labels(self): + """ + Read/write boolean, |True| if the series has data labels. Assigning + |True| causes data labels to be added to the plot. Assigning False + removes any existing data labels. + """ + return self._element.dLbls is not None + + @has_data_labels.setter + def has_data_labels(self, value): + """ + Add, remove, or leave alone the ``<c:dLbls>`` child element depending + on current state and assigned *value*. If *value* is |True| and no + ``<c:dLbls>`` element is present, a new default element is added with + default child elements and settings. When |False|, any existing dLbls + element is removed. + """ + if bool(value) is False: + self._element._remove_dLbls() + else: + if self._element.dLbls is None: + dLbls = self._element._add_dLbls() + dLbls.showVal.val = True + + @lazyproperty + def series(self): + """ + A sequence of |Series| objects representing the series in this plot, + in the order they appear in the plot. + """ + return SeriesCollection(self._element) + + @property + def vary_by_categories(self): + """ + Read/write boolean value specifying whether to use a different color + for each of the points in this plot. Only effective when there is + a single series; PowerPoint automatically varies color by series when + more than one series is present. + """ + varyColors = self._element.varyColors + if varyColors is None: + return True + return varyColors.val + + @vary_by_categories.setter + def vary_by_categories(self, value): + self._element.get_or_add_varyColors().val = bool(value) + + +class AreaPlot(_BasePlot): + """ + An area plot. + """ + + +class Area3DPlot(_BasePlot): + """ + A 3-dimensional area plot. + """ + + +class BarPlot(_BasePlot): + """ + A bar chart-style plot. + """ + + @property + def gap_width(self): + """ + Width of gap between bar(s) of each category, as an integer + percentage of the bar width. The default value for a new bar chart is + 150, representing 150% or 1.5 times the width of a single bar. + """ + gapWidth = self._element.gapWidth + if gapWidth is None: + return 150 + return gapWidth.val + + @gap_width.setter + def gap_width(self, value): + gapWidth = self._element.get_or_add_gapWidth() + gapWidth.val = value + + @property + def overlap(self): + """ + Read/write int value in range -100..100 specifying a percentage of + the bar width by which to overlap adjacent bars in a multi-series bar + chart. Default is 0. A setting of -100 creates a gap of a full bar + width and a setting of 100 causes all the bars in a category to be + superimposed. A stacked bar plot has overlap of 100 by default. + """ + overlap = self._element.overlap + if overlap is None: + return 0 + return overlap.val + + @overlap.setter + def overlap(self, value): + """ + Set the value of the ``<c:overlap>`` child element to *int_value*, + or remove the overlap element if *int_value* is 0. + """ + if value == 0: + self._element._remove_overlap() + return + self._element.get_or_add_overlap().val = value + + +class BubblePlot(_BasePlot): + """ + A bubble chart plot. + """ + + @property + def bubble_scale(self): + """ + An integer between 0 and 300 inclusive indicating the percentage of + the default size at which bubbles should be displayed. Assigning + |None| produces the same behavior as assigning `100`. + """ + bubbleScale = self._element.bubbleScale + if bubbleScale is None: + return 100 + return bubbleScale.val + + @bubble_scale.setter + def bubble_scale(self, value): + bubbleChart = self._element + bubbleChart._remove_bubbleScale() + if value is None: + return + bubbleScale = bubbleChart._add_bubbleScale() + bubbleScale.val = value + + +class DoughnutPlot(_BasePlot): + """ + An doughnut plot. + """ + + +class LinePlot(_BasePlot): + """ + A line chart-style plot. + """ + + +class PiePlot(_BasePlot): + """ + A pie chart-style plot. + """ + + +class RadarPlot(_BasePlot): + """ + A radar-style plot. + """ + + +class XyPlot(_BasePlot): + """ + An XY (scatter) plot. + """ + + +def PlotFactory(xChart, chart): + """ + Return an instance of the appropriate subclass of _BasePlot based on the + tagname of *xChart*. + """ + try: + PlotCls = { + qn("c:areaChart"): AreaPlot, + qn("c:area3DChart"): Area3DPlot, + qn("c:barChart"): BarPlot, + qn("c:bubbleChart"): BubblePlot, + qn("c:doughnutChart"): DoughnutPlot, + qn("c:lineChart"): LinePlot, + qn("c:pieChart"): PiePlot, + qn("c:radarChart"): RadarPlot, + qn("c:scatterChart"): XyPlot, + }[xChart.tag] + except KeyError: + raise ValueError("unsupported plot type %s" % xChart.tag) + + return PlotCls(xChart, chart) + + +class PlotTypeInspector(object): + """ + "One-shot" service object that knows how to identify the type of a plot + as a member of the XL_CHART_TYPE enumeration. + """ + + @classmethod + def chart_type(cls, plot): + """ + Return the member of :ref:`XlChartType` that corresponds to the chart + type of *plot*. + """ + try: + chart_type_method = { + "AreaPlot": cls._differentiate_area_chart_type, + "Area3DPlot": cls._differentiate_area_3d_chart_type, + "BarPlot": cls._differentiate_bar_chart_type, + "BubblePlot": cls._differentiate_bubble_chart_type, + "DoughnutPlot": cls._differentiate_doughnut_chart_type, + "LinePlot": cls._differentiate_line_chart_type, + "PiePlot": cls._differentiate_pie_chart_type, + "RadarPlot": cls._differentiate_radar_chart_type, + "XyPlot": cls._differentiate_xy_chart_type, + }[plot.__class__.__name__] + except KeyError: + raise NotImplementedError( + "chart_type() not implemented for %s" % plot.__class__.__name__ + ) + return chart_type_method(plot) + + @classmethod + def _differentiate_area_3d_chart_type(cls, plot): + return { + ST_Grouping.STANDARD: XL.THREE_D_AREA, + ST_Grouping.STACKED: XL.THREE_D_AREA_STACKED, + ST_Grouping.PERCENT_STACKED: XL.THREE_D_AREA_STACKED_100, + }[plot._element.grouping_val] + + @classmethod + def _differentiate_area_chart_type(cls, plot): + return { + ST_Grouping.STANDARD: XL.AREA, + ST_Grouping.STACKED: XL.AREA_STACKED, + ST_Grouping.PERCENT_STACKED: XL.AREA_STACKED_100, + }[plot._element.grouping_val] + + @classmethod + def _differentiate_bar_chart_type(cls, plot): + barChart = plot._element + if barChart.barDir.val == ST_BarDir.BAR: + return { + ST_Grouping.CLUSTERED: XL.BAR_CLUSTERED, + ST_Grouping.STACKED: XL.BAR_STACKED, + ST_Grouping.PERCENT_STACKED: XL.BAR_STACKED_100, + }[barChart.grouping_val] + if barChart.barDir.val == ST_BarDir.COL: + return { + ST_Grouping.CLUSTERED: XL.COLUMN_CLUSTERED, + ST_Grouping.STACKED: XL.COLUMN_STACKED, + ST_Grouping.PERCENT_STACKED: XL.COLUMN_STACKED_100, + }[barChart.grouping_val] + raise ValueError("invalid barChart.barDir value '%s'" % barChart.barDir.val) + + @classmethod + def _differentiate_bubble_chart_type(cls, plot): + def first_bubble3D(bubbleChart): + results = bubbleChart.xpath("c:ser/c:bubble3D") + return results[0] if results else None + + bubbleChart = plot._element + bubble3D = first_bubble3D(bubbleChart) + + if bubble3D is None: + return XL.BUBBLE + if bubble3D.val: + return XL.BUBBLE_THREE_D_EFFECT + return XL.BUBBLE + + @classmethod + def _differentiate_doughnut_chart_type(cls, plot): + doughnutChart = plot._element + explosion = doughnutChart.xpath("./c:ser/c:explosion") + return XL.DOUGHNUT_EXPLODED if explosion else XL.DOUGHNUT + + @classmethod + def _differentiate_line_chart_type(cls, plot): + lineChart = plot._element + + def has_line_markers(): + matches = lineChart.xpath('c:ser/c:marker/c:symbol[@val="none"]') + if matches: + return False + return True + + if has_line_markers(): + return { + ST_Grouping.STANDARD: XL.LINE_MARKERS, + ST_Grouping.STACKED: XL.LINE_MARKERS_STACKED, + ST_Grouping.PERCENT_STACKED: XL.LINE_MARKERS_STACKED_100, + }[plot._element.grouping_val] + else: + return { + ST_Grouping.STANDARD: XL.LINE, + ST_Grouping.STACKED: XL.LINE_STACKED, + ST_Grouping.PERCENT_STACKED: XL.LINE_STACKED_100, + }[plot._element.grouping_val] + + @classmethod + def _differentiate_pie_chart_type(cls, plot): + pieChart = plot._element + explosion = pieChart.xpath("./c:ser/c:explosion") + return XL.PIE_EXPLODED if explosion else XL.PIE + + @classmethod + def _differentiate_radar_chart_type(cls, plot): + radarChart = plot._element + radar_style = radarChart.xpath("c:radarStyle")[0].get("val") + + def noMarkers(): + matches = radarChart.xpath("c:ser/c:marker/c:symbol") + if matches and matches[0].get("val") == "none": + return True + return False + + if radar_style is None: + return XL.RADAR + if radar_style == "filled": + return XL.RADAR_FILLED + if noMarkers(): + return XL.RADAR + return XL.RADAR_MARKERS + + @classmethod + def _differentiate_xy_chart_type(cls, plot): + scatterChart = plot._element + + def noLine(): + return bool(scatterChart.xpath("c:ser/c:spPr/a:ln/a:noFill")) + + def noMarkers(): + symbols = scatterChart.xpath("c:ser/c:marker/c:symbol") + if symbols and symbols[0].get("val") == "none": + return True + return False + + scatter_style = scatterChart.xpath("c:scatterStyle")[0].get("val") + + if scatter_style == "lineMarker": + if noLine(): + return XL.XY_SCATTER + if noMarkers(): + return XL.XY_SCATTER_LINES_NO_MARKERS + return XL.XY_SCATTER_LINES + + if scatter_style == "smoothMarker": + if noMarkers(): + return XL.XY_SCATTER_SMOOTH_NO_MARKERS + return XL.XY_SCATTER_SMOOTH + + return XL.XY_SCATTER diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/point.py b/.venv/lib/python3.12/site-packages/pptx/chart/point.py new file mode 100644 index 00000000..2d42436c --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/chart/point.py @@ -0,0 +1,101 @@ +"""Data point-related objects.""" + +from __future__ import annotations + +from collections.abc import Sequence + +from pptx.chart.datalabel import DataLabel +from pptx.chart.marker import Marker +from pptx.dml.chtfmt import ChartFormat +from pptx.util import lazyproperty + + +class _BasePoints(Sequence): + """ + Sequence providing access to the individual data points in a series. + """ + + def __init__(self, ser): + super(_BasePoints, self).__init__() + self._element = ser + self._ser = ser + + def __getitem__(self, idx): + if idx < 0 or idx >= self.__len__(): + raise IndexError("point index out of range") + return Point(self._ser, idx) + + +class BubblePoints(_BasePoints): + """ + Sequence providing access to the individual data points in + a |BubbleSeries| object. + """ + + def __len__(self): + return min( + self._ser.xVal_ptCount_val, + self._ser.yVal_ptCount_val, + self._ser.bubbleSize_ptCount_val, + ) + + +class CategoryPoints(_BasePoints): + """ + Sequence providing access to individual |Point| objects, each + representing the visual properties of a data point in the specified + category series. + """ + + def __len__(self): + return self._ser.cat_ptCount_val + + +class Point(object): + """ + Provides access to the properties of an individual data point in + a series, such as the visual properties of its marker and the text and + font of its data label. + """ + + def __init__(self, ser, idx): + super(Point, self).__init__() + self._element = ser + self._ser = ser + self._idx = idx + + @lazyproperty + def data_label(self): + """ + The |DataLabel| object representing the label on this data point. + """ + return DataLabel(self._ser, self._idx) + + @lazyproperty + def format(self): + """ + The |ChartFormat| object providing access to the shape formatting + properties of this data point, such as line and fill. + """ + dPt = self._ser.get_or_add_dPt_for_point(self._idx) + return ChartFormat(dPt) + + @lazyproperty + def marker(self): + """ + The |Marker| instance for this point, providing access to the visual + properties of the data point marker, such as fill and line. Setting + these properties overrides any value set at the series level. + """ + dPt = self._ser.get_or_add_dPt_for_point(self._idx) + return Marker(dPt) + + +class XyPoints(_BasePoints): + """ + Sequence providing access to the individual data points in an |XySeries| + object. + """ + + def __len__(self): + return min(self._ser.xVal_ptCount_val, self._ser.yVal_ptCount_val) diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/series.py b/.venv/lib/python3.12/site-packages/pptx/chart/series.py new file mode 100644 index 00000000..16112eab --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/chart/series.py @@ -0,0 +1,258 @@ +"""Series-related objects.""" + +from __future__ import annotations + +from collections.abc import Sequence + +from pptx.chart.datalabel import DataLabels +from pptx.chart.marker import Marker +from pptx.chart.point import BubblePoints, CategoryPoints, XyPoints +from pptx.dml.chtfmt import ChartFormat +from pptx.oxml.ns import qn +from pptx.util import lazyproperty + + +class _BaseSeries(object): + """ + Base class for |BarSeries| and other series classes. + """ + + def __init__(self, ser): + super(_BaseSeries, self).__init__() + self._element = ser + self._ser = ser + + @lazyproperty + def format(self): + """ + The |ChartFormat| instance for this series, providing access to shape + properties such as fill and line. + """ + return ChartFormat(self._ser) + + @property + def index(self): + """ + The zero-based integer index of this series as reported in its + `c:ser/c:idx` element. + """ + return self._element.idx.val + + @property + def name(self): + """ + The string label given to this series, appears as the title of the + column for this series in the Excel worksheet. It also appears as the + label for this series in the legend. + """ + names = self._element.xpath("./c:tx//c:pt/c:v/text()") + name = names[0] if names else "" + return name + + +class _BaseCategorySeries(_BaseSeries): + """Base class for |BarSeries| and other category chart series classes.""" + + @lazyproperty + def data_labels(self): + """|DataLabels| object controlling data labels for this series.""" + return DataLabels(self._ser.get_or_add_dLbls()) + + @lazyproperty + def points(self): + """ + The |CategoryPoints| object providing access to individual data + points in this series. + """ + return CategoryPoints(self._ser) + + @property + def values(self): + """ + Read-only. A sequence containing the float values for this series, in + the order they appear on the chart. + """ + + def iter_values(): + val = self._element.val + if val is None: + return + for idx in range(val.ptCount_val): + yield val.pt_v(idx) + + return tuple(iter_values()) + + +class _MarkerMixin(object): + """ + Mixin class providing `.marker` property for line-type chart series. The + line-type charts are Line, XY, and Radar. + """ + + @lazyproperty + def marker(self): + """ + The |Marker| instance for this series, providing access to data point + marker properties such as fill and line. Setting these properties + determines the appearance of markers for all points in this series + that are not overridden by settings at the point level. + """ + return Marker(self._ser) + + +class AreaSeries(_BaseCategorySeries): + """ + A data point series belonging to an area plot. + """ + + +class BarSeries(_BaseCategorySeries): + """A data point series belonging to a bar plot.""" + + @property + def invert_if_negative(self): + """ + |True| if a point having a value less than zero should appear with a + fill different than those with a positive value. |False| if the fill + should be the same regardless of the bar's value. When |True|, a bar + with a solid fill appears with white fill; in a bar with gradient + fill, the direction of the gradient is reversed, e.g. dark -> light + instead of light -> dark. The term "invert" here should be understood + to mean "invert the *direction* of the *fill gradient*". + """ + invertIfNegative = self._element.invertIfNegative + if invertIfNegative is None: + return True + return invertIfNegative.val + + @invert_if_negative.setter + def invert_if_negative(self, value): + invertIfNegative = self._element.get_or_add_invertIfNegative() + invertIfNegative.val = value + + +class LineSeries(_BaseCategorySeries, _MarkerMixin): + """ + A data point series belonging to a line plot. + """ + + @property + def smooth(self): + """ + Read/write boolean specifying whether to use curve smoothing to + form the line connecting the data points in this series into + a continuous curve. If |False|, a series of straight line segments + are used to connect the points. + """ + smooth = self._element.smooth + if smooth is None: + return True + return smooth.val + + @smooth.setter + def smooth(self, value): + self._element.get_or_add_smooth().val = value + + +class PieSeries(_BaseCategorySeries): + """ + A data point series belonging to a pie plot. + """ + + +class RadarSeries(_BaseCategorySeries, _MarkerMixin): + """ + A data point series belonging to a radar plot. + """ + + +class XySeries(_BaseSeries, _MarkerMixin): + """ + A data point series belonging to an XY (scatter) plot. + """ + + def iter_values(self): + """ + Generate each float Y value in this series, in the order they appear + on the chart. A value of `None` represents a missing Y value + (corresponding to a blank Excel cell). + """ + yVal = self._element.yVal + if yVal is None: + return + + for idx in range(yVal.ptCount_val): + yield yVal.pt_v(idx) + + @lazyproperty + def points(self): + """ + The |XyPoints| object providing access to individual data points in + this series. + """ + return XyPoints(self._ser) + + @property + def values(self): + """ + Read-only. A sequence containing the float values for this series, in + the order they appear on the chart. + """ + return tuple(self.iter_values()) + + +class BubbleSeries(XySeries): + """ + A data point series belonging to a bubble plot. + """ + + @lazyproperty + def points(self): + """ + The |BubblePoints| object providing access to individual data point + objects used to discover and adjust the formatting and data labels of + a data point. + """ + return BubblePoints(self._ser) + + +class SeriesCollection(Sequence): + """ + A sequence of |Series| objects. + """ + + def __init__(self, parent_elm): + # *parent_elm* can be either a c:plotArea or xChart element + super(SeriesCollection, self).__init__() + self._element = parent_elm + + def __getitem__(self, index): + ser = self._element.sers[index] + return _SeriesFactory(ser) + + def __len__(self): + return len(self._element.sers) + + +def _SeriesFactory(ser): + """ + Return an instance of the appropriate subclass of _BaseSeries based on the + xChart element *ser* appears in. + """ + xChart_tag = ser.getparent().tag + + try: + SeriesCls = { + qn("c:areaChart"): AreaSeries, + qn("c:barChart"): BarSeries, + qn("c:bubbleChart"): BubbleSeries, + qn("c:doughnutChart"): PieSeries, + qn("c:lineChart"): LineSeries, + qn("c:pieChart"): PieSeries, + qn("c:radarChart"): RadarSeries, + qn("c:scatterChart"): XySeries, + }[xChart_tag] + except KeyError: + raise NotImplementedError("series class for %s not yet implemented" % xChart_tag) + + return SeriesCls(ser) diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/xlsx.py b/.venv/lib/python3.12/site-packages/pptx/chart/xlsx.py new file mode 100644 index 00000000..30b21272 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/chart/xlsx.py @@ -0,0 +1,272 @@ +"""Chart builder and related objects.""" + +from __future__ import annotations + +import io +from contextlib import contextmanager + +from xlsxwriter import Workbook + + +class _BaseWorkbookWriter(object): + """Base class for workbook writers, providing shared members.""" + + def __init__(self, chart_data): + super(_BaseWorkbookWriter, self).__init__() + self._chart_data = chart_data + + @property + def xlsx_blob(self): + """bytes for Excel file containing chart_data.""" + xlsx_file = io.BytesIO() + with self._open_worksheet(xlsx_file) as (workbook, worksheet): + self._populate_worksheet(workbook, worksheet) + return xlsx_file.getvalue() + + @contextmanager + def _open_worksheet(self, xlsx_file): + """ + Enable XlsxWriter Worksheet object to be opened, operated on, and + then automatically closed within a `with` statement. A filename or + stream object (such as an `io.BytesIO` instance) is expected as + *xlsx_file*. + """ + workbook = Workbook(xlsx_file, {"in_memory": True}) + worksheet = workbook.add_worksheet() + yield workbook, worksheet + workbook.close() + + def _populate_worksheet(self, workbook, worksheet): + """ + Must be overridden by each subclass to provide the particulars of + writing the spreadsheet data. + """ + raise NotImplementedError("must be provided by each subclass") + + +class CategoryWorkbookWriter(_BaseWorkbookWriter): + """ + Determines Excel worksheet layout and can write an Excel workbook from + a CategoryChartData object. Serves as the authority for Excel worksheet + ranges. + """ + + @property + def categories_ref(self): + """ + The Excel worksheet reference to the categories for this chart (not + including the column heading). + """ + categories = self._chart_data.categories + if categories.depth == 0: + raise ValueError("chart data contains no categories") + right_col = chr(ord("A") + categories.depth - 1) + bottom_row = categories.leaf_count + 1 + return "Sheet1!$A$2:$%s$%d" % (right_col, bottom_row) + + def series_name_ref(self, series): + """ + Return the Excel worksheet reference to the cell containing the name + for *series*. This also serves as the column heading for the series + values. + """ + return "Sheet1!$%s$1" % self._series_col_letter(series) + + def values_ref(self, series): + """ + The Excel worksheet reference to the values for this series (not + including the column heading). + """ + return "Sheet1!${col_letter}$2:${col_letter}${bottom_row}".format( + **{ + "col_letter": self._series_col_letter(series), + "bottom_row": len(series) + 1, + } + ) + + @staticmethod + def _column_reference(column_number): + """Return str Excel column reference like 'BQ' for *column_number*. + + *column_number* is an int in the range 1-16384 inclusive, where + 1 maps to column 'A'. + """ + if column_number < 1 or column_number > 16384: + raise ValueError("column_number must be in range 1-16384") + + # ---Work right-to-left, one order of magnitude at a time. Note there + # is no zero representation in Excel address scheme, so this is + # not just a conversion to base-26--- + + col_ref = "" + while column_number: + remainder = column_number % 26 + if remainder == 0: + remainder = 26 + + col_letter = chr(ord("A") + remainder - 1) + col_ref = col_letter + col_ref + + # ---Advance to next order of magnitude or terminate loop. The + # minus-one in this expression reflects the fact the next lower + # order of magnitude has a minumum value of 1 (not zero). This is + # essentially the complement to the "if it's 0 make it 26' step + # above.--- + column_number = (column_number - 1) // 26 + + return col_ref + + def _populate_worksheet(self, workbook, worksheet): + """ + Write the chart data contents to *worksheet* in category chart + layout. Write categories starting in the first column starting in + the second row, and proceeding one column per category level (for + charts having multi-level categories). Write series as columns + starting in the next following column, placing the series title in + the first cell. + """ + self._write_categories(workbook, worksheet) + self._write_series(workbook, worksheet) + + def _series_col_letter(self, series): + """ + The letter of the Excel worksheet column in which the data for a + series appears. + """ + column_number = 1 + series.categories.depth + series.index + return self._column_reference(column_number) + + def _write_categories(self, workbook, worksheet): + """ + Write the categories column(s) to *worksheet*. Categories start in + the first column starting in the second row, and proceeding one + column per category level (for charts having multi-level categories). + A date category is formatted as a date. All others are formatted + `General`. + """ + categories = self._chart_data.categories + num_format = workbook.add_format({"num_format": categories.number_format}) + depth = categories.depth + for idx, level in enumerate(categories.levels): + col = depth - idx - 1 + self._write_cat_column(worksheet, col, level, num_format) + + def _write_cat_column(self, worksheet, col, level, num_format): + """ + Write a category column defined by *level* to *worksheet* at offset + *col* and formatted with *num_format*. + """ + worksheet.set_column(col, col, 10) # wide enough for a date + for off, name in level: + row = off + 1 + worksheet.write(row, col, name, num_format) + + def _write_series(self, workbook, worksheet): + """ + Write the series column(s) to *worksheet*. Series start in the column + following the last categories column, placing the series title in the + first cell. + """ + col_offset = self._chart_data.categories.depth + for idx, series in enumerate(self._chart_data): + num_format = workbook.add_format({"num_format": series.number_format}) + series_col = idx + col_offset + worksheet.write(0, series_col, series.name) + worksheet.write_column(1, series_col, series.values, num_format) + + +class XyWorkbookWriter(_BaseWorkbookWriter): + """ + Determines Excel worksheet layout and can write an Excel workbook from XY + chart data. Serves as the authority for Excel worksheet ranges. + """ + + def series_name_ref(self, series): + """ + Return the Excel worksheet reference to the cell containing the name + for *series*. This also serves as the column heading for the series + Y values. + """ + row = self.series_table_row_offset(series) + 1 + return "Sheet1!$B$%d" % row + + def series_table_row_offset(self, series): + """ + Return the number of rows preceding the data table for *series* in + the Excel worksheet. + """ + title_and_spacer_rows = series.index * 2 + data_point_rows = series.data_point_offset + return title_and_spacer_rows + data_point_rows + + def x_values_ref(self, series): + """ + The Excel worksheet reference to the X values for this chart (not + including the column label). + """ + top_row = self.series_table_row_offset(series) + 2 + bottom_row = top_row + len(series) - 1 + return "Sheet1!$A$%d:$A$%d" % (top_row, bottom_row) + + def y_values_ref(self, series): + """ + The Excel worksheet reference to the Y values for this chart (not + including the column label). + """ + top_row = self.series_table_row_offset(series) + 2 + bottom_row = top_row + len(series) - 1 + return "Sheet1!$B$%d:$B$%d" % (top_row, bottom_row) + + def _populate_worksheet(self, workbook, worksheet): + """ + Write chart data contents to *worksheet* in the standard XY chart + layout. Write the data for each series to a separate two-column + table, X values in column A and Y values in column B. Place the + series label in the first (heading) cell of the column. + """ + chart_num_format = workbook.add_format({"num_format": self._chart_data.number_format}) + for series in self._chart_data: + series_num_format = workbook.add_format({"num_format": series.number_format}) + offset = self.series_table_row_offset(series) + # write X values + worksheet.write_column(offset + 1, 0, series.x_values, chart_num_format) + # write Y values + worksheet.write(offset, 1, series.name) + worksheet.write_column(offset + 1, 1, series.y_values, series_num_format) + + +class BubbleWorkbookWriter(XyWorkbookWriter): + """ + Service object that knows how to write an Excel workbook from bubble + chart data. + """ + + def bubble_sizes_ref(self, series): + """ + The Excel worksheet reference to the range containing the bubble + sizes for *series* (not including the column heading cell). + """ + top_row = self.series_table_row_offset(series) + 2 + bottom_row = top_row + len(series) - 1 + return "Sheet1!$C$%d:$C$%d" % (top_row, bottom_row) + + def _populate_worksheet(self, workbook, worksheet): + """ + Write chart data contents to *worksheet* in the bubble chart layout. + Write the data for each series to a separate three-column table with + X values in column A, Y values in column B, and bubble sizes in + column C. Place the series label in the first (heading) cell of the + values column. + """ + chart_num_format = workbook.add_format({"num_format": self._chart_data.number_format}) + for series in self._chart_data: + series_num_format = workbook.add_format({"num_format": series.number_format}) + offset = self.series_table_row_offset(series) + # write X values + worksheet.write_column(offset + 1, 0, series.x_values, chart_num_format) + # write Y values + worksheet.write(offset, 1, series.name) + worksheet.write_column(offset + 1, 1, series.y_values, series_num_format) + # write bubble sizes + worksheet.write(offset, 2, "Size") + worksheet.write_column(offset + 1, 2, series.bubble_sizes, chart_num_format) diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/xmlwriter.py b/.venv/lib/python3.12/site-packages/pptx/chart/xmlwriter.py new file mode 100644 index 00000000..703c53dd --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/chart/xmlwriter.py @@ -0,0 +1,1840 @@ +"""Composers for default chart XML for various chart types.""" + +from __future__ import annotations + +from copy import deepcopy +from xml.sax.saxutils import escape + +from pptx.enum.chart import XL_CHART_TYPE +from pptx.oxml import parse_xml +from pptx.oxml.ns import nsdecls + + +def ChartXmlWriter(chart_type, chart_data): + """ + Factory function returning appropriate XML writer object for + *chart_type*, loaded with *chart_type* and *chart_data*. + """ + XL_CT = XL_CHART_TYPE + try: + BuilderCls = { + XL_CT.AREA: _AreaChartXmlWriter, + XL_CT.AREA_STACKED: _AreaChartXmlWriter, + XL_CT.AREA_STACKED_100: _AreaChartXmlWriter, + XL_CT.BAR_CLUSTERED: _BarChartXmlWriter, + XL_CT.BAR_STACKED: _BarChartXmlWriter, + XL_CT.BAR_STACKED_100: _BarChartXmlWriter, + XL_CT.BUBBLE: _BubbleChartXmlWriter, + XL_CT.BUBBLE_THREE_D_EFFECT: _BubbleChartXmlWriter, + XL_CT.COLUMN_CLUSTERED: _BarChartXmlWriter, + XL_CT.COLUMN_STACKED: _BarChartXmlWriter, + XL_CT.COLUMN_STACKED_100: _BarChartXmlWriter, + XL_CT.DOUGHNUT: _DoughnutChartXmlWriter, + XL_CT.DOUGHNUT_EXPLODED: _DoughnutChartXmlWriter, + XL_CT.LINE: _LineChartXmlWriter, + XL_CT.LINE_MARKERS: _LineChartXmlWriter, + XL_CT.LINE_MARKERS_STACKED: _LineChartXmlWriter, + XL_CT.LINE_MARKERS_STACKED_100: _LineChartXmlWriter, + XL_CT.LINE_STACKED: _LineChartXmlWriter, + XL_CT.LINE_STACKED_100: _LineChartXmlWriter, + XL_CT.PIE: _PieChartXmlWriter, + XL_CT.PIE_EXPLODED: _PieChartXmlWriter, + XL_CT.RADAR: _RadarChartXmlWriter, + XL_CT.RADAR_FILLED: _RadarChartXmlWriter, + XL_CT.RADAR_MARKERS: _RadarChartXmlWriter, + XL_CT.XY_SCATTER: _XyChartXmlWriter, + XL_CT.XY_SCATTER_LINES: _XyChartXmlWriter, + XL_CT.XY_SCATTER_LINES_NO_MARKERS: _XyChartXmlWriter, + XL_CT.XY_SCATTER_SMOOTH: _XyChartXmlWriter, + XL_CT.XY_SCATTER_SMOOTH_NO_MARKERS: _XyChartXmlWriter, + }[chart_type] + except KeyError: + raise NotImplementedError("XML writer for chart type %s not yet implemented" % chart_type) + return BuilderCls(chart_type, chart_data) + + +def SeriesXmlRewriterFactory(chart_type, chart_data): + """ + Return a |_BaseSeriesXmlRewriter| subclass appropriate to *chart_type*. + """ + XL_CT = XL_CHART_TYPE + + RewriterCls = { + # There are 73 distinct chart types, only specify non-category + # types, others default to _CategorySeriesXmlRewriter. Stock-type + # charts are multi-plot charts, so no guaratees on how they turn + # out. + XL_CT.BUBBLE: _BubbleSeriesXmlRewriter, + XL_CT.BUBBLE_THREE_D_EFFECT: _BubbleSeriesXmlRewriter, + XL_CT.XY_SCATTER: _XySeriesXmlRewriter, + XL_CT.XY_SCATTER_LINES: _XySeriesXmlRewriter, + XL_CT.XY_SCATTER_LINES_NO_MARKERS: _XySeriesXmlRewriter, + XL_CT.XY_SCATTER_SMOOTH: _XySeriesXmlRewriter, + XL_CT.XY_SCATTER_SMOOTH_NO_MARKERS: _XySeriesXmlRewriter, + }.get(chart_type, _CategorySeriesXmlRewriter) + + return RewriterCls(chart_data) + + +class _BaseChartXmlWriter(object): + """ + Generates XML text (unicode) for a default chart, like the one added by + PowerPoint when you click the *Add Column Chart* button on the ribbon. + Differentiated XML for different chart types is provided by subclasses. + """ + + def __init__(self, chart_type, series_seq): + super(_BaseChartXmlWriter, self).__init__() + self._chart_type = chart_type + self._chart_data = series_seq + self._series_seq = list(series_seq) + + @property + def xml(self): + """ + The full XML stream for the chart specified by this chart builder, as + unicode text. This method must be overridden by each subclass. + """ + raise NotImplementedError("must be implemented by all subclasses") + + +class _BaseSeriesXmlWriter(object): + """ + Provides shared members for series XML writers. + """ + + def __init__(self, series, date_1904=False): + super(_BaseSeriesXmlWriter, self).__init__() + self._series = series + self._date_1904 = date_1904 + + @property + def name(self): + """ + The XML-escaped name for this series. + """ + return escape(self._series.name) + + def numRef_xml(self, wksht_ref, number_format, values): + """ + Return the ``<c:numRef>`` element specified by the parameters as + unicode text. + """ + pt_xml = self.pt_xml(values) + return ( + " <c:numRef>\n" + " <c:f>{wksht_ref}</c:f>\n" + " <c:numCache>\n" + " <c:formatCode>{number_format}</c:formatCode>\n" + "{pt_xml}" + " </c:numCache>\n" + " </c:numRef>\n" + ).format(**{"wksht_ref": wksht_ref, "number_format": number_format, "pt_xml": pt_xml}) + + def pt_xml(self, values): + """ + Return the ``<c:ptCount>`` and sequence of ``<c:pt>`` elements + corresponding to *values* as a single unicode text string. + `c:ptCount` refers to the number of `c:pt` elements in this sequence. + The `idx` attribute value for `c:pt` elements locates the data point + in the overall data point sequence of the chart and is started at + *offset*. + """ + xml = (' <c:ptCount val="{pt_count}"/>\n').format(pt_count=len(values)) + + pt_tmpl = ( + ' <c:pt idx="{idx}">\n' + " <c:v>{value}</c:v>\n" + " </c:pt>\n" + ) + for idx, value in enumerate(values): + if value is None: + continue + xml += pt_tmpl.format(idx=idx, value=value) + + return xml + + @property + def tx(self): + """ + Return a ``<c:tx>`` oxml element for this series, containing the + series name. + """ + xml = self._tx_tmpl.format( + **{ + "wksht_ref": self._series.name_ref, + "series_name": self.name, + "nsdecls": " %s" % nsdecls("c"), + } + ) + return parse_xml(xml) + + @property + def tx_xml(self): + """ + Return the ``<c:tx>`` (tx is short for 'text') element for this + series as unicode text. This element contains the series name. + """ + return self._tx_tmpl.format( + **{ + "wksht_ref": self._series.name_ref, + "series_name": self.name, + "nsdecls": "", + } + ) + + @property + def _tx_tmpl(self): + """ + The string formatting template for the ``<c:tx>`` element for this + series, containing the series title and spreadsheet range reference. + """ + return ( + " <c:tx{nsdecls}>\n" + " <c:strRef>\n" + " <c:f>{wksht_ref}</c:f>\n" + " <c:strCache>\n" + ' <c:ptCount val="1"/>\n' + ' <c:pt idx="0">\n' + " <c:v>{series_name}</c:v>\n" + " </c:pt>\n" + " </c:strCache>\n" + " </c:strRef>\n" + " </c:tx>\n" + ) + + +class _BaseSeriesXmlRewriter(object): + """ + Base class for series XML rewriters. + """ + + def __init__(self, chart_data): + super(_BaseSeriesXmlRewriter, self).__init__() + self._chart_data = chart_data + + def replace_series_data(self, chartSpace): + """ + Rewrite the series data under *chartSpace* using the chart data + contents. All series-level formatting is left undisturbed. If + the chart data contains fewer series than *chartSpace*, the extra + series in *chartSpace* are deleted. If *chart_data* contains more + series than the *chartSpace* element, new series are added to the + last plot in the chart and series formatting is "cloned" from the + last series in that plot. + """ + plotArea, date_1904 = chartSpace.plotArea, chartSpace.date_1904 + chart_data = self._chart_data + self._adjust_ser_count(plotArea, len(chart_data)) + for ser, series_data in zip(plotArea.sers, chart_data): + self._rewrite_ser_data(ser, series_data, date_1904) + + def _add_cloned_sers(self, plotArea, count): + """ + Add `c:ser` elements to the last xChart element in *plotArea*, cloned + from the last `c:ser` child of that last xChart. + """ + + def clone_ser(ser): + new_ser = deepcopy(ser) + new_ser.idx.val = plotArea.next_idx + new_ser.order.val = plotArea.next_order + ser.addnext(new_ser) + return new_ser + + last_ser = plotArea.last_ser + for _ in range(count): + last_ser = clone_ser(last_ser) + + def _adjust_ser_count(self, plotArea, new_ser_count): + """ + Adjust the number of c:ser elements in *plotArea* to *new_ser_count*. + Excess c:ser elements are deleted from the end, along with any xChart + elements that are left empty as a result. Series elements are + considered in xChart + series order. Any new c:ser elements required + are added to the last xChart element and cloned from the last c:ser + element in that xChart. + """ + ser_count_diff = new_ser_count - len(plotArea.sers) + if ser_count_diff > 0: + self._add_cloned_sers(plotArea, ser_count_diff) + elif ser_count_diff < 0: + self._trim_ser_count_by(plotArea, abs(ser_count_diff)) + + def _rewrite_ser_data(self, ser, series_data, date_1904): + """ + Rewrite selected child elements of *ser* based on the values in + *series_data*. + """ + raise NotImplementedError("must be implemented by each subclass") + + def _trim_ser_count_by(self, plotArea, count): + """ + Remove the last *count* ser elements from *plotArea*. Any xChart + elements having no ser child elements after trimming are also + removed. + """ + extra_sers = plotArea.sers[-count:] + for ser in extra_sers: + parent = ser.getparent() + parent.remove(ser) + extra_xCharts = [xChart for xChart in plotArea.iter_xCharts() if len(xChart.sers) == 0] + for xChart in extra_xCharts: + parent = xChart.getparent() + parent.remove(xChart) + + +class _AreaChartXmlWriter(_BaseChartXmlWriter): + """ + Provides specialized methods particular to the ``<c:areaChart>`` element. + """ + + @property + def xml(self): + return ( + "<?xml version='1.0' encoding='UTF-8' standalone='yes'?>\n" + '<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawin' + 'gml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/draw' + 'ingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/off' + 'iceDocument/2006/relationships">\n' + ' <c:date1904 val="0"/>\n' + ' <c:roundedCorners val="0"/>\n' + " <c:chart>\n" + ' <c:autoTitleDeleted val="0"/>\n' + " <c:plotArea>\n" + " <c:layout/>\n" + " <c:areaChart>\n" + "{grouping_xml}" + ' <c:varyColors val="0"/>\n' + "{ser_xml}" + " <c:dLbls>\n" + ' <c:showLegendKey val="0"/>\n' + ' <c:showVal val="0"/>\n' + ' <c:showCatName val="0"/>\n' + ' <c:showSerName val="0"/>\n' + ' <c:showPercent val="0"/>\n' + ' <c:showBubbleSize val="0"/>\n' + " </c:dLbls>\n" + ' <c:axId val="-2101159928"/>\n' + ' <c:axId val="-2100718248"/>\n' + " </c:areaChart>\n" + "{cat_ax_xml}" + " <c:valAx>\n" + ' <c:axId val="-2100718248"/>\n' + " <c:scaling>\n" + ' <c:orientation val="minMax"/>\n' + " </c:scaling>\n" + ' <c:delete val="0"/>\n' + ' <c:axPos val="l"/>\n' + " <c:majorGridlines/>\n" + ' <c:numFmt formatCode="General" sourceLinked="1"/>\n' + ' <c:majorTickMark val="out"/>\n' + ' <c:minorTickMark val="none"/>\n' + ' <c:tickLblPos val="nextTo"/>\n' + ' <c:crossAx val="-2101159928"/>\n' + ' <c:crosses val="autoZero"/>\n' + ' <c:crossBetween val="midCat"/>\n' + " </c:valAx>\n" + " </c:plotArea>\n" + " <c:legend>\n" + ' <c:legendPos val="r"/>\n' + " <c:layout/>\n" + ' <c:overlay val="0"/>\n' + " </c:legend>\n" + ' <c:plotVisOnly val="1"/>\n' + ' <c:dispBlanksAs val="zero"/>\n' + ' <c:showDLblsOverMax val="0"/>\n' + " </c:chart>\n" + " <c:txPr>\n" + " <a:bodyPr/>\n" + " <a:lstStyle/>\n" + " <a:p>\n" + " <a:pPr>\n" + ' <a:defRPr sz="1800"/>\n' + " </a:pPr>\n" + " <a:endParaRPr/>\n" + " </a:p>\n" + " </c:txPr>\n" + "</c:chartSpace>\n" + ).format( + **{ + "grouping_xml": self._grouping_xml, + "ser_xml": self._ser_xml, + "cat_ax_xml": self._cat_ax_xml, + } + ) + + @property + def _cat_ax_xml(self): + categories = self._chart_data.categories + + if categories.are_dates: + return ( + " <c:dateAx>\n" + ' <c:axId val="-2101159928"/>\n' + " <c:scaling>\n" + ' <c:orientation val="minMax"/>\n' + " </c:scaling>\n" + ' <c:delete val="0"/>\n' + ' <c:axPos val="b"/>\n' + ' <c:numFmt formatCode="{nf}" sourceLinked="1"/>\n' + ' <c:majorTickMark val="out"/>\n' + ' <c:minorTickMark val="none"/>\n' + ' <c:tickLblPos val="nextTo"/>\n' + ' <c:crossAx val="-2100718248"/>\n' + ' <c:crosses val="autoZero"/>\n' + ' <c:auto val="1"/>\n' + ' <c:lblOffset val="100"/>\n' + ' <c:baseTimeUnit val="days"/>\n' + " </c:dateAx>\n" + ).format(**{"nf": categories.number_format}) + + return ( + " <c:catAx>\n" + ' <c:axId val="-2101159928"/>\n' + " <c:scaling>\n" + ' <c:orientation val="minMax"/>\n' + " </c:scaling>\n" + ' <c:delete val="0"/>\n' + ' <c:axPos val="b"/>\n' + ' <c:numFmt formatCode="General" sourceLinked="1"/>\n' + ' <c:majorTickMark val="out"/>\n' + ' <c:minorTickMark val="none"/>\n' + ' <c:tickLblPos val="nextTo"/>\n' + ' <c:crossAx val="-2100718248"/>\n' + ' <c:crosses val="autoZero"/>\n' + ' <c:auto val="1"/>\n' + ' <c:lblAlgn val="ctr"/>\n' + ' <c:lblOffset val="100"/>\n' + ' <c:noMultiLvlLbl val="0"/>\n' + " </c:catAx>\n" + ) + + @property + def _grouping_xml(self): + val = { + XL_CHART_TYPE.AREA: "standard", + XL_CHART_TYPE.AREA_STACKED: "stacked", + XL_CHART_TYPE.AREA_STACKED_100: "percentStacked", + }[self._chart_type] + return ' <c:grouping val="%s"/>\n' % val + + @property + def _ser_xml(self): + xml = "" + for series in self._chart_data: + xml_writer = _CategorySeriesXmlWriter(series) + xml += ( + " <c:ser>\n" + ' <c:idx val="{ser_idx}"/>\n' + ' <c:order val="{ser_order}"/>\n' + "{tx_xml}" + "{cat_xml}" + "{val_xml}" + " </c:ser>\n" + ).format( + **{ + "ser_idx": series.index, + "ser_order": series.index, + "tx_xml": xml_writer.tx_xml, + "cat_xml": xml_writer.cat_xml, + "val_xml": xml_writer.val_xml, + } + ) + return xml + + +class _BarChartXmlWriter(_BaseChartXmlWriter): + """ + Provides specialized methods particular to the ``<c:barChart>`` element. + """ + + @property + def xml(self): + return ( + "<?xml version='1.0' encoding='UTF-8' standalone='yes'?>\n" + '<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawin' + 'gml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/draw' + 'ingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/off' + 'iceDocument/2006/relationships">\n' + ' <c:date1904 val="0"/>\n' + " <c:chart>\n" + ' <c:autoTitleDeleted val="0"/>\n' + " <c:plotArea>\n" + " <c:barChart>\n" + "{barDir_xml}" + "{grouping_xml}" + "{ser_xml}" + "{overlap_xml}" + ' <c:axId val="-2068027336"/>\n' + ' <c:axId val="-2113994440"/>\n' + " </c:barChart>\n" + "{cat_ax_xml}" + " <c:valAx>\n" + ' <c:axId val="-2113994440"/>\n' + " <c:scaling/>\n" + ' <c:delete val="0"/>\n' + ' <c:axPos val="{val_ax_pos}"/>\n' + " <c:majorGridlines/>\n" + ' <c:majorTickMark val="out"/>\n' + ' <c:minorTickMark val="none"/>\n' + ' <c:tickLblPos val="nextTo"/>\n' + ' <c:crossAx val="-2068027336"/>\n' + ' <c:crosses val="autoZero"/>\n' + " </c:valAx>\n" + " </c:plotArea>\n" + ' <c:dispBlanksAs val="gap"/>\n' + " </c:chart>\n" + " <c:txPr>\n" + " <a:bodyPr/>\n" + " <a:lstStyle/>\n" + " <a:p>\n" + " <a:pPr>\n" + ' <a:defRPr sz="1800"/>\n' + " </a:pPr>\n" + ' <a:endParaRPr lang="en-US"/>\n' + " </a:p>\n" + " </c:txPr>\n" + "</c:chartSpace>\n" + ).format( + **{ + "barDir_xml": self._barDir_xml, + "grouping_xml": self._grouping_xml, + "ser_xml": self._ser_xml, + "overlap_xml": self._overlap_xml, + "cat_ax_xml": self._cat_ax_xml, + "val_ax_pos": self._val_ax_pos, + } + ) + + @property + def _barDir_xml(self): + XL = XL_CHART_TYPE + bar_types = (XL.BAR_CLUSTERED, XL.BAR_STACKED, XL.BAR_STACKED_100) + col_types = (XL.COLUMN_CLUSTERED, XL.COLUMN_STACKED, XL.COLUMN_STACKED_100) + if self._chart_type in bar_types: + return ' <c:barDir val="bar"/>\n' + elif self._chart_type in col_types: + return ' <c:barDir val="col"/>\n' + raise NotImplementedError("no _barDir_xml() for chart type %s" % self._chart_type) + + @property + def _cat_ax_pos(self): + return { + XL_CHART_TYPE.BAR_CLUSTERED: "l", + XL_CHART_TYPE.BAR_STACKED: "l", + XL_CHART_TYPE.BAR_STACKED_100: "l", + XL_CHART_TYPE.COLUMN_CLUSTERED: "b", + XL_CHART_TYPE.COLUMN_STACKED: "b", + XL_CHART_TYPE.COLUMN_STACKED_100: "b", + }[self._chart_type] + + @property + def _cat_ax_xml(self): + categories = self._chart_data.categories + + if categories.are_dates: + return ( + " <c:dateAx>\n" + ' <c:axId val="-2068027336"/>\n' + " <c:scaling>\n" + ' <c:orientation val="minMax"/>\n' + " </c:scaling>\n" + ' <c:delete val="0"/>\n' + ' <c:axPos val="{cat_ax_pos}"/>\n' + ' <c:numFmt formatCode="{nf}" sourceLinked="1"/>\n' + ' <c:majorTickMark val="out"/>\n' + ' <c:minorTickMark val="none"/>\n' + ' <c:tickLblPos val="nextTo"/>\n' + ' <c:crossAx val="-2113994440"/>\n' + ' <c:crosses val="autoZero"/>\n' + ' <c:auto val="1"/>\n' + ' <c:lblOffset val="100"/>\n' + ' <c:baseTimeUnit val="days"/>\n' + " </c:dateAx>\n" + ).format(**{"cat_ax_pos": self._cat_ax_pos, "nf": categories.number_format}) + + return ( + " <c:catAx>\n" + ' <c:axId val="-2068027336"/>\n' + " <c:scaling>\n" + ' <c:orientation val="minMax"/>\n' + " </c:scaling>\n" + ' <c:delete val="0"/>\n' + ' <c:axPos val="{cat_ax_pos}"/>\n' + ' <c:majorTickMark val="out"/>\n' + ' <c:minorTickMark val="none"/>\n' + ' <c:tickLblPos val="nextTo"/>\n' + ' <c:crossAx val="-2113994440"/>\n' + ' <c:crosses val="autoZero"/>\n' + ' <c:auto val="1"/>\n' + ' <c:lblAlgn val="ctr"/>\n' + ' <c:lblOffset val="100"/>\n' + ' <c:noMultiLvlLbl val="0"/>\n' + " </c:catAx>\n" + ).format(**{"cat_ax_pos": self._cat_ax_pos}) + + @property + def _grouping_xml(self): + XL = XL_CHART_TYPE + clustered_types = (XL.BAR_CLUSTERED, XL.COLUMN_CLUSTERED) + stacked_types = (XL.BAR_STACKED, XL.COLUMN_STACKED) + percentStacked_types = (XL.BAR_STACKED_100, XL.COLUMN_STACKED_100) + if self._chart_type in clustered_types: + return ' <c:grouping val="clustered"/>\n' + elif self._chart_type in stacked_types: + return ' <c:grouping val="stacked"/>\n' + elif self._chart_type in percentStacked_types: + return ' <c:grouping val="percentStacked"/>\n' + raise NotImplementedError("no _grouping_xml() for chart type %s" % self._chart_type) + + @property + def _overlap_xml(self): + XL = XL_CHART_TYPE + percentStacked_types = ( + XL.BAR_STACKED, + XL.BAR_STACKED_100, + XL.COLUMN_STACKED, + XL.COLUMN_STACKED_100, + ) + if self._chart_type in percentStacked_types: + return ' <c:overlap val="100"/>\n' + return "" + + @property + def _ser_xml(self): + xml = "" + for series in self._chart_data: + xml_writer = _CategorySeriesXmlWriter(series) + xml += ( + " <c:ser>\n" + ' <c:idx val="{ser_idx}"/>\n' + ' <c:order val="{ser_order}"/>\n' + "{tx_xml}" + "{cat_xml}" + "{val_xml}" + " </c:ser>\n" + ).format( + **{ + "ser_idx": series.index, + "ser_order": series.index, + "tx_xml": xml_writer.tx_xml, + "cat_xml": xml_writer.cat_xml, + "val_xml": xml_writer.val_xml, + } + ) + return xml + + @property + def _val_ax_pos(self): + return { + XL_CHART_TYPE.BAR_CLUSTERED: "b", + XL_CHART_TYPE.BAR_STACKED: "b", + XL_CHART_TYPE.BAR_STACKED_100: "b", + XL_CHART_TYPE.COLUMN_CLUSTERED: "l", + XL_CHART_TYPE.COLUMN_STACKED: "l", + XL_CHART_TYPE.COLUMN_STACKED_100: "l", + }[self._chart_type] + + +class _DoughnutChartXmlWriter(_BaseChartXmlWriter): + """ + Provides specialized methods particular to the ``<c:doughnutChart>`` + element. + """ + + @property + def xml(self): + return ( + "<?xml version='1.0' encoding='UTF-8' standalone='yes'?>\n" + '<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawin' + 'gml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/draw' + 'ingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/off' + 'iceDocument/2006/relationships">\n' + ' <c:date1904 val="0"/>\n' + ' <c:roundedCorners val="0"/>\n' + " <c:chart>\n" + ' <c:autoTitleDeleted val="0"/>\n' + " <c:plotArea>\n" + " <c:layout/>\n" + " <c:doughnutChart>\n" + ' <c:varyColors val="1"/>\n' + "{ser_xml}" + " <c:dLbls>\n" + ' <c:showLegendKey val="0"/>\n' + ' <c:showVal val="0"/>\n' + ' <c:showCatName val="0"/>\n' + ' <c:showSerName val="0"/>\n' + ' <c:showPercent val="0"/>\n' + ' <c:showBubbleSize val="0"/>\n' + ' <c:showLeaderLines val="1"/>\n' + " </c:dLbls>\n" + ' <c:firstSliceAng val="0"/>\n' + ' <c:holeSize val="50"/>\n' + " </c:doughnutChart>\n" + " </c:plotArea>\n" + " <c:legend>\n" + ' <c:legendPos val="r"/>\n' + " <c:layout/>\n" + ' <c:overlay val="0"/>\n' + " </c:legend>\n" + ' <c:plotVisOnly val="1"/>\n' + ' <c:dispBlanksAs val="gap"/>\n' + ' <c:showDLblsOverMax val="0"/>\n' + " </c:chart>\n" + " <c:txPr>\n" + " <a:bodyPr/>\n" + " <a:lstStyle/>\n" + " <a:p>\n" + " <a:pPr>\n" + ' <a:defRPr sz="1800"/>\n' + " </a:pPr>\n" + " <a:endParaRPr/>\n" + " </a:p>\n" + " </c:txPr>\n" + "</c:chartSpace>\n" + ).format(**{"ser_xml": self._ser_xml}) + + @property + def _explosion_xml(self): + if self._chart_type == XL_CHART_TYPE.DOUGHNUT_EXPLODED: + return ' <c:explosion val="25"/>\n' + return "" + + @property + def _ser_xml(self): + xml = "" + for series in self._chart_data: + xml_writer = _CategorySeriesXmlWriter(series) + xml += ( + " <c:ser>\n" + ' <c:idx val="{ser_idx}"/>\n' + ' <c:order val="{ser_order}"/>\n' + "{tx_xml}" + "{explosion_xml}" + "{cat_xml}" + "{val_xml}" + " </c:ser>\n" + ).format( + **{ + "ser_idx": series.index, + "ser_order": series.index, + "tx_xml": xml_writer.tx_xml, + "explosion_xml": self._explosion_xml, + "cat_xml": xml_writer.cat_xml, + "val_xml": xml_writer.val_xml, + } + ) + return xml + + +class _LineChartXmlWriter(_BaseChartXmlWriter): + """ + Provides specialized methods particular to the ``<c:lineChart>`` element. + """ + + @property + def xml(self): + return ( + "<?xml version='1.0' encoding='UTF-8' standalone='yes'?>\n" + '<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawin' + 'gml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/draw' + 'ingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/off' + 'iceDocument/2006/relationships">\n' + ' <c:date1904 val="0"/>\n' + " <c:chart>\n" + ' <c:autoTitleDeleted val="0"/>\n' + " <c:plotArea>\n" + " <c:lineChart>\n" + "{grouping_xml}" + ' <c:varyColors val="0"/>\n' + "{ser_xml}" + ' <c:marker val="1"/>\n' + ' <c:smooth val="0"/>\n' + ' <c:axId val="2118791784"/>\n' + ' <c:axId val="2140495176"/>\n' + " </c:lineChart>\n" + "{cat_ax_xml}" + " <c:valAx>\n" + ' <c:axId val="2140495176"/>\n' + " <c:scaling/>\n" + ' <c:delete val="0"/>\n' + ' <c:axPos val="l"/>\n' + " <c:majorGridlines/>\n" + ' <c:majorTickMark val="out"/>\n' + ' <c:minorTickMark val="none"/>\n' + ' <c:tickLblPos val="nextTo"/>\n' + ' <c:crossAx val="2118791784"/>\n' + ' <c:crosses val="autoZero"/>\n' + " </c:valAx>\n" + " </c:plotArea>\n" + " <c:legend>\n" + ' <c:legendPos val="r"/>\n' + " <c:layout/>\n" + ' <c:overlay val="0"/>\n' + " </c:legend>\n" + ' <c:plotVisOnly val="1"/>\n' + ' <c:dispBlanksAs val="gap"/>\n' + ' <c:showDLblsOverMax val="0"/>\n' + " </c:chart>\n" + " <c:txPr>\n" + " <a:bodyPr/>\n" + " <a:lstStyle/>\n" + " <a:p>\n" + " <a:pPr>\n" + ' <a:defRPr sz="1800"/>\n' + " </a:pPr>\n" + ' <a:endParaRPr lang="en-US"/>\n' + " </a:p>\n" + " </c:txPr>\n" + "</c:chartSpace>\n" + ).format( + **{ + "grouping_xml": self._grouping_xml, + "ser_xml": self._ser_xml, + "cat_ax_xml": self._cat_ax_xml, + } + ) + + @property + def _cat_ax_xml(self): + categories = self._chart_data.categories + + if categories.are_dates: + return ( + " <c:dateAx>\n" + ' <c:axId val="2118791784"/>\n' + " <c:scaling>\n" + ' <c:orientation val="minMax"/>\n' + " </c:scaling>\n" + ' <c:delete val="0"/>\n' + ' <c:axPos val="b"/>\n' + ' <c:numFmt formatCode="{nf}" sourceLinked="1"/>\n' + ' <c:majorTickMark val="out"/>\n' + ' <c:minorTickMark val="none"/>\n' + ' <c:tickLblPos val="nextTo"/>\n' + ' <c:crossAx val="2140495176"/>\n' + ' <c:crosses val="autoZero"/>\n' + ' <c:auto val="1"/>\n' + ' <c:lblOffset val="100"/>\n' + ' <c:baseTimeUnit val="days"/>\n' + " </c:dateAx>\n" + ).format(**{"nf": categories.number_format}) + + return ( + " <c:catAx>\n" + ' <c:axId val="2118791784"/>\n' + " <c:scaling>\n" + ' <c:orientation val="minMax"/>\n' + " </c:scaling>\n" + ' <c:delete val="0"/>\n' + ' <c:axPos val="b"/>\n' + ' <c:majorTickMark val="out"/>\n' + ' <c:minorTickMark val="none"/>\n' + ' <c:tickLblPos val="nextTo"/>\n' + ' <c:crossAx val="2140495176"/>\n' + ' <c:crosses val="autoZero"/>\n' + ' <c:auto val="1"/>\n' + ' <c:lblAlgn val="ctr"/>\n' + ' <c:lblOffset val="100"/>\n' + ' <c:noMultiLvlLbl val="0"/>\n' + " </c:catAx>\n" + ) + + @property + def _grouping_xml(self): + XL = XL_CHART_TYPE + standard_types = (XL.LINE, XL.LINE_MARKERS) + stacked_types = (XL.LINE_STACKED, XL.LINE_MARKERS_STACKED) + percentStacked_types = (XL.LINE_STACKED_100, XL.LINE_MARKERS_STACKED_100) + if self._chart_type in standard_types: + return ' <c:grouping val="standard"/>\n' + elif self._chart_type in stacked_types: + return ' <c:grouping val="stacked"/>\n' + elif self._chart_type in percentStacked_types: + return ' <c:grouping val="percentStacked"/>\n' + raise NotImplementedError("no _grouping_xml() for chart type %s" % self._chart_type) + + @property + def _marker_xml(self): + XL = XL_CHART_TYPE + no_marker_types = (XL.LINE, XL.LINE_STACKED, XL.LINE_STACKED_100) + if self._chart_type in no_marker_types: + return ( + " <c:marker>\n" + ' <c:symbol val="none"/>\n' + " </c:marker>\n" + ) + return "" + + @property + def _ser_xml(self): + xml = "" + for series in self._chart_data: + xml_writer = _CategorySeriesXmlWriter(series) + xml += ( + " <c:ser>\n" + ' <c:idx val="{ser_idx}"/>\n' + ' <c:order val="{ser_order}"/>\n' + "{tx_xml}" + "{marker_xml}" + "{cat_xml}" + "{val_xml}" + ' <c:smooth val="0"/>\n' + " </c:ser>\n" + ).format( + **{ + "ser_idx": series.index, + "ser_order": series.index, + "tx_xml": xml_writer.tx_xml, + "marker_xml": self._marker_xml, + "cat_xml": xml_writer.cat_xml, + "val_xml": xml_writer.val_xml, + } + ) + return xml + + +class _PieChartXmlWriter(_BaseChartXmlWriter): + """ + Provides specialized methods particular to the ``<c:pieChart>`` element. + """ + + @property + def xml(self): + return ( + "<?xml version='1.0' encoding='UTF-8' standalone='yes'?>\n" + '<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawin' + 'gml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/draw' + 'ingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/off' + 'iceDocument/2006/relationships">\n' + " <c:chart>\n" + ' <c:autoTitleDeleted val="0"/>\n' + " <c:plotArea>\n" + " <c:pieChart>\n" + ' <c:varyColors val="1"/>\n' + "{ser_xml}" + " </c:pieChart>\n" + " </c:plotArea>\n" + ' <c:dispBlanksAs val="gap"/>\n' + " </c:chart>\n" + " <c:txPr>\n" + " <a:bodyPr/>\n" + " <a:lstStyle/>\n" + " <a:p>\n" + " <a:pPr>\n" + ' <a:defRPr sz="1800"/>\n' + " </a:pPr>\n" + ' <a:endParaRPr lang="en-US"/>\n' + " </a:p>\n" + " </c:txPr>\n" + "</c:chartSpace>\n" + ).format(**{"ser_xml": self._ser_xml}) + + @property + def _explosion_xml(self): + if self._chart_type == XL_CHART_TYPE.PIE_EXPLODED: + return ' <c:explosion val="25"/>\n' + return "" + + @property + def _ser_xml(self): + xml_writer = _CategorySeriesXmlWriter(self._chart_data[0]) + xml = ( + " <c:ser>\n" + ' <c:idx val="0"/>\n' + ' <c:order val="0"/>\n' + "{tx_xml}" + "{explosion_xml}" + "{cat_xml}" + "{val_xml}" + " </c:ser>\n" + ).format( + **{ + "tx_xml": xml_writer.tx_xml, + "explosion_xml": self._explosion_xml, + "cat_xml": xml_writer.cat_xml, + "val_xml": xml_writer.val_xml, + } + ) + return xml + + +class _RadarChartXmlWriter(_BaseChartXmlWriter): + """ + Generates XML for the ``<c:radarChart>`` element. + """ + + @property + def xml(self): + return ( + "<?xml version='1.0' encoding='UTF-8' standalone='yes'?>\n" + '<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawin' + 'gml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/draw' + 'ingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/off' + 'iceDocument/2006/relationships">\n' + ' <c:date1904 val="0"/>\n' + ' <c:roundedCorners val="0"/>\n' + ' <mc:AlternateContent xmlns:mc="http://schemas.openxmlformats.' + 'org/markup-compatibility/2006">\n' + ' <mc:Choice xmlns:c14="http://schemas.microsoft.com/office/d' + 'rawing/2007/8/2/chart" Requires="c14">\n' + ' <c14:style val="118"/>\n' + " </mc:Choice>\n" + " <mc:Fallback>\n" + ' <c:style val="18"/>\n' + " </mc:Fallback>\n" + " </mc:AlternateContent>\n" + " <c:chart>\n" + ' <c:autoTitleDeleted val="0"/>\n' + " <c:plotArea>\n" + " <c:layout/>\n" + " <c:radarChart>\n" + ' <c:radarStyle val="{radar_style}"/>\n' + ' <c:varyColors val="0"/>\n' + "{ser_xml}" + ' <c:axId val="2073612648"/>\n' + ' <c:axId val="-2112772216"/>\n' + " </c:radarChart>\n" + " <c:catAx>\n" + ' <c:axId val="2073612648"/>\n' + " <c:scaling>\n" + ' <c:orientation val="minMax"/>\n' + " </c:scaling>\n" + ' <c:delete val="0"/>\n' + ' <c:axPos val="b"/>\n' + " <c:majorGridlines/>\n" + ' <c:numFmt formatCode="m/d/yy" sourceLinked="1"/>\n' + ' <c:majorTickMark val="out"/>\n' + ' <c:minorTickMark val="none"/>\n' + ' <c:tickLblPos val="nextTo"/>\n' + ' <c:crossAx val="-2112772216"/>\n' + ' <c:crosses val="autoZero"/>\n' + ' <c:auto val="1"/>\n' + ' <c:lblAlgn val="ctr"/>\n' + ' <c:lblOffset val="100"/>\n' + ' <c:noMultiLvlLbl val="0"/>\n' + " </c:catAx>\n" + " <c:valAx>\n" + ' <c:axId val="-2112772216"/>\n' + " <c:scaling>\n" + ' <c:orientation val="minMax"/>\n' + " </c:scaling>\n" + ' <c:delete val="0"/>\n' + ' <c:axPos val="l"/>\n' + " <c:majorGridlines/>\n" + ' <c:numFmt formatCode="General" sourceLinked="1"/>\n' + ' <c:majorTickMark val="cross"/>\n' + ' <c:minorTickMark val="none"/>\n' + ' <c:tickLblPos val="nextTo"/>\n' + ' <c:crossAx val="2073612648"/>\n' + ' <c:crosses val="autoZero"/>\n' + ' <c:crossBetween val="between"/>\n' + " </c:valAx>\n" + " </c:plotArea>\n" + ' <c:plotVisOnly val="1"/>\n' + ' <c:dispBlanksAs val="gap"/>\n' + ' <c:showDLblsOverMax val="0"/>\n' + " </c:chart>\n" + " <c:txPr>\n" + " <a:bodyPr/>\n" + " <a:lstStyle/>\n" + " <a:p>\n" + " <a:pPr>\n" + ' <a:defRPr sz="1800"/>\n' + " </a:pPr>\n" + ' <a:endParaRPr lang="en-US"/>\n' + " </a:p>\n" + " </c:txPr>\n" + "</c:chartSpace>\n" + ).format(**{"radar_style": self._radar_style, "ser_xml": self._ser_xml}) + + @property + def _marker_xml(self): + if self._chart_type == XL_CHART_TYPE.RADAR: + return ( + " <c:marker>\n" + ' <c:symbol val="none"/>\n' + " </c:marker>\n" + ) + return "" + + @property + def _radar_style(self): + if self._chart_type == XL_CHART_TYPE.RADAR_FILLED: + return "filled" + return "marker" + + @property + def _ser_xml(self): + xml = "" + for series in self._chart_data: + xml_writer = _CategorySeriesXmlWriter(series) + xml += ( + " <c:ser>\n" + ' <c:idx val="{ser_idx}"/>\n' + ' <c:order val="{ser_order}"/>\n' + "{tx_xml}" + "{marker_xml}" + "{cat_xml}" + "{val_xml}" + ' <c:smooth val="0"/>\n' + " </c:ser>\n" + ).format( + **{ + "ser_idx": series.index, + "ser_order": series.index, + "tx_xml": xml_writer.tx_xml, + "marker_xml": self._marker_xml, + "cat_xml": xml_writer.cat_xml, + "val_xml": xml_writer.val_xml, + } + ) + return xml + + +class _XyChartXmlWriter(_BaseChartXmlWriter): + """ + Generates XML for the ``<c:scatterChart>`` element. + """ + + @property + def xml(self): + xml = ( + "<?xml version='1.0' encoding='UTF-8' standalone='yes'?>\n" + '<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawin' + 'gml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/draw' + 'ingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/off' + 'iceDocument/2006/relationships">\n' + " <c:chart>\n" + " <c:plotArea>\n" + " <c:scatterChart>\n" + ' <c:scatterStyle val="%s"/>\n' + ' <c:varyColors val="0"/>\n' + "%s" + ' <c:axId val="-2128940872"/>\n' + ' <c:axId val="-2129643912"/>\n' + " </c:scatterChart>\n" + " <c:valAx>\n" + ' <c:axId val="-2128940872"/>\n' + " <c:scaling>\n" + ' <c:orientation val="minMax"/>\n' + " </c:scaling>\n" + ' <c:delete val="0"/>\n' + ' <c:axPos val="b"/>\n' + ' <c:numFmt formatCode="General" sourceLinked="1"/>\n' + ' <c:majorTickMark val="out"/>\n' + ' <c:minorTickMark val="none"/>\n' + ' <c:tickLblPos val="nextTo"/>\n' + ' <c:crossAx val="-2129643912"/>\n' + ' <c:crosses val="autoZero"/>\n' + ' <c:crossBetween val="midCat"/>\n' + " </c:valAx>\n" + " <c:valAx>\n" + ' <c:axId val="-2129643912"/>\n' + " <c:scaling>\n" + ' <c:orientation val="minMax"/>\n' + " </c:scaling>\n" + ' <c:delete val="0"/>\n' + ' <c:axPos val="l"/>\n' + " <c:majorGridlines/>\n" + ' <c:numFmt formatCode="General" sourceLinked="1"/>\n' + ' <c:majorTickMark val="out"/>\n' + ' <c:minorTickMark val="none"/>\n' + ' <c:tickLblPos val="nextTo"/>\n' + ' <c:crossAx val="-2128940872"/>\n' + ' <c:crosses val="autoZero"/>\n' + ' <c:crossBetween val="midCat"/>\n' + " </c:valAx>\n" + " </c:plotArea>\n" + " <c:legend>\n" + ' <c:legendPos val="r"/>\n' + " <c:layout/>\n" + ' <c:overlay val="0"/>\n' + " </c:legend>\n" + ' <c:plotVisOnly val="1"/>\n' + ' <c:dispBlanksAs val="gap"/>\n' + ' <c:showDLblsOverMax val="0"/>\n' + " </c:chart>\n" + " <c:txPr>\n" + " <a:bodyPr/>\n" + " <a:lstStyle/>\n" + " <a:p>\n" + " <a:pPr>\n" + ' <a:defRPr sz="1800"/>\n' + " </a:pPr>\n" + ' <a:endParaRPr lang="en-US"/>\n' + " </a:p>\n" + " </c:txPr>\n" + "</c:chartSpace>\n" + ) % (self._scatterStyle_val, self._ser_xml) + return xml + + @property + def _marker_xml(self): + no_marker_types = ( + XL_CHART_TYPE.XY_SCATTER_LINES_NO_MARKERS, + XL_CHART_TYPE.XY_SCATTER_SMOOTH_NO_MARKERS, + ) + if self._chart_type in no_marker_types: + return ( + " <c:marker>\n" + ' <c:symbol val="none"/>\n' + " </c:marker>\n" + ) + return "" + + @property + def _scatterStyle_val(self): + smooth_types = ( + XL_CHART_TYPE.XY_SCATTER_SMOOTH, + XL_CHART_TYPE.XY_SCATTER_SMOOTH_NO_MARKERS, + ) + if self._chart_type in smooth_types: + return "smoothMarker" + return "lineMarker" + + @property + def _ser_xml(self): + xml = "" + for series in self._chart_data: + xml_writer = _XySeriesXmlWriter(series) + xml += ( + " <c:ser>\n" + ' <c:idx val="{ser_idx}"/>\n' + ' <c:order val="{ser_order}"/>\n' + "{tx_xml}" + "{spPr_xml}" + "{marker_xml}" + "{xVal_xml}" + "{yVal_xml}" + ' <c:smooth val="0"/>\n' + " </c:ser>\n" + ).format( + **{ + "ser_idx": series.index, + "ser_order": series.index, + "tx_xml": xml_writer.tx_xml, + "spPr_xml": self._spPr_xml, + "marker_xml": self._marker_xml, + "xVal_xml": xml_writer.xVal_xml, + "yVal_xml": xml_writer.yVal_xml, + } + ) + return xml + + @property + def _spPr_xml(self): + if self._chart_type == XL_CHART_TYPE.XY_SCATTER: + return ( + " <c:spPr>\n" + ' <a:ln w="47625">\n' + " <a:noFill/>\n" + " </a:ln>\n" + " </c:spPr>\n" + ) + return "" + + +class _BubbleChartXmlWriter(_XyChartXmlWriter): + """ + Provides specialized methods particular to the ``<c:bubbleChart>`` + element. + """ + + @property + def xml(self): + xml = ( + "<?xml version='1.0' encoding='UTF-8' standalone='yes'?>\n" + '<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawin' + 'gml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/draw' + 'ingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/off' + 'iceDocument/2006/relationships">\n' + " <c:chart>\n" + ' <c:autoTitleDeleted val="0"/>\n' + " <c:plotArea>\n" + " <c:layout/>\n" + " <c:bubbleChart>\n" + ' <c:varyColors val="0"/>\n' + "%s" + " <c:dLbls>\n" + ' <c:showLegendKey val="0"/>\n' + ' <c:showVal val="0"/>\n' + ' <c:showCatName val="0"/>\n' + ' <c:showSerName val="0"/>\n' + ' <c:showPercent val="0"/>\n' + ' <c:showBubbleSize val="0"/>\n' + " </c:dLbls>\n" + ' <c:bubbleScale val="100"/>\n' + ' <c:showNegBubbles val="0"/>\n' + ' <c:axId val="-2115720072"/>\n' + ' <c:axId val="-2115723560"/>\n' + " </c:bubbleChart>\n" + " <c:valAx>\n" + ' <c:axId val="-2115720072"/>\n' + " <c:scaling>\n" + ' <c:orientation val="minMax"/>\n' + " </c:scaling>\n" + ' <c:delete val="0"/>\n' + ' <c:axPos val="b"/>\n' + ' <c:numFmt formatCode="General" sourceLinked="1"/>\n' + ' <c:majorTickMark val="out"/>\n' + ' <c:minorTickMark val="none"/>\n' + ' <c:tickLblPos val="nextTo"/>\n' + ' <c:crossAx val="-2115723560"/>\n' + ' <c:crosses val="autoZero"/>\n' + ' <c:crossBetween val="midCat"/>\n' + " </c:valAx>\n" + " <c:valAx>\n" + ' <c:axId val="-2115723560"/>\n' + " <c:scaling>\n" + ' <c:orientation val="minMax"/>\n' + " </c:scaling>\n" + ' <c:delete val="0"/>\n' + ' <c:axPos val="l"/>\n' + " <c:majorGridlines/>\n" + ' <c:numFmt formatCode="General" sourceLinked="1"/>\n' + ' <c:majorTickMark val="out"/>\n' + ' <c:minorTickMark val="none"/>\n' + ' <c:tickLblPos val="nextTo"/>\n' + ' <c:crossAx val="-2115720072"/>\n' + ' <c:crosses val="autoZero"/>\n' + ' <c:crossBetween val="midCat"/>\n' + " </c:valAx>\n" + " </c:plotArea>\n" + " <c:legend>\n" + ' <c:legendPos val="r"/>\n' + " <c:layout/>\n" + ' <c:overlay val="0"/>\n' + " </c:legend>\n" + ' <c:plotVisOnly val="1"/>\n' + ' <c:dispBlanksAs val="gap"/>\n' + ' <c:showDLblsOverMax val="0"/>\n' + " </c:chart>\n" + " <c:txPr>\n" + " <a:bodyPr/>\n" + " <a:lstStyle/>\n" + " <a:p>\n" + " <a:pPr>\n" + ' <a:defRPr sz="1800"/>\n' + " </a:pPr>\n" + ' <a:endParaRPr lang="en-US"/>\n' + " </a:p>\n" + " </c:txPr>\n" + "</c:chartSpace>\n" + ) % self._ser_xml + return xml + + @property + def _bubble3D_val(self): + if self._chart_type == XL_CHART_TYPE.BUBBLE_THREE_D_EFFECT: + return "1" + return "0" + + @property + def _ser_xml(self): + xml = "" + for series in self._chart_data: + xml_writer = _BubbleSeriesXmlWriter(series) + xml += ( + " <c:ser>\n" + ' <c:idx val="{ser_idx}"/>\n' + ' <c:order val="{ser_order}"/>\n' + "{tx_xml}" + ' <c:invertIfNegative val="0"/>\n' + "{xVal_xml}" + "{yVal_xml}" + "{bubbleSize_xml}" + ' <c:bubble3D val="{bubble3D_val}"/>\n' + " </c:ser>\n" + ).format( + **{ + "ser_idx": series.index, + "ser_order": series.index, + "tx_xml": xml_writer.tx_xml, + "xVal_xml": xml_writer.xVal_xml, + "yVal_xml": xml_writer.yVal_xml, + "bubbleSize_xml": xml_writer.bubbleSize_xml, + "bubble3D_val": self._bubble3D_val, + } + ) + return xml + + +class _CategorySeriesXmlWriter(_BaseSeriesXmlWriter): + """ + Generates XML snippets particular to a category chart series. + """ + + @property + def cat(self): + """ + Return the ``<c:cat>`` element XML for this series, as an oxml + element. + """ + categories = self._series.categories + + if categories.are_numeric: + return parse_xml( + self._numRef_cat_tmpl.format( + **{ + "wksht_ref": self._series.categories_ref, + "number_format": categories.number_format, + "cat_count": categories.leaf_count, + "cat_pt_xml": self._cat_num_pt_xml, + "nsdecls": " %s" % nsdecls("c"), + } + ) + ) + + if categories.depth == 1: + return parse_xml( + self._cat_tmpl.format( + **{ + "wksht_ref": self._series.categories_ref, + "cat_count": categories.leaf_count, + "cat_pt_xml": self._cat_pt_xml, + "nsdecls": " %s" % nsdecls("c"), + } + ) + ) + + return parse_xml( + self._multiLvl_cat_tmpl.format( + **{ + "wksht_ref": self._series.categories_ref, + "cat_count": categories.leaf_count, + "lvl_xml": self._lvl_xml(categories), + "nsdecls": " %s" % nsdecls("c"), + } + ) + ) + + @property + def cat_xml(self): + """ + The unicode XML snippet for the ``<c:cat>`` element for this series, + containing the category labels and spreadsheet reference. + """ + categories = self._series.categories + + if categories.are_numeric: + return self._numRef_cat_tmpl.format( + **{ + "wksht_ref": self._series.categories_ref, + "number_format": categories.number_format, + "cat_count": categories.leaf_count, + "cat_pt_xml": self._cat_num_pt_xml, + "nsdecls": "", + } + ) + + if categories.depth == 1: + return self._cat_tmpl.format( + **{ + "wksht_ref": self._series.categories_ref, + "cat_count": categories.leaf_count, + "cat_pt_xml": self._cat_pt_xml, + "nsdecls": "", + } + ) + + return self._multiLvl_cat_tmpl.format( + **{ + "wksht_ref": self._series.categories_ref, + "cat_count": categories.leaf_count, + "lvl_xml": self._lvl_xml(categories), + "nsdecls": "", + } + ) + + @property + def val(self): + """ + The ``<c:val>`` XML for this series, as an oxml element. + """ + xml = self._val_tmpl.format( + **{ + "nsdecls": " %s" % nsdecls("c"), + "values_ref": self._series.values_ref, + "number_format": self._series.number_format, + "val_count": len(self._series), + "val_pt_xml": self._val_pt_xml, + } + ) + return parse_xml(xml) + + @property + def val_xml(self): + """ + Return the unicode XML snippet for the ``<c:val>`` element describing + this series, containing the series values and their spreadsheet range + reference. + """ + return self._val_tmpl.format( + **{ + "nsdecls": "", + "values_ref": self._series.values_ref, + "number_format": self._series.number_format, + "val_count": len(self._series), + "val_pt_xml": self._val_pt_xml, + } + ) + + @property + def _cat_num_pt_xml(self): + """ + The unicode XML snippet for the ``<c:pt>`` elements when category + labels are numeric (including date type). + """ + xml = "" + for idx, category in enumerate(self._series.categories): + xml += ( + ' <c:pt idx="{cat_idx}">\n' + " <c:v>{cat_lbl_str}</c:v>\n" + " </c:pt>\n" + ).format( + **{ + "cat_idx": idx, + "cat_lbl_str": category.numeric_str_val(self._date_1904), + } + ) + return xml + + @property + def _cat_pt_xml(self): + """ + The unicode XML snippet for the ``<c:pt>`` elements containing the + category names for this series. + """ + xml = "" + for idx, category in enumerate(self._series.categories): + xml += ( + ' <c:pt idx="{cat_idx}">\n' + " <c:v>{cat_label}</c:v>\n" + " </c:pt>\n" + ).format(**{"cat_idx": idx, "cat_label": escape(str(category.label))}) + return xml + + @property + def _cat_tmpl(self): + """ + The template for the ``<c:cat>`` element for this series, containing + the category labels and spreadsheet reference. + """ + return ( + " <c:cat{nsdecls}>\n" + " <c:strRef>\n" + " <c:f>{wksht_ref}</c:f>\n" + " <c:strCache>\n" + ' <c:ptCount val="{cat_count}"/>\n' + "{cat_pt_xml}" + " </c:strCache>\n" + " </c:strRef>\n" + " </c:cat>\n" + ) + + def _lvl_xml(self, categories): + """ + The unicode XML snippet for the ``<c:lvl>`` elements containing + multi-level category names. + """ + + def lvl_pt_xml(level): + xml = "" + for idx, name in level: + xml += ( + ' <c:pt idx="%d">\n' + " <c:v>%s</c:v>\n" + " </c:pt>\n" + ) % (idx, escape("%s" % name)) + return xml + + xml = "" + for level in categories.levels: + xml += (" <c:lvl>\n" "{lvl_pt_xml}" " </c:lvl>\n").format( + **{"lvl_pt_xml": lvl_pt_xml(level)} + ) + return xml + + @property + def _multiLvl_cat_tmpl(self): + """ + The template for the ``<c:cat>`` element for this series when there + are multi-level (nested) categories. + """ + return ( + " <c:cat{nsdecls}>\n" + " <c:multiLvlStrRef>\n" + " <c:f>{wksht_ref}</c:f>\n" + " <c:multiLvlStrCache>\n" + ' <c:ptCount val="{cat_count}"/>\n' + "{lvl_xml}" + " </c:multiLvlStrCache>\n" + " </c:multiLvlStrRef>\n" + " </c:cat>\n" + ) + + @property + def _numRef_cat_tmpl(self): + """ + The template for the ``<c:cat>`` element for this series when the + labels are numeric (or date) values. + """ + return ( + " <c:cat{nsdecls}>\n" + " <c:numRef>\n" + " <c:f>{wksht_ref}</c:f>\n" + " <c:numCache>\n" + " <c:formatCode>{number_format}</c:formatCode>\n" + ' <c:ptCount val="{cat_count}"/>\n' + "{cat_pt_xml}" + " </c:numCache>\n" + " </c:numRef>\n" + " </c:cat>\n" + ) + + @property + def _val_pt_xml(self): + """ + The unicode XML snippet containing the ``<c:pt>`` elements containing + the values for this series. + """ + xml = "" + for idx, value in enumerate(self._series.values): + if value is None: + continue + xml += ( + ' <c:pt idx="{val_idx:d}">\n' + " <c:v>{value}</c:v>\n" + " </c:pt>\n" + ).format(**{"val_idx": idx, "value": value}) + return xml + + @property + def _val_tmpl(self): + """ + The template for the ``<c:val>`` element for this series, containing + the series values and their spreadsheet range reference. + """ + return ( + " <c:val{nsdecls}>\n" + " <c:numRef>\n" + " <c:f>{values_ref}</c:f>\n" + " <c:numCache>\n" + " <c:formatCode>{number_format}</c:formatCode>\n" + ' <c:ptCount val="{val_count}"/>\n' + "{val_pt_xml}" + " </c:numCache>\n" + " </c:numRef>\n" + " </c:val>\n" + ) + + +class _XySeriesXmlWriter(_BaseSeriesXmlWriter): + """ + Generates XML snippets particular to an XY series. + """ + + @property + def xVal(self): + """ + Return the ``<c:xVal>`` element for this series as an oxml element. + This element contains the X values for this series. + """ + xml = self._xVal_tmpl.format( + **{ + "nsdecls": " %s" % nsdecls("c"), + "numRef_xml": self.numRef_xml( + self._series.x_values_ref, + self._series.number_format, + self._series.x_values, + ), + } + ) + return parse_xml(xml) + + @property + def xVal_xml(self): + """ + Return the ``<c:xVal>`` element for this series as unicode text. This + element contains the X values for this series. + """ + return self._xVal_tmpl.format( + **{ + "nsdecls": "", + "numRef_xml": self.numRef_xml( + self._series.x_values_ref, + self._series.number_format, + self._series.x_values, + ), + } + ) + + @property + def yVal(self): + """ + Return the ``<c:yVal>`` element for this series as an oxml element. + This element contains the Y values for this series. + """ + xml = self._yVal_tmpl.format( + **{ + "nsdecls": " %s" % nsdecls("c"), + "numRef_xml": self.numRef_xml( + self._series.y_values_ref, + self._series.number_format, + self._series.y_values, + ), + } + ) + return parse_xml(xml) + + @property + def yVal_xml(self): + """ + Return the ``<c:yVal>`` element for this series as unicode text. This + element contains the Y values for this series. + """ + return self._yVal_tmpl.format( + **{ + "nsdecls": "", + "numRef_xml": self.numRef_xml( + self._series.y_values_ref, + self._series.number_format, + self._series.y_values, + ), + } + ) + + @property + def _xVal_tmpl(self): + """ + The template for the ``<c:xVal>`` element for this series, containing + the X values and their spreadsheet range reference. + """ + return " <c:xVal{nsdecls}>\n" "{numRef_xml}" " </c:xVal>\n" + + @property + def _yVal_tmpl(self): + """ + The template for the ``<c:yVal>`` element for this series, containing + the Y values and their spreadsheet range reference. + """ + return " <c:yVal{nsdecls}>\n" "{numRef_xml}" " </c:yVal>\n" + + +class _BubbleSeriesXmlWriter(_XySeriesXmlWriter): + """ + Generates XML snippets particular to a bubble chart series. + """ + + @property + def bubbleSize(self): + """ + Return the ``<c:bubbleSize>`` element for this series as an oxml + element. This element contains the bubble size values for this + series. + """ + xml = self._bubbleSize_tmpl.format( + **{ + "nsdecls": " %s" % nsdecls("c"), + "numRef_xml": self.numRef_xml( + self._series.bubble_sizes_ref, + self._series.number_format, + self._series.bubble_sizes, + ), + } + ) + return parse_xml(xml) + + @property + def bubbleSize_xml(self): + """ + Return the ``<c:bubbleSize>`` element for this series as unicode + text. This element contains the bubble size values for all the + data points in the chart. + """ + return self._bubbleSize_tmpl.format( + **{ + "nsdecls": "", + "numRef_xml": self.numRef_xml( + self._series.bubble_sizes_ref, + self._series.number_format, + self._series.bubble_sizes, + ), + } + ) + + @property + def _bubbleSize_tmpl(self): + """ + The template for the ``<c:bubbleSize>`` element for this series, + containing the bubble size values and their spreadsheet range + reference. + """ + return " <c:bubbleSize{nsdecls}>\n" "{numRef_xml}" " </c:bubbleSize>\n" + + +class _BubbleSeriesXmlRewriter(_BaseSeriesXmlRewriter): + """ + A series rewriter suitable for bubble charts. + """ + + def _rewrite_ser_data(self, ser, series_data, date_1904): + """ + Rewrite the ``<c:tx>``, ``<c:cat>`` and ``<c:val>`` child elements + of *ser* based on the values in *series_data*. + """ + ser._remove_tx() + ser._remove_xVal() + ser._remove_yVal() + ser._remove_bubbleSize() + + xml_writer = _BubbleSeriesXmlWriter(series_data) + + ser._insert_tx(xml_writer.tx) + ser._insert_xVal(xml_writer.xVal) + ser._insert_yVal(xml_writer.yVal) + ser._insert_bubbleSize(xml_writer.bubbleSize) + + +class _CategorySeriesXmlRewriter(_BaseSeriesXmlRewriter): + """ + A series rewriter suitable for category charts. + """ + + def _rewrite_ser_data(self, ser, series_data, date_1904): + """ + Rewrite the ``<c:tx>``, ``<c:cat>`` and ``<c:val>`` child elements + of *ser* based on the values in *series_data*. + """ + ser._remove_tx() + ser._remove_cat() + ser._remove_val() + + xml_writer = _CategorySeriesXmlWriter(series_data, date_1904) + + ser._insert_tx(xml_writer.tx) + ser._insert_cat(xml_writer.cat) + ser._insert_val(xml_writer.val) + + +class _XySeriesXmlRewriter(_BaseSeriesXmlRewriter): + """ + A series rewriter suitable for XY (aka. scatter) charts. + """ + + def _rewrite_ser_data(self, ser, series_data, date_1904): + """ + Rewrite the ``<c:tx>``, ``<c:xVal>`` and ``<c:yVal>`` child elements + of *ser* based on the values in *series_data*. + """ + ser._remove_tx() + ser._remove_xVal() + ser._remove_yVal() + + xml_writer = _XySeriesXmlWriter(series_data) + + ser._insert_tx(xml_writer.tx) + ser._insert_xVal(xml_writer.xVal) + ser._insert_yVal(xml_writer.yVal) |
