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/builder.py
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"""
|
|
2
|
+
图表构建器 - 有状态的编排器
|
|
3
|
+
|
|
4
|
+
这是重构后的核心,使用有状态的构建器模式来编排 oxml 层的调用。
|
|
5
|
+
解决 P(n,2) 组合爆炸问题。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import List, Dict, Optional
|
|
9
|
+
from pptx.chart.chart import Chart
|
|
10
|
+
import pandas as pd
|
|
11
|
+
from collections import defaultdict
|
|
12
|
+
|
|
13
|
+
from ._log import debug_print as print
|
|
14
|
+
from .oxml import (
|
|
15
|
+
extract_axis_ids,
|
|
16
|
+
create_value_axis,
|
|
17
|
+
optimize_axis_labels,
|
|
18
|
+
optimize_category_axis,
|
|
19
|
+
create_plot_element,
|
|
20
|
+
add_axis_refs,
|
|
21
|
+
add_plot_categories,
|
|
22
|
+
add_series_to_plot,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# 导入样式模块
|
|
26
|
+
try:
|
|
27
|
+
from .styles import DEFAULT_STYLE_CONFIG
|
|
28
|
+
except ImportError:
|
|
29
|
+
# 如果样式模块不存在,设置为 None
|
|
30
|
+
DEFAULT_STYLE_CONFIG = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ChartBuilder:
|
|
34
|
+
"""
|
|
35
|
+
有状态的图表构建器
|
|
36
|
+
|
|
37
|
+
职责:
|
|
38
|
+
1. 管理坐标轴(主轴/次轴)
|
|
39
|
+
2. 按 (type, axis) 分组系列
|
|
40
|
+
3. 委托 oxml 层创建 XML 元素
|
|
41
|
+
|
|
42
|
+
优势:
|
|
43
|
+
- 解决 P(n,2) 组合问题:不需要为每种组合写代码
|
|
44
|
+
- 只需为每种图表类型实现一次 XML 生成
|
|
45
|
+
- 主函数动态组合它们
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, chart: Chart, df: pd.DataFrame, categories_col: str, style_config=None, layout_config=None, polish: bool = True, date_format: str = None, orientation: str = 'vertical'):
|
|
49
|
+
"""
|
|
50
|
+
初始化构建器
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
chart: python-pptx 创建的基础图表(用于激活 XML 结构)
|
|
54
|
+
df: 数据源 DataFrame
|
|
55
|
+
categories_col: 分类列名
|
|
56
|
+
style_config: 样式配置对象(可选,默认使用 DEFAULT_STYLE_CONFIG)
|
|
57
|
+
layout_config: 布局配置对象(可选,包含图例、轴配置)
|
|
58
|
+
"""
|
|
59
|
+
self.chart = chart
|
|
60
|
+
self.df = df
|
|
61
|
+
self.categories_col = categories_col
|
|
62
|
+
self.style_config = style_config if style_config is not None else DEFAULT_STYLE_CONFIG
|
|
63
|
+
self.layout_config = layout_config # 布局配置
|
|
64
|
+
self.polish = polish # 报告级收尾处理
|
|
65
|
+
self.date_format = date_format # 日期分类的 strftime 格式(如 '%Y/%m')
|
|
66
|
+
self.orientation = orientation # 'vertical' | 'horizontal'(条形图)
|
|
67
|
+
|
|
68
|
+
# 访问 XML 结构
|
|
69
|
+
self.chartSpace = chart._chartSpace
|
|
70
|
+
self.plotArea = self.chartSpace.plotArea
|
|
71
|
+
|
|
72
|
+
# 提取现有坐标轴 ID
|
|
73
|
+
self.cat_ax_id, self.pri_val_ax_id = extract_axis_ids(self.plotArea)
|
|
74
|
+
self.sec_val_ax_id = None
|
|
75
|
+
|
|
76
|
+
# 系列计数器(用于 Excel 列索引)
|
|
77
|
+
self._series_counter = 0
|
|
78
|
+
|
|
79
|
+
print(f"\n📊 ChartBuilder 初始化完成")
|
|
80
|
+
print(f" - 分类轴 ID: {self.cat_ax_id}")
|
|
81
|
+
print(f" - 主值轴 ID: {self.pri_val_ax_id}")
|
|
82
|
+
if self.style_config is not None:
|
|
83
|
+
print(f" - 样式配置: 已启用")
|
|
84
|
+
else:
|
|
85
|
+
print(f" - 样式配置: 默认")
|
|
86
|
+
if self.layout_config is not None:
|
|
87
|
+
print(f" - 布局配置: 已启用")
|
|
88
|
+
|
|
89
|
+
def ensure_secondary_axis(self) -> int:
|
|
90
|
+
"""
|
|
91
|
+
确保次值轴存在(只创建一次)
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
次值轴 ID
|
|
95
|
+
"""
|
|
96
|
+
if self.sec_val_ax_id is None:
|
|
97
|
+
# 生成新的轴 ID
|
|
98
|
+
new_ax_id = max(self.cat_ax_id, self.pri_val_ax_id) + 1000
|
|
99
|
+
|
|
100
|
+
# ⭐ 创建次值轴(右侧,标签在右边,crosses='max')
|
|
101
|
+
self.sec_val_ax_id = create_value_axis(
|
|
102
|
+
self.plotArea,
|
|
103
|
+
ax_id=new_ax_id,
|
|
104
|
+
cross_ax_id=self.cat_ax_id,
|
|
105
|
+
position='r',
|
|
106
|
+
tick_label_position='high',
|
|
107
|
+
crosses_at='max' # 右轴线在图表右边
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# ⭐ 优化主值轴(左侧,标签在左边,crosses='min',移除网格线)
|
|
111
|
+
optimize_axis_labels(
|
|
112
|
+
self.plotArea,
|
|
113
|
+
self.pri_val_ax_id,
|
|
114
|
+
tick_label_position='low',
|
|
115
|
+
crosses_at='min', # 左轴线在图表左边
|
|
116
|
+
remove_gridlines=True # 移除内部横框
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# ⭐ 优化分类轴(移除日期间的小竖线)
|
|
120
|
+
optimize_category_axis(
|
|
121
|
+
self.plotArea,
|
|
122
|
+
self.cat_ax_id,
|
|
123
|
+
remove_tick_marks=True # 移除底部日期间的小竖线
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
print(f"\n⭐ 次值轴已创建")
|
|
127
|
+
print(f" - 次值轴 ID: {self.sec_val_ax_id}")
|
|
128
|
+
print(f" - 位置: 右侧 (position='r', crosses='max')")
|
|
129
|
+
print(f" - 标签: 右侧 (tickLblPos='high')")
|
|
130
|
+
print(f" - 主值轴已优化: 左侧 (tickLblPos='low', crosses='min')")
|
|
131
|
+
|
|
132
|
+
return self.sec_val_ax_id
|
|
133
|
+
|
|
134
|
+
def clear_bootstrap_chart(self):
|
|
135
|
+
"""
|
|
136
|
+
清理引导时创建的图表元素
|
|
137
|
+
|
|
138
|
+
Note:
|
|
139
|
+
使用 python-pptx 创建基础图表时,会自动创建一个图表元素。
|
|
140
|
+
我们需要清理它,以便完全通过 XML 自定义。
|
|
141
|
+
"""
|
|
142
|
+
# 查找并删除 python-pptx 自动创建的图表元素
|
|
143
|
+
# 这些通常是 <c:barChart>, <c:lineChart> 等
|
|
144
|
+
|
|
145
|
+
# 获取所有可能的图表类型元素
|
|
146
|
+
chart_types = ['barChart', 'lineChart', 'areaChart', 'scatterChart', 'bubbleChart', 'pieChart']
|
|
147
|
+
|
|
148
|
+
for chart_type in chart_types:
|
|
149
|
+
# 查找所有该类型的图表元素
|
|
150
|
+
elements = self.plotArea.xpath(f'./c:{chart_type}')
|
|
151
|
+
for elem in elements:
|
|
152
|
+
# 删除这个元素
|
|
153
|
+
self.plotArea.remove(elem)
|
|
154
|
+
print(f" → 清理引导图表元素: <c:{chart_type}>")
|
|
155
|
+
|
|
156
|
+
print(f" → 引导图表已清理")
|
|
157
|
+
|
|
158
|
+
def add_plot(self, series_group: List[Dict], plot_order_index: int = 0):
|
|
159
|
+
"""
|
|
160
|
+
添加一组系列(同类型、同轴)
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
series_group: 系列配置列表
|
|
164
|
+
[
|
|
165
|
+
{"key": "col1", "name": "系列1", "type": "bar", "axis": "primary"},
|
|
166
|
+
{"key": "col2", "name": "系列2", "type": "bar", "axis": "primary"}
|
|
167
|
+
]
|
|
168
|
+
plot_order_index: 绘图顺序索引(0=最底层,1=上一层,依此类推)
|
|
169
|
+
|
|
170
|
+
Notes:
|
|
171
|
+
- 组内所有系列必须有相同的 type 和 axis
|
|
172
|
+
- 会创建一个新的绘图元素 (<c:barChart>, <c:lineChart> 等)
|
|
173
|
+
- 并为每个系列添加 <c:ser> 元素
|
|
174
|
+
"""
|
|
175
|
+
if not series_group:
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
# 从第一个系列获取共享属性
|
|
179
|
+
first_cfg = series_group[0]
|
|
180
|
+
chart_type = first_cfg.get("type", "bar")
|
|
181
|
+
axis_type = first_cfg.get("axis", "primary")
|
|
182
|
+
grouping = first_cfg.get("grouping")
|
|
183
|
+
|
|
184
|
+
# 决定使用哪个值轴
|
|
185
|
+
if axis_type == 'primary':
|
|
186
|
+
val_ax_id = self.pri_val_ax_id
|
|
187
|
+
elif axis_type == 'secondary':
|
|
188
|
+
val_ax_id = self.ensure_secondary_axis()
|
|
189
|
+
else:
|
|
190
|
+
raise ValueError(f"未知的轴类型: {axis_type}")
|
|
191
|
+
|
|
192
|
+
print(f"\n➕ 添加绘图组: type={chart_type}, axis={axis_type}, order={plot_order_index}")
|
|
193
|
+
print(f" - 使用值轴 ID: {val_ax_id}")
|
|
194
|
+
|
|
195
|
+
# 创建绘图元素(不包含轴引用)
|
|
196
|
+
plot_element = create_plot_element(
|
|
197
|
+
self.plotArea,
|
|
198
|
+
chart_type,
|
|
199
|
+
self.cat_ax_id,
|
|
200
|
+
val_ax_id,
|
|
201
|
+
order_index=plot_order_index, # ⭐ 传递 order_index
|
|
202
|
+
grouping=grouping,
|
|
203
|
+
bar_dir='bar' if self.orientation == 'horizontal' else 'col',
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# ⭐ 新方案:不在 plot 级别添加共享分类数据
|
|
207
|
+
# 每个系列有自己的 <c:cat> 元素,避免重复
|
|
208
|
+
# categories = self.df[self.categories_col].tolist()
|
|
209
|
+
# add_plot_categories(plot_element, categories)
|
|
210
|
+
# print(f" - 添加共享分类数据({len(categories)} 个分类)")
|
|
211
|
+
|
|
212
|
+
# 为每个系列添加 <c:ser>
|
|
213
|
+
for series_cfg in series_group:
|
|
214
|
+
add_series_to_plot(
|
|
215
|
+
plot_element,
|
|
216
|
+
chart_type,
|
|
217
|
+
series_cfg,
|
|
218
|
+
self._series_counter,
|
|
219
|
+
self.df,
|
|
220
|
+
self.categories_col,
|
|
221
|
+
self.style_config, # ⭐ 传递样式配置
|
|
222
|
+
date_format=self.date_format,
|
|
223
|
+
)
|
|
224
|
+
print(f" - 添加系列: '{series_cfg['name']}' (索引 {self._series_counter})")
|
|
225
|
+
self._series_counter += 1
|
|
226
|
+
|
|
227
|
+
# ⭐ 关键修复:在所有系列添加完成后,再添加轴引用
|
|
228
|
+
# 确保 XML 元素顺序正确:<c:cat> <c:ser> ... <c:ser> <c:axId> <c:axId>
|
|
229
|
+
add_axis_refs(plot_element, self.cat_ax_id, val_ax_id)
|
|
230
|
+
print(f" - 添加轴引用: cat_ax={self.cat_ax_id}, val_ax={val_ax_id}")
|
|
231
|
+
|
|
232
|
+
def build(self, series_config: List[Dict]):
|
|
233
|
+
"""
|
|
234
|
+
构建完整的组合图
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
series_config: 系列配置列表
|
|
238
|
+
[
|
|
239
|
+
{"key": "col1", "name": "系列1", "type": "bar", "axis": "primary"},
|
|
240
|
+
{"key": "col2", "name": "系列2", "type": "line", "axis": "secondary"},
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
构建后的 Chart 对象
|
|
245
|
+
"""
|
|
246
|
+
print("\n" + "=" * 80)
|
|
247
|
+
print("🔨 开始构建组合图")
|
|
248
|
+
print("=" * 80)
|
|
249
|
+
|
|
250
|
+
# 1. 按 (type, axis) 分组
|
|
251
|
+
plot_groups = self._group_series(series_config)
|
|
252
|
+
|
|
253
|
+
print(f"\n📦 系列分组结果:")
|
|
254
|
+
for key, group in plot_groups.items():
|
|
255
|
+
print(f" - {key}: {len(group)} 个系列")
|
|
256
|
+
|
|
257
|
+
# 2. 清理引导图表(可选)
|
|
258
|
+
self.clear_bootstrap_chart()
|
|
259
|
+
|
|
260
|
+
# 3. ⭐ 按堆叠顺序添加绘图组
|
|
261
|
+
# 规则:先添加的绘图组在底层,后添加的在上层
|
|
262
|
+
# 策略:先画"背景"(柱状图/面积图),再画"前景"(折线图/散点图)
|
|
263
|
+
plot_order_counter = 0
|
|
264
|
+
|
|
265
|
+
# 3.1 先添加所有"背景"图表 (bar, area, bubble)
|
|
266
|
+
for (plot_type, axis_type, grouping), series_group in plot_groups.items():
|
|
267
|
+
if plot_type in ('bar', 'column', 'area', 'bubble'):
|
|
268
|
+
self.add_plot(series_group, plot_order_counter)
|
|
269
|
+
plot_order_counter += 1
|
|
270
|
+
|
|
271
|
+
# 3.2 再添加所有"前景"图表 (line, scatter)
|
|
272
|
+
for (plot_type, axis_type, grouping), series_group in plot_groups.items():
|
|
273
|
+
if plot_type in ('line', 'scatter'):
|
|
274
|
+
self.add_plot(series_group, plot_order_counter)
|
|
275
|
+
plot_order_counter += 1
|
|
276
|
+
|
|
277
|
+
# 4. ⭐ 应用布局配置(图例、轴格式等)
|
|
278
|
+
if self.layout_config is not None:
|
|
279
|
+
self.layout_config.apply_to_chart(self.chart)
|
|
280
|
+
|
|
281
|
+
# ⭐ 单系列时渲染器会在 autoTitleDeleted≠1 时自动生成"系列名"大标题
|
|
282
|
+
# → 未显式配置标题则无条件关闭(写入 <c:autoTitleDeleted val="1"/>)
|
|
283
|
+
if self.layout_config is None or not getattr(self.layout_config, "title", None):
|
|
284
|
+
self.chart.has_title = False
|
|
285
|
+
|
|
286
|
+
# 5. ⭐ ChartJunkCleaner: 自动清洗默认 PPT 样式
|
|
287
|
+
try:
|
|
288
|
+
from .cleaner import clean_chart
|
|
289
|
+
clean_chart(self.chart)
|
|
290
|
+
print(f" → ChartJunkCleaner applied")
|
|
291
|
+
except Exception as e:
|
|
292
|
+
print(f" → ChartJunkCleaner skipped: {e}")
|
|
293
|
+
|
|
294
|
+
# 6. ⭐ 报告级收尾:智能轴范围、双轴对齐、柱宽、排版降噪
|
|
295
|
+
if self.polish:
|
|
296
|
+
try:
|
|
297
|
+
from .polish import polish_combo_chart
|
|
298
|
+
skip_primary = (
|
|
299
|
+
self.layout_config is not None
|
|
300
|
+
and self.layout_config.value_axis_config is not None
|
|
301
|
+
and (self.layout_config.value_axis_config.min_value is not None
|
|
302
|
+
or self.layout_config.value_axis_config.max_value is not None)
|
|
303
|
+
)
|
|
304
|
+
skip_secondary = (
|
|
305
|
+
self.layout_config is not None
|
|
306
|
+
and self.layout_config.secondary_value_axis_config is not None
|
|
307
|
+
and (self.layout_config.secondary_value_axis_config.min_value is not None
|
|
308
|
+
or self.layout_config.secondary_value_axis_config.max_value is not None)
|
|
309
|
+
)
|
|
310
|
+
def _text_pref(cfg):
|
|
311
|
+
if cfg is None or not getattr(cfg, "font_name", None):
|
|
312
|
+
return None
|
|
313
|
+
return (cfg.font_name, cfg.font_size_pt or 9)
|
|
314
|
+
|
|
315
|
+
lc = self.layout_config
|
|
316
|
+
polish_combo_chart(
|
|
317
|
+
self.chart,
|
|
318
|
+
self.df,
|
|
319
|
+
self.categories_col,
|
|
320
|
+
series_config,
|
|
321
|
+
skip_primary_scale=skip_primary,
|
|
322
|
+
skip_secondary_scale=skip_secondary,
|
|
323
|
+
cat_text=_text_pref(lc.category_axis_config) if lc else None,
|
|
324
|
+
val_text=_text_pref(lc.value_axis_config) if lc else None,
|
|
325
|
+
sec_text=_text_pref(lc.secondary_value_axis_config) if lc else None,
|
|
326
|
+
)
|
|
327
|
+
print(f" → polish applied")
|
|
328
|
+
except Exception as e:
|
|
329
|
+
print(f" → polish skipped: {e}")
|
|
330
|
+
|
|
331
|
+
print("\n" + "=" * 80)
|
|
332
|
+
print("✅ 组合图构建完成!")
|
|
333
|
+
print("=" * 80)
|
|
334
|
+
|
|
335
|
+
return self.chart
|
|
336
|
+
|
|
337
|
+
@staticmethod
|
|
338
|
+
def _group_series(series_config: List[Dict]) -> Dict[tuple, List[Dict]]:
|
|
339
|
+
"""
|
|
340
|
+
按 (type, axis) 分组系列
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
series_config: 系列配置列表
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
分组后的字典 {(type, axis): [series_cfg, ...]}
|
|
347
|
+
|
|
348
|
+
Examples:
|
|
349
|
+
输入:
|
|
350
|
+
[
|
|
351
|
+
{"key": "s1", "type": "bar", "axis": "primary"},
|
|
352
|
+
{"key": "s2", "type": "bar", "axis": "primary"},
|
|
353
|
+
{"key": "s3", "type": "line", "axis": "secondary"},
|
|
354
|
+
]
|
|
355
|
+
|
|
356
|
+
输出:
|
|
357
|
+
{
|
|
358
|
+
("bar", "primary"): [{"key": "s1", ...}, {"key": "s2", ...}],
|
|
359
|
+
("line", "secondary"): [{"key": "s3", ...}]
|
|
360
|
+
}
|
|
361
|
+
"""
|
|
362
|
+
groups = defaultdict(list)
|
|
363
|
+
for cfg in series_config:
|
|
364
|
+
key = (
|
|
365
|
+
cfg.get("type", "bar"),
|
|
366
|
+
cfg.get("axis", "primary"),
|
|
367
|
+
cfg.get("grouping"),
|
|
368
|
+
)
|
|
369
|
+
groups[key].append(cfg)
|
|
370
|
+
return dict(groups)
|
ablechart/cleaner.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""ChartJunkCleaner — 自动清洗图表,去除默认 PPT 味
|
|
2
|
+
|
|
3
|
+
在 ChartBuilder.build() 完成后自动执行:
|
|
4
|
+
- 移除图表外边框
|
|
5
|
+
- Y 轴网格线 -> 极浅虚线或隐藏
|
|
6
|
+
- 坐标轴刻度线 -> inside 或 none
|
|
7
|
+
- 图例框边框 -> 移除
|
|
8
|
+
- 默认线宽 -> 1.5pt
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from lxml import etree
|
|
12
|
+
|
|
13
|
+
from .oxml_ns import NAMESPACES
|
|
14
|
+
from .tokens import get_chart_token # 颜色真源
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ChartJunkCleaner:
|
|
18
|
+
"""清洗图表默认样式,向 JP 标准靠拢"""
|
|
19
|
+
|
|
20
|
+
# 网格线色取自 tokens.CHART_TOKENS["gridline"](渲染时读取)
|
|
21
|
+
GRID_WIDTH = 6350 # 0.5pt in EMUs
|
|
22
|
+
LINE_WIDTH = 19050 # 1.5pt in EMUs
|
|
23
|
+
|
|
24
|
+
def __init__(self, chart):
|
|
25
|
+
self.chart = chart
|
|
26
|
+
self.chartSpace = chart._chartSpace
|
|
27
|
+
self.plotArea = self.chartSpace.plotArea
|
|
28
|
+
|
|
29
|
+
def clean(self):
|
|
30
|
+
"""执行全部清洗步骤"""
|
|
31
|
+
self._remove_chart_border()
|
|
32
|
+
self._clean_gridlines()
|
|
33
|
+
self._clean_tick_marks()
|
|
34
|
+
self._clean_legend_border()
|
|
35
|
+
self._clean_plot_area_border()
|
|
36
|
+
return self.chart
|
|
37
|
+
|
|
38
|
+
def _remove_chart_border(self):
|
|
39
|
+
"""移除图表外边框"""
|
|
40
|
+
spPr = self.chartSpace.find('c:spPr', namespaces=NAMESPACES)
|
|
41
|
+
if spPr is None:
|
|
42
|
+
spPr = etree.SubElement(self.chartSpace, f"{{{NAMESPACES['c']}}}spPr")
|
|
43
|
+
|
|
44
|
+
# 移除线条
|
|
45
|
+
ln = spPr.find('a:ln', namespaces=NAMESPACES)
|
|
46
|
+
if ln is not None:
|
|
47
|
+
spPr.remove(ln)
|
|
48
|
+
ln = etree.SubElement(spPr, f"{{{NAMESPACES['a']}}}ln")
|
|
49
|
+
etree.SubElement(ln, f"{{{NAMESPACES['a']}}}noFill")
|
|
50
|
+
|
|
51
|
+
def _clean_gridlines(self):
|
|
52
|
+
"""将 Y 轴网格线设为极浅虚线或隐藏"""
|
|
53
|
+
# 处理所有值轴的主要网格线
|
|
54
|
+
for axis_tag in ['valAx', 'catAx']:
|
|
55
|
+
for axis in self.plotArea.findall(f'c:{axis_tag}', namespaces=NAMESPACES):
|
|
56
|
+
major_gl = axis.find('c:majorGridlines', namespaces=NAMESPACES)
|
|
57
|
+
if major_gl is not None:
|
|
58
|
+
self._set_light_gridline(major_gl)
|
|
59
|
+
|
|
60
|
+
# 移除次要网格线
|
|
61
|
+
minor_gl = axis.find('c:minorGridlines', namespaces=NAMESPACES)
|
|
62
|
+
if minor_gl is not None:
|
|
63
|
+
axis.remove(minor_gl)
|
|
64
|
+
|
|
65
|
+
def _set_light_gridline(self, gridline_elem):
|
|
66
|
+
"""将网格线设为极浅灰色虚线"""
|
|
67
|
+
# 清除现有 spPr
|
|
68
|
+
spPr = gridline_elem.find('c:spPr', namespaces=NAMESPACES)
|
|
69
|
+
if spPr is not None:
|
|
70
|
+
gridline_elem.remove(spPr)
|
|
71
|
+
|
|
72
|
+
spPr = etree.SubElement(gridline_elem, f"{{{NAMESPACES['c']}}}spPr")
|
|
73
|
+
ln = etree.SubElement(spPr, f"{{{NAMESPACES['a']}}}ln")
|
|
74
|
+
ln.set('w', str(self.GRID_WIDTH))
|
|
75
|
+
|
|
76
|
+
# 颜色
|
|
77
|
+
solidFill = etree.SubElement(ln, f"{{{NAMESPACES['a']}}}solidFill")
|
|
78
|
+
srgbClr = etree.SubElement(solidFill, f"{{{NAMESPACES['a']}}}srgbClr")
|
|
79
|
+
srgbClr.set('val', get_chart_token("gridline"))
|
|
80
|
+
|
|
81
|
+
# 虚线样式
|
|
82
|
+
prstDash = etree.SubElement(ln, f"{{{NAMESPACES['a']}}}prstDash")
|
|
83
|
+
prstDash.set('val', 'dot')
|
|
84
|
+
|
|
85
|
+
def _clean_tick_marks(self):
|
|
86
|
+
"""坐标轴刻度线 -> none"""
|
|
87
|
+
for axis_tag in ['valAx', 'catAx']:
|
|
88
|
+
for axis in self.plotArea.findall(f'c:{axis_tag}', namespaces=NAMESPACES):
|
|
89
|
+
# 主刻度线
|
|
90
|
+
major_tick = axis.find('c:majorTickMark', namespaces=NAMESPACES)
|
|
91
|
+
if major_tick is not None:
|
|
92
|
+
major_tick.set('val', 'none')
|
|
93
|
+
else:
|
|
94
|
+
mt = etree.SubElement(axis, f"{{{NAMESPACES['c']}}}majorTickMark")
|
|
95
|
+
mt.set('val', 'none')
|
|
96
|
+
|
|
97
|
+
# 次刻度线
|
|
98
|
+
minor_tick = axis.find('c:minorTickMark', namespaces=NAMESPACES)
|
|
99
|
+
if minor_tick is not None:
|
|
100
|
+
minor_tick.set('val', 'none')
|
|
101
|
+
else:
|
|
102
|
+
mt = etree.SubElement(axis, f"{{{NAMESPACES['c']}}}minorTickMark")
|
|
103
|
+
mt.set('val', 'none')
|
|
104
|
+
|
|
105
|
+
def _clean_legend_border(self):
|
|
106
|
+
"""移除图例框边框"""
|
|
107
|
+
legend = self.chartSpace.find('.//c:legend', namespaces=NAMESPACES)
|
|
108
|
+
if legend is None:
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
spPr = legend.find('c:spPr', namespaces=NAMESPACES)
|
|
112
|
+
if spPr is None:
|
|
113
|
+
spPr = etree.SubElement(legend, f"{{{NAMESPACES['c']}}}spPr")
|
|
114
|
+
|
|
115
|
+
# 移除线条
|
|
116
|
+
ln = spPr.find('a:ln', namespaces=NAMESPACES)
|
|
117
|
+
if ln is not None:
|
|
118
|
+
spPr.remove(ln)
|
|
119
|
+
ln = etree.SubElement(spPr, f"{{{NAMESPACES['a']}}}ln")
|
|
120
|
+
etree.SubElement(ln, f"{{{NAMESPACES['a']}}}noFill")
|
|
121
|
+
|
|
122
|
+
# 移除填充
|
|
123
|
+
for fill_tag in ['solidFill', 'gradFill', 'pattFill']:
|
|
124
|
+
fill = spPr.find(f'a:{fill_tag}', namespaces=NAMESPACES)
|
|
125
|
+
if fill is not None:
|
|
126
|
+
spPr.remove(fill)
|
|
127
|
+
etree.SubElement(spPr, f"{{{NAMESPACES['a']}}}noFill")
|
|
128
|
+
|
|
129
|
+
def _clean_plot_area_border(self):
|
|
130
|
+
"""移除 plotArea 边框"""
|
|
131
|
+
spPr = self.plotArea.find('c:spPr', namespaces=NAMESPACES)
|
|
132
|
+
if spPr is None:
|
|
133
|
+
spPr = etree.SubElement(self.plotArea, f"{{{NAMESPACES['c']}}}spPr")
|
|
134
|
+
|
|
135
|
+
ln = spPr.find('a:ln', namespaces=NAMESPACES)
|
|
136
|
+
if ln is not None:
|
|
137
|
+
spPr.remove(ln)
|
|
138
|
+
ln = etree.SubElement(spPr, f"{{{NAMESPACES['a']}}}ln")
|
|
139
|
+
etree.SubElement(ln, f"{{{NAMESPACES['a']}}}noFill")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def clean_chart(chart):
|
|
143
|
+
"""便捷函数: 对 chart 执行全部清洗"""
|
|
144
|
+
return ChartJunkCleaner(chart).clean()
|