init: 폴더구조 설계 및 인프라 설계

This commit is contained in:
DongHyeonka
2026-07-24 14:31:53 +09:00
parent 34ad612281
commit f9c463f87a
1839 changed files with 323096 additions and 1 deletions
@@ -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()
@@ -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)
@@ -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)
@@ -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
@@ -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()
@@ -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
@@ -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
@@ -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
@@ -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)
@@ -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)
@@ -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)
File diff suppressed because it is too large Load Diff