"""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
[docs]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)
[docs] @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)
[docs] @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)
[docs] 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)
[docs] @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
[docs]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
@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)