ablechart 0.1.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.
@@ -0,0 +1,267 @@
1
+ """
2
+ 绘图区 (Plot) XML 操作模块
3
+
4
+ 负责创建不同类型的图表绘图区 (<c:barChart>, <c:lineChart> 等)。
5
+ """
6
+
7
+ from lxml import etree
8
+ from typing import Literal
9
+
10
+ from ..oxml_ns import NAMESPACES
11
+ from ..date_axis import format_category_label
12
+
13
+ ChartType = Literal['bar', 'column', 'line', 'area', 'scatter', 'bubble']
14
+ ChartGrouping = Literal['clustered', 'standard', 'stacked', 'percent_stacked']
15
+
16
+
17
+ def create_plot_element(
18
+ plotArea,
19
+ chart_type: ChartType,
20
+ cat_ax_id: int,
21
+ val_ax_id: int,
22
+ order_index: int = 0,
23
+ grouping: ChartGrouping | None = None,
24
+ bar_dir: str = 'col',
25
+ ):
26
+ """
27
+ 创建图表绘图区元素
28
+
29
+ Args:
30
+ plotArea: 父绘图区元素
31
+ chart_type: 图表类型 ('bar', 'line', 'area', 'scatter', 'bubble')
32
+ cat_ax_id: 分类轴 ID
33
+ val_ax_id: 值轴 ID
34
+ order_index: 绘图顺序索引(0=最底层,越大越在上层)
35
+
36
+ Returns:
37
+ 创建的绘图元素 (lxml Element)
38
+
39
+ Raises:
40
+ ValueError: 如果图表类型不支持
41
+
42
+ Notes:
43
+ - 每个绘图元素会自动关联指定的坐标轴
44
+ - 调用方需要自己添加系列 (<c:ser>)
45
+ - order_index 决定图表的堆叠顺序
46
+ """
47
+ chart_type = chart_type.lower()
48
+
49
+ if chart_type in ('bar', 'column'):
50
+ return _create_bar_plot(plotArea, cat_ax_id, val_ax_id, order_index, grouping or 'clustered', bar_dir)
51
+ elif chart_type == 'line':
52
+ return _create_line_plot(plotArea, cat_ax_id, val_ax_id, order_index, grouping or 'standard')
53
+ elif chart_type == 'area':
54
+ return _create_area_plot(plotArea, cat_ax_id, val_ax_id, order_index, grouping or 'standard')
55
+ elif chart_type == 'scatter':
56
+ return _create_scatter_plot(plotArea, cat_ax_id, val_ax_id, order_index)
57
+ elif chart_type == 'bubble':
58
+ return _create_bubble_plot(plotArea, cat_ax_id, val_ax_id, order_index)
59
+ else:
60
+ raise ValueError(f"不支持的图表类型: {chart_type}")
61
+
62
+
63
+ def _normalize_grouping(chart_type: str, grouping: str) -> str:
64
+ mapping = {
65
+ "clustered": "clustered",
66
+ "standard": "standard",
67
+ "stacked": "stacked",
68
+ "percent_stacked": "percentStacked",
69
+ "percentStacked": "percentStacked",
70
+ }
71
+ normalized = mapping.get(grouping, grouping)
72
+
73
+ if chart_type in ("bar", "column"):
74
+ return normalized if normalized in {"clustered", "stacked", "percentStacked"} else "clustered"
75
+ if chart_type == "area":
76
+ return normalized if normalized in {"standard", "stacked", "percentStacked"} else "standard"
77
+ if chart_type == "line":
78
+ return normalized if normalized in {"standard", "stacked", "percentStacked"} else "standard"
79
+ return normalized
80
+
81
+
82
+ def _create_bar_plot(plotArea, cat_ax_id: int, val_ax_id: int, order_index: int, grouping_value: str, bar_dir: str = 'col'):
83
+ """创建柱状图元素"""
84
+ barChart = etree.SubElement(plotArea, f"{{{NAMESPACES['c']}}}barChart")
85
+
86
+ # barDir: 柱状图方向 ('col' = 垂直柱状, 'bar' = 水平条形)
87
+ barDir = etree.SubElement(barChart, f"{{{NAMESPACES['c']}}}barDir")
88
+ barDir.set('val', bar_dir if bar_dir in ('col', 'bar') else 'col')
89
+
90
+ # grouping: 分组方式 ('clustered' = 簇状, 'stacked' = 堆叠)
91
+ grouping = etree.SubElement(barChart, f"{{{NAMESPACES['c']}}}grouping")
92
+ normalized = _normalize_grouping("bar", grouping_value)
93
+ grouping.set('val', normalized)
94
+
95
+ # varyColors: 是否每个系列使用不同颜色
96
+ varyColors = etree.SubElement(barChart, f"{{{NAMESPACES['c']}}}varyColors")
97
+ varyColors.set('val', '0')
98
+
99
+ if normalized in {"stacked", "percentStacked"}:
100
+ overlap = etree.SubElement(barChart, f"{{{NAMESPACES['c']}}}overlap")
101
+ overlap.set('val', '100')
102
+
103
+ # ⭐ 绘图顺序(决定堆叠层次,数字越小越在底层)
104
+ # OOXML 规范建议在 varyColors 之后添加
105
+ # 注意:这里不是 <c:ser> 的 order,而是整个 plot 的渲染顺序
106
+ # 但 PowerPoint 实际使用 XML 元素出现的顺序来决定堆叠
107
+ # 所以这个标签主要是语义化,真正的顺序由 XML 元素在 plotArea 中的位置决定
108
+
109
+ # ⚠️ 注意:不在这里添加轴引用!
110
+ # 轴引用应该在所有系列之后添加,由调用方在添加完系列后调用 add_axis_refs()
111
+
112
+ return barChart
113
+
114
+
115
+ def _create_line_plot(plotArea, cat_ax_id: int, val_ax_id: int, order_index: int, grouping_value: str):
116
+ """创建折线图元素"""
117
+ lineChart = etree.SubElement(plotArea, f"{{{NAMESPACES['c']}}}lineChart")
118
+
119
+ # grouping: 分组方式 ('standard' = 标准)
120
+ grouping = etree.SubElement(lineChart, f"{{{NAMESPACES['c']}}}grouping")
121
+ grouping.set('val', _normalize_grouping("line", grouping_value))
122
+
123
+ # varyColors: 是否每个系列使用不同颜色
124
+ varyColors = etree.SubElement(lineChart, f"{{{NAMESPACES['c']}}}varyColors")
125
+ varyColors.set('val', '0')
126
+
127
+ # ⚠️ 注意:不在这里添加轴引用!
128
+ # 轴引用应该在所有系列之后添加
129
+
130
+ return lineChart
131
+
132
+
133
+ def _create_area_plot(plotArea, cat_ax_id: int, val_ax_id: int, order_index: int, grouping_value: str):
134
+ """创建面积图元素"""
135
+ areaChart = etree.SubElement(plotArea, f"{{{NAMESPACES['c']}}}areaChart")
136
+
137
+ # grouping: 分组方式 ('standard' = 标准)
138
+ grouping = etree.SubElement(areaChart, f"{{{NAMESPACES['c']}}}grouping")
139
+ grouping.set('val', _normalize_grouping("area", grouping_value))
140
+
141
+ # varyColors
142
+ varyColors = etree.SubElement(areaChart, f"{{{NAMESPACES['c']}}}varyColors")
143
+ varyColors.set('val', '0')
144
+
145
+ # ⚠️ 注意:不在这里添加轴引用!
146
+
147
+ return areaChart
148
+
149
+
150
+ def _create_scatter_plot(plotArea, cat_ax_id: int, val_ax_id: int, order_index: int):
151
+ """创建散点图元素"""
152
+ scatterChart = etree.SubElement(plotArea, f"{{{NAMESPACES['c']}}}scatterChart")
153
+
154
+ # scatterStyle: 散点样式 ('lineMarker' = 带线和标记)
155
+ scatterStyle = etree.SubElement(scatterChart, f"{{{NAMESPACES['c']}}}scatterStyle")
156
+ scatterStyle.set('val', 'lineMarker')
157
+
158
+ # varyColors
159
+ varyColors = etree.SubElement(scatterChart, f"{{{NAMESPACES['c']}}}varyColors")
160
+ varyColors.set('val', '0')
161
+
162
+ # ⚠️ 注意:不在这里添加轴引用!
163
+
164
+ return scatterChart
165
+
166
+
167
+ def _create_bubble_plot(plotArea, cat_ax_id: int, val_ax_id: int, order_index: int):
168
+ """创建气泡图元素"""
169
+ bubbleChart = etree.SubElement(plotArea, f"{{{NAMESPACES['c']}}}bubbleChart")
170
+
171
+ varyColors = etree.SubElement(bubbleChart, f"{{{NAMESPACES['c']}}}varyColors")
172
+ varyColors.set('val', '0')
173
+
174
+ dLbls = etree.SubElement(bubbleChart, f"{{{NAMESPACES['c']}}}dLbls")
175
+ for tag_name in ("showLegendKey", "showVal", "showCatName", "showSerName", "showPercent", "showBubbleSize"):
176
+ label_elem = etree.SubElement(dLbls, f"{{{NAMESPACES['c']}}}{tag_name}")
177
+ label_elem.set('val', '0')
178
+
179
+ bubbleScale = etree.SubElement(bubbleChart, f"{{{NAMESPACES['c']}}}bubbleScale")
180
+ bubbleScale.set('val', '100')
181
+
182
+ showNegBubbles = etree.SubElement(bubbleChart, f"{{{NAMESPACES['c']}}}showNegBubbles")
183
+ showNegBubbles.set('val', '0')
184
+
185
+ return bubbleChart
186
+
187
+
188
+ def add_axis_refs(plot_element, cat_ax_id: int, val_ax_id: int):
189
+ """
190
+ 为绘图元素添加坐标轴引用(应该在所有系列之后调用)
191
+
192
+ Args:
193
+ plot_element: 绘图元素 (<c:barChart>, <c:lineChart> 等)
194
+ cat_ax_id: 分类轴 ID
195
+ val_ax_id: 值轴 ID
196
+ """
197
+ axId1 = etree.SubElement(plot_element, f"{{{NAMESPACES['c']}}}axId")
198
+ axId1.set('val', str(cat_ax_id))
199
+
200
+ axId2 = etree.SubElement(plot_element, f"{{{NAMESPACES['c']}}}axId")
201
+ axId2.set('val', str(val_ax_id))
202
+
203
+
204
+ def add_plot_categories(plot_element, categories: list):
205
+ """
206
+ 为绘图元素添加共享的分类数据(在所有系列之前调用)
207
+
208
+ Args:
209
+ plot_element: 绘图元素 (<c:barChart>, <c:lineChart> 等)
210
+ categories: 分类列表(X轴数据)
211
+
212
+ Notes:
213
+ - 在 OOXML 规范中,<c:cat> 是图表级别的共享元素
214
+ - 应该在添加任何 <c:ser> 系列之前调用
215
+ - 所有系列共享同一组分类数据
216
+ - ⭐ 自动检测日期类型,使用 numCache(数值缓存)而非 strCache
217
+ """
218
+ from datetime import datetime
219
+
220
+ # ⭐ 检测是否为日期类型
221
+ is_date_data = False
222
+ if categories and isinstance(categories[0], (datetime, float)):
223
+ # datetime 对象或浮点数(Excel 日期序列号)
224
+ is_date_data = True
225
+
226
+ cat = etree.SubElement(plot_element, f"{{{NAMESPACES['c']}}}cat")
227
+
228
+ if is_date_data:
229
+ # ⭐ 新方案:将日期格式化为字符串,使用 strRef + strCache
230
+ # 这样 PowerPoint 就会将其作为文本标签显示,不会出现 1900 年问题
231
+ strRef = etree.SubElement(cat, f"{{{NAMESPACES['c']}}}strRef")
232
+
233
+ # f (公式引用)
234
+ f_elem = etree.SubElement(strRef, f"{{{NAMESPACES['c']}}}f")
235
+ f_elem.text = f"Sheet1!$A$2:$A${len(categories) + 1}"
236
+
237
+ # strCache (字符串缓存)
238
+ strCache = etree.SubElement(strRef, f"{{{NAMESPACES['c']}}}strCache")
239
+ ptCount = etree.SubElement(strCache, f"{{{NAMESPACES['c']}}}ptCount")
240
+ ptCount.set('val', str(len(categories)))
241
+
242
+ # 添加每个分类点(格式化为字符串)
243
+ for i, cat_value in enumerate(categories):
244
+ pt = etree.SubElement(strCache, f"{{{NAMESPACES['c']}}}pt")
245
+ pt.set('idx', str(i))
246
+ v = etree.SubElement(pt, f"{{{NAMESPACES['c']}}}v")
247
+
248
+ v.text = format_category_label(cat_value, "yyyy/mm")
249
+ else:
250
+ # ⭐ 使用 strRef + strCache(普通分类轴)
251
+ strRef = etree.SubElement(cat, f"{{{NAMESPACES['c']}}}strRef")
252
+
253
+ # f (公式引用)
254
+ f_elem = etree.SubElement(strRef, f"{{{NAMESPACES['c']}}}f")
255
+ f_elem.text = f"Sheet1!$A$2:$A${len(categories) + 1}"
256
+
257
+ # strCache (字符串缓存)
258
+ strCache = etree.SubElement(strRef, f"{{{NAMESPACES['c']}}}strCache")
259
+ ptCount = etree.SubElement(strCache, f"{{{NAMESPACES['c']}}}ptCount")
260
+ ptCount.set('val', str(len(categories)))
261
+
262
+ # 添加每个分类点
263
+ for i, cat_value in enumerate(categories):
264
+ pt = etree.SubElement(strCache, f"{{{NAMESPACES['c']}}}pt")
265
+ pt.set('idx', str(i))
266
+ v = etree.SubElement(pt, f"{{{NAMESPACES['c']}}}v")
267
+ v.text = format_category_label(cat_value)