skills/ppt-station-skill/ppt_station/composer/layouts/chart.py

342 lines
12 KiB
Python
Raw Normal View History

"""图表类布局 — 复用 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,
)