返回 ppt-master
chart_data.py
1 """Native chart metadata normalization."""
2
3 from __future__ import annotations
4
5 import math
6 from typing import Any
7
8 from .marker_common import (
9 _chart_bool,
10 _clean_hex,
11 _compact_key,
12 _first_present,
13 _hex_or_none,
14 _number,
15 _powerpoint_line_width_emu,
16 )
17
18
19 def _chart_number(value: Any) -> int | float:
20 if isinstance(value, bool):
21 raise RuntimeError("Native PPTX chart values must be numeric")
22 try:
23 number = float(value)
24 except (TypeError, ValueError, OverflowError) as exc:
25 raise RuntimeError("Native PPTX chart value is not numeric") from exc
26 if not math.isfinite(number):
27 raise RuntimeError(f"Native PPTX chart value must be finite: {value}")
28 return int(number) if number.is_integer() else number
29
30
31 def _chart_list(value: Any, field_name: str) -> list[Any]:
32 if value is None:
33 return []
34 if not isinstance(value, list):
35 raise RuntimeError(f"Native PPTX chart {field_name} must be a list")
36 return value
37
38
39 def _data_labels_config(payload: dict[str, Any]) -> dict[str, Any] | None:
40 raw = _first_present(payload.get("data_labels"), payload.get("dataLabels"))
41 if raw is None:
42 return None
43 if isinstance(raw, bool):
44 return {} if raw else None
45 if not isinstance(raw, dict):
46 raise RuntimeError("Native PPTX chart data_labels must be a boolean or object")
47 return raw
48
49
50 def _data_label_position(value: Any, chart_type: str, grouping: str | None) -> str | None:
51 """Normalize and validate a label position for its chart plot."""
52 if chart_type == "area":
53 if value is not None:
54 raise RuntimeError("Native PPTX area data labels do not support label position")
55 return None
56 is_stacked = chart_type in {"bar", "column"} and grouping in {
57 "percentStacked", "stacked",
58 }
59 default = "ctr" if is_stacked else (
60 "outEnd" if chart_type in {"bar", "column"} else "t"
61 )
62 if value is None:
63 return default
64 aliases = {
65 "above": "t",
66 "bestfit": "bestFit",
67 "center": "ctr",
68 "inbase": "inBase",
69 "insidebase": "inBase",
70 "insideend": "inEnd",
71 "inend": "inEnd",
72 "outend": "outEnd",
73 "outsideend": "outEnd",
74 }
75 position = aliases.get(_compact_key(value))
76 if not position:
77 raise RuntimeError(
78 "Native PPTX chart data label position must be one of: "
79 "above, best_fit, center, inside_base, inside_end, outside_end"
80 )
81 if chart_type in {"bar", "column"}:
82 if position not in {"ctr", "inBase", "inEnd", "outEnd"}:
83 raise RuntimeError(
84 "Native PPTX bar/column data label position must be one of: "
85 "center, inside_base, inside_end, outside_end"
86 )
87 if is_stacked and position == "outEnd":
88 raise RuntimeError(
89 "Native PPTX stacked bar/column data labels do not support outside_end"
90 )
91 elif chart_type == "line" and position not in {"bestFit", "ctr", "t"}:
92 raise RuntimeError(
93 "Native PPTX line data label position must be one of: above, best_fit, center"
94 )
95 return position
96
97
98 def _chart_data_labels(
99 payload: dict[str, Any],
100 chart_type: str,
101 grouping: str | None,
102 point_count: int,
103 ) -> dict[str, Any] | None:
104 config = _data_labels_config(payload)
105 if config is None:
106 return None
107 if chart_type not in {"area", "bar", "column", "line"}:
108 raise RuntimeError(
109 f"Native PPTX {chart_type} chart data labels are outside current support"
110 )
111 _data_label_position(config.get("position"), chart_type, grouping)
112 if config.get("color") is not None and _hex_or_none(config["color"]) is None:
113 raise RuntimeError("Native PPTX chart data_labels.color must be a color")
114 point_items = _data_label_point_items(
115 config,
116 chart_type,
117 grouping,
118 point_count,
119 )
120 for item in point_items:
121 if item.get("color") is not None and _hex_or_none(item["color"]) is None:
122 raise RuntimeError(
123 "Native PPTX chart data_labels.points color must be a color"
124 )
125 colors = _chart_list(
126 _first_present(
127 config.get("colors"),
128 config.get("label_colors"),
129 config.get("labelColors"),
130 ),
131 "data_labels.colors",
132 )
133 if colors and len(colors) != point_count:
134 raise RuntimeError("Native PPTX chart data_labels.colors must match point count")
135 if any(_hex_or_none(color) is None for color in colors):
136 raise RuntimeError("Native PPTX chart data_labels.colors entries must be colors")
137 return config
138
139
140 def _data_label_point_items(
141 config: dict[str, Any],
142 chart_type: str,
143 grouping: str | None,
144 point_count: int,
145 ) -> list[dict[str, Any]]:
146 """Normalize selected point labels and validate their plot semantics."""
147 raw_points = config.get("points")
148 if raw_points is None:
149 return []
150 items: list[dict[str, Any]] = []
151 seen: set[int] = set()
152 for item in _chart_list(raw_points, "data_labels.points"):
153 if isinstance(item, dict):
154 raw_index = item.get("idx")
155 data = dict(item)
156 else:
157 raw_index = item
158 data = {}
159 if isinstance(raw_index, bool):
160 raise RuntimeError("Native PPTX chart data_labels.points idx must be an integer")
161 index_value = _number(raw_index, "data_labels.points idx")
162 if not index_value.is_integer():
163 raise RuntimeError("Native PPTX chart data_labels.points idx must be an integer")
164 index = int(index_value)
165 if index < 0 or index >= point_count:
166 raise RuntimeError("Native PPTX chart data_labels.points idx is outside point range")
167 if index in seen:
168 raise RuntimeError("Native PPTX chart data_labels.points idx values must be unique")
169 _data_label_position(
170 _first_present(data.get("position"), config.get("position")),
171 chart_type,
172 grouping,
173 )
174 seen.add(index)
175 data["idx"] = index
176 items.append(data)
177 return items
178
179
180 _CATEGORY_CHART_TYPES = {
181 "area",
182 "bar",
183 "column",
184 "doughnut",
185 "line",
186 "of_pie",
187 "pie",
188 "radar",
189 }
190 _XY_CHART_TYPES = {"scatter", "bubble"}
191 _CHARTEX_CHART_TYPES = {
192 "box_whisker",
193 "funnel",
194 "histogram",
195 "pareto",
196 "sunburst",
197 "treemap",
198 "waterfall",
199 }
200 _DEFERRED_CHART_TYPES = {
201 "bullet",
202 "gantt",
203 "heatmap",
204 "map",
205 }
206 _UNSUPPORTED_3D_CHART_TYPES = {
207 "area3d",
208 "bar3d",
209 "column3d",
210 "line3d",
211 "pie3d",
212 "surface",
213 }
214 _DEFAULT_CHART_COLORS = [
215 "4472C4",
216 "ED7D31",
217 "A5A5A5",
218 "FFC000",
219 "5B9BD5",
220 "70AD47",
221 "264478",
222 "9E480E",
223 ]
224
225 _AXIS_ROLE_DEFAULTS = {
226 "category": ("text", "bottom"),
227 "secondary_category": ("text", "bottom"),
228 "secondary_value": ("value", "right"),
229 "value": ("value", "left"),
230 "x": ("value", "bottom"),
231 "y": ("value", "left"),
232 }
233
234
235 def _chart_axes(
236 payload: dict[str, Any],
237 allowed_roles: set[str],
238 ) -> dict[str, dict[str, Any]]:
239 """Normalize the narrow classic-chart axis contract."""
240 raw_axes = payload.get("axes")
241 if raw_axes is None:
242 return {}
243 if not isinstance(raw_axes, dict):
244 raise RuntimeError("Native PPTX chart axes must be an object")
245
246 unknown_roles = set(raw_axes) - allowed_roles
247 if unknown_roles:
248 roles = ", ".join(sorted(unknown_roles))
249 raise RuntimeError(f"Native PPTX chart axes contains unsupported role(s): {roles}")
250
251 axes: dict[str, dict[str, Any]] = {}
252 for role, raw_config in raw_axes.items():
253 if not isinstance(raw_config, dict):
254 raise RuntimeError(f"Native PPTX chart axes.{role} must be an object")
255 allowed_fields = {
256 "kind", "label_position", "major_gridlines", "major_unit",
257 "maximum", "minimum", "number_format", "position", "reverse",
258 "visible",
259 }
260 unknown_fields = set(raw_config) - allowed_fields
261 if unknown_fields:
262 fields = ", ".join(sorted(unknown_fields))
263 raise RuntimeError(
264 f"Native PPTX chart axes.{role} contains unsupported field(s): {fields}"
265 )
266 default_kind, default_position = _AXIS_ROLE_DEFAULTS[role]
267 kind = _compact_key(raw_config.get("kind") or default_kind)
268 if kind not in {"date", "text", "value"}:
269 raise RuntimeError(
270 f"Native PPTX chart axes.{role}.kind must be date, text, or value"
271 )
272 if role in {"category", "secondary_category"} and kind not in {"date", "text"}:
273 raise RuntimeError(f"Native PPTX chart axes.{role}.kind must be date or text")
274 if role in {"value", "secondary_value", "x", "y"} and kind != "value":
275 raise RuntimeError(f"Native PPTX chart axes.{role}.kind must be value")
276
277 position_aliases = {
278 "b": "bottom",
279 "bottom": "bottom",
280 "l": "left",
281 "left": "left",
282 "r": "right",
283 "right": "right",
284 "t": "top",
285 "top": "top",
286 }
287 position = position_aliases.get(
288 _compact_key(raw_config.get("position") or default_position)
289 )
290 if position is None:
291 raise RuntimeError(
292 f"Native PPTX chart axes.{role}.position must be bottom, left, right, or top"
293 )
294 allowed_positions = (
295 {"bottom", "top"}
296 if role in {"category", "secondary_category", "x"}
297 else {"left", "right"}
298 )
299 if position not in allowed_positions:
300 choices = ", ".join(sorted(allowed_positions))
301 raise RuntimeError(
302 f"Native PPTX chart axes.{role}.position must be one of: {choices}"
303 )
304
305 config: dict[str, Any] = {"kind": kind, "position": position}
306 for field in ("visible", "reverse", "major_gridlines"):
307 value = raw_config.get(field)
308 if value is None:
309 continue
310 if not isinstance(value, bool):
311 raise RuntimeError(
312 f"Native PPTX chart axes.{role}.{field} must be a boolean"
313 )
314 config[field] = value
315
316 raw_label_position = raw_config.get("label_position")
317 if raw_label_position is not None:
318 label_aliases = {
319 "high": "high",
320 "low": "low",
321 "nextto": "next_to",
322 "none": "none",
323 }
324 label_position = label_aliases.get(_compact_key(raw_label_position))
325 if label_position is None:
326 raise RuntimeError(
327 f"Native PPTX chart axes.{role}.label_position must be one of: "
328 "high, low, next_to, none"
329 )
330 config["label_position"] = label_position
331
332 raw_number_format = raw_config.get("number_format")
333 if raw_number_format is not None:
334 if not isinstance(raw_number_format, str):
335 raise RuntimeError(
336 f"Native PPTX chart axes.{role}.number_format must be a string"
337 )
338 if not raw_number_format.strip():
339 raise RuntimeError(
340 f"Native PPTX chart axes.{role}.number_format must be non-empty"
341 )
342 config["number_format"] = raw_number_format
343
344 for field in ("minimum", "maximum", "major_unit"):
345 value = raw_config.get(field)
346 if value is None:
347 continue
348 number = _chart_number(value)
349 if field == "major_unit":
350 if role not in {"value", "secondary_value", "x", "y"}:
351 raise RuntimeError(
352 f"Native PPTX chart axes.{role}.major_unit is unsupported"
353 )
354 if number <= 0:
355 raise RuntimeError(
356 f"Native PPTX chart axes.{role}.major_unit must be positive"
357 )
358 config[field] = number
359 if (
360 config.get("minimum") is not None
361 and config.get("maximum") is not None
362 and config["minimum"] >= config["maximum"]
363 ):
364 raise RuntimeError(
365 f"Native PPTX chart axes.{role}.minimum must be less than maximum"
366 )
367 axes[role] = config
368 return axes
369
370
371 def _category_axis_is_date(axes: dict[str, dict[str, Any]]) -> bool:
372 return axes.get("category", {}).get("kind") == "date"
373
374
375 def _chart_kind(payload: dict[str, Any]) -> tuple[str, str | None, str | None]:
376 raw_type = payload.get("type") or payload.get("chart_type") or "column"
377 key = _compact_key(raw_type)
378 aliases: dict[str, tuple[str, str | None, str | None]] = {
379 "area": ("area", "standard", None),
380 "areastacked": ("area", "stacked", None),
381 "areastacked100": ("area", "percentStacked", None),
382 "area100": ("area", "percentStacked", None),
383 "bar": ("bar", "clustered", None),
384 "barofpie": ("of_pie", None, "bar"),
385 "barclustered": ("bar", "clustered", None),
386 "barstacked": ("bar", "stacked", None),
387 "barstacked100": ("bar", "percentStacked", None),
388 "boxandwhisker": ("box_whisker", None, None),
389 "boxplot": ("box_whisker", None, None),
390 "boxwhisker": ("box_whisker", None, None),
391 "bubble": ("bubble", None, None),
392 "bullet": ("bullet", None, None),
393 "bulletchart": ("bullet", None, None),
394 "combo": ("combo", None, None),
395 "combochart": ("combo", None, None),
396 "choropleth": ("map", None, None),
397 "conebarclustered": ("bar3d", "clustered", "cone"),
398 "conebarstacked": ("bar3d", "stacked", "cone"),
399 "conebarstacked100": ("bar3d", "percentStacked", "cone"),
400 "conecol": ("column3d", "clustered", "cone"),
401 "conecolclustered": ("column3d", "clustered", "cone"),
402 "conecolstacked": ("column3d", "stacked", "cone"),
403 "conecolstacked100": ("column3d", "percentStacked", "cone"),
404 "col": ("column", "clustered", None),
405 "column": ("column", "clustered", None),
406 "columnclustered": ("column", "clustered", None),
407 "columnstacked": ("column", "stacked", None),
408 "columnstacked100": ("column", "percentStacked", None),
409 "contour": ("surface", None, "topView"),
410 "contourwireframe": ("surface", None, "topViewWireframe"),
411 "cylinderbarclustered": ("bar3d", "clustered", "cylinder"),
412 "cylinderbarstacked": ("bar3d", "stacked", "cylinder"),
413 "cylinderbarstacked100": ("bar3d", "percentStacked", "cylinder"),
414 "cylindercol": ("column3d", "clustered", "cylinder"),
415 "cylindercolclustered": ("column3d", "clustered", "cylinder"),
416 "cylindercolstacked": ("column3d", "stacked", "cylinder"),
417 "cylindercolstacked100": ("column3d", "percentStacked", "cylinder"),
418 "doughnut": ("doughnut", None, None),
419 "doughnutexploded": ("doughnut", None, "exploded"),
420 "donut": ("doughnut", None, None),
421 "donutexploded": ("doughnut", None, "exploded"),
422 "filledmap": ("map", None, None),
423 "funnel": ("funnel", None, None),
424 "funnelchart": ("funnel", None, None),
425 "gantt": ("gantt", None, None),
426 "ganttchart": ("gantt", None, None),
427 "geo": ("map", None, None),
428 "geomap": ("map", None, None),
429 "heatmap": ("heatmap", None, None),
430 "heatmapchart": ("heatmap", None, None),
431 "histogram": ("histogram", None, None),
432 "histogramchart": ("histogram", None, None),
433 "line": ("line", "standard", "line"),
434 "linemarkers": ("line", "standard", "lineMarker"),
435 "linemarkersstacked": ("line", "stacked", "lineMarker"),
436 "linemarkersstacked100": ("line", "percentStacked", "lineMarker"),
437 "linestacked": ("line", "stacked", "line"),
438 "linestacked100": ("line", "percentStacked", "line"),
439 "linestackedmarkers": ("line", "stacked", "lineMarker"),
440 "linestackedmarkers100": ("line", "percentStacked", "lineMarker"),
441 "pie": ("pie", None, None),
442 "pieexploded": ("pie", None, "exploded"),
443 "ofpie": ("of_pie", None, "pie"),
444 "pieofpie": ("of_pie", None, "pie"),
445 "pareto": ("pareto", None, None),
446 "paretochart": ("pareto", None, None),
447 "pyramidbarclustered": ("bar3d", "clustered", "pyramid"),
448 "pyramidbarstacked": ("bar3d", "stacked", "pyramid"),
449 "pyramidbarstacked100": ("bar3d", "percentStacked", "pyramid"),
450 "pyramidcol": ("column3d", "clustered", "pyramid"),
451 "pyramidcolclustered": ("column3d", "clustered", "pyramid"),
452 "pyramidcolstacked": ("column3d", "stacked", "pyramid"),
453 "pyramidcolstacked100": ("column3d", "percentStacked", "pyramid"),
454 "radar": ("radar", None, "line"),
455 "radarfilled": ("radar", None, "filled"),
456 "radarmarkers": ("radar", None, "lineMarker"),
457 "scatter": ("scatter", None, "marker"),
458 "stock": ("stock", None, "hlc"),
459 "stockhlc": ("stock", None, "hlc"),
460 "stockohlc": ("stock", None, "ohlc"),
461 "stockvhlc": ("stock", None, "vhlc"),
462 "stockvohlc": ("stock", None, "vohlc"),
463 "surface": ("surface", None, "surface3D"),
464 "surface3d": ("surface", None, "surface3D"),
465 "surfacewireframe": ("surface", None, "surface3DWireframe"),
466 "surfacetopview": ("surface", None, "topView"),
467 "surfacetopviewwireframe": ("surface", None, "topViewWireframe"),
468 "sunburst": ("sunburst", None, None),
469 "sunburstchart": ("sunburst", None, None),
470 "map": ("map", None, None),
471 "mapchart": ("map", None, None),
472 "threedarea": ("area3d", "standard", None),
473 "threedareastacked": ("area3d", "stacked", None),
474 "threedareastacked100": ("area3d", "percentStacked", None),
475 "threedbar": ("bar3d", "clustered", "box"),
476 "threedbarclustered": ("bar3d", "clustered", "box"),
477 "threedbarstacked": ("bar3d", "stacked", "box"),
478 "threedbarstacked100": ("bar3d", "percentStacked", "box"),
479 "threedcolumn": ("column3d", "clustered", "box"),
480 "threedcolumnclustered": ("column3d", "clustered", "box"),
481 "threedcolumnstacked": ("column3d", "stacked", "box"),
482 "threedcolumnstacked100": ("column3d", "percentStacked", "box"),
483 "threedline": ("line3d", "standard", None),
484 "threedpie": ("pie3d", None, None),
485 "threedpieexploded": ("pie3d", None, "exploded"),
486 "treemap": ("treemap", None, None),
487 "treemapchart": ("treemap", None, None),
488 "waterfall": ("waterfall", None, None),
489 "waterfallchart": ("waterfall", None, None),
490 "xy": ("scatter", None, "marker"),
491 "xyscatter": ("scatter", None, "marker"),
492 "xyscatterlines": ("scatter", None, "lineMarker"),
493 "xyscatterlinesnomarkers": ("scatter", None, "line"),
494 "xyscattersmooth": ("scatter", None, "smoothMarker"),
495 "xyscattersmoothnomarkers": ("scatter", None, "smooth"),
496 }
497 if key.startswith("100percentstacked"):
498 key = key.replace("100percentstacked", "", 1) + "stacked100"
499 if key.startswith("percentstacked"):
500 key = key.replace("percentstacked", "", 1) + "stacked100"
501 if key.startswith("3d"):
502 key = "threed" + key[2:]
503 chart_type, grouping, style = aliases.get(key, (key, None, None))
504 if chart_type in _UNSUPPORTED_3D_CHART_TYPES:
505 raise RuntimeError("Native PPTX 3D charts are intentionally unsupported")
506 if chart_type in _DEFERRED_CHART_TYPES:
507 raise RuntimeError(
508 f"Native PPTX {chart_type} chart is outside current basic chart support"
509 )
510
511 supported = sorted(_CATEGORY_CHART_TYPES | _XY_CHART_TYPES | _CHARTEX_CHART_TYPES | {"combo", "stock"})
512 if chart_type not in supported:
513 raise RuntimeError(f"Native PPTX chart type must be one of: {', '.join(supported)}")
514 return chart_type, grouping, style
515
516
517 def _chart_grouping(
518 chart_type: str,
519 payload: dict[str, Any],
520 alias_grouping: str | None,
521 ) -> str | None:
522 grouping = payload.get("grouping") or payload.get("chart_grouping") or alias_grouping
523 if not grouping and payload.get("stacked"):
524 grouping = "stacked"
525 if not grouping:
526 return "clustered" if chart_type in {"bar", "column"} else "standard"
527
528 aliases = {
529 "100": "percentStacked",
530 "100percent": "percentStacked",
531 "100percentstacked": "percentStacked",
532 "clustered": "clustered",
533 "percent": "percentStacked",
534 "percentstacked": "percentStacked",
535 "stacked": "stacked",
536 "standard": "standard",
537 }
538 normalized = aliases.get(_compact_key(grouping))
539 if chart_type in {"bar", "column"}:
540 allowed = {"clustered", "stacked", "percentStacked"}
541 elif chart_type in {"area", "line"}:
542 allowed = {"standard", "stacked", "percentStacked"}
543 else:
544 allowed = {"standard"}
545 if normalized not in allowed:
546 if normalized in {"clustered", "standard"}:
547 allowed_text = ", ".join(sorted(allowed))
548 raise RuntimeError(f"Native PPTX {chart_type} chart grouping must be one of: {allowed_text}")
549 raise RuntimeError(
550 f"Native PPTX {grouping} grouping is outside current basic chart support"
551 )
552 return normalized
553
554
555 def _line_style(payload: dict[str, Any], alias_style: str | None) -> str:
556 raw_style = payload.get("line_style") or payload.get("lineStyle") or alias_style
557 if raw_style is None:
558 raw_style = "lineMarker" if payload.get("markers") else "line"
559 aliases = {
560 "line": "line",
561 "linemarker": "lineMarker",
562 "marker": "lineMarker",
563 "markers": "lineMarker",
564 "none": "line",
565 "nomarker": "line",
566 "nomarkers": "line",
567 }
568 style = aliases.get(_compact_key(raw_style))
569 if not style:
570 raise RuntimeError("Native PPTX line_style must be one of: line, lineMarker")
571 return style
572
573
574 def _radar_style(payload: dict[str, Any], alias_style: str | None) -> tuple[str, str | None]:
575 raw_style = payload.get("radar_style") or payload.get("radarStyle") or alias_style or "line"
576 aliases = {
577 "filled": ("filled", None),
578 "line": ("marker", "none"),
579 "linemarker": ("marker", "circle"),
580 "marker": ("marker", "none"),
581 "markers": ("marker", "circle"),
582 "standard": ("marker", "none"),
583 }
584 style = aliases.get(_compact_key(raw_style))
585 if not style:
586 raise RuntimeError(
587 f"Native PPTX radar_style {raw_style} is outside current basic chart support"
588 )
589 return style
590
591
592 def _category_series(payload: dict[str, Any], categories: list[Any]) -> list[dict[str, Any]]:
593 raw_series = payload.get("series", [])
594 if not categories or not isinstance(raw_series, list) or not raw_series:
595 raise RuntimeError("Native PPTX chart requires non-empty categories and series")
596 root_point_colors = _first_present(
597 payload.get("point_colors"),
598 payload.get("pointColors"),
599 )
600 if root_point_colors is not None and len(raw_series) != 1:
601 raise RuntimeError("Native PPTX chart root point_colors is only valid for one series")
602
603 series: list[dict[str, Any]] = []
604 for idx, item in enumerate(raw_series, start=1):
605 if not isinstance(item, dict):
606 raise RuntimeError("Native PPTX chart series entries must be objects")
607 values = [
608 _chart_number(value)
609 for value in _chart_list(item.get("values", []), "series[].values")
610 ]
611 if len(values) != len(categories):
612 raise RuntimeError("Native PPTX chart series values must match categories length")
613 raw_point_colors = _first_present(
614 item.get("point_colors"),
615 item.get("pointColors"),
616 root_point_colors if idx == 1 else None,
617 )
618 point_colors = [
619 _clean_hex(color, "#4472C4")
620 for color in _chart_list(raw_point_colors, "series[].point_colors")
621 ]
622 if point_colors and len(point_colors) != len(values):
623 raise RuntimeError("Native PPTX chart series point_colors must match values length")
624 series_item = {"name": str(item.get("name") or f"Series {idx}"), "values": values}
625 if point_colors:
626 series_item["point_colors"] = point_colors
627 fill_opacity = _first_present(
628 item.get("fill_opacity"),
629 item.get("fillOpacity"),
630 )
631 if fill_opacity is not None:
632 fill_opacity = _number(fill_opacity, "series fill_opacity")
633 if not 0 <= fill_opacity <= 1:
634 raise RuntimeError(
635 "Native PPTX chart series fill_opacity must be between 0 and 1"
636 )
637 series_item["fill_opacity"] = fill_opacity
638 line_width = _first_present(
639 item.get("line_width"),
640 item.get("lineWidth"),
641 )
642 if line_width is not None:
643 line_width = _number(line_width, "series line_width")
644 if line_width <= 0:
645 raise RuntimeError("Native PPTX chart series line_width must be positive")
646 _powerpoint_line_width_emu(line_width, "series line_width")
647 series_item["line_width"] = line_width
648 series.append(series_item)
649 return series
650
651
652 def _category_chart_data(
653 payload: dict[str, Any],
654 chart_type: str,
655 alias_grouping: str | None,
656 alias_style: str | None,
657 ) -> dict[str, Any]:
658 axes = _chart_axes(payload, {"category", "value"})
659 if axes and chart_type in {"bar", "doughnut", "of_pie", "pie"}:
660 raise RuntimeError(
661 f"Native PPTX {chart_type} chart axes are outside current support"
662 )
663 if _category_axis_is_date(axes) and chart_type != "area":
664 raise RuntimeError(
665 "Native PPTX date category axes are currently supported for area charts only"
666 )
667 raw_categories = _chart_list(payload.get("categories", []), "categories")
668 categories = (
669 [_chart_number(item) for item in raw_categories]
670 if _category_axis_is_date(axes)
671 else [str(item) for item in raw_categories]
672 )
673 style = payload.get("style") if isinstance(payload.get("style"), dict) else {}
674
675 series = _category_series(payload, categories)
676 if chart_type in {"doughnut", "of_pie", "pie"}:
677 if len(series) != 1:
678 raise RuntimeError("Native PPTX pie-family charts support exactly one series")
679
680 of_pie_type = None
681 if chart_type == "of_pie":
682 raw_of_pie_type = (
683 payload.get("of_pie_type")
684 or payload.get("ofPieType")
685 or payload.get("secondary_type")
686 or alias_style
687 or "pie"
688 )
689 of_pie_aliases = {
690 "bar": "bar",
691 "barofpie": "bar",
692 "pie": "pie",
693 "pieofpie": "pie",
694 }
695 of_pie_type = of_pie_aliases.get(_compact_key(raw_of_pie_type))
696 if not of_pie_type:
697 raise RuntimeError("Native PPTX of_pie_type must be one of: bar, pie")
698
699 line_style = _line_style(payload, alias_style) if chart_type == "line" else None
700 radar_style = None
701 radar_marker_style = None
702 if chart_type == "radar":
703 radar_style, radar_marker_style = _radar_style(payload, alias_style)
704
705 if alias_style == "exploded" or payload.get("exploded"):
706 raise RuntimeError("Native PPTX exploded pie/doughnut is outside current basic chart support")
707
708 grouping = (
709 _chart_grouping(chart_type, payload, alias_grouping)
710 if chart_type in {"bar", "column", "line", "area"}
711 else None
712 )
713 return {
714 "kind": "category",
715 "type": chart_type,
716 "categories": categories,
717 "grouping": grouping,
718 "of_pie_type": of_pie_type,
719 "line_style": line_style,
720 "radar_marker_style": radar_marker_style,
721 "radar_style": radar_style,
722 "show_value_axis_labels": _chart_bool(
723 _first_present(
724 payload.get("show_value_axis_labels"),
725 payload.get("showValueAxisLabels"),
726 style.get("show_value_axis_labels"),
727 style.get("showValueAxisLabels"),
728 ),
729 True,
730 ),
731 "data_labels": _chart_data_labels(
732 payload,
733 chart_type,
734 grouping,
735 len(categories),
736 ),
737 "axes": axes,
738 "series": series,
739 }
740
741
742 def _combo_axis_name(plot_payload: dict[str, Any]) -> str:
743 axis = plot_payload.get("axis") or plot_payload.get("value_axis")
744 if axis is None and plot_payload.get("secondary_axis"):
745 axis = "secondary"
746 axis_key = _compact_key(axis or "primary")
747 aliases = {
748 "left": "primary",
749 "primary": "primary",
750 "right": "secondary",
751 "secondary": "secondary",
752 "secondaryaxis": "secondary",
753 }
754 normalized = aliases.get(axis_key)
755 if not normalized:
756 raise RuntimeError("Native PPTX combo plot axis must be primary or secondary")
757 return normalized
758
759
760 def _combo_plot_type(plot_payload: dict[str, Any]) -> tuple[str, str | None, str | None]:
761 chart_type, alias_grouping, alias_style = _chart_kind(plot_payload)
762 if chart_type not in {"area", "column", "line"}:
763 raise RuntimeError("Native PPTX combo plots support column, line, and area only")
764 has_area_fill = bool(_first_present(plot_payload.get("area_fill"), plot_payload.get("areaFill")))
765 if chart_type == "line" and has_area_fill:
766 chart_type = "area"
767 return chart_type, alias_grouping, alias_style
768
769
770 def _plot_series_area_style(plot_payload: dict[str, Any]) -> bool:
771 for item in _chart_list(plot_payload.get("series", []), "series"):
772 if not isinstance(item, dict):
773 continue
774 if _first_present(
775 item.get("fill_opacity"),
776 item.get("fillOpacity"),
777 ) is not None:
778 return True
779 return False
780
781
782 def _combo_series_indices(
783 plot_payload: dict[str, Any],
784 series_count: int,
785 ) -> list[int] | None:
786 raw_indices = plot_payload.get("series_indices")
787 if raw_indices is None:
788 return None
789 indices: list[int] = []
790 for value in _chart_list(raw_indices, "plots[].series_indices"):
791 if isinstance(value, bool) or not isinstance(value, int) or value < 0:
792 raise RuntimeError(
793 "Native PPTX combo series_indices must contain non-negative integers"
794 )
795 indices.append(value)
796 if len(indices) != series_count or len(set(indices)) != len(indices):
797 raise RuntimeError(
798 "Native PPTX combo series_indices must be unique and match series length"
799 )
800 return indices
801
802
803 def _combo_plot_entry(
804 plot_payload: dict[str, Any],
805 categories: list[Any],
806 *,
807 category_is_numeric: bool,
808 axes: dict[str, dict[str, Any]],
809 fallback_series: list[dict[str, Any]] | None = None,
810 ) -> dict[str, Any]:
811 chart_type, alias_grouping, alias_style = _combo_plot_type(plot_payload)
812 if chart_type == "line" and _plot_series_area_style(plot_payload):
813 raise RuntimeError(
814 "Native PPTX combo line plot with series fill_opacity requires area_fill: true"
815 )
816 axis = _combo_axis_name(plot_payload)
817 category_role = "secondary_category" if axis == "secondary" else "category"
818 axis_is_date = axes.get(category_role, {}).get("kind") == "date"
819 raw_numeric = plot_payload.get("category_numeric")
820 if raw_numeric is not None and not isinstance(raw_numeric, bool):
821 raise RuntimeError(
822 "Native PPTX combo plot category_numeric must be a boolean"
823 )
824 if axis_is_date and raw_numeric is False:
825 raise RuntimeError(
826 "Native PPTX combo date-axis categories must remain numeric"
827 )
828 plot_category_is_numeric = axis_is_date or (
829 raw_numeric if raw_numeric is not None else category_is_numeric
830 )
831 raw_plot_categories = plot_payload.get("categories")
832 category_items = (
833 categories
834 if raw_plot_categories is None
835 else _chart_list(raw_plot_categories, "plots[].categories")
836 )
837 plot_categories = (
838 [_chart_number(item) for item in category_items]
839 if plot_category_is_numeric
840 else [str(item) for item in category_items]
841 )
842 if not plot_categories:
843 raise RuntimeError("Native PPTX combo plot categories must be non-empty")
844 plot_series = fallback_series or _category_series(plot_payload, plot_categories)
845 grouping = (
846 _chart_grouping(chart_type, plot_payload, alias_grouping)
847 if chart_type in {"area", "column", "line"}
848 else None
849 )
850 entry: dict[str, Any] = {
851 "axis": axis,
852 "categories": plot_categories,
853 "category_is_numeric": plot_category_is_numeric,
854 "data_labels": _chart_data_labels(
855 plot_payload,
856 chart_type,
857 grouping,
858 len(plot_categories),
859 ),
860 "grouping": grouping,
861 "series": plot_series,
862 "type": chart_type,
863 }
864 series_indices = _combo_series_indices(plot_payload, len(plot_series))
865 if series_indices is not None:
866 entry["series_indices"] = series_indices
867 if chart_type == "line":
868 entry["line_style"] = _line_style(plot_payload, alias_style)
869 return entry
870
871
872 def _combo_chart_data(payload: dict[str, Any]) -> dict[str, Any]:
873 axes = _chart_axes(
874 payload,
875 {"category", "secondary_category", "secondary_value", "value"},
876 )
877 raw_category_numeric = payload.get("category_numeric")
878 if raw_category_numeric is not None and not isinstance(raw_category_numeric, bool):
879 raise RuntimeError("Native PPTX combo category_numeric must be a boolean")
880 primary_axis_is_date = _category_axis_is_date(axes)
881 if primary_axis_is_date and raw_category_numeric is False:
882 raise RuntimeError("Native PPTX combo date-axis categories must remain numeric")
883 category_is_numeric = primary_axis_is_date or raw_category_numeric is True
884 raw_categories = _chart_list(payload.get("categories", []), "categories")
885 categories = (
886 [_chart_number(item) for item in raw_categories]
887 if category_is_numeric
888 else [str(item) for item in raw_categories]
889 )
890 if not categories:
891 raise RuntimeError("Native PPTX combo chart categories must be non-empty")
892 raw_plots = payload.get("plots", payload.get("chart_plots"))
893 plots: list[dict[str, Any]] = []
894
895 if raw_plots is not None:
896 for item in _chart_list(raw_plots, "plots"):
897 if not isinstance(item, dict):
898 raise RuntimeError("Native PPTX combo plots must be objects")
899 plots.append(_combo_plot_entry(
900 item,
901 categories,
902 category_is_numeric=category_is_numeric,
903 axes=axes,
904 ))
905 else:
906 raw_series = _chart_list(payload.get("series", []), "series")
907 if not raw_series:
908 raise RuntimeError("Native PPTX combo chart requires plots or typed series")
909 for idx, item in enumerate(raw_series, start=1):
910 if not isinstance(item, dict):
911 raise RuntimeError("Native PPTX chart series entries must be objects")
912 if not (item.get("type") or item.get("chart_type")):
913 raise RuntimeError("Native PPTX combo series entries require type")
914 if any(
915 field in item
916 for field in ("categories", "category_numeric", "series_indices")
917 ):
918 raise RuntimeError(
919 "Native PPTX combo typed series with plot-scoped metadata "
920 "must use plots"
921 )
922 one_series = _category_series({"series": [item]}, categories)
923 plot = _combo_plot_entry(
924 item,
925 categories,
926 category_is_numeric=category_is_numeric,
927 axes=axes,
928 fallback_series=one_series,
929 )
930 signature = (
931 plot["axis"],
932 plot.get("grouping"),
933 plot.get("line_style"),
934 plot["type"],
935 )
936 previous = plots[-1] if plots else None
937 previous_signature = (
938 previous.get("axis"),
939 previous.get("grouping"),
940 previous.get("line_style"),
941 previous.get("type"),
942 ) if previous else None
943 if (
944 previous is not None
945 and signature == previous_signature
946 and plot.get("data_labels") == previous.get("data_labels")
947 ):
948 previous["series"].extend(plot["series"])
949 else:
950 plots.append(plot)
951
952 if not plots:
953 raise RuntimeError("Native PPTX combo chart requires at least one plot")
954 if not any(plot["axis"] == "primary" for plot in plots):
955 raise RuntimeError("Native PPTX combo chart requires a primary-axis plot")
956 has_secondary_plot = any(plot["axis"] == "secondary" for plot in plots)
957 if not has_secondary_plot and {
958 "secondary_category", "secondary_value",
959 }.intersection(axes):
960 raise RuntimeError(
961 "Native PPTX combo secondary axes require a secondary-axis plot"
962 )
963 series_index_groups = [plot.get("series_indices") for plot in plots]
964 if any(group is not None for group in series_index_groups):
965 if any(group is None for group in series_index_groups):
966 raise RuntimeError(
967 "Native PPTX combo series_indices must cover every plot"
968 )
969 flat_indices = [
970 index
971 for group in series_index_groups
972 for index in group
973 ]
974 if sorted(flat_indices) != list(range(len(flat_indices))):
975 raise RuntimeError(
976 "Native PPTX combo series_indices must form one contiguous range"
977 )
978 flat_series: list[dict[str, Any]] = []
979 independent_categories = any(
980 plot["categories"] != categories
981 or plot["category_is_numeric"] != category_is_numeric
982 for plot in plots
983 )
984 next_column = 1
985 for plot in plots:
986 plot["start_index"] = len(flat_series)
987 if independent_categories:
988 plot["category_column"] = next_column
989 plot["start_column"] = next_column + 1
990 next_column += len(plot["series"]) + 1
991 flat_series.extend(plot["series"])
992 if not flat_series:
993 raise RuntimeError("Native PPTX combo chart requires at least one series")
994
995 return {
996 "axes": axes,
997 "categories": categories,
998 "category_is_numeric": category_is_numeric,
999 "independent_categories": independent_categories,
1000 "kind": "combo",
1001 "plots": plots,
1002 "series": flat_series,
1003 "type": "combo",
1004 }
1005
1006
1007 def _chart_values(payload: dict[str, Any], field_name: str = "values") -> list[int | float]:
1008 raw_values = payload.get(field_name)
1009 if raw_values is None and isinstance(payload.get("series"), list) and payload["series"]:
1010 first_series = payload["series"][0]
1011 if isinstance(first_series, dict):
1012 raw_values = first_series.get("values")
1013 values = [_chart_number(value) for value in _chart_list(raw_values, field_name)]
1014 if not values:
1015 raise RuntimeError(f"Native PPTX chart {field_name} must be a non-empty list")
1016 return values
1017
1018
1019 def _chart_categories(payload: dict[str, Any], count: int | None = None) -> list[str]:
1020 raw_categories = payload.get("categories", payload.get("labels", []))
1021 categories = [str(item) for item in _chart_list(raw_categories, "categories")]
1022 if count is not None:
1023 if not categories:
1024 categories = [f"Category {idx + 1}" for idx in range(count)]
1025 if len(categories) != count:
1026 raise RuntimeError("Native PPTX chart categories length must match values length")
1027 elif not categories:
1028 raise RuntimeError("Native PPTX chart requires non-empty categories")
1029 return categories
1030
1031
1032 def _hierarchy_levels(payload: dict[str, Any], count: int) -> list[list[str]]:
1033 raw_levels = payload.get("levels")
1034 if raw_levels is not None:
1035 levels = [
1036 [str(value) for value in _chart_list(level, "levels[]")]
1037 for level in _chart_list(raw_levels, "levels")
1038 ]
1039 else:
1040 raw_categories = _chart_list(payload.get("categories", []), "categories")
1041 if raw_categories and all(isinstance(item, list) for item in raw_categories):
1042 path_rows = [[str(value) for value in item] for item in raw_categories]
1043 else:
1044 path_rows = [[str(item)] for item in raw_categories]
1045 if len(path_rows) != count:
1046 raise RuntimeError("Native PPTX hierarchical chart categories length must match values length")
1047 max_depth = max((len(row) for row in path_rows), default=0)
1048 levels = [
1049 [row[depth] if depth < len(row) else "" for row in path_rows]
1050 for depth in range(max_depth)
1051 ]
1052
1053 if not levels:
1054 raise RuntimeError("Native PPTX hierarchical charts require levels or path categories")
1055 for level in levels:
1056 if len(level) != count:
1057 raise RuntimeError("Native PPTX hierarchical chart levels must match values length")
1058 return levels
1059
1060
1061 def _treemap_parent_labels(payload: dict[str, Any]) -> str:
1062 raw = payload.get("parent_label_layout", payload.get("parent_labels", "overlapping"))
1063 aliases = {
1064 "banner": "banner",
1065 "none": "none",
1066 "overlapping": "overlapping",
1067 }
1068 layout = aliases.get(_compact_key(raw))
1069 if not layout:
1070 raise RuntimeError(
1071 "Native PPTX treemap parent_label_layout must be one of: banner, none, overlapping"
1072 )
1073 return layout
1074
1075
1076 def _chartex_chart_data(payload: dict[str, Any], chart_type: str) -> dict[str, Any]:
1077 if chart_type in {"sunburst", "treemap"}:
1078 values = _chart_values(payload)
1079 levels = _hierarchy_levels(payload, len(values))
1080 data = {
1081 "kind": "chartex",
1082 "levels": levels,
1083 "type": chart_type,
1084 "values": values,
1085 }
1086 if chart_type == "treemap":
1087 data["parent_labels"] = _treemap_parent_labels(payload)
1088 return data
1089
1090 if chart_type == "histogram":
1091 return {
1092 "kind": "chartex",
1093 "type": chart_type,
1094 "values": _chart_values(payload),
1095 }
1096
1097 if chart_type in {"funnel", "pareto", "waterfall"}:
1098 values = _chart_values(payload)
1099 data = {
1100 "categories": _chart_categories(payload, len(values)),
1101 "kind": "chartex",
1102 "type": chart_type,
1103 "values": values,
1104 }
1105 if chart_type == "waterfall":
1106 raw_subtotals = payload.get(
1107 "subtotals",
1108 payload.get("subtotal_indices", []),
1109 )
1110 subtotals: list[int] = []
1111 seen_subtotals: set[int] = set()
1112 for value in _chart_list(raw_subtotals, "subtotals"):
1113 index = _chart_number(value)
1114 if not isinstance(index, int):
1115 raise RuntimeError("Native PPTX waterfall subtotal indices must be integers")
1116 if index < 0 or index >= len(values):
1117 raise RuntimeError(
1118 "Native PPTX waterfall subtotal index is outside point range"
1119 )
1120 if index in seen_subtotals:
1121 raise RuntimeError(
1122 "Native PPTX waterfall subtotal indices must be unique"
1123 )
1124 seen_subtotals.add(index)
1125 subtotals.append(index)
1126 data["subtotals"] = subtotals
1127 return data
1128
1129 if chart_type == "box_whisker":
1130 raw_series = _chart_list(payload.get("series", []), "series")
1131 if not raw_series:
1132 raise RuntimeError("Native PPTX boxWhisker chart requires non-empty series")
1133 series: list[dict[str, Any]] = []
1134 for idx, item in enumerate(raw_series, start=1):
1135 if not isinstance(item, dict):
1136 raise RuntimeError("Native PPTX chart series entries must be objects")
1137 values = [_chart_number(value) for value in _chart_list(item.get("values", []), "series[].values")]
1138 if not values:
1139 raise RuntimeError("Native PPTX boxWhisker series values must be non-empty")
1140 categories = item.get("categories")
1141 if categories is None:
1142 categories = [str(item.get("name") or f"Series {idx}")] * len(values)
1143 categories_list = [str(value) for value in _chart_list(categories, "series[].categories")]
1144 if len(categories_list) != len(values):
1145 raise RuntimeError("Native PPTX boxWhisker series categories must match values length")
1146 series.append({
1147 "categories": categories_list,
1148 "name": str(item.get("name") or f"Series {idx}"),
1149 "values": values,
1150 })
1151 return {
1152 "kind": "chartex",
1153 "series": series,
1154 "type": chart_type,
1155 }
1156
1157 raise RuntimeError(f"Native PPTX {chart_type} chart is outside current basic chart support")
1158
1159
1160 def _stock_chart_data(payload: dict[str, Any]) -> dict[str, Any]:
1161 if _data_labels_config(payload) is not None:
1162 raise RuntimeError("Native PPTX stock chart data labels are outside current support")
1163 axes = _chart_axes(payload, {"category", "value"})
1164 if "category" in axes and not _category_axis_is_date(axes):
1165 raise RuntimeError("Native PPTX stock chart category axis must be date")
1166 categories = [
1167 _chart_number(item)
1168 for item in _chart_list(payload.get("categories", payload.get("dates", [])), "categories")
1169 ]
1170 if not categories:
1171 raise RuntimeError("Native PPTX stock chart requires non-empty categories or dates")
1172
1173 raw_series = payload.get("series")
1174 if raw_series is None:
1175 field_names = [("open", "Open"), ("high", "High"), ("low", "Low"), ("close", "Close")]
1176 raw_series = [
1177 {"name": default_name, "values": payload.get(field_name, [])}
1178 for field_name, default_name in field_names
1179 ]
1180 series = _category_series({"series": raw_series}, categories)
1181 if len(series) != 4:
1182 raise RuntimeError("Native PPTX stock chart requires exactly four series: open, high, low, close")
1183 return {
1184 "axes": axes,
1185 "categories": categories,
1186 "kind": "category",
1187 "series": series,
1188 "type": "stock",
1189 }
1190
1191
1192 def _point_values(point: Any, *, chart_type: str) -> tuple[Any, Any, Any | None]:
1193 if isinstance(point, dict):
1194 return point.get("x"), point.get("y"), point.get("size", point.get("bubble_size"))
1195 if isinstance(point, (list, tuple)):
1196 if len(point) < 2:
1197 raise RuntimeError("Native PPTX XY chart points require x and y")
1198 size = point[2] if len(point) > 2 else None
1199 return point[0], point[1], size
1200 raise RuntimeError("Native PPTX XY chart points must be objects or arrays")
1201
1202
1203 def _xy_chart_data(
1204 payload: dict[str, Any],
1205 chart_type: str,
1206 alias_style: str | None,
1207 ) -> dict[str, Any]:
1208 if _data_labels_config(payload) is not None:
1209 raise RuntimeError(
1210 f"Native PPTX {chart_type} chart data labels are outside current support"
1211 )
1212 axes = _chart_axes(payload, {"x", "y"})
1213 raw_series = payload.get("series", [])
1214 if not isinstance(raw_series, list) or not raw_series:
1215 raise RuntimeError("Native PPTX XY chart requires non-empty series")
1216
1217 series: list[dict[str, Any]] = []
1218 for idx, item in enumerate(raw_series, start=1):
1219 if not isinstance(item, dict):
1220 raise RuntimeError("Native PPTX chart series entries must be objects")
1221
1222 if item.get("points") is not None:
1223 points = [
1224 _point_values(point, chart_type=chart_type)
1225 for point in _chart_list(item.get("points"), "series[].points")
1226 ]
1227 x_values = [_chart_number(point[0]) for point in points]
1228 y_values = [_chart_number(point[1]) for point in points]
1229 size_values = [_chart_number(point[2]) for point in points if point[2] is not None]
1230 else:
1231 x_raw = _chart_list(item.get("x", item.get("xs", [])), "series[].x")
1232 y_raw = _chart_list(
1233 item.get("y", item.get("ys", item.get("values", []))),
1234 "series[].y",
1235 )
1236 size_raw = _chart_list(
1237 item.get("size", item.get("sizes", item.get("bubble_size", []))),
1238 "series[].size",
1239 )
1240 x_values = [_chart_number(value) for value in x_raw]
1241 y_values = [_chart_number(value) for value in y_raw]
1242 size_values = [_chart_number(value) for value in size_raw]
1243
1244 if not x_values or len(x_values) != len(y_values):
1245 raise RuntimeError("Native PPTX XY chart x/y values must be non-empty and same length")
1246 if chart_type == "bubble" and len(size_values) != len(x_values):
1247 raise RuntimeError("Native PPTX bubble chart requires one size per x/y value")
1248
1249 series.append({
1250 "name": str(item.get("name") or f"Series {idx}"),
1251 "sizes": size_values,
1252 "x": x_values,
1253 "y": y_values,
1254 })
1255
1256 scatter_style = _compact_key(payload.get("scatter_style") or alias_style or "marker")
1257 style_aliases = {
1258 "line": "line",
1259 "linemarker": "lineMarker",
1260 "markers": "marker",
1261 "marker": "marker",
1262 "smooth": "smooth",
1263 "smoothmarker": "smoothMarker",
1264 }
1265 if chart_type == "scatter" and scatter_style not in style_aliases:
1266 raise RuntimeError("Native PPTX scatter_style is unsupported")
1267 return {
1268 "axes": axes,
1269 "kind": "xy",
1270 "type": chart_type,
1271 "scatter_style": style_aliases.get(scatter_style, "marker"),
1272 "series": series,
1273 }
1274
1275
1276 def _chart_data(payload: dict[str, Any]) -> dict[str, Any]:
1277 chart_type, alias_grouping, alias_style = _chart_kind(payload)
1278 if (
1279 chart_type not in _CATEGORY_CHART_TYPES | {"combo", "stock"} | _XY_CHART_TYPES
1280 and _data_labels_config(payload) is not None
1281 ):
1282 raise RuntimeError(
1283 f"Native PPTX {chart_type} chart data labels are outside current support"
1284 )
1285 if chart_type == "combo":
1286 return _combo_chart_data(payload)
1287 if chart_type in _CHARTEX_CHART_TYPES:
1288 return _chartex_chart_data(payload, chart_type)
1289 if chart_type == "stock":
1290 return _stock_chart_data(payload)
1291 if chart_type in _XY_CHART_TYPES:
1292 return _xy_chart_data(payload, chart_type, alias_style)
1293 return _category_chart_data(payload, chart_type, alias_grouping, alias_style)
1294
1295
1296 def validate_chart_payload(payload: dict[str, Any]) -> None:
1297 """Check a native chart payload against the export schema.
1298
1299 Public contract for the pptx_to_svg importer: raises RuntimeError on any
1300 payload the native chart exporter cannot represent.
1301 """
1302 _chart_data(payload)
1303
1304
1305 def validate_data_label_position(
1306 value: Any,
1307 chart_type: str,
1308 grouping: str | None,
1309 ) -> None:
1310 """Check a data-label position against the export schema.
1311
1312 Public contract for the pptx_to_svg importer: raises RuntimeError when the
1313 position is not representable for the given plot.
1314 """
1315 _data_label_position(value, chart_type, grouping)
1316
1316 lines PYTHON