JavaScript is required

谷歌经营损益丝带图

这是一个独立的财务信息图查看器,将 Google 利润表渲染为左到右关系图,包含按金额缩放的节点条和按比例流动的丝带连线。它结合递归 RGJsonData 预处理、自定义节点与连线插槽,以及轻量选中态交互,形成紧凑的最终态参考。

Google Income Statement Ribbon 信息图

这个示例构建了什么

这个示例构建了一个独立的 Google 损益表财务信息图查看器。界面展示的是一种从左到右的拆解方式,其中收入类别汇入一个共享根节点,而支出类别则从该根节点向外分叉,因此整个图读起来更像是一份紧凑的报表摘要,而不是一棵通用树。

最显眼的部分包括按数据比例缩放的节点列、宽大的丝带式连接、针对部分 Google 业务线的品牌化标注,以及分层渐变背景。用户可以通过大面积可点击的 ribbon 区域检查流向带,也可以点击空白画布来清除 checked 状态。

它的主要价值在于封装方式。与相关的教程式示例相比,这个版本只暴露最终完成的查看器,因此更适合作为面向生产场景的参考来研究。

数据是如何组织的

源数据一开始是一个嵌套对象,具有两个顶层分支:revenuesexpenses。每个条目都携带业务字段,例如 nameamountcoloryear_over_year_changepercentage_of_revenuemargin 以及可选的 desc,而嵌套的 details 数组则描述更深层级的拆分。

在执行 setJsonData 之前,getMyJsonData() 会把这份嵌套结构转换为 RGJsonData,其中包含一个合成的根节点、一个扁平的 nodes 数组以及一个扁平的 lines 数组。递归辅助函数 findNodesFromOrignData() 还会把每个节点负载中的 details 移除,从而让剩余指标可以直接在节点插槽内部渲染。

这一步预处理不只是把树拍平。收入条目按 child -> parent 连接,支出条目按 parent -> child 连接,并且每条连线都会把基于百分比的偏移和高度元数据存入 line.data。正是这些额外的几何数据,使自定义连线插槽能够渲染出按比例变化的 ribbon,而不是统一粗细的笔画。

在真实应用中,这种结构同样可以表示损益表、预算分配树、类别贡献图,或任何既关心金额又关心流向占比的拆分视图。

relation-graph 是如何使用的

入口文件保持了最小化组合:RGProvider 包裹 StepFinalVersion,而实际的查看器组件则通过 RGHooks.useGraphInstance() 读取图实例。这个 hook 是示例中的核心,因为它同时用于图初始化阶段和自定义连线插槽内部。

图本身被配置为一个从左到右的树布局,并具有较大的水平与垂直间距。配置项让结构层的默认值保持简单:矩形基础节点、标准曲线连线、左右连接点、默认 3px 连线宽度,以及默认 1px 节点边框宽度。这些默认值主要承担图骨架的作用,因为真正可见的表现层会被插槽和 CSS 覆盖。

RelationGraph 绑定了两个图事件:onLineClickonCanvasClick。本地的连线处理函数只做日志输出,因此这个示例保持在查看器模式,而不会打开编辑器或面板。不过,画布处理函数是有实际作用的,因为它会调用 clearChecked() 来重置 checked 状态。

最重要的自定义点是 RGSlotOnNodeRGSlotOnLine。节点插槽会渲染一个 HTML 块,其高度依赖 node.data.amount,然后在上方堆叠节点标签、美元金额,以及可选的百分比或同比文本。连线插槽会通过 getLinkByLine() 解析当前实时连线,再使用 createAreaLinePathWithOffset() 重建 SVG 面积路径,并通过 graphInstance.onLineClick(...) 把点击事件转发回 relation-graph。

样式表则补全了最终效果。它用固定的径向渐变替换了朴素的画布背景,移除了可见的节点边框,为 checked 节点增加了光晕,并让自定义 ribbon 路径在 hover 和 checked 状态下通过提高透明度表现反馈。这些覆盖项共同把一个标准树布局转换成了面向展示的信息图。

关键交互

图会在挂载时初始化。initializeGraph() 加载转换后的 JSON 数据,将图移动到中心,按视口自适应缩放,然后再轻微缩小一点,以便完整构图拥有更充足的留白空间。

连线检查是通过自定义 ribbon 主体实现的,而不是通过细窄的默认线条。每个填充的 SVG 面积都会在整个表面上捕获点击,并把该事件转发到 relation-graph 内置的连线点击处理逻辑中,这使宽大的财务流向更容易被检查。

点击画布会起到重置作用。点击空白区域会清除 checked 状态,从而移除图元素上的活动视觉强调。

整个交互模型被刻意保持得很轻。这个示例没有加入编辑、钻取或侧边面板检查功能。甚至应用层的 onLineClick 处理函数也只是记录被点击的连线,因此重点始终放在图形呈现和 checked 状态反馈上。

关键代码片段

这个片段表明,这个独立示例只是对最终实现的一层 provider 包装。

const Example: React.FC = () => {

    return (
        <RGProvider><StepFinalVersion /></RGProvider>
    );
};

这个片段展示了从左到右树布局的基础配置,以及支撑自定义渲染层的图默认值。

const graphOptions: RGOptions = {
    debug: false,
    layout: {
        layoutName: 'tree',
        from: 'left',
        treeNodeGapH: 300,
        treeNodeGapV: 100
    },
    defaultNodeShape: RGNodeShape.rect,
    defaultLineShape: RGLineShape.StandardCurve,
    defaultLineWidth: 3,
    defaultJunctionPoint: RGJunctionPoint.lr,
    defaultNodeBorderWidth: 1
};

这个片段展示了挂载时的数据加载与视口调整。

const initializeGraph = async () => {
    const myJsonData = await getMyJsonData();
    await graphInstance.setJsonData(myJsonData);
    graphInstance.moveToCenter();
    graphInstance.zoomToFit();
    graphInstance.zoom(-10); // Zoom out by 10% based on current zoom level
};

这个片段证明,收入连线会以 child-to-parent ribbon 的形式存储,并带有基于百分比的偏移信息。

if (income) {
    allJsonLines.push({
        from: cJsonNode.id,
        to: parentJsonNode.id,
        color: cJsonNode.color,
        data: {
            fromOffsetYPercent: 0,
            fromHeightPercent: 1,
            toOffsetYPercent: sumAmount / parentJsonNode.data.amount,
            toHeightPercent: cJsonNode.data.amount / parentJsonNode.data.amount,
        }
    });
}

这个片段证明,支出连线会反转方向,同时保留相同的比例几何思路。

allJsonLines.push({
    from: parentJsonNode.id,
    to: cJsonNode.id,
    color: cJsonNode.color,
    data: {
        fromOffsetYPercent: sumAmount / parentJsonNode.data.amount,
        fromHeightPercent: cJsonNode.data.amount / parentJsonNode.data.amount,
        toOffsetYPercent: 0,
        toHeightPercent: 1,
    }
});

这个片段展示了由金额驱动的节点标记,以及层叠显示的财务标签。

<div style={{width: '40px', height: `${(node.data?.amount / 100) * 200}px`, position: 'relative'}}>
    <div style={{
        position: 'absolute',
        top: '0px',
        left: '0px',
        color: '#0c63ff',
        width: '200px',
        whiteSpace: 'nowrap',
        textAlign: 'left'
    }}>
        <div style={{transform: 'translateY(-110%)'}}>
            <div style={{fontSize: '24px'}}>{node.text}</div>
            <div style={{fontSize: '22px'}}>$ {node.data.amount} B</div>

这个片段展示了自定义连线插槽如何根据实时连线位置重建 ribbon 几何形状,并保持连线可点击。

const graphInstance = RGHooks.useGraphInstance();
const link = graphInstance.getLinkByLine(lineConfig.line)!;
const path = createAreaLinePathWithOffset(
    link.fromNode,
    link.toNode,
    lineConfig.line,
    1
);

const onClick = (e) => {
    graphInstance.onLineClick(lineConfig.line, e);
};

这个片段展示了最终呈现同样依赖 CSS,而不只是图配置。

.rg-node-peel.rg-node-checked {
    .rg-node {
        border: none;
        box-shadow: 0 0 0 10px var(--rg-node-color);
    }
}

.my-rg-line {
    opacity: 0.5;
    pointer-events: fill;
    cursor: pointer;

这个示例的独特之处

对比数据表明,这个示例与 demo-for-google-income-statement 最接近,但差异很关键:这组文件移除了六步教程外壳,只保留最终完成的查看器。因此,当目标是复用完整信息图模式,而不是讲解其构建过程时,它是更好的起点。

它少见的实现模式在于围绕一个合成根节点进行双向预处理。收入分支向内指,支出分支向外指,而两侧都保留百分比元数据,供连线插槽后续转换为按比例变化的 ribbon。这比许多其他示例共享的普通从左到右树形骨架更具专门性。

node-content-lines 相比,这里的关键启发并不是富节点内容中的连接器定位。这里更突出的思路是自定义边渲染:示例在预处理期间存储几何比例,并在渲染时根据实时节点位置重新计算填充的流向带。

canvas-eventcustomize-fullscreen-action 相比,这个示例使用了相似的 hook 初始化查看器基线,但服务于不同目的。那些示例强调事件埋点或外围页面行为,而这个示例则专注于经过打磨的财务叙事,包括按金额缩放的列、ribbon 流向、品牌化标注、checked 光晕以及渐变画布。

这种模式还适用于哪里

这种模式非常适合迁移到其他报表式视图中,特别是当同一份数据集既需要层级关系,又需要按比例展示流向时。示例场景包括运营成本拆分、部门预算分配、业务单元贡献图,以及可持续性或资源流向摘要。

它也适用于那些需要精致叙事型图形而不是编辑界面的仪表盘。团队可以保留相同的递归预处理与连线几何方案,同时把 Google 专属的标签、颜色、logo 和说明文字替换成自己的业务领域内容。