skills/ppt-station-skill/ppt_station/chart_builder/oxml/axes.py
wangyitong a65adcc2e5 Initial commit: merged, deduplicated, and vetted skill collection
Sources: extracted from two upstream archives (skill-repo, skills-main),
merged with the following policy:

- 15 broken symlinks (pointing to /Users/jameslee/.cc-switch/skills or
  ../../.agents/skills on a foreign machine) discarded
- 3 real name collisions with identical content (ai-pair, ifind-http-api,
  zhipu-websearch) kept as one copy
- Functional overlaps deduped keeping the strongest variant:
  - docx family: kept docx (official, full toolchain) + docx-cn
    (GB/T 9704 Chinese official-document constants),
    dropped docx_writer (no scripts, name collided with docx)
  - humanizer family: kept humanizer-zh (6 zh reference docs),
    dropped humanizer (en, redundant for CN workflow)
- Skills that only ran in a foreign environment removed:
  ablemind-ops, app-publish, hlb-design-system, openclaw-adj-skill,
  claude-driver
- alphapai excluded from this public repo because its SKILL.md hard-coded
  live credentials

Result: 25 skills, 572 files, ~7.5 MB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:47:12 +08:00

216 lines
7.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
坐标轴 XML 操作模块
负责创建、提取和优化坐标轴元素。
"""
from lxml import etree
from typing import Tuple
from ..oxml_ns import NAMESPACES
def extract_axis_ids(plotArea) -> Tuple[int, int]:
"""
提取现有坐标轴的 ID
Args:
plotArea: 绘图区元素 (lxml Element)
Returns:
(cat_ax_id, val_ax_id) 元组
Raises:
ValueError: 如果无法找到坐标轴 ID
"""
# python-pptx 的 BaseOxmlElement.xpath() 已经注册了命名空间
cat_ax_elements = plotArea.xpath('.//c:catAx/c:axId')
val_ax_elements = plotArea.xpath('.//c:valAx/c:axId')
if not cat_ax_elements or not val_ax_elements:
raise ValueError("无法找到现有坐标轴 ID")
cat_ax_id = int(cat_ax_elements[0].get('val'))
val_ax_id = int(val_ax_elements[0].get('val'))
return cat_ax_id, val_ax_id
def create_value_axis(
plotArea,
ax_id: int,
cross_ax_id: int,
position: str = 'r',
tick_label_position: str = 'high',
crosses_at: str = 'max',
) -> int:
"""
创建一个新的值轴 (Y轴)
Args:
plotArea: 绘图区元素 (lxml Element)
ax_id: 新轴的 ID
cross_ax_id: 交叉轴的 ID通常是分类轴
position: 轴位置 ('l'=左, 'r'=右, 't'=顶, 'b'=底)
tick_label_position: 标签位置 ('low'=左/底, 'high'=右/顶, 'nextTo'=靠近轴)
crosses_at: 交叉位置 ('min'=最小值/左边, 'max'=最大值/右边)
Returns:
创建的轴 ID
Notes:
- 严格按照 OOXML 规范 (ISO/IEC 29500-1:2016) 的元素顺序
- 'low''high' 用于双轴图,确保标签不重叠
- crosses_at='min' 让轴线在图表左边,'max' 让轴线在图表右边
"""
# 创建值轴元素
valAx = etree.SubElement(plotArea, f"{{{NAMESPACES['c']}}}valAx")
# ⭐ 严格按照 OOXML 规范顺序添加子元素
# 1. axId (轴 ID) - 必需
axId_elem = etree.SubElement(valAx, f"{{{NAMESPACES['c']}}}axId")
axId_elem.set('val', str(ax_id))
# 2. scaling (缩放) - 必需
scaling = etree.SubElement(valAx, f"{{{NAMESPACES['c']}}}scaling")
orientation = etree.SubElement(scaling, f"{{{NAMESPACES['c']}}}orientation")
orientation.set('val', 'minMax')
# 3. delete (是否隐藏) - 必需
delete = etree.SubElement(valAx, f"{{{NAMESPACES['c']}}}delete")
delete.set('val', '0')
# 4. axPos (轴位置) - 必需
axPos = etree.SubElement(valAx, f"{{{NAMESPACES['c']}}}axPos")
axPos.set('val', position)
# 5. majorGridlines (主网格线) - 可选
# 次轴通常不显示网格线,避免与主轴重叠
# 如果需要,调用方可以手动添加
# 6. numFmt (数字格式) - 可选
numFmt = etree.SubElement(valAx, f"{{{NAMESPACES['c']}}}numFmt")
numFmt.set('formatCode', 'General')
numFmt.set('sourceLinked', '0')
# 7. majorTickMark (主刻度线) - 可选
majorTickMark = etree.SubElement(valAx, f"{{{NAMESPACES['c']}}}majorTickMark")
majorTickMark.set('val', 'out')
# 8. minorTickMark (次刻度线) - 可选
minorTickMark = etree.SubElement(valAx, f"{{{NAMESPACES['c']}}}minorTickMark")
minorTickMark.set('val', 'none')
# 9. tickLblPos (标签位置) - 可选
tickLblPos = etree.SubElement(valAx, f"{{{NAMESPACES['c']}}}tickLblPos")
tickLblPos.set('val', tick_label_position)
# 10. crossAx (交叉轴 ID) - 必需
crossAx = etree.SubElement(valAx, f"{{{NAMESPACES['c']}}}crossAx")
crossAx.set('val', str(cross_ax_id))
# 11. crosses (交叉方式) - 可选
# ⭐ 关键修复:根据 crosses_at 参数决定交叉位置
# 'min' = 在最小值(左边)交叉,'max' = 在最大值(右边)交叉
crosses = etree.SubElement(valAx, f"{{{NAMESPACES['c']}}}crosses")
crosses.set('val', crosses_at)
# 12. crossBetween (交叉位置) - 可选
crossBetween = etree.SubElement(valAx, f"{{{NAMESPACES['c']}}}crossBetween")
crossBetween.set('val', 'between')
return ax_id
def optimize_axis_labels(
plotArea,
ax_id: int,
tick_label_position: str = 'low',
crosses_at: str = 'min',
remove_gridlines: bool = True,
):
"""
优化现有轴的标签位置和交叉位置
Args:
plotArea: 绘图区元素
ax_id: 要优化的轴 ID
tick_label_position: 标签位置 ('low'=左/底, 'high'=右/顶)
crosses_at: 交叉位置 ('min'=最小值/左边, 'max'=最大值/右边)
remove_gridlines: 是否移除网格线(默认移除)
Notes:
主要用于优化主值轴,使其与次值轴协调
crosses_at='min' 让主轴线在图表左边,'max' 让次轴线在图表右边
"""
# 查找指定的值轴
val_ax_elements = plotArea.xpath(f'.//c:valAx[c:axId[@val="{ax_id}"]]')
if not val_ax_elements:
return # 轴不存在,跳过
val_ax = val_ax_elements[0]
# ⭐ 设置 crosses 位置
crosses_elements = val_ax.xpath('./c:crosses')
if crosses_elements:
crosses_elements[0].set('val', crosses_at)
# 设置标签位置
tickLblPos_elements = val_ax.xpath('./c:tickLblPos')
if tickLblPos_elements:
tickLblPos_elements[0].set('val', tick_label_position)
else:
# 如果不存在,创建一个
tickLblPos = etree.Element(f"{{{NAMESPACES['c']}}}tickLblPos")
tickLblPos.set('val', tick_label_position)
# 插入到 crossAx 之前(保持正确顺序)
cross_ax_elements = val_ax.xpath('./c:crossAx')
if cross_ax_elements:
cross_ax_elements[0].addprevious(tickLblPos)
# ⭐ 移除网格线(取消内部横框)
if remove_gridlines:
gridlines = val_ax.xpath('./c:majorGridlines')
for gridline in gridlines:
val_ax.remove(gridline)
print(f" → 已移除主值轴网格线")
def optimize_category_axis(
plotArea,
cat_ax_id: int,
remove_tick_marks: bool = True,
):
"""
优化分类轴X轴的显示
Args:
plotArea: 绘图区元素
cat_ax_id: 分类轴 ID
remove_tick_marks: 是否移除主刻度线(默认移除,即取消日期间的小竖线)
Notes:
用于清理分类轴的视觉元素,让图表更简洁
"""
# 查找分类轴
cat_ax_elements = plotArea.xpath(f'.//c:catAx[c:axId[@val="{cat_ax_id}"]]')
if not cat_ax_elements:
return # 轴不存在,跳过
cat_ax = cat_ax_elements[0]
# ⭐ 移除或设置主刻度线为 'none'(取消日期间的小竖线)
if remove_tick_marks:
majorTickMark_elements = cat_ax.xpath('./c:majorTickMark')
if majorTickMark_elements:
# 修改为 'none' 而不是删除元素
majorTickMark_elements[0].set('val', 'none')
print(f" → 已移除分类轴主刻度线(日期间的小竖线)")
# 同时也设置次刻度线为 'none'
minorTickMark_elements = cat_ax.xpath('./c:minorTickMark')
if minorTickMark_elements:
minorTickMark_elements[0].set('val', 'none')