# 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贡献）等页面均使用堆叠柱变体。
真正的"利润桥"瀑布图（浮动柱）在收益归因报告中极为常见，需单独布局支持。
