对比元素到节点与元素到元素连线
这个示例在单个 relation-graph canvas 内构建了一个三栏对比面板。左侧面板将位置标记显示为叠加在地图上的真实图节点,中间面板显示可选择的兴趣小组卡片,右侧面板则将相同的位置显示为普通 HTML 标记。当用户选中某个小组时,示例会从同一个列表项重新绘制两条带动画的连接线:一条连到左侧的图节点,另一条连到右侧的 HTML 标记。
对比 Element-to-Node 与 Element-to-Element 连线
这个示例构建了什么
这个示例在单个 relation-graph canvas 内构建了一个三栏对比面板。左侧面板将位置标记显示为叠加在地图上的真实图节点,中间面板显示可选择的兴趣小组卡片,右侧面板则将相同的位置显示为普通 HTML 标记。当用户选中某个小组时,示例会从同一个列表项重新绘制两条带动画的连接线:一条连到左侧的图节点,另一条连到右侧的 HTML 标记。
最终呈现的效果,与其说是在展示图拓扑,不如说是在对比端点策略。它让开发者可以在相同坐标和相同样式下,对比目标端保留在图模型内部,和变成普通 DOM 目标之后会有什么差异。一个浮动辅助窗口补充了运行时 canvas 设置和图片导出能力,但这个示例最核心的亮点,是同步进行的并排连接器对比。
数据如何组织
这个示例在 MyGraph.tsx 中使用了一组很小的内联数据集。每条记录都包含 groupId、groupName,以及一个带有明确 x 和 y 坐标的 location 对象。这里没有调用 setJsonData()。相反,初始化流程会接收这组内联数组,把它存入 React state interestGroups,再通过 addNodes(...) 将其映射为图节点。
同一个 interestGroups state 被复用了三次:
- 用来创建固定位置的图节点,id 形如
node-location-a - 用来渲染中间列的列表项,并赋予它们类似
group-a的 connect-target id - 用来渲染右侧面板的 HTML 标记,并赋予它们类似
el-location-a的 connect-target id
这是该示例最重要的数据层启示:同一条源记录同时驱动图原生端点和普通 DOM 端点。在生产场景中,这些记录可以表示校园地图中的商店、仓储站点、维护点、活动展位或服务台,此时同一个界面需要在同一组物理坐标上对比多种锚点策略。
如何使用 relation-graph
RGProvider 包裹了整个 demo,以便 hooks 可以访问当前激活的图上下文。图本身运行在 layout.layoutName = 'fixed' 模式下,这意味着节点会停留在 addNodes(...) 提供的精确坐标上。选项里还关闭了调试模式,将边框交点设为默认连接模式,并让 canvas 初始进入滚轮缩放和拖拽移动模式。
这个示例以四种不同方式使用 relation-graph:
RGHooks.useGraphInstance()用于驱动图的初始化和运行时控制。它负责添加固定位置节点、居中视口、适配场景、清除已有 fake line、添加替换后的 fake line、更新运行时选项,并为图片导出准备 canvas。RGSlotOnNode用MapPin标记替换默认的节点渲染器。节点插槽使用checked样式,并通过node.data.myGroupId将点击事件转发回共享的小组选择处理函数。RGSlotOnCanvas是外围场景的组合位置。这个 demo 并没有渲染成普通的节点连线图,而是把左侧地图、中间列表和右侧地图一起组织成一个由 canvas 承载的界面。RGConnectTarget为列表卡片和右侧 HTML 标记提供可挂接的 DOM 端点。左侧连接器的目标是真实节点,因此那条 fake line 需要设置toType = RGInnerConnectTargetType.Node。右侧连接器则使用普通的 connect-target id。
浮动辅助窗口是一个共享子组件,而不是这个示例独有的功能,但它在技术上仍然很重要。它的设置面板使用 RGHooks.useGraphStore() 反映当前 canvas 模式,并通过 graphInstance.setOptions(...) 在运行时切换滚轮和拖拽行为。导出时,它会调用 prepareForImageGeneration(),使用 modern-screenshot 捕获 canvas DOM,下载生成的 blob,然后再恢复图状态。
样式定义位于本地 SCSS 文件中。样式表为小组卡片提供了紫色选中光晕,把两种标记都做成带动画的图钉式目标,并让线条标签通过 var(--rg-line-color) 继承当前连接器颜色。
关键交互
- 挂载阶段的 effect 会初始化节点、居中视口、适配场景,并在短暂延迟后自动选中小组
a,从而在页面一加载时就展示对比效果。 - 点击列表卡片、左侧图标记或右侧 HTML 标记,都会调用同一个
onGroupClick(...)函数。 - 每次选中都会更新
activeGroupId、清除旧的 fake line,并为当前小组插入一对新的带动画曲线连接器。 - 辅助窗口可以被拖动、最小化、切换到设置覆盖层,并用于修改滚轮模式、修改拖拽模式,或下载当前 canvas 的图片。
关键代码片段
这个片段展示了示例如何从一组内联数据集开始,并在任何连接器逻辑运行之前,先将其映射为固定位置的图节点。
const myGroups = [
{ groupId: 'a', groupName: 'Sports Group', location: { x: 260, y: 300 } },
// ...
{ groupId: 'f', groupName: 'Science Research Group', location: { x: 600, y: 240 } }
];
setInterestGroups(myGroups);
graphInstance.addNodes(myGroups.map(n => ({
id: 'node-location-' + n.groupId,
text: n.groupName,
x: n.location.x,
y: n.location.y,
disableDrag: true,
data: { myGroupId: n.groupId }
})));
这个片段证明了一次选择会重新生成两种不同的 fake line:一条连到真实节点,另一条连到 DOM 标记。
const myFakeLines: JsonLine[] = [
{
from: 'group-' + groupId,
to: 'node-location-' + groupId,
toType: RGInnerConnectTargetType.Node,
lineShape: RGLineShape.StandardCurve,
text: 'Element To Node',
animation: 2
},
{
from: 'group-' + groupId,
to: 'el-location-' + groupId,
lineShape: RGLineShape.StandardCurve,
text: 'Element To Element',
animation: 2
}
];
graphInstance.clearFakeLines();
graphInstance.addFakeLines(myFakeLines);
这个片段展示了图侧目标如何被渲染成自定义节点标记,并如何回流到共享的选择状态中。
<RGSlotOnNode>
{({ node, checked }: RGNodeSlotProps) => (
<div
className={`pointer-events-auto cursor-point c-i-location ${checked ? 'c-i-location-active' : ''}`}
onClick={() => onGroupClick(node.data.myGroupId)}
>
<MapPin className="transform translate-y-[-25px] translate-x-[-5px]" size={24} />
</div>
)}
</RGSlotOnNode>
这个片段展示了同一份 state 如何同时驱动右侧对比面板中的普通 HTML connect target。
{interestGroups.map(group => (
<div
key={group.groupId}
className="absolute"
style={{ left: group.location.x + 'px', top: group.location.y + 'px' }}
>
<RGConnectTarget targetId={`el-location-${group.groupId}`} junctionPoint={RGJunctionPoint.lr}>
<div onClick={() => onGroupClick(group.groupId)} className="pointer-events-auto cursor-point c-i-location">
<MapPin className="transform translate-y-[-25px] translate-x-[-5px]" size={24} />
</div>
</RGConnectTarget>
</div>
))}
这个示例的独特之处
从对比角度来看,这个示例并不只是一个基础的 element-to-node demo。它最鲜明的特点在于:同一个被选中的源项会同时重绘两条同步连接器,一条终止于真实的 relation-graph 节点,另一条终止于普通 HTML 标记。因此,与只展示单一模式的示例相比,它更适合作为端点挂接策略决策时的参考。
与 element-line-edit 相比,这个 demo 的重点不再是单个由节点承载的目标,而是并排对照。与 element-lines 相比,它保留了一个位于图模型中的目标端,而不是把两个端点都放在普通 HTML 中。与 interest-group 和 inventory-structure-diagram 这类更广义的地图式面板相比,它在领域结构上的投入更少,而更强调让端点差异易于观察。
从另一层对比意义上说,这个示例还保留了空间等价性。同一组 interestGroups 坐标同时映射到了左侧图节点和右侧 HTML 标记,因此视觉差异来自端点类型,而不是布局漂移。固定布局节点投影、RGSlotOnNode、RGSlotOnCanvas、成对的 RGConnectTarget 用法、自动默认选择,以及同步 fake line 替换这些组合,在整个示例集合中都相对少见。
这种模式还能用在哪里
这种模式非常适合用于那些需要评估锚点应继续保留为图原生对象,还是迁移为普通 DOM 的产品。典型场景包括校园与设施地图、物流看板、座位或展位规划器、零售平面系统,以及监控面板。在这些场景中,同一个实体往往需要同时在列表和空间表面上被高亮。
它同样适合作为一种迁移模式。团队可以把一个端点保留在图模型中,以获得视口感知行为、选中状态和 slot 渲染能力,同时把另一个端点暴露为普通 HTML,以便更容易与现有卡片、面板或覆盖层集成。因此,这个示例既适合作为 UI 对比面板,也适合作为构建 graph-plus-DOM 混合界面的起点。