skills/ppt-station-skill/ppt_station/chart_builder/api.py

365 lines
12 KiB
Python
Raw Normal View History

"""
公共 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
)