AI-video-lifeplan-demo/.qoder/repowiki/zh/content/前端组件体系/资产消耗图表 (LifePlanChart).md

427 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 资产消耗图表 (LifePlanChart)
<cite>
**本文引用的文件**
- [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)
</cite>
## 目录
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<br/>注册 Composition"]
LifePlan["LifePlanChart.tsx<br/>资产消耗趋势图"]
Dual["DualChartView.tsx<br/>双图联动视图"]
end
subgraph "数据层"
Mock["mock-data.json<br/>演示数据"]
Types["types.ts<br/>YearlySnapshot 接口"]
RenderTypes["render-props-types.ts<br/>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["绘制三行文本:<br/>1) 年份<br/>2) 主被保险人年龄加粗<br/>3) 配偶年龄若存在"]
RenderRows --> End["完成渲染"]
```
**图表来源**
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L16-L65)
**章节来源**
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L16-L65)
### 渐变填充逻辑:正负资产分色
- 计算规则:
- 若全部资产余额非负:渐变完全为蓝色。
- 若全部资产余额非正:渐变完全为红色。
- 若混合:按最大值与最小值的比例计算零线位置,形成蓝到红的渐变过渡。
- 关键点:
- 基于“可见数据”计算梯度偏移,确保与当前屏幕显示一致。
- 通过 <linearGradient> 的两个 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)<br/>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<LifePlanChartProps>
+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 的计算逻辑与 <linearGradient> 的 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)