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>
342 lines
12 KiB
Python
342 lines
12 KiB
Python
"""图表类布局 — 复用 ppt_station ChartBuilder"""
|
||
|
||
from pptx.util import Inches
|
||
from ..helpers import set_slide_bg, add_text, add_rect, add_page_header, setup_content_page
|
||
|
||
|
||
def _add_chart_to_slide(slide, data, position, size, theme=None):
|
||
"""调用 ppt_station 的 create_combo_chart,自动关联主题配色"""
|
||
from ppt_station.chart_builder.api import create_combo_chart
|
||
from ppt_station.chart_builder.styles import StyleConfig
|
||
|
||
# 如果 data 没有显式 style_config,从 theme 自动构建
|
||
style_config = data.get("style_config")
|
||
if style_config is None and theme is not None:
|
||
scheme = theme.get("chart_default_scheme", "default")
|
||
style_config = StyleConfig(
|
||
color_scheme=scheme,
|
||
line_width_pt=1.5,
|
||
marker_style="none",
|
||
)
|
||
|
||
create_combo_chart(
|
||
slide=slide,
|
||
df=data["df"],
|
||
categories_col=data["categories_col"],
|
||
series_config=data["series_config"],
|
||
position=position,
|
||
size=size,
|
||
style_config=style_config,
|
||
layout_config=data.get("layout_config"),
|
||
)
|
||
|
||
|
||
def layout_chart_full(slide, data, theme):
|
||
"""全幅图表页 — 标题 + 占满的图表
|
||
|
||
data keys:
|
||
title (str): 页面标题
|
||
subtitle (str, optional): 副标题
|
||
df (DataFrame): 图表数据
|
||
categories_col (str): 分类列名
|
||
series_config (list): 系列配置
|
||
style_config (StyleConfig, optional): 样式
|
||
layout_config (ChartLayoutConfig, optional): 布局
|
||
insight (str, optional): 结论先行文本(2-3行,副标题与图表之间)
|
||
footnote (str, optional): 数据来源/脚注(auto-footer 渲染)
|
||
"""
|
||
m, sw, content_y, content_w, footer_y = setup_content_page(slide, data, theme)
|
||
|
||
subtitle_h = 0
|
||
if data.get("subtitle"):
|
||
chart_sub_size = theme.get("chart_subtitle_size", 11)
|
||
add_text(slide, data["subtitle"],
|
||
x=m, y=content_y - 0.05, w=content_w, h=0.3,
|
||
font_size=chart_sub_size, color=theme["text_muted"],
|
||
font_name=theme["body_font"])
|
||
subtitle_h = 0.35
|
||
|
||
# insight 插槽
|
||
insight_h = 0
|
||
if data.get("insight"):
|
||
add_text(slide, data["insight"],
|
||
x=m, y=content_y + subtitle_h, w=content_w, h=0.50,
|
||
font_size=theme.get("body_size", 12),
|
||
color=theme["text_muted"], font_name=theme["body_font"])
|
||
insight_h = 0.55
|
||
|
||
# 图表
|
||
chart_top = content_y + subtitle_h + insight_h
|
||
chart_h = footer_y - chart_top - 0.25
|
||
_add_chart_to_slide(
|
||
slide, data,
|
||
position=(Inches(m), Inches(chart_top)),
|
||
size=(Inches(content_w), Inches(chart_h)),
|
||
theme=theme,
|
||
)
|
||
|
||
|
||
def layout_chart_text(slide, data, theme):
|
||
"""左图表 + 右文字 — 图文并排
|
||
|
||
data keys:
|
||
title (str): 页面标题
|
||
df, categories_col, series_config, style_config, layout_config: 图表参数
|
||
text_title (str, optional): 右侧小标题
|
||
text_body (str): 右侧正文
|
||
text_bullets (list[str], optional): 右侧要点列表
|
||
insight (str, optional): 结论先行文本(header 之后、图文之前)
|
||
footnote (str, optional): 数据来源/脚注(auto-footer 渲染)
|
||
"""
|
||
m, sw, content_y, content_w, footer_y = setup_content_page(slide, data, theme)
|
||
|
||
# insight 插槽
|
||
insight_h = 0
|
||
if data.get("insight"):
|
||
add_text(slide, data["insight"],
|
||
x=m, y=content_y, w=content_w, h=0.50,
|
||
font_size=theme.get("body_size", 12),
|
||
color=theme["text_muted"], font_name=theme["body_font"])
|
||
insight_h = 0.55
|
||
|
||
# 左侧图表 (占 58%)
|
||
chart_w = content_w * 0.58
|
||
chart_top = content_y + insight_h + 0.05
|
||
chart_h = footer_y - chart_top - 0.25
|
||
_add_chart_to_slide(
|
||
slide, data,
|
||
position=(Inches(m), Inches(chart_top)),
|
||
size=(Inches(chart_w), Inches(chart_h)),
|
||
theme=theme,
|
||
)
|
||
|
||
# 右侧文字
|
||
text_x = m + chart_w + 0.4
|
||
text_w = sw - text_x - m
|
||
|
||
if data.get("text_title"):
|
||
add_text(slide, data["text_title"],
|
||
x=text_x, y=content_y + 0.1, w=text_w, h=0.5,
|
||
font_size=theme["subtitle_size"], color=theme["primary"],
|
||
bold=True, font_name=theme["header_font"])
|
||
|
||
body_y = content_y + 0.7 if data.get("text_title") else content_y + 0.1
|
||
add_text(slide, data.get("text_body", ""),
|
||
x=text_x, y=body_y, w=text_w, h=2.5,
|
||
font_size=theme["body_size"], color=theme["text_dark"],
|
||
font_name=theme["body_font"])
|
||
|
||
if data.get("text_bullets"):
|
||
bullet_y = body_y + 2.8
|
||
for j, bullet in enumerate(data["text_bullets"]):
|
||
add_text(slide, f" {bullet}",
|
||
x=text_x, y=bullet_y + j * 0.45, w=text_w, h=0.4,
|
||
font_size=theme["body_size"] - 1, color=theme["text_dark"],
|
||
font_name=theme["body_font"])
|
||
|
||
|
||
def layout_two_charts(slide, data, theme):
|
||
"""双图表页 — 左右各一个图表
|
||
|
||
data keys:
|
||
title (str): 页面标题
|
||
left (dict): 左图表 (df, categories_col, series_config, ...)
|
||
right (dict): 右图表 (同上)
|
||
left_title (str, optional): 左图小标题
|
||
right_title (str, optional): 右图小标题
|
||
insight (str, optional): 结论先行文本(header 之后、图表之前)
|
||
footnote (str, optional): 数据来源/脚注(auto-footer 渲染)
|
||
"""
|
||
m, sw, content_y, content_w, footer_y = setup_content_page(slide, data, theme)
|
||
|
||
# insight 插槽
|
||
insight_h = 0
|
||
if data.get("insight"):
|
||
add_text(slide, data["insight"],
|
||
x=m, y=content_y, w=content_w, h=0.50,
|
||
font_size=theme.get("body_size", 12),
|
||
color=theme["text_muted"], font_name=theme["body_font"])
|
||
insight_h = 0.55
|
||
|
||
half_w = (content_w - 0.4) / 2
|
||
chart_top = content_y + insight_h + 0.05
|
||
chart_h = footer_y - chart_top - 0.25
|
||
|
||
# 小标题
|
||
sub_base_y = content_y + insight_h
|
||
if data.get("left_title"):
|
||
chart_sub_size = theme.get("chart_subtitle_size", 11)
|
||
add_text(slide, data["left_title"],
|
||
x=m, y=sub_base_y - 0.05, w=half_w, h=0.25,
|
||
font_size=chart_sub_size, color=theme["text_muted"],
|
||
font_name=theme["body_font"])
|
||
chart_top = sub_base_y + 0.25
|
||
chart_h = footer_y - chart_top - 0.25
|
||
|
||
if data.get("right_title"):
|
||
chart_sub_size = theme.get("chart_subtitle_size", 11)
|
||
add_text(slide, data["right_title"],
|
||
x=m + half_w + 0.4, y=sub_base_y - 0.05, w=half_w, h=0.25,
|
||
font_size=chart_sub_size, color=theme["text_muted"],
|
||
font_name=theme["body_font"])
|
||
|
||
# 左图表
|
||
_add_chart_to_slide(
|
||
slide, data["left"],
|
||
position=(Inches(m), Inches(chart_top)),
|
||
size=(Inches(half_w), Inches(chart_h)),
|
||
theme=theme,
|
||
)
|
||
|
||
# 右图表
|
||
_add_chart_to_slide(
|
||
slide, data["right"],
|
||
position=(Inches(m + half_w + 0.4), Inches(chart_top)),
|
||
size=(Inches(half_w), Inches(chart_h)),
|
||
theme=theme,
|
||
)
|
||
|
||
|
||
def layout_two_charts_vertical(slide, data, theme):
|
||
"""上下双图 — 共享日期轴,最大化水平空间
|
||
|
||
data keys:
|
||
title (str): 页面标题
|
||
top (dict): 上图表 (df, categories_col, series_config, ...)
|
||
bottom (dict): 下图表 (同上)
|
||
top_title (str, optional): 上图小标题
|
||
bottom_title (str, optional): 下图小标题
|
||
insight (str, optional): 结论先行文本(header 与上图之间)
|
||
footnote (str, optional): 数据来源/脚注(auto-footer 渲染)
|
||
"""
|
||
m, sw, content_y, content_w, footer_y = setup_content_page(slide, data, theme)
|
||
|
||
# insight 插槽
|
||
insight_h = 0
|
||
if data.get("insight"):
|
||
add_text(slide, data["insight"],
|
||
x=m, y=content_y, w=content_w, h=0.50,
|
||
font_size=theme.get("body_size", 12),
|
||
color=theme["text_muted"], font_name=theme["body_font"])
|
||
insight_h = 0.55
|
||
|
||
chart_area_top = content_y + insight_h
|
||
chart_area_h = footer_y - chart_area_top - 0.25
|
||
gap = 0.15
|
||
each_h = (chart_area_h - gap) / 2
|
||
|
||
# 上图小标题
|
||
sub_h = 0
|
||
chart_sub_size = theme.get("chart_subtitle_size", 11)
|
||
if data.get("top_title"):
|
||
add_text(slide, data["top_title"],
|
||
x=m, y=chart_area_top, w=content_w, h=0.25,
|
||
font_size=chart_sub_size, color=theme["text_muted"],
|
||
font_name=theme["body_font"])
|
||
sub_h = 0.28
|
||
each_h = (chart_area_h - gap - sub_h * 2) / 2
|
||
|
||
top_chart_y = chart_area_top + sub_h
|
||
_add_chart_to_slide(
|
||
slide, data["top"],
|
||
position=(Inches(m), Inches(top_chart_y)),
|
||
size=(Inches(content_w), Inches(each_h)),
|
||
theme=theme,
|
||
)
|
||
|
||
# 下图小标题
|
||
bottom_label_y = top_chart_y + each_h + gap
|
||
if data.get("bottom_title"):
|
||
add_text(slide, data["bottom_title"],
|
||
x=m, y=bottom_label_y, w=content_w, h=0.25,
|
||
font_size=chart_sub_size, color=theme["text_muted"],
|
||
font_name=theme["body_font"])
|
||
bottom_chart_y = bottom_label_y + sub_h
|
||
else:
|
||
bottom_chart_y = bottom_label_y
|
||
|
||
_add_chart_to_slide(
|
||
slide, data["bottom"],
|
||
position=(Inches(m), Inches(bottom_chart_y)),
|
||
size=(Inches(content_w), Inches(each_h)),
|
||
theme=theme,
|
||
)
|
||
|
||
|
||
def layout_chart_table(slide, data, theme):
|
||
"""左图表 + 右表格 — 趋势可视化 + 截面数据快照
|
||
|
||
data keys:
|
||
title (str): 页面标题
|
||
df (DataFrame): 图表数据
|
||
categories_col (str): 分类列名
|
||
series_config (list): 系列配置
|
||
style_config (StyleConfig, optional): 图表样式
|
||
layout_config (ChartLayoutConfig, optional): 图表布局
|
||
headers (list[str]): 表格表头
|
||
rows (list[list]): 表格数据行
|
||
table_df (DataFrame, optional): 若提供,自动转为 headers/rows
|
||
insight (str, optional): 结论先行文本
|
||
chart_ratio (float, optional): 图表宽度占比(默认 0.62)
|
||
footnote (str, optional): 数据来源/脚注(auto-footer 渲染)
|
||
"""
|
||
from ..helpers import add_table
|
||
|
||
m, sw, content_y, content_w, footer_y = setup_content_page(slide, data, theme)
|
||
|
||
# insight 插槽
|
||
insight_h = 0
|
||
if data.get("insight"):
|
||
add_text(slide, data["insight"],
|
||
x=m, y=content_y, w=content_w, h=0.50,
|
||
font_size=theme.get("body_size", 12),
|
||
color=theme["text_muted"], font_name=theme["body_font"])
|
||
insight_h = 0.55
|
||
|
||
area_top = content_y + insight_h
|
||
area_h = footer_y - area_top - 0.25
|
||
|
||
# 图表/表格分割
|
||
chart_ratio = data.get("chart_ratio", 0.62)
|
||
gap = 0.4
|
||
chart_w = content_w * chart_ratio
|
||
table_w = content_w - chart_w - gap
|
||
|
||
# 左侧图表
|
||
_add_chart_to_slide(
|
||
slide, data,
|
||
position=(Inches(m), Inches(area_top)),
|
||
size=(Inches(chart_w), Inches(area_h)),
|
||
theme=theme,
|
||
)
|
||
|
||
# 右侧表格 — 自动从 table_df 转换
|
||
import pandas as pd
|
||
headers = data.get("headers")
|
||
rows = data.get("rows")
|
||
if not rows and "table_df" in data:
|
||
tdf = data["table_df"]
|
||
if isinstance(tdf, pd.DataFrame):
|
||
headers = headers or list(tdf.columns)
|
||
rows = tdf.values.tolist()
|
||
if not rows and "df" in data:
|
||
tdf = data["df"]
|
||
if isinstance(tdf, pd.DataFrame):
|
||
headers = headers or list(tdf.columns)
|
||
rows = tdf.values.tolist()
|
||
|
||
if headers and rows:
|
||
table_x = m + chart_w + gap
|
||
n_rows = len(rows)
|
||
table_h = min(n_rows * 0.45 + 0.45, area_h)
|
||
add_table(
|
||
slide,
|
||
headers=headers,
|
||
rows=rows,
|
||
x=table_x, y=area_top, w=table_w, h=table_h,
|
||
theme=theme,
|
||
font_name=theme["body_font"],
|
||
font_size=10,
|
||
ib_style=True,
|
||
)
|