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.
- ablechart/__init__.py +351 -0
- ablechart/_log.py +34 -0
- ablechart/annotations.py +614 -0
- ablechart/api.py +518 -0
- ablechart/builder.py +370 -0
- ablechart/cleaner.py +144 -0
- ablechart/date_axis.py +673 -0
- ablechart/examples.py +113 -0
- ablechart/inspect.py +230 -0
- ablechart/layout.py +535 -0
- ablechart/metadata.py +198 -0
- ablechart/oxml/__init__.py +22 -0
- ablechart/oxml/axes.py +219 -0
- ablechart/oxml/plots.py +267 -0
- ablechart/oxml/series.py +509 -0
- ablechart/oxml_ns.py +8 -0
- ablechart/parser.py +1007 -0
- ablechart/plot_area.py +119 -0
- ablechart/polish.py +560 -0
- ablechart/presets.py +519 -0
- ablechart/range_chart.py +180 -0
- ablechart/range_snapshot.py +1174 -0
- ablechart/replace.py +321 -0
- ablechart/scatter.py +301 -0
- ablechart/schema.py +205 -0
- ablechart/semantic_anchor.py +125 -0
- ablechart/semantic_family.py +2239 -0
- ablechart/spec.py +1375 -0
- ablechart/styles.py +298 -0
- ablechart/tokens.py +163 -0
- ablechart/waterfall.py +750 -0
- ablechart-0.1.0.dist-info/METADATA +279 -0
- ablechart-0.1.0.dist-info/RECORD +36 -0
- ablechart-0.1.0.dist-info/WHEEL +5 -0
- ablechart-0.1.0.dist-info/licenses/LICENSE +21 -0
- ablechart-0.1.0.dist-info/top_level.txt +1 -0
ablechart/oxml/plots.py
ADDED
|
@@ -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)
|