305 lines
12 KiB
Markdown
305 lines
12 KiB
Markdown
|
|
# Layout: `chart_table` — 上图表+下表格
|
|||
|
|
|
|||
|
|
> 幻灯片尺寸: 13.333 × 7.5 英寸 | 边距: 0.6 英寸 | 内容宽: 12.133 英寸
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 用途
|
|||
|
|
|
|||
|
|
图表和明细数据一页展示:走势图 + 数据明细、收益图 + 各期收益表、指数图 + 成分股数据。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ASCII 线框图
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────────────────────────┐
|
|||
|
|
│ [标题] y=0.25 │
|
|||
|
|
│ ▬▬ (accent 装饰线) y=0.90 │
|
|||
|
|
│ │
|
|||
|
|
│ ╔══════════════════════════════════════════════════════╗ │
|
|||
|
|
│ ║ ║ │
|
|||
|
|
│ ║ [ 图 表 区 ] ║ │
|
|||
|
|
│ ║ ║ │
|
|||
|
|
│ ╚══════════════════════════════════════════════════════╝ │
|
|||
|
|
│ │
|
|||
|
|
│ ┌────────┬─────────┬─────────┬─────────┬─────────────┐ │
|
|||
|
|
│ │ 指标名 │ 数值A │ 数值B │ 数值C │ 数值D │ │ ← 表头行(accent背景)
|
|||
|
|
│ ├────────┼─────────┼─────────┼─────────┼─────────────┤ │
|
|||
|
|
│ │ 行1 │ xx.x% │ xx.x% │ xx.x% │ xx.x% │ │
|
|||
|
|
│ │ 行2 │ xx.x% │ xx.x% │ xx.x% │ xx.x% │ │
|
|||
|
|
│ │ 行3 │ xx.x% │ xx.x% │ xx.x% │ xx.x% │ │
|
|||
|
|
│ └────────┴─────────┴─────────┴─────────┴─────────────┘ │
|
|||
|
|
└─────────────────────────────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 分区比例与布局参数
|
|||
|
|
|
|||
|
|
### 高度分配
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
chart_table_ratio: 55/45(默认)、60/40、70/30
|
|||
|
|
|
|||
|
|
chart_start_y = 1.20
|
|||
|
|
chart_end_y = 1.20 + 7.5 * ratio_chart = 5.325(默认55%)
|
|||
|
|
table_start_y = chart_end_y + 0.15(间距)
|
|||
|
|
table_end_y = 7.10
|
|||
|
|
|
|||
|
|
实际各值:
|
|||
|
|
ratio 55/45: chart_h=3.525", table_area_h=1.60"
|
|||
|
|
ratio 60/40: chart_h=3.900", table_area_h=1.225"
|
|||
|
|
ratio 70/30: chart_h=4.550", table_area_h=0.575"(仅一行表头+1-2数据行)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> **建议比例**: 55/45(图表适中,表格可放3-5行数据)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 精确坐标标注
|
|||
|
|
|
|||
|
|
### 固定元素
|
|||
|
|
|
|||
|
|
| 元素 | x | y | w | h | 字号 | 颜色 | 对齐 |
|
|||
|
|
|------|---|---|---|---|------|------|------|
|
|||
|
|
| 页面标题 | 0.60 | 0.25 | 12.133 | 0.60 | title_size-2 | text_dark | 左 |
|
|||
|
|
| 装饰线 | 0.60 | 0.90 | 2.00 | 0.04 | — | accent | — |
|
|||
|
|
|
|||
|
|
### 图表区
|
|||
|
|
|
|||
|
|
| 参数 | 值(55/45 比例) |
|
|||
|
|
|------|----------------|
|
|||
|
|
| chart_x | 0.60 |
|
|||
|
|
| chart_y | 1.20 |
|
|||
|
|
| chart_w | 12.133 |
|
|||
|
|
| chart_h | 3.525 |
|
|||
|
|
|
|||
|
|
### 表格区(55/45 比例,表格起始 y=4.875)
|
|||
|
|
|
|||
|
|
**表头行**:
|
|||
|
|
|
|||
|
|
| 元素 | x | y | w | h | 字号 | 颜色 | 对齐 |
|
|||
|
|
|------|---|---|---|---|------|------|------|
|
|||
|
|
| 表头背景 | 0.60 | 4.875 | 12.133 | 0.38 | — | primary | — |
|
|||
|
|
| 表头文字(每列) | col_x | 4.875 | col_w | 0.38 | body_size-1 | text_light | 中 |
|
|||
|
|
|
|||
|
|
**数据行(斑马条纹)**:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
row_h = 0.38
|
|||
|
|
最多行数 = floor((7.10 - 4.875 - 0.38) / 0.38) = floor(1.845 / 0.38) ≈ 4行
|
|||
|
|
|
|||
|
|
偶数行背景: secondary(极淡),奇数行: card_bg(白)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
| 元素 | x | y(相对 row_y) | w | h | 字号 | 颜色 | 对齐 |
|
|||
|
|
|------|---|----------------|---|---|------|------|------|
|
|||
|
|
| 行背景(偶数行) | 0.60 | +0 | 12.133 | 0.38 | — | secondary(30%透明)| — |
|
|||
|
|
| 第1列(标签) | 0.60 | +0.03 | col_widths[0] | 0.32 | body_size-1 | text_dark | 左 |
|
|||
|
|
| 数据列(对齐右) | col_x[j] | +0.03 | col_widths[j] | 0.32 | body_size-1 | text_dark | 右 |
|
|||
|
|
| 行间细线 | 0.60 | +0.36 | 12.133 | 0.02 | — | border | — |
|
|||
|
|
|
|||
|
|
### 列宽分配(自动计算)
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def calc_col_widths(headers, table_w=12.133):
|
|||
|
|
"""
|
|||
|
|
第1列(标签列)分配 25%,其余等分剩余空间
|
|||
|
|
或根据 headers 自定义
|
|||
|
|
"""
|
|||
|
|
n_cols = len(headers)
|
|||
|
|
label_w = table_w * 0.25 # 3.033"
|
|||
|
|
data_w = (table_w - label_w) / (n_cols - 1)
|
|||
|
|
return [label_w] + [data_w] * (n_cols - 1)
|
|||
|
|
|
|||
|
|
# 示例(5列):
|
|||
|
|
# label_w = 3.033",每数据列 = (12.133-3.033)/4 = 2.275"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**正负值着色规则**(表格数据):
|
|||
|
|
- 含 "+" 或正数: `positive` 绿色
|
|||
|
|
- 含 "-" 或负数: `negative` 红色
|
|||
|
|
- 中性/非数字: `text_dark`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 数据接口
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
data = {
|
|||
|
|
"title": str, # 页面标题
|
|||
|
|
"subtitle": str, # 可选
|
|||
|
|
# 图表参数(与 layout_chart_full 相同)
|
|||
|
|
"df": pd.DataFrame, # 图表数据
|
|||
|
|
"categories_col": str, # 分类列名(X轴)
|
|||
|
|
"series_config": list, # 系列配置
|
|||
|
|
"style_config": StyleConfig | None, # 图表样式
|
|||
|
|
"layout_config": ChartLayoutConfig | None,
|
|||
|
|
# 表格参数
|
|||
|
|
"headers": list[str], # 列头(含第一列标签名)
|
|||
|
|
"rows": list[list[str]], # 数据行,每行一个列表
|
|||
|
|
"col_widths": list[float] | None, # 可选,自定义各列宽(英寸),默认自动分配
|
|||
|
|
"chart_ratio": float, # 可选,图表占高比(0.5-0.75),默认 0.55
|
|||
|
|
"header_color": str, # 可选,表头背景色变量名,默认 "primary"
|
|||
|
|
"zebra": bool, # 是否斑马条纹,默认 True
|
|||
|
|
"highlight_last_row": bool, # 是否高亮最后一行(汇总行),默认 False
|
|||
|
|
"source": str, # 可选,数据来源
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 实现伪代码
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def layout_chart_table(slide, data, theme):
|
|||
|
|
from ppt_station.chart_builder.api import create_combo_chart
|
|||
|
|
m = theme["margin"]
|
|||
|
|
sw, sh = 13.333, 7.5
|
|||
|
|
cw = sw - 2 * m
|
|||
|
|
|
|||
|
|
set_slide_bg(slide, theme["bg_light"])
|
|||
|
|
add_text(slide, data["title"],
|
|||
|
|
x=m, y=0.25, w=cw, h=0.60,
|
|||
|
|
font_size=theme["title_size"]-2, color=theme["text_dark"],
|
|||
|
|
bold=True, font_name=theme["header_font"])
|
|||
|
|
add_rect(slide, m, 0.90, 2.0, 0.04, fill=theme["accent"])
|
|||
|
|
|
|||
|
|
ratio = data.get("chart_ratio", 0.55)
|
|||
|
|
chart_y = 1.20
|
|||
|
|
chart_h = (sh - chart_y - 0.40) * ratio # 可用高度的 ratio 给图表
|
|||
|
|
table_y = chart_y + chart_h + 0.15
|
|||
|
|
|
|||
|
|
# 图表
|
|||
|
|
from pptx.util import Inches
|
|||
|
|
create_combo_chart(
|
|||
|
|
slide=slide,
|
|||
|
|
df=data["df"],
|
|||
|
|
categories_col=data["categories_col"],
|
|||
|
|
series_config=data["series_config"],
|
|||
|
|
position=(Inches(m), Inches(chart_y)),
|
|||
|
|
size=(Inches(cw), Inches(chart_h)),
|
|||
|
|
style_config=data.get("style_config"),
|
|||
|
|
layout_config=data.get("layout_config"),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 表格
|
|||
|
|
headers = data["headers"]
|
|||
|
|
rows = data["rows"]
|
|||
|
|
n_cols = len(headers)
|
|||
|
|
|
|||
|
|
col_widths = data.get("col_widths")
|
|||
|
|
if col_widths is None:
|
|||
|
|
label_w = cw * 0.25
|
|||
|
|
data_w = (cw - label_w) / (n_cols - 1)
|
|||
|
|
col_widths = [label_w] + [data_w] * (n_cols - 1)
|
|||
|
|
|
|||
|
|
col_xs = [m]
|
|||
|
|
for w in col_widths[:-1]:
|
|||
|
|
col_xs.append(col_xs[-1] + w)
|
|||
|
|
|
|||
|
|
row_h = 0.38
|
|||
|
|
header_color = theme.get(data.get("header_color", "primary"), theme["primary"])
|
|||
|
|
|
|||
|
|
# 表头
|
|||
|
|
add_rect(slide, m, table_y, cw, row_h, fill=header_color)
|
|||
|
|
for j, (header, cx, cw_j) in enumerate(zip(headers, col_xs, col_widths)):
|
|||
|
|
align = "left" if j == 0 else "right"
|
|||
|
|
px = cx + (0.10 if j == 0 else 0)
|
|||
|
|
add_text(slide, header,
|
|||
|
|
x=px, y=table_y+0.04, w=cw_j-0.10, h=row_h-0.08,
|
|||
|
|
font_size=theme["body_size"]-1, color=theme["text_light"],
|
|||
|
|
bold=True, align=align, font_name=theme["body_font"])
|
|||
|
|
|
|||
|
|
# 数据行
|
|||
|
|
for k, row in enumerate(rows):
|
|||
|
|
ry = table_y + row_h + k * row_h
|
|||
|
|
if ry + row_h > sh - 0.30:
|
|||
|
|
break # 超出页面则停止
|
|||
|
|
|
|||
|
|
# 斑马背景
|
|||
|
|
if data.get("zebra", True) and k % 2 == 1:
|
|||
|
|
add_rect(slide, m, ry, cw, row_h, fill=theme["secondary"], alpha=20)
|
|||
|
|
|
|||
|
|
# 汇总行高亮
|
|||
|
|
is_last = (k == len(rows) - 1)
|
|||
|
|
if is_last and data.get("highlight_last_row", False):
|
|||
|
|
add_rect(slide, m, ry, cw, row_h, fill=theme["secondary"], alpha=40)
|
|||
|
|
|
|||
|
|
for j, (cell, cx, cw_j) in enumerate(zip(row, col_xs, col_widths)):
|
|||
|
|
align = "left" if j == 0 else "right"
|
|||
|
|
# 正负着色
|
|||
|
|
cell_str = str(cell)
|
|||
|
|
if j > 0:
|
|||
|
|
try:
|
|||
|
|
val = float(cell_str.replace("%", "").replace("+", "").replace(",", ""))
|
|||
|
|
c = theme["positive"] if val > 0 else (theme["negative"] if val < 0 else theme["text_dark"])
|
|||
|
|
except ValueError:
|
|||
|
|
c = theme["text_dark"]
|
|||
|
|
else:
|
|||
|
|
c = theme["text_dark"]
|
|||
|
|
bold = is_last and data.get("highlight_last_row", False)
|
|||
|
|
px = cx + (0.10 if j == 0 else 0)
|
|||
|
|
add_text(slide, cell_str,
|
|||
|
|
x=px, y=ry+0.04, w=cw_j-0.10, h=row_h-0.08,
|
|||
|
|
font_size=theme["body_size"]-1, color=c,
|
|||
|
|
bold=bold, align=align, font_name=theme["body_font"])
|
|||
|
|
|
|||
|
|
# 行间线
|
|||
|
|
add_rect(slide, m, ry+row_h-0.01, cw, 0.01, fill=theme["border"])
|
|||
|
|
|
|||
|
|
if data.get("source"):
|
|||
|
|
add_text(slide, f"数据来源:{data['source']}",
|
|||
|
|
x=m, y=7.10, w=cw, h=0.30,
|
|||
|
|
font_size=theme["caption_size"], color=theme["text_muted"],
|
|||
|
|
font_name=theme["body_font"])
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 设计要点
|
|||
|
|
|
|||
|
|
1. **55/45 比例** 是最常用配置:图表够大,表格放得下 3-4 行有效数据
|
|||
|
|
2. **表头用 `primary` 背景 + `text_light` 白字**,与数据行形成强烈对比
|
|||
|
|
3. **斑马条纹** 轻微区分奇偶行(透明度 20% 的 secondary),不喧宾夺主
|
|||
|
|
4. **正负值自动着色** 让数字表格直观传达方向
|
|||
|
|
5. **70/30 比例** 适合图表是核心时,表格仅作辅助参考(仅1-2行数据)
|
|||
|
|
6. **表格列宽** 第1列(标签/日期)固定 25%,其余等分
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 测试数据示例
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
import pandas as pd
|
|||
|
|
from ppt_station.chart_builder.models import SeriesConfig
|
|||
|
|
|
|||
|
|
df = pd.DataFrame({
|
|||
|
|
"日期": ["2024-01", "2024-02", "2024-03", "2024-04",
|
|||
|
|
"2024-05", "2024-06", "2024-07", "2024-08"],
|
|||
|
|
"组合收益": [1.2, 2.5, -0.8, 3.1, 2.0, 1.5, 0.7, 2.3],
|
|||
|
|
"基准收益": [0.8, 1.9, -1.2, 2.4, 1.6, 1.1, 0.5, 1.8],
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
data = {
|
|||
|
|
"title": "2024年月度收益对比",
|
|||
|
|
"df": df,
|
|||
|
|
"categories_col": "日期",
|
|||
|
|
"series_config": [
|
|||
|
|
SeriesConfig(key="组合收益", name="本组合", type="bar", axis="primary"),
|
|||
|
|
SeriesConfig(key="基准收益", name="基准", type="line", axis="primary"),
|
|||
|
|
],
|
|||
|
|
"chart_ratio": 0.55,
|
|||
|
|
"headers": ["时间段", "1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月"],
|
|||
|
|
"rows": [
|
|||
|
|
["本组合(%)", "+1.2", "+2.5", "-0.8", "+3.1", "+2.0", "+1.5", "+0.7", "+2.3"],
|
|||
|
|
["基准(%)", "+0.8", "+1.9", "-1.2", "+2.4", "+1.6", "+1.1", "+0.5", "+1.8"],
|
|||
|
|
["超额(%)", "+0.4", "+0.6", "+0.4", "+0.7", "+0.4", "+0.4", "+0.2", "+0.5"],
|
|||
|
|
["累计超额(%)", "+0.4", "+1.0", "+1.4", "+2.1", "+2.5", "+2.9", "+3.1", "+3.6"],
|
|||
|
|
],
|
|||
|
|
"highlight_last_row": True,
|
|||
|
|
"source": "Wind 资讯,内部测算",
|
|||
|
|
}
|
|||
|
|
```
|