[{"data":1,"prerenderedAt":7},["ShallowReactive",2],{"example-markdown-content:zh:google-income-statement-ribbon-tutorial":3},{"markdown":4,"isPaidExample":5,"isTruncated":5,"charLimit":6},"# 构建一个分步骤展示的 Google 损益表丝带关系图\n\n## 这个示例构建了什么\n\n这个示例构建的不是一个固定不变的单一图，而是一个围绕单个损益表数据集的分步骤引导式查看器。一个悬浮的辅助窗口允许用户在六个阶段之间切换：普通树图、节点插槽版本、按金额缩放节点版本、填充丝带线版本、感知偏移的丝带版本，以及最终的品牌化信息图版本。\n\n画布中，收入分支流向一个合成根节点，费用或利润分支则从该根节点流出，因此最终画面读起来更像财务拆解图，而不是通用层级结构。这个示例的主要价值不仅在于最终外观，还在于所有中间步骤都保留在同一个示例中可见。\n\n## 数据是如何组织的\n\n`my-data.ts` 中的源数据是一个嵌套对象，包含两个顶层分支：`revenues` 和 `expenses`。`getMyJsonData()` 会创建一个合成的 `root` 节点，然后由 `findNodesFromOrignData()` 递归地把嵌套的 `details` 数组拍平成 `RGJsonData` 所需的 `nodes` 和 `lines`。\n\n预处理阶段还会在每条连线上存储渲染元数据。每一条生成的边都会在 `line.data` 中携带 `fromOffsetYPercent`、`fromHeightPercent`、`toOffsetYPercent` 和 `toHeightPercent`，后续自定义连线渲染器会用这些值来控制丝带高度和挂接位置。\n\n生成链接的方向是有意设计的。收入子节点朝向根节点连接，而费用和利润一侧则从根节点向外连接。在实际项目中，这种模式可以表示收入构成、成本拆解、利润桥接、预算分配树，或任何边的粗细需要表达贡献占比的嵌套指标结构。\n\n## relation-graph 是如何使用的\n\n每个阶段都挂载在各自的 `RGProvider` 中，每个步骤组件都使用 `RGHooks.useGraphInstance()`，并在 `useEffect` 中执行初始化流程。各版本共享的基础 `RelationGraph` 配置保持稳定：树布局、从左到右方向、`treeNodeGapH: 300`、`treeNodeGapV: 100`、矩形节点、标准曲线连线、3px 线宽，以及左右连接点。\n\n随后，这个示例按层逐步增加定制能力。第 2 步引入 `RGSlotOnNode`，用自定义 HTML 内容替换默认节点主体。第 3 步保留插槽方式，但让节点高度与 `node.data.amount` 成比例，并在锚点盒子上方增加业务指标。第 4 步加入 `RGSlotOnLine`，用 `createAreaLinePath()` 生成的填充 SVG 区域替代细线描边。第 5 步则升级为 `createAreaLinePathWithOffset()`，它会读取预处理阶段生成的百分比数据，让每条丝带只占据对应比例的垂直切片。\n\n在后续的自定义连线阶段中，示例通过 `graphInstance.getLinkByLine(lineConfig.line)` 恢复实时几何信息，并通过 `graphInstance.onLineClick(...)` 手动把点击转发回 relation-graph，这样自定义 SVG 路径仍然能参与图的交互。最终阶段同时保留两类插槽，加入面向具体产品的选中节点 SVG 组件，绑定 `onLineClick` 和 `onCanvasClick`，并在 `zoomToFit()` 之后额外执行一次 `zoom(-10)`，以便拉开信息图构图空间。\n\n展示层细节通过本地样式处理，而不是通过布局改动实现。`Step4Version.scss` 会移除可见节点边框，添加多径向背景，定义丝带透明度与悬停行为，并加入用于已选节点和品牌说明块的光晕样式。\n\n## 关键交互\n\n- 悬浮辅助窗口会在六个完整的图实现之间切换 `stepName`，并同步更新说明文字。\n- 辅助窗口可以通过标题栏拖拽，也可以最小化，这让用户在不丢失步骤控制的情况下检查图形。\n- 在第 4 步及后续阶段中，可见的连线主体是设置了 `pointer-events: fill` 的填充 SVG 丝带，因此默认描边在视觉上被替换后，自定义渲染的边仍可点击。\n- 在最终阶段中，丝带点击仍会被转发回 relation-graph，而画布点击会通过 `graphInstance.clearChecked()` 清除选中状态。\n\n## 关键代码片段\n\n下面这个片段展示了包装组件如何切换完整的图变体，这也是为什么该示例更像一个教程，而不是单一的最终视图。\n\n```tsx\n{stepName === 'v1' && \u003CRGProvider>\u003CStep1Version />\u003C/RGProvider>}\n{stepName === 'v2' && \u003CRGProvider>\u003CStep2Version />\u003C/RGProvider>}\n{stepName === 'v3' && \u003CRGProvider>\u003CStep3Version />\u003C/RGProvider>}\n{stepName === 'v4' && \u003CRGProvider>\u003CStep4Version />\u003C/RGProvider>}\n{stepName === 'v5' && \u003CRGProvider>\u003CStep5Version />\u003C/RGProvider>}\n{stepName === 'final' && \u003CRGProvider>\u003CStepFinalVersion />\u003C/RGProvider>}\n```\n\n下面这个片段展示了拍平后的图如何在每条连线上保留百分比元数据，以供后续丝带几何计算使用。\n\n```ts\nallJsonLines.push({\n  from: parentJsonNode.id,\n  to: cJsonNode.id,\n  color: cJsonNode.color,\n  data: {\n    fromOffsetYPercent: sumAmount / parentJsonNode.data.amount,\n    fromHeightPercent: cJsonNode.data.amount / parentJsonNode.data.amount,\n    toOffsetYPercent: 0,\n    toHeightPercent: 1,\n  }\n});\n```\n\n下面这个片段展示了节点高度如何直接由业务金额驱动，而不是依赖固定的视觉预设。\n\n```tsx\n\u003Cdiv style={{width: '40px', height: `${(node.data?.amount / 100) * 200}px`, position: 'relative'}}>\n  \u003Cdiv style={{\n    position: 'absolute',\n    top: '0px',\n    left: '0px',\n    color: node.color,\n    width: '200px',\n  }}>\n```\n\n下面这个片段展示了自定义连线插槽如何使用实时连线几何信息，并把点击再转发回 relation-graph。\n\n```tsx\nconst link = graphInstance.getLinkByLine(lineConfig.line)!;\nconst path = createAreaLinePathWithOffset(\n  link.fromNode,\n  link.toNode,\n  lineConfig.line,\n  1\n);\n\nconst onClick = (e) => {\n  graphInstance.onLineClick(lineConfig.line, e);\n};\n```\n\n下面这个片段展示了最终阶段如何在节点插槽内部加入品牌专属说明块，而不是修改底层图数据结构。\n\n```tsx\n{node.text === 'Google Cloud' && (\n  \u003Cdiv className=\"c-node-icon-desc\">\n    \u003CIconForGoogleCloud/>\n    \u003Cdiv style={{\n      fontSize: '20px',\n      color: '#666666',\n      width: '250px',\n    }}>{node.data.desc}\u003C/div>\n  \u003C/div>\n)}\n```\n\n## 这个示例的独特之处\n\n根据准备好的对比数据，最接近的相邻示例是 `demo-for-google-income-statement-final`，但两者意图不同。那个只展示最终结果的相邻示例，是一个面向成品信息图的简洁参考；而这个示例保留了从普通树渲染到品牌化丝带图的完整构建路径。\n\n稀缺之处不在于某一个单独的 API 调用，而在于这些能力的组合。这个示例把围绕合成根节点的递归预处理、收入与费用流向相反的分支方向、按金额缩放的节点插槽、可点击的丝带式连线插槽，以及一个可拖拽的教程外壳组合在一起，并在其中切换六种实现。因此，当目标是教学或研究渐进式图定制时，它会是一个很强的起点。\n\n对比数据也将它与其他自定义连线查看器区分开来。相较于 `adv-line-slot-2`，这里的自定义连线逻辑用于表达财务构成，而不是路线状态或运动轨迹。相较于 `node-line-tips-contentmenu` 和 `customize-fullscreen-action` 这类示例，外围窗口主要承担的是分阶段视觉演化的编排层职责，而不是 tooltip/menu 展示或全屏包装教学。\n\n## 这种模式还适用于哪里\n\n这种模式非常适合用于投资者关系可视化、CFO 仪表盘，以及需要把单个父级指标拆解为带品牌属性且能直观看到占比权重的内部业务复盘场景。\n\n它也适用于预算分配视图、gross-to-net bridge、云成本拆解，以及产品组合构成图等场景。在这些场景中，树布局仍然有价值，但细线边不足以表达贡献占比。\n\n这种教程外壳式方法也可以复用于金融之外的场景。团队可以用同样的多阶段包装方式，展示一个普通 relation-graph 示例如何逐步演进成更定制化的交付物，而不必让读者手工对比多个独立 demo。\n",false,500,1782615382154]