skills/ppt-station-skill/reference/layout-waterfall.md

293 lines
10 KiB
Markdown
Raw Permalink Normal View History

# Layout: `waterfall` — 瀑布图/归因分析
> 幻灯片尺寸: 13.333 × 7.5 英寸 | 边距: 0.6 英寸 | 内容宽: 12.133 英寸
---
## 用途
收益归因、成本分解、利润桥图Profit Bridge、组合贡献分析。
展示"从起点 → 各因素贡献 → 终点"的累积变化过程。
---
## ASCII 线框图
```
┌─────────────────────────────────────────────────────────────┐
│ [标题] y=0.25 │
│ ▬▬ (accent 装饰线 2.0") y=0.90 │
│ │
│ 起始值 增加项 增加项 减少项 最终值 │
│ ┌──────┐ ┌──────┐ │
│ │ │ │ +██ │ +██ │ │ │
│ │ 基础 │ │ │ │ -██ │ ┌──────┐ │
│ │ │ │ │ │ │ │ 合计 │ │
│ └──────┘ └──────┘ └──────┘ └──────┘ │
│ 8.50% +2.10% +1.30% -1.80% 10.10% │
│ [期初收益] [权益贡献] [债券贡献] [汇率拖累] [期末收益] │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## 数据结构说明
瀑布图数据包含:
- **start** — 起始值(期初,作为第一个完整柱)
- **items** — 各贡献因素(浮动柱,正向或负向)
- **end** — 终止值(期末,作为最后一个完整柱,可自动计算)
---
## 精确坐标标注
### 固定元素
| 元素 | 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 | — |
| 零基准线 | 0.60 | 图表区中心 | 12.133 | 0.02 | — | text_muted | — |
### 图表区域
| 参数 | 值 | 说明 |
|------|---|------|
| chart_x | 0.60 | 图表左边界 |
| chart_y | 1.30 | 图表顶部(内容区起始) |
| chart_w | 12.133 | 图表宽度 |
| chart_h | 5.50 | 图表高度(到 y=6.80 |
| bar_area_h | 4.00 | 柱体绘制区高度y=1.50 ~ y=5.50 |
| label_y | 5.60 | 标签行 y柱下方 |
| value_y | 1.20 | 数值标注 y柱上方浮动 |
| footer_y | 6.20 | 数据来源行 |
### 柱体尺寸计算
```
N = len(items) + 2 # 总柱数start + items + end
bar_gap = 0.20 # 柱间距(英寸)
bar_w = (chart_w - bar_gap * (N - 1)) / N # 每柱宽
# N=5: bar_w = (12.133 - 0.80) / 5 = 2.267"
# N=6: bar_w = (12.133 - 1.00) / 6 = 1.856"
# N=7: bar_w = (12.133 - 1.20) / 7 = 1.562"
# N=8: bar_w = (12.133 - 1.40) / 8 = 1.342"
bar_x[i] = chart_x + i * (bar_w + bar_gap)
```
### 柱体高度计算
```
# 图表坐标系
y_axis_max = max(所有累积值的最大值) * 1.15 # 留 15% 上边距
y_axis_min = min(所有累积值的最小值) * 1.15 # 留 15% 下边距(负值时)
y_range = y_axis_max - y_axis_min
# 将数据值映射到像素(英寸)
def value_to_y(v):
return chart_y + bar_area_h * (1 - (v - y_axis_min) / y_range)
# 起始柱完整柱从0到start_value
start_bar_top = value_to_y(start_value)
start_bar_bottom = value_to_y(0)
start_bar_h = start_bar_bottom - start_bar_top
# 贡献柱(浮动柱,从累积值到累积值+delta
for item in items:
bar_top = value_to_y(max(cumsum, cumsum + item.value))
bar_bottom = value_to_y(min(cumsum, cumsum + item.value))
bar_h = bar_bottom - bar_top
cumsum += item.value
# 连接虚线(前一柱的浮动底部 → 当前柱底部)
连接线: w=bar_gap, h=0.01, 颜色=text_muted, 虚线样式
```
### 颜色规则
| 柱类型 | 条件 | 颜色变量 |
|--------|------|---------|
| 起始柱 | — | primary |
| 正向贡献柱 | item.value > 0 | positive |
| 负向贡献柱 | item.value < 0 | negative |
| 终止柱 | — | primary |
| 连接虚线 | — | text_muted |
### 数值标注(柱顶/底 + 数值)
| 元素 | 位置 | 字号 | 颜色 | 格式 |
|------|------|------|------|------|
| 起始/终止数值 | 柱顶上方 0.10" | body_size | text_dark | "X.XX%" |
| 正向贡献值 | 柱顶上方 0.10" | body_size | positive | "+X.XX%" |
| 负向贡献值 | 柱底下方 0.10" | body_size | negative | "-X.XX%" |
| 柱体内标注(高柱时) | 柱体垂直居中 | body_size-1 | text_light | 同上 |
### 底部标签行
| 元素 | x | y | w | h | 字号 | 颜色 | 对齐 |
|------|---|---|---|---|------|------|------|
| 柱体标签 | bar_x[i] | 5.60 | bar_w | 0.45 | caption_size | text_muted | 中 |
---
## 数据接口
```python
data = {
"title": str, # 页面标题
"subtitle": str, # 可选,如 "2024年全年收益归因分析"
"start": {
"label": str, # 如 "期初收益"
"value": float, # 如 8.50
"value_fmt": str, # 如 "8.50%" 或 "850亿"
},
"items": [
{
"label": str, # 贡献因素名称8字以内
"value": float, # 数值(正=向上,负=向下)
"value_fmt": str, # 显示文字(如 "+2.10%"
"direction": "up" | "down", # 可选,若不传则根据 value 符号判断
},
...
],
"end": {
"label": str, # 如 "期末收益"
"value": float | None, # None则自动计算(start + sum(items))
"value_fmt": str | None,
},
"unit": str, # 可选,如 "%" 或 "亿元"用于Y轴标注
"show_connector": bool, # 是否显示连接虚线,默认 True
}
```
---
## 实现伪代码
```python
def layout_waterfall(slide, data, theme):
m = theme["margin"]
sw = 13.333
cw = sw - 2 * m # 12.133
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"])
# 计算柱体布局
all_bars = [data["start"]] + data["items"] + [data["end"]]
N = len(all_bars)
bar_gap = 0.20
bar_w = (cw - bar_gap * (N - 1)) / N
chart_y = 1.50
bar_area_h = 4.00
# 计算值域
cumsum = data["start"]["value"]
all_values = [0, cumsum]
for item in data["items"]:
cumsum += item["value"]
all_values.append(cumsum)
end_val = data["end"].get("value") or cumsum
all_values.append(end_val)
y_max = max(all_values) * 1.15
y_min = min(all_values) * 1.15 if min(all_values) < 0 else 0
y_range = y_max - y_min
def val_to_y(v):
return chart_y + bar_area_h * (1 - (v - y_min) / y_range)
# 绘制零线
zero_y = val_to_y(0)
add_rect(slide, m, zero_y, cw, 0.015, fill=theme["text_muted"])
# 绘制柱体
cumsum = 0
prev_top_y = None
for i, bar in enumerate(all_bars):
bx = m + i * (bar_w + bar_gap)
is_total = (i == 0 or i == N - 1)
if is_total:
v = bar.get("value", cumsum)
top_y = val_to_y(max(v, 0))
bot_y = val_to_y(min(v, 0))
color = theme["primary"]
else:
v = bar["value"]
base = cumsum
top_y = val_to_y(max(base, base + v))
bot_y = val_to_y(min(base, base + v))
color = theme["positive"] if v >= 0 else theme["negative"]
cumsum += v
bar_h = bot_y - top_y
add_rect(slide, bx, top_y, bar_w, bar_h, fill=color)
# 连接线
if prev_top_y is not None and data.get("show_connector", True) and not is_total:
add_rect(slide, bx - bar_gap, prev_top_y, bar_gap, 0.01,
fill=theme["text_muted"])
prev_top_y = bot_y if v >= 0 else top_y
# 数值标注
fmt = bar.get("value_fmt", f"{v:+.2f}%")
label_y = top_y - 0.30 if v >= 0 else bot_y + 0.05
add_text(slide, fmt,
x=bx, y=label_y, w=bar_w, h=0.28,
font_size=theme["body_size"], color=color,
bold=True, align="center", font_name=theme["body_font"])
# 底部标签
add_text(slide, bar["label"],
x=bx, y=5.60, w=bar_w, h=0.45,
font_size=theme["caption_size"], color=theme["text_muted"],
align="center", font_name=theme["body_font"])
```
---
## 设计要点
1. **柱间连接虚线** 从前一柱的"累积底部"水平延伸到当前柱,帮助读者追踪累积过程
2. **数值标注位置**: 正向柱标在顶部上方,负向柱标在底部下方(避免遮挡柱体)
3. **起始/终止柱**(汇总柱)用 `primary` 色,与贡献柱颜色形成视觉区分
4. **最多 8 个柱**(含起始和终止);超过时建议合并小贡献项为"其他"
5. **含负值时**零线显示并加粗0.02"),负值柱延伸到零线以下
---
## 测试数据示例
```python
data = {
"title": "2024年组合收益归因分析",
"subtitle": "单位: 百分点(%",
"start": {"label": "期初基准", "value": 0, "value_fmt": "0%"},
"items": [
{"label": "权益资产", "value": 3.20, "value_fmt": "+3.20%"},
{"label": "债券资产", "value": 1.80, "value_fmt": "+1.80%"},
{"label": "另类资产", "value": 0.90, "value_fmt": "+0.90%"},
{"label": "汇率影响", "value": -0.60, "value_fmt": "-0.60%"},
{"label": "费用扣除", "value": -0.30, "value_fmt": "-0.30%"},
],
"end": {"label": "期末收益", "value": None, "value_fmt": None}, # 自动计算=5.00%
}
```
---
## 与 投行级研报 GTM 的对应
投行级研报 P4GDP分解、P44EPS贡献等页面均使用堆叠柱变体。
真正的"利润桥"瀑布图(浮动柱)在收益归因报告中极为常见,需单独布局支持。