# 资产消耗图表 (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)