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/metadata.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Chart metadata persistence.
|
|
2
|
+
|
|
3
|
+
Belongs to: **metadata** lifecycle per ADR-0007 §1.
|
|
4
|
+
Realises: ADR-0004 §2 (metadata 5-piece round-trip contract)
|
|
5
|
+
+ ADR-0007 §3 (layered metadata strategy).
|
|
6
|
+
|
|
7
|
+
Single entrypoint for chart-level semantic metadata. Today only **layer 1**
|
|
8
|
+
(embedded workbook hidden sheet) is implemented; the layered strategy goal
|
|
9
|
+
in ADR-0007 §3 is:
|
|
10
|
+
|
|
11
|
+
1. embedded workbook hidden sheet ← layer 1, this module (default)
|
|
12
|
+
2. chart semantic anchor / invisible shape
|
|
13
|
+
← layer 2, lives in ``semantic_anchor.py`` for shape-composition
|
|
14
|
+
families that have no chart container
|
|
15
|
+
3. custom XML part ← layer 3, requires PRD + compat check
|
|
16
|
+
4. shape alt text / name ← layer 4, selector hint only
|
|
17
|
+
|
|
18
|
+
This module holds the **single source of truth** for ``METADATA_SHEET_NAME``
|
|
19
|
+
and ``METADATA_SCHEMA_VERSION``; previously these were duplicated in
|
|
20
|
+
``api.py`` and ``parser.py``, a real silent-drift hazard.
|
|
21
|
+
|
|
22
|
+
Public:
|
|
23
|
+
|
|
24
|
+
- :data:`METADATA_SHEET_NAME`
|
|
25
|
+
- :data:`METADATA_SCHEMA_VERSION`
|
|
26
|
+
- :class:`ChartMetadataV1`
|
|
27
|
+
- :func:`write_chart_metadata`
|
|
28
|
+
|
|
29
|
+
Backward-compat (kept for existing callers):
|
|
30
|
+
|
|
31
|
+
- :func:`_write_embedded_metadata` — original signature preserved;
|
|
32
|
+
internally delegates to layer 1 backend.
|
|
33
|
+
|
|
34
|
+
ADR-0007 §2: this module avoids Pydantic; uses standard-library
|
|
35
|
+
``dataclasses`` only.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import io
|
|
41
|
+
import json
|
|
42
|
+
from dataclasses import dataclass, field
|
|
43
|
+
from typing import Any, Dict, List, Optional
|
|
44
|
+
|
|
45
|
+
from ._log import debug_print as print
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# Authoritative constants (DO NOT redefine in api.py / parser.py)
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
METADATA_SHEET_NAME: str = "_pptchartengine_meta"
|
|
53
|
+
"""Name of the hidden sheet inside a chart's embedded workbook that holds
|
|
54
|
+
chart-level semantic metadata. Authoritative source; ``api.py`` and
|
|
55
|
+
``parser.py`` re-import this rather than redefining."""
|
|
56
|
+
|
|
57
|
+
METADATA_SCHEMA_VERSION: str = "2"
|
|
58
|
+
"""Current schema version written to ``B1`` of the metadata sheet.
|
|
59
|
+
Future schema changes must bump this AND maintain a backward-compat reader
|
|
60
|
+
in ``parser.py`` (ADR-0004 backward-compat clause)."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# Public schema
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True)
|
|
69
|
+
class ChartMetadataV1:
|
|
70
|
+
"""Chart-level semantic metadata payload (ADR-0007 §3 layer 1, schema v2).
|
|
71
|
+
|
|
72
|
+
All fields are technical-layer only per ADR-0007 §2 — no business slot,
|
|
73
|
+
no ``user_id``, no prompt.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
categories_col: str
|
|
77
|
+
series_config: List[Dict[str, Any]]
|
|
78
|
+
chart_family: Optional[str] = None
|
|
79
|
+
extra: Dict[str, Any] = field(default_factory=dict)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
# Public API
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def write_chart_metadata(chart, metadata: ChartMetadataV1) -> None:
|
|
88
|
+
"""Persist :class:`ChartMetadataV1` into the chart's embedded workbook.
|
|
89
|
+
|
|
90
|
+
Routes through layer 1 (embedded workbook hidden sheet). Failure is
|
|
91
|
+
currently swallowed with a stderr-like log (preserves prior
|
|
92
|
+
``_write_embedded_metadata`` semantics); a future schema version should
|
|
93
|
+
promote this to a typed :class:`ablechart.replace` style result or
|
|
94
|
+
raise a dedicated ``MetadataWriteError``.
|
|
95
|
+
"""
|
|
96
|
+
_write_workbook_hidden_sheet(
|
|
97
|
+
chart=chart,
|
|
98
|
+
categories_col=metadata.categories_col,
|
|
99
|
+
series_config=metadata.series_config,
|
|
100
|
+
chart_family=metadata.chart_family,
|
|
101
|
+
extra=metadata.extra,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
# Backward-compat (kept until callers migrate to write_chart_metadata)
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _write_embedded_metadata(
|
|
111
|
+
chart,
|
|
112
|
+
categories_col: str,
|
|
113
|
+
series_config: List[Dict],
|
|
114
|
+
metadata: Optional[Dict] = None,
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Legacy signature preserved for ``api.py`` / ``scatter.py`` /
|
|
117
|
+
``semantic_family.py`` call sites. Routes to layer 1 backend.
|
|
118
|
+
|
|
119
|
+
``metadata`` here is a free-form dict that historically carried
|
|
120
|
+
``chart_family`` plus arbitrary extra fields. We extract ``chart_family``
|
|
121
|
+
if present, treat the whole dict as ``extra``.
|
|
122
|
+
"""
|
|
123
|
+
chart_family = None
|
|
124
|
+
if metadata is not None:
|
|
125
|
+
chart_family = metadata.get("chart_family")
|
|
126
|
+
_write_workbook_hidden_sheet(
|
|
127
|
+
chart=chart,
|
|
128
|
+
categories_col=categories_col,
|
|
129
|
+
series_config=series_config,
|
|
130
|
+
chart_family=chart_family,
|
|
131
|
+
extra=metadata,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
# Layer 1 backend — embedded workbook hidden sheet
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _write_workbook_hidden_sheet(
|
|
141
|
+
chart,
|
|
142
|
+
categories_col: str,
|
|
143
|
+
series_config: List[Dict[str, Any]],
|
|
144
|
+
chart_family: Optional[str] = None,
|
|
145
|
+
extra: Optional[Dict[str, Any]] = None,
|
|
146
|
+
) -> None:
|
|
147
|
+
"""Layer 1 implementation: write metadata into a hidden sheet inside
|
|
148
|
+
chart's embedded xlsx workbook. Preserves the exact byte layout of the
|
|
149
|
+
legacy ``_write_embedded_metadata`` so round-trip parsers stay happy."""
|
|
150
|
+
try:
|
|
151
|
+
from openpyxl import load_workbook
|
|
152
|
+
|
|
153
|
+
chart_part = chart.part
|
|
154
|
+
xlsx_part = chart_part.chart_workbook.xlsx_part
|
|
155
|
+
xlsx_stream = io.BytesIO(xlsx_part.blob)
|
|
156
|
+
wb = load_workbook(xlsx_stream)
|
|
157
|
+
|
|
158
|
+
if METADATA_SHEET_NAME in wb.sheetnames:
|
|
159
|
+
del wb[METADATA_SHEET_NAME]
|
|
160
|
+
|
|
161
|
+
ws = wb.create_sheet(METADATA_SHEET_NAME)
|
|
162
|
+
ws.sheet_state = "hidden"
|
|
163
|
+
ws["A1"] = "schema_version"
|
|
164
|
+
ws["B1"] = METADATA_SCHEMA_VERSION
|
|
165
|
+
ws["A2"] = "categories_col"
|
|
166
|
+
ws["B2"] = categories_col
|
|
167
|
+
ws["A3"] = "series_count"
|
|
168
|
+
ws["B3"] = len(series_config)
|
|
169
|
+
ws["A4"] = "chart_family"
|
|
170
|
+
ws["B4"] = chart_family
|
|
171
|
+
ws["A5"] = "chart_metadata_json"
|
|
172
|
+
# Legacy parity: the json blob is the full ``extra`` dict (which in
|
|
173
|
+
# practice already carries chart_family); we don't add a second copy.
|
|
174
|
+
ws["B5"] = json.dumps(extra, ensure_ascii=False) if extra else None
|
|
175
|
+
ws.append([])
|
|
176
|
+
ws.append([
|
|
177
|
+
"series_index", "key", "name", "type",
|
|
178
|
+
"axis", "grouping", "x_key", "size_key",
|
|
179
|
+
])
|
|
180
|
+
|
|
181
|
+
for index, series in enumerate(series_config):
|
|
182
|
+
ws.append([
|
|
183
|
+
index,
|
|
184
|
+
series.get("key"),
|
|
185
|
+
series.get("name"),
|
|
186
|
+
series.get("type"),
|
|
187
|
+
series.get("axis"),
|
|
188
|
+
series.get("grouping"),
|
|
189
|
+
series.get("x_key"),
|
|
190
|
+
series.get("size_key"),
|
|
191
|
+
])
|
|
192
|
+
|
|
193
|
+
output_stream = io.BytesIO()
|
|
194
|
+
wb.save(output_stream)
|
|
195
|
+
xlsx_part._blob = output_stream.getvalue()
|
|
196
|
+
|
|
197
|
+
except Exception as e:
|
|
198
|
+
print(f" ⚠️ 写入图表元数据失败: {e}")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OXML 包 - 底层 XML 操作
|
|
3
|
+
|
|
4
|
+
这个包负责所有的 lxml 操作,不关心业务逻辑(主轴/次轴)。
|
|
5
|
+
只知道如何根据指令创建标准的 OOXML 结构。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .axes import extract_axis_ids, create_value_axis, optimize_axis_labels, optimize_category_axis
|
|
9
|
+
from .plots import create_plot_element, add_axis_refs, add_plot_categories
|
|
10
|
+
from .series import add_series_to_plot
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
'extract_axis_ids',
|
|
14
|
+
'create_value_axis',
|
|
15
|
+
'optimize_axis_labels',
|
|
16
|
+
'optimize_category_axis',
|
|
17
|
+
'create_plot_element',
|
|
18
|
+
'add_axis_refs',
|
|
19
|
+
'add_plot_categories',
|
|
20
|
+
'add_series_to_plot',
|
|
21
|
+
]
|
|
22
|
+
|
ablechart/oxml/axes.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""
|
|
2
|
+
坐标轴 XML 操作模块
|
|
3
|
+
|
|
4
|
+
负责创建、提取和优化坐标轴元素。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from lxml import etree
|
|
8
|
+
from typing import Tuple
|
|
9
|
+
|
|
10
|
+
from .._log import debug_print as print
|
|
11
|
+
from ..oxml_ns import NAMESPACES
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def extract_axis_ids(plotArea) -> Tuple[int, int]:
|
|
15
|
+
"""
|
|
16
|
+
提取现有坐标轴的 ID
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
plotArea: 绘图区元素 (lxml Element)
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
(cat_ax_id, val_ax_id) 元组
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
ValueError: 如果无法找到坐标轴 ID
|
|
26
|
+
"""
|
|
27
|
+
# python-pptx 的 BaseOxmlElement.xpath() 已经注册了命名空间
|
|
28
|
+
cat_ax_elements = plotArea.xpath('.//c:catAx/c:axId')
|
|
29
|
+
val_ax_elements = plotArea.xpath('.//c:valAx/c:axId')
|
|
30
|
+
|
|
31
|
+
if cat_ax_elements and val_ax_elements:
|
|
32
|
+
cat_ax_id = int(cat_ax_elements[0].get('val'))
|
|
33
|
+
val_ax_id = int(val_ax_elements[0].get('val'))
|
|
34
|
+
return cat_ax_id, val_ax_id
|
|
35
|
+
|
|
36
|
+
if len(val_ax_elements) >= 2:
|
|
37
|
+
x_axis_id = int(val_ax_elements[0].get('val'))
|
|
38
|
+
y_axis_id = int(val_ax_elements[1].get('val'))
|
|
39
|
+
return x_axis_id, y_axis_id
|
|
40
|
+
|
|
41
|
+
raise ValueError("无法找到现有坐标轴 ID")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def create_value_axis(
|
|
45
|
+
plotArea,
|
|
46
|
+
ax_id: int,
|
|
47
|
+
cross_ax_id: int,
|
|
48
|
+
position: str = 'r',
|
|
49
|
+
tick_label_position: str = 'high',
|
|
50
|
+
crosses_at: str = 'max',
|
|
51
|
+
) -> int:
|
|
52
|
+
"""
|
|
53
|
+
创建一个新的值轴 (Y轴)
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
plotArea: 绘图区元素 (lxml Element)
|
|
57
|
+
ax_id: 新轴的 ID
|
|
58
|
+
cross_ax_id: 交叉轴的 ID(通常是分类轴)
|
|
59
|
+
position: 轴位置 ('l'=左, 'r'=右, 't'=顶, 'b'=底)
|
|
60
|
+
tick_label_position: 标签位置 ('low'=左/底, 'high'=右/顶, 'nextTo'=靠近轴)
|
|
61
|
+
crosses_at: 交叉位置 ('min'=最小值/左边, 'max'=最大值/右边)
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
创建的轴 ID
|
|
65
|
+
|
|
66
|
+
Notes:
|
|
67
|
+
- 严格按照 OOXML 规范 (ISO/IEC 29500-1:2016) 的元素顺序
|
|
68
|
+
- 'low' 和 'high' 用于双轴图,确保标签不重叠
|
|
69
|
+
- crosses_at='min' 让轴线在图表左边,'max' 让轴线在图表右边
|
|
70
|
+
"""
|
|
71
|
+
# 创建值轴元素
|
|
72
|
+
valAx = etree.SubElement(plotArea, f"{{{NAMESPACES['c']}}}valAx")
|
|
73
|
+
|
|
74
|
+
# ⭐ 严格按照 OOXML 规范顺序添加子元素
|
|
75
|
+
|
|
76
|
+
# 1. axId (轴 ID) - 必需
|
|
77
|
+
axId_elem = etree.SubElement(valAx, f"{{{NAMESPACES['c']}}}axId")
|
|
78
|
+
axId_elem.set('val', str(ax_id))
|
|
79
|
+
|
|
80
|
+
# 2. scaling (缩放) - 必需
|
|
81
|
+
scaling = etree.SubElement(valAx, f"{{{NAMESPACES['c']}}}scaling")
|
|
82
|
+
orientation = etree.SubElement(scaling, f"{{{NAMESPACES['c']}}}orientation")
|
|
83
|
+
orientation.set('val', 'minMax')
|
|
84
|
+
|
|
85
|
+
# 3. delete (是否隐藏) - 必需
|
|
86
|
+
delete = etree.SubElement(valAx, f"{{{NAMESPACES['c']}}}delete")
|
|
87
|
+
delete.set('val', '0')
|
|
88
|
+
|
|
89
|
+
# 4. axPos (轴位置) - 必需
|
|
90
|
+
axPos = etree.SubElement(valAx, f"{{{NAMESPACES['c']}}}axPos")
|
|
91
|
+
axPos.set('val', position)
|
|
92
|
+
|
|
93
|
+
# 5. majorGridlines (主网格线) - 可选
|
|
94
|
+
# 次轴通常不显示网格线,避免与主轴重叠
|
|
95
|
+
# 如果需要,调用方可以手动添加
|
|
96
|
+
|
|
97
|
+
# 6. numFmt (数字格式) - 可选
|
|
98
|
+
numFmt = etree.SubElement(valAx, f"{{{NAMESPACES['c']}}}numFmt")
|
|
99
|
+
numFmt.set('formatCode', 'General')
|
|
100
|
+
numFmt.set('sourceLinked', '0')
|
|
101
|
+
|
|
102
|
+
# 7. majorTickMark (主刻度线) - 可选
|
|
103
|
+
majorTickMark = etree.SubElement(valAx, f"{{{NAMESPACES['c']}}}majorTickMark")
|
|
104
|
+
majorTickMark.set('val', 'out')
|
|
105
|
+
|
|
106
|
+
# 8. minorTickMark (次刻度线) - 可选
|
|
107
|
+
minorTickMark = etree.SubElement(valAx, f"{{{NAMESPACES['c']}}}minorTickMark")
|
|
108
|
+
minorTickMark.set('val', 'none')
|
|
109
|
+
|
|
110
|
+
# 9. tickLblPos (标签位置) - 可选
|
|
111
|
+
tickLblPos = etree.SubElement(valAx, f"{{{NAMESPACES['c']}}}tickLblPos")
|
|
112
|
+
tickLblPos.set('val', tick_label_position)
|
|
113
|
+
|
|
114
|
+
# 10. crossAx (交叉轴 ID) - 必需
|
|
115
|
+
crossAx = etree.SubElement(valAx, f"{{{NAMESPACES['c']}}}crossAx")
|
|
116
|
+
crossAx.set('val', str(cross_ax_id))
|
|
117
|
+
|
|
118
|
+
# 11. crosses (交叉方式) - 可选
|
|
119
|
+
# ⭐ 关键修复:根据 crosses_at 参数决定交叉位置
|
|
120
|
+
# 'min' = 在最小值(左边)交叉,'max' = 在最大值(右边)交叉
|
|
121
|
+
crosses = etree.SubElement(valAx, f"{{{NAMESPACES['c']}}}crosses")
|
|
122
|
+
crosses.set('val', crosses_at)
|
|
123
|
+
|
|
124
|
+
# 12. crossBetween (交叉位置) - 可选
|
|
125
|
+
crossBetween = etree.SubElement(valAx, f"{{{NAMESPACES['c']}}}crossBetween")
|
|
126
|
+
crossBetween.set('val', 'between')
|
|
127
|
+
|
|
128
|
+
return ax_id
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def optimize_axis_labels(
|
|
132
|
+
plotArea,
|
|
133
|
+
ax_id: int,
|
|
134
|
+
tick_label_position: str = 'low',
|
|
135
|
+
crosses_at: str = 'min',
|
|
136
|
+
remove_gridlines: bool = True,
|
|
137
|
+
):
|
|
138
|
+
"""
|
|
139
|
+
优化现有轴的标签位置和交叉位置
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
plotArea: 绘图区元素
|
|
143
|
+
ax_id: 要优化的轴 ID
|
|
144
|
+
tick_label_position: 标签位置 ('low'=左/底, 'high'=右/顶)
|
|
145
|
+
crosses_at: 交叉位置 ('min'=最小值/左边, 'max'=最大值/右边)
|
|
146
|
+
remove_gridlines: 是否移除网格线(默认移除)
|
|
147
|
+
|
|
148
|
+
Notes:
|
|
149
|
+
主要用于优化主值轴,使其与次值轴协调
|
|
150
|
+
crosses_at='min' 让主轴线在图表左边,'max' 让次轴线在图表右边
|
|
151
|
+
"""
|
|
152
|
+
# 查找指定的值轴
|
|
153
|
+
val_ax_elements = plotArea.xpath(f'.//c:valAx[c:axId[@val="{ax_id}"]]')
|
|
154
|
+
if not val_ax_elements:
|
|
155
|
+
return # 轴不存在,跳过
|
|
156
|
+
|
|
157
|
+
val_ax = val_ax_elements[0]
|
|
158
|
+
|
|
159
|
+
# ⭐ 设置 crosses 位置
|
|
160
|
+
crosses_elements = val_ax.xpath('./c:crosses')
|
|
161
|
+
if crosses_elements:
|
|
162
|
+
crosses_elements[0].set('val', crosses_at)
|
|
163
|
+
|
|
164
|
+
# 设置标签位置
|
|
165
|
+
tickLblPos_elements = val_ax.xpath('./c:tickLblPos')
|
|
166
|
+
if tickLblPos_elements:
|
|
167
|
+
tickLblPos_elements[0].set('val', tick_label_position)
|
|
168
|
+
else:
|
|
169
|
+
# 如果不存在,创建一个
|
|
170
|
+
tickLblPos = etree.Element(f"{{{NAMESPACES['c']}}}tickLblPos")
|
|
171
|
+
tickLblPos.set('val', tick_label_position)
|
|
172
|
+
# 插入到 crossAx 之前(保持正确顺序)
|
|
173
|
+
cross_ax_elements = val_ax.xpath('./c:crossAx')
|
|
174
|
+
if cross_ax_elements:
|
|
175
|
+
cross_ax_elements[0].addprevious(tickLblPos)
|
|
176
|
+
|
|
177
|
+
# ⭐ 移除网格线(取消内部横框)
|
|
178
|
+
if remove_gridlines:
|
|
179
|
+
gridlines = val_ax.xpath('./c:majorGridlines')
|
|
180
|
+
for gridline in gridlines:
|
|
181
|
+
val_ax.remove(gridline)
|
|
182
|
+
print(f" → 已移除主值轴网格线")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def optimize_category_axis(
|
|
186
|
+
plotArea,
|
|
187
|
+
cat_ax_id: int,
|
|
188
|
+
remove_tick_marks: bool = True,
|
|
189
|
+
):
|
|
190
|
+
"""
|
|
191
|
+
优化分类轴(X轴)的显示
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
plotArea: 绘图区元素
|
|
195
|
+
cat_ax_id: 分类轴 ID
|
|
196
|
+
remove_tick_marks: 是否移除主刻度线(默认移除,即取消日期间的小竖线)
|
|
197
|
+
|
|
198
|
+
Notes:
|
|
199
|
+
用于清理分类轴的视觉元素,让图表更简洁
|
|
200
|
+
"""
|
|
201
|
+
# 查找分类轴
|
|
202
|
+
cat_ax_elements = plotArea.xpath(f'.//c:catAx[c:axId[@val="{cat_ax_id}"]]')
|
|
203
|
+
if not cat_ax_elements:
|
|
204
|
+
return # 轴不存在,跳过
|
|
205
|
+
|
|
206
|
+
cat_ax = cat_ax_elements[0]
|
|
207
|
+
|
|
208
|
+
# ⭐ 移除或设置主刻度线为 'none'(取消日期间的小竖线)
|
|
209
|
+
if remove_tick_marks:
|
|
210
|
+
majorTickMark_elements = cat_ax.xpath('./c:majorTickMark')
|
|
211
|
+
if majorTickMark_elements:
|
|
212
|
+
# 修改为 'none' 而不是删除元素
|
|
213
|
+
majorTickMark_elements[0].set('val', 'none')
|
|
214
|
+
print(f" → 已移除分类轴主刻度线(日期间的小竖线)")
|
|
215
|
+
|
|
216
|
+
# 同时也设置次刻度线为 'none'
|
|
217
|
+
minorTickMark_elements = cat_ax.xpath('./c:minorTickMark')
|
|
218
|
+
if minorTickMark_elements:
|
|
219
|
+
minorTickMark_elements[0].set('val', 'none')
|