293 lines
10 KiB
Markdown
293 lines
10 KiB
Markdown
|
|
# 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 的对应
|
|||
|
|
|
|||
|
|
投行级研报 P4(GDP分解)、P44(EPS贡献)等页面均使用堆叠柱变体。
|
|||
|
|
真正的"利润桥"瀑布图(浮动柱)在收益归因报告中极为常见,需单独布局支持。
|