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,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)