JavaScript is required

自定义力学布局颜色聚类

这个示例以随机位置和随机颜色初始化仅节点图,挂载自定义 `RGLayouts.ForceLayout` 子类,让同色节点在深色画布上聚集。悬浮辅助窗可调整力学参数、切换画布交互模式,并导出当前聚类视图。

自定义力布局实现基于颜色的节点聚类

这个示例构建了什么

这个示例构建了一个全屏的力布局实验场:大量圆形节点在深色画布上漂移,并逐渐重新聚合成基于颜色的簇。图一开始是一个没有边的节点场,节点带有随机位置以及随机的红、黄、蓝填充色,随后由一个自定义力求解器把同色节点彼此拉近。

用户可以通过两个滑块调整运动参数,用一个按钮重新为所有节点着色,使用内置工具栏控制运行中的布局,并打开一个浮动设置面板来修改 canvas 模式和导出图片。这个示例的重点不是领域数据,而是集中演示如何用基于属性的吸引规则替换 relation-graph 默认力行为中的一部分。

数据是如何组织的

这个 demo 声明了一个较大的内联 rawNodes 数组,并将其包装进一个 RGJsonData 对象中,包含 rootId: 'a'nodes 和一个空的 lines 数组。在任何内容插入图之前,每个节点都会被原地修改,填入随机 x、随机 y,以及从 redyellowblue 中随机选择的 color

这个预处理步骤很重要,因为图最初是以 fixed 布局初始化的,所以这些预设坐标会先成为可见的起始状态,之后自定义布局器才会接管。在真实应用中,这种结构同样可以表示按分群组织的用户、按状态分组的资产、按严重级别分组的告警,或任何其他需要基于节点属性而不是显式边来进行聚类的数据集。

relation-graph 是如何使用的

index.tsxRGProvider 包裹页面,MyGraph.tsx 则把 RGHooks.useGraphInstance() 作为主要控制入口。这个示例没有调用 setJsonData(),而是通过 addNodes()addLines() 插入已经准备好的纯节点数据集,随后创建一个 MyForceLayout 实例,并通过 setLayoutor(myLayout, true, true) 挂载上去。这个挂载步骤很关键,因为它让图实例和内置工具栏能够像控制内置力布局一样控制这个自定义布局器。

这个自定义布局器继承自 RGLayouts.ForceLayout。在 resetCalcNodes() 中,它会把每个节点的 color 复制到计算状态里的 myColor。在 calcNodesPosition() 中,它仍然会调用 addGravityByNode() 来处理节点两两之间的排斥力,但当两个节点颜色相同时,还会调用 addElasticByLine()。这就是核心实现细节:吸引力不是来自可见图连线,而是来自节点元数据。

图选项经过了专门调整,以便让聚类效果更容易观察。节点是没有边框的圆形 60x60 元素,默认线条形状设为直线,尽管一开始并不会渲染任何线条;内置工具栏则水平放在右下角。样式部分通过 SCSS 覆盖完成,设置了深色画布背景,并强制节点标签显示为白色。本示例中没有自定义节点、连线、画布或视口插槽。

共享的 DraggableWindow 组件补充了工作区辅助能力。它提供了一个可拖拽的说明面板、一个由 RGHooks.useGraphStore() 驱动的设置浮层、通过 graphInstance.setOptions() 进行的运行时配置修改,以及通过 prepareForImageGeneration()domToImageByModernScreenshot()restoreAfterImageGeneration() 实现的图片导出。

关键交互

  • 移动 Node Repulsion Coefficient 滑块会更新 React state,重写自定义布局器选项,并重新启动自动布局。
  • 移动 Line Elastic Coefficient 滑块也会修改同一组运行时选项,但在这个场景里,它影响的是自定义的同色吸引规则,而不是可见边的弹簧效果。
  • 点击 Randomly Change Colors 会通过 updateNode() 重写每个节点的颜色,短暂停顿以便颜色变化可见,然后重新运行布局,让新的簇重新形成。
  • 浮动辅助窗口可以被拖动、最小化,并切换成设置面板。
  • 设置面板可以修改滚轮行为、修改画布拖拽行为,并下载当前图的图片。
  • 由于自定义布局器被挂载到了图实例上,内置工具栏仍然可用,因此用户可以直接在图 UI 中停止和重新启动运行中的力模拟。

关键代码片段

这个片段展示了在挂载自定义求解器之前,图以 fixed 布局启动,使用圆形节点,并把工具栏放在右下角。

const graphOptions: RGOptions = {
    debug: true,
    defaultLineColor: 'rgba(255, 255, 255, 0.6)',
    defaultNodeColor: 'rgba(255, 255, 255, 0.6)',
    defaultNodeBorderWidth: 0,
    defaultNodeShape: RGNodeShape.circle,
    defaultNodeWidth: 60,
    defaultNodeHeight: 60,
    toolBarDirection: 'h',
    toolBarPositionH: 'right',
    toolBarPositionV: 'bottom',
    layout: {
        layoutName: 'fixed'
    }
};

这个片段说明,数据集在插入之前已经完成预处理:在一个纯节点的 RGJsonData 载荷上预先写入随机坐标和随机分组颜色。

const data: RGJsonData = {
    rootId: 'a',
    nodes: rawNodes,
    lines: []
};
data.nodes.forEach(node => {
    node.x = Math.random() * 300;
    node.y = Math.random() * 300;
    node.color = ['red', 'yellow', 'blue'][Math.floor(Math.random() * 3)]
});
graphInstance.addNodes(data.nodes);
graphInstance.addLines(data.lines);

这段代码就是自定义布局规则:常规的节点排斥力仍然生效,但同色节点对之间还会额外获得一个弹性拉力。

if (i !== j) {
    const __node2 = this.forCalcNodes[j];
    if (__node2.dragging) continue;

    this.addGravityByNode(__node1, __node2);

    if (__node1.myColor === __node2.myColor) {
        this.addElasticByLine(
            __node1,
            __node2,
            1 // Elasticity coefficient
        );
    }
}

这个处理函数展示了这些滑块如何接入运行中的布局器,而无需重新构建图数据。

const updateLayoutOptions = async () => {
    graphInstance.stopAutoLayout();
    const forceLayout = graphInstance.layoutor as MyForceLayout;
    if (forceLayout) {
        forceLayout.updateOptions({
            force_node_repulsion,
            force_line_elastic
        });
        forceLayout.start();
    }
    graphInstance.startAutoLayout();
};

这个重新着色流程说明,一个简单的按钮如何转化为可见的重新聚类交互。

graphInstance.getNodes().forEach(node => {
    const newColor = ['red', 'yellow', 'blue'][Math.floor(Math.random() * 3)];
    graphInstance.updateNode(node, {
        color: newColor
    });
});
await graphInstance.sleep(600);
await updateLayoutOptions();

这段共享辅助面板代码说明,这个示例还通过 relation-graph hooks 和实例 API 暴露了运行时画布设置与图片导出能力。

const { options } = RGHooks.useGraphStore();
const dragMode = options.dragEventAction;
const wheelMode = options.wheelEventAction;

const downloadImage = async () => {
    const canvasDom = await graphInstance.prepareForImageGeneration();
    let graphBackgroundColor = graphInstance.getOptions().backgroundColor;
    if (!graphBackgroundColor || graphBackgroundColor === 'transparent') {
        graphBackgroundColor = '#ffffff';
    }
    // ...
    await graphInstance.restoreAfterImageGeneration();
};

这个示例的独特之处

根据准备好的对比数据,这个示例的独特性在于:它通过继承 RGLayouts.ForceLayout,即使在初始数据集没有任何连线的情况下,也能基于节点属性生成聚类。这一点很重要,因为附近的其他力布局示例在驱动运动的因素上并不相同。customer-layout-force-circular 也安装了一个自定义力布局器,但它保留了显式连线、环形约束和锚定节点;而本示例则专注于开放场景中的自由聚类。

对比数据还表明,这个示例的运行时交互组合并不常见:两个实时力参数滑块配合一个 Randomly Change Colors 操作,刻意在重新启动布局前暂停一下,让用户先看到属性发生变化,再观察簇如何重新形成。相比之下,force-classifier-pro 增加了实时节点流、分组维度切换和更重的覆盖层 UI,因此本示例是一个更小、更可控的定制化参考。

它少见的功能组合也值得注意。这个示例把无边的内联数据集、按颜色编码的圆形节点、深色画布、共享浮动工作区外壳,以及一个可在同一图实例上反复调参与重新触发的自定义布局器结合在一起。浮动辅助窗口和设置浮层本身并不独特,但在这个 demo 里,它们服务的是一次力求解器实验,而不是通用查看器。

这种模式还适用于哪里

这种模式非常适合用于那些关系是从属性中推导出来、而不是以显式边存储的聚类视图。例如按分群组织的客户或用户群体、按状态分组的基础设施资产、按严重级别分组的告警,或按主题标签分组的文档集合。

当团队需要自定义力规则,但仍希望保留 relation-graph 的运行时控制、工具栏行为和图实例 API 时,这种方式也同样适用。这个 demo 使用节点颜色作为分组信号,但相同的方法也可以扩展到部门、优先级、归属、风险等级,或任何其他应当影响画布上运动方式的分类字段。