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

393 lines
17 KiB
Markdown
Raw Permalink Normal View History

# 布局标注:`dashboard`(仪表盘页)
> 设计师交付物 · v1.0 · 2026-02-20
> 参考来源:投行级研报 图表页设计语言 + 现有 kpi.py / chart.py 实现规范
---
## 设计意图
一页内同时展示**核心数字 + 趋势图表**。金融场景典型用途:
- 组合季报首页:年化收益 / 夏普比率 / 最大回撤 / 规模 + 净值曲线图
- 宏观月报GDP / CPI / PMI / 利率 + 经济指标走势图
- 基金评价:超额收益 / 信息比率 / 跟踪误差 / Calmar + 比较图
---
## 变体 A主变体顶部 KPI 条 + 下方大图
### ASCII 线框图
```
┌─────────────────────────────────────────────────────────────┐ y=0
│ ▌ 页面标题bold, title_size-2, text_dark │ y=0.25~0.85
│ ████████ accent 装饰线2.0w × 0.04h │ y=0.90
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ y=1.10
│ │ ▌▌▌▌▌▌▌▌▌▌▌ │ │ ▌▌▌▌▌▌▌▌▌▌▌ │ │ ▌▌▌▌▌▌▌▌▌▌▌ │ │ (顶部色条 h=0.05)
│ │ 指标名 │ │ 指标名 │ │ 指标名 │ │ y=1.42
│ │ │ │ │ │ │ │
│ │ 12.3% │ │ 1.87 │ │ -4.2% │ │ y=1.72 大数字
│ │ │ │ │ │ │ │
│ │ +2.1% ↑ │ │ +0.15 ↑ │ │ 本期最低 │ │ y=2.50 变化/注释
│ └──────────────┘ └──────────────┘ └──────────────┘ │ y=2.80
│ │
│ 图表区副标题可选body_size, text_muted │ y=3.05
│ │
│ ┌────────────────────────────────────────────────────────┐ │ y=3.35
│ │ │ │
│ │ 大图表full width │ │ h=3.90
│ │ │ │
│ └────────────────────────────────────────────────────────┘ │ y=7.25
└─────────────────────────────────────────────────────────────┘ y=7.50
```
### 精确元素标注英寸m = theme["margin"] = 0.6
#### 全局常量
```
sw = 13.333 # 幻灯片宽度
sh = 7.5 # 幻灯片高度
m = theme["margin"] # 默认 0.6
content_w = sw - 2*m # = 12.133
```
#### 页面标题区
| 元素 | x | y | w | h | 样式 |
|------|---|---|---|---|------|
| 背景 | 0 | 0 | 13.333 | 7.5 | `bg_light` |
| 页面标题 | `m` | 0.25 | `content_w` | 0.6 | `text_dark`, bold, `title_size-2`, left, header_font |
| accent 装饰线 | `m` | 0.90 | 2.0 | 0.04 | `accent` fill |
#### KPI 卡片区3 张卡片)
```
n = len(cards) # 3 或 4
gap = 0.30 # 卡片间距
card_w = (content_w - gap*(n-1)) / n
card_h = 1.70
start_y = 1.10
```
**3 张卡片时**`card_w = (12.133 - 0.60) / 3 = 3.844`
**4 张卡片时**`card_w = (12.133 - 0.90) / 4 = 2.808`
每张卡片(以第 i 张为例,`cx = m + i*(card_w+gap)`
| 元素 | x相对卡片左边 | y相对幻灯片 | w | h | 样式 |
|------|----------------|---------------|---|---|------|
| 卡片背景 | `cx` | `start_y` = 1.10 | `card_w` | 1.70 | `card_bg` fill, `border` 边框 1pt, 圆角 0.08" |
| 顶部色条 | `cx + 0.12` | 1.10 + 0.10 = **1.20** | `card_w - 0.24` | 0.05 | `primary` fill |
| 指标名label | `cx + 0.20` | **1.40** | `card_w - 0.40` | 0.28 | `text_muted`, `body_size`, center, body_font |
| 大数字value | `cx + 0.20` | **1.72** | `card_w - 0.40` | 0.72 | `primary`, **`kpi_size`**, bold, center+middle, header_font |
| 变化值change | `cx + 0.20` | **2.50** | `card_w - 0.40` | 0.25 | `positive`/`negative`, `body_size+1`, bold, center, body_font |
> **change 颜色判断**:以 `+` 开头 → `positive`;以 `-` 开头 → `negative`
#### 图表区
| 元素 | x | y | w | h | 样式 |
|------|---|---|---|---|------|
| 图表区副标题(可选) | `m` | **3.05** | `content_w` | 0.25 | `text_muted`, `body_size-1`, left, body_font |
| 图表 | `m` | **3.35** | `content_w` | **3.90** | ChartBuilder |
> 有副标题时图表上移 0 px副标题和图表不重叠副标题占 3.05~3.30,图表从 3.35 开始)
> 底部安全区3.35 + 3.90 = **7.25**,留 0.25 底部安全边距 ✓
---
## 变体 B次变体左图 60% + 右侧 KPI 竖排
用于 KPI 较多4-5 个)且图表更重要的场景。
### ASCII 线框图
```
┌──────────────────────────────────────────────────────────┐
│ 页面标题 │ y=0.25
│ ▌▌ accent 线 │ y=0.90
│ │
│ ┌─────────────────────────────┐ ┌────────────────────┐ │ y=1.10
│ │ │ │ 指标名 │ │
│ │ │ │ 12.3% │ │ KPI card 1
│ │ │ │ +2.1% ↑ │ │
│ │ │ └────────────────────┘ │
│ │ 大图表 │ ┌────────────────────┐ │
│ │ (60% 宽) │ │ 指标名 │ │ KPI card 2
│ │ │ │ 1.87 │ │
│ │ │ └────────────────────┘ │
│ │ │ ┌────────────────────┐ │
│ │ │ │ 指标名 │ │ KPI card 3
│ │ │ │ -4.2% │ │
│ └─────────────────────────────┘ └────────────────────┘ │
└──────────────────────────────────────────────────────────┘
```
### 精确标注
```
chart_w = content_w * 0.60 # = 7.280
right_w = content_w * 0.38 # = 4.611 (留 0.02 给中间分隔)
gap_lr = content_w - chart_w - right_w # ≈ 0.242 居中分隔
right_x = m + chart_w + gap_lr # ≈ 8.122
chart_top = 1.20
chart_h = 7.5 - chart_top - 0.25 # = 6.05
```
| 元素 | x | y | w | h |
|------|---|---|---|---|
| 大图表 | `m`=0.6 | 1.20 | 7.280 | 6.05 |
| KPI 卡片竖排n张 | 8.122 | 见下方计算 | 4.611 | 见下方 |
**竖排 KPI 卡片高度计算**
```
total_h = 6.05 # 与图表等高
n = len(cards) # 通常 3-5 张
gap_v = 0.20 # 纵向间距
card_h = (total_h - gap_v*(n-1)) / n
start_y = 1.20
# 第 i 张 y = start_y + i*(card_h + gap_v)
```
竖排卡片内部布局card_h 可变):
- 左侧竖色条(而非顶部色条):`right_x`, `card_y+0.1`, `0.05`, `card_h-0.2`, `primary` fill
- 指标名:`right_x+0.25`, `card_y+0.15`, `right_w-0.45`, `0.28`, `text_muted`, body_size
- 大数字:`right_x+0.25`, `card_y+0.45`, `right_w-0.45`, `card_h*0.45`, `primary`, kpi_size*0.85, bold
- 变化值:底部 0.28 高度区域positive/negative
---
## 数据接口
### 变体 Atop_kpi 模式)
```python
data = {
# 必须
"title": "组合运行情况", # 页面标题
"cards": [ # 3-4 个 KPI 卡片
{
"label": "年化收益率", # 指标名
"value": "12.3%", # 已格式化数值(大数字)
"change": "+2.1%", # 可选:变化值(+开头→绿,-开头→红)
},
{
"label": "夏普比率",
"value": "1.87",
"change": "+0.15",
},
{
"label": "最大回撤",
"value": "-4.2%",
# change 省略时不显示变化行
},
],
# 图表(同 chart_full 的图表参数)
"df": df, # DataFrame
"categories_col": "日期",
"series_config": [...],
"style_config": None, # 可选
"layout_config": None, # 可选
# 可选
"subtitle": None, # 页面副标题accent 线后)
"chart_label": "净值走势", # 图表区左上方小标题
"variant": "top_kpi", # "top_kpi"(默认)| "side_kpi"
}
```
### 变体 Bside_kpi 模式)
同上,改 `"variant": "side_kpi"`,图表和 cards 含义相同。
---
## 实现伪代码
```python
def layout_dashboard(slide, data, theme):
m = theme["margin"]
sw = 13.333
content_w = sw - 2 * m
variant = data.get("variant", "top_kpi")
set_slide_bg(slide, theme["bg_light"])
# ── 标题区 ──────────────────────────────────────
add_text(slide, data["title"],
x=m, y=0.25, w=content_w, h=0.6,
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"])
if data.get("subtitle"):
add_text(slide, data["subtitle"],
x=m, y=1.00, w=content_w, h=0.30,
font_size=theme["body_size"] - 1, color=theme["text_muted"],
font_name=theme["body_font"])
# ── 分支:变体 A顶部 KPI + 下方图表)────────────
if variant == "top_kpi":
cards = data["cards"]
n = len(cards)
gap = 0.30
card_w = (content_w - gap * (n - 1)) / n
card_h = 1.70
start_y = 1.10
for i, card in enumerate(cards):
cx = m + i * (card_w + gap)
# 卡片背景(圆角)
add_rounded_rect(slide, cx, start_y, card_w, card_h,
fill=theme["card_bg"], line_color=theme["border"],
line_width=1)
# 顶部色条
add_rect(slide, cx + 0.12, start_y + 0.10, card_w - 0.24, 0.05,
fill=theme["primary"])
# 指标名
add_text(slide, card["label"],
x=cx + 0.20, y=start_y + 0.30, w=card_w - 0.40, h=0.28,
font_size=theme["body_size"], color=theme["text_muted"],
align="center", font_name=theme["body_font"])
# 大数字
add_text(slide, card["value"],
x=cx + 0.20, y=start_y + 0.62, w=card_w - 0.40, h=0.72,
font_size=theme["kpi_size"], color=theme["primary"],
bold=True, align="center", valign="middle",
font_name=theme["header_font"])
# 变化值
if card.get("change"):
change = card["change"]
color = theme["positive"] if not change.startswith("-") else theme["negative"]
add_text(slide, change,
x=cx + 0.20, y=start_y + 1.40, w=card_w - 0.40, h=0.25,
font_size=theme["body_size"] + 1, color=color,
bold=True, align="center", font_name=theme["body_font"])
# 图表区副标题
if data.get("chart_label"):
add_text(slide, data["chart_label"],
x=m, y=3.05, w=content_w, h=0.25,
font_size=theme["body_size"] - 1, color=theme["text_muted"],
font_name=theme["body_font"])
# 大图表
_add_chart_to_slide(
slide, data,
position=(Inches(m), Inches(3.35)),
size=(Inches(content_w), Inches(3.90)),
)
# ── 分支:变体 B左图 + 右侧竖排 KPI───────────
elif variant == "side_kpi":
chart_w = content_w * 0.60 # 7.280
right_w = content_w * 0.38 # 4.611
gap_lr = content_w - chart_w - right_w
right_x = m + chart_w + gap_lr
chart_top = 1.20
chart_h = 7.5 - chart_top - 0.25
_add_chart_to_slide(
slide, data,
position=(Inches(m), Inches(chart_top)),
size=(Inches(chart_w), Inches(chart_h)),
)
cards = data["cards"]
n = len(cards)
gap_v = 0.20
card_h = (chart_h - gap_v * (n - 1)) / n
for i, card in enumerate(cards):
cy = chart_top + i * (card_h + gap_v)
add_rounded_rect(slide, right_x, cy, right_w, card_h,
fill=theme["card_bg"], line_color=theme["border"], line_width=1)
# 左侧竖色条
add_rect(slide, right_x, cy + 0.10, 0.05, card_h - 0.20,
fill=theme["primary"])
# 指标名
add_text(slide, card["label"],
x=right_x + 0.25, y=cy + 0.15, w=right_w - 0.45, h=0.28,
font_size=theme["body_size"], color=theme["text_muted"],
font_name=theme["body_font"])
# 大数字(自适应 card_h
add_text(slide, card["value"],
x=right_x + 0.25, y=cy + 0.45, w=right_w - 0.45,
h=card_h * 0.45,
font_size=int(theme["kpi_size"] * 0.85), color=theme["primary"],
bold=True, valign="middle", font_name=theme["header_font"])
# 变化值
if card.get("change"):
change = card["change"]
color = theme["positive"] if not change.startswith("-") else theme["negative"]
add_text(slide, change,
x=right_x + 0.25, y=cy + card_h - 0.35, w=right_w - 0.45, h=0.28,
font_size=theme["body_size"], color=color,
bold=True, font_name=theme["body_font"])
```
---
## 设计决策说明
### 为什么 KPI 大数字用 `kpi_size` 而不是固定字号?
卡片数量影响卡片宽度3张→3.84英寸 vs 4张→2.81英寸),字号应跟随卡片宽度变化。`kpi_size` 是主题全局设置开发者可按主题调整建议3张卡时 kpi_size=36pt4张时=28pt
### 为什么顶部色条而非圆角变化?
参考 投行级研报 和现有 kpi.py 的一致性。色条 `primary` 颜色给卡片一个品牌锚点,同时避免全彩卡片在报告中过于抢眼。
### 变体 A 的图表起始 y=3.35 而非 3.10
- KPI 区结束1.10 + 1.70 = 2.80
- 分隔 gap0.25 → y=3.05
- 副标题区3.05~3.30(高度 0.25
- 图表内边距0.05
- 图表起始3.35
- 这样即使没有副标题,图表也在 y=3.35 开始(不因 `chart_label` 有无而跳动)
### 变体 B 的图表比例为 60%
投行级研报 中"1大+2小"布局中大图约占 60%。60% 给图表足够面积展示时间序列细节40% 给右侧 KPI 仍有约 5 英寸宽度,足够显示 2-3 位小数的数值。
---
## 与现有布局的区别
| 布局 | 与 dashboard 的区别 |
|------|-------------------|
| `kpi_cards` | 只有数字卡片,没有图表 |
| `chart_full` | 只有图表,没有 KPI 数字 |
| `two_charts` | 两个图表,没有 KPI 数字 |
| `dashboard` | **唯一** 在一页内同时显示 KPI + 趋势图的布局 |
---
## 测试用 data 示例
```python
import pandas as pd
df = pd.DataFrame({
"日期": pd.date_range("2024-01-01", periods=12, freq="ME"),
"组合净值": [1.00, 1.02, 1.05, 1.08, 1.07, 1.10, 1.12, 1.09, 1.13, 1.15, 1.18, 1.21],
"基准净值": [1.00, 1.01, 1.03, 1.05, 1.04, 1.06, 1.07, 1.06, 1.08, 1.10, 1.12, 1.14],
})
data = {
"title": "组合2024年运行情况",
"subtitle": "截至2024年12月31日",
"cards": [
{"label": "年化收益率", "value": "21.0%", "change": "+9.0% vs 基准"},
{"label": "夏普比率", "value": "1.87", "change": "+0.23"},
{"label": "最大回撤", "value": "-2.8%", "change": "-1.4% vs 基准"},
],
"df": df,
"categories_col": "日期",
"series_config": [
{"key": "组合净值", "name": "组合净值", "type": "line", "axis": "primary"},
{"key": "基准净值", "name": "基准净值", "type": "line", "axis": "primary"},
],
"chart_label": "净值走势2024年",
"variant": "top_kpi",
}
```