JavaScript is required

画布坐标尺

这个示例构建了一个全屏 relation-graph 查看器,并在其上叠加了两个额外的工作区辅助层:位于顶部和左侧边缘的制图风格标尺,以及悬浮在画布上方的辅助窗口。图本身在同一块画布上组合了两种不同的结构:一个环形中心簇,以及其右侧的一棵独立树。

混合图工作区的 Canvas 坐标标尺

这个示例构建了什么

这个示例构建了一个全屏 relation-graph 查看器,并在其上叠加了两个额外的工作区辅助层:位于顶部和左侧边缘的制图风格标尺,以及悬浮在画布上方的辅助窗口。图本身在同一块画布上组合了两种不同的结构:一个环形中心簇,以及其右侧的一棵独立树。

用户可以平移和缩放图,通过辅助面板切换画布的滚轮和拖拽模式,移动或最小化悬浮窗口,并将当前图导出为图片。这里最值得关注的并不只是图数据本身,而是这个标尺叠加层,它会持续显示当前可见区域对应的 canvas 坐标。

数据是如何组织的

图数据不是通过一次 setJsonData 调用加载的。相反,MyGraph.tsx 内联声明了两个 RGJsonData 对象:根节点为 atreeJsonData,以及根节点为 2centerJsonData。这两组数据通过分别调用 addNodesaddLines 被插入到同一个图实例中。

在布局运行之前,代码会修改这两组数据。树节点被设置为半透明并使用透明填充,这样连线几何仍然可见。中心簇节点则被重新设置为 80x80 的圆形,并使用更小的文字;中心簇的连线也被切换为直接附着在边框上的直线。完成这些修改后,示例会手动放置这两个连通分量,而不是依赖一个全局自动布局。

在真实应用中,这种数据形态同样可以表示一个主设备枢纽加一棵依赖树、一个流程核心加下游分支,或者一个中心实体加一个附属层级。示例中的内联标签只是占位内容,但这种模式可以直接迁移到业务 ID、名称和关系标签上。

relation-graph 是如何使用的

index.tsxRGProvider 包裹整个 demo,MyGraph.tsx 则使用 RGHooks.useGraphInstance() 直接操作当前激活的图实例。图以 layoutName: 'fixed' 启动,这一点很重要,因为这个示例希望在同一张画布上手动组合多个布局,而不是让单个布局接管整个场景。

组合流程如下:

  • 插入树数据集;
  • 重新设置样式并插入中心数据集;
  • 为中心簇创建一个 center 布局;
  • 使用 getNodesRectBox(...) 测量该簇的边界;
  • 将树根节点向右移动;
  • 为第二个簇创建一个从左到右的 tree 布局;
  • 最后执行 moveToCenter()zoomToFit()

标尺本身通过 RGSlotOnView 挂载,而不是放在节点内容内部,也不是放在整个画布的背景插槽中。MyCanvasCaliper.tsx 使用 getViewBoundingClientRect() 配合 getCanvasXyByViewXy(...) 来计算当前可见的 canvas 范围,再根据这个范围以及来自 RGHooks.useGraphStore()options.canvasZoom 推导出 SVG 刻度线和标签。因此,标尺始终与当前视口对齐,并且会随着缩放变化而调整刻度密度。

悬浮辅助窗口是一个本地子组件,而不是 relation-graph 的内置功能。但它仍然大量使用图 API:setOptions(...) 用于切换拖拽和滚轮模式,prepareForImageGeneration()restoreAfterImageGeneration() 用于包裹导出流程,getOptions() 则提供截图时使用的背景色兜底值。样式部分较为轻量:my-relation-graph.scss 基本保留了 relation-graph 选择器的开放状态,而 MyCanvasCaliper.scss 通过绝对定位放置叠加层,并设置 pointer-events: none,因此它不会阻挡图交互。

关键交互

  • 平移和缩放会立即改变标尺,因为叠加层会根据当前视口和缩放状态重新计算可见的 canvas 坐标与刻度间隔。
  • 悬浮辅助窗口可以通过标题栏拖动、最小化,并且可以切换到设置面板,而无需离开图视图。
  • 设置面板可以把滚轮行为切换为滚动、缩放或无动作,也可以把画布拖拽行为切换为框选、移动或无动作。
  • Download Image 操作会在 relation-graph 为图片生成准备好 canvas DOM 之后,通过共享的截图辅助流程导出当前图视图。

关键代码片段

下面这个片段展示了外层图被有意保持在固定模式下,以便手动组合多个独立布局:

const graphOptions: RGOptions = {
    defaultNodeBorderWidth: 1,
    defaultNodeWidth: 170,
    defaultNodeHeight: 40,
    toolBarDirection: 'h',
    toolBarPositionH: 'right',
    toolBarPositionV: 'bottom',
    defaultLineShape: RGLineShape.StandardCurve,
    defaultJunctionPoint: RGJunctionPoint.lr,
    defaultLineTextOnPath: true,
    lineTextMaxLength: 10,
    defaultLineColor: '#000000',
    layout: {
        layoutName: 'fixed'
    }
};

下面这个片段展示了中心数据集会在插入前先被重新设置样式,因此混合场景拥有明显不同的几何外观和连接点行为:

centerJsonData.nodes.forEach(node => {
    node.opacity = 0.5;
    node.nodeShape = RGNodeShape.circle;
    node.borderWidth = 1;
    node.width = 80;
    node.height = 80;
    node.fontSize = 10;
    node.color = 'transparent';
});
centerJsonData.lines.forEach(line => {
    line.lineShape = RGLineShape.StandardStraight;
    line.fromJunctionPoint = RGJunctionPoint.border;
    line.toJunctionPoint = RGJunctionPoint.border;
});

下面这个片段展示了两阶段布局组合:先放置中心簇,测量其范围,再把树放到右侧:

const myCenterLayout = graphInstance.createLayout({
    layoutName: 'center'
});
const centerNodes = graphInstance.getNetworkNodesByNode(centerRootNode);
myCenterLayout.placeNodes(centerNodes, centerRootNode);
const nodesRectInfo = graphInstance.getNodesRectBox(centerNodes);
treeRootNode.x = nodesRectInfo.maxX + 100;

const myTreeLayout = graphInstance.createLayout({
    layoutName: 'tree',
    from: 'left',
    treeNodeGapH: 200,
    treeNodeGapV: 30
});

下面这个片段展示了标尺的核心技巧:把视口坐标换算回 canvas 坐标,再根据当前缩放值缩放这些刻度:

const graphViewBox = graphInstance.getViewBoundingClientRect();
const visibleAreaStart = graphInstance.getCanvasXyByViewXy({
    x: 0,
    y: 0
});
const visibleAreaEnd = graphInstance.getCanvasXyByViewXy({
    x: graphViewBox.width,
    y: graphViewBox.height
});
const visibleArea = {
    x: visibleAreaStart.x,
    y: visibleAreaStart.y,
    width: visibleAreaEnd.x - visibleAreaStart.x,
    height: visibleAreaEnd.y - visibleAreaStart.y
};
const scale = options.canvasZoom! / 100 || 1;

下面这个片段展示了悬浮面板如何改变 relation-graph 的运行时行为,并把图片导出暴露为一个工作区工具:

<SettingRow
    label="Wheel Event:"
    options={[
        { label: 'Scroll', value: 'scroll' },
        { label: 'Zoom', value: 'zoom' },
        { label: 'None', value: 'none' },
    ]}
    value={wheelMode}
    onChange={(newValue: string) => { graphInstance.setOptions({ wheelEventAction: newValue }); }}
/>

这个示例的独特之处

它最重要的区别在于,混合布局图本身并不是最终要传达的重点。对比数据表明,multiple-layout-mixing-in-single-canvas 已经演示了非常相似的中心簇加树形结构组合。这个示例新增的重点,是一个安装在 RGSlotOnView 中、由视口边界、视口到 canvas 的坐标换算以及实时缩放状态驱动的测量型叠加层。

它也不同于 graph-offset。后者使用了一个大面积的背景式网格来解释 canvas 偏移和重新居中;而这里的叠加层是绑定到当前可见区域上的,而不是绑定到静态图空间网格上,因此这些标尺的行为更像一个针对用户当前观察区域的检查工具。

在其他复用同一个悬浮辅助窗口的查看器类示例中,这个示例把额外 UI 用在了分析性用途上。面板和导出流程属于共享基础设施,但这里更值得注意的组合是:手动拼接的中心簇加树形场景、让连线几何保持清晰可见的透明节点,以及把画布变成具备坐标感知能力工作区的顶部和左侧标尺。

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

当一个图需要表现得更像工作区而不是普通查看器时,这种模式就很有用。典型的迁移场景包括工程图、工厂布局、设备拓扑审查、类似地图的规划界面,以及任何需要用户在导航过程中检查大致 canvas 位置的图。

对于那些希望引入轻量测量辅助、但又不想演变成完整编辑器的工具来说,它同样是一个很好的起点。团队可以在保持实际图模型不变的情况下,基于同样的方法继续扩展出吸附参考线、选择标尺、打印安全边距、视口注释,或拖拽过程中的对齐辅助功能。