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/layout.py
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
"""
|
|
2
|
+
图表布局配置模块
|
|
3
|
+
|
|
4
|
+
控制图表的结构性属性:
|
|
5
|
+
- 图例位置、大小
|
|
6
|
+
- 横轴配置(日期轴、刻度)
|
|
7
|
+
- 纵轴配置(位置、刻度)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from pptx.enum.chart import XL_LEGEND_POSITION, XL_TICK_MARK, XL_TICK_LABEL_POSITION
|
|
11
|
+
from pptx.util import Pt
|
|
12
|
+
from typing import Optional, Dict, Any
|
|
13
|
+
|
|
14
|
+
from ._log import debug_print as print
|
|
15
|
+
|
|
16
|
+
# ============================================================================
|
|
17
|
+
# 图例配置
|
|
18
|
+
# ============================================================================
|
|
19
|
+
|
|
20
|
+
class LegendConfig:
|
|
21
|
+
"""图例配置"""
|
|
22
|
+
|
|
23
|
+
# 预设位置
|
|
24
|
+
BOTTOM = XL_LEGEND_POSITION.BOTTOM # 底部
|
|
25
|
+
TOP = XL_LEGEND_POSITION.TOP # 顶部
|
|
26
|
+
LEFT = XL_LEGEND_POSITION.LEFT # 左侧
|
|
27
|
+
RIGHT = XL_LEGEND_POSITION.RIGHT # 右侧
|
|
28
|
+
CORNER = XL_LEGEND_POSITION.CORNER # 右上角
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
position = XL_LEGEND_POSITION.BOTTOM,
|
|
33
|
+
font_size_pt: float = 10,
|
|
34
|
+
font_name: str = "微软雅黑",
|
|
35
|
+
include_in_layout: bool = False,
|
|
36
|
+
):
|
|
37
|
+
"""
|
|
38
|
+
初始化图例配置
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
position: 图例位置
|
|
42
|
+
- XL_LEGEND_POSITION.BOTTOM (默认)
|
|
43
|
+
- XL_LEGEND_POSITION.TOP
|
|
44
|
+
- XL_LEGEND_POSITION.LEFT
|
|
45
|
+
- XL_LEGEND_POSITION.RIGHT
|
|
46
|
+
- XL_LEGEND_POSITION.CORNER
|
|
47
|
+
font_size_pt: 字体大小(pt)
|
|
48
|
+
font_name: 字体名称(默认:黑体)
|
|
49
|
+
include_in_layout: 是否包含在布局中(False 防止图例覆盖图表)
|
|
50
|
+
"""
|
|
51
|
+
self.position = position
|
|
52
|
+
self.font_size_pt = font_size_pt
|
|
53
|
+
self.font_name = font_name
|
|
54
|
+
self.include_in_layout = include_in_layout
|
|
55
|
+
|
|
56
|
+
def apply_to_chart(self, chart):
|
|
57
|
+
"""应用到图表"""
|
|
58
|
+
chart.has_legend = True
|
|
59
|
+
chart.legend.position = self.position
|
|
60
|
+
chart.legend.include_in_layout = self.include_in_layout
|
|
61
|
+
|
|
62
|
+
if self.font_size_pt:
|
|
63
|
+
chart.legend.font.size = Pt(self.font_size_pt)
|
|
64
|
+
if self.font_name:
|
|
65
|
+
chart.legend.font.name = self.font_name
|
|
66
|
+
|
|
67
|
+
print(f" - 图例配置: 位置={self._position_name()}, 字体={self.font_name} {self.font_size_pt}pt")
|
|
68
|
+
|
|
69
|
+
def _position_name(self):
|
|
70
|
+
"""获取位置名称"""
|
|
71
|
+
position_names = {
|
|
72
|
+
XL_LEGEND_POSITION.BOTTOM: "底部",
|
|
73
|
+
XL_LEGEND_POSITION.TOP: "顶部",
|
|
74
|
+
XL_LEGEND_POSITION.LEFT: "左侧",
|
|
75
|
+
XL_LEGEND_POSITION.RIGHT: "右侧",
|
|
76
|
+
XL_LEGEND_POSITION.CORNER: "右上角",
|
|
77
|
+
}
|
|
78
|
+
return position_names.get(self.position, "未知")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ============================================================================
|
|
82
|
+
# 横轴配置
|
|
83
|
+
# ============================================================================
|
|
84
|
+
|
|
85
|
+
class CategoryAxisConfig:
|
|
86
|
+
"""横轴(分类轴)配置"""
|
|
87
|
+
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
is_date_axis: bool = False,
|
|
91
|
+
major_unit: Optional[float] = None,
|
|
92
|
+
major_unit_days: Optional[int] = None,
|
|
93
|
+
tick_label_position = XL_TICK_LABEL_POSITION.LOW,
|
|
94
|
+
major_tick_mark = XL_TICK_MARK.NONE,
|
|
95
|
+
minor_tick_mark = XL_TICK_MARK.NONE,
|
|
96
|
+
number_format: Optional[str] = None,
|
|
97
|
+
font_size_pt: float = 10,
|
|
98
|
+
font_name: str = "微软雅黑",
|
|
99
|
+
):
|
|
100
|
+
"""
|
|
101
|
+
初始化横轴配置
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
is_date_axis: 是否为日期轴
|
|
105
|
+
major_unit: 主刻度单位(用于日期轴,单位为天)
|
|
106
|
+
major_unit_days: 主刻度单位(天)- 便捷参数,与 major_unit 相同
|
|
107
|
+
tick_label_position: 刻度标签位置
|
|
108
|
+
major_tick_mark: 主刻度线样式
|
|
109
|
+
minor_tick_mark: 次刻度线样式
|
|
110
|
+
number_format: 数字格式(如 'yyyy-mm-dd')
|
|
111
|
+
font_size_pt: 字体大小
|
|
112
|
+
font_name: 字体名称(默认:黑体)
|
|
113
|
+
"""
|
|
114
|
+
self.is_date_axis = is_date_axis
|
|
115
|
+
self.major_unit = major_unit or major_unit_days
|
|
116
|
+
self.tick_label_position = tick_label_position
|
|
117
|
+
self.major_tick_mark = major_tick_mark
|
|
118
|
+
self.minor_tick_mark = minor_tick_mark
|
|
119
|
+
self.number_format = number_format
|
|
120
|
+
self.font_size_pt = font_size_pt
|
|
121
|
+
self.font_name = font_name
|
|
122
|
+
|
|
123
|
+
def apply_to_chart(self, chart):
|
|
124
|
+
"""应用到图表"""
|
|
125
|
+
try:
|
|
126
|
+
category_axis = chart.category_axis
|
|
127
|
+
|
|
128
|
+
# 设置日期轴
|
|
129
|
+
if self.is_date_axis:
|
|
130
|
+
from pptx.enum.chart import XL_CATEGORY_TYPE
|
|
131
|
+
category_axis.category_type = XL_CATEGORY_TYPE.TIME_SCALE
|
|
132
|
+
print(f" - 横轴: 日期轴")
|
|
133
|
+
|
|
134
|
+
# 设置刻度单位
|
|
135
|
+
if self.major_unit:
|
|
136
|
+
from pptx.enum.chart import XL_TIME_UNIT
|
|
137
|
+
category_axis.major_unit = self.major_unit
|
|
138
|
+
category_axis.major_unit_scale = XL_TIME_UNIT.DAYS
|
|
139
|
+
print(f" - 刻度单位: {self.major_unit} 天")
|
|
140
|
+
else:
|
|
141
|
+
print(f" - 横轴: 分类轴")
|
|
142
|
+
|
|
143
|
+
# 设置刻度标签位置
|
|
144
|
+
category_axis.tick_label_position = self.tick_label_position
|
|
145
|
+
|
|
146
|
+
# 设置刻度线
|
|
147
|
+
category_axis.major_tick_mark = self.major_tick_mark
|
|
148
|
+
category_axis.minor_tick_mark = self.minor_tick_mark
|
|
149
|
+
|
|
150
|
+
# 设置数字格式
|
|
151
|
+
if self.number_format:
|
|
152
|
+
category_axis.tick_labels.number_format = self.number_format
|
|
153
|
+
print(f" - 日期格式: {self.number_format}")
|
|
154
|
+
|
|
155
|
+
# 设置字体大小和字体名称
|
|
156
|
+
if self.font_size_pt:
|
|
157
|
+
category_axis.tick_labels.font.size = Pt(self.font_size_pt)
|
|
158
|
+
if self.font_name:
|
|
159
|
+
category_axis.tick_labels.font.name = self.font_name
|
|
160
|
+
print(f" - 横轴字体: {self.font_name} {self.font_size_pt}pt")
|
|
161
|
+
|
|
162
|
+
except Exception as e:
|
|
163
|
+
print(f" ⚠️ 横轴配置失败: {e}")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# ============================================================================
|
|
167
|
+
# 纵轴配置
|
|
168
|
+
# ============================================================================
|
|
169
|
+
|
|
170
|
+
class ValueAxisConfig:
|
|
171
|
+
"""纵轴(数值轴)配置"""
|
|
172
|
+
|
|
173
|
+
def __init__(
|
|
174
|
+
self,
|
|
175
|
+
tick_label_position = XL_TICK_LABEL_POSITION.LOW,
|
|
176
|
+
major_tick_mark = XL_TICK_MARK.OUTSIDE,
|
|
177
|
+
minor_tick_mark = XL_TICK_MARK.NONE,
|
|
178
|
+
number_format: Optional[str] = None,
|
|
179
|
+
font_size_pt: float = 10,
|
|
180
|
+
font_name: str = "微软雅黑",
|
|
181
|
+
has_major_gridlines: bool = True,
|
|
182
|
+
min_value: Optional[float] = None,
|
|
183
|
+
max_value: Optional[float] = None,
|
|
184
|
+
major_unit: Optional[float] = None,
|
|
185
|
+
):
|
|
186
|
+
"""
|
|
187
|
+
初始化纵轴配置
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
tick_label_position: 刻度标签位置
|
|
191
|
+
major_tick_mark: 主刻度线样式
|
|
192
|
+
minor_tick_mark: 次刻度线样式
|
|
193
|
+
number_format: 数字格式(如 '0.00%')
|
|
194
|
+
font_size_pt: 字体大小
|
|
195
|
+
font_name: 字体名称(默认:黑体)
|
|
196
|
+
has_major_gridlines: 是否显示主网格线
|
|
197
|
+
min_value: 最小值(可选)
|
|
198
|
+
max_value: 最大值(可选)
|
|
199
|
+
major_unit: 主刻度单位(可选)
|
|
200
|
+
"""
|
|
201
|
+
self.tick_label_position = tick_label_position
|
|
202
|
+
self.major_tick_mark = major_tick_mark
|
|
203
|
+
self.minor_tick_mark = minor_tick_mark
|
|
204
|
+
self.number_format = number_format
|
|
205
|
+
self.font_size_pt = font_size_pt
|
|
206
|
+
self.font_name = font_name
|
|
207
|
+
self.has_major_gridlines = has_major_gridlines
|
|
208
|
+
self.min_value = min_value
|
|
209
|
+
self.max_value = max_value
|
|
210
|
+
self.major_unit = major_unit
|
|
211
|
+
|
|
212
|
+
def apply_to_chart(self, chart):
|
|
213
|
+
"""应用到图表(主值轴)"""
|
|
214
|
+
try:
|
|
215
|
+
# ⭐ 通过 XML 直接设置主值轴,确保格式生效
|
|
216
|
+
chart_element = chart._element
|
|
217
|
+
val_ax_elements = chart_element.findall('.//{http://schemas.openxmlformats.org/drawingml/2006/chart}valAx')
|
|
218
|
+
|
|
219
|
+
# 主值轴:纵向图在左侧('l'),横向条形图在底部('b')
|
|
220
|
+
primary_ax = None
|
|
221
|
+
for ax in val_ax_elements:
|
|
222
|
+
ax_pos = ax.find('.//{http://schemas.openxmlformats.org/drawingml/2006/chart}axPos')
|
|
223
|
+
if ax_pos is not None and ax_pos.get('val') in ('l', 'b'):
|
|
224
|
+
primary_ax = ax
|
|
225
|
+
break
|
|
226
|
+
|
|
227
|
+
if primary_ax is None:
|
|
228
|
+
print(f" ⚠️ 未找到主值轴")
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
from lxml import etree
|
|
232
|
+
|
|
233
|
+
# 设置数字格式(通过 XML)
|
|
234
|
+
if self.number_format:
|
|
235
|
+
num_fmt = primary_ax.find('.//{http://schemas.openxmlformats.org/drawingml/2006/chart}numFmt')
|
|
236
|
+
if num_fmt is None:
|
|
237
|
+
# 创建 numFmt 元素
|
|
238
|
+
num_fmt = etree.Element('{http://schemas.openxmlformats.org/drawingml/2006/chart}numFmt')
|
|
239
|
+
# 在 tickLblPos 之后插入
|
|
240
|
+
tick_lbl_pos = primary_ax.find('.//{http://schemas.openxmlformats.org/drawingml/2006/chart}tickLblPos')
|
|
241
|
+
if tick_lbl_pos is not None:
|
|
242
|
+
parent_list = list(primary_ax)
|
|
243
|
+
pos_index = parent_list.index(tick_lbl_pos)
|
|
244
|
+
primary_ax.insert(pos_index + 1, num_fmt)
|
|
245
|
+
else:
|
|
246
|
+
primary_ax.append(num_fmt)
|
|
247
|
+
|
|
248
|
+
num_fmt.set('formatCode', self.number_format)
|
|
249
|
+
num_fmt.set('sourceLinked', '0')
|
|
250
|
+
print(f" - 主值轴格式: {self.number_format}")
|
|
251
|
+
|
|
252
|
+
# 设置字体(通过 XML)
|
|
253
|
+
if self.font_size_pt or self.font_name:
|
|
254
|
+
self._apply_font_to_axis_xml(primary_ax, self.font_name, self.font_size_pt)
|
|
255
|
+
print(f" - 主值轴字体: {self.font_name} {self.font_size_pt}pt")
|
|
256
|
+
|
|
257
|
+
# 设置轴范围和刻度间隔(通过 XML)
|
|
258
|
+
if self.min_value is not None or self.max_value is not None:
|
|
259
|
+
scaling = primary_ax.find('.//{http://schemas.openxmlformats.org/drawingml/2006/chart}scaling')
|
|
260
|
+
if scaling is None:
|
|
261
|
+
scaling = etree.Element('{http://schemas.openxmlformats.org/drawingml/2006/chart}scaling')
|
|
262
|
+
# 在 axId 之后插入
|
|
263
|
+
ax_id = primary_ax.find('.//{http://schemas.openxmlformats.org/drawingml/2006/chart}axId')
|
|
264
|
+
if ax_id is not None:
|
|
265
|
+
parent_list = list(primary_ax)
|
|
266
|
+
ax_id_index = parent_list.index(ax_id)
|
|
267
|
+
primary_ax.insert(ax_id_index + 1, scaling)
|
|
268
|
+
else:
|
|
269
|
+
primary_ax.insert(0, scaling)
|
|
270
|
+
|
|
271
|
+
# 添加 orientation
|
|
272
|
+
orientation = etree.SubElement(scaling, '{http://schemas.openxmlformats.org/drawingml/2006/chart}orientation')
|
|
273
|
+
orientation.set('val', 'minMax')
|
|
274
|
+
|
|
275
|
+
if self.min_value is not None:
|
|
276
|
+
min_elem = scaling.find('.//{http://schemas.openxmlformats.org/drawingml/2006/chart}min')
|
|
277
|
+
if min_elem is None:
|
|
278
|
+
min_elem = etree.SubElement(scaling, '{http://schemas.openxmlformats.org/drawingml/2006/chart}min')
|
|
279
|
+
min_elem.set('val', str(self.min_value))
|
|
280
|
+
print(f" - 主值轴最小值: {self.min_value}")
|
|
281
|
+
|
|
282
|
+
if self.max_value is not None:
|
|
283
|
+
max_elem = scaling.find('.//{http://schemas.openxmlformats.org/drawingml/2006/chart}max')
|
|
284
|
+
if max_elem is None:
|
|
285
|
+
max_elem = etree.SubElement(scaling, '{http://schemas.openxmlformats.org/drawingml/2006/chart}max')
|
|
286
|
+
max_elem.set('val', str(self.max_value))
|
|
287
|
+
print(f" - 主值轴最大值: {self.max_value}")
|
|
288
|
+
|
|
289
|
+
if self.major_unit is not None:
|
|
290
|
+
major_unit_elem = primary_ax.find('.//{http://schemas.openxmlformats.org/drawingml/2006/chart}majorUnit')
|
|
291
|
+
if major_unit_elem is None:
|
|
292
|
+
major_unit_elem = etree.SubElement(primary_ax, '{http://schemas.openxmlformats.org/drawingml/2006/chart}majorUnit')
|
|
293
|
+
major_unit_elem.set('val', str(self.major_unit))
|
|
294
|
+
print(f" - 主值轴刻度间隔: {self.major_unit}")
|
|
295
|
+
|
|
296
|
+
except Exception as e:
|
|
297
|
+
print(f" ⚠️ 主值轴配置失败: {e}")
|
|
298
|
+
import traceback
|
|
299
|
+
traceback.print_exc()
|
|
300
|
+
|
|
301
|
+
def _apply_font_to_axis_xml(self, axis_element, font_name: str, font_size_pt: float):
|
|
302
|
+
"""通过 XML 设置轴字体"""
|
|
303
|
+
from lxml import etree
|
|
304
|
+
|
|
305
|
+
# 查找或创建 txPr
|
|
306
|
+
txPr = axis_element.find('.//{http://schemas.openxmlformats.org/drawingml/2006/chart}txPr')
|
|
307
|
+
if txPr is None:
|
|
308
|
+
txPr = etree.SubElement(axis_element, '{http://schemas.openxmlformats.org/drawingml/2006/chart}txPr')
|
|
309
|
+
bodyPr = etree.SubElement(txPr, '{http://schemas.openxmlformats.org/drawingml/2006/main}bodyPr')
|
|
310
|
+
lstStyle = etree.SubElement(txPr, '{http://schemas.openxmlformats.org/drawingml/2006/main}lstStyle')
|
|
311
|
+
|
|
312
|
+
# 设置字体
|
|
313
|
+
p = txPr.find('.//{http://schemas.openxmlformats.org/drawingml/2006/main}p')
|
|
314
|
+
if p is None:
|
|
315
|
+
p = etree.SubElement(txPr, '{http://schemas.openxmlformats.org/drawingml/2006/main}p')
|
|
316
|
+
|
|
317
|
+
pPr = p.find('.//{http://schemas.openxmlformats.org/drawingml/2006/main}pPr')
|
|
318
|
+
if pPr is None:
|
|
319
|
+
pPr = etree.SubElement(p, '{http://schemas.openxmlformats.org/drawingml/2006/main}pPr')
|
|
320
|
+
|
|
321
|
+
defRPr = pPr.find('.//{http://schemas.openxmlformats.org/drawingml/2006/main}defRPr')
|
|
322
|
+
if defRPr is None:
|
|
323
|
+
defRPr = etree.SubElement(pPr, '{http://schemas.openxmlformats.org/drawingml/2006/main}defRPr')
|
|
324
|
+
|
|
325
|
+
if font_size_pt:
|
|
326
|
+
defRPr.set('sz', str(int(font_size_pt * 100)))
|
|
327
|
+
|
|
328
|
+
if font_name:
|
|
329
|
+
latin = defRPr.find('.//{http://schemas.openxmlformats.org/drawingml/2006/main}latin')
|
|
330
|
+
if latin is None:
|
|
331
|
+
latin = etree.SubElement(defRPr, '{http://schemas.openxmlformats.org/drawingml/2006/main}latin')
|
|
332
|
+
latin.set('typeface', font_name)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
# ============================================================================
|
|
337
|
+
# 默认配置
|
|
338
|
+
# ============================================================================
|
|
339
|
+
|
|
340
|
+
# 默认图例配置:底部、9pt 黑体
|
|
341
|
+
DEFAULT_LEGEND_CONFIG = LegendConfig(
|
|
342
|
+
position=XL_LEGEND_POSITION.BOTTOM,
|
|
343
|
+
font_size_pt=9,
|
|
344
|
+
font_name="微软雅黑",
|
|
345
|
+
include_in_layout=False,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
# 默认横轴配置:普通分类轴、外侧刻度线、9pt 黑体
|
|
349
|
+
DEFAULT_CATEGORY_AXIS_CONFIG = CategoryAxisConfig(
|
|
350
|
+
is_date_axis=False,
|
|
351
|
+
major_tick_mark=XL_TICK_MARK.OUTSIDE, # ⭐ 改为 OUTSIDE(与原图一致)
|
|
352
|
+
minor_tick_mark=XL_TICK_MARK.NONE,
|
|
353
|
+
font_size_pt=9,
|
|
354
|
+
font_name="微软雅黑",
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
# 默认纵轴配置:无网格线、9pt 黑体
|
|
358
|
+
DEFAULT_VALUE_AXIS_CONFIG = ValueAxisConfig(
|
|
359
|
+
tick_label_position=XL_TICK_LABEL_POSITION.LOW,
|
|
360
|
+
major_tick_mark=XL_TICK_MARK.OUTSIDE,
|
|
361
|
+
minor_tick_mark=XL_TICK_MARK.NONE,
|
|
362
|
+
font_size_pt=9,
|
|
363
|
+
font_name="微软雅黑",
|
|
364
|
+
has_major_gridlines=False, # ⭐ 改为 False(原图无网格线)
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
# ============================================================================
|
|
369
|
+
# 完整布局配置
|
|
370
|
+
# ============================================================================
|
|
371
|
+
|
|
372
|
+
class ChartLayoutConfig:
|
|
373
|
+
"""完整的图表布局配置"""
|
|
374
|
+
|
|
375
|
+
def __init__(
|
|
376
|
+
self,
|
|
377
|
+
title: Optional[str] = None,
|
|
378
|
+
legend_config: Optional[LegendConfig] = None,
|
|
379
|
+
category_axis_config: Optional[CategoryAxisConfig] = None,
|
|
380
|
+
value_axis_config: Optional[ValueAxisConfig] = None,
|
|
381
|
+
secondary_value_axis_config: Optional[ValueAxisConfig] = None, # ⭐ 新增次值轴配置
|
|
382
|
+
date_axis_config = None, # DateAxisConfig(避免循环导入)
|
|
383
|
+
):
|
|
384
|
+
"""
|
|
385
|
+
初始化布局配置
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
title: 图表标题
|
|
389
|
+
legend_config: 图例配置
|
|
390
|
+
category_axis_config: 横轴配置
|
|
391
|
+
value_axis_config: 主值轴(左轴)配置
|
|
392
|
+
secondary_value_axis_config: 次值轴(右轴)配置
|
|
393
|
+
date_axis_config: 日期轴配置(DateAxisConfig,用于精确控制日期轴)
|
|
394
|
+
"""
|
|
395
|
+
self.title = title
|
|
396
|
+
self.legend_config = legend_config or DEFAULT_LEGEND_CONFIG
|
|
397
|
+
self.category_axis_config = category_axis_config or DEFAULT_CATEGORY_AXIS_CONFIG
|
|
398
|
+
self.value_axis_config = value_axis_config or DEFAULT_VALUE_AXIS_CONFIG
|
|
399
|
+
self.secondary_value_axis_config = secondary_value_axis_config # ⭐ 新增
|
|
400
|
+
self.date_axis_config = date_axis_config # ⭐ 新增
|
|
401
|
+
|
|
402
|
+
def apply_to_chart(self, chart):
|
|
403
|
+
"""应用所有配置到图表"""
|
|
404
|
+
print(f"\n⚙️ 应用布局配置:")
|
|
405
|
+
|
|
406
|
+
# 应用标题
|
|
407
|
+
if self.title:
|
|
408
|
+
try:
|
|
409
|
+
chart.has_title = True
|
|
410
|
+
chart.chart_title.text_frame.text = self.title
|
|
411
|
+
print(f" - 标题: {self.title}")
|
|
412
|
+
except Exception as e:
|
|
413
|
+
print(f" ⚠️ 标题设置失败: {e}")
|
|
414
|
+
|
|
415
|
+
# 应用图例配置
|
|
416
|
+
if self.legend_config:
|
|
417
|
+
self.legend_config.apply_to_chart(chart)
|
|
418
|
+
|
|
419
|
+
# ⭐ 应用日期轴配置(优先于普通横轴配置)
|
|
420
|
+
if self.date_axis_config:
|
|
421
|
+
self.date_axis_config.apply_to_chart(chart)
|
|
422
|
+
# 应用横轴配置(如果没有日期轴配置)
|
|
423
|
+
elif self.category_axis_config:
|
|
424
|
+
self.category_axis_config.apply_to_chart(chart)
|
|
425
|
+
|
|
426
|
+
# 应用主值轴配置(左轴)
|
|
427
|
+
if self.value_axis_config:
|
|
428
|
+
self.value_axis_config.apply_to_chart(chart)
|
|
429
|
+
|
|
430
|
+
# ⭐ 应用次值轴配置(右轴)
|
|
431
|
+
if self.secondary_value_axis_config:
|
|
432
|
+
self._apply_secondary_axis_config(chart)
|
|
433
|
+
|
|
434
|
+
def _apply_secondary_axis_config(self, chart):
|
|
435
|
+
"""应用次值轴配置(通过 XML)"""
|
|
436
|
+
try:
|
|
437
|
+
# 通过 XML 查找次值轴
|
|
438
|
+
chart_element = chart._element
|
|
439
|
+
val_ax_elements = chart_element.findall('.//{http://schemas.openxmlformats.org/drawingml/2006/chart}valAx')
|
|
440
|
+
|
|
441
|
+
# 次值轴通常是第二个 valAx(position='r')
|
|
442
|
+
secondary_ax = None
|
|
443
|
+
for ax in val_ax_elements:
|
|
444
|
+
ax_pos = ax.find('.//{http://schemas.openxmlformats.org/drawingml/2006/chart}axPos')
|
|
445
|
+
if ax_pos is not None and ax_pos.get('val') == 'r':
|
|
446
|
+
secondary_ax = ax
|
|
447
|
+
break
|
|
448
|
+
|
|
449
|
+
if secondary_ax is None:
|
|
450
|
+
print(f" ⚠️ 未找到次值轴")
|
|
451
|
+
return
|
|
452
|
+
|
|
453
|
+
config = self.secondary_value_axis_config
|
|
454
|
+
|
|
455
|
+
# 设置数字格式
|
|
456
|
+
if config.number_format:
|
|
457
|
+
num_fmt = secondary_ax.find('.//{http://schemas.openxmlformats.org/drawingml/2006/chart}numFmt')
|
|
458
|
+
if num_fmt is not None:
|
|
459
|
+
num_fmt.set('formatCode', config.number_format)
|
|
460
|
+
num_fmt.set('sourceLinked', '0')
|
|
461
|
+
print(f" - 次值轴格式: {config.number_format}")
|
|
462
|
+
|
|
463
|
+
# 设置字体
|
|
464
|
+
if config.font_size_pt or config.font_name:
|
|
465
|
+
from pptx.util import Pt
|
|
466
|
+
from lxml import etree
|
|
467
|
+
|
|
468
|
+
# 查找或创建 txPr (文本属性)
|
|
469
|
+
txPr = secondary_ax.find('.//{http://schemas.openxmlformats.org/drawingml/2006/chart}txPr')
|
|
470
|
+
if txPr is None:
|
|
471
|
+
txPr = etree.SubElement(secondary_ax, '{http://schemas.openxmlformats.org/drawingml/2006/chart}txPr')
|
|
472
|
+
bodyPr = etree.SubElement(txPr, '{http://schemas.openxmlformats.org/drawingml/2006/main}bodyPr')
|
|
473
|
+
lstStyle = etree.SubElement(txPr, '{http://schemas.openxmlformats.org/drawingml/2006/main}lstStyle')
|
|
474
|
+
|
|
475
|
+
# 设置字体
|
|
476
|
+
p = txPr.find('.//{http://schemas.openxmlformats.org/drawingml/2006/main}p')
|
|
477
|
+
if p is None:
|
|
478
|
+
p = etree.SubElement(txPr, '{http://schemas.openxmlformats.org/drawingml/2006/main}p')
|
|
479
|
+
|
|
480
|
+
pPr = p.find('.//{http://schemas.openxmlformats.org/drawingml/2006/main}pPr')
|
|
481
|
+
if pPr is None:
|
|
482
|
+
pPr = etree.SubElement(p, '{http://schemas.openxmlformats.org/drawingml/2006/main}pPr')
|
|
483
|
+
|
|
484
|
+
defRPr = pPr.find('.//{http://schemas.openxmlformats.org/drawingml/2006/main}defRPr')
|
|
485
|
+
if defRPr is None:
|
|
486
|
+
defRPr = etree.SubElement(pPr, '{http://schemas.openxmlformats.org/drawingml/2006/main}defRPr')
|
|
487
|
+
|
|
488
|
+
if config.font_size_pt:
|
|
489
|
+
defRPr.set('sz', str(int(config.font_size_pt * 100)))
|
|
490
|
+
|
|
491
|
+
if config.font_name:
|
|
492
|
+
latin = defRPr.find('.//{http://schemas.openxmlformats.org/drawingml/2006/main}latin')
|
|
493
|
+
if latin is None:
|
|
494
|
+
latin = etree.SubElement(defRPr, '{http://schemas.openxmlformats.org/drawingml/2006/main}latin')
|
|
495
|
+
latin.set('typeface', config.font_name)
|
|
496
|
+
|
|
497
|
+
print(f" - 次值轴字体: {config.font_name} {config.font_size_pt}pt")
|
|
498
|
+
|
|
499
|
+
# ⭐ 设置轴范围和刻度间隔(通过 XML)
|
|
500
|
+
from lxml import etree
|
|
501
|
+
|
|
502
|
+
if config.min_value is not None or config.max_value is not None:
|
|
503
|
+
# 查找或创建 scaling 元素
|
|
504
|
+
scaling = secondary_ax.find('.//{http://schemas.openxmlformats.org/drawingml/2006/chart}scaling')
|
|
505
|
+
if scaling is None:
|
|
506
|
+
scaling = etree.SubElement(secondary_ax, '{http://schemas.openxmlformats.org/drawingml/2006/chart}scaling')
|
|
507
|
+
# 添加 orientation
|
|
508
|
+
orientation = etree.SubElement(scaling, '{http://schemas.openxmlformats.org/drawingml/2006/chart}orientation')
|
|
509
|
+
orientation.set('val', 'minMax')
|
|
510
|
+
|
|
511
|
+
if config.min_value is not None:
|
|
512
|
+
min_elem = scaling.find('.//{http://schemas.openxmlformats.org/drawingml/2006/chart}min')
|
|
513
|
+
if min_elem is None:
|
|
514
|
+
min_elem = etree.SubElement(scaling, '{http://schemas.openxmlformats.org/drawingml/2006/chart}min')
|
|
515
|
+
min_elem.set('val', str(config.min_value))
|
|
516
|
+
print(f" - 次值轴最小值: {config.min_value}")
|
|
517
|
+
|
|
518
|
+
if config.max_value is not None:
|
|
519
|
+
max_elem = scaling.find('.//{http://schemas.openxmlformats.org/drawingml/2006/chart}max')
|
|
520
|
+
if max_elem is None:
|
|
521
|
+
max_elem = etree.SubElement(scaling, '{http://schemas.openxmlformats.org/drawingml/2006/chart}max')
|
|
522
|
+
max_elem.set('val', str(config.max_value))
|
|
523
|
+
print(f" - 次值轴最大值: {config.max_value}")
|
|
524
|
+
|
|
525
|
+
if config.major_unit is not None:
|
|
526
|
+
major_unit_elem = secondary_ax.find('.//{http://schemas.openxmlformats.org/drawingml/2006/chart}majorUnit')
|
|
527
|
+
if major_unit_elem is None:
|
|
528
|
+
major_unit_elem = etree.SubElement(secondary_ax, '{http://schemas.openxmlformats.org/drawingml/2006/chart}majorUnit')
|
|
529
|
+
major_unit_elem.set('val', str(config.major_unit))
|
|
530
|
+
print(f" - 次值轴刻度间隔: {config.major_unit}")
|
|
531
|
+
|
|
532
|
+
except Exception as e:
|
|
533
|
+
print(f" ⚠️ 次值轴配置失败: {e}")
|
|
534
|
+
import traceback
|
|
535
|
+
traceback.print_exc()
|