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