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/datalabel.py
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"""Data label-related objects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pptx2.oxml.ns import qn
|
|
6
|
+
from pptx2.text.text import Font, TextFrame
|
|
7
|
+
from pptx2.util import Pt, lazyproperty
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DataLabels(object):
|
|
11
|
+
"""Provides access to properties of data labels for a plot or a series.
|
|
12
|
+
|
|
13
|
+
This is not a collection and does not provide access to individual data
|
|
14
|
+
labels. Access to individual labels is via the |Point| object. The
|
|
15
|
+
properties this object provides control formatting of *all* the data
|
|
16
|
+
labels in its scope.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, dLbls):
|
|
20
|
+
super(DataLabels, self).__init__()
|
|
21
|
+
self._element = dLbls
|
|
22
|
+
|
|
23
|
+
@lazyproperty
|
|
24
|
+
def font(self):
|
|
25
|
+
"""
|
|
26
|
+
The |Font| object that provides access to the text properties for
|
|
27
|
+
these data labels, such as bold, italic, etc.
|
|
28
|
+
"""
|
|
29
|
+
defRPr = self._element.defRPr
|
|
30
|
+
font = Font(defRPr)
|
|
31
|
+
return font
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def number_format(self):
|
|
35
|
+
"""
|
|
36
|
+
Read/write string specifying the format for the numbers on this set
|
|
37
|
+
of data labels. Returns 'General' if no number format has been set.
|
|
38
|
+
Note that this format string has no effect on rendered data labels
|
|
39
|
+
when :meth:`number_format_is_linked` is |True|. Assigning a format
|
|
40
|
+
string to this property automatically sets
|
|
41
|
+
:meth:`number_format_is_linked` to |False|.
|
|
42
|
+
"""
|
|
43
|
+
numFmt = self._element.numFmt
|
|
44
|
+
if numFmt is None:
|
|
45
|
+
return "General"
|
|
46
|
+
return numFmt.formatCode
|
|
47
|
+
|
|
48
|
+
@number_format.setter
|
|
49
|
+
def number_format(self, value):
|
|
50
|
+
self._element.get_or_add_numFmt().formatCode = value
|
|
51
|
+
self.number_format_is_linked = False
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def number_format_is_linked(self):
|
|
55
|
+
"""
|
|
56
|
+
Read/write boolean specifying whether number formatting should be
|
|
57
|
+
taken from the source spreadsheet rather than the value of
|
|
58
|
+
:meth:`number_format`.
|
|
59
|
+
"""
|
|
60
|
+
numFmt = self._element.numFmt
|
|
61
|
+
if numFmt is None:
|
|
62
|
+
return True
|
|
63
|
+
souceLinked = numFmt.sourceLinked
|
|
64
|
+
if souceLinked is None:
|
|
65
|
+
return True
|
|
66
|
+
return numFmt.sourceLinked
|
|
67
|
+
|
|
68
|
+
@number_format_is_linked.setter
|
|
69
|
+
def number_format_is_linked(self, value):
|
|
70
|
+
numFmt = self._element.get_or_add_numFmt()
|
|
71
|
+
# `formatCode` is schema-required on <c:numFmt>; default it whenever it's
|
|
72
|
+
# absent (newly created or a pre-existing element that lacks it) so the
|
|
73
|
+
# linked flag can't be toggled into an invalid, PowerPoint-rejected
|
|
74
|
+
# element. An existing explicit value is preserved.
|
|
75
|
+
if numFmt.get("formatCode") is None:
|
|
76
|
+
numFmt.formatCode = "General"
|
|
77
|
+
numFmt.sourceLinked = value
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def position(self):
|
|
81
|
+
"""
|
|
82
|
+
Read/write :ref:`XlDataLabelPosition` enumeration value specifying
|
|
83
|
+
the position of the data labels with respect to their data point, or
|
|
84
|
+
|None| if no position is specified. Assigning |None| causes
|
|
85
|
+
PowerPoint to choose the default position, which varies by chart
|
|
86
|
+
type.
|
|
87
|
+
"""
|
|
88
|
+
dLblPos = self._element.dLblPos
|
|
89
|
+
if dLblPos is None:
|
|
90
|
+
return None
|
|
91
|
+
return dLblPos.val
|
|
92
|
+
|
|
93
|
+
@position.setter
|
|
94
|
+
def position(self, value):
|
|
95
|
+
if value is None:
|
|
96
|
+
self._element._remove_dLblPos()
|
|
97
|
+
return
|
|
98
|
+
self._element.get_or_add_dLblPos().val = value
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def show_category_name(self):
|
|
102
|
+
"""Read/write. True when name of category should appear in label."""
|
|
103
|
+
return self._element.get_or_add_showCatName().val
|
|
104
|
+
|
|
105
|
+
@show_category_name.setter
|
|
106
|
+
def show_category_name(self, value):
|
|
107
|
+
self._element.get_or_add_showCatName().val = bool(value)
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def show_legend_key(self):
|
|
111
|
+
"""Read/write. True when data label displays legend-color swatch."""
|
|
112
|
+
return self._element.get_or_add_showLegendKey().val
|
|
113
|
+
|
|
114
|
+
@show_legend_key.setter
|
|
115
|
+
def show_legend_key(self, value):
|
|
116
|
+
self._element.get_or_add_showLegendKey().val = bool(value)
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def show_percentage(self):
|
|
120
|
+
"""Read/write. True when data label displays percentage.
|
|
121
|
+
|
|
122
|
+
This option is not operative on all chart types. Percentage appears
|
|
123
|
+
on polar charts such as pie and donut.
|
|
124
|
+
"""
|
|
125
|
+
return self._element.get_or_add_showPercent().val
|
|
126
|
+
|
|
127
|
+
@show_percentage.setter
|
|
128
|
+
def show_percentage(self, value):
|
|
129
|
+
self._element.get_or_add_showPercent().val = bool(value)
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def show_series_name(self):
|
|
133
|
+
"""Read/write. True when data label displays series name."""
|
|
134
|
+
return self._element.get_or_add_showSerName().val
|
|
135
|
+
|
|
136
|
+
@show_series_name.setter
|
|
137
|
+
def show_series_name(self, value):
|
|
138
|
+
self._element.get_or_add_showSerName().val = bool(value)
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def show_value(self):
|
|
142
|
+
"""Read/write. True when label displays numeric value of datapoint."""
|
|
143
|
+
return self._element.get_or_add_showVal().val
|
|
144
|
+
|
|
145
|
+
@show_value.setter
|
|
146
|
+
def show_value(self, value):
|
|
147
|
+
self._element.get_or_add_showVal().val = bool(value)
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def collision_strategy(self):
|
|
151
|
+
"""Write-only — read is unsupported.
|
|
152
|
+
|
|
153
|
+
Strategies for handling overlapping data labels on
|
|
154
|
+
bar / column charts. Native PowerPoint doesn't reposition
|
|
155
|
+
labels at all, so this is a small set of heuristics that
|
|
156
|
+
nudge the typography and bar geometry into a less-collidey
|
|
157
|
+
layout. For genuine collision avoidance (re-laying out
|
|
158
|
+
labels around their bars), reach for ``add_plotly_figure``
|
|
159
|
+
— Plotly's chart engine handles this natively.
|
|
160
|
+
|
|
161
|
+
Assignable values:
|
|
162
|
+
|
|
163
|
+
* ``"auto"`` — apply a sensible default for the chart shape:
|
|
164
|
+
shrink data-label font to 8 pt, and on bar / column plots
|
|
165
|
+
with five or more categories *and* multiple series,
|
|
166
|
+
reduce ``gapWidth`` to 60 (thicker bars, more room
|
|
167
|
+
between labels). No-op on plots that don't carry
|
|
168
|
+
``gapWidth``.
|
|
169
|
+
* ``"shrink"`` — only shrink the font (8 pt). Doesn't
|
|
170
|
+
touch bar geometry.
|
|
171
|
+
* ``"compact"`` — drop font to 8 pt *and* set
|
|
172
|
+
``gapWidth=60``, regardless of category count. Use when
|
|
173
|
+
you've eyeballed the chart and want explicit thickening.
|
|
174
|
+
|
|
175
|
+
Read-only access raises ``AttributeError`` because the
|
|
176
|
+
property doesn't have a single canonical value to return —
|
|
177
|
+
the underlying state is split across font size and the
|
|
178
|
+
plot's ``gapWidth`` attribute.
|
|
179
|
+
"""
|
|
180
|
+
raise AttributeError(
|
|
181
|
+
"data_labels.collision_strategy is write-only; read "
|
|
182
|
+
"data_labels.font.size and the plot's gap_width directly."
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
@collision_strategy.setter
|
|
186
|
+
def collision_strategy(self, value):
|
|
187
|
+
if value not in ("auto", "shrink", "compact"):
|
|
188
|
+
raise ValueError(
|
|
189
|
+
"collision_strategy must be 'auto', 'shrink', or 'compact'; "
|
|
190
|
+
f"got {value!r}"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Step 1 — typography. Always shrink labels.
|
|
194
|
+
self.font.size = Pt(8)
|
|
195
|
+
|
|
196
|
+
# Step 2 — bar geometry. Walk up to the plot element to read
|
|
197
|
+
# series / category counts and write gapWidth. Only meaningful
|
|
198
|
+
# on bar / column charts (which carry ``c:gapWidth``); other
|
|
199
|
+
# plot types silently skip this step.
|
|
200
|
+
plot_elm = self._element.getparent()
|
|
201
|
+
if plot_elm is None:
|
|
202
|
+
return
|
|
203
|
+
gap_tag = qn("c:gapWidth")
|
|
204
|
+
if plot_elm.find(gap_tag) is None and plot_elm.tag not in (
|
|
205
|
+
qn("c:barChart"),
|
|
206
|
+
qn("c:bar3DChart"),
|
|
207
|
+
):
|
|
208
|
+
# Not a gap-width-bearing plot; typography update is enough.
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
if value == "shrink":
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
if value == "compact":
|
|
215
|
+
self._set_or_add_gap_width(plot_elm, 60)
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
# value == "auto" — apply only when the heuristic warrants
|
|
219
|
+
# (≥5 categories AND multi-series).
|
|
220
|
+
ser_count = len(plot_elm.findall(qn("c:ser")))
|
|
221
|
+
cat_count = self._first_series_category_count(plot_elm)
|
|
222
|
+
if ser_count >= 2 and cat_count >= 5:
|
|
223
|
+
self._set_or_add_gap_width(plot_elm, 60)
|
|
224
|
+
|
|
225
|
+
@staticmethod
|
|
226
|
+
def _set_or_add_gap_width(plot_elm, val):
|
|
227
|
+
"""Set ``<c:gapWidth val="..."/>`` on *plot_elm* (replacing if present)."""
|
|
228
|
+
gap_tag = qn("c:gapWidth")
|
|
229
|
+
existing = plot_elm.find(gap_tag)
|
|
230
|
+
if existing is not None:
|
|
231
|
+
existing.set("val", str(int(val)))
|
|
232
|
+
return
|
|
233
|
+
# CT_BarChart/CT_Bar3DChart are strict sequences: gapWidth must
|
|
234
|
+
# precede overlap/serLines/axId/extLst (a bare append lands after
|
|
235
|
+
# axId, which PowerPoint rejects with the repair prompt).
|
|
236
|
+
gw = plot_elm.makeelement(gap_tag, {"val": str(int(val))})
|
|
237
|
+
successor_tags = ("c:overlap", "c:gapDepth", "c:shape", "c:serLines", "c:axId", "c:extLst")
|
|
238
|
+
for successor_tag in successor_tags:
|
|
239
|
+
successor = plot_elm.find(qn(successor_tag))
|
|
240
|
+
if successor is not None:
|
|
241
|
+
successor.addprevious(gw)
|
|
242
|
+
return
|
|
243
|
+
plot_elm.append(gw)
|
|
244
|
+
|
|
245
|
+
@staticmethod
|
|
246
|
+
def _first_series_category_count(plot_elm):
|
|
247
|
+
"""Count category points on the first series, or 0 if unreadable."""
|
|
248
|
+
ser = plot_elm.find(qn("c:ser"))
|
|
249
|
+
if ser is None:
|
|
250
|
+
return 0
|
|
251
|
+
cat = ser.find(qn("c:cat"))
|
|
252
|
+
if cat is None:
|
|
253
|
+
return 0
|
|
254
|
+
# Categories live as ``c:strRef/c:strCache/c:pt`` or
|
|
255
|
+
# ``c:numRef/c:numCache/c:pt``; both lead to ``c:pt`` in the
|
|
256
|
+
# rendered cache.
|
|
257
|
+
pts = cat.findall(f".//{qn('c:pt')}")
|
|
258
|
+
return len(pts)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class DataLabel(object):
|
|
262
|
+
"""
|
|
263
|
+
The data label associated with an individual data point.
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
def __init__(self, ser, idx):
|
|
267
|
+
super(DataLabel, self).__init__()
|
|
268
|
+
self._ser = self._element = ser
|
|
269
|
+
self._idx = idx
|
|
270
|
+
|
|
271
|
+
@lazyproperty
|
|
272
|
+
def font(self):
|
|
273
|
+
"""The |Font| object providing text formatting for this data label.
|
|
274
|
+
|
|
275
|
+
This font object is used to customize the appearance of automatically
|
|
276
|
+
inserted text, such as the data point value. The font applies to the
|
|
277
|
+
entire data label. More granular control of the appearance of custom
|
|
278
|
+
data label text is controlled by a font object on runs in the text
|
|
279
|
+
frame.
|
|
280
|
+
"""
|
|
281
|
+
txPr = self._get_or_add_txPr()
|
|
282
|
+
text_frame = TextFrame(txPr, self)
|
|
283
|
+
paragraph = text_frame.paragraphs[0]
|
|
284
|
+
return paragraph.font
|
|
285
|
+
|
|
286
|
+
@property
|
|
287
|
+
def has_text_frame(self):
|
|
288
|
+
"""
|
|
289
|
+
Return |True| if this data label has a text frame (implying it has
|
|
290
|
+
custom data label text), and |False| otherwise. Assigning |True|
|
|
291
|
+
causes a text frame to be added if not already present. Assigning
|
|
292
|
+
|False| causes any existing text frame to be removed along with any
|
|
293
|
+
text contained in the text frame.
|
|
294
|
+
"""
|
|
295
|
+
dLbl = self._dLbl
|
|
296
|
+
if dLbl is None:
|
|
297
|
+
return False
|
|
298
|
+
if dLbl.xpath("c:tx/c:rich"):
|
|
299
|
+
return True
|
|
300
|
+
return False
|
|
301
|
+
|
|
302
|
+
@has_text_frame.setter
|
|
303
|
+
def has_text_frame(self, value):
|
|
304
|
+
if bool(value) is True:
|
|
305
|
+
self._get_or_add_tx_rich()
|
|
306
|
+
else:
|
|
307
|
+
self._remove_tx_rich()
|
|
308
|
+
|
|
309
|
+
@property
|
|
310
|
+
def position(self):
|
|
311
|
+
"""
|
|
312
|
+
Read/write :ref:`XlDataLabelPosition` member specifying the position
|
|
313
|
+
of this data label with respect to its data point, or |None| if no
|
|
314
|
+
position is specified. Assigning |None| causes PowerPoint to choose
|
|
315
|
+
the default position, which varies by chart type.
|
|
316
|
+
"""
|
|
317
|
+
dLbl = self._dLbl
|
|
318
|
+
if dLbl is None:
|
|
319
|
+
return None
|
|
320
|
+
dLblPos = dLbl.dLblPos
|
|
321
|
+
if dLblPos is None:
|
|
322
|
+
return None
|
|
323
|
+
return dLblPos.val
|
|
324
|
+
|
|
325
|
+
@position.setter
|
|
326
|
+
def position(self, value):
|
|
327
|
+
if value is None:
|
|
328
|
+
dLbl = self._dLbl
|
|
329
|
+
if dLbl is None:
|
|
330
|
+
return
|
|
331
|
+
dLbl._remove_dLblPos()
|
|
332
|
+
return
|
|
333
|
+
dLbl = self._get_or_add_dLbl()
|
|
334
|
+
dLbl.get_or_add_dLblPos().val = value
|
|
335
|
+
|
|
336
|
+
@property
|
|
337
|
+
def text_frame(self):
|
|
338
|
+
"""
|
|
339
|
+
|TextFrame| instance for this data label, containing the text of the
|
|
340
|
+
data label and providing access to its text formatting properties.
|
|
341
|
+
"""
|
|
342
|
+
rich = self._get_or_add_rich()
|
|
343
|
+
return TextFrame(rich, self)
|
|
344
|
+
|
|
345
|
+
@property
|
|
346
|
+
def _dLbl(self):
|
|
347
|
+
"""
|
|
348
|
+
Return the |CT_DLbl| instance referring specifically to this
|
|
349
|
+
individual data label (having the same index value), or |None| if not
|
|
350
|
+
present.
|
|
351
|
+
"""
|
|
352
|
+
return self._ser.get_dLbl(self._idx)
|
|
353
|
+
|
|
354
|
+
def _get_or_add_dLbl(self):
|
|
355
|
+
"""
|
|
356
|
+
The ``CT_DLbl`` instance referring specifically to this individual
|
|
357
|
+
data label, newly created if not yet present in the XML.
|
|
358
|
+
"""
|
|
359
|
+
return self._ser.get_or_add_dLbl(self._idx)
|
|
360
|
+
|
|
361
|
+
def _get_or_add_rich(self):
|
|
362
|
+
"""
|
|
363
|
+
Return the `c:rich` element representing the text frame for this data
|
|
364
|
+
label, newly created with its ancestors if not present.
|
|
365
|
+
"""
|
|
366
|
+
dLbl = self._get_or_add_dLbl()
|
|
367
|
+
|
|
368
|
+
# having a c:spPr or c:txPr when a c:tx is present causes the "can't
|
|
369
|
+
# save" bug on bubble charts. Remove c:spPr and c:txPr when present.
|
|
370
|
+
dLbl._remove_spPr()
|
|
371
|
+
dLbl._remove_txPr()
|
|
372
|
+
|
|
373
|
+
return dLbl.get_or_add_rich()
|
|
374
|
+
|
|
375
|
+
def _get_or_add_tx_rich(self):
|
|
376
|
+
"""
|
|
377
|
+
Return the `c:tx` element for this data label, with its `c:rich`
|
|
378
|
+
child and descendants, newly created if not yet present.
|
|
379
|
+
"""
|
|
380
|
+
dLbl = self._get_or_add_dLbl()
|
|
381
|
+
|
|
382
|
+
# having a c:spPr or c:txPr when a c:tx is present causes the "can't
|
|
383
|
+
# save" bug on bubble charts. Remove c:spPr and c:txPr when present.
|
|
384
|
+
dLbl._remove_spPr()
|
|
385
|
+
dLbl._remove_txPr()
|
|
386
|
+
|
|
387
|
+
return dLbl.get_or_add_tx_rich()
|
|
388
|
+
|
|
389
|
+
def _get_or_add_txPr(self):
|
|
390
|
+
"""Return the `c:txPr` element for this data label.
|
|
391
|
+
|
|
392
|
+
The `c:txPr` element and its parent `c:dLbl` element are created if
|
|
393
|
+
not yet present.
|
|
394
|
+
"""
|
|
395
|
+
dLbl = self._get_or_add_dLbl()
|
|
396
|
+
return dLbl.get_or_add_txPr()
|
|
397
|
+
|
|
398
|
+
def _remove_tx_rich(self):
|
|
399
|
+
"""
|
|
400
|
+
Remove any `c:tx/c:rich` child of the `c:dLbl` element for this data
|
|
401
|
+
label. Do nothing if that element is not present.
|
|
402
|
+
"""
|
|
403
|
+
dLbl = self._dLbl
|
|
404
|
+
if dLbl is None:
|
|
405
|
+
return
|
|
406
|
+
dLbl.remove_tx_rich()
|
pptx2/chart/legend.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Legend of a chart."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pptx2.enum.chart import XL_LEGEND_POSITION
|
|
6
|
+
from pptx2.text.text import Font
|
|
7
|
+
from pptx2.util import lazyproperty
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Legend(object):
|
|
11
|
+
"""
|
|
12
|
+
Represents the legend in a chart. A chart can have at most one legend.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, legend_elm):
|
|
16
|
+
super(Legend, self).__init__()
|
|
17
|
+
self._element = legend_elm
|
|
18
|
+
|
|
19
|
+
@lazyproperty
|
|
20
|
+
def font(self):
|
|
21
|
+
"""
|
|
22
|
+
The |Font| object that provides access to the text properties for
|
|
23
|
+
this legend, such as bold, italic, etc.
|
|
24
|
+
"""
|
|
25
|
+
defRPr = self._element.defRPr
|
|
26
|
+
font = Font(defRPr)
|
|
27
|
+
return font
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def horz_offset(self):
|
|
31
|
+
"""
|
|
32
|
+
Adjustment of the x position of the legend from its default.
|
|
33
|
+
Expressed as a float between -1.0 and 1.0 representing a fraction of
|
|
34
|
+
the chart width. Negative values move the legend left, positive
|
|
35
|
+
values move it to the right. |None| if no setting is specified.
|
|
36
|
+
"""
|
|
37
|
+
return self._element.horz_offset
|
|
38
|
+
|
|
39
|
+
@horz_offset.setter
|
|
40
|
+
def horz_offset(self, value):
|
|
41
|
+
self._element.horz_offset = value
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def include_in_layout(self):
|
|
45
|
+
"""|True| if legend should be located inside plot area.
|
|
46
|
+
|
|
47
|
+
Read/write boolean specifying whether legend should be placed inside
|
|
48
|
+
the plot area. In many cases this will cause it to be superimposed on
|
|
49
|
+
the chart itself. Assigning |None| to this property causes any
|
|
50
|
+
`c:overlay` element to be removed, which is interpreted the same as
|
|
51
|
+
|True|. This use case should rarely be required and assigning
|
|
52
|
+
a boolean value is recommended.
|
|
53
|
+
"""
|
|
54
|
+
overlay = self._element.overlay
|
|
55
|
+
if overlay is None:
|
|
56
|
+
return True
|
|
57
|
+
return overlay.val
|
|
58
|
+
|
|
59
|
+
@include_in_layout.setter
|
|
60
|
+
def include_in_layout(self, value):
|
|
61
|
+
if value is None:
|
|
62
|
+
self._element._remove_overlay()
|
|
63
|
+
return
|
|
64
|
+
self._element.get_or_add_overlay().val = bool(value)
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def position(self):
|
|
68
|
+
"""
|
|
69
|
+
Read/write :ref:`XlLegendPosition` enumeration value specifying the
|
|
70
|
+
general region of the chart in which to place the legend.
|
|
71
|
+
"""
|
|
72
|
+
legendPos = self._element.legendPos
|
|
73
|
+
if legendPos is None:
|
|
74
|
+
return XL_LEGEND_POSITION.RIGHT
|
|
75
|
+
return legendPos.val
|
|
76
|
+
|
|
77
|
+
@position.setter
|
|
78
|
+
def position(self, position):
|
|
79
|
+
# Write the ``val`` attribute directly rather than going through
|
|
80
|
+
# ``CT_LegendPos.val``: the latter is an ``OptionalAttribute`` whose
|
|
81
|
+
# setter *strips* the attribute when the assigned value matches the
|
|
82
|
+
# OOXML default (``"r"``). PowerPoint's strict parser rejects the
|
|
83
|
+
# resulting bare ``<c:legendPos/>`` element and "repairs" the chart
|
|
84
|
+
# by deleting it on open, so always emit the attribute explicitly.
|
|
85
|
+
legendPos = self._element.get_or_add_legendPos()
|
|
86
|
+
legendPos.set("val", XL_LEGEND_POSITION.to_xml(position))
|
pptx2/chart/marker.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Marker-related objects.
|
|
2
|
+
|
|
3
|
+
Only the line-type charts Line, XY, and Radar have markers.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pptx2.dml.chtfmt import ChartFormat
|
|
9
|
+
from pptx2.shared import ElementProxy
|
|
10
|
+
from pptx2.util import lazyproperty
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Marker(ElementProxy):
|
|
14
|
+
"""
|
|
15
|
+
Represents a data point marker, such as a diamond or circle, on
|
|
16
|
+
a line-type chart.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
@lazyproperty
|
|
20
|
+
def format(self):
|
|
21
|
+
"""
|
|
22
|
+
The |ChartFormat| instance for this marker, providing access to shape
|
|
23
|
+
properties such as fill and line.
|
|
24
|
+
"""
|
|
25
|
+
marker = self._element.get_or_add_marker()
|
|
26
|
+
return ChartFormat(marker)
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def size(self):
|
|
30
|
+
"""
|
|
31
|
+
An integer between 2 and 72 inclusive indicating the size of this
|
|
32
|
+
marker in points. A value of |None| indicates no explicit value is
|
|
33
|
+
set and the size is inherited from a higher-level setting or the
|
|
34
|
+
PowerPoint default (which may be 9). Assigning |None| removes any
|
|
35
|
+
explicitly assigned size, causing this value to be inherited.
|
|
36
|
+
"""
|
|
37
|
+
marker = self._element.marker
|
|
38
|
+
if marker is None:
|
|
39
|
+
return None
|
|
40
|
+
return marker.size_val
|
|
41
|
+
|
|
42
|
+
@size.setter
|
|
43
|
+
def size(self, value):
|
|
44
|
+
marker = self._element.get_or_add_marker()
|
|
45
|
+
marker._remove_size()
|
|
46
|
+
if value is None:
|
|
47
|
+
return
|
|
48
|
+
size = marker._add_size()
|
|
49
|
+
size.val = value
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def style(self):
|
|
53
|
+
"""
|
|
54
|
+
A member of the :ref:`XlMarkerStyle` enumeration indicating the shape
|
|
55
|
+
of this marker. Returns |None| if no explicit style has been set,
|
|
56
|
+
which corresponds to the "Automatic" option in the PowerPoint UI.
|
|
57
|
+
"""
|
|
58
|
+
marker = self._element.marker
|
|
59
|
+
if marker is None:
|
|
60
|
+
return None
|
|
61
|
+
return marker.symbol_val
|
|
62
|
+
|
|
63
|
+
@style.setter
|
|
64
|
+
def style(self, value):
|
|
65
|
+
marker = self._element.get_or_add_marker()
|
|
66
|
+
marker._remove_symbol()
|
|
67
|
+
if value is None:
|
|
68
|
+
return
|
|
69
|
+
symbol = marker._add_symbol()
|
|
70
|
+
symbol.val = value
|
pptx2/chart/palettes.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Named chart color palettes, applied independently of `chart_style`.
|
|
2
|
+
|
|
3
|
+
The PowerPoint `chart_style` integer (1-48) bundles a fill palette together with
|
|
4
|
+
font, axis, and gridline tweaks. Users who want to recolor only the series — for
|
|
5
|
+
example to match a brand palette — without inheriting the rest of the style end
|
|
6
|
+
up either fighting the chart_style enum or hand-painting each series. This module
|
|
7
|
+
exposes a small set of curated palettes plus a tiny resolver that
|
|
8
|
+
`Chart.apply_palette` uses to translate a name or sequence into concrete
|
|
9
|
+
``RGBColor`` values.
|
|
10
|
+
|
|
11
|
+
Adding a palette here is intentionally lightweight: drop a new entry in
|
|
12
|
+
``CHART_PALETTES`` and it becomes addressable by name from ``apply_palette``.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Iterable, Sequence, Union
|
|
18
|
+
|
|
19
|
+
from pptx2.dml.color import RGBColor
|
|
20
|
+
|
|
21
|
+
ColorLike = Union[RGBColor, str, Sequence[int]]
|
|
22
|
+
"""Anything ``_to_rgb`` knows how to turn into an `RGBColor`."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
CHART_PALETTES: dict[str, tuple[str, ...]] = {
|
|
26
|
+
# Saturated, contemporary brand palette — pairs well with the "Modern"
|
|
27
|
+
# design-token starter pack.
|
|
28
|
+
"modern": (
|
|
29
|
+
"#2D3142",
|
|
30
|
+
"#4F5D75",
|
|
31
|
+
"#EF8354",
|
|
32
|
+
"#BFC0C0",
|
|
33
|
+
"#FFFFFF",
|
|
34
|
+
"#057DCD",
|
|
35
|
+
),
|
|
36
|
+
# Conservative, presentation-friendly palette — close to PowerPoint's
|
|
37
|
+
# default Office accents but a touch desaturated for print.
|
|
38
|
+
"classic": (
|
|
39
|
+
"#1F4E79",
|
|
40
|
+
"#2E75B6",
|
|
41
|
+
"#9DC3E6",
|
|
42
|
+
"#C00000",
|
|
43
|
+
"#7F6000",
|
|
44
|
+
"#548235",
|
|
45
|
+
),
|
|
46
|
+
# Higher-contrast editorial palette — useful for narrative decks where
|
|
47
|
+
# one series should clearly dominate.
|
|
48
|
+
"editorial": (
|
|
49
|
+
"#0B132B",
|
|
50
|
+
"#1C2541",
|
|
51
|
+
"#3A506B",
|
|
52
|
+
"#5BC0BE",
|
|
53
|
+
"#6FFFE9",
|
|
54
|
+
"#FF6B6B",
|
|
55
|
+
),
|
|
56
|
+
# Vibrant categorical palette — eight colors so 5+ series stay readable.
|
|
57
|
+
"vibrant": (
|
|
58
|
+
"#E63946",
|
|
59
|
+
"#F1A208",
|
|
60
|
+
"#2A9D8F",
|
|
61
|
+
"#264653",
|
|
62
|
+
"#7209B7",
|
|
63
|
+
"#3A86FF",
|
|
64
|
+
"#FB5607",
|
|
65
|
+
"#06D6A0",
|
|
66
|
+
),
|
|
67
|
+
# Monochrome blue ramp — ordered, useful when categories have a natural
|
|
68
|
+
# progression (light-to-dark).
|
|
69
|
+
"monochrome_blue": (
|
|
70
|
+
"#CFE2F3",
|
|
71
|
+
"#9FC5E8",
|
|
72
|
+
"#6FA8DC",
|
|
73
|
+
"#3D85C6",
|
|
74
|
+
"#0B5394",
|
|
75
|
+
"#073763",
|
|
76
|
+
),
|
|
77
|
+
# Monochrome warm ramp.
|
|
78
|
+
"monochrome_warm": (
|
|
79
|
+
"#FFF4E6",
|
|
80
|
+
"#FFD8A8",
|
|
81
|
+
"#FFA94D",
|
|
82
|
+
"#FF922B",
|
|
83
|
+
"#E8590C",
|
|
84
|
+
"#A5450A",
|
|
85
|
+
),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def palette_names() -> tuple[str, ...]:
|
|
90
|
+
"""Return the names of the built-in palettes, in declaration order."""
|
|
91
|
+
return tuple(CHART_PALETTES.keys())
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def resolve_palette(palette: Union[str, Iterable[ColorLike]]) -> tuple[RGBColor, ...]:
|
|
95
|
+
"""Return a tuple of `RGBColor` values for a palette name or sequence.
|
|
96
|
+
|
|
97
|
+
`palette` is either:
|
|
98
|
+
|
|
99
|
+
* A string naming a built-in palette (see :func:`palette_names`), or
|
|
100
|
+
* An iterable of color-likes — `RGBColor`, hex strings (with or without
|
|
101
|
+
leading ``'#'``), or 3-tuples of ints in ``0-255``.
|
|
102
|
+
|
|
103
|
+
Raises ``ValueError`` if `palette` is an unknown name or resolves to an
|
|
104
|
+
empty sequence (which would make :meth:`Chart.apply_palette` a no-op and
|
|
105
|
+
silently mask user error).
|
|
106
|
+
"""
|
|
107
|
+
if isinstance(palette, str):
|
|
108
|
+
try:
|
|
109
|
+
colors = CHART_PALETTES[palette]
|
|
110
|
+
except KeyError:
|
|
111
|
+
raise ValueError(
|
|
112
|
+
"unknown palette %r; choose from %r" % (palette, palette_names())
|
|
113
|
+
)
|
|
114
|
+
return tuple(RGBColor.from_hex(c) for c in colors)
|
|
115
|
+
|
|
116
|
+
resolved = tuple(_to_rgb(c) for c in palette)
|
|
117
|
+
if not resolved:
|
|
118
|
+
raise ValueError("palette must contain at least one color")
|
|
119
|
+
return resolved
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _to_rgb(color: ColorLike) -> RGBColor:
|
|
123
|
+
if isinstance(color, RGBColor):
|
|
124
|
+
return color
|
|
125
|
+
if isinstance(color, str):
|
|
126
|
+
return RGBColor.from_hex(color)
|
|
127
|
+
# Treat any other sequence as an (r, g, b) triple.
|
|
128
|
+
r, g, b = color
|
|
129
|
+
return RGBColor(r, g, b)
|