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

365 lines
12 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.

"""
公共 API - 简洁的高层接口
这是用户(您的工作室同事)唯一需要导入的模块。
隐藏所有实现细节。
"""
from typing import List, Dict, Optional
from pptx.slide import Slide
from pptx.enum.chart import XL_CHART_TYPE
from pptx.chart.data import CategoryChartData
from pptx.util import Inches
import pandas as pd
from .builder import ChartBuilder
# 导入样式模块
try:
from .styles import StyleConfig, DEFAULT_STYLE_CONFIG
except ImportError:
StyleConfig = None
DEFAULT_STYLE_CONFIG = None
# 导入布局模块
try:
from .layout import ChartLayoutConfig
except ImportError:
ChartLayoutConfig = None
def create_combo_chart(
slide: Slide,
df: pd.DataFrame,
categories_col: str,
series_config: List[Dict],
position: tuple = (Inches(1), Inches(2)),
size: tuple = (Inches(8), Inches(4.5)),
style_config=None,
layout_config=None,
):
"""
创建组合图(支持 P(n,2) 任意组合)
Args:
slide: 幻灯片对象
df: 数据 DataFrame
categories_col: 分类列名X 轴)
series_config: 系列配置列表
[
{"key": "销售额", "name": "销售额", "type": "bar", "axis": "primary"},
{"key": "增长率", "name": "增长率", "type": "line", "axis": "secondary"},
{"key": "市场份额", "name": "市场份额", "type": "line", "axis": "secondary"},
]
position: 图表位置 (left, top)
size: 图表大小 (width, height)
style_config: 样式配置对象(可选,默认使用 DEFAULT_STYLE_CONFIG
可以是 StyleConfig 实例,或 None 使用默认样式
layout_config: 布局配置对象(可选)
可以是 ChartLayoutConfig 实例,包含图例、轴配置
Returns:
Chart 对象
Examples:
>>> # 示例 1: 使用默认样式和布局
>>> chart = create_combo_chart(
... slide=slide,
... df=df,
... categories_col="日期",
... series_config=[
... {"key": "销售额", "name": "销售额", "type": "bar", "axis": "primary"},
... {"key": "增长率", "name": "增长率", "type": "line", "axis": "secondary"},
... ]
... )
>>> # 示例 2: 自定义样式 + 布局
>>> from ppt_station.chart_builder.styles import StyleConfig
>>> from ppt_station.chart_builder.layout import (
... ChartLayoutConfig,
... LegendConfig,
... CategoryAxisConfig,
... )
>>>
>>> # 样式配置
>>> custom_style = StyleConfig(
... color_scheme="dark_only",
... line_width_pt=1.5,
... marker_style="none",
... )
>>>
>>> # 布局配置
>>> custom_layout = ChartLayoutConfig(
... legend_config=LegendConfig(position="bottom", font_size_pt=10),
... category_axis_config=CategoryAxisConfig(
... is_date_axis=True,
... major_unit_days=7, # 每周显示一个刻度
... number_format="yyyy-mm-dd",
... ),
... )
>>>
>>> chart = create_combo_chart(
... slide=slide,
... df=df,
... categories_col="日期",
... series_config=[...],
... style_config=custom_style,
... layout_config=custom_layout
... )
Supported Combinations:
- type: 'bar', 'column', 'line', 'area' (散点图待实现)
- axis: 'primary', 'secondary'
- 任意 (type1, axis1) + (type2, axis2) 的组合
- 支持主轴多种类型,次轴多种类型
Notes:
- 分类X轴在 Excel 中占用 A 列
- 系列数据从 B 列开始
- 支持超过 25 个系列AA, AB, ...
- 左轴标签在左侧,右轴标签在右侧,不会重叠
- 默认样式无标记点、1pt 线宽、深浅色交替
- 默认布局:图例在底部、横轴普通分类轴
"""
if not series_config:
raise ValueError("series_config 不能为空")
# 1. 按 (type, axis) 分组(用于决定引导图表类型)
plot_groups = _group_series(series_config)
# 2. 创建引导图表(写入全部系列数据到嵌入 Excel
chart = _bootstrap_chart(
slide, df, categories_col, series_config, position, size
)
# ⭐ 核心修复:修正嵌入的 Excel 工作表中的日期数据
# 如果分类列是日期类型,需要将 Excel 工作表中的文本日期转换为真实的日期数值
_fix_embedded_excel_dates(chart, df, categories_col)
# 3. 使用构建器完成剩余工作(传递样式配置和布局配置)
builder = ChartBuilder(
chart,
df,
categories_col,
style_config=style_config if style_config is not None else DEFAULT_STYLE_CONFIG,
layout_config=layout_config
)
# 注意:引导图表已经创建了第一个系列,构建器会继续追加
# 如果需要完全自定义,可以在 builder.clear_bootstrap_chart() 中清理
return builder.build(series_config)
def _bootstrap_chart(
slide: Slide,
df: pd.DataFrame,
categories_col: str,
series_config: List[Dict],
position: tuple,
size: tuple,
):
"""
创建引导图表(写入全部系列数据到嵌入 Excel
用途:
- 激活 <c:plotArea>,使其可以通过 XML 访问
- 创建初始的分类轴和值轴
- 将所有系列数据写入嵌入 Excel确保"编辑数据"不丢数据)
- 图表 XML 结构后续由 ChartBuilder 重建
Args:
slide: 幻灯片对象
df: 数据 DataFrame
categories_col: 分类列名
series_config: 全部系列配置列表
position: (left, top)
size: (width, height)
Returns:
Chart 对象
"""
chart_data = CategoryChartData()
# 设置分类X轴
categories = df[categories_col].tolist()
if pd.api.types.is_datetime64_any_dtype(df[categories_col]):
categories_bootstrap = [cat.strftime("%Y-%m-%d") if hasattr(cat, 'strftime') else str(cat) for cat in categories]
else:
categories_bootstrap = categories
chart_data.categories = categories_bootstrap
# 添加全部系列数据(确保嵌入 Excel 包含所有列)
for series_cfg in series_config:
chart_data.add_series(
series_cfg["name"],
df[series_cfg["key"]].tolist()
)
# 使用第一个系列的类型决定引导图表类型
chart_type = _get_chart_type(series_config[0].get("type", "bar"))
# 创建图表
left, top = position
width, height = size
graphic_frame = slide.shapes.add_chart(
chart_type, left, top, width, height, chart_data
)
return graphic_frame.chart
def _get_chart_type(type_str: str) -> XL_CHART_TYPE:
"""将图表类型字符串转换为 XL_CHART_TYPE 枚举"""
type_map = {
"bar": XL_CHART_TYPE.COLUMN_CLUSTERED,
"column": XL_CHART_TYPE.COLUMN_CLUSTERED,
"line": XL_CHART_TYPE.LINE,
"area": XL_CHART_TYPE.AREA,
}
return type_map.get(type_str.lower(), XL_CHART_TYPE.COLUMN_CLUSTERED)
def _group_series(series_config: List[Dict]) -> Dict[tuple, List[Dict]]:
"""按 (type, axis) 分组系列"""
from collections import defaultdict
groups = defaultdict(list)
for cfg in series_config:
key = (cfg.get("type", "bar"), cfg.get("axis", "primary"))
groups[key].append(cfg)
return dict(groups)
def _fix_embedded_excel_dates(chart, df: pd.DataFrame, categories_col: str):
"""
修正嵌入的 Excel 工作表中的日期数据
新方案:将日期格式化为字符串标签(如 "2024/01"
这样 PowerPoint 就会正确显示,而不会出现 1900 年问题
Args:
chart: python-pptx Chart 对象
df: 数据 DataFrame
categories_col: 分类列名
"""
# 检查是否为日期类型
if not pd.api.types.is_datetime64_any_dtype(df[categories_col]):
print(f" → 分类列不是日期类型,跳过 Excel 工作表修正")
return # 不是日期类型,无需修正
print(f"\n🔧 修正嵌入的 Excel 工作表日期数据(转换为格式化字符串)...")
try:
from datetime import datetime
from openpyxl import load_workbook
import io
# 获取嵌入的 Excel 数据
chart_part = chart.part
xlsx_part = chart_part.chart_workbook.xlsx_part
print(f" → 找到嵌入的 Excel 工作表")
# 将 Excel blob 加载为 openpyxl workbook
xlsx_stream = io.BytesIO(xlsx_part.blob)
wb = load_workbook(xlsx_stream)
ws = wb.active
print(f" → 工作表行数: {ws.max_row}, 列数: {ws.max_column}")
# 获取日期数据
categories = df[categories_col].tolist()
print(f" → 准备修正 {len(categories)} 个日期值")
print(f" → 第一个值: {categories[0]} (类型: {type(categories[0])})")
# 修正 A 列(分类列)的数据 - 转换为格式化字符串
# Excel 工作表的第一行是表头,数据从第二行开始
fixed_count = 0
for i, cat_value in enumerate(categories, start=2):
if hasattr(cat_value, 'to_pydatetime'):
cat_value = cat_value.to_pydatetime()
if isinstance(cat_value, datetime):
# ⭐ 将日期格式化为字符串 "yyyy/mm"(年份/月份)
date_str = cat_value.strftime('%Y/%m')
ws.cell(row=i, column=1).value = date_str
# 不设置数字格式,保持为文本
fixed_count += 1
print(f" → 已修正 {fixed_count} 个单元格")
print(f" → 示例:{categories[0].strftime('%Y/%m') if isinstance(categories[0], datetime) or hasattr(categories[0], 'strftime') else 'N/A'}")
# 将修改后的 workbook 写回 blob
output_stream = io.BytesIO()
wb.save(output_stream)
xlsx_part._blob = output_stream.getvalue()
print(f" ✅ 嵌入 Excel 工作表修正完成({fixed_count} 个日期值转换为格式化字符串)")
except Exception as e:
print(f" ⚠️ 修正嵌入 Excel 工作表失败: {e}")
import traceback
traceback.print_exc()
# ============================================================================
# 便捷函数:向后兼容
# ============================================================================
def create_dual_axis_chart(
slide: Slide,
df: pd.DataFrame,
categories_col: str,
bar_columns: List[str],
bar_names: List[str],
line_columns: List[str],
line_names: List[str],
position: tuple = (Inches(1), Inches(2)),
size: tuple = (Inches(8), Inches(4.5)),
):
"""
便捷函数:创建双轴组合图(柱状图 + 折线图)
这是向后兼容的 API与旧的 xml_chart_patcher 接口一致。
Example:
>>> create_dual_axis_chart(
... slide=slide,
... df=df,
... categories_col="日期",
... bar_columns=["销售额", "成本"],
... bar_names=["销售额", "成本"],
... line_columns=["利润率"],
... line_names=["利润率"],
... )
"""
# 构建统一的 series_config
series_config = []
# 主轴柱状图
for col, name in zip(bar_columns, bar_names):
series_config.append({
"key": col,
"name": name,
"type": "bar",
"axis": "primary"
})
# 次轴折线图
for col, name in zip(line_columns, line_names):
series_config.append({
"key": col,
"name": name,
"type": "line",
"axis": "secondary"
})
# 调用统一的 API
return create_combo_chart(
slide, df, categories_col, series_config, position, size
)