可视化—gojs 超多超实用经验分享(一)

2023-06-20,,

目录
1. 设置分组模板,默认样式,统一最小宽度,展开收起状态监听
2. 分组名称显示成员个数: 分组名称+成员个数: name(children)
3. 分组成员为空时,不显示 placeholder 占位留白
4. 分组第一次展开请求获取成员接口,监听展开收起状态 subGraphExpandedChanged:fn
5. 设置节点模板
6. 设置节点的 icon,此处以文字为例
7. 文案太长截取显示...,鼠标悬浮显示全部
8. 设置鼠标移入、移出状态,点击节点高亮相关联节点
9. 动态追加节点
10. 节点众多需要不同表现样式时,可定义不同的节点模板
11. 每个节点前后追加 input/output spot 的两种方式
12. 设置连线模板,箭头样式,连线上添加关系文字
13. 动态追加连线 addLinkData()
14. 点击节点使其相关联节点与连线及文字高亮显示
15. 点击画布清空当前高亮状态元素
16. 删除连线的方法
17. 动态根据需求 展开和收起某一个或全部的分组
18. 关闭画布重新渲染时的动画
19. 画布无限拖拽功能
20. 图禁止复制,禁止删除,开启鼠标滚轮缩放,toolTip 触发 hoverDelay 初始定位
21. 消除水印
更多分享 详见下一篇博文

可视化的库非常多,如:echarts、highcharts、antv 系列、d3、gojs.....

按照可自定义绘图的程度排序: gojs、d3js > antv > echarts 、highcharts

如果需求简单,不需要自定义图元素,那么 echarts 、highcharts 看中哪个 demo 效果就选用哪个库。

如果有一定程度需要自定义图元素,那么可以看 antv g2/g6 demo 是否能满足需求,可自定义大部分图元素。

如果上面的都不能解决你的需求,那么就是高可定制的,可以考虑 d3js、gojs,还是先去看 demo,看哪个更接近你的需求就采用哪个。

gojs 是一个用于构建交互式可视化图的 js 库,使用可自定义的模板和布局构建复杂节点、链接和组,从而构建出简单到复杂的各类图,如流程图、脑图、组织图、甘特图等。而且提供了许多用于用户交互的高级功能,例如拖放、复制和粘贴、就地文本编辑......

本文是关于如何使用可视化库 gojs 的介绍及使用时的小技巧。gojs 的高可自定义性,非常适合需求复杂的图交互。

绘制基本流程简单介绍,

引入库,可以下载,也可以引入 cdn 地址
html 文件创建画布容器,并设置宽高
创建实例,定义布局,样式,交互,属性,事件等
绑定节点和连线数据,渲染图表

先绘制出基本的实例,让后续的学习,有个大致的轮廓

<template
>>
<div>
<div id="myDiagramDiv" style="height: 1000px;"></div>
</div>
</template>
<script lang="ts" setup>
import go from "@/assets/js/go";
import { onMounted } from "vue"; let diagram: any = null;
const $ = go.GraphObject.make;
onMounted(() => {
init();
}); function init() {
// 创建diagram实例,
diagram = $(go.Diagram, "myDiagramDiv"); // 分组模板
diagram.groupTemplate = $(go.Group, "Auto", {
/* options 后期主要学习部分 */
}); // 连线模板
diagram.linkTemplate = $(go.Link, {
/* options 后期主要学习部分 */
}); // 节点模板
diagram.nodeTemplate = $(go.Node, "Auto", {
/* options 后期主要学习部分 */
}); // 绘制节点模板 追加新的 自定义的模板类型
diagram.nodeTemplateMap.add(); diagram.layout = $(go.LayeredDigraphLayout, {
direction: 0, // 布局方向,0 水平 90 垂直
layerSpacing: 120, // 节点间隔
isOngoing: false,
}); var nodeDataArray = []; // 节点集合
var linkDataArray = []; // 分组集合
diagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);
}
</script>

1. 设置分组模板,默认样式,统一最小宽度,展开收起状态监听

// 分组模板
diagram.groupTemplate = $(
go.Group,
"Auto",
{
layout: $(go.LayeredDigraphLayout, { direction: 0, columnSpacing: 5 }),
isSubGraphExpanded: false, // 默认展开 true 、折叠false.
subGraphExpandedChanged: function (group) {}, // 功能 小 ## 4
},
$(
go.Shape,
"RoundedRectangle", // 分组形状,圆角矩形
{ parameter1: 5, opacity: 0.7, minSize: new go.Size(120, NaN) }, // 圆角 透明度 统一最小宽度
new go.Binding("fill", "color"), // 绑定填充色,如果是固定颜色,可以直接在上述对象中,填写对应的属性,如 fill:"#ccc"
new go.Binding("stroke", "color") // 绑定描边色,同上
),
$(
go.Panel,
"Vertical",
{ defaultAlignment: go.Spot.Left, margin: 4 },
$(
go.Panel,
"Horizontal",
{ defaultAlignment: go.Spot.Top, margin: 4, padding: new go.Margin(5, 5, 5, 2) },
// 设置收缩按钮,用于展开折叠子图 +/-
$("SubGraphExpanderButton", { padding: new go.Margin(0, 5, 0, 0) }),
$(
go.TextBlock,
{ font: "bold 12px Sans-Serif", stroke: "white" },
new go.Binding("text", "", (node) => node.key + `(${node.children})`) // 分组名称+成员个数: name(children)
)
),
// 分组展开后的 面板占位
$(
go.Placeholder,
{ background: "white" },
new go.Binding("padding", "", function (node) {
// 每组背景色和边距
return node.children ? new go.Margin(10, 10) : new go.Margin(0, 10);
})
)
)
);

2. 分组名称显示成员个数: 分组名称+成员个数: name(children)

$(
go.TextBlock,
{ font: "bold 12px Sans-Serif", stroke: "white" },
new go.Binding("text", "", (node) => node.key + `(${node.children})`) // 分组名称+成员个数: name(children)
);

3. 分组成员为空时,不显示 placeholder 占位留白

// 分组展开后的 面板占位
$(
go.Placeholder,
{ background: "white" },
new go.Binding("padding", "", function (node) {
// 每组背景色和边距
return node.children ? new go.Margin(10, 10) : new go.Margin(0, 10);
})
);

4. 分组第一次展开请求获取成员接口,监听展开收起状态 subGraphExpandedChanged:fn

subGraphExpandedChanged: function (group) {
// 子图展开或收起的状态 group.isSubGraphExpanded
var groupData = group.part.data; // 获取分组 数据
if (!groupData.isRequested) {
// 设置一个标识位,标明该分组数据是否 有请求过接口,
// 未请求过,可以编写请求接口代码,或者添加已知节点代码,
groupData.isRequested = true;
diagram.model.addNodeData({
key: "任意要显示的node节点名称",
group: groupData.key,
color: groupData.color,
icon: groupData.icon,
});
// diagram.model.addLinkData({ from: "", to: "" }) // 新增节点 有连线关系则添加连线对象
diagram.animationManager.stopAnimation(); // 取消动画
}
// 请求过 就直接 展开或收起分组 isOngoing 属性true会自动布局,但是会影响用户拖拽效果,因此分组自行布局后,需要在改为false
diagram.layout.isOngoing = true;
setTimeout(() => {
diagram.layout.isOngoing = false;
});
}

5. 设置节点模板

// 节点模板
diagram.nodeTemplate = $(
go.Node,
"Auto",
{
mouseEnter: mouseEnter,
mouseLeave: mouseLeave,
click: nodeclick,
},
$(
go.Shape,
"Rectangle",
{ strokeWidth: 1, stroke: "white", fill: "white" },
new go.Binding("stroke", "isHighlighted", (sel) => (sel ? "#1E90FF" : "white")).ofObject(), // 鼠标选中高亮样式
new go.Binding("strokeWidth", "isHighlighted", (sel) => (sel ? 3 : 1)).ofObject()
),
$(
go.Panel,
"Horizontal",
{ width: 280, padding: new go.Margin(5, 5, 5, 2) },
$(
go.TextBlock, // 设置 icon
{ font: "bold 12px Sans-Serif", stroke: "white", width: 24, textAlign: "center" },
new go.Binding("text", "icon"), // 绑定icon 图表文案
new go.Binding("background", "color") // 绑定 背景色
),
$(
go.TextBlock,
{
margin: 5,
width: 240,
font: "15px Verdana",
stroke: "#444",
maxSize: new go.Size(260, NaN),
maxLines: 1,
overflow: go.TextBlock.OverflowEllipsis, // maxSize maxLines overflow 属性联合使用,用于文案截断 显示...
name: "TEXT", // 命名随意,用于后期 鼠标状态事件,节点成员的获取
},
new go.Binding("text", "key")
),
{
toolTip: $(
// 鼠标悬浮显示全部文案 ,默认触发时间比较长,可以通过属性来修改
"ToolTip",
$(go.TextBlock, { margin: 4 }, new go.Binding("text", "key"))
),
}
)
);

6. 设置节点的 icon,此处以文字为例

$(
go.TextBlock, // 设置 icon
{ font: "bold 12px Sans-Serif", stroke: "white", width: 24, textAlign: "center" },
new go.Binding("text", "icon"), // 绑定icon 图表文案
new go.Binding("background", "color") // 绑定 背景色
),

7. 文案太长截取显示...,鼠标悬浮显示全部

{
toolTip: $(
// 鼠标悬浮显示全部文案 ,默认触发时间比较长,可以通过属性来修改
"ToolTip",
$(go.TextBlock, { margin: 4 }, new go.Binding("text", "key"))
);
}

8. 设置鼠标移入、移出状态,点击节点高亮相关联节点

function mouseEnter(e, obj) {
var text = obj.findObject("TEXT");
text.stroke = "#1E90FF";
} function mouseLeave(e, obj) {
var text = obj.findObject("TEXT");
text.stroke = "#444";
} function nodeclick(e, node) {
var diagram = node.diagram;
diagram.clearHighlighteds();
node.findNodesConnected().each(function (l) {
l.isHighlighted = true;
});
node.linksConnected.each(function (n) {
n.isHighlighted = true;
});
}

9. 动态追加节点

diagram.model.addNodeData({
key: "任意要显示的node节点名称",
group: "分组名",
color: "节点背景颜色",
icon: "icon文字",
});

10. 节点众多需要不同表现样式时,可定义不同的节点模板

仅有一种不同形式时,可使用 diagram.nodeTemplateMap.add(node)调用不同的节点模板
多种不同模板时,是封装一个方法,生成模板,在调用diagram.nodeTemplateMap.set(typename, node)

11. 每个节点前后追加 input/output spot 的两种方式

方法一: 该方法需要重点关注的方法是 makePort,函数调用位置及返回值

var nodeDataArray = [{ key: "A", category: "Start", text: "节点设置左右连接点" }];

var linkDataArray = [
{ from: "A", to: "B", frompid: "1", topid: "1" }, // createPort方法 portId
{ from: "B", to: "C", frompid: "2", topid: "2" },
]; diagram.model.linkFromPortIdProperty = "frompid"; // 连接点对应名称
diagram.model.linkToPortIdProperty = "topid"; diagram.nodeTemplateMap.add(
"Start", // nodeDataArray 中的 category
$(
go.Node,
"Auto",
{ width: 260, height: 80 },
$(go.Shape, "Rectangle", { fill: "white", stroke: "white", strokeWidth: 1 }),
$(
go.Panel,
"Vertical",
{ padding: new go.Margin(5, 5, 5, 2) },
$(
go.TextBlock,
"节点设置左右连接点", // 1. 节点文本也可以直接写在这
{ font: "18px Sans-Serif", stroke: "#444", textAlign: "center" },
new go.Binding("text") // 2.文本也可以通过绑定 nodeDataArray 中的 text, 或者其他任意字段
)
),
$(
go.Panel,
"Vertical",
{
alignment: go.Spot.Left,
alignmentFocus: new go.Spot(0, 0.5, 8, 0),
},
makePort(2, 3).inSpotList // 需要返回一个数组,表示 2个 入边连接点
),
$(
go.Panel,
"Vertical",
{
alignment: go.Spot.Right,
alignmentFocus: new go.Spot(1, 0.5, -8, 0),
},
makePort(2, 3).outSpotList // 需要返回一个数组,表示 3个 出边连接点
)
)
); function makePort(inCount, outCount) {
let inSpot = inCount;
let outSpot = outCount;
let inSpotList: any = [];
let outSpotList: any = []; for (let i = 1; i <= inSpot; i++) {
inSpotList.push(createPort(i, "Left"));
}
for (let i = 1; i <= outSpot; i++) {
outSpotList.push(createPort(i, "Right"));
} function createPort(portId, pos) {
var port = $(go.Shape, "Rectangle", {
fill: "gray",
stroke: null,
desiredSize: new go.Size(8, 8),
portId: String(portId), // 该属性比较重要,用于给每一个连接点 命名,
toMaxLinks: 3,
cursor: "pointer",
}); var panel = $(go.Panel, "Horizontal", { margin: new go.Margin(2, 0) });
port.fromSpot = go.Spot[pos];
port.fromLinkable = true;
panel.alignment = go.Spot["Top" + pos];
panel.add(port); return panel;
}
return { inSpotList, outSpotList };
}

方法二: 该方法需要重点关注的方法itemArray,在数据中分别定义了 leftArrayrightArray,用于循环显示子元素

diagram.nodeTemplate = $(
go.Node,
"Table",
$(
go.Panel,
"Horizontal",
{ row: 1, column: 2 },
$(
go.TextBlock, // 资产名称
{
margin: 5,
width: 240,
font: "15px Verdana",
stroke: "#444",
}
)
),
$(go.Panel, "Vertical", new go.Binding("itemArray", "leftArray"), {
// 节点 左侧 入边连接点 循环显示
row: 1,
column: 0,
itemTemplate: $(
go.Panel,
{
_side: "left",
fromSpot: go.Spot.Left,
toSpot: go.Spot.Left,
cursor: "pointer",
},
new go.Binding("portId", "portId"),
$(
go.Shape,
"Rectangle",
{
stroke: null,
strokeWidth: 1,
desiredSize: new go.Size(8, 8),
margin: new go.Margin(1, 5, 1, 0),
},
new go.Binding("fill", "portColor")
)
),
}),
$(go.Panel, "Vertical", new go.Binding("itemArray", "rightArray"), {
// 节点 右侧 出边连接点 循环显示
row: 1,
column: 3,
itemTemplate: $(
go.Panel,
{
_side: "right",
fromSpot: go.Spot.Right,
toSpot: go.Spot.Right,
cursor: "pointer",
},
new go.Binding("portId", "portId"),
$(
go.Shape,
"Rectangle",
{
stroke: null,
strokeWidth: 0,
desiredSize: new go.Size(8, 8),
margin: new go.Margin(1, 0),
},
new go.Binding("fill", "portColor")
)
),
})
);
var nodeDataArray = [
{ key: "A", rightArray: [{ portColor: "#33B12C", portId: "left0" }], rightArray: [{ portColor: "#33B12C", portId: "right0" }] },
{ key: "B", rightArray: [{ portColor: "#F29941", portId: "left0" }], rightArray: [{ portColor: "#F29941", portId: "right0" }] },
{ key: "C", rightArray: [{ portColor: "#11C67B", portId: "left0" }], rightArray: [{ portColor: "#11C67B", portId: "right0" }] },
]; var linkDataArray = [
{ from: "A", to: "B", frompid: "right0", topid: "left0" },
{ from: "B", to: "C", frompid: "right0", topid: "left0" },
]; diagram.model.linkFromPortIdProperty = "frompid"; // 连接点对应名称
diagram.model.linkToPortIdProperty = "topid";

12. 设置连线模板,箭头样式,连线上添加关系文字

// 连线模板
diagram.linkTemplate = $(
go.Link,
{
routing: go.Link.Orthogonal,
corner: 25,
relinkableFrom: true,
relinkableTo: true,
},
$(go.Shape, { isPanelMain: true, stroke: "transparent" }),
$(go.Shape, { isPanelMain: true, stroke: "#ccc", strokeWidth: 2 }, new go.Binding("stroke", "color"), new go.Binding("strokeWidth", "strokeWidth")),
$(
go.Shape,
{ toArrow: "standard", strokeWidth: 1, fill: "#ccc" }, // 箭头
new go.Binding("stroke", "color"),
new go.Binding("fill", "color")
),
$(
go.Panel,
"Auto", // 连线上的文字
$(go.Shape, { fill: "white", stroke: "white" }),
$(go.TextBlock, { stroke: "#ff6600", visible: false }, new go.Binding("text", "linkText"), new go.Binding("visible", "linkText", (a) => (a ? true : false)), new go.Binding("stroke", "isHighlighted", (sel) => (sel ? "#1E90FF" : "#ff6600")).ofObject())
)
);

13. 动态追加连线 addLinkData()

diagram.model.addLinkData({ from: "节点key", to: "节点key", color: "线的颜色", linkText: "连线上的文字" }); // 不指定连接点,直接连
diagram.model.addLinkData({ from: "节点key", to: "节点key", color: "线的颜色", linkText: "连线上的文字", frompid: "right0", topid: "left0" }); // 设置入边和出边的连接点 var nodeDataArray = [
{ key: "A", rightArray: [{ portColor: "#33B12C", portId: "left0" }], rightArray: [{ portColor: "#33B12C", portId: "right0" }] },
{ key: "B", rightArray: [{ portColor: "#F29941", portId: "left0" }], rightArray: [{ portColor: "#F29941", portId: "right0" }] },
{ key: "C", rightArray: [{ portColor: "#11C67B", portId: "left0" }], rightArray: [{ portColor: "#11C67B", portId: "right0" }] },
]; var linkDataArray = [
{ from: "A", to: "B", frompid: "right0", topid: "left0" },
{ from: "B", to: "C", frompid: "right0", topid: "left0" },
]; diagram.model.linkFromPortIdProperty = "frompid"; // 连接点对应名称
diagram.model.linkToPortIdProperty = "topid";

14. 点击节点使其相关联节点与连线及文字高亮显示

function nodeclick(e, node) {
var diagram = node.diagram;
diagram.clearHighlighteds();
node.findNodesConnected().each(function (l) {
l.isHighlighted = true;
});
node.linksConnected.each(function (n) {
n.isHighlighted = true;
});
}

15. 点击画布清空当前高亮状态元素

diagram.click = function (e) {
e.diagram.commit(function (d) {
d.clearHighlighteds();
}, "no highlighteds");
};

16. 删除连线的方法

删除一条: diagram.model.removeLinkData(linkData); ,这个方法,我试了几个都没有成功,可能是linkData获取的不对,又由于我是要全部删除,因此使用了 diagram.model.removeLinkDataCollection方法,进行批量删除,但是在实际过程中发现,调用这个方法只能删除一半,(也不知道是什么原因,如果有耐心的有缘人,读到此处并解决了问题,欢迎留言帮我解惑),但是呢办法总比困难多,写一个 while 循环就可以搞定了

while (diagram.model.linkDataArray.length) {
diagram.model.removeLinkDataCollection(diagram.model.linkDataArray);
}

17. 动态根据需求 展开和收起某一个或全部的分组

展开或收起某一个分组:

// groupKey 在 nodeDataArray节点列表中的, 分组节点的 Key值
diagram.findNodeForKey(groupKey).isSubGraphExpanded = true; // 展开
diagram.findNodeForKey(groupKey).isSubGraphExpanded = false; // 收起

展开或收起全部分组: 这个功能我用在了,在每次展开某种特定条件的分组时,先关闭之前所有的分组,在进行新一轮的展开操作

function graphExpandCollaps() {
const { nodeDataArray } = diagram.model;
nodeDataArray.forEach((v) => {
if (v.isGroup) {
diagram.findNodeForKey(v.groupName).isSubGraphExpanded = false; // 分组全部收起
}
});
}

18. 关闭画布重新渲染时的动画

diagram.animationManager.stopAnimation(); // 取消动画

19. 画布无限拖拽功能

画布固定宽度和高度之后,对拖拽功能很不友好,当内容比较多时,容易拖拽不动,再则在使用 mac 浏览器时,会触发浏览器的前进后退事件。设置画布可以无限滚动后,解决

diagram.scrollMode = go.Diagram.InfiniteScroll;

20. 图禁止复制,禁止删除,开启鼠标滚轮缩放,toolTip 触发 hoverDelay 初始定位

diagram = $(go.Diagram, "myDiagramDiv", {
"toolManager.hoverDelay": 200, // toolTip触发
"toolManager.mouseWheelBehavior": go.ToolManager.WheelZoom, // 开启鼠标滚轮缩放
// initialContentAlignment: go.Spot.Center, // 居中:第一次 不好使
// contentAlignment: go.Spot.Center, // 可以居中,但是居中之后不可以拖动
initialPosition: new go.Point(-150, -300), // 初始坐标
allowCopy: false, // 禁止复制
allowDelete: false, // 禁止删除
scale: 1, // 缩放会恢复默认值 diagram.scale = 1
minScale: 0.4, // 缩小 diagram.scale -= 0.1
maxScale: 1.4, // 放大 diagram.scale += 0.1
});

21. 消除水印

在其他文章中看到要删除一个函数的方法,但是由于 gojs 是压缩过的,每个版本的变量都不一样,因此查到另外一个方式,亲测有效,

全文搜索 String.fromCharCode(a.charCodeAt(g)^b[(b[c]+b[d])%256]),大家再搜索时,需要注意一个空格,不然可能会导致搜索不到结果。

在 for 循环的后面 加上一个 判断, 需要跟画布上水印的文字进行比对一下,我觉得其实写一个条件语句,应该就可以命中了。

if (f.indexOf("GoJS 2.2 evaluation") > -1 || f.indexOf("(c) 1998-2022 Northwoods Software") > -1 || f.indexOf("Not for distribution or production use") > -1 || f.indexOf("gojs.net") > -1) {
return "";
} else {
return f;
}

更多分享 详见下一篇博文

gojs 超多超实用经验分享(二)

可视化—gojs 超多超实用经验分享(一)的相关教程结束。

《可视化—gojs 超多超实用经验分享(一).doc》

下载本文的Word格式文档,以方便收藏与打印。