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/api.py ADDED
@@ -0,0 +1,49 @@
1
+ """Directly exposed API classes, Presentation for now.
2
+
3
+ Provides some syntactic sugar for interacting with the pptx2.presentation.Package graph and also
4
+ provides some insulation so not so many classes in the other modules need to be named as internal
5
+ (leading underscore).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from typing import IO, TYPE_CHECKING
12
+
13
+ from pptx2.opc.constants import CONTENT_TYPE as CT
14
+ from pptx2.package import Package
15
+
16
+ if TYPE_CHECKING:
17
+ from pptx2 import presentation
18
+ from pptx2.parts.presentation import PresentationPart
19
+
20
+
21
+ def Presentation(pptx: str | IO[bytes] | None = None) -> presentation.Presentation:
22
+ """
23
+ Return a |Presentation| object loaded from *pptx*, where *pptx* can be
24
+ either a path to a ``.pptx`` file (a string) or a file-like object. If
25
+ *pptx* is missing or ``None``, the built-in default presentation
26
+ "template" is loaded.
27
+ """
28
+ if pptx is None:
29
+ pptx = _default_pptx_path()
30
+
31
+ presentation_part = Package.open(pptx).main_document_part
32
+
33
+ if not _is_pptx_package(presentation_part):
34
+ tmpl = "file '%s' is not a PowerPoint file, content type is '%s'"
35
+ raise ValueError(tmpl % (pptx, presentation_part.content_type))
36
+
37
+ return presentation_part.presentation
38
+
39
+
40
+ def _default_pptx_path() -> str:
41
+ """Return the path to the built-in default .pptx package."""
42
+ _thisdir = os.path.split(__file__)[0]
43
+ return os.path.join(_thisdir, "templates", "default.pptx")
44
+
45
+
46
+ def _is_pptx_package(prs_part: PresentationPart):
47
+ """Return |True| if *prs_part* is a valid main document part, |False| otherwise."""
48
+ valid_content_types = (CT.PML_PRESENTATION_MAIN, CT.PML_PRES_MACRO_MAIN)
49
+ return prs_part.content_type in valid_content_types
pptx2/audit.py ADDED
@@ -0,0 +1,258 @@
1
+ """One-call deck audit — combines lint, picture sanity, and empty-slide checks.
2
+
3
+ For agents producing a deck end-to-end the question is "did I do a
4
+ good job?". :func:`audit` returns a small structured report so the
5
+ agent can include a "what I shipped" summary in its reply to the user
6
+ without crawling every slide manually::
7
+
8
+ from pptx2 import audit
9
+
10
+ report = audit(prs)
11
+ print(report.markdown())
12
+
13
+ The report contains:
14
+
15
+ * ``lint_issues`` — every :class:`~pptx2.lint.LintIssue` aggregated
16
+ across slides, each annotated with the slide index it came from.
17
+ * ``broken_pictures`` — pictures whose dimensions are zero or whose
18
+ embedded image part appears corrupt.
19
+ * ``empty_slides`` — slide indexes whose only shapes are background
20
+ decoration (full-bleed rects, layout placeholders).
21
+ * ``font_warnings`` — fonts referenced by shapes that aren't typically
22
+ pre-installed on Windows / macOS / Linux (raises false positives;
23
+ treat as advisory).
24
+ * ``size_warnings`` — pictures larger than ``size_warn_bytes`` (default
25
+ 2 MB) — flagged so callers can choose to compress.
26
+
27
+ The audit is read-only — it never mutates the deck.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ from dataclasses import dataclass, field
33
+ from typing import TYPE_CHECKING, Any
34
+
35
+ if TYPE_CHECKING:
36
+ from pptx2.api import Presentation
37
+ from pptx2.lint import LintIssue
38
+
39
+
40
+ __all__ = ["AuditReport", "audit"]
41
+
42
+
43
+ # Fonts that ship with stock Windows / macOS / Office installs. Used as
44
+ # a (very conservative) safe-list for the font_warnings probe.
45
+ _COMMON_FONTS = frozenset(
46
+ name.lower()
47
+ for name in (
48
+ "Arial", "Calibri", "Helvetica", "Times New Roman", "Cambria",
49
+ "Verdana", "Tahoma", "Georgia", "Courier New", "Consolas",
50
+ "Segoe UI", "Trebuchet MS", "Garamond", "Impact", "Comic Sans MS",
51
+ "Lucida Console", "Palatino", "Palatino Linotype", "Symbol",
52
+ "Wingdings", "Wingdings 2", "Wingdings 3", "Webdings",
53
+ "Inter", "Roboto", "Open Sans", "Lato", "Noto Sans",
54
+ )
55
+ )
56
+
57
+
58
+ @dataclass
59
+ class AuditReport:
60
+ """Structured summary returned by :func:`audit`."""
61
+
62
+ lint_issues: list[tuple[int, "LintIssue"]] = field(default_factory=list)
63
+ broken_pictures: list[tuple[int, Any]] = field(default_factory=list)
64
+ empty_slides: list[int] = field(default_factory=list)
65
+ font_warnings: list[tuple[int, str]] = field(default_factory=list)
66
+ size_warnings: list[tuple[int, str, int]] = field(default_factory=list)
67
+ total_slides: int = 0
68
+
69
+ @property
70
+ def has_errors(self) -> bool:
71
+ from pptx2.lint import LintSeverity
72
+
73
+ return any(
74
+ issue.severity == LintSeverity.ERROR
75
+ for _idx, issue in self.lint_issues
76
+ )
77
+
78
+ def markdown(self) -> str:
79
+ """Render the report as a markdown string suitable for chat replies."""
80
+ lines = [f"# Audit report — {self.total_slides} slide(s)"]
81
+ if not (
82
+ self.lint_issues
83
+ or self.broken_pictures
84
+ or self.empty_slides
85
+ or self.font_warnings
86
+ or self.size_warnings
87
+ ):
88
+ lines.append("")
89
+ lines.append("**No issues found.**")
90
+ return "\n".join(lines)
91
+
92
+ if self.lint_issues:
93
+ lines.append("")
94
+ lines.append(f"## Lint ({len(self.lint_issues)})")
95
+ for idx, issue in self.lint_issues:
96
+ lines.append(f"- slide {idx}: {issue}")
97
+ if self.broken_pictures:
98
+ lines.append("")
99
+ lines.append(f"## Broken pictures ({len(self.broken_pictures)})")
100
+ for idx, shape in self.broken_pictures:
101
+ lines.append(f"- slide {idx}: `{shape.name}` is zero-sized or missing image data")
102
+ if self.empty_slides:
103
+ lines.append("")
104
+ lines.append("## Empty slides")
105
+ for idx in self.empty_slides:
106
+ lines.append(f"- slide {idx}: no content-bearing shapes")
107
+ if self.font_warnings:
108
+ lines.append("")
109
+ lines.append(f"## Font warnings ({len(self.font_warnings)})")
110
+ seen = set()
111
+ for idx, font in self.font_warnings:
112
+ key = (idx, font)
113
+ if key in seen:
114
+ continue
115
+ seen.add(key)
116
+ lines.append(f"- slide {idx}: uses `{font}` (not in the common safe-list)")
117
+ if self.size_warnings:
118
+ lines.append("")
119
+ lines.append(f"## Large pictures ({len(self.size_warnings)})")
120
+ for idx, name, size_bytes in self.size_warnings:
121
+ lines.append(
122
+ f"- slide {idx}: `{name}` is {size_bytes / 1_000_000:.1f} MB"
123
+ )
124
+ return "\n".join(lines)
125
+
126
+ def to_dict(self) -> dict[str, Any]:
127
+ """Return a JSON-serializable dict of the whole audit.
128
+
129
+ This is the machine-readable counterpart to :meth:`markdown` — built
130
+ for the agent loop that generates a deck, audits it, and feeds the
131
+ result back to a model (or a CI gate) to decide what to fix. Every
132
+ lint issue is expanded via :meth:`~pptx2.lint.LintIssue.to_dict`,
133
+ and slide-relative shape references are reduced to names::
134
+
135
+ report = audit(prs)
136
+ if report.to_dict()["has_errors"]:
137
+ ...
138
+
139
+ Top-level keys: ``total_slides``, ``has_errors``, ``lint_issues``,
140
+ ``broken_pictures``, ``empty_slides``, ``font_warnings``,
141
+ ``size_warnings``.
142
+ """
143
+ return {
144
+ "total_slides": self.total_slides,
145
+ "has_errors": self.has_errors,
146
+ "lint_issues": [
147
+ {"slide": idx, **issue.to_dict()} for idx, issue in self.lint_issues
148
+ ],
149
+ "broken_pictures": [
150
+ {"slide": idx, "shape": shape.name} for idx, shape in self.broken_pictures
151
+ ],
152
+ "empty_slides": list(self.empty_slides),
153
+ "font_warnings": [
154
+ {"slide": idx, "font": font} for idx, font in self.font_warnings
155
+ ],
156
+ "size_warnings": [
157
+ {"slide": idx, "shape": name, "bytes": size_bytes}
158
+ for idx, name, size_bytes in self.size_warnings
159
+ ],
160
+ }
161
+
162
+ def to_json(self, *, indent: int | None = 2) -> str:
163
+ """Return :meth:`to_dict` serialized as a JSON string."""
164
+ import json
165
+
166
+ return json.dumps(self.to_dict(), indent=indent)
167
+
168
+ def __str__(self) -> str:
169
+ return self.markdown()
170
+
171
+
172
+ def audit(
173
+ prs: "Presentation",
174
+ *,
175
+ size_warn_bytes: int = 2_000_000,
176
+ check_fonts: bool = True,
177
+ ) -> AuditReport:
178
+ """Walk the deck and return an :class:`AuditReport` summary.
179
+
180
+ Read-only — never mutates the presentation. Each slide is linted
181
+ in passing-defaults mode; per-slide overrides should be supplied
182
+ via the slide-level ``slide.lint(...)`` API directly.
183
+ """
184
+ from pptx2.shapes.picture import Picture
185
+
186
+ report = AuditReport()
187
+ slides = list(prs.slides)
188
+ report.total_slides = len(slides)
189
+
190
+ # Read slide dimensions once for the full-bleed-background heuristic.
191
+ slide_w = int(prs.slide_width) if prs.slide_width else 0
192
+ slide_h = int(prs.slide_height) if prs.slide_height else 0
193
+ bleed_w = slide_w * 0.95
194
+ bleed_h = slide_h * 0.95
195
+
196
+ for idx, slide in enumerate(slides):
197
+ # Lint each slide and prefix with the index.
198
+ for issue in slide.lint().issues:
199
+ report.lint_issues.append((idx, issue))
200
+
201
+ content_shapes = 0
202
+ for shape in slide.shapes:
203
+ # Skip layout placeholders that are entirely inherited.
204
+ if (
205
+ shape.is_placeholder
206
+ and getattr(shape, "has_text_frame", False)
207
+ and not shape.text_frame.text.strip()
208
+ ):
209
+ continue
210
+ # Skip full-bleed background rectangles — they're decoration,
211
+ # not content, so a slide that only contains them should be
212
+ # flagged as empty (matches the documented contract for
213
+ # ``empty_slides``).
214
+ try:
215
+ sw = int(shape.width)
216
+ sh = int(shape.height)
217
+ except Exception:
218
+ sw = sh = 0
219
+ if (
220
+ bleed_w > 0
221
+ and bleed_h > 0
222
+ and sw >= bleed_w
223
+ and sh >= bleed_h
224
+ ):
225
+ has_text = (
226
+ getattr(shape, "has_text_frame", False)
227
+ and shape.text_frame.text.strip()
228
+ )
229
+ if not has_text:
230
+ continue
231
+ content_shapes += 1
232
+
233
+ # Picture sanity.
234
+ if isinstance(shape, Picture):
235
+ if int(shape.width) <= 0 or int(shape.height) <= 0:
236
+ report.broken_pictures.append((idx, shape))
237
+ # Picture size warning via the underlying image part.
238
+ try:
239
+ img = shape.image # type: ignore[attr-defined]
240
+ size_bytes = len(img.blob)
241
+ if size_bytes > size_warn_bytes:
242
+ report.size_warnings.append((idx, shape.name, size_bytes))
243
+ except Exception:
244
+ pass
245
+
246
+ # Font warning sweep.
247
+ if check_fonts and getattr(shape, "has_text_frame", False):
248
+ tf = shape.text_frame
249
+ for para in tf.paragraphs:
250
+ for run in para.runs:
251
+ name = run.font.name
252
+ if name and name.lower() not in _COMMON_FONTS:
253
+ report.font_warnings.append((idx, name))
254
+
255
+ if content_shapes == 0:
256
+ report.empty_slides.append(idx)
257
+
258
+ return report
File without changes
@@ -0,0 +1,381 @@
1
+ """Series-level analytics: trendlines and error bars.
2
+
3
+ These objects wrap the ``<c:trendline>`` and ``<c:errBars>`` children of a
4
+ ``<c:ser>`` element and expose an Excel-like authoring API on chart series.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Sequence
10
+
11
+ from pptx2.enum.chart import (
12
+ XL_ERROR_BAR_INCLUDE,
13
+ XL_ERROR_BAR_TYPE,
14
+ XL_TRENDLINE_TYPE,
15
+ )
16
+ from pptx2.oxml import parse_xml
17
+ from pptx2.oxml.ns import nsdecls
18
+
19
+ # -- map of the short-name strings accepted at the API boundary to the
20
+ # -- corresponding enum member, so callers can pass e.g. "linear" or
21
+ # -- "movingAvg" without importing the enum.
22
+ _TRENDLINE_ALIASES = {
23
+ "linear": XL_TRENDLINE_TYPE.LINEAR,
24
+ "exp": XL_TRENDLINE_TYPE.EXPONENTIAL,
25
+ "exponential": XL_TRENDLINE_TYPE.EXPONENTIAL,
26
+ "log": XL_TRENDLINE_TYPE.LOGARITHMIC,
27
+ "logarithmic": XL_TRENDLINE_TYPE.LOGARITHMIC,
28
+ "movingavg": XL_TRENDLINE_TYPE.MOVING_AVERAGE,
29
+ "moving_average": XL_TRENDLINE_TYPE.MOVING_AVERAGE,
30
+ "poly": XL_TRENDLINE_TYPE.POLYNOMIAL,
31
+ "polynomial": XL_TRENDLINE_TYPE.POLYNOMIAL,
32
+ "power": XL_TRENDLINE_TYPE.POWER,
33
+ }
34
+
35
+ _ERR_DIRECTION_ALIASES = {
36
+ "both": XL_ERROR_BAR_TYPE.BOTH,
37
+ "plus": XL_ERROR_BAR_TYPE.PLUS,
38
+ "minus": XL_ERROR_BAR_TYPE.MINUS,
39
+ }
40
+
41
+
42
+ def _resolve_trendline_type(kind):
43
+ """Return the `XL_TRENDLINE_TYPE` member for *kind*.
44
+
45
+ *kind* may already be an enum member, or one of the short-name strings
46
+ such as ``"linear"`` / ``"movingAvg"`` / ``"poly"``.
47
+ """
48
+ if isinstance(kind, XL_TRENDLINE_TYPE):
49
+ return kind
50
+ try:
51
+ return _TRENDLINE_ALIASES[str(kind).lower()]
52
+ except KeyError:
53
+ raise ValueError(
54
+ "trendline kind must be one of %r or an XL_TRENDLINE_TYPE member; got %r"
55
+ % (sorted(set(_TRENDLINE_ALIASES)), kind)
56
+ )
57
+
58
+
59
+ def _resolve_err_direction(direction):
60
+ """Return the `XL_ERROR_BAR_TYPE` member for *direction*."""
61
+ if isinstance(direction, XL_ERROR_BAR_TYPE):
62
+ return direction
63
+ try:
64
+ return _ERR_DIRECTION_ALIASES[str(direction).lower()]
65
+ except KeyError:
66
+ raise ValueError(
67
+ "error-bar direction must be 'both', 'plus', or 'minus' (or an "
68
+ "XL_ERROR_BAR_TYPE member); got %r" % (direction,)
69
+ )
70
+
71
+
72
+ class Trendline(object):
73
+ """A single ``<c:trendline>`` fitted to a chart series."""
74
+
75
+ def __init__(self, trendline):
76
+ self._element = trendline
77
+ self._trendline = trendline
78
+
79
+ @property
80
+ def trendline_type(self):
81
+ """Member of :ref:`XL_TRENDLINE_TYPE` for this trendline's curve."""
82
+ return self._trendline.trendlineType.val
83
+
84
+ @property
85
+ def show_equation(self):
86
+ """Read/write bool: whether the fitted equation is displayed."""
87
+ dispEq = self._trendline.dispEq
88
+ if dispEq is None:
89
+ return False
90
+ return bool(dispEq.val)
91
+
92
+ @show_equation.setter
93
+ def show_equation(self, value):
94
+ if bool(value):
95
+ self._trendline.get_or_add_dispEq().val = True
96
+ else:
97
+ self._trendline._remove_dispEq()
98
+
99
+ @property
100
+ def show_r_squared(self):
101
+ """Read/write bool: whether the R-squared value is displayed."""
102
+ dispRSqr = self._trendline.dispRSqr
103
+ if dispRSqr is None:
104
+ return False
105
+ return bool(dispRSqr.val)
106
+
107
+ @show_r_squared.setter
108
+ def show_r_squared(self, value):
109
+ if bool(value):
110
+ self._trendline.get_or_add_dispRSqr().val = True
111
+ else:
112
+ self._trendline._remove_dispRSqr()
113
+
114
+ @property
115
+ def name(self):
116
+ """Read/write str: the legend label for this trendline, or |None|."""
117
+ name = self._trendline.name
118
+ if name is None:
119
+ return None
120
+ return name.text
121
+
122
+ @name.setter
123
+ def name(self, value):
124
+ if value is None:
125
+ self._trendline._remove_name()
126
+ return
127
+ self._trendline.get_or_add_name().text = str(value)
128
+
129
+ @property
130
+ def order(self):
131
+ """Read/write int: polynomial order (2-6), or |None| if unset."""
132
+ order = self._trendline.order
133
+ if order is None:
134
+ return None
135
+ return order.val
136
+
137
+ @order.setter
138
+ def order(self, value):
139
+ if value is None:
140
+ self._trendline._remove_order()
141
+ return
142
+ order = int(value)
143
+ if not 2 <= order <= 6:
144
+ raise ValueError(
145
+ "polynomial trendline order must be in the range 2-6 "
146
+ "(ST_Order); got %r" % (value,)
147
+ )
148
+ self._trendline.get_or_add_order().val = order
149
+
150
+ @property
151
+ def period(self):
152
+ """Read/write int: moving-average period (>=2), or |None| if unset."""
153
+ period = self._trendline.period
154
+ if period is None:
155
+ return None
156
+ return period.val
157
+
158
+ @period.setter
159
+ def period(self, value):
160
+ if value is None:
161
+ self._trendline._remove_period()
162
+ return
163
+ period = int(value)
164
+ if period < 2:
165
+ raise ValueError(
166
+ "moving-average trendline period must be 2 or greater "
167
+ "(ST_Period); got %r" % (value,)
168
+ )
169
+ self._trendline.get_or_add_period().val = period
170
+
171
+ @property
172
+ def forward(self):
173
+ """Read/write float: forward projection in periods, or |None|."""
174
+ forward = self._trendline.forward
175
+ if forward is None:
176
+ return None
177
+ return forward.val
178
+
179
+ @forward.setter
180
+ def forward(self, value):
181
+ if value is None:
182
+ self._trendline._remove_forward()
183
+ return
184
+ self._trendline.get_or_add_forward().val = float(value)
185
+
186
+ @property
187
+ def backward(self):
188
+ """Read/write float: backward projection in periods, or |None|."""
189
+ backward = self._trendline.backward
190
+ if backward is None:
191
+ return None
192
+ return backward.val
193
+
194
+ @backward.setter
195
+ def backward(self, value):
196
+ if value is None:
197
+ self._trendline._remove_backward()
198
+ return
199
+ self._trendline.get_or_add_backward().val = float(value)
200
+
201
+
202
+ class Trendlines(Sequence):
203
+ """The collection of ``<c:trendline>`` elements for a series.
204
+
205
+ Supports ``len()``, iteration, indexed access, and ``.add(...)``.
206
+ """
207
+
208
+ def __init__(self, ser):
209
+ self._element = ser
210
+ self._ser = ser
211
+
212
+ def __getitem__(self, index):
213
+ trendlines = self._ser.trendline_lst
214
+ if isinstance(index, slice):
215
+ return [Trendline(t) for t in trendlines[index]]
216
+ return Trendline(trendlines[index])
217
+
218
+ def __len__(self):
219
+ return len(self._ser.trendline_lst)
220
+
221
+ def add(
222
+ self,
223
+ kind=XL_TRENDLINE_TYPE.LINEAR,
224
+ *,
225
+ show_equation=False,
226
+ show_r_squared=False,
227
+ name=None,
228
+ order=None,
229
+ period=None,
230
+ forward=None,
231
+ backward=None,
232
+ ):
233
+ """Add and return a new |Trendline| of the given *kind*.
234
+
235
+ *kind* accepts an :class:`XL_TRENDLINE_TYPE` member or a short-name
236
+ string (``"linear"``, ``"exp"``, ``"log"``, ``"movingAvg"``,
237
+ ``"poly"``, ``"power"``). *order* applies to polynomial trendlines,
238
+ *period* to moving-average trendlines. *forward* / *backward* project
239
+ the curve that many periods past the data.
240
+ """
241
+ trendline_type = _resolve_trendline_type(kind)
242
+ trendline = self._ser._add_trendline()
243
+ # -- write the `val` attribute explicitly (even for the schema default
244
+ # -- "linear") so the emitted XML matches Excel/PowerPoint output and
245
+ # -- is unambiguous to readers.
246
+ trendline.trendlineType.set("val", trendline_type.xml_value)
247
+
248
+ proxy = Trendline(trendline)
249
+ if name is not None:
250
+ proxy.name = name
251
+ if order is not None:
252
+ proxy.order = order
253
+ if period is not None:
254
+ proxy.period = period
255
+ if forward is not None:
256
+ proxy.forward = forward
257
+ if backward is not None:
258
+ proxy.backward = backward
259
+ # -- dispRSqr / dispEq come late in the schema, set after order/period
260
+ if show_r_squared:
261
+ proxy.show_r_squared = True
262
+ if show_equation:
263
+ proxy.show_equation = True
264
+ return proxy
265
+
266
+
267
+ class ErrorBars(object):
268
+ """The ``<c:errBars>`` element for a series, with Excel-style constructors.
269
+
270
+ The collection is materialised lazily — the underlying ``<c:errBars>``
271
+ element is only created when one of the constructor methods (``fixed``,
272
+ ``percentage``, ``standard_deviation``, ``standard_error``, ``custom``) is
273
+ called. Calling a constructor a second time replaces the prior settings.
274
+ """
275
+
276
+ def __init__(self, ser):
277
+ self._element = ser
278
+ self._ser = ser
279
+
280
+ @property
281
+ def exists(self):
282
+ """True if an ``<c:errBars>`` element is present on this series."""
283
+ return self._ser.errBars is not None
284
+
285
+ @property
286
+ def error_bar_type(self):
287
+ """The :ref:`XL_ERROR_BAR_TYPE` direction, or |None| if no error bars."""
288
+ errBars = self._ser.errBars
289
+ if errBars is None or errBars.errBarType is None:
290
+ return None
291
+ return errBars.errBarType.val
292
+
293
+ @property
294
+ def include_type(self):
295
+ """The :ref:`XL_ERROR_BAR_INCLUDE` value-type, or |None|."""
296
+ errBars = self._ser.errBars
297
+ if errBars is None or errBars.errValType is None:
298
+ return None
299
+ return errBars.errValType.val
300
+
301
+ @property
302
+ def value(self):
303
+ """The numeric error amount (fixed/percentage/stdDev), or |None|."""
304
+ errBars = self._ser.errBars
305
+ if errBars is None or errBars.val is None:
306
+ return None
307
+ return float(errBars.val.get("val"))
308
+
309
+ def remove(self):
310
+ """Remove any error bars present on this series."""
311
+ self._ser._remove_errBars()
312
+
313
+ # -- Excel-mirroring constructors -----------------------------------
314
+
315
+ def fixed(self, value, *, direction="both"):
316
+ """Set fixed-value error bars of magnitude *value*."""
317
+ return self._configure(XL_ERROR_BAR_INCLUDE.FIXED_VALUE, direction, value=value)
318
+
319
+ def percentage(self, pct, *, direction="both"):
320
+ """Set percentage error bars of *pct* percent of each value."""
321
+ return self._configure(XL_ERROR_BAR_INCLUDE.PERCENTAGE, direction, value=pct)
322
+
323
+ def standard_deviation(self, n=1, *, direction="both"):
324
+ """Set error bars of *n* standard deviations."""
325
+ return self._configure(XL_ERROR_BAR_INCLUDE.STANDARD_DEVIATION, direction, value=n)
326
+
327
+ def standard_error(self, *, direction="both"):
328
+ """Set standard-error error bars (no numeric amount)."""
329
+ return self._configure(XL_ERROR_BAR_INCLUDE.STANDARD_ERROR, direction, value=None)
330
+
331
+ def custom(self, plus, minus, *, direction="both"):
332
+ """Set custom error bars from per-point *plus* / *minus* sequences.
333
+
334
+ *plus* and *minus* are sequences of floats, one per data point.
335
+ """
336
+ errBars = self._reset_errBars(direction)
337
+ errBars.get_or_add_errValType().set("val", XL_ERROR_BAR_INCLUDE.CUSTOM.xml_value)
338
+ errBars.append(self._num_data_source("c:plus", plus))
339
+ errBars.append(self._num_data_source("c:minus", minus))
340
+ return self
341
+
342
+ # -- internals ------------------------------------------------------
343
+
344
+ def _configure(self, include_type, direction, *, value):
345
+ errBars = self._reset_errBars(direction)
346
+ errBars.get_or_add_errValType().set("val", include_type.xml_value)
347
+ if value is not None:
348
+ val = parse_xml('<c:val %s val="%s"/>' % (nsdecls("c"), float(value)))
349
+ errBars.append(val)
350
+ return self
351
+
352
+ def _reset_errBars(self, direction):
353
+ """Return a fresh ``<c:errBars>`` element with *direction* applied.
354
+
355
+ Any pre-existing error bars are removed first so repeated constructor
356
+ calls don't accumulate stale ``plus`` / ``minus`` / ``val`` children.
357
+ """
358
+ self._ser._remove_errBars()
359
+ errBars = self._ser.get_or_add_errBars()
360
+ errBars.get_or_add_errBarType().set("val", _resolve_err_direction(direction).xml_value)
361
+ return errBars
362
+
363
+ @staticmethod
364
+ def _num_data_source(tag, values):
365
+ """Return a ``<c:plus>`` or ``<c:minus>`` numeric-literal data source."""
366
+ seq = list(values)
367
+ pts = "".join(
368
+ '<c:pt idx="%d"><c:v>%s</c:v></c:pt>' % (i, float(v))
369
+ for i, v in enumerate(seq)
370
+ if v is not None
371
+ )
372
+ xml = (
373
+ "<%s %s>"
374
+ "<c:numLit>"
375
+ "<c:formatCode>General</c:formatCode>"
376
+ '<c:ptCount val="%d"/>'
377
+ "%s"
378
+ "</c:numLit>"
379
+ "</%s>"
380
+ ) % (tag, nsdecls("c"), len(seq), pts, tag)
381
+ return parse_xml(xml)