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