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

18 KiB
Raw Blame History

资产消耗图表 (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 的类型契约。

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

图表来源

章节来源

核心组件

  • 组件名称:LifePlanChart
  • 功能定位:资产余额随生命周期变化的趋势可视化,支持 HUD 实时信息与平滑动画
  • 关键特性:
    • 基于 Recharts 的 AreaChart
    • 自定义 X 轴三行标签(年份、主被保险人年龄、配偶年龄)
    • 渐变填充(正资产蓝、负资产红)
    • 基于 Remotion 的逐帧插值动画
    • HUD 实时显示年份、资产余额、收入与支出

章节来源

架构总览

LifePlanChart 在 Remotion 中作为 Composition 的子组件运行,通过 useCurrentFrame 获取当前帧号,用 interpolate 将帧号映射到 timeline 的索引区间,从而计算出可见数据片段与插值点,实现“从左至右”推进的平滑动画。HUD 区域根据当前数据点实时更新;Y 轴域基于全量 timeline 计算,避免动画过程中因极值变化导致的跳变;X 轴域在短序列时固定范围,保证标签可读性。

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 : "实时显示年份/资产余额/收入/支出"

图表来源

详细组件分析

数据模型与类型映射

  • YearlySnapshot:单年度数据结构,包含年份、年龄(主被保险人与可选配偶)、财务指标(收入、支出、资产余额),以及可选事件与关键标记。
  • LifePlanChartProps:组件属性,包括 timeline、meta(货币单位、起始年份)、是否隐藏头部、外部传入的索引与进度,用于与上层联动或外部控制动画。
  • render_props.json 映射:后端生成的 render_props.json 中的 charts.assetDepletion.timeline 字段与 YearlySnapshot 结构一致,可直接作为 defaultProps 传入 LifePlanChart。
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"

图表来源

章节来源

自定义 X 轴刻度:三行标签渲染

  • 渲染内容:年份(第一行)、主被保险人年龄(第二行,加粗)、配偶年龄(第三行,若无则显示“-”)。
  • 实现要点:
    • 通过 payload.index 获取当前刻度对应 timeline 的索引。
    • 从 timelineData 中取出对应 YearlySnapshot,安全地渲染三行文本。
    • 使用 translate(x,y) 对齐刻度位置,设置字体大小、颜色与对齐方式。
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["完成渲染"]

图表来源

章节来源

渐变填充逻辑:正负资产分色

  • 计算规则:
    • 若全部资产余额非负:渐变完全为蓝色。
    • 若全部资产余额非正:渐变完全为红色。
    • 若混合:按最大值与最小值的比例计算零线位置,形成蓝到红的渐变过渡。
  • 关键点:
    • 基于“可见数据”计算梯度偏移,确保与当前屏幕显示一致。
    • 通过 的两个 stop 点共同决定颜色分界,实现平滑过渡。
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

图表来源

章节来源

HUD 区域:实时信息展示

  • 内容:
    • 当前年份
    • 资产余额(正数为蓝色,负数为红色)
    • 收入(绿色,带“+”前缀)
    • 支出(红色,带“-”前缀)
  • 数据来源:基于当前帧计算出的 dataEndIndex 对应的 YearlySnapshot。
  • 设计考虑:使用统一背景与阴影提升可读性,布局在隐藏头部模式下仍保持一致性。

章节来源

动画数据流与插值算法

  • 插值映射:将帧号映射到 timeline 的索引区间,得到 exactIndex。
  • 数据切片:dataEndIndex 为 floor(exactIndex),fraction 为小数部分。
  • 可见数据:取 timeline.slice(0, dataEndIndex+1),并在末尾插入按 fraction 插值得到的“当前帧”插值点。
  • 插值字段:
    • year:线性插值
    • ages.rep/spouse:线性插值(若一方缺失则不参与插值)
    • financials.income/expenditure/assetBalance:线性插值
  • 外部控制:支持 externalDataIndex 与 externalFraction,便于与其他图表联动(例如 DualChartView)。
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 渲染"]

图表来源

章节来源

Y 轴域与 X 轴域策略

  • Y 轴域:基于全量 timeline 的资产余额极值计算,添加 10% 边距,避免动画过程中的剧烈跳变。
  • X 轴域:当可见数据长度小于 10 时,固定 X 轴范围为 timeline[0].year 到 timeline[0].year + 9,确保标签可读;否则使用类别类型自动适配。

章节来源

与 render_props.json 的映射关系示例

  • 后端输出:charts.assetDepletion.timeline 为 YearlySnapshot 数组,meta 包含 currencyUnit 与 startYear。
  • 前端使用:在 Root.tsx 中将 mock-data.json 的 timeline 与 meta 作为 defaultProps 传给 LifePlanChart。
  • 双图联动:在 DualChartView.tsx 中将 CashFlowData 转换为 YearlySnapshot 形态,再传给 LifePlanChart,实现两图同步动画。
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 : "渲染资产消耗趋势图"

图表来源

章节来源

组件类图(代码级)

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 : "接收"

图表来源

章节来源

依赖关系分析

  • 组件依赖:
    • 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
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

图表来源

章节来源

性能考量

  • 使用 React.memo 包裹 LifePlanChart,避免 props 未变化时的重渲染。
  • 使用 React.useMemo:
    • 缓存 gradientOffset,仅在 visibleData 变化时重新计算。
    • 缓存 xAxisDomain,避免每帧重建数组。
    • 在上层(如 DualChartView)将 CashFlowData 转换为 YearlySnapshot 的结果缓存,避免每帧重建数组。
  • 关闭 AreaChart 内置动画(isAnimationActive=false),改用插值驱动的平滑动画,减少不必要的重绘。
  • Y 轴域基于全量 timeline 计算,避免动画过程中的跳变,降低视觉抖动。
  • X 轴域在短序列时固定范围,提高标签可读性与渲染稳定性。

章节来源

故障排查指南

  • 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 通过 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,两者共享同一动画进度,实现同步推进。

章节来源

高级开发者优化建议

  • 性能优化:
    • 在上层(如 DualChartView)对数据转换结果使用 useMemo 缓存。
    • 在 LifePlanChart 内部对 gradientOffset、xAxisDomain、visibleData 的构建进行 memo 化。
    • 避免在渲染函数中创建新对象或数组,减少不必要的重渲染。
  • 自定义动画曲线:
    • 通过自定义 interpolate 的缓动函数(如 easeInOut)替换默认线性映射,改变推进速度曲线。
    • 在外部控制外部动画进度(externalDataIndex 与 externalFraction),实现多图表联动与同步暂停/加速。
  • 数据校验:
    • 使用 render-props-types.ts 中的 Zod Schema 对 render_props.json 进行运行时校验,确保数据结构一致性。

章节来源