本文是关于如何使用可视化库 gojs 完成节点分组关系展示的,从零基础到实现最终效果。希望对使用 gojs 的小伙伴有帮助。
1. 节点分组需求及 demo 展示
需求
- 能正确展示组的层次,以及节点之间的关系。
- 单选节点、多选节点,获取到节点信息
- 选中组,能选中组中的节点,能获取到组中的节点信息
- 选中节点,当前节点视为根节点,能选中根节点连线下的所有节点,并获取到节点信息
2. 准备
- 从后端获取到的接口数据:
const data = {
"properties": [
{ "key": "t-2272", "parentKey": "j-1051", "name": "哈哈" },
{ "key": "p-344", "parentKey": "g--1586357764", "name": "test" },
{ "key": "t-2271", "parentKey": "j-1051", "name": "查询" },
{ "key": "t-2275", "parentKey": "j-1052", "name": "开开心心" },
{ "key": "j-1054", "parentKey": "p-344", "name": "嘻嘻" },
{ "key": "t-2274", "parentKey": "j-1052", "name": "查询" },
{ "key": "j-1051", "parentKey": "p-444", "name": "hello" },
{ "key": "j-1052", "parentKey": "p-444", "name": "编辑" },
{ "key": "t-2281", "parentKey": "j-1054", "name": "嘻嘻" },
{ "key": "p-444", "parentKey": "g--1586357624", "name": "test" },
{ "key": "g--1586357624", "name": "数据组1" },
{ "key": "g--1586357764", "name": "数据组2" },
{ "key": "t-2273", "parentKey": "j-1051", "name": "新建" }
],
"dependencies": [
{ "sourceKey": "t-2272", "targetKey": "t-2274" },
{ "sourceKey": "t-2274", "targetKey": "t-2275" },
{ "sourceKey": "t-2273", "targetKey": "t-2272" },
{ "sourceKey": "t-2271", "targetKey": "t-2272" },
{ "sourceKey": "t-2272", "targetKey": "t-2281" }
]
}
- 参考 gojs demo:grouping、 navigation
3. 实现步骤1:数据组建
-
gojs 图表实例所需数据结构如下:
diagram.model = new go.GraphLinksModel( [ // node data { key: "A"}, { key: "F", group: "Omega"}, { key: "G"}, { key: "Chi", isGroup: true }, ], [ // link data { from: "A", to: "A" }, { from: "F", to: "G" }, { from: "G", to: "Chi"} ] );
-
根据接口数据构建出的最终数据如下:
node
[
{ "key": "g--1586357624", "text": "数据组1", "type": "g", "isGroup": true },
{ "key": "p-444", "text": "test", "type": "p", "isGroup": true, "group": "g--1586357624" },
{ "key": "j-1051", "text": "hello", "type": "j", "isGroup": true, "group": "p-444" },
{ "key": "t-2272", "text": "哈哈", "type": "t", "group": "j-1051" },
{ "key": "t-2271", "text": "查询", "type": "t", "group": "j-1051" },
{ "key": "t-2273", "text": "新建", "type": "t", "group": "j-1051" },
{ "key": "j-1052", "text": "编辑", "type": "j", "isGroup": true, "group": "p-444" },
{ "key": "t-2275", "text": "开开心心", "type": "t", "group": "j-1052" },
{ "key": "t-2274", "text": "查询", "type": "t", "group": "j-1052" },
{ "key": "g--1586357764", "text": "数据组2", "type": "g", "isGroup": true },
{ "key": "p-344", "text": "test", "type": "p", "isGroup": true, "group": "g--1586357764" },
{ "key": "j-1054", "text": "嘻嘻", "type": "j", "isGroup": true, "group": "p-344" },
{ "key": "t-2281", "text": "嘻嘻", "type": "t", "group": "j-1054" }
]
link
[
{ "from": "t-2272", "to": "t-2274", "nextLinks": [ "t-2274", "t-2275" ] },
{ "from": "t-2274", "to": "t-2275", "nextLinks": [ "t-2275" ] },
{ "from": "t-2273", "to": "t-2272", "nextLinks": [ "t-2272", "t-2274", "t-2275" ] },
{ "from": "t-2271", "to": "t-2272", "nextLinks": [ "t-2272", "t-2274", "t-2275" ] },
{ "from": "t-2272", "to": "t-2281", "nextLinks": [ "t-2281" ] }
]
如何根据接口数据组装出所需数据就不介绍了。text字段用于显示组及节点的标题,nextLinks是为后面做选中当前节点,能选中节点连线下的所有节点做数据准备。
大家如果感兴趣,可以先不读后面的,自己根据组装出的数据自己实现下后面的交互。
4. 实现步骤2:构建图表容器、实例,自定义布局、节点、连线、组的样式等属性。
- 容器
<div class="diagram" id="diagram"></div>
- 去除水印、画布蓝色边框,参考前篇
- 构建图表实例
import * as go from './go-module.js';
const $ = go.GraphObject.make;
const diagram = $(go.Diagram,
'diagram', // diagram 绘图容器的 id
{
layout: $(go.TreeLayout, // 布局方式
{
angle: 90, // 自上而下,0 从左到右
arrangement: go.TreeLayout.ArrangementHorizontal
}
)
}
);
- 自定义节点、连线、组
const config = {
borderColor: '#d1d9e2',
groupTextColor: '#444',
nodeTextColor: '#585858',
linkColor: '#666',
selectedLinkColor: '#2090ff',
}
图表颜色值统一管理。
定义节点
diagram.nodeTemplate = $(go.Node,
"Auto",
$(go.Shape, "Rectangle", // 节点形状:矩形
{ stroke: config.borderColor, // 边框颜色
strokeWidth: 1, // 边框宽度
fill: "white", // 形状填充颜色
},
),
$(go.TextBlock, // 节点文本
{ margin: 4,
stroke: config.nodeTextColor // 文本颜色
},
new go.Binding("text", "text"), // 将 model 中的 text 属性进行绑定,用于节点显示文本
),
{ doubleClick: nodeDblClick, // 节点双击事件,选中节点下的所有节点
},
);
定义边
diagram.linkTemplate = $(go.Link,
{
curve: go.Link.Bezier // 贝塞尔曲线
},
// 连线
$(go.Shape, { name: 'link', strokeWidth: 1, stroke: config.linkColor }),
// 连线的箭头
$(go.Shape, { name: 'linkArrow', toArrow: "OpenTriangle", stroke: config.linkColor })
);
定义组
diagram.groupTemplate = $(go.Group,
"Auto",
{ // 定义分组的内部布局
layout: $(go.TreeLayout,
{ angle: 90, arrangement: go.TreeLayout.ArrangementHorizontal }),
isSubGraphExpanded: false, // 默认展开true、折叠false
// 分组单击事件
click: (e, group) => {
// todo 实现组选中,选中组中所有节点
}
},
$(go.Shape, // 定义分组形状及描述
"Rectangle",
{
parameter1: 14,
fill: "rgba(2, 153, 255, .2)", // 填充色
stroke: config.borderColor, // 边框色
strokeWidth: 1,
},
),
$(go.Panel, "Vertical",
{ defaultAlignment: go.Spot.Left, margin: 4 },
$(go.Panel, "Horizontal",
{ defaultAlignment: go.Spot.Top, margin: 4 },
$("SubGraphExpanderButton"), // 设置收缩按钮,用于展开折叠子图
$(go.TextBlock, // 定义文本
{
alignment: go.Spot.TopLeft,
font: "Bold 12px Sans-Serif",
stroke: config.groupTextColor,
},
new go.Binding("text"), // 将 model 中的 text 属性进行绑定,用于节点显示文本
)
),
// 创建占位符来表示组内容所在的区域
$(go.Placeholder, { padding: new go.Margin(5, 10) })
)
)
- 绑定数据
diagram.model = new go.GraphLinksModel(
[], // nodes
[] // links
)
5. 实现步骤3:交互处理
-
选中分组交互相对简单,就不附上代码了。
-
选中节点,选中节点连线下的所有节点
function nodeDblClick (e, node) {
// 遍历每一条边进行设置
let goneNodes = []; // 记录遍历过的,避免再次遍历它
const forEdges = (edges, isSelected) => {
edges.forEach(edge => {
if (edge && edge.nextLinks) { // 当前节点下面有多个节点
edge.nextLinks.forEach((id, i) => {
if (!goneNodes.includes(id)) { // 避免遍历过的
goneNodes.push(id);
const node = diagram.findNodeForKey(id);
node.isSelected = isSelected;
highlightLink(node, node.isSelected);
// 递归设置节点连线上下游的每一个节点选中及连线高亮,linkArr 为前面组装出的图的边数据
forEdges(linkArr.filter(e => e.from === id), isSelected)
}
})
}
})
}
const {key: nodeId} = node.data;
// 存在多条边,linkArr 为前面组装出的图的边数据
const edges = linkArr.filter(e => e.from === nodeId);
// 先清除上次高亮的连线
clearHightLink();
// 高亮当前节点的连线
highlightLink(node, node.isSelected);
// 循环设置当前节点连线上下游的每一个节
点选中及连线高亮
forEdges(edges, node.isSelected);
}
优化:思路:先统计数据,再对统计数据进行UI处理。职责分明,增强可读性。
function nodeDblClick (e, node) {
// 遍历每一条边
let allNodes = []; // 统计连线上的所有节点
const forEdges = (edges) => {
edges.forEach(edge => {
if (edge && edge.nextLinks) { // 当前节点下面有多个节点
edge.nextLinks.forEach((id, i) => {
allNodes.push(id);
// 递归节点连线上下游的每一个节点,linkArr 为前面组装出的图的边数据
forEdges(linkArr.filter(e => e.from === id))
})
}
})
}
const {key: nodeId} = node.data;
// 存在多条边,linkArr 为前面组装出的图的边数据
const edges = linkArr.filter(e => e.from === nodeId);
// 循环统计当前节点连线上下游的每一个节点
forEdges(edges);
// 先清除上次高亮的连线
clearHightLink();
// 先统计出所有的,再去重,再对节点进行处理
// 设置统计出的连线上的所有节点及边高亮
allNodes.push(nodeId);
allNodes = [...new Set(allNodes)]; // 去重
allNodes.forEach(id => {
const node = diagram.findNodeForKey(id);
node.isSelected = true;
highlightLink(node, true);
})
}
clearHightLink 方法:清除连线高亮
highlightLink 方法:根据node获取到对应的id找出node的出去的线设置颜色等高亮。
最后
完成这个效果难点在哪,我自己的感受是:
- 组装数据:如何组装出图表所需的数据,特别是选中节点要选中节点连线下的所有节点,怎么组装数据才方便后面的处理。
- 在自定义布局、节点、连线、组属性及样式上,特别是细节处理,需要大量翻看文档指南或api,查看案例是等,确定哪个属性的哪个值改了才是需要的。
- 在做交互时,需要理清思路,看文档事件相关的部分。特别难的是,节点信息打印出来查看时,看不到具体的,只能看出来是 迭代器,可以遍历,但看不出具体的数据,只能通过相应 api 才能得到。
关于本文的代码,只放了核心部分的。
最后的最后,有不到位的地方或者错误的地方,亦或是更好的意见,欢迎指出。
非常感谢!!!