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
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""Named chart "quick layouts" mirroring PowerPoint's gallery.
|
|
2
|
+
|
|
3
|
+
PowerPoint's *Chart Design → Quick Layout* gallery exposes 10-11 named
|
|
4
|
+
layout presets that toggle title / legend / axis-title / gridline
|
|
5
|
+
visibility in opinionated combinations. Replicating *every* layout from
|
|
6
|
+
the gallery requires a couple of features we don't yet model (data
|
|
7
|
+
labels positioned inside bars, "best fit" pie labels), so this module
|
|
8
|
+
ships the subset that maps cleanly onto the existing high-level API:
|
|
9
|
+
title, legend (with position), axis titles, and gridlines.
|
|
10
|
+
|
|
11
|
+
Public surface::
|
|
12
|
+
|
|
13
|
+
from pptx2.chart.quick_layouts import (
|
|
14
|
+
apply_quick_layout, layout_names, QUICK_LAYOUTS,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
chart.apply_quick_layout("title_legend_right") # convenience method on Chart
|
|
18
|
+
apply_quick_layout(chart, "title_legend_right") # functional form
|
|
19
|
+
|
|
20
|
+
A layout spec is a plain dict with any of the keys below. Missing keys
|
|
21
|
+
mean "don't touch this property" so two layouts can be composed by
|
|
22
|
+
calling ``apply_quick_layout`` twice.
|
|
23
|
+
|
|
24
|
+
============================== ==========================================
|
|
25
|
+
``has_title`` bool — toggle the chart title.
|
|
26
|
+
``title_text`` str — set chart title text (forces ``has_title=True``).
|
|
27
|
+
``has_legend`` bool — toggle the legend.
|
|
28
|
+
``legend_position`` ``XL_LEGEND_POSITION`` member *or* its
|
|
29
|
+
lowercase string name (``"right"``, ``"left"``,
|
|
30
|
+
``"top"``, ``"bottom"``, ``"corner"``).
|
|
31
|
+
``legend_in_layout`` bool — whether the legend overlaps the plot area.
|
|
32
|
+
``has_category_axis_title`` bool — toggle the category-axis title.
|
|
33
|
+
``category_axis_title_text`` str — set category-axis title text.
|
|
34
|
+
``has_value_axis_title`` bool — toggle the value-axis title.
|
|
35
|
+
``value_axis_title_text`` str — set value-axis title text.
|
|
36
|
+
``has_major_gridlines`` bool — value-axis major gridlines.
|
|
37
|
+
``has_minor_gridlines`` bool — value-axis minor gridlines.
|
|
38
|
+
============================== ==========================================
|
|
39
|
+
|
|
40
|
+
Charts without a category axis (e.g. pie charts) silently skip the
|
|
41
|
+
category-axis keys; same for value-axis keys on charts without a value
|
|
42
|
+
axis. This matches how PowerPoint's gallery degrades.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
from typing import Any, Mapping
|
|
48
|
+
|
|
49
|
+
from pptx2.enum.chart import XL_LEGEND_POSITION
|
|
50
|
+
|
|
51
|
+
QUICK_LAYOUTS: dict[str, dict[str, Any]] = {
|
|
52
|
+
# --- Layouts 1-3 from the gallery: title + variations on legend slot --
|
|
53
|
+
"title_legend_right": {
|
|
54
|
+
"has_title": True,
|
|
55
|
+
"has_legend": True,
|
|
56
|
+
"legend_position": XL_LEGEND_POSITION.RIGHT,
|
|
57
|
+
"legend_in_layout": False,
|
|
58
|
+
"has_major_gridlines": True,
|
|
59
|
+
"has_minor_gridlines": False,
|
|
60
|
+
},
|
|
61
|
+
"title_legend_bottom": {
|
|
62
|
+
"has_title": True,
|
|
63
|
+
"has_legend": True,
|
|
64
|
+
"legend_position": XL_LEGEND_POSITION.BOTTOM,
|
|
65
|
+
"legend_in_layout": False,
|
|
66
|
+
"has_major_gridlines": True,
|
|
67
|
+
"has_minor_gridlines": False,
|
|
68
|
+
},
|
|
69
|
+
"title_legend_top": {
|
|
70
|
+
"has_title": True,
|
|
71
|
+
"has_legend": True,
|
|
72
|
+
"legend_position": XL_LEGEND_POSITION.TOP,
|
|
73
|
+
"legend_in_layout": False,
|
|
74
|
+
"has_major_gridlines": True,
|
|
75
|
+
"has_minor_gridlines": False,
|
|
76
|
+
},
|
|
77
|
+
"title_legend_left": {
|
|
78
|
+
"has_title": True,
|
|
79
|
+
"has_legend": True,
|
|
80
|
+
"legend_position": XL_LEGEND_POSITION.LEFT,
|
|
81
|
+
"legend_in_layout": False,
|
|
82
|
+
"has_major_gridlines": True,
|
|
83
|
+
"has_minor_gridlines": False,
|
|
84
|
+
},
|
|
85
|
+
"title_no_legend": {
|
|
86
|
+
"has_title": True,
|
|
87
|
+
"has_legend": False,
|
|
88
|
+
"has_major_gridlines": True,
|
|
89
|
+
"has_minor_gridlines": False,
|
|
90
|
+
},
|
|
91
|
+
"no_title_no_legend": {
|
|
92
|
+
"has_title": False,
|
|
93
|
+
"has_legend": False,
|
|
94
|
+
"has_major_gridlines": True,
|
|
95
|
+
"has_minor_gridlines": False,
|
|
96
|
+
},
|
|
97
|
+
# --- Layouts with axis titles -----------------------------------------
|
|
98
|
+
"title_axes_legend_right": {
|
|
99
|
+
"has_title": True,
|
|
100
|
+
"has_legend": True,
|
|
101
|
+
"legend_position": XL_LEGEND_POSITION.RIGHT,
|
|
102
|
+
"legend_in_layout": False,
|
|
103
|
+
"has_category_axis_title": True,
|
|
104
|
+
"has_value_axis_title": True,
|
|
105
|
+
"has_major_gridlines": True,
|
|
106
|
+
"has_minor_gridlines": False,
|
|
107
|
+
},
|
|
108
|
+
"title_axes_legend_bottom": {
|
|
109
|
+
"has_title": True,
|
|
110
|
+
"has_legend": True,
|
|
111
|
+
"legend_position": XL_LEGEND_POSITION.BOTTOM,
|
|
112
|
+
"legend_in_layout": False,
|
|
113
|
+
"has_category_axis_title": True,
|
|
114
|
+
"has_value_axis_title": True,
|
|
115
|
+
"has_major_gridlines": True,
|
|
116
|
+
"has_minor_gridlines": False,
|
|
117
|
+
},
|
|
118
|
+
# --- Minimal / dense -------------------------------------------------
|
|
119
|
+
"minimal": {
|
|
120
|
+
"has_title": False,
|
|
121
|
+
"has_legend": False,
|
|
122
|
+
"has_major_gridlines": False,
|
|
123
|
+
"has_minor_gridlines": False,
|
|
124
|
+
},
|
|
125
|
+
"dense": {
|
|
126
|
+
"has_title": True,
|
|
127
|
+
"has_legend": True,
|
|
128
|
+
"legend_position": XL_LEGEND_POSITION.RIGHT,
|
|
129
|
+
"legend_in_layout": False,
|
|
130
|
+
"has_major_gridlines": True,
|
|
131
|
+
"has_minor_gridlines": True,
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def layout_names() -> tuple[str, ...]:
|
|
137
|
+
"""Return the names of the built-in quick layouts, in declaration order."""
|
|
138
|
+
return tuple(QUICK_LAYOUTS.keys())
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def apply_quick_layout(chart, layout, **overrides) -> None:
|
|
142
|
+
"""Apply a quick-layout preset to `chart`.
|
|
143
|
+
|
|
144
|
+
`layout` is either the name of a built-in preset (see
|
|
145
|
+
:func:`layout_names`) or a dict in the spec format described in the
|
|
146
|
+
module docstring. Missing spec keys are left untouched on the chart,
|
|
147
|
+
so layouts can be composed. Charts that lack a category or value
|
|
148
|
+
axis silently skip the corresponding axis keys.
|
|
149
|
+
|
|
150
|
+
Any keyword arguments are merged on top of the resolved layout — they
|
|
151
|
+
override the preset where they collide. This is the pattern for
|
|
152
|
+
"use this preset, but with this title"::
|
|
153
|
+
|
|
154
|
+
apply_quick_layout(chart, "title_legend_right", title_text="Q4 ARR")
|
|
155
|
+
apply_quick_layout(
|
|
156
|
+
chart, "title_axes_legend_bottom",
|
|
157
|
+
value_axis_title_text="Revenue (£m)",
|
|
158
|
+
has_major_gridlines=False,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
Unknown override keys raise :class:`TypeError`.
|
|
162
|
+
"""
|
|
163
|
+
spec = _resolve_layout(layout)
|
|
164
|
+
if overrides:
|
|
165
|
+
unknown = set(overrides) - _VALID_SPEC_KEYS
|
|
166
|
+
if unknown:
|
|
167
|
+
raise TypeError(
|
|
168
|
+
"unknown quick-layout override(s): %s; valid keys: %s"
|
|
169
|
+
% (sorted(unknown), sorted(_VALID_SPEC_KEYS))
|
|
170
|
+
)
|
|
171
|
+
spec.update(overrides)
|
|
172
|
+
_apply_title(chart, spec)
|
|
173
|
+
_apply_legend(chart, spec)
|
|
174
|
+
_apply_category_axis(chart, spec)
|
|
175
|
+
_apply_value_axis(chart, spec)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
_VALID_SPEC_KEYS = frozenset(
|
|
179
|
+
{
|
|
180
|
+
"has_title",
|
|
181
|
+
"title_text",
|
|
182
|
+
"has_legend",
|
|
183
|
+
"legend_position",
|
|
184
|
+
"legend_in_layout",
|
|
185
|
+
"has_category_axis_title",
|
|
186
|
+
"category_axis_title_text",
|
|
187
|
+
"has_value_axis_title",
|
|
188
|
+
"value_axis_title_text",
|
|
189
|
+
"has_major_gridlines",
|
|
190
|
+
"has_minor_gridlines",
|
|
191
|
+
}
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _resolve_layout(layout) -> dict[str, Any]:
|
|
196
|
+
if isinstance(layout, Mapping):
|
|
197
|
+
return dict(layout)
|
|
198
|
+
if isinstance(layout, str):
|
|
199
|
+
try:
|
|
200
|
+
return dict(QUICK_LAYOUTS[layout])
|
|
201
|
+
except KeyError:
|
|
202
|
+
raise ValueError(
|
|
203
|
+
"unknown quick layout %r; choose from %r" % (layout, layout_names())
|
|
204
|
+
)
|
|
205
|
+
raise TypeError(
|
|
206
|
+
"layout must be a name or spec mapping, got %s" % type(layout).__name__
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _apply_title(chart, spec: Mapping[str, Any]) -> None:
|
|
211
|
+
title_text = spec.get("title_text")
|
|
212
|
+
if title_text is not None:
|
|
213
|
+
chart.has_title = True
|
|
214
|
+
chart.chart_title.text_frame.text = title_text
|
|
215
|
+
elif "has_title" in spec:
|
|
216
|
+
chart.has_title = bool(spec["has_title"])
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
_LEGEND_POSITION_NAMES = {
|
|
220
|
+
"right": XL_LEGEND_POSITION.RIGHT,
|
|
221
|
+
"left": XL_LEGEND_POSITION.LEFT,
|
|
222
|
+
"top": XL_LEGEND_POSITION.TOP,
|
|
223
|
+
"bottom": XL_LEGEND_POSITION.BOTTOM,
|
|
224
|
+
"corner": XL_LEGEND_POSITION.CORNER,
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _coerce_legend_position(value: Any) -> XL_LEGEND_POSITION:
|
|
229
|
+
"""Accept an :class:`XL_LEGEND_POSITION` member, int value, or lowercase name.
|
|
230
|
+
|
|
231
|
+
The reference docs were inconsistent about which form was canonical
|
|
232
|
+
(``"bottom"`` in prose, ``XL_LEGEND_POSITION.BOTTOM`` in code).
|
|
233
|
+
Accept both at the boundary, plus the historical integer form
|
|
234
|
+
(``-4107`` etc.) that ``Legend.position`` already supported via
|
|
235
|
+
``XL_LEGEND_POSITION.to_xml``, so config-driven layouts that
|
|
236
|
+
serialised enum values as ints keep working. Unknown strings or
|
|
237
|
+
out-of-range integers raise :class:`ValueError`.
|
|
238
|
+
"""
|
|
239
|
+
if isinstance(value, XL_LEGEND_POSITION):
|
|
240
|
+
return value
|
|
241
|
+
if isinstance(value, str):
|
|
242
|
+
try:
|
|
243
|
+
return _LEGEND_POSITION_NAMES[value.lower()]
|
|
244
|
+
except KeyError:
|
|
245
|
+
raise ValueError(
|
|
246
|
+
"legend_position string must be one of %s; got %r"
|
|
247
|
+
% (sorted(_LEGEND_POSITION_NAMES), value)
|
|
248
|
+
) from None
|
|
249
|
+
# ``bool`` is a subclass of ``int`` — guard explicitly so
|
|
250
|
+
# ``legend_position=True`` doesn't silently resolve to whichever
|
|
251
|
+
# member happens to have value 1.
|
|
252
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
253
|
+
try:
|
|
254
|
+
return XL_LEGEND_POSITION(value)
|
|
255
|
+
except ValueError:
|
|
256
|
+
raise ValueError(
|
|
257
|
+
"legend_position int %r is not a valid XL_LEGEND_POSITION "
|
|
258
|
+
"value" % value
|
|
259
|
+
) from None
|
|
260
|
+
raise TypeError(
|
|
261
|
+
"legend_position must be an XL_LEGEND_POSITION member, int value, "
|
|
262
|
+
"or string name (one of %s); got %r"
|
|
263
|
+
% (sorted(_LEGEND_POSITION_NAMES), value)
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _apply_legend(chart, spec: Mapping[str, Any]) -> None:
|
|
268
|
+
if "has_legend" in spec:
|
|
269
|
+
chart.has_legend = bool(spec["has_legend"])
|
|
270
|
+
|
|
271
|
+
if not chart.has_legend:
|
|
272
|
+
# Don't touch position/in_layout when the legend is off — those
|
|
273
|
+
# writes would silently re-add a legend element.
|
|
274
|
+
return
|
|
275
|
+
|
|
276
|
+
legend = chart.legend
|
|
277
|
+
if "legend_position" in spec:
|
|
278
|
+
legend.position = _coerce_legend_position(spec["legend_position"])
|
|
279
|
+
if "legend_in_layout" in spec:
|
|
280
|
+
legend.include_in_layout = bool(spec["legend_in_layout"])
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _apply_category_axis(chart, spec: Mapping[str, Any]) -> None:
|
|
284
|
+
cat_keys = (
|
|
285
|
+
"has_category_axis_title",
|
|
286
|
+
"category_axis_title_text",
|
|
287
|
+
)
|
|
288
|
+
if not any(k in spec for k in cat_keys):
|
|
289
|
+
return
|
|
290
|
+
try:
|
|
291
|
+
axis = chart.category_axis
|
|
292
|
+
except (ValueError, NotImplementedError):
|
|
293
|
+
# Pie/doughnut charts have no category axis — nothing to do.
|
|
294
|
+
return
|
|
295
|
+
title_text = spec.get("category_axis_title_text")
|
|
296
|
+
if title_text is not None:
|
|
297
|
+
axis.has_title = True
|
|
298
|
+
axis.axis_title.text_frame.text = title_text
|
|
299
|
+
elif "has_category_axis_title" in spec:
|
|
300
|
+
axis.has_title = bool(spec["has_category_axis_title"])
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _apply_value_axis(chart, spec: Mapping[str, Any]) -> None:
|
|
304
|
+
val_keys = (
|
|
305
|
+
"has_value_axis_title",
|
|
306
|
+
"value_axis_title_text",
|
|
307
|
+
"has_major_gridlines",
|
|
308
|
+
"has_minor_gridlines",
|
|
309
|
+
)
|
|
310
|
+
if not any(k in spec for k in val_keys):
|
|
311
|
+
return
|
|
312
|
+
try:
|
|
313
|
+
axis = chart.value_axis
|
|
314
|
+
except (ValueError, NotImplementedError):
|
|
315
|
+
return
|
|
316
|
+
title_text = spec.get("value_axis_title_text")
|
|
317
|
+
if title_text is not None:
|
|
318
|
+
axis.has_title = True
|
|
319
|
+
axis.axis_title.text_frame.text = title_text
|
|
320
|
+
elif "has_value_axis_title" in spec:
|
|
321
|
+
axis.has_title = bool(spec["has_value_axis_title"])
|
|
322
|
+
if "has_major_gridlines" in spec:
|
|
323
|
+
axis.has_major_gridlines = bool(spec["has_major_gridlines"])
|
|
324
|
+
if "has_minor_gridlines" in spec:
|
|
325
|
+
axis.has_minor_gridlines = bool(spec["has_minor_gridlines"])
|
pptx2/chart/series.py
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""Series-related objects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
|
|
7
|
+
from pptx2.chart.analytics import ErrorBars, Trendlines
|
|
8
|
+
from pptx2.chart.datalabel import DataLabels
|
|
9
|
+
from pptx2.chart.marker import Marker
|
|
10
|
+
from pptx2.chart.point import BubblePoints, CategoryPoints, XyPoints
|
|
11
|
+
from pptx2.dml.chtfmt import ChartFormat
|
|
12
|
+
from pptx2.oxml.ns import qn
|
|
13
|
+
from pptx2.util import lazyproperty
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _BaseSeries(object):
|
|
17
|
+
"""
|
|
18
|
+
Base class for |BarSeries| and other series classes.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, ser):
|
|
22
|
+
super(_BaseSeries, self).__init__()
|
|
23
|
+
self._element = ser
|
|
24
|
+
self._ser = ser
|
|
25
|
+
|
|
26
|
+
@lazyproperty
|
|
27
|
+
def format(self):
|
|
28
|
+
"""
|
|
29
|
+
The |ChartFormat| instance for this series, providing access to shape
|
|
30
|
+
properties such as fill and line.
|
|
31
|
+
"""
|
|
32
|
+
return ChartFormat(self._ser)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def index(self):
|
|
36
|
+
"""
|
|
37
|
+
The zero-based integer index of this series as reported in its
|
|
38
|
+
`c:ser/c:idx` element.
|
|
39
|
+
"""
|
|
40
|
+
return self._element.idx.val
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def name(self):
|
|
44
|
+
"""
|
|
45
|
+
The string label given to this series, appears as the title of the
|
|
46
|
+
column for this series in the Excel worksheet. It also appears as the
|
|
47
|
+
label for this series in the legend.
|
|
48
|
+
"""
|
|
49
|
+
names = self._element.xpath("./c:tx//c:pt/c:v/text()")
|
|
50
|
+
name = names[0] if names else ""
|
|
51
|
+
return name
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class _BaseCategorySeries(_BaseSeries):
|
|
55
|
+
"""Base class for |BarSeries| and other category chart series classes."""
|
|
56
|
+
|
|
57
|
+
@lazyproperty
|
|
58
|
+
def data_labels(self):
|
|
59
|
+
"""|DataLabels| object controlling data labels for this series."""
|
|
60
|
+
return DataLabels(self._ser.get_or_add_dLbls())
|
|
61
|
+
|
|
62
|
+
@lazyproperty
|
|
63
|
+
def points(self):
|
|
64
|
+
"""
|
|
65
|
+
The |CategoryPoints| object providing access to individual data
|
|
66
|
+
points in this series.
|
|
67
|
+
"""
|
|
68
|
+
return CategoryPoints(self._ser)
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def values(self):
|
|
72
|
+
"""
|
|
73
|
+
Read-only. A sequence containing the float values for this series, in
|
|
74
|
+
the order they appear on the chart.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def iter_values():
|
|
78
|
+
val = self._element.val
|
|
79
|
+
if val is None:
|
|
80
|
+
return
|
|
81
|
+
for idx in range(val.ptCount_val):
|
|
82
|
+
yield val.pt_v(idx)
|
|
83
|
+
|
|
84
|
+
return tuple(iter_values())
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class _MarkerMixin(object):
|
|
88
|
+
"""
|
|
89
|
+
Mixin class providing `.marker` property for line-type chart series. The
|
|
90
|
+
line-type charts are Line, XY, and Radar.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
@lazyproperty
|
|
94
|
+
def marker(self):
|
|
95
|
+
"""
|
|
96
|
+
The |Marker| instance for this series, providing access to data point
|
|
97
|
+
marker properties such as fill and line. Setting these properties
|
|
98
|
+
determines the appearance of markers for all points in this series
|
|
99
|
+
that are not overridden by settings at the point level.
|
|
100
|
+
"""
|
|
101
|
+
return Marker(self._ser)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class _AnalyticsMixin(object):
|
|
105
|
+
"""Mixin adding ``trendlines`` and ``error_bars`` to a series.
|
|
106
|
+
|
|
107
|
+
Only mixed into the series types whose schema complexType permits
|
|
108
|
+
``<c:trendline>`` / ``<c:errBars>`` children (bar, line, scatter, area,
|
|
109
|
+
bubble). Pie and radar series do not allow them.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
@lazyproperty
|
|
113
|
+
def trendlines(self):
|
|
114
|
+
"""The |Trendlines| collection for this series.
|
|
115
|
+
|
|
116
|
+
Supports ``len()``, iteration, indexed access, and
|
|
117
|
+
``.add(kind, ...)`` to fit a new analytical trendline.
|
|
118
|
+
"""
|
|
119
|
+
return Trendlines(self._ser)
|
|
120
|
+
|
|
121
|
+
@lazyproperty
|
|
122
|
+
def error_bars(self):
|
|
123
|
+
"""The |ErrorBars| object for this series.
|
|
124
|
+
|
|
125
|
+
Provides Excel-style constructors: ``fixed(value)``,
|
|
126
|
+
``percentage(pct)``, ``standard_deviation(n)``, ``standard_error()``,
|
|
127
|
+
and ``custom(plus, minus)``.
|
|
128
|
+
"""
|
|
129
|
+
return ErrorBars(self._ser)
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def axis_group(self):
|
|
133
|
+
"""Read/write ``"primary"`` or ``"secondary"`` axis group.
|
|
134
|
+
|
|
135
|
+
A series shares the value axis of the plot (chart group) it belongs
|
|
136
|
+
to. Assigning ``"secondary"`` moves this series' whole plot onto a
|
|
137
|
+
secondary value axis, creating that axis (and a hidden secondary
|
|
138
|
+
category axis it crosses) if necessary. The newly-allocated axis ids
|
|
139
|
+
stay within the signed-int32 range so PowerPoint never flags the file
|
|
140
|
+
for repair. Assigning ``"primary"`` is only valid while the series is
|
|
141
|
+
already on the primary axis (moving back is unsupported).
|
|
142
|
+
"""
|
|
143
|
+
plotArea = self._ser.xpath("ancestor::c:plotArea")[0]
|
|
144
|
+
secondary = plotArea.secondary_value_axis
|
|
145
|
+
if secondary is None:
|
|
146
|
+
return "primary"
|
|
147
|
+
secondary_id = secondary.xpath("c:axId")[0].get("val")
|
|
148
|
+
xChart = self._ser.getparent()
|
|
149
|
+
ax_ids = {a.get("val") for a in xChart.xpath("c:axId")}
|
|
150
|
+
return "secondary" if secondary_id in ax_ids else "primary"
|
|
151
|
+
|
|
152
|
+
@axis_group.setter
|
|
153
|
+
def axis_group(self, value):
|
|
154
|
+
if value not in ("primary", "secondary"):
|
|
155
|
+
raise ValueError("axis_group must be 'primary' or 'secondary', got %r" % (value,))
|
|
156
|
+
if value == "primary":
|
|
157
|
+
if self.axis_group == "secondary":
|
|
158
|
+
raise ValueError("moving a series back to the primary axis group is not supported")
|
|
159
|
+
return
|
|
160
|
+
if self.axis_group == "secondary":
|
|
161
|
+
return
|
|
162
|
+
plotArea = self._ser.xpath("ancestor::c:plotArea")[0]
|
|
163
|
+
secondary = plotArea.secondary_value_axis
|
|
164
|
+
if secondary is None:
|
|
165
|
+
# Move THIS series' plot (not just the front-most one) onto the new
|
|
166
|
+
# secondary axis — critical for combo charts with multiple plots.
|
|
167
|
+
plotArea.add_secondary_value_axis(self._ser.getparent())
|
|
168
|
+
return
|
|
169
|
+
# -- a secondary axis already exists; re-point this series' plot onto
|
|
170
|
+
# -- the secondary axis ids.
|
|
171
|
+
val2_id = secondary.xpath("c:axId")[0].get("val")
|
|
172
|
+
cross2_id = secondary.xpath("c:crossAx")[0].get("val")
|
|
173
|
+
ax_ids = self._ser.getparent().xpath("c:axId")
|
|
174
|
+
if len(ax_ids) >= 2:
|
|
175
|
+
ax_ids[0].set("val", str(cross2_id))
|
|
176
|
+
ax_ids[1].set("val", str(val2_id))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class AreaSeries(_BaseCategorySeries, _AnalyticsMixin):
|
|
180
|
+
"""
|
|
181
|
+
A data point series belonging to an area plot.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class BarSeries(_BaseCategorySeries, _AnalyticsMixin):
|
|
186
|
+
"""A data point series belonging to a bar plot."""
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def invert_if_negative(self):
|
|
190
|
+
"""
|
|
191
|
+
|True| if a point having a value less than zero should appear with a
|
|
192
|
+
fill different than those with a positive value. |False| if the fill
|
|
193
|
+
should be the same regardless of the bar's value. When |True|, a bar
|
|
194
|
+
with a solid fill appears with white fill; in a bar with gradient
|
|
195
|
+
fill, the direction of the gradient is reversed, e.g. dark -> light
|
|
196
|
+
instead of light -> dark. The term "invert" here should be understood
|
|
197
|
+
to mean "invert the *direction* of the *fill gradient*".
|
|
198
|
+
"""
|
|
199
|
+
invertIfNegative = self._element.invertIfNegative
|
|
200
|
+
if invertIfNegative is None:
|
|
201
|
+
return True
|
|
202
|
+
return invertIfNegative.val
|
|
203
|
+
|
|
204
|
+
@invert_if_negative.setter
|
|
205
|
+
def invert_if_negative(self, value):
|
|
206
|
+
invertIfNegative = self._element.get_or_add_invertIfNegative()
|
|
207
|
+
invertIfNegative.val = value
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class LineSeries(_BaseCategorySeries, _MarkerMixin, _AnalyticsMixin):
|
|
211
|
+
"""
|
|
212
|
+
A data point series belonging to a line plot.
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def smooth(self):
|
|
217
|
+
"""
|
|
218
|
+
Read/write boolean specifying whether to use curve smoothing to
|
|
219
|
+
form the line connecting the data points in this series into
|
|
220
|
+
a continuous curve. If |False|, a series of straight line segments
|
|
221
|
+
are used to connect the points.
|
|
222
|
+
"""
|
|
223
|
+
smooth = self._element.smooth
|
|
224
|
+
if smooth is None:
|
|
225
|
+
return True
|
|
226
|
+
return smooth.val
|
|
227
|
+
|
|
228
|
+
@smooth.setter
|
|
229
|
+
def smooth(self, value):
|
|
230
|
+
self._element.get_or_add_smooth().val = value
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class PieSeries(_BaseCategorySeries):
|
|
234
|
+
"""
|
|
235
|
+
A data point series belonging to a pie plot.
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class RadarSeries(_BaseCategorySeries, _MarkerMixin):
|
|
240
|
+
"""
|
|
241
|
+
A data point series belonging to a radar plot.
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class XySeries(_BaseSeries, _MarkerMixin, _AnalyticsMixin):
|
|
246
|
+
"""
|
|
247
|
+
A data point series belonging to an XY (scatter) plot.
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
def iter_values(self):
|
|
251
|
+
"""
|
|
252
|
+
Generate each float Y value in this series, in the order they appear
|
|
253
|
+
on the chart. A value of `None` represents a missing Y value
|
|
254
|
+
(corresponding to a blank Excel cell).
|
|
255
|
+
"""
|
|
256
|
+
yVal = self._element.yVal
|
|
257
|
+
if yVal is None:
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
for idx in range(yVal.ptCount_val):
|
|
261
|
+
yield yVal.pt_v(idx)
|
|
262
|
+
|
|
263
|
+
@lazyproperty
|
|
264
|
+
def points(self):
|
|
265
|
+
"""
|
|
266
|
+
The |XyPoints| object providing access to individual data points in
|
|
267
|
+
this series.
|
|
268
|
+
"""
|
|
269
|
+
return XyPoints(self._ser)
|
|
270
|
+
|
|
271
|
+
@property
|
|
272
|
+
def values(self):
|
|
273
|
+
"""
|
|
274
|
+
Read-only. A sequence containing the float values for this series, in
|
|
275
|
+
the order they appear on the chart.
|
|
276
|
+
"""
|
|
277
|
+
return tuple(self.iter_values())
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class BubbleSeries(XySeries):
|
|
281
|
+
"""
|
|
282
|
+
A data point series belonging to a bubble plot.
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
@lazyproperty
|
|
286
|
+
def points(self):
|
|
287
|
+
"""
|
|
288
|
+
The |BubblePoints| object providing access to individual data point
|
|
289
|
+
objects used to discover and adjust the formatting and data labels of
|
|
290
|
+
a data point.
|
|
291
|
+
"""
|
|
292
|
+
return BubblePoints(self._ser)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class SeriesCollection(Sequence):
|
|
296
|
+
"""
|
|
297
|
+
A sequence of |Series| objects.
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
def __init__(self, parent_elm):
|
|
301
|
+
# *parent_elm* can be either a c:plotArea or xChart element
|
|
302
|
+
super(SeriesCollection, self).__init__()
|
|
303
|
+
self._element = parent_elm
|
|
304
|
+
|
|
305
|
+
def __getitem__(self, index):
|
|
306
|
+
ser = self._element.sers[index]
|
|
307
|
+
return _SeriesFactory(ser)
|
|
308
|
+
|
|
309
|
+
def __len__(self):
|
|
310
|
+
return len(self._element.sers)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _SeriesFactory(ser):
|
|
314
|
+
"""
|
|
315
|
+
Return an instance of the appropriate subclass of _BaseSeries based on the
|
|
316
|
+
xChart element *ser* appears in.
|
|
317
|
+
"""
|
|
318
|
+
xChart_tag = ser.getparent().tag
|
|
319
|
+
|
|
320
|
+
try:
|
|
321
|
+
SeriesCls = {
|
|
322
|
+
qn("c:areaChart"): AreaSeries,
|
|
323
|
+
qn("c:barChart"): BarSeries,
|
|
324
|
+
qn("c:bubbleChart"): BubbleSeries,
|
|
325
|
+
qn("c:doughnutChart"): PieSeries,
|
|
326
|
+
qn("c:lineChart"): LineSeries,
|
|
327
|
+
qn("c:pieChart"): PieSeries,
|
|
328
|
+
qn("c:radarChart"): RadarSeries,
|
|
329
|
+
qn("c:scatterChart"): XySeries,
|
|
330
|
+
}[xChart_tag]
|
|
331
|
+
except KeyError:
|
|
332
|
+
raise NotImplementedError("series class for %s not yet implemented" % xChart_tag)
|
|
333
|
+
|
|
334
|
+
return SeriesCls(ser)
|