按属性分组的力导聚类
这个示例构建了一个全屏力导布局实验场,它的表现更像一个实时分群场,而不是传统的关系图。它从少量合成用户节点开始,随着时间不断追加新用户,并允许查看者按 Region、Major、Grade 或 Gender 对同一批群体重新分组。核心点在于,可见的聚类来自共享的元数据取值,而不是来自可见连线。
实时按属性分组的力导聚类
这个示例构建了什么
这个示例构建了一个全屏力导布局实验场,它的表现更像一个实时分群场,而不是传统的关系图。它从少量合成用户节点开始,随着时间不断追加新用户,并允许查看者按 Region、Major、Grade 或 Gender 对同一批群体重新分组。核心点在于,可见的聚类来自共享的元数据取值,而不是来自可见连线。
数据如何组织
数据是本地生成的合成数据。DataLegendPanel.tsx 定义了年级、地区、专业和性别的查找表,generateNodes(...) 又复用了这些数组来创建每一条用户记录。
每个节点都会把分类属性存入 node.data,根据年级推导 node.color,根据性别推导 node.nodeShape,并在插入前把当前激活的分组键写入 node.data.currentGroupValue。初始加载时会构造一个符合 RGJsonData 形状的对象,其中包含节点和一个空的 lines 数组,但图中的内容是通过 addNodes(...) 和 addLines(...) 填充的,而不是通过 setJsonData(...)。
在真实应用中,同样的数据结构可以表示学生、客户、设备、工单,或任何其他需要按分类字段分组、同时又要在多个维度之间保持视觉可比性的实体。
relation-graph 的使用方式
这个示例包裹在 RGProvider 中,并由 RGHooks.useGraphInstance() 驱动图实例的生命周期。基础 options 对象启用了调试模式,配置了 60x60 的半透明节点,将工具栏保持为右下角的横向布局,并从一个标准力导布局开始,同时显式设置了斥力和弹性参数的默认值。
在插入第一批节点后,代码会停止默认自动布局,并通过 setLayoutor(...) 把布局器替换为一个 MyForceLayout 实例。这个子类继承自 RGLayouts.ForceLayout,会把 node.data.currentGroupValue 复制到其内部计算状态中,并且只在两个节点拥有相同当前分组值时增加弹性吸引。最终得到的是一个无边的聚类场,它的行为会随着所选分组维度的变化而变化。
插槽承担了大部分视觉定制。RGSlotOnNode 渲染自定义节点主体,其中包含姓名标签、专业首字母徽标和地区旗帜徽标。RGSlotOnView 将图例固定在左上角。RGSlotOnCanvas 在图后方加入发光的圆形 ElectricBorderCard 覆层。共享的 DraggableWindow 辅助组件则提供了浮动控制窗、一个可通过 setOptions(...) 更新滚轮和拖拽行为的设置覆层,以及通过 prepareForImageGeneration(...) 和 restoreAfterImageGeneration(...) 导出的截图能力。
my-relation-graph.scss 中的样式表补完了整个场景:它强制画布使用黑色背景,为选中节点添加彩色光晕,并在这个持续运动的场域周围维持深色技术风格。
关键交互
- Group By 按钮会通过重写每个节点的
currentGroupValue并重新启动自动布局,在 Region、Major、Grade 和 Gender 之间切换聚类方式。 - 一个循环定时器会一次追加一个新的合成用户,直到图规模增长到大约 300 个节点,因此场景会在首次绘制后持续重组。
- 浮动控制窗可以拖动、最小化,并切换到共享设置覆层。
- 设置覆层可以把滚轮行为在
scroll、zoom和none之间切换,并把画布拖拽行为在selection、move和none之间切换。 - 同一个设置覆层还可以将当前图导出为图片。
- 内置工具栏仍然可用于标准图操作,而自定义布局器的安装方式也保证了 relation-graph 的布局控制依然可用。
关键代码片段
这个片段展示了该示例如何显式保留力导配置,并把内置工具栏定位为工作区设计的一部分。
toolBarDirection: 'h',
toolBarPositionH: 'right',
toolBarPositionV: 'bottom',
defaultLineShape: RGLineShape.StandardStraight, // 使用枚举值 1
defaultJunctionPoint: RGJunctionPoint.border,
layout: {
layoutName: 'force',
maxLayoutTimes: 500,
force_node_repulsion: 0.4,
force_line_elastic: 0.1
}
这个片段展示了自定义布局器如何把当前激活的分组值带入其力计算状态。
this.visibleNodes.forEach((thisNode: RGNode) => {
const calcNode = {
rgNode: thisNode,
Fx: 0,
Fy: 0,
x: thisNode.x,
y: thisNode.y,
// ...
myGroupBy: thisNode.data.currentGroupValue // 记录颜色用于计算
};
这个片段展示了聚类的核心规则:所有可见节点彼此排斥,但只有同组节点才会获得额外吸引力。
// 1. 计算斥力 (原有逻辑)
this.addGravityByNode(__node1, __node2);
// 2. 自定义逻辑:只有颜色相同时才增加弹性(类似引力)
if (__node1.myGroupBy === __node2.myGroupBy) {
this.addElasticByLine(
__node1,
__node2,
1 // 弹性系数
);
}
这个片段展示了每个生成节点在进入图之前,如何被编码为带有多个可见属性。
node.data = {
userRegion: region.code,
userRegionIcon: region.icon,
userMajor: majors[Math.floor(Math.random() * majors.length)],
userGrade: randomGrade.grade,
userGender
};
node.color = randomGrade.color;
node.nodeShape = userGender === 'Male' ? RGNodeShape.rect : RGNodeShape.circle;
node.data.currentGroupValue = node.data[groupBy.current];
这个片段展示了重新分组时,如何为现有群体更新当前激活的聚类键,然后重新启动布局。
graphInstance.getNodes().forEach(node => {
graphInstance.updateNodeData(node, {
currentGroupValue: node.data[groupBy.current]
});
});
setTimeout(async () => {
graphInstance.startAutoLayout();
}, 200);
这个示例的独特之处
这个示例的价值并不在于它是唯一一个自定义力导布局演示。它的意义来自比较数据所呈现出的一组特定组合。
- 与
force-classifier相比,它把相同的属性切换聚类思路扩展到了一个流式场景中,新节点会在首次绘制后持续进入,同时还通过画布覆层加入了更强的视觉外壳。 - 与
customer-layout-force和customer-layout-force-circular相比,它强调的是对合成用户元数据的语义分组,而不是仅按颜色聚类、滑杆调参或轨道式约束。 - 与
expand-forever相比,这里的图增长是自主且无边的。运动来自聚类物理和群体增长,而不是懒加载式树展开。 - 与
toys-galaxy相比,这种风格化呈现依然服务于分析目的:运动是在解释元数据分组,而不是在表现轨道编排。 - 这里最强的组合在于:由元数据驱动的自定义力学规则、基于定时器的节点增长、无边聚类、多属性徽标节点,以及同一视图中的共享运行时画布工具。
这一模式还适用于哪里
这一模式可以迁移到需要展示群体在分组规则改变时如何重新分布的仪表盘中。例如,按校区、专业或年级重新分组的学生群体;按地区、细分市场或生命周期阶段重新分组的客户;按站点、状态或固件家族重新分组的设备;以及按来源、岗位类别或招聘阶段重新分组的候选人。
它也适用于动画化的监控界面,在这类界面中,新实体会不断到来,并且应立即加入当前所选的聚类逻辑。在这些场景下,可见连线可以继续隐藏,而节点元数据、基于插槽的徽标以及自定义力规则将承载大部分含义。