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.
Files changed (175) hide show
  1. pptx2/__init__.py +152 -0
  2. pptx2/_color.py +75 -0
  3. pptx2/_slide_importer.py +597 -0
  4. pptx2/_svg.py +155 -0
  5. pptx2/_template_applier.py +292 -0
  6. pptx2/_textstyle.py +187 -0
  7. pptx2/accessibility.py +365 -0
  8. pptx2/action.py +270 -0
  9. pptx2/animation.py +2237 -0
  10. pptx2/api.py +49 -0
  11. pptx2/audit.py +258 -0
  12. pptx2/chart/__init__.py +0 -0
  13. pptx2/chart/analytics.py +381 -0
  14. pptx2/chart/axis.py +543 -0
  15. pptx2/chart/category.py +200 -0
  16. pptx2/chart/chart.py +670 -0
  17. pptx2/chart/data.py +864 -0
  18. pptx2/chart/datalabel.py +406 -0
  19. pptx2/chart/legend.py +86 -0
  20. pptx2/chart/marker.py +70 -0
  21. pptx2/chart/palettes.py +129 -0
  22. pptx2/chart/plot.py +462 -0
  23. pptx2/chart/point.py +101 -0
  24. pptx2/chart/quick_layouts.py +325 -0
  25. pptx2/chart/series.py +334 -0
  26. pptx2/chart/xlsx.py +272 -0
  27. pptx2/chart/xmlwriter.py +1845 -0
  28. pptx2/compose/__init__.py +28 -0
  29. pptx2/compose/from_spec.py +1094 -0
  30. pptx2/design/__init__.py +8 -0
  31. pptx2/design/components.py +607 -0
  32. pptx2/design/figures.py +389 -0
  33. pptx2/design/layout.py +370 -0
  34. pptx2/design/recipes.py +1967 -0
  35. pptx2/design/style.py +209 -0
  36. pptx2/design/tokens.py +915 -0
  37. pptx2/diagrams.py +754 -0
  38. pptx2/dml/__init__.py +0 -0
  39. pptx2/dml/chtfmt.py +40 -0
  40. pptx2/dml/color.py +496 -0
  41. pptx2/dml/effect.py +909 -0
  42. pptx2/dml/fill.py +691 -0
  43. pptx2/dml/line.py +287 -0
  44. pptx2/dml/picture.py +212 -0
  45. pptx2/dml/three_d.py +381 -0
  46. pptx2/enum/__init__.py +0 -0
  47. pptx2/enum/action.py +71 -0
  48. pptx2/enum/animation.py +31 -0
  49. pptx2/enum/base.py +218 -0
  50. pptx2/enum/chart.py +574 -0
  51. pptx2/enum/dml.py +740 -0
  52. pptx2/enum/lang.py +685 -0
  53. pptx2/enum/presentation.py +133 -0
  54. pptx2/enum/shapes.py +1029 -0
  55. pptx2/enum/text.py +230 -0
  56. pptx2/exc.py +42 -0
  57. pptx2/formats.py +139 -0
  58. pptx2/geometry.py +420 -0
  59. pptx2/inherit.py +109 -0
  60. pptx2/lint.py +2256 -0
  61. pptx2/math.py +177 -0
  62. pptx2/media.py +197 -0
  63. pptx2/opc/__init__.py +0 -0
  64. pptx2/opc/constants.py +332 -0
  65. pptx2/opc/oxml.py +188 -0
  66. pptx2/opc/package.py +762 -0
  67. pptx2/opc/packuri.py +109 -0
  68. pptx2/opc/serialized.py +296 -0
  69. pptx2/opc/shared.py +20 -0
  70. pptx2/opc/spec.py +45 -0
  71. pptx2/oxml/__init__.py +555 -0
  72. pptx2/oxml/action.py +53 -0
  73. pptx2/oxml/chart/__init__.py +0 -0
  74. pptx2/oxml/chart/axis.py +337 -0
  75. pptx2/oxml/chart/chart.py +481 -0
  76. pptx2/oxml/chart/datalabel.py +253 -0
  77. pptx2/oxml/chart/legend.py +72 -0
  78. pptx2/oxml/chart/marker.py +61 -0
  79. pptx2/oxml/chart/plot.py +365 -0
  80. pptx2/oxml/chart/series.py +425 -0
  81. pptx2/oxml/chart/shared.py +220 -0
  82. pptx2/oxml/coreprops.py +288 -0
  83. pptx2/oxml/dml/__init__.py +0 -0
  84. pptx2/oxml/dml/color.py +135 -0
  85. pptx2/oxml/dml/effect.py +213 -0
  86. pptx2/oxml/dml/fill.py +316 -0
  87. pptx2/oxml/dml/line.py +12 -0
  88. pptx2/oxml/dml/three_d.py +110 -0
  89. pptx2/oxml/ns.py +135 -0
  90. pptx2/oxml/presentation.py +313 -0
  91. pptx2/oxml/shapes/__init__.py +19 -0
  92. pptx2/oxml/shapes/autoshape.py +467 -0
  93. pptx2/oxml/shapes/connector.py +107 -0
  94. pptx2/oxml/shapes/graphfrm.py +347 -0
  95. pptx2/oxml/shapes/groupshape.py +329 -0
  96. pptx2/oxml/shapes/picture.py +270 -0
  97. pptx2/oxml/shapes/shared.py +577 -0
  98. pptx2/oxml/simpletypes.py +1027 -0
  99. pptx2/oxml/slide.py +563 -0
  100. pptx2/oxml/table.py +650 -0
  101. pptx2/oxml/text.py +815 -0
  102. pptx2/oxml/theme.py +36 -0
  103. pptx2/oxml/xmlchemy.py +717 -0
  104. pptx2/package.py +222 -0
  105. pptx2/parts/__init__.py +0 -0
  106. pptx2/parts/chart.py +95 -0
  107. pptx2/parts/coreprops.py +167 -0
  108. pptx2/parts/diagram.py +37 -0
  109. pptx2/parts/embeddedpackage.py +93 -0
  110. pptx2/parts/image.py +275 -0
  111. pptx2/parts/media.py +37 -0
  112. pptx2/parts/presentation.py +136 -0
  113. pptx2/parts/slide.py +371 -0
  114. pptx2/presentation.py +408 -0
  115. pptx2/py.typed +0 -0
  116. pptx2/render.py +586 -0
  117. pptx2/section.py +272 -0
  118. pptx2/shapes/__init__.py +26 -0
  119. pptx2/shapes/autoshape.py +442 -0
  120. pptx2/shapes/base.py +1078 -0
  121. pptx2/shapes/connector.py +297 -0
  122. pptx2/shapes/freeform.py +337 -0
  123. pptx2/shapes/graphfrm.py +316 -0
  124. pptx2/shapes/group.py +264 -0
  125. pptx2/shapes/picture.py +422 -0
  126. pptx2/shapes/placeholder.py +468 -0
  127. pptx2/shapes/shapetree.py +2027 -0
  128. pptx2/shared.py +82 -0
  129. pptx2/skill/SKILL.md +450 -0
  130. pptx2/skill/__init__.py +78 -0
  131. pptx2/skill/__main__.py +64 -0
  132. pptx2/skill/references/animations.md +189 -0
  133. pptx2/skill/references/basics.md +421 -0
  134. pptx2/skill/references/charts.md +254 -0
  135. pptx2/skill/references/compose.md +234 -0
  136. pptx2/skill/references/design.md +366 -0
  137. pptx2/skill/references/effects.md +249 -0
  138. pptx2/skill/references/end-to-end-deck.md +231 -0
  139. pptx2/skill/references/geometry-and-arrows.md +334 -0
  140. pptx2/skill/references/lint.md +275 -0
  141. pptx2/skill/references/math.md +86 -0
  142. pptx2/skill/references/picture-effects.md +129 -0
  143. pptx2/skill/references/render.md +151 -0
  144. pptx2/skill/references/smart-art.md +75 -0
  145. pptx2/skill/references/space-aware-authoring.md +249 -0
  146. pptx2/skill/references/tables.md +244 -0
  147. pptx2/skill/references/theme.md +127 -0
  148. pptx2/skill/references/three-d.md +109 -0
  149. pptx2/skill/references/transitions.md +100 -0
  150. pptx2/slide.py +1244 -0
  151. pptx2/smart_art.py +220 -0
  152. pptx2/spec.py +633 -0
  153. pptx2/table.py +1181 -0
  154. pptx2/table_styles.py +184 -0
  155. pptx2/templates/default.pptx +0 -0
  156. pptx2/templates/docx-icon.emf +0 -0
  157. pptx2/templates/generic-icon.emf +0 -0
  158. pptx2/templates/notes.xml +23 -0
  159. pptx2/templates/notesMaster.xml +352 -0
  160. pptx2/templates/pptx-icon.emf +0 -0
  161. pptx2/templates/theme.xml +321 -0
  162. pptx2/templates/xlsx-icon.emf +0 -0
  163. pptx2/text/__init__.py +0 -0
  164. pptx2/text/fonts.py +482 -0
  165. pptx2/text/layout.py +374 -0
  166. pptx2/text/text.py +1272 -0
  167. pptx2/theme.py +721 -0
  168. pptx2/types.py +36 -0
  169. pptx2/util.py +263 -0
  170. python_pptx2-2.13.0.dist-info/METADATA +351 -0
  171. python_pptx2-2.13.0.dist-info/RECORD +175 -0
  172. python_pptx2-2.13.0.dist-info/WHEEL +5 -0
  173. python_pptx2-2.13.0.dist-info/entry_points.txt +3 -0
  174. python_pptx2-2.13.0.dist-info/licenses/LICENSE +22 -0
  175. python_pptx2-2.13.0.dist-info/top_level.txt +1 -0
@@ -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
@@ -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)