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>
228 lines
8.5 KiB
Python
228 lines
8.5 KiB
Python
"""
|
||
绘图区 (Plot) XML 操作模块
|
||
|
||
负责创建不同类型的图表绘图区 (<c:barChart>, <c:lineChart> 等)。
|
||
"""
|
||
|
||
from lxml import etree
|
||
from typing import Literal
|
||
|
||
from ..oxml_ns import NAMESPACES
|
||
|
||
ChartType = Literal['bar', 'column', 'line', 'area', 'scatter']
|
||
|
||
|
||
def create_plot_element(
|
||
plotArea,
|
||
chart_type: ChartType,
|
||
cat_ax_id: int,
|
||
val_ax_id: int,
|
||
order_index: int = 0,
|
||
):
|
||
"""
|
||
创建图表绘图区元素
|
||
|
||
Args:
|
||
plotArea: 父绘图区元素
|
||
chart_type: 图表类型 ('bar', 'line', 'area', 'scatter')
|
||
cat_ax_id: 分类轴 ID
|
||
val_ax_id: 值轴 ID
|
||
order_index: 绘图顺序索引(0=最底层,越大越在上层)
|
||
|
||
Returns:
|
||
创建的绘图元素 (lxml Element)
|
||
|
||
Raises:
|
||
ValueError: 如果图表类型不支持
|
||
|
||
Notes:
|
||
- 每个绘图元素会自动关联指定的坐标轴
|
||
- 调用方需要自己添加系列 (<c:ser>)
|
||
- order_index 决定图表的堆叠顺序
|
||
"""
|
||
chart_type = chart_type.lower()
|
||
|
||
if chart_type in ('bar', 'column'):
|
||
return _create_bar_plot(plotArea, cat_ax_id, val_ax_id, order_index)
|
||
elif chart_type == 'line':
|
||
return _create_line_plot(plotArea, cat_ax_id, val_ax_id, order_index)
|
||
elif chart_type == 'area':
|
||
return _create_area_plot(plotArea, cat_ax_id, val_ax_id, order_index)
|
||
elif chart_type == 'scatter':
|
||
return _create_scatter_plot(plotArea, cat_ax_id, val_ax_id, order_index)
|
||
else:
|
||
raise ValueError(f"不支持的图表类型: {chart_type}")
|
||
|
||
|
||
def _create_bar_plot(plotArea, cat_ax_id: int, val_ax_id: int, order_index: int):
|
||
"""创建柱状图元素"""
|
||
barChart = etree.SubElement(plotArea, f"{{{NAMESPACES['c']}}}barChart")
|
||
|
||
# barDir: 柱状图方向 ('col' = 垂直柱状, 'bar' = 水平条形)
|
||
barDir = etree.SubElement(barChart, f"{{{NAMESPACES['c']}}}barDir")
|
||
barDir.set('val', 'col')
|
||
|
||
# grouping: 分组方式 ('clustered' = 簇状, 'stacked' = 堆叠)
|
||
grouping = etree.SubElement(barChart, f"{{{NAMESPACES['c']}}}grouping")
|
||
grouping.set('val', 'clustered')
|
||
|
||
# varyColors: 是否每个系列使用不同颜色
|
||
varyColors = etree.SubElement(barChart, f"{{{NAMESPACES['c']}}}varyColors")
|
||
varyColors.set('val', '0')
|
||
|
||
# ⭐ 绘图顺序(决定堆叠层次,数字越小越在底层)
|
||
# OOXML 规范建议在 varyColors 之后添加
|
||
# 注意:这里不是 <c:ser> 的 order,而是整个 plot 的渲染顺序
|
||
# 但 PowerPoint 实际使用 XML 元素出现的顺序来决定堆叠
|
||
# 所以这个标签主要是语义化,真正的顺序由 XML 元素在 plotArea 中的位置决定
|
||
|
||
# ⚠️ 注意:不在这里添加轴引用!
|
||
# 轴引用应该在所有系列之后添加,由调用方在添加完系列后调用 add_axis_refs()
|
||
|
||
return barChart
|
||
|
||
|
||
def _create_line_plot(plotArea, cat_ax_id: int, val_ax_id: int, order_index: int):
|
||
"""创建折线图元素"""
|
||
lineChart = etree.SubElement(plotArea, f"{{{NAMESPACES['c']}}}lineChart")
|
||
|
||
# grouping: 分组方式 ('standard' = 标准)
|
||
grouping = etree.SubElement(lineChart, f"{{{NAMESPACES['c']}}}grouping")
|
||
grouping.set('val', 'standard')
|
||
|
||
# varyColors: 是否每个系列使用不同颜色
|
||
varyColors = etree.SubElement(lineChart, f"{{{NAMESPACES['c']}}}varyColors")
|
||
varyColors.set('val', '0')
|
||
|
||
# ⚠️ 注意:不在这里添加轴引用!
|
||
# 轴引用应该在所有系列之后添加
|
||
|
||
return lineChart
|
||
|
||
|
||
def _create_area_plot(plotArea, cat_ax_id: int, val_ax_id: int, order_index: int):
|
||
"""创建面积图元素"""
|
||
areaChart = etree.SubElement(plotArea, f"{{{NAMESPACES['c']}}}areaChart")
|
||
|
||
# grouping: 分组方式 ('standard' = 标准)
|
||
grouping = etree.SubElement(areaChart, f"{{{NAMESPACES['c']}}}grouping")
|
||
grouping.set('val', 'standard')
|
||
|
||
# varyColors
|
||
varyColors = etree.SubElement(areaChart, f"{{{NAMESPACES['c']}}}varyColors")
|
||
varyColors.set('val', '0')
|
||
|
||
# ⚠️ 注意:不在这里添加轴引用!
|
||
|
||
return areaChart
|
||
|
||
|
||
def _create_scatter_plot(plotArea, cat_ax_id: int, val_ax_id: int, order_index: int):
|
||
"""创建散点图元素"""
|
||
scatterChart = etree.SubElement(plotArea, f"{{{NAMESPACES['c']}}}scatterChart")
|
||
|
||
# scatterStyle: 散点样式 ('lineMarker' = 带线和标记)
|
||
scatterStyle = etree.SubElement(scatterChart, f"{{{NAMESPACES['c']}}}scatterStyle")
|
||
scatterStyle.set('val', 'lineMarker')
|
||
|
||
# varyColors
|
||
varyColors = etree.SubElement(scatterChart, f"{{{NAMESPACES['c']}}}varyColors")
|
||
varyColors.set('val', '0')
|
||
|
||
# ⚠️ 注意:不在这里添加轴引用!
|
||
|
||
return scatterChart
|
||
|
||
|
||
def add_axis_refs(plot_element, cat_ax_id: int, val_ax_id: int):
|
||
"""
|
||
为绘图元素添加坐标轴引用(应该在所有系列之后调用)
|
||
|
||
Args:
|
||
plot_element: 绘图元素 (<c:barChart>, <c:lineChart> 等)
|
||
cat_ax_id: 分类轴 ID
|
||
val_ax_id: 值轴 ID
|
||
"""
|
||
axId1 = etree.SubElement(plot_element, f"{{{NAMESPACES['c']}}}axId")
|
||
axId1.set('val', str(cat_ax_id))
|
||
|
||
axId2 = etree.SubElement(plot_element, f"{{{NAMESPACES['c']}}}axId")
|
||
axId2.set('val', str(val_ax_id))
|
||
|
||
|
||
def add_plot_categories(plot_element, categories: list):
|
||
"""
|
||
为绘图元素添加共享的分类数据(在所有系列之前调用)
|
||
|
||
Args:
|
||
plot_element: 绘图元素 (<c:barChart>, <c:lineChart> 等)
|
||
categories: 分类列表(X轴数据)
|
||
|
||
Notes:
|
||
- 在 OOXML 规范中,<c:cat> 是图表级别的共享元素
|
||
- 应该在添加任何 <c:ser> 系列之前调用
|
||
- 所有系列共享同一组分类数据
|
||
- ⭐ 自动检测日期类型,使用 numCache(数值缓存)而非 strCache
|
||
"""
|
||
from datetime import datetime
|
||
|
||
# ⭐ 检测是否为日期类型
|
||
is_date_data = False
|
||
if categories and isinstance(categories[0], (datetime, float)):
|
||
# datetime 对象或浮点数(Excel 日期序列号)
|
||
is_date_data = True
|
||
|
||
cat = etree.SubElement(plot_element, f"{{{NAMESPACES['c']}}}cat")
|
||
|
||
if is_date_data:
|
||
# ⭐ 新方案:将日期格式化为字符串,使用 strRef + strCache
|
||
# 这样 PowerPoint 就会将其作为文本标签显示,不会出现 1900 年问题
|
||
strRef = etree.SubElement(cat, f"{{{NAMESPACES['c']}}}strRef")
|
||
|
||
# f (公式引用)
|
||
f_elem = etree.SubElement(strRef, f"{{{NAMESPACES['c']}}}f")
|
||
f_elem.text = f"Sheet1!$A$2:$A${len(categories) + 1}"
|
||
|
||
# strCache (字符串缓存)
|
||
strCache = etree.SubElement(strRef, f"{{{NAMESPACES['c']}}}strCache")
|
||
ptCount = etree.SubElement(strCache, f"{{{NAMESPACES['c']}}}ptCount")
|
||
ptCount.set('val', str(len(categories)))
|
||
|
||
# 添加每个分类点(格式化为字符串)
|
||
for i, cat_value in enumerate(categories):
|
||
pt = etree.SubElement(strCache, f"{{{NAMESPACES['c']}}}pt")
|
||
pt.set('idx', str(i))
|
||
v = etree.SubElement(pt, f"{{{NAMESPACES['c']}}}v")
|
||
|
||
if isinstance(cat_value, datetime):
|
||
# 格式化为 "yyyy/mm"(年份/月份)
|
||
v.text = cat_value.strftime('%Y/%m')
|
||
elif isinstance(cat_value, float):
|
||
# 假设是 Excel 日期序列号,转换为日期字符串
|
||
base_date = datetime(1899, 12, 30)
|
||
from datetime import timedelta
|
||
actual_date = base_date + timedelta(days=cat_value)
|
||
v.text = actual_date.strftime('%Y/%m')
|
||
else:
|
||
v.text = str(cat_value)
|
||
else:
|
||
# ⭐ 使用 strRef + strCache(普通分类轴)
|
||
strRef = etree.SubElement(cat, f"{{{NAMESPACES['c']}}}strRef")
|
||
|
||
# f (公式引用)
|
||
f_elem = etree.SubElement(strRef, f"{{{NAMESPACES['c']}}}f")
|
||
f_elem.text = f"Sheet1!$A$2:$A${len(categories) + 1}"
|
||
|
||
# strCache (字符串缓存)
|
||
strCache = etree.SubElement(strRef, f"{{{NAMESPACES['c']}}}strCache")
|
||
ptCount = etree.SubElement(strCache, f"{{{NAMESPACES['c']}}}ptCount")
|
||
ptCount.set('val', str(len(categories)))
|
||
|
||
# 添加每个分类点
|
||
for i, cat_value in enumerate(categories):
|
||
pt = etree.SubElement(strCache, f"{{{NAMESPACES['c']}}}pt")
|
||
pt.set('idx', str(i))
|
||
v = etree.SubElement(pt, f"{{{NAMESPACES['c']}}}v")
|
||
v.text = str(cat_value)
|
||
|