JavaScript is required

力导向聚类与受控锚点运动

这个示例使用隐藏的零透明度连线,让三组节点在实时力导布局中保持聚类,并通过定时器驱动组锚点围绕根节点运动。它结合插槽节点渲染、画布背景、运行时连线可见性检查、主题切换、`RGMiniView` 以及共享悬浮设置与导出面板,适合作为在内置力求解器之上实现受控运动的参考。

在银河风格图中为隐藏连线力导向聚类添加动画

这个示例构建了什么

这个示例构建了一个全屏的力导向布局查看器,其中包含一个中心根节点、三个一级分组节点,以及每个分组下的十二个子标签。可见效果是一个银河风格场景:根节点显示为带电边框的球体,第一圈节点表现得像放大的行星,而子节点则围绕这些移动中的锚点保持聚类。

用户可以启动或停止运动,显示或隐藏通常不可见的连线,在普通外层容器和银河皮肤之间切换,拖动悬浮控制窗口,打开画布设置面板,并将图导出为图片。图本身会持续挂载,而这些控件只负责改变场景的行为和外观。

重点并不只是装饰性样式。更重要的经验是,relation-graph 的力导向布局可以在代码持续移动选定锚点节点的同时保持活跃,并且还能按需显示让每个聚类保持在一起的隐藏支撑连线。

数据是如何组织的

数据保存在一个名为 myJsonData 的内联 RGJsonData 对象中。它声明了 rootId: 'root'、一个扁平的 nodes 数组,以及一个扁平的 lines 数组。整个数据集一共包含 40 个节点:一个固定根节点、三个顶层分组节点(g1g2g3),以及平均分配到这些分组下的 36 个子标签。

图在加载前会进行一个很小的预处理步骤。初始化时,它会遍历 lines 数组,为所有尚未带有 id 的连线分配诸如 l1l2l3 这样的合成 id,然后再将数据传给 graphInstance.setJsonData(...)。这些连线记录一开始都带有 opacity: 0,因此它们首先充当力导向布局的支撑结构,而不是可见边。

节点记录还携带了一些展示元数据。node.data.myClassName 会被自定义节点插槽消费,用来为绿色组和蓝色组添加颜色变体。源码中还包含 imgOffset 字段,但在本次查看的文件里,渲染时并没有读取它们。

在真实产品中,这种结构同样可以表示围绕分类中心的分组商品、围绕平台域的服务、围绕主题聚类的标签,或围绕高层概念组织的知识图谱实体。隐藏连线尤其适合那些布局关系很重要,但这些关系在最终场景中又不应该长期可见的场景。

relation-graph 是如何使用的

index.tsx 使用 RGProvider 包裹整个页面,这样图组件和共享的悬浮工具窗口就可以解析同一个 relation-graph 上下文。在 MyGraph.tsx 中,这个示例渲染了一个使用内置 force 布局的 RelationGraph。选项将 maxLayoutTimes 设为几乎无限,并通过 force_node_repulsion: 0.1force_line_elastic: 2 调整求解器,因此在锚点移动时布局也会持续响应。

图选项还将大部分可见样式交给了自定义标记和 SCSS。defaultNodeShape 被设置为 RGNodeShape.circledefaultNodeWidthdefaultNodeHeight 都是 0defaultNodeBorderWidth0,而 defaultLineShapeRGLineShape.StandardCurve。这种组合意味着 relation-graph 负责几何计算和力学行为,而示例中的插槽和样式表决定场景的最终外观。

RGHooks.useGraphInstance() 驱动了几乎所有运行时行为。它负责加载 JSON、读取节点和选项、通过 updateNodePosition(...) 更新锚点位置、通过 moveToCenter()zoomToFit() 让视口重新居中并自适应、通过 getLines() 配合 updateLine(...) 重写连线透明度,以及在拖动后通过 startAutoLayout() 重新启动布局。在共享的 CanvasSettingsPanel 中,RGHooks.useGraphStore() 暴露当前的滚轮模式和拖动画布模式,而 setOptions(...) 可以实时应用新的配置。同一个面板还会在截图捕获前后分别调用 prepareForImageGeneration()restoreAfterImageGeneration()

三个 relation-graph 插槽共同塑造了页面。RGSlotOnCanvas 会在图后面注入一个大型圆形背景层。RGSlotOnNodeElectricBorderCard 替换根节点,并将其他所有节点渲染为圆形标签,其 class 还能从 node.data.myClassName 扩展。RGSlotOnView 则挂载了始终可见的 RGMiniView 作为总览图。随后,样式表会在基础 .my-graph 规则和 .my-graph-style-galaxy 覆盖规则之间切换,而不需要重建图实例。

这仍然是一个偏查看器的示例,而不是编辑器。这里使用的运行时 API 主要用于运动、检查、视口管理、实时选项变更和导出,而不是节点或连线的编辑创建。

关键交互

Move 选择器是核心交互。它会开启或关闭一个 100 毫秒的定时器,而这个定时器会持续围绕根节点重新定位三个一级锚点。

Line Visible 选择器会暴露结构支撑层。打开之后,代码会把所有已加载连线的 opacity0 改写为 1,这样用户就可以检查那些正在塑造力导向聚类、但平时被隐藏起来的边。

Graph Style 选择器会在 my-graph-style-galaxy 和基础 .my-graph 外层 class 之间切换。图数据和布局实例保持不变;变化的只有外层包装级别的样式。

拖动节点会以两种方式影响实时布局循环。轨道运动逻辑会跳过当前正在被拖动的节点,而 onNodeDragEnd 会在自动布局已经停止时重新启动它。

悬浮工具窗口本身也是可交互的。用户可以通过标题栏拖动它、将其最小化、打开设置浮层、改变滚轮与画布拖动行为,并下载图片。点击节点标签会触发一个简单的 alert,显示该节点文本,但相比布局和检查行为,这个点击交互是次要的。

关键代码片段

下面这段选项配置表明,这个示例依赖 relation-graph 内置的 force 求解器,并在自定义标记负责视觉样式的同时让布局持续运行。

const graphOptions: RGOptions = {
    debug: false,
    defaultLineColor: '#aaaaaa',
    defaultNodeBorderWidth: 0,
    defaultNodeShape: RGNodeShape.circle,
    defaultNodeWidth: 0,
    defaultNodeHeight: 0,
    defaultLineShape: RGLineShape.StandardCurve,
    defaultLineTextOnPath: true,
    layout: {
        layoutName: 'force',
        maxLayoutTimes: Number.MAX_SAFE_INTEGER,
        force_node_repulsion: 0.1,
        force_line_elastic: 2
    }
};

这个初始化步骤展示了在将数据加载到图实例之前唯一的一次预处理。

myJsonData.lines.forEach((line: JsonLine, index: number) => {
    if (!line.id) {
        line.id = `l${index + 1}`;
    }
});

这个运动循环说明,轨道效果并不只是交给求解器自行处理。代码会计算三个一级锚点的位置,并把这些位置写回当前运行中的图。

level1Nodes.forEach((thisNode, index) => {
    if (thisNode.id === graphOptions.draggingNodeId) {
        return;
    }
    const deg = level1DegSet[index] + rotateCountRef.current * level1SpeedSet[index];
    const nodePoint = getOvalPoint(rootNode.x, rootNode.y, level1CircleSet[index], deg);

    const newX = nodePoint.x - level1NodeR;
    const newY = nodePoint.y - level1NodeR;

    graphInstance.updateNodePosition(thisNode.id, newX, newY);
});

这个辅助函数证明,隐藏支撑层仍然是图模型的一部分,并且可以通过重写连线透明度按需显示出来。

const updateMyData = () => {
    graphInstance.getLines().forEach(line => {
        graphInstance.updateLine(line, {
            opacity: lineOpacity
        });
    });
};

这段插槽代码展示了该示例如何在不改变底层图数据的情况下,加入背景层、自定义节点渲染以及始终开启的缩略图。

<RelationGraph
    options={graphOptions}
    onNodeDragEnd={onNodeDragEnd}
>
    <RGSlotOnCanvas>{/* ... */}</RGSlotOnCanvas>
    <RGSlotOnNode>{/* ... */}</RGSlotOnNode>
    <RGSlotOnView>
        <RGMiniView />
    </RGSlotOnView>
</RelationGraph>

这个节点插槽分支决定了根节点会被渲染成示例中的中心球体,而不是普通的圆形节点。

node.id === 'root' ? (
    <ElectricBorderCard width={'200px'} height={'200px'} borderRadius="50%">
        <div className="p-3 h-full w-full">
            <div className="my-root h-full w-full"></div>
        </div>
    </ElectricBorderCard>
) : (

这个导出辅助函数展示了共享设置面板如何先让 relation-graph 为截图做好画布准备,然后再运行截图工具。

const canvasDom = await graphInstance.prepareForImageGeneration();
let graphBackgroundColor = graphInstance.getOptions().backgroundColor;
if (!graphBackgroundColor || graphBackgroundColor === 'transparent') {
    graphBackgroundColor = '#ffffff';
}
const imageBlob = await domToImageByModernScreenshot(canvasDom, {
    backgroundColor: graphBackgroundColor
});
// ...
await graphInstance.restoreAfterImageGeneration();

这段 SCSS 片段说明,银河皮肤是一种外层包装级别的覆盖样式,而不是一套独立的图配置。

.my-graph.my-graph-style-galaxy {
    .relation-graph {
        .rg-map {
            background: radial-gradient(circle at 20% 20%,
                    #003963 0%,
                    #002247 40%,
                    #001223 65%,
                    #161616 100%);
        }

这个示例的独特之处

对比数据将这个示例描述为比静态布局更聚焦于编排式力学行为。它最清晰的差异点在于:图会持续保持活跃,而一个 100 毫秒的定时器会不断重新定位三个位于第一圈的锚点。这让整个场景呈现为一个轨道系统,而不是一张仅在收敛后停下来的力导向快照。

它之所以突出,还因为那些塑造聚类的隐藏连线可以在运行时被显示出来。这个力学支撑层并不是一份独立的调试数据集,也不是一次性的预处理技巧。它始终保留在已加载的图模型里,而 Line Visible 控件会通过重写透明度把它暴露出来。对比说明认为,这种可检查的隐藏支撑层是它与附近其他查看器示例最显著的差异之一。

css-theme 相比,这个示例使用外层 class 主题切换来支撑一个会运动的力导向场景,而不是一个更广义的主题展示。与 zodiac-partner 相比,它更强调实时聚类行为、支撑结构检查以及轨道式锚点运动,而不是围绕选择驱动的流程。与 gee-thumbnail-diagram 相比,缩略图是辅助性的界面元素,而不是主要教学重点。在这些相邻示例之间,它独特的组合在于:隐藏连线力导向聚类、程序化锚点运动、按需显示支撑层、基于插槽的银河风格样式、带电边框根节点,以及始终可见的 RGMiniView

这种模式还适用于哪些场景

这种模式可以复用于分组商品目录、服务组合、主题地图或知识图谱摘要等场景,在这些场景中,聚类需要在视觉上贴近某个中心,但用于聚类的边大多数时候又应保持隐藏。当结构边主要是为了引导布局,而不是向终端用户表达显式关系时,它会是一个很实用的方案。

它也适用于那些锚点节点需要受控运动或受控坐标、同时周围聚类仍要自然响应的运营或教学视图。例如动画化的平台地图、叙事式演示流程、旋转式分类展示,以及那些希望比静态 force 布局更具动态感的引导演示。

第三类迁移场景是调试与解释。团队可以默认展示一个经过打磨的场景,然后在需要解释节点为何会那样聚集、验证布局结构,或将图作为文档截图保存时,按需显示这层支撑结构。