# 资产消耗图表 (LifePlanChart)
**本文引用的文件**
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx)
- [types.ts](file://src/types.ts)
- [render-props-types.ts](file://frontend/src/render-props-types.ts)
- [mock-data.json](file://src/mock-data.json)
- [Root.tsx](file://src/Root.tsx)
- [DualChartView.tsx](file://src/DualChartView.tsx)
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能考量](#性能考量)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件围绕 LifePlanChart 组件展开,系统性阐述其作为“资产消耗趋势可视化图表”的实现方式。组件基于 Recharts 的 AreaChart 展示用户生命周期内资产余额的变化,结合 Remotion 的 useCurrentFrame 和 interpolate 实现逐帧动画的数据流;通过自定义 X 轴刻度 CustomXAxisTick 渲染“年份+主被保险人年龄+配偶年龄”的三行标签;采用渐变填充策略(蓝色代表正资产、红色代表负资产)直观反映资产状态;HUD 区域实时显示当前帧对应的年份、资产余额、收入与支出;并通过数据插值算法实现平滑的逐帧动画效果。文档还给出与 render_props.json 数据结构的映射关系示例,说明 YearlySnapshot 类型如何驱动图表渲染,并提供面向初学者的使用示例与面向高级开发者的性能优化建议。
## 项目结构
本项目采用前端 Remotion 视频渲染管线,LifePlanChart 位于 src 目录下,配合 Root.tsx 注册为 Composition,mock-data.json 提供演示数据,frontend/src/render-props-types.ts 定义了后端生成的 render_props.json 的类型契约。
```mermaid
graph TB
subgraph "前端渲染层"
Root["Root.tsx
注册 Composition"]
LifePlan["LifePlanChart.tsx
资产消耗趋势图"]
Dual["DualChartView.tsx
双图联动视图"]
end
subgraph "数据层"
Mock["mock-data.json
演示数据"]
Types["types.ts
YearlySnapshot 接口"]
RenderTypes["render-props-types.ts
render_props.json 类型契约"]
end
Root --> LifePlan
Root --> Dual
LifePlan --> Types
Dual --> LifePlan
LifePlan -. 使用 .-> Mock
Dual -. 使用 .-> RenderTypes
```
**图表来源**
- [Root.tsx](file://src/Root.tsx#L1-L68)
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L1-L349)
- [DualChartView.tsx](file://src/DualChartView.tsx#L1-L176)
- [types.ts](file://src/types.ts#L1-L30)
- [render-props-types.ts](file://frontend/src/render-props-types.ts#L1-L251)
- [mock-data.json](file://src/mock-data.json#L1-L61)
**章节来源**
- [Root.tsx](file://src/Root.tsx#L1-L68)
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L1-L349)
- [types.ts](file://src/types.ts#L1-L30)
- [render-props-types.ts](file://frontend/src/render-props-types.ts#L1-L251)
- [mock-data.json](file://src/mock-data.json#L1-L61)
## 核心组件
- 组件名称:LifePlanChart
- 功能定位:资产余额随生命周期变化的趋势可视化,支持 HUD 实时信息与平滑动画
- 关键特性:
- 基于 Recharts 的 AreaChart
- 自定义 X 轴三行标签(年份、主被保险人年龄、配偶年龄)
- 渐变填充(正资产蓝、负资产红)
- 基于 Remotion 的逐帧插值动画
- HUD 实时显示年份、资产余额、收入与支出
**章节来源**
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L1-L349)
- [types.ts](file://src/types.ts#L1-L30)
## 架构总览
LifePlanChart 在 Remotion 中作为 Composition 的子组件运行,通过 useCurrentFrame 获取当前帧号,用 interpolate 将帧号映射到 timeline 的索引区间,从而计算出可见数据片段与插值点,实现“从左至右”推进的平滑动画。HUD 区域根据当前数据点实时更新;Y 轴域基于全量 timeline 计算,避免动画过程中因极值变化导致的跳变;X 轴域在短序列时固定范围,保证标签可读性。
```mermaid
sequenceDiagram
participant R as "Remotion 播放器"
participant C as "LifePlanChart"
participant RC as "Recharts 图表"
participant U as "HUD 区域"
R->>C : "useCurrentFrame() 获取当前帧"
C->>C : "interpolate(帧号) => exactIndex"
C->>C : "计算 dataEndIndex 与 fraction"
C->>C : "构建 visibleData含插值点"
C->>RC : "传入 visibleData、渐变配置、Y/X 轴域"
RC-->>U : "渲染图表与 HUD"
U-->>R : "实时显示年份/资产余额/收入/支出"
```
**图表来源**
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L70-L141)
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L175-L210)
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L292-L341)
## 详细组件分析
### 数据模型与类型映射
- YearlySnapshot:单年度数据结构,包含年份、年龄(主被保险人与可选配偶)、财务指标(收入、支出、资产余额),以及可选事件与关键标记。
- LifePlanChartProps:组件属性,包括 timeline、meta(货币单位、起始年份)、是否隐藏头部、外部传入的索引与进度,用于与上层联动或外部控制动画。
- render_props.json 映射:后端生成的 render_props.json 中的 charts.assetDepletion.timeline 字段与 YearlySnapshot 结构一致,可直接作为 defaultProps 传入 LifePlanChart。
```mermaid
erDiagram
YEARLY_SNAPSHOT {
number year
number rep_age
number? spouse_age
number income
number expenditure
number asset_balance
string[]? events
boolean? is_critical
}
LIFE_PLAN_CHART_PROPS {
YearlySnapshot[] timeline
object meta
boolean hide_header
number? external_data_index
number? external_fraction
}
RENDER_PROPS {
object meta
object charts
object script
object assets
}
RENDER_PROPS ||--|| LIFE_PLAN_CHART_PROPS : "defaultProps"
LIFE_PLAN_CHART_PROPS ||--o{ YEARLY_SNAPSHOT : "timeline"
```
**图表来源**
- [types.ts](file://src/types.ts#L1-L30)
- [render-props-types.ts](file://frontend/src/render-props-types.ts#L55-L61)
- [Root.tsx](file://src/Root.tsx#L17-L29)
**章节来源**
- [types.ts](file://src/types.ts#L1-L30)
- [render-props-types.ts](file://frontend/src/render-props-types.ts#L55-L61)
- [Root.tsx](file://src/Root.tsx#L17-L29)
### 自定义 X 轴刻度:三行标签渲染
- 渲染内容:年份(第一行)、主被保险人年龄(第二行,加粗)、配偶年龄(第三行,若无则显示“-”)。
- 实现要点:
- 通过 payload.index 获取当前刻度对应 timeline 的索引。
- 从 timelineData 中取出对应 YearlySnapshot,安全地渲染三行文本。
- 使用 translate(x,y) 对齐刻度位置,设置字体大小、颜色与对齐方式。
```mermaid
flowchart TD
Start["进入 CustomXAxisTick 渲染"] --> GetIndex["根据 payload.index 获取索引"]
GetIndex --> Lookup["从 timelineData 查找 YearlySnapshot"]
Lookup --> HasData{"是否存在数据点?"}
HasData --> |否| EndNull["返回空节点"]
HasData --> |是| RenderRows["绘制三行文本:
1) 年份
2) 主被保险人年龄加粗
3) 配偶年龄若存在"]
RenderRows --> End["完成渲染"]
```
**图表来源**
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L16-L65)
**章节来源**
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L16-L65)
### 渐变填充逻辑:正负资产分色
- 计算规则:
- 若全部资产余额非负:渐变完全为蓝色。
- 若全部资产余额非正:渐变完全为红色。
- 若混合:按最大值与最小值的比例计算零线位置,形成蓝到红的渐变过渡。
- 关键点:
- 基于“可见数据”计算梯度偏移,确保与当前屏幕显示一致。
- 通过 的两个 stop 点共同决定颜色分界,实现平滑过渡。
```mermaid
flowchart TD
Start["开始计算渐变偏移"] --> CheckEmpty{"visibleData 是否为空?"}
CheckEmpty --> |是| Off0["返回偏移 0"]
CheckEmpty --> |否| Extrema["计算 visibleData 中的最大值与最小值"]
Extrema --> AllPos{"最小值 >= 0 ?"}
AllPos --> |是| Off1["返回偏移 1全蓝"]
AllPos --> |否| AllNeg{"最大值 <= 0 ?"}
AllNeg --> |是| Off0["返回偏移 0全红"]
AllNeg --> |否| Mix["偏移 = 最大值/(最大值-最小值)"]
Off1 --> End["结束"]
Off0 --> End
Mix --> End
```
**图表来源**
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L142-L163)
**章节来源**
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L142-L163)
### HUD 区域:实时信息展示
- 内容:
- 当前年份
- 资产余额(正数为蓝色,负数为红色)
- 收入(绿色,带“+”前缀)
- 支出(红色,带“-”前缀)
- 数据来源:基于当前帧计算出的 dataEndIndex 对应的 YearlySnapshot。
- 设计考虑:使用统一背景与阴影提升可读性,布局在隐藏头部模式下仍保持一致性。
**章节来源**
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L203-L289)
### 动画数据流与插值算法
- 插值映射:将帧号映射到 timeline 的索引区间,得到 exactIndex。
- 数据切片:dataEndIndex 为 floor(exactIndex),fraction 为小数部分。
- 可见数据:取 timeline.slice(0, dataEndIndex+1),并在末尾插入按 fraction 插值得到的“当前帧”插值点。
- 插值字段:
- year:线性插值
- ages.rep/spouse:线性插值(若一方缺失则不参与插值)
- financials.income/expenditure/assetBalance:线性插值
- 外部控制:支持 externalDataIndex 与 externalFraction,便于与其他图表联动(例如 DualChartView)。
```mermaid
flowchart TD
F0["useCurrentFrame()"] --> Map["interpolate(帧号) => exactIndex"]
Map --> Split["dataEndIndex = floor(exactIndex)
fraction = exactIndex - dataEndIndex"]
Split --> Slice["visibleData = timeline.slice(0, dataEndIndex+1)"]
Split --> Interp{"fraction > 0 且 dataEndIndex < length-1 ?"}
Interp --> |是| Build["构造插值点year/ages/financials"]
Interp --> |否| Skip["跳过插值"]
Build --> Append["追加到 visibleData"]
Skip --> Append
Append --> Render["Recharts 渲染"]
```
**图表来源**
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L81-L141)
**章节来源**
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L81-L141)
### Y 轴域与 X 轴域策略
- Y 轴域:基于全量 timeline 的资产余额极值计算,添加 10% 边距,避免动画过程中的剧烈跳变。
- X 轴域:当可见数据长度小于 10 时,固定 X 轴范围为 timeline[0].year 到 timeline[0].year + 9,确保标签可读;否则使用类别类型自动适配。
**章节来源**
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L175-L189)
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L311-L319)
### 与 render_props.json 的映射关系示例
- 后端输出:charts.assetDepletion.timeline 为 YearlySnapshot 数组,meta 包含 currencyUnit 与 startYear。
- 前端使用:在 Root.tsx 中将 mock-data.json 的 timeline 与 meta 作为 defaultProps 传给 LifePlanChart。
- 双图联动:在 DualChartView.tsx 中将 CashFlowData 转换为 YearlySnapshot 形态,再传给 LifePlanChart,实现两图同步动画。
```mermaid
sequenceDiagram
participant Backend as "Python 后端"
participant JSON as "render_props.json"
participant Frontend as "Remotion 前端"
participant Root as "Root.tsx"
participant LifePlan as "LifePlanChart"
Backend->>JSON : "生成 charts.assetDepletion"
Frontend->>Root : "加载 render_props.json"
Root->>LifePlan : "defaultProps = { timeline, meta }"
LifePlan-->>Frontend : "渲染资产消耗趋势图"
```
**图表来源**
- [render-props-types.ts](file://frontend/src/render-props-types.ts#L55-L61)
- [Root.tsx](file://src/Root.tsx#L17-L29)
- [DualChartView.tsx](file://src/DualChartView.tsx#L48-L76)
**章节来源**
- [render-props-types.ts](file://frontend/src/render-props-types.ts#L55-L61)
- [Root.tsx](file://src/Root.tsx#L17-L29)
- [DualChartView.tsx](file://src/DualChartView.tsx#L48-L76)
### 组件类图(代码级)
```mermaid
classDiagram
class YearlySnapshot {
+number year
+object ages
+number ages.rep
+number? ages.spouse
+object financials
+number financials.income
+number financials.expenditure
+number financials.assetBalance
+string[]? events
+boolean? isCritical
}
class LifePlanChartProps {
+YearlySnapshot[] timeline
+object meta
+string meta.currencyUnit
+number meta.startYear
+boolean hideHeader
+number? externalDataIndex
+number? externalFraction
}
class LifePlanChart {
+React.FC
+useCurrentFrame()
+interpolate()
+React.memo()
}
LifePlanChartProps --> YearlySnapshot : "timeline"
LifePlanChart --> LifePlanChartProps : "接收"
```
**图表来源**
- [types.ts](file://src/types.ts#L1-L30)
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L70-L110)
**章节来源**
- [types.ts](file://src/types.ts#L1-L30)
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L70-L110)
## 依赖关系分析
- 组件依赖:
- Recharts:AreaChart、XAxis、YAxis、CartesianGrid、ReferenceLine、ResponsiveContainer
- Remotion:useCurrentFrame、useVideoConfig、interpolate
- React:memo、useMemo、useCurrentFrame
- 数据依赖:
- YearlySnapshot 接口定义数据结构
- render_props.json 的 charts.assetDepletion.timeline 与 meta
- 上层依赖:
- Root.tsx 注册 Composition 并传入 defaultProps
- DualChartView.tsx 将 CashFlowData 转换为 YearlySnapshot 传入 LifePlanChart
```mermaid
graph LR
LifePlan["LifePlanChart.tsx"] --> Recharts["Recharts 组件"]
LifePlan --> Remotion["Remotion hooks"]
LifePlan --> Types["types.ts"]
Root["Root.tsx"] --> LifePlan
Dual["DualChartView.tsx"] --> LifePlan
RenderTypes["render-props-types.ts"] --> Root
Mock["mock-data.json"] --> Root
```
**图表来源**
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L1-L30)
- [Root.tsx](file://src/Root.tsx#L1-L68)
- [DualChartView.tsx](file://src/DualChartView.tsx#L1-L176)
- [types.ts](file://src/types.ts#L1-L30)
- [render-props-types.ts](file://frontend/src/render-props-types.ts#L55-L61)
- [mock-data.json](file://src/mock-data.json#L1-L61)
**章节来源**
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L1-L30)
- [Root.tsx](file://src/Root.tsx#L1-L68)
- [DualChartView.tsx](file://src/DualChartView.tsx#L1-L176)
- [types.ts](file://src/types.ts#L1-L30)
- [render-props-types.ts](file://frontend/src/render-props-types.ts#L55-L61)
- [mock-data.json](file://src/mock-data.json#L1-L61)
## 性能考量
- 使用 React.memo 包裹 LifePlanChart,避免 props 未变化时的重渲染。
- 使用 React.useMemo:
- 缓存 gradientOffset,仅在 visibleData 变化时重新计算。
- 缓存 xAxisDomain,避免每帧重建数组。
- 在上层(如 DualChartView)将 CashFlowData 转换为 YearlySnapshot 的结果缓存,避免每帧重建数组。
- 关闭 AreaChart 内置动画(isAnimationActive=false),改用插值驱动的平滑动画,减少不必要的重绘。
- Y 轴域基于全量 timeline 计算,避免动画过程中的跳变,降低视觉抖动。
- X 轴域在短序列时固定范围,提高标签可读性与渲染稳定性。
**章节来源**
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L67-L70)
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L142-L174)
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L175-L189)
- [DualChartView.tsx](file://src/DualChartView.tsx#L48-L76)
## 故障排查指南
- HUD 数值异常:
- 检查 currentData 来源是否正确(dataEndIndex 对应的 timeline 元素)。
- 确认 financials 字段是否包含 income、expenditure、assetBalance。
- 渐变颜色不符合预期:
- 确认 visibleData 是否为空或仅包含正/负值。
- 检查 gradientOffset 的计算逻辑与 的 stop 配置。
- X 轴标签错位或拥挤:
- 检查 CustomXAxisTick 的 payload.index 与 timelineData 的索引映射。
- 调整 interval 与 domain,或在短序列时固定 X 轴范围。
- 动画卡顿或跳变:
- 确保关闭 AreaChart 内置动画,使用插值驱动。
- 检查 externalDataIndex 与 externalFraction 是否由上层正确传递。
- 使用 memo 与 useMemo 缓存昂贵计算。
**章节来源**
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L16-L65)
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L142-L189)
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L311-L341)
- [DualChartView.tsx](file://src/DualChartView.tsx#L120-L130)
## 结论
LifePlanChart 通过 Remotion 的逐帧插值与 Recharts 的 AreaChart,实现了资产余额随生命周期平滑推进的可视化。其三行 X 轴标签清晰呈现时间与年龄信息,渐变填充直观表达资产正负状态,HUD 实时反馈关键财务指标。借助 memo 与 useMemo 的性能优化,以及与 render_props.json 的强类型映射,组件在复杂视频场景中具备良好的稳定性与扩展性。
## 附录
### 初学者使用示例
- 直接渲染(使用 mock 数据):
- 在 Root.tsx 中注册 LifePlanChart Composition,并将 mock-data.json 的 timeline 与 meta 作为 defaultProps 传入。
- 运行 Remotion 渲染,即可看到资产消耗趋势动画。
- 双图联动:
- 在 DualChartView.tsx 中将 CashFlowData 转换为 YearlySnapshot,分别传给 LifePlanChart 与 CashFlowChart,两者共享同一动画进度,实现同步推进。
**章节来源**
- [Root.tsx](file://src/Root.tsx#L17-L29)
- [DualChartView.tsx](file://src/DualChartView.tsx#L48-L76)
### 高级开发者优化建议
- 性能优化:
- 在上层(如 DualChartView)对数据转换结果使用 useMemo 缓存。
- 在 LifePlanChart 内部对 gradientOffset、xAxisDomain、visibleData 的构建进行 memo 化。
- 避免在渲染函数中创建新对象或数组,减少不必要的重渲染。
- 自定义动画曲线:
- 通过自定义 interpolate 的缓动函数(如 easeInOut)替换默认线性映射,改变推进速度曲线。
- 在外部控制外部动画进度(externalDataIndex 与 externalFraction),实现多图表联动与同步暂停/加速。
- 数据校验:
- 使用 render-props-types.ts 中的 Zod Schema 对 render_props.json 进行运行时校验,确保数据结构一致性。
**章节来源**
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L67-L70)
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L142-L174)
- [render-props-types.ts](file://frontend/src/render-props-types.ts#L194-L231)