364 lines
16 KiB
Markdown
364 lines
16 KiB
Markdown
# 图表渲染
|
||
|
||
<cite>
|
||
**本文引用的文件列表**
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx)
|
||
- [CashFlowChart.tsx](file://src/CashFlowChart.tsx)
|
||
- [DualChartView.tsx](file://src/DualChartView.tsx)
|
||
- [LifePlanBarChart.tsx](file://src/LifePlanBarChart.tsx)
|
||
- [types.ts](file://src/types.ts)
|
||
- [Root.tsx](file://src/Root.tsx)
|
||
- [index.ts](file://src/index.ts)
|
||
- [render-props-types.ts](file://frontend/src/render-props-types.ts)
|
||
- [mock-data.json](file://src/mock-data.json)
|
||
- [mock-cashflow.json](file://src/mock-cashflow.json)
|
||
</cite>
|
||
|
||
## 目录
|
||
1. [简介](#简介)
|
||
2. [项目结构](#项目结构)
|
||
3. [核心组件](#核心组件)
|
||
4. [架构总览](#架构总览)
|
||
5. [组件详解](#组件详解)
|
||
6. [依赖关系分析](#依赖关系分析)
|
||
7. [性能考量](#性能考量)
|
||
8. [故障排查指南](#故障排查指南)
|
||
9. [结论](#结论)
|
||
10. [附录:数据绑定与扩展示例](#附录数据绑定与扩展示例)
|
||
|
||
## 简介
|
||
本文件围绕财务可视化图表渲染展开,重点说明 LifePlanChart 与 CashFlowChart 如何基于 render_props.json 中的数据实现动态动画效果;解释 DualChartView 的双图表同步机制与 LifePlanBarChart 的时间轴展示逻辑;阐述 React 组件如何利用 Recharts 库实现平滑数据插值动画并与解说脚本的时间轴精确同步。同时给出面向初学者的数据绑定示例与面向高级开发者的性能优化策略(如 React.memo、虚拟化渲染)及响应式设计模式,并提供自定义样式与新增财务指标的方法。
|
||
|
||
## 项目结构
|
||
前端使用 Remotion 构建视频合成,图表组件通过 Recharts 渲染,数据来自 mock 文件与后端生成的 render_props.json。Root.tsx 将各图表注册为 Composition,供渲染器调用。
|
||
|
||
```mermaid
|
||
graph TB
|
||
subgraph "Remotion 前端"
|
||
IDX["index.ts<br/>注册根组件"]
|
||
ROOT["Root.tsx<br/>注册 Composition"]
|
||
LIFEC["LifePlanChart.tsx"]
|
||
CASH["CashFlowChart.tsx"]
|
||
DUAL["DualChartView.tsx"]
|
||
BAR["LifePlanBarChart.tsx"]
|
||
TYPES["types.ts"]
|
||
RPT["render-props-types.ts"]
|
||
end
|
||
subgraph "数据"
|
||
MDATA["mock-data.json"]
|
||
MCASH["mock-cashflow.json"]
|
||
end
|
||
IDX --> ROOT
|
||
ROOT --> LIFEC
|
||
ROOT --> CASH
|
||
ROOT --> DUAL
|
||
ROOT --> BAR
|
||
LIFEC --> TYPES
|
||
CASH --> TYPES
|
||
DUAL --> LIFEC
|
||
DUAL --> CASH
|
||
DUAL --> TYPES
|
||
LIFEC --> MDATA
|
||
CASH --> MCASH
|
||
DUAL --> MCASH
|
||
RPT --> DUAL
|
||
```
|
||
|
||
**图示来源**
|
||
- [index.ts](file://src/index.ts#L1-L6)
|
||
- [Root.tsx](file://src/Root.tsx#L1-L68)
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L1-L349)
|
||
- [CashFlowChart.tsx](file://src/CashFlowChart.tsx#L1-L441)
|
||
- [DualChartView.tsx](file://src/DualChartView.tsx#L1-L176)
|
||
- [LifePlanBarChart.tsx](file://src/LifePlanBarChart.tsx#L1-L258)
|
||
- [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)
|
||
- [mock-cashflow.json](file://src/mock-cashflow.json#L1-L200)
|
||
|
||
**章节来源**
|
||
- [index.ts](file://src/index.ts#L1-L6)
|
||
- [Root.tsx](file://src/Root.tsx#L1-L68)
|
||
|
||
## 核心组件
|
||
- LifePlanChart:资产余额趋势面积图,支持 HUD 与渐变色背景,基于帧数进行平滑插值,禁用 Recharts 内部动画以保证与 Remotion 同步。
|
||
- CashFlowChart:收支明细组合图(堆叠柱状+收入线),按年内进度分段增长,禁用 Recharts 内部动画,支持固定域补零与 HUD。
|
||
- DualChartView:双图表容器,统一动画进度,将 CashFlow 数据转换为 LifePlan 格式并传入子组件,实现同步高亮与动画。
|
||
- LifePlanBarChart:资产余额柱状图,逐年的柱子线性增长,禁用 Recharts 内部动画,支持固定域补零与颜色区分正负。
|
||
|
||
**章节来源**
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L1-L349)
|
||
- [CashFlowChart.tsx](file://src/CashFlowChart.tsx#L1-L441)
|
||
- [DualChartView.tsx](file://src/DualChartView.tsx#L1-L176)
|
||
- [LifePlanBarChart.tsx](file://src/LifePlanBarChart.tsx#L1-L258)
|
||
|
||
## 架构总览
|
||
图表渲染采用“外部驱动帧率 + 内部数据切片”的模式:
|
||
- 使用 useCurrentFrame 与 interpolate 计算全局动画进度,确保两图同步。
|
||
- 子组件根据外部传入的 dataEndIndex/yearProgress 进行可见数据切片与插值点注入,避免 Recharts 自身动画导致的跳变。
|
||
- DualChartView 将 CashFlow 数据映射为 LifePlan 格式,使两条曲线在同一时间轴上对齐。
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant Frame as "帧计数(useCurrentFrame)"
|
||
participant Interp as "插值(interpolate)"
|
||
participant DView as "DualChartView"
|
||
participant LChart as "LifePlanChart"
|
||
participant CChart as "CashFlowChart"
|
||
Frame->>Interp : 计算 exactProgress
|
||
Interp-->>DView : 返回 dataEndIndex/yearProgress
|
||
DView->>LChart : 传入 externalDataIndex/externalFraction
|
||
DView->>CChart : 传入 externalDataIndex/externalYearProgress
|
||
LChart->>LChart : 切片可见数据 + 插值点
|
||
CChart->>CChart : 切片可见数据 + 分段增长
|
||
LChart-->>DView : HUD 当前数据
|
||
CChart-->>DView : HUD 当前数据
|
||
```
|
||
|
||
**图示来源**
|
||
- [DualChartView.tsx](file://src/DualChartView.tsx#L31-L77)
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L81-L141)
|
||
- [CashFlowChart.tsx](file://src/CashFlowChart.tsx#L86-L179)
|
||
|
||
## 组件详解
|
||
|
||
### LifePlanChart 组件
|
||
- 动画与插值
|
||
- 通过 interpolate 将帧数映射到 timeline 索引,得到 dataEndIndex 与 fraction。
|
||
- 对当前年与下一年进行线性插值,生成 interpolatedPoint 并拼接到可见数据末尾,形成平滑过渡。
|
||
- 渐变与坐标轴
|
||
- gradientOffset 基于可见数据计算,确保渐变与屏幕显示一致。
|
||
- Y 轴基于全量 timeline 计算域,避免资产余额剧烈变化时 Y 轴跳变。
|
||
- X 轴在固定域(少于 10 年)时设置 domain 与 type,防止偏移。
|
||
- HUD 与样式
|
||
- HUD 展示当前年份、资产余额、收入与支出,数值颜色随正负变化。
|
||
- 禁用 Recharts 内部动画,由外部帧驱动。
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
Start(["进入组件"]) --> Calc["计算 exactIndex/fraction"]
|
||
Calc --> Slice["切片 visibleData 到 dataEndIndex"]
|
||
Slice --> Interp{"fraction > 0 且有下一年?"}
|
||
Interp -- 是 --> MakePoint["生成插值点 interpolatedPoint"]
|
||
MakePoint --> Append["追加到 visibleData"]
|
||
Interp -- 否 --> Skip["跳过插值"]
|
||
Append --> Grad["计算 gradientOffset"]
|
||
Skip --> Grad
|
||
Grad --> Domain["计算 Y/X 轴 domain"]
|
||
Domain --> Render["渲染 AreaChart/HUD"]
|
||
Render --> End(["退出"])
|
||
```
|
||
|
||
**图示来源**
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L81-L190)
|
||
|
||
**章节来源**
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L1-L349)
|
||
- [types.ts](file://src/types.ts#L1-L30)
|
||
|
||
### CashFlowChart 组件
|
||
- 动画与分段增长
|
||
- 通过 interpolate 得到 exactProgress,拆分为 dataEndIndex 与 yearProgress。
|
||
- 将一年分为 6 段(5 种支出类别 + 收入线),按段起止计算 getMultiplier,仅对当前年份的支出类目应用该系数,收入线保持不变。
|
||
- 插值与固定域
|
||
- 在当前年与下一年之间插入 interpolatedPoint,X 方向延伸至 year + yearProgress,Y 方向保持当前年不变。
|
||
- 固定域模式下补零年份,避免偏移。
|
||
- HUD 与样式
|
||
- HUD 展示当前年份、年龄、总收入、总支出与结余,颜色随正负变化。
|
||
- 禁用 Recharts 内部动画,由外部帧驱动。
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
Start(["进入组件"]) --> Progress["计算 exactProgress/yearProgress"]
|
||
Progress --> Slice["切片 visibleData 到 dataEndIndex"]
|
||
Slice --> Cur{"是否为当前年?"}
|
||
Cur -- 是 --> Seg["按段计算 getMultiplier"]
|
||
Seg --> Apply["对当前年支出项乘以系数"]
|
||
Cur -- 否 --> Keep["保持原值"]
|
||
Apply --> Interp["生成 interpolatedPoint"]
|
||
Keep --> Interp
|
||
Interp --> Pad{"visibleData < 10?"}
|
||
Pad -- 是 --> Fill["补零年份"]
|
||
Pad -- 否 --> Skip["跳过补零"]
|
||
Fill --> Domain["计算 Y/X 轴 domain"]
|
||
Skip --> Domain
|
||
Domain --> Render["渲染 ComposedChart/HUD"]
|
||
Render --> End(["退出"])
|
||
```
|
||
|
||
**图示来源**
|
||
- [CashFlowChart.tsx](file://src/CashFlowChart.tsx#L86-L237)
|
||
|
||
**章节来源**
|
||
- [CashFlowChart.tsx](file://src/CashFlowChart.tsx#L1-L441)
|
||
|
||
### DualChartView 双图表同步
|
||
- 统一动画
|
||
- 使用固定动画结束帧(animationEndFrame)与 interpolate,得到 dataEndIndex 与 yearProgress。
|
||
- 数据转换
|
||
- 将 CashFlow 数据映射为 LifePlan 格式(year/ages/financials),并标记 isCritical。
|
||
- 外部参数传递
|
||
- 通过 externalDataIndex 与 externalFraction/externalYearProgress 将统一进度传递给两个子组件,确保同步高亮与动画。
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant D as "DualChartView"
|
||
participant L as "LifePlanChart"
|
||
participant C as "CashFlowChart"
|
||
D->>D : 计算 exactProgress/dataEndIndex/yearProgress
|
||
D->>L : 传入 externalDataIndex/dataEndIndex
|
||
D->>L : 传入 externalFraction/yearProgress
|
||
D->>C : 传入 externalDataIndex/dataEndIndex
|
||
D->>C : 传入 externalYearProgress/yearProgress
|
||
L-->>D : HUD 当前数据
|
||
C-->>D : HUD 当前数据
|
||
```
|
||
|
||
**图示来源**
|
||
- [DualChartView.tsx](file://src/DualChartView.tsx#L31-L77)
|
||
|
||
**章节来源**
|
||
- [DualChartView.tsx](file://src/DualChartView.tsx#L1-L176)
|
||
|
||
### LifePlanBarChart 时间轴展示
|
||
- 线性增长
|
||
- 每帧计算 currentYearIndex 与 yearProgress,仅对当前年份的柱子应用 growthMultiplier,实现线性生长。
|
||
- 固定域补零
|
||
- 少于 10 年时补零年份,保证坐标轴稳定。
|
||
- 颜色与 Cell
|
||
- 通过 Cell 为每个柱子设置颜色,正负值分别使用不同颜色。
|
||
|
||
**章节来源**
|
||
- [LifePlanBarChart.tsx](file://src/LifePlanBarChart.tsx#L1-L258)
|
||
|
||
## 依赖关系分析
|
||
- 组件间依赖
|
||
- LifePlanChart 与 CashFlowChart 共同依赖 types.ts 中的 YearlySnapshot/CashFlowData 类型。
|
||
- DualChartView 同时依赖 LifePlanChart 与 CashFlowChart,并通过 useMemo 将 CashFlow 数据转换为 LifePlan 格式,减少重渲染。
|
||
- 外部依赖
|
||
- Recharts:AreaChart、ComposedChart、BarChart、XAxis、YAxis、ReferenceLine、ResponsiveContainer 等。
|
||
- Remotion:useCurrentFrame、useVideoConfig、interpolate 提供帧驱动的动画。
|
||
- 数据契约
|
||
- render-props-types.ts 定义了 render_props.json 的结构,包括 charts、script、assets 等,其中 script 中的 chartSync 字段用于与图表高亮联动。
|
||
|
||
```mermaid
|
||
graph LR
|
||
RPT["render-props-types.ts"] --> DUAL["DualChartView.tsx"]
|
||
DUAL --> LIFEC["LifePlanChart.tsx"]
|
||
DUAL --> CASH["CashFlowChart.tsx"]
|
||
LIFEC --> TYPES["types.ts"]
|
||
CASH --> TYPES
|
||
DUAL --> TYPES
|
||
ROOT["Root.tsx"] --> LIFEC
|
||
ROOT --> CASH
|
||
ROOT --> DUAL
|
||
ROOT --> BAR["LifePlanBarChart.tsx"]
|
||
```
|
||
|
||
**图示来源**
|
||
- [render-props-types.ts](file://frontend/src/render-props-types.ts#L70-L135)
|
||
- [DualChartView.tsx](file://src/DualChartView.tsx#L1-L176)
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L1-L349)
|
||
- [CashFlowChart.tsx](file://src/CashFlowChart.tsx#L1-L441)
|
||
- [types.ts](file://src/types.ts#L1-L30)
|
||
- [Root.tsx](file://src/Root.tsx#L1-L68)
|
||
|
||
**章节来源**
|
||
- [render-props-types.ts](file://frontend/src/render-props-types.ts#L1-L251)
|
||
- [Root.tsx](file://src/Root.tsx#L1-L68)
|
||
|
||
## 性能考量
|
||
- 减少重渲染
|
||
- 使用 React.memo 包裹图表组件,避免 props 未变化时的重复渲染。
|
||
- 使用 useMemo 缓存转换后的 LifePlan 数据与 meta,降低数组创建成本。
|
||
- 禁用内部动画
|
||
- 所有图表均设置 isAnimationActive=false,交由外部帧驱动,避免 Recharts 内部动画与 Remotion 帧驱动冲突。
|
||
- 坐标轴域稳定性
|
||
- Y 轴基于全量 timeline 计算域,X 轴在固定域时设置 domain 与 type,避免图表抖动。
|
||
- 虚拟化与大数据集
|
||
- 当 timeline 数据量较大时,建议:
|
||
- 仅渲染可见窗口内的数据(已通过切片实现)。
|
||
- 对 X 轴 tick 进行采样(interval 已设置)。
|
||
- 使用 ResponsiveContainer 与固定宽高,避免布局抖动。
|
||
- 样式与主题
|
||
- 使用统一的 margin、单位与字体,确保双图表视觉对齐。
|
||
|
||
[本节为通用性能建议,不直接分析具体文件]
|
||
|
||
## 故障排查指南
|
||
- 动画不同步
|
||
- 检查 DualChartView 是否使用相同的 interpolation 参数与 animationEndFrame。
|
||
- 确认子组件是否正确接收 externalDataIndex/externalFraction 或 externalYearProgress。
|
||
- Y 轴跳变
|
||
- 确保 Y 轴 domain 基于全量 timeline 计算,而非 visibleData。
|
||
- X 轴偏移
|
||
- 固定域模式下需设置 domain 与 type,避免坐标轴错位。
|
||
- HUD 数值异常
|
||
- 确认 HUD 使用的是原始数据而非已动画化的值(CashFlowChart 中已明确使用 flattenedData[dataEndIndex])。
|
||
- 解说脚本高亮
|
||
- render_props.json 的 script 中 chartSync 字段可指定 chartId、highlightYear 与 animationStart,用于在特定帧触发高亮或动画开始。
|
||
|
||
**章节来源**
|
||
- [DualChartView.tsx](file://src/DualChartView.tsx#L31-L77)
|
||
- [CashFlowChart.tsx](file://src/CashFlowChart.tsx#L226-L237)
|
||
- [render-props-types.ts](file://frontend/src/render-props-types.ts#L73-L93)
|
||
|
||
## 结论
|
||
本项目通过“外部帧驱动 + 内部数据切片”的方式,实现了与 Remotion 帧率严格同步的财务可视化动画。LifePlanChart 与 CashFlowChart 均禁用 Recharts 内部动画,配合 DualChartView 的统一进度控制,达到双图表同步高亮与平滑过渡的效果。render-props-types.ts 明确了脚本与图表的联动契约,便于后续扩展与维护。
|
||
|
||
[本节为总结,不直接分析具体文件]
|
||
|
||
## 附录:数据绑定与扩展示例
|
||
|
||
### 初学者:数据绑定示例
|
||
- 使用 Root.tsx 注册 Composition,并将 mock 数据作为 defaultProps 传入图表组件。
|
||
- LifePlanChart 接收 timeline 与 meta,CashFlowChart 接收 timeline 与 meta。
|
||
- DualChartView 接收 cashflowData,内部将 CashFlow 数据映射为 LifePlan 格式并传入 LifePlanChart。
|
||
|
||
参考路径
|
||
- [Root.tsx](file://src/Root.tsx#L14-L64)
|
||
- [mock-data.json](file://src/mock-data.json#L1-L61)
|
||
- [mock-cashflow.json](file://src/mock-cashflow.json#L1-L200)
|
||
|
||
**章节来源**
|
||
- [Root.tsx](file://src/Root.tsx#L14-L64)
|
||
|
||
### 高级开发者:性能优化策略
|
||
- 使用 React.memo 包裹图表组件,避免不必要的重渲染。
|
||
- 使用 useMemo 缓存昂贵的转换逻辑(如 DualChartView 中的映射)。
|
||
- 禁用 Recharts 内部动画,统一由外部帧驱动。
|
||
- 对大数据集进行可见窗口渲染与采样,减少 DOM 节点数量。
|
||
- 使用 ResponsiveContainer 与固定宽高,避免布局抖动。
|
||
|
||
**章节来源**
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L70-L78)
|
||
- [CashFlowChart.tsx](file://src/CashFlowChart.tsx#L75-L82)
|
||
- [DualChartView.tsx](file://src/DualChartView.tsx#L49-L67)
|
||
|
||
### 响应式设计模式
|
||
- 使用 ResponsiveContainer 适配不同分辨率。
|
||
- 通过 memoized 的 domain 与 type 控制坐标轴行为。
|
||
- 在固定域模式下补零年份,保证视觉一致性。
|
||
|
||
**章节来源**
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L166-L190)
|
||
- [CashFlowChart.tsx](file://src/CashFlowChart.tsx#L181-L211)
|
||
- [LifePlanBarChart.tsx](file://src/LifePlanBarChart.tsx#L105-L127)
|
||
|
||
### 自定义样式与新增财务指标
|
||
- 自定义样式
|
||
- 在 AreaChart/ComposedChart/BarChart 中设置 stroke、fill、strokeWidth、dot、connectNulls 等属性。
|
||
- 使用 ReferenceLine 添加零线等辅助线。
|
||
- 新增财务指标
|
||
- 在 CashFlowData 中增加新字段(如教育支出、保险费用等),并在 CashFlowChart 中添加对应条形与 HUD 展示。
|
||
- 在 LifePlanChart 的 financials 中加入新指标(如负债、投资收益等),并在 HUD 与图表中相应更新。
|
||
|
||
参考路径
|
||
- [CashFlowChart.tsx](file://src/CashFlowChart.tsx#L382-L432)
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L332-L340)
|
||
- [types.ts](file://src/types.ts#L1-L30)
|
||
|
||
**章节来源**
|
||
- [CashFlowChart.tsx](file://src/CashFlowChart.tsx#L382-L432)
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L332-L340)
|
||
- [types.ts](file://src/types.ts#L1-L30) |