about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/pptx/chart
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/pptx/chart
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are here HEAD master
Diffstat (limited to '.venv/lib/python3.12/site-packages/pptx/chart')
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/axis.py523
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/category.py200
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/chart.py280
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/data.py864
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/datalabel.py288
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/legend.py79
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/marker.py70
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/plot.py412
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/point.py101
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/series.py258
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/xlsx.py272
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/xmlwriter.py1840
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)