返回 ppt-master
chart_read.py
1 """Read native PowerPoint chart display caches for slide-library analysis.
2
3 The template-fill workflow edits chart data from explicit fill plans. This
4 module only reads the data currently visible in a chart XML part, keeping
5 workbook parsing out of the analyzer.
6 """
7
8 from __future__ import annotations
9
10 from typing import Any
11 from xml.etree import ElementTree as ET
12
13 from .ooxml import NS
14
15
16 def empty_chart_data() -> dict[str, Any]:
17 return {
18 "chart_type": None,
19 "category_count": 0,
20 "series_count": 0,
21 "categories": [],
22 "series": [],
23 }
24
25
26 def _local_name(tag: str) -> str:
27 return tag.rsplit("}", 1)[-1]
28
29
30 def _chart_types_with_series(chart_root: ET.Element) -> list[ET.Element]:
31 plot_area = chart_root.find(".//c:plotArea", NS)
32 if plot_area is None:
33 return []
34 chart_types: list[ET.Element] = []
35 for child in list(plot_area):
36 if _local_name(child.tag).endswith("Chart") and child.findall("c:ser", NS):
37 chart_types.append(child)
38 return chart_types
39
40
41 def _coerce_number(value: str) -> int | float | str:
42 try:
43 number = float(value)
44 except ValueError:
45 return value
46 if number.is_integer():
47 return int(number)
48 return number
49
50
51 def _cache_values(parent: ET.Element | None, *, numeric: bool = False) -> list[Any]:
52 if parent is None:
53 return []
54 cache = parent.find(".//c:strCache", NS)
55 if cache is None:
56 cache = parent.find(".//c:numCache", NS)
57 if cache is None:
58 return []
59 values: list[Any] = []
60 for point in cache.findall("c:pt", NS):
61 value = point.findtext("c:v", default="", namespaces=NS)
62 values.append(_coerce_number(value) if numeric else value)
63 return values
64
65
66 def _series_name(series: ET.Element, fallback: str) -> str:
67 tx = series.find("c:tx", NS)
68 if tx is None:
69 return fallback
70 values = _cache_values(tx)
71 if values:
72 return str(values[0])
73 direct = tx.findtext("c:v", default="", namespaces=NS)
74 return direct or fallback
75
76
77 def read_chart_data(chart_root: ET.Element) -> dict[str, Any]:
78 """Return a compact summary of chart type, categories, series, and values."""
79 chart_types = _chart_types_with_series(chart_root)
80 if not chart_types:
81 return empty_chart_data()
82
83 first_series = chart_types[0].find("c:ser", NS)
84 categories = _cache_values(first_series.find("c:cat", NS)) if first_series is not None else []
85 series_payload: list[dict[str, Any]] = []
86 plot_types: list[str] = []
87 for chart_type in chart_types:
88 plot_name = _local_name(chart_type.tag)
89 plot_types.append(plot_name)
90 for series in chart_type.findall("c:ser", NS):
91 index = len(series_payload) + 1
92 series_categories = _cache_values(series.find("c:cat", NS))
93 if not categories and series_categories:
94 categories = series_categories
95 series_payload.append(
96 {
97 "name": _series_name(series, f"系列{index}"),
98 "values": _cache_values(series.find("c:val", NS), numeric=True),
99 "chart_type": plot_name,
100 }
101 )
102
103 return {
104 "chart_type": plot_types[0] if len(set(plot_types)) == 1 else "comboChart",
105 "plot_types": plot_types,
106 "category_count": len(categories),
107 "series_count": len(series_payload),
108 "categories": categories,
109 "series": series_payload,
110 }
111
111 lines PYTHON