线条形状、锚点与标签位置
这个示例在已加载的人像关系图上演示全图级的边形态控制、曲线连接锚点和标签位置控制。它通过 `getLines()` 配合 `updateLine()` 原地重设所有已渲染边,同时通过悬浮辅助窗提供共享画布设置和图片导出。
运行时线条形状、连接点与标签放置控制
这个示例构建了什么
这个示例构建了一个全屏关系图,同一份已加载的图数据可以在运行时实时切换样式。画布在平铺背景上展示圆形头像节点、按颜色区分的关系边,以及一个悬浮控制窗口。该窗口可以把每条线在直线与曲线走线之间切换、改变曲线与节点的连接方式,并在带框文本标签与沿路径放置的文本之间切换。
图本身的内容在加载后保持不变。它的主要亮点是,用户无需重建数据集或更改节点位置,就可以在一张稠密的头像关系图上比较多种边渲染行为。
数据如何组织
数据来自 mock-data-api.ts 中的本地 RGJsonData 对象。它声明了 rootId: "N13"、21 个节点以及更多的连线记录。每个节点都带有可见样式字段,例如 color、borderColor 和 data.icon;每条线都带有 from、to、text、color、fontColor 以及一个类型化的 data 载荷。
布局之前没有真正的预处理。fetchJsonData() 只是用一个简短的 Promise 包装这个静态对象,而 initializeGraph() 会把结果直接传给 setJsonData()。有些节点对会被有意重复出现,例如 N1 -> N15 和 N13 -> N8,这有助于示例展示在线条重复连接时,线条形状和连接方式的选择会如何表现。在实际项目中,同样的结构可以表示人物网络、调查链路、利益相关方地图,或同一对实体之间存在多种连接类型的服务关系。
如何使用 relation-graph
RGProvider 包裹整个页面,RGHooks.useGraphInstance() 则是 MyGraph 内部的主要运行时控制接口。图选项使用内置的力导向布局,关闭调试模式,设置圆形节点,将 multiLineDistance 保持为 20,把布局迭代次数上限设为 50,并提供节点颜色和边框的兜底设置。
这里重要的实现细节在于,当选择器发生变化时,示例不会重建 RGJsonData。异步加载完成后,initializeGraph() 会调用 loading()、setJsonData()、updateMyGraphData()、clearLoading()、moveToCenter() 和 zoomToFit()。之后,第二个 effect 会监听 lineShape、lineJunctionPoint 和 textOnPath,然后遍历 graphInstance.getLines(),并用 updateLine(...) 重写每一条已经渲染出来的线。
这一步线条更新流程正是 relation-graph 功能组合使用的地方。lineShape 会在 RGLineShape.StandardStraight 与 RGLineShape.StandardCurve 之间切换。启用直线模式时,两个端点都会被强制恢复为 RGJunctionPoint.border;启用曲线模式时,选中的连接点值会同时写入 fromJunctionPoint 和 toJunctionPoint。同一轮更新还会切换 useTextOnPath,因此标签位置可以在不重新加载图的情况下发生变化。
这个示例还使用了 slot 和共享辅助组件。RGSlotOnNode 用圆形肖像头像和位于节点下方的标签替换了默认节点主体。DraggableWindow 承载线条控制项,并且可以打开共享的 CanvasSettingsPanel,其中 RGHooks.useGraphStore() 和 graphInstance.setOptions() 用于切换滚轮与拖拽行为。这个共享面板还可以通过 prepareForImageGeneration()、domToImageByModernScreenshot() 和 restoreAfterImageGeneration() 导出当前图像。
本地 SCSS 则完成了最终的视觉呈现,包括平铺画布背景、白色带框线条标签,以及被选中节点的蓝色光晕。
关键交互
Line Shape选择器会把所有已加载的边改写为直线或曲线走线。Line JunctionPoint选择器只会在启用曲线走线时出现,让用户比较 border、paired-side 和 single-side 锚点模式。Line Text On Path选择器会在普通带框标签与直接渲染在线条路径上的文本之间切换。- 点击空白画布区域会调用
clearChecked(),从图中移除选中高亮。 - 悬浮辅助窗口可以拖拽、最小化、切换为画布设置覆盖层,并可用于下载当前图像。
关键代码片段
这个数据片段表明,示例从一个固定的 RGJsonData 对象开始,其中包含根节点、每个节点的样式以及自定义头像 URL。
const jsonData = {
"rootId": "N13",
"nodes": [
{
"id": "N1",
"text": "Liangping.Hou",
"color": "#ec6941",
"borderColor": "#ff875e",
"data": {
这个选项片段证明,该图在运行时线条更新开始前,使用了力导向布局、圆形节点以及固定的重复边间距。
const graphOptions: RGOptions = {
debug: false,
defaultLineShape: RGLineShape.StandardStraight,
defaultNodeShape: RGNodeShape.circle,
multiLineDistance: 20,
layout: {
layoutName: 'force',
maxLayoutTimes: 50
},
defaultNodeBorderWidth: 2,
这段初始化流程表明,图只加载一次,应用当前线条设置,然后再将视口居中并适配到合适大小。
const initializeGraph = async () => {
const myJsonData: RGJsonData = await fetchJsonData();
graphInstance.loading();
await graphInstance.setJsonData(myJsonData);
await updateMyGraphData();
graphInstance.clearLoading();
graphInstance.moveToCenter();
graphInstance.zoomToFit();
};
这个更新函数是核心技巧:它重写每一条已经渲染的线,而不是重新生成数据集。
const updateMyGraphData = async () => {
graphInstance.getLines().forEach((line) => {
graphInstance.updateLine(line, {
lineShape,
fromJunctionPoint: lineShape === RGLineShape.StandardStraight ? RGJunctionPoint.border : lineJunctionPoint,
toJunctionPoint: lineShape === RGLineShape.StandardStraight ? RGJunctionPoint.border : lineJunctionPoint,
useTextOnPath: textOnPath
});
});
};
这个控制片段证明,连接点选择是有条件的,只有图正在显示曲线时它才有意义。
{
lineShape !== RGLineShape.StandardStraight &&
<div>
<div className="text-base py-2">Line JunctionPoint:</div>
<SimpleUISelect
data={[
{ value: RGJunctionPoint.border, text: 'Border' },
{ value: RGJunctionPoint.ltrb, text: 'Left/Top/Right/Bottom' },
{ value: RGJunctionPoint.lr, text: 'Left/Right' },
{ value: RGJunctionPoint.tb, text: 'Top/Bottom' },
这个节点 slot 表明,示例把图级线条控制与自定义头像节点渲染结合在了一起。
<RGSlotOnNode>
{({ node }: RGNodeSlotProps) => (
<div className="w-12 h-12 flex place-items-center justify-center">
<div className="my-node-avatar" style={{ backgroundImage: `url(${node.data?.icon})` }} />
<div className="absolute transform translate-y-[35px]" style={{ color: node.color }}>{node.text}</div>
</div>
)}
</RGSlotOnNode>
这个共享设置片段表明,这个悬浮工作区也可以在不更改图数据的情况下导出当前图像。
const downloadImage = async () => {
const canvasDom = await graphInstance.prepareForImageGeneration();
let graphBackgroundColor = graphInstance.getOptions().backgroundColor;
if (!graphBackgroundColor || graphBackgroundColor === 'transparent') {
graphBackgroundColor = '#ffffff';
}
const imageBlob = await domToImageByModernScreenshot(canvasDom, {
backgroundColor: graphBackgroundColor
});
if (imageBlob) {
downloadBlob(imageBlob, 'my-image-name');
}
这个示例的独特之处
比较记录表明,这个示例最接近 line-multi-lines-gap、custom-line-style、search-and-focus 和 use-dagre-layout-2,但它们也揭示了一个更聚焦的要点。这里的主要重点是在图已经加载完成后,对内置边行为进行图级控制:形状切换、曲线连接点选择,以及沿路径文本切换。
与 line-multi-lines-gap 相比,这个示例在重复边间距上的着力较少,而更关注曲线连接器如何附着到节点上。与 custom-line-style 相比,它的区别价值不在于 CSS 外观换肤,而在于对内置线条几何和标签放置的运行时控制。与 search-and-focus 和 use-dagre-layout-2 相比,这个图的重点也不是导航或重新布局;节点位置保持在力导向布局中,变化的只有边的渲染方式。
比较文件也限制了可以安全提出的结论。悬浮辅助窗口、画布设置、图像导出以及头像风格的关系场景,并不是这个示例独有的,它也不是一个结构编辑器。真正让它突出的,是稠密头像关系图、图级 getLines() 加 updateLine() 更新、仅在曲线模式下出现的连接点选择器,以及在带框标签和沿路径文本之间进行运行时切换的组合。
这种模式还适用于哪里
- 关系图或社交图场景中,团队需要在固定数据集上比较更易读的连接器样式,再决定生产环境默认值。
- 调查、风险或欺诈图谱中,一对实体之间可能存在多种关系类型,而边的可读性比结构编辑更重要。
- 服务和依赖关系图中,需要一个紧凑的控制界面来调整连接器走线、锚点策略和标签放置,而无需重新运行布局逻辑。
- 内部演示或 QA 工作台中,设计师和工程师需要在真实的图场景中审查线条行为,并导出当前状态的快照。