skills/ppt-station-skill/ppt_station/chart_builder/oxml/plots.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

228 lines
8.5 KiB
Python
Raw 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.

"""
绘图区 (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)