python-pptx2 2.13.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pptx2/__init__.py +152 -0
- pptx2/_color.py +75 -0
- pptx2/_slide_importer.py +597 -0
- pptx2/_svg.py +155 -0
- pptx2/_template_applier.py +292 -0
- pptx2/_textstyle.py +187 -0
- pptx2/accessibility.py +365 -0
- pptx2/action.py +270 -0
- pptx2/animation.py +2237 -0
- pptx2/api.py +49 -0
- pptx2/audit.py +258 -0
- pptx2/chart/__init__.py +0 -0
- pptx2/chart/analytics.py +381 -0
- pptx2/chart/axis.py +543 -0
- pptx2/chart/category.py +200 -0
- pptx2/chart/chart.py +670 -0
- pptx2/chart/data.py +864 -0
- pptx2/chart/datalabel.py +406 -0
- pptx2/chart/legend.py +86 -0
- pptx2/chart/marker.py +70 -0
- pptx2/chart/palettes.py +129 -0
- pptx2/chart/plot.py +462 -0
- pptx2/chart/point.py +101 -0
- pptx2/chart/quick_layouts.py +325 -0
- pptx2/chart/series.py +334 -0
- pptx2/chart/xlsx.py +272 -0
- pptx2/chart/xmlwriter.py +1845 -0
- pptx2/compose/__init__.py +28 -0
- pptx2/compose/from_spec.py +1094 -0
- pptx2/design/__init__.py +8 -0
- pptx2/design/components.py +607 -0
- pptx2/design/figures.py +389 -0
- pptx2/design/layout.py +370 -0
- pptx2/design/recipes.py +1967 -0
- pptx2/design/style.py +209 -0
- pptx2/design/tokens.py +915 -0
- pptx2/diagrams.py +754 -0
- pptx2/dml/__init__.py +0 -0
- pptx2/dml/chtfmt.py +40 -0
- pptx2/dml/color.py +496 -0
- pptx2/dml/effect.py +909 -0
- pptx2/dml/fill.py +691 -0
- pptx2/dml/line.py +287 -0
- pptx2/dml/picture.py +212 -0
- pptx2/dml/three_d.py +381 -0
- pptx2/enum/__init__.py +0 -0
- pptx2/enum/action.py +71 -0
- pptx2/enum/animation.py +31 -0
- pptx2/enum/base.py +218 -0
- pptx2/enum/chart.py +574 -0
- pptx2/enum/dml.py +740 -0
- pptx2/enum/lang.py +685 -0
- pptx2/enum/presentation.py +133 -0
- pptx2/enum/shapes.py +1029 -0
- pptx2/enum/text.py +230 -0
- pptx2/exc.py +42 -0
- pptx2/formats.py +139 -0
- pptx2/geometry.py +420 -0
- pptx2/inherit.py +109 -0
- pptx2/lint.py +2256 -0
- pptx2/math.py +177 -0
- pptx2/media.py +197 -0
- pptx2/opc/__init__.py +0 -0
- pptx2/opc/constants.py +332 -0
- pptx2/opc/oxml.py +188 -0
- pptx2/opc/package.py +762 -0
- pptx2/opc/packuri.py +109 -0
- pptx2/opc/serialized.py +296 -0
- pptx2/opc/shared.py +20 -0
- pptx2/opc/spec.py +45 -0
- pptx2/oxml/__init__.py +555 -0
- pptx2/oxml/action.py +53 -0
- pptx2/oxml/chart/__init__.py +0 -0
- pptx2/oxml/chart/axis.py +337 -0
- pptx2/oxml/chart/chart.py +481 -0
- pptx2/oxml/chart/datalabel.py +253 -0
- pptx2/oxml/chart/legend.py +72 -0
- pptx2/oxml/chart/marker.py +61 -0
- pptx2/oxml/chart/plot.py +365 -0
- pptx2/oxml/chart/series.py +425 -0
- pptx2/oxml/chart/shared.py +220 -0
- pptx2/oxml/coreprops.py +288 -0
- pptx2/oxml/dml/__init__.py +0 -0
- pptx2/oxml/dml/color.py +135 -0
- pptx2/oxml/dml/effect.py +213 -0
- pptx2/oxml/dml/fill.py +316 -0
- pptx2/oxml/dml/line.py +12 -0
- pptx2/oxml/dml/three_d.py +110 -0
- pptx2/oxml/ns.py +135 -0
- pptx2/oxml/presentation.py +313 -0
- pptx2/oxml/shapes/__init__.py +19 -0
- pptx2/oxml/shapes/autoshape.py +467 -0
- pptx2/oxml/shapes/connector.py +107 -0
- pptx2/oxml/shapes/graphfrm.py +347 -0
- pptx2/oxml/shapes/groupshape.py +329 -0
- pptx2/oxml/shapes/picture.py +270 -0
- pptx2/oxml/shapes/shared.py +577 -0
- pptx2/oxml/simpletypes.py +1027 -0
- pptx2/oxml/slide.py +563 -0
- pptx2/oxml/table.py +650 -0
- pptx2/oxml/text.py +815 -0
- pptx2/oxml/theme.py +36 -0
- pptx2/oxml/xmlchemy.py +717 -0
- pptx2/package.py +222 -0
- pptx2/parts/__init__.py +0 -0
- pptx2/parts/chart.py +95 -0
- pptx2/parts/coreprops.py +167 -0
- pptx2/parts/diagram.py +37 -0
- pptx2/parts/embeddedpackage.py +93 -0
- pptx2/parts/image.py +275 -0
- pptx2/parts/media.py +37 -0
- pptx2/parts/presentation.py +136 -0
- pptx2/parts/slide.py +371 -0
- pptx2/presentation.py +408 -0
- pptx2/py.typed +0 -0
- pptx2/render.py +586 -0
- pptx2/section.py +272 -0
- pptx2/shapes/__init__.py +26 -0
- pptx2/shapes/autoshape.py +442 -0
- pptx2/shapes/base.py +1078 -0
- pptx2/shapes/connector.py +297 -0
- pptx2/shapes/freeform.py +337 -0
- pptx2/shapes/graphfrm.py +316 -0
- pptx2/shapes/group.py +264 -0
- pptx2/shapes/picture.py +422 -0
- pptx2/shapes/placeholder.py +468 -0
- pptx2/shapes/shapetree.py +2027 -0
- pptx2/shared.py +82 -0
- pptx2/skill/SKILL.md +450 -0
- pptx2/skill/__init__.py +78 -0
- pptx2/skill/__main__.py +64 -0
- pptx2/skill/references/animations.md +189 -0
- pptx2/skill/references/basics.md +421 -0
- pptx2/skill/references/charts.md +254 -0
- pptx2/skill/references/compose.md +234 -0
- pptx2/skill/references/design.md +366 -0
- pptx2/skill/references/effects.md +249 -0
- pptx2/skill/references/end-to-end-deck.md +231 -0
- pptx2/skill/references/geometry-and-arrows.md +334 -0
- pptx2/skill/references/lint.md +275 -0
- pptx2/skill/references/math.md +86 -0
- pptx2/skill/references/picture-effects.md +129 -0
- pptx2/skill/references/render.md +151 -0
- pptx2/skill/references/smart-art.md +75 -0
- pptx2/skill/references/space-aware-authoring.md +249 -0
- pptx2/skill/references/tables.md +244 -0
- pptx2/skill/references/theme.md +127 -0
- pptx2/skill/references/three-d.md +109 -0
- pptx2/skill/references/transitions.md +100 -0
- pptx2/slide.py +1244 -0
- pptx2/smart_art.py +220 -0
- pptx2/spec.py +633 -0
- pptx2/table.py +1181 -0
- pptx2/table_styles.py +184 -0
- pptx2/templates/default.pptx +0 -0
- pptx2/templates/docx-icon.emf +0 -0
- pptx2/templates/generic-icon.emf +0 -0
- pptx2/templates/notes.xml +23 -0
- pptx2/templates/notesMaster.xml +352 -0
- pptx2/templates/pptx-icon.emf +0 -0
- pptx2/templates/theme.xml +321 -0
- pptx2/templates/xlsx-icon.emf +0 -0
- pptx2/text/__init__.py +0 -0
- pptx2/text/fonts.py +482 -0
- pptx2/text/layout.py +374 -0
- pptx2/text/text.py +1272 -0
- pptx2/theme.py +721 -0
- pptx2/types.py +36 -0
- pptx2/util.py +263 -0
- python_pptx2-2.13.0.dist-info/METADATA +351 -0
- python_pptx2-2.13.0.dist-info/RECORD +175 -0
- python_pptx2-2.13.0.dist-info/WHEEL +5 -0
- python_pptx2-2.13.0.dist-info/entry_points.txt +3 -0
- python_pptx2-2.13.0.dist-info/licenses/LICENSE +22 -0
- python_pptx2-2.13.0.dist-info/top_level.txt +1 -0
pptx2/chart/chart.py
ADDED
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
"""Chart-related objects such as Chart and ChartTitle."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
|
|
8
|
+
from pptx2.chart.axis import CategoryAxis, DateAxis, ValueAxis
|
|
9
|
+
from pptx2.chart.legend import Legend
|
|
10
|
+
from pptx2.chart.plot import PlotFactory, PlotTypeInspector
|
|
11
|
+
from pptx2.chart.series import SeriesCollection
|
|
12
|
+
from pptx2.chart.xmlwriter import SeriesXmlRewriterFactory
|
|
13
|
+
from pptx2.dml.chtfmt import ChartFormat
|
|
14
|
+
from pptx2.enum.chart import XL_CHART_TYPE
|
|
15
|
+
from pptx2.shared import ElementProxy, PartElementProxy
|
|
16
|
+
from pptx2.text.text import Font, TextFrame
|
|
17
|
+
from pptx2.util import lazyproperty
|
|
18
|
+
|
|
19
|
+
_SINGLE_SERIES_CHART_TYPES = frozenset(
|
|
20
|
+
{
|
|
21
|
+
XL_CHART_TYPE.PIE,
|
|
22
|
+
XL_CHART_TYPE.PIE_EXPLODED,
|
|
23
|
+
XL_CHART_TYPE.PIE_OF_PIE,
|
|
24
|
+
XL_CHART_TYPE.BAR_OF_PIE,
|
|
25
|
+
XL_CHART_TYPE.THREE_D_PIE,
|
|
26
|
+
XL_CHART_TYPE.THREE_D_PIE_EXPLODED,
|
|
27
|
+
XL_CHART_TYPE.DOUGHNUT,
|
|
28
|
+
XL_CHART_TYPE.DOUGHNUT_EXPLODED,
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Chart types where the user-visible colour is the line stroke
|
|
33
|
+
# (``c:spPr/a:ln/a:solidFill``), not the per-series fill. Setting only
|
|
34
|
+
# the fill on these — as ``apply_palette`` used to do — leaves the
|
|
35
|
+
# rendered line at the default Office palette colour, silently no-op'ing
|
|
36
|
+
# the recolour. Membership picks up every variant of LINE and
|
|
37
|
+
# XY_SCATTER that draws lines.
|
|
38
|
+
_LINE_STROKE_CHART_TYPES = frozenset(
|
|
39
|
+
{
|
|
40
|
+
XL_CHART_TYPE.LINE,
|
|
41
|
+
XL_CHART_TYPE.LINE_MARKERS,
|
|
42
|
+
XL_CHART_TYPE.LINE_MARKERS_STACKED,
|
|
43
|
+
XL_CHART_TYPE.LINE_MARKERS_STACKED_100,
|
|
44
|
+
XL_CHART_TYPE.LINE_STACKED,
|
|
45
|
+
XL_CHART_TYPE.LINE_STACKED_100,
|
|
46
|
+
XL_CHART_TYPE.THREE_D_LINE,
|
|
47
|
+
XL_CHART_TYPE.XY_SCATTER_LINES,
|
|
48
|
+
XL_CHART_TYPE.XY_SCATTER_LINES_NO_MARKERS,
|
|
49
|
+
XL_CHART_TYPE.XY_SCATTER_SMOOTH,
|
|
50
|
+
XL_CHART_TYPE.XY_SCATTER_SMOOTH_NO_MARKERS,
|
|
51
|
+
XL_CHART_TYPE.RADAR,
|
|
52
|
+
XL_CHART_TYPE.RADAR_MARKERS,
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Chart(PartElementProxy):
|
|
58
|
+
"""A chart object."""
|
|
59
|
+
|
|
60
|
+
# Set by ``GraphicFrame.chart`` so callers can navigate back to the
|
|
61
|
+
# parent shape without keeping the ``add_chart`` return value
|
|
62
|
+
# around. Lives on the class so mocks spec'd against ``Chart``
|
|
63
|
+
# (e.g. ``instance_mock(request, Chart)``) see it; the per-instance
|
|
64
|
+
# write in ``GraphicFrame.chart`` shadows the class default.
|
|
65
|
+
_parent_shape = None
|
|
66
|
+
|
|
67
|
+
def __init__(self, chartSpace, chart_part):
|
|
68
|
+
super(Chart, self).__init__(chartSpace, chart_part)
|
|
69
|
+
self._chartSpace = chartSpace
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def shape(self):
|
|
73
|
+
"""The :class:`GraphicFrame` shape that contains this chart.
|
|
74
|
+
|
|
75
|
+
Raises :class:`ValueError` when the chart was reached via a path
|
|
76
|
+
that did not flow through a graphic frame (e.g. constructed
|
|
77
|
+
directly from a chart part). In normal use — chart returned by
|
|
78
|
+
``slide.shapes.add_chart(...).chart`` or by iterating
|
|
79
|
+
``slide.shapes`` — the parent shape is cached on first access.
|
|
80
|
+
|
|
81
|
+
This is the canonical accessor for animating, measuring, or
|
|
82
|
+
styling a chart's parent shape; reach for it instead of
|
|
83
|
+
``chart.element.getparent().getparent()`` or keeping the
|
|
84
|
+
``add_chart`` return value separately.
|
|
85
|
+
"""
|
|
86
|
+
if self._parent_shape is None:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
"chart.shape is unavailable on this Chart: it was not "
|
|
89
|
+
"reached through GraphicFrame.chart. Hold onto the "
|
|
90
|
+
"shape returned by slide.shapes.add_chart(...) (which "
|
|
91
|
+
"is the GraphicFrame), or fetch the chart through "
|
|
92
|
+
"slide.shapes[i].chart, to get a usable .shape ref."
|
|
93
|
+
)
|
|
94
|
+
return self._parent_shape
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def category_axis(self):
|
|
98
|
+
"""
|
|
99
|
+
The category axis of this chart. In the case of an XY or Bubble
|
|
100
|
+
chart, this is the X axis. Raises |ValueError| if no category
|
|
101
|
+
axis is defined (as is the case for a pie chart, for example).
|
|
102
|
+
"""
|
|
103
|
+
catAx_lst = self._chartSpace.catAx_lst
|
|
104
|
+
if catAx_lst:
|
|
105
|
+
return CategoryAxis(catAx_lst[0])
|
|
106
|
+
|
|
107
|
+
dateAx_lst = self._chartSpace.dateAx_lst
|
|
108
|
+
if dateAx_lst:
|
|
109
|
+
return DateAxis(dateAx_lst[0])
|
|
110
|
+
|
|
111
|
+
valAx_lst = self._chartSpace.valAx_lst
|
|
112
|
+
if valAx_lst:
|
|
113
|
+
return ValueAxis(valAx_lst[0])
|
|
114
|
+
|
|
115
|
+
raise ValueError("chart has no category axis")
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def chart_style(self):
|
|
119
|
+
"""
|
|
120
|
+
Read/write integer index of chart style used to format this chart.
|
|
121
|
+
Range is from 1 to 48. Value is |None| if no explicit style has been
|
|
122
|
+
assigned, in which case the default chart style is used. Assigning
|
|
123
|
+
|None| causes any explicit setting to be removed. The integer index
|
|
124
|
+
corresponds to the style's position in the chart style gallery in the
|
|
125
|
+
PowerPoint UI.
|
|
126
|
+
"""
|
|
127
|
+
style = self._chartSpace.style
|
|
128
|
+
if style is None:
|
|
129
|
+
return None
|
|
130
|
+
return style.val
|
|
131
|
+
|
|
132
|
+
@chart_style.setter
|
|
133
|
+
def chart_style(self, value):
|
|
134
|
+
self._chartSpace._remove_style()
|
|
135
|
+
if value is None:
|
|
136
|
+
return
|
|
137
|
+
self._chartSpace._add_style(val=value)
|
|
138
|
+
|
|
139
|
+
def apply_quick_layout(self, layout, **overrides):
|
|
140
|
+
"""Apply a "quick layout" preset to this chart.
|
|
141
|
+
|
|
142
|
+
`layout` is either the name of a built-in preset (see
|
|
143
|
+
:func:`pptx2.chart.quick_layouts.layout_names`) or a dict spec.
|
|
144
|
+
Any keyword arguments are merged on top of the resolved preset and
|
|
145
|
+
override the named-layout values where they collide — e.g.::
|
|
146
|
+
|
|
147
|
+
chart.apply_quick_layout("title_legend_right", title_text="Q4 ARR")
|
|
148
|
+
"""
|
|
149
|
+
from pptx2.chart.quick_layouts import apply_quick_layout
|
|
150
|
+
|
|
151
|
+
apply_quick_layout(self, layout, **overrides)
|
|
152
|
+
|
|
153
|
+
def apply_palette(self, palette):
|
|
154
|
+
"""Recolor every series in this chart from a palette of solid colors.
|
|
155
|
+
|
|
156
|
+
`palette` is either the name of a built-in preset (see
|
|
157
|
+
:func:`pptx2.chart.palettes.palette_names`) or an iterable of
|
|
158
|
+
color-likes — :class:`pptx2.dml.color.RGBColor`, hex strings (with or
|
|
159
|
+
without leading ``'#'``), or 3-tuples of ints in ``0-255``. Colors are
|
|
160
|
+
applied in order; if the chart has more series than colors, the
|
|
161
|
+
palette wraps.
|
|
162
|
+
|
|
163
|
+
This is independent of :attr:`chart_style`: it sets the per-series
|
|
164
|
+
``spPr`` solid-fill foreground color directly, so it overrides the
|
|
165
|
+
theme-derived colors that ``chart_style`` resolves to but leaves the
|
|
166
|
+
``chart_style`` value itself untouched.
|
|
167
|
+
|
|
168
|
+
Pie and doughnut charts have a single series, so per-series colors
|
|
169
|
+
recolour every slice the same. Calling this on a pie/doughnut emits a
|
|
170
|
+
``UserWarning`` and routes through :meth:`color_by_category`, which is
|
|
171
|
+
almost always what was meant. Use :meth:`recolour` for explicit control.
|
|
172
|
+
"""
|
|
173
|
+
if self._is_single_series_chart():
|
|
174
|
+
warnings.warn(
|
|
175
|
+
"apply_palette() is series-level; pie and doughnut charts have "
|
|
176
|
+
"a single series, so per-slice recolouring needs "
|
|
177
|
+
"color_by_category(). Routing through it for you. Call "
|
|
178
|
+
"chart.recolour(palette) to silence this, or "
|
|
179
|
+
"chart.color_by_category(palette) to be explicit.",
|
|
180
|
+
UserWarning,
|
|
181
|
+
stacklevel=2,
|
|
182
|
+
)
|
|
183
|
+
self.color_by_category(palette)
|
|
184
|
+
return
|
|
185
|
+
from pptx2.chart.palettes import resolve_palette
|
|
186
|
+
|
|
187
|
+
colors = resolve_palette(palette)
|
|
188
|
+
is_line_stroke = self._is_line_stroke_chart()
|
|
189
|
+
for idx, series in enumerate(self.series):
|
|
190
|
+
color = colors[idx % len(colors)]
|
|
191
|
+
fill = series.format.fill
|
|
192
|
+
fill.solid()
|
|
193
|
+
fill.fore_color.rgb = color
|
|
194
|
+
# For LINE / scatter-with-lines variants the user-visible
|
|
195
|
+
# colour is the line stroke, not the fill — also pin
|
|
196
|
+
# ``series.format.line.color.rgb`` so the recolour shows up
|
|
197
|
+
# in PowerPoint and LibreOffice.
|
|
198
|
+
if is_line_stroke:
|
|
199
|
+
series.format.line.color.rgb = color
|
|
200
|
+
|
|
201
|
+
def _is_single_series_chart(self):
|
|
202
|
+
"""True for chart types where one series renders as N slices/bars.
|
|
203
|
+
|
|
204
|
+
Pie / doughnut variants conceptually have a single series whose
|
|
205
|
+
points are the user-visible coloured regions. ``apply_palette`` —
|
|
206
|
+
being series-level — therefore can't recolour them per-slice, and
|
|
207
|
+
callers almost always want per-point colouring instead.
|
|
208
|
+
|
|
209
|
+
``IndexError`` is caught alongside the type-detection errors
|
|
210
|
+
because ``self.chart_type`` walks ``self.plots[0]``, which
|
|
211
|
+
raises ``IndexError`` on a chart with no plot element (rare
|
|
212
|
+
but possible for partially-constructed chart parts).
|
|
213
|
+
"""
|
|
214
|
+
try:
|
|
215
|
+
return self.chart_type in _SINGLE_SERIES_CHART_TYPES
|
|
216
|
+
except (NotImplementedError, KeyError, AttributeError, IndexError):
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
def _is_line_stroke_chart(self):
|
|
220
|
+
"""True for chart types where the visible colour is the line stroke.
|
|
221
|
+
|
|
222
|
+
LINE / LINE_MARKERS / XY_SCATTER_LINES and friends render the
|
|
223
|
+
series as a stroked path; the fill colour set by ``apply_palette``
|
|
224
|
+
alone has no visible effect. ``apply_palette`` keys off this to
|
|
225
|
+
also set ``series.format.line.color.rgb``. See
|
|
226
|
+
:meth:`_is_single_series_chart` for the rationale on the
|
|
227
|
+
``IndexError`` branch.
|
|
228
|
+
"""
|
|
229
|
+
try:
|
|
230
|
+
return self.chart_type in _LINE_STROKE_CHART_TYPES
|
|
231
|
+
except (NotImplementedError, KeyError, AttributeError, IndexError):
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
def recolour(self, palette, *, by="auto"):
|
|
235
|
+
"""Recolour the chart from `palette`, auto-dispatching by chart type.
|
|
236
|
+
|
|
237
|
+
This is the recommended single entry point for chart recolouring;
|
|
238
|
+
it picks between :meth:`apply_palette` (series-level) and
|
|
239
|
+
:meth:`color_by_category` (point-level) so callers don't have to
|
|
240
|
+
remember which is right for their chart type.
|
|
241
|
+
|
|
242
|
+
``by``:
|
|
243
|
+
|
|
244
|
+
* ``"auto"`` (default) — point-level for pie / doughnut, otherwise
|
|
245
|
+
series-level. Matches user intent in nearly every case.
|
|
246
|
+
* ``"series"`` — force series-level (same as
|
|
247
|
+
:meth:`apply_palette`).
|
|
248
|
+
* ``"category"`` — force point-level (same as
|
|
249
|
+
:meth:`color_by_category`).
|
|
250
|
+
|
|
251
|
+
`palette` accepts the same forms as :meth:`apply_palette`.
|
|
252
|
+
"""
|
|
253
|
+
if by not in ("auto", "series", "category"):
|
|
254
|
+
raise ValueError(f"by must be 'auto', 'series', or 'category'; got {by!r}")
|
|
255
|
+
if by == "category" or (by == "auto" and self._is_single_series_chart()):
|
|
256
|
+
self.color_by_category(palette)
|
|
257
|
+
else:
|
|
258
|
+
# Skip the soft-warning route in apply_palette for the explicit
|
|
259
|
+
# series path — caller asked for it, no second-guessing.
|
|
260
|
+
from pptx2.chart.palettes import resolve_palette
|
|
261
|
+
|
|
262
|
+
colors = resolve_palette(palette)
|
|
263
|
+
is_line_stroke = self._is_line_stroke_chart()
|
|
264
|
+
for idx, series in enumerate(self.series):
|
|
265
|
+
color = colors[idx % len(colors)]
|
|
266
|
+
fill = series.format.fill
|
|
267
|
+
fill.solid()
|
|
268
|
+
fill.fore_color.rgb = color
|
|
269
|
+
if is_line_stroke:
|
|
270
|
+
series.format.line.color.rgb = color
|
|
271
|
+
|
|
272
|
+
# US-spelling alias kept stable.
|
|
273
|
+
recolor = recolour
|
|
274
|
+
|
|
275
|
+
def color_by_category(self, palette):
|
|
276
|
+
"""Recolor every *data point* (category) instead of every series.
|
|
277
|
+
|
|
278
|
+
Useful for stacked-bar / stacked-column charts where you want each
|
|
279
|
+
category segment within a stack to read as a discrete color. Also
|
|
280
|
+
works for single-series column/bar charts (each bar gets its own
|
|
281
|
+
color).
|
|
282
|
+
|
|
283
|
+
`palette` accepts the same forms as :meth:`apply_palette`: a
|
|
284
|
+
named preset, or an iterable of color-likes.
|
|
285
|
+
|
|
286
|
+
For each series, the palette is walked in order across the
|
|
287
|
+
category points, wrapping when there are more categories than
|
|
288
|
+
colors. The palette is the same for every series so a given
|
|
289
|
+
category index resolves to the same color across the whole chart.
|
|
290
|
+
"""
|
|
291
|
+
from pptx2.chart.palettes import resolve_palette
|
|
292
|
+
|
|
293
|
+
colors = resolve_palette(palette)
|
|
294
|
+
for series in self.series:
|
|
295
|
+
try:
|
|
296
|
+
points = series.points
|
|
297
|
+
except AttributeError:
|
|
298
|
+
continue
|
|
299
|
+
for cat_idx, point in enumerate(points):
|
|
300
|
+
fill = point.format.fill
|
|
301
|
+
fill.solid()
|
|
302
|
+
fill.fore_color.rgb = colors[cat_idx % len(colors)]
|
|
303
|
+
|
|
304
|
+
@property
|
|
305
|
+
def chart_title(self):
|
|
306
|
+
"""A |ChartTitle| object providing access to title properties.
|
|
307
|
+
|
|
308
|
+
Calling this property is destructive in the sense it adds a chart
|
|
309
|
+
title element (`c:title`) to the chart XML if one is not already
|
|
310
|
+
present. Use :attr:`has_title` to test for presence of a chart title
|
|
311
|
+
non-destructively.
|
|
312
|
+
"""
|
|
313
|
+
return ChartTitle(self._element.get_or_add_title())
|
|
314
|
+
|
|
315
|
+
@property
|
|
316
|
+
def text_color(self):
|
|
317
|
+
"""Write-only facade — read is unsupported.
|
|
318
|
+
|
|
319
|
+
Charts inherit text colour from theme slot ``tx1`` (which defaults
|
|
320
|
+
to black on a light theme). On a dark deck that means manually
|
|
321
|
+
threading the same colour through ``chart.font.color``,
|
|
322
|
+
``chart.legend.font.color``, ``chart.chart_title.text_frame``
|
|
323
|
+
runs, and every plot's ``data_labels.font.color`` — the most
|
|
324
|
+
common copy-paste in dark-deck authoring.
|
|
325
|
+
|
|
326
|
+
Assigning ``chart.text_color = "#FFFFFF"`` (or an
|
|
327
|
+
:class:`~pptx2.dml.color.RGBColor` / ``(r, g, b)`` tuple)
|
|
328
|
+
walks all four locations and pins them. Read is unsupported (no
|
|
329
|
+
canonical "single" text colour); read individual fonts instead.
|
|
330
|
+
"""
|
|
331
|
+
raise AttributeError(
|
|
332
|
+
"chart.text_color is write-only; read individual fonts "
|
|
333
|
+
"(chart.font.color, chart.legend.font.color, …) instead."
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
@text_color.setter
|
|
337
|
+
def text_color(self, value):
|
|
338
|
+
from pptx2._color import coerce_color
|
|
339
|
+
|
|
340
|
+
try:
|
|
341
|
+
rgb = coerce_color(value)
|
|
342
|
+
except (TypeError, ValueError) as exc:
|
|
343
|
+
raise TypeError(
|
|
344
|
+
"text_color must be RGBColor, 6-digit hex string with or "
|
|
345
|
+
"without '#', or (r, g, b) tuple; got "
|
|
346
|
+
f"{type(value).__name__}: {exc}"
|
|
347
|
+
) from exc
|
|
348
|
+
|
|
349
|
+
# 1. Chart-wide default text properties (c:chartSpace/c:txPr).
|
|
350
|
+
self.font.color.rgb = rgb
|
|
351
|
+
|
|
352
|
+
# 2. Legend font — only when one is present, so the read doesn't
|
|
353
|
+
# silently materialise a legend element.
|
|
354
|
+
if self.has_legend:
|
|
355
|
+
self.legend.font.color.rgb = rgb
|
|
356
|
+
|
|
357
|
+
# 3. Chart title — only when both the title element and its
|
|
358
|
+
# rich-text frame already exist. Reading
|
|
359
|
+
# ``chart_title.text_frame`` would otherwise materialise a
|
|
360
|
+
# ``<c:tx><c:rich>...</c:rich></c:tx>`` subtree on a title that's
|
|
361
|
+
# currently empty / inheriting, which is a side-effect callers
|
|
362
|
+
# don't expect from a colour-setting facade.
|
|
363
|
+
if self.has_title:
|
|
364
|
+
chart_title = self.chart_title
|
|
365
|
+
if chart_title.has_text_frame:
|
|
366
|
+
for paragraph in chart_title.text_frame.paragraphs:
|
|
367
|
+
for run in paragraph.runs:
|
|
368
|
+
run.font.color.rgb = rgb
|
|
369
|
+
|
|
370
|
+
# 4. Per-plot data labels — only on plots that already have them.
|
|
371
|
+
for plot in self.plots:
|
|
372
|
+
if plot.has_data_labels:
|
|
373
|
+
plot.data_labels.font.color.rgb = rgb
|
|
374
|
+
|
|
375
|
+
@property
|
|
376
|
+
def line_color(self):
|
|
377
|
+
"""Write-only facade — read is unsupported.
|
|
378
|
+
|
|
379
|
+
On a dark deck the default axis lines and gridlines render as
|
|
380
|
+
dim grey on a dark background and look broken. Pinning each
|
|
381
|
+
axis line + gridline takes 4–6 separate writes; assigning
|
|
382
|
+
``chart.line_color = "#3a3e5f"`` walks them all and skips the
|
|
383
|
+
ones that don't apply to this chart type (e.g. pie / doughnut
|
|
384
|
+
have no axes).
|
|
385
|
+
|
|
386
|
+
The set covered:
|
|
387
|
+
|
|
388
|
+
* ``category_axis.format.line``
|
|
389
|
+
* ``value_axis.format.line``
|
|
390
|
+
* ``category_axis.major_gridlines.format.line`` (if present)
|
|
391
|
+
* ``value_axis.major_gridlines.format.line`` (if present)
|
|
392
|
+
|
|
393
|
+
Materialisation is deliberately conservative: gridlines aren't
|
|
394
|
+
created if absent (no surprise change of chart appearance), and
|
|
395
|
+
on chart types without axes the property is a no-op rather than
|
|
396
|
+
raising.
|
|
397
|
+
"""
|
|
398
|
+
raise AttributeError(
|
|
399
|
+
"chart.line_color is write-only; read individual format.line "
|
|
400
|
+
"objects (chart.category_axis.format.line.color, …) instead."
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
@line_color.setter
|
|
404
|
+
def line_color(self, value):
|
|
405
|
+
from pptx2._color import coerce_color
|
|
406
|
+
|
|
407
|
+
try:
|
|
408
|
+
rgb = coerce_color(value)
|
|
409
|
+
except (TypeError, ValueError) as exc:
|
|
410
|
+
raise TypeError(
|
|
411
|
+
"line_color must be RGBColor, 6-digit hex string with or "
|
|
412
|
+
"without '#', or (r, g, b) tuple; got "
|
|
413
|
+
f"{type(value).__name__}: {exc}"
|
|
414
|
+
) from exc
|
|
415
|
+
|
|
416
|
+
for axis_attr in ("category_axis", "value_axis"):
|
|
417
|
+
try:
|
|
418
|
+
axis = getattr(self, axis_attr)
|
|
419
|
+
except ValueError:
|
|
420
|
+
# Pie / doughnut etc. — no axis of this kind. Skip silently.
|
|
421
|
+
continue
|
|
422
|
+
axis.format.line.color.rgb = rgb
|
|
423
|
+
if axis.has_major_gridlines:
|
|
424
|
+
axis.major_gridlines.format.line.color.rgb = rgb
|
|
425
|
+
|
|
426
|
+
def apply_dark_theme(self, *, text="#FFFFFF", line="#3A3E5F"):
|
|
427
|
+
"""One-call dark-theme styling for the chart.
|
|
428
|
+
|
|
429
|
+
Equivalent to::
|
|
430
|
+
|
|
431
|
+
chart.text_color = text
|
|
432
|
+
chart.line_color = line
|
|
433
|
+
|
|
434
|
+
Pins every text-bearing element to ``text`` and every axis line
|
|
435
|
+
+ gridline to ``line``. Both arguments accept any colour-like
|
|
436
|
+
value (``RGBColor``, hex string, ``(r, g, b)`` tuple).
|
|
437
|
+
|
|
438
|
+
This is a deliberately small, opinionated convenience; for full
|
|
439
|
+
control reach for ``text_color`` and ``line_color`` directly, or
|
|
440
|
+
style each location explicitly.
|
|
441
|
+
"""
|
|
442
|
+
self.text_color = text
|
|
443
|
+
self.line_color = line
|
|
444
|
+
|
|
445
|
+
@property
|
|
446
|
+
def chart_type(self):
|
|
447
|
+
"""Member of :ref:`XlChartType` enumeration specifying type of this chart.
|
|
448
|
+
|
|
449
|
+
If the chart has two plots, for example, a line plot overlayed on a bar plot,
|
|
450
|
+
the type reported is for the first (back-most) plot. Read-only.
|
|
451
|
+
"""
|
|
452
|
+
first_plot = self.plots[0]
|
|
453
|
+
return PlotTypeInspector.chart_type(first_plot)
|
|
454
|
+
|
|
455
|
+
@lazyproperty
|
|
456
|
+
def font(self):
|
|
457
|
+
"""Font object controlling text format defaults for this chart."""
|
|
458
|
+
defRPr = self._chartSpace.get_or_add_txPr().p_lst[0].get_or_add_pPr().get_or_add_defRPr()
|
|
459
|
+
return Font(defRPr)
|
|
460
|
+
|
|
461
|
+
@property
|
|
462
|
+
def has_legend(self):
|
|
463
|
+
"""
|
|
464
|
+
Read/write boolean, |True| if the chart has a legend. Assigning
|
|
465
|
+
|True| causes a legend to be added to the chart if it doesn't already
|
|
466
|
+
have one. Assigning False removes any existing legend definition
|
|
467
|
+
along with any existing legend settings.
|
|
468
|
+
"""
|
|
469
|
+
return self._chartSpace.chart.has_legend
|
|
470
|
+
|
|
471
|
+
@has_legend.setter
|
|
472
|
+
def has_legend(self, value):
|
|
473
|
+
self._chartSpace.chart.has_legend = bool(value)
|
|
474
|
+
|
|
475
|
+
@property
|
|
476
|
+
def has_title(self):
|
|
477
|
+
"""Read/write boolean, specifying whether this chart has a title.
|
|
478
|
+
|
|
479
|
+
Assigning |True| causes a title to be added if not already present.
|
|
480
|
+
Assigning |False| removes any existing title along with its text and
|
|
481
|
+
settings.
|
|
482
|
+
"""
|
|
483
|
+
title = self._chartSpace.chart.title
|
|
484
|
+
if title is None:
|
|
485
|
+
return False
|
|
486
|
+
return True
|
|
487
|
+
|
|
488
|
+
@has_title.setter
|
|
489
|
+
def has_title(self, value):
|
|
490
|
+
chart = self._chartSpace.chart
|
|
491
|
+
if bool(value) is False:
|
|
492
|
+
chart._remove_title()
|
|
493
|
+
autoTitleDeleted = chart.get_or_add_autoTitleDeleted()
|
|
494
|
+
autoTitleDeleted.val = True
|
|
495
|
+
return
|
|
496
|
+
chart.get_or_add_title()
|
|
497
|
+
|
|
498
|
+
@property
|
|
499
|
+
def legend(self):
|
|
500
|
+
"""
|
|
501
|
+
A |Legend| object providing access to the properties of the legend
|
|
502
|
+
for this chart.
|
|
503
|
+
"""
|
|
504
|
+
legend_elm = self._chartSpace.chart.legend
|
|
505
|
+
if legend_elm is None:
|
|
506
|
+
return None
|
|
507
|
+
return Legend(legend_elm)
|
|
508
|
+
|
|
509
|
+
@lazyproperty
|
|
510
|
+
def plots(self):
|
|
511
|
+
"""
|
|
512
|
+
The sequence of plots in this chart. A plot, called a *chart group*
|
|
513
|
+
in the Microsoft API, is a distinct sequence of one or more series
|
|
514
|
+
depicted in a particular charting type. For example, a chart having
|
|
515
|
+
a series plotted as a line overlaid on three series plotted as
|
|
516
|
+
columns would have two plots; the first corresponding to the three
|
|
517
|
+
column series and the second to the line series. Plots are sequenced
|
|
518
|
+
in the order drawn, i.e. back-most to front-most. Supports *len()*,
|
|
519
|
+
membership (e.g. ``p in plots``), iteration, slicing, and indexed
|
|
520
|
+
access (e.g. ``plot = plots[i]``).
|
|
521
|
+
"""
|
|
522
|
+
plotArea = self._chartSpace.chart.plotArea
|
|
523
|
+
return _Plots(plotArea, self)
|
|
524
|
+
|
|
525
|
+
def replace_data(self, chart_data):
|
|
526
|
+
"""
|
|
527
|
+
Use the categories and series values in the |ChartData| object
|
|
528
|
+
*chart_data* to replace those in the XML and Excel worksheet for this
|
|
529
|
+
chart.
|
|
530
|
+
"""
|
|
531
|
+
rewriter = SeriesXmlRewriterFactory(self.chart_type, chart_data)
|
|
532
|
+
rewriter.replace_series_data(self._chartSpace)
|
|
533
|
+
self._workbook.update_from_xlsx_blob(chart_data.xlsx_blob)
|
|
534
|
+
|
|
535
|
+
@lazyproperty
|
|
536
|
+
def series(self):
|
|
537
|
+
"""
|
|
538
|
+
A |SeriesCollection| object containing all the series in this
|
|
539
|
+
chart. When the chart has multiple plots, all the series for the
|
|
540
|
+
first plot appear before all those for the second, and so on. Series
|
|
541
|
+
within a plot have an explicit ordering and appear in that sequence.
|
|
542
|
+
"""
|
|
543
|
+
return SeriesCollection(self._chartSpace.plotArea)
|
|
544
|
+
|
|
545
|
+
@property
|
|
546
|
+
def value_axis(self):
|
|
547
|
+
"""
|
|
548
|
+
The |ValueAxis| object providing access to properties of the value
|
|
549
|
+
axis of this chart. Raises |ValueError| if the chart has no value
|
|
550
|
+
axis.
|
|
551
|
+
"""
|
|
552
|
+
valAx_lst = self._chartSpace.valAx_lst
|
|
553
|
+
if not valAx_lst:
|
|
554
|
+
raise ValueError("chart has no value axis")
|
|
555
|
+
|
|
556
|
+
idx = 1 if len(valAx_lst) > 1 else 0
|
|
557
|
+
return ValueAxis(valAx_lst[idx])
|
|
558
|
+
|
|
559
|
+
@property
|
|
560
|
+
def secondary_value_axis(self):
|
|
561
|
+
"""A |ValueAxis| for the chart's secondary (right-hand) value axis.
|
|
562
|
+
|
|
563
|
+
Accessing this property is *destructive*: if the chart has only a
|
|
564
|
+
single value axis it adds a secondary value axis (plus a hidden
|
|
565
|
+
secondary category axis it crosses), then re-points the front-most
|
|
566
|
+
plot — ``chart.plots[1]`` in a two-plot combo chart — onto the new
|
|
567
|
+
axes so that plot is measured against the secondary scale. If a
|
|
568
|
+
secondary value axis already exists it is returned unchanged.
|
|
569
|
+
|
|
570
|
+
Newly-allocated axis ids stay within the signed-int32 range
|
|
571
|
+
(``1..2**31-1``); ids at or above ``2**31`` make PowerPoint flag the
|
|
572
|
+
file for repair.
|
|
573
|
+
|
|
574
|
+
Raises |ValueError| if the chart type has no value axis (e.g. a pie
|
|
575
|
+
chart).
|
|
576
|
+
"""
|
|
577
|
+
plotArea = self._chartSpace.plotArea
|
|
578
|
+
if not plotArea.xpath("c:valAx"):
|
|
579
|
+
raise ValueError("chart has no value axis")
|
|
580
|
+
valAx = plotArea.add_secondary_value_axis()
|
|
581
|
+
return ValueAxis(valAx)
|
|
582
|
+
|
|
583
|
+
@property
|
|
584
|
+
def _workbook(self):
|
|
585
|
+
"""
|
|
586
|
+
The |ChartWorkbook| object providing access to the Excel source data
|
|
587
|
+
for this chart.
|
|
588
|
+
"""
|
|
589
|
+
return self.part.chart_workbook
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
class ChartTitle(ElementProxy):
|
|
593
|
+
"""Provides properties for manipulating a chart title."""
|
|
594
|
+
|
|
595
|
+
# This shares functionality with AxisTitle, which could be factored out
|
|
596
|
+
# into a base class, perhaps pptx2.chart.shared.BaseTitle. I suspect they
|
|
597
|
+
# actually differ in certain fuller behaviors, but at present they're
|
|
598
|
+
# essentially identical.
|
|
599
|
+
|
|
600
|
+
def __init__(self, title):
|
|
601
|
+
super(ChartTitle, self).__init__(title)
|
|
602
|
+
self._title = title
|
|
603
|
+
|
|
604
|
+
@lazyproperty
|
|
605
|
+
def format(self):
|
|
606
|
+
"""|ChartFormat| object providing access to line and fill formatting.
|
|
607
|
+
|
|
608
|
+
Return the |ChartFormat| object providing shape formatting properties
|
|
609
|
+
for this chart title, such as its line color and fill.
|
|
610
|
+
"""
|
|
611
|
+
return ChartFormat(self._title)
|
|
612
|
+
|
|
613
|
+
@property
|
|
614
|
+
def has_text_frame(self):
|
|
615
|
+
"""Read/write Boolean specifying whether this title has a text frame.
|
|
616
|
+
|
|
617
|
+
Return |True| if this chart title has a text frame, and |False|
|
|
618
|
+
otherwise. Assigning |True| causes a text frame to be added if not
|
|
619
|
+
already present. Assigning |False| causes any existing text frame to
|
|
620
|
+
be removed along with its text and formatting.
|
|
621
|
+
"""
|
|
622
|
+
if self._title.tx_rich is None:
|
|
623
|
+
return False
|
|
624
|
+
return True
|
|
625
|
+
|
|
626
|
+
@has_text_frame.setter
|
|
627
|
+
def has_text_frame(self, value):
|
|
628
|
+
if bool(value) is False:
|
|
629
|
+
self._title._remove_tx()
|
|
630
|
+
return
|
|
631
|
+
self._title.get_or_add_tx_rich()
|
|
632
|
+
|
|
633
|
+
@property
|
|
634
|
+
def text_frame(self):
|
|
635
|
+
"""|TextFrame| instance for this chart title.
|
|
636
|
+
|
|
637
|
+
Return a |TextFrame| instance allowing read/write access to the text
|
|
638
|
+
of this chart title and its text formatting properties. Accessing this
|
|
639
|
+
property is destructive in the sense it adds a text frame if one is
|
|
640
|
+
not present. Use :attr:`has_text_frame` to test for the presence of
|
|
641
|
+
a text frame non-destructively.
|
|
642
|
+
"""
|
|
643
|
+
rich = self._title.get_or_add_tx_rich()
|
|
644
|
+
return TextFrame(rich, self)
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
class _Plots(Sequence):
|
|
648
|
+
"""
|
|
649
|
+
The sequence of plots in a chart, such as a bar plot or a line plot. Most
|
|
650
|
+
charts have only a single plot. The concept is necessary when two chart
|
|
651
|
+
types are displayed in a single set of axes, like a bar plot with
|
|
652
|
+
a superimposed line plot.
|
|
653
|
+
"""
|
|
654
|
+
|
|
655
|
+
def __init__(self, plotArea, chart):
|
|
656
|
+
super(_Plots, self).__init__()
|
|
657
|
+
self._plotArea = plotArea
|
|
658
|
+
self._chart = chart
|
|
659
|
+
|
|
660
|
+
def __getitem__(self, index):
|
|
661
|
+
xCharts = self._plotArea.xCharts
|
|
662
|
+
if isinstance(index, slice):
|
|
663
|
+
plots = [PlotFactory(xChart, self._chart) for xChart in xCharts]
|
|
664
|
+
return plots[index]
|
|
665
|
+
else:
|
|
666
|
+
xChart = xCharts[index]
|
|
667
|
+
return PlotFactory(xChart, self._chart)
|
|
668
|
+
|
|
669
|
+
def __len__(self):
|
|
670
|
+
return len(self._plotArea.xCharts)
|