现有的项目工程,在改造之后需要一种更好的展示链路的方式。当前的项目框架提供的图形有限,没有足够的API支持。所以,在调研了一些开源的图形工具之后,选择了蚂蚁的G6包,作为链路图形展示的基础。
G6的文档相对来说不够详细,对一些细节没有解释,同时一些提供的包都已过期,需要用户重新设计。本文以链路图的设计代码为例,讲解了G6的一些使用方法和原理,希望能够帮助到一些对图形有需要的同学。
G6是一个由纯 JavaScript 编写的关系图基础技术框架,是解决流程图和关系分析的图表库,集成了大量的交互,可以轻松的进行动态流程图和关系网络的开发,让用户获得关系数据的直接体验。
官方文档提供了两种方式,一种是npm引入包,一种是html直接引入script方式。
首先需要在前端工程项目下执行命令下载g6包:
1npm install --save @antv/g6
然后在js文件中引入g6包:
1import G6 from '@antv/g6'
这种就是更直接的引入方式,在项目框架中,在前端关联的后端工程index.vm文件里,直接加上:
1<script src="https://gw.alipayobjects.com/os/antv/pkg/_antv.g6-3.0.5-beta.12/build/g6.js"></script>
2<script src="https://gw.alipayobjects.com/os/antv/pkg/_antv.hierarchy-0.5.0/build/hierarchy.js"></script>
3<script src="https://gw.alipayobjects.com/os/antv/pkg/_antv.g6-3.0.5-beta.12/build/minimap.js"></script>
这里解释一下:
第一个包是g6的基础包
第二个是做复杂布局的时候用的组件包(可选)
第三个是图形的缩略图用的包(可选)
按照自己图形的复杂程度选择下载相应的包。
那么在实际的使用过程中,根据官方文档给的demo,结合自己框架的特点,做了改造和新的功能。现在就直接po代码来讲解一下链路图的生成过程。
1 <div className="button_style" id="mountNode"></div>
在前端放的就是一个画布,“mountNode”对应graph设置的container名称。
首先介绍一些关键字段,放在后端的state里面:
1state: {
2 //图形相关
3 ERROR_COLOR: "#F5222D",
4 SIMPLE_TREE_NODE: "simple-tree-node",
5 TREE_NODE: "tree-node",
6 //画布的宽度和高度,某些场景高度会设定固定值
7 CANVAS_WIDTH: window.innerWidth,
8 CANVAS_HEIGHT: 500,
9 //画布内容溢出的宽度和高度
10 LIMIT_OVERFLOW_WIDTH: window.innerWidth - 100,
11 LIMIT_OVERFLOW_HEIGHT: window.innerHeight - 100,
12 TIP_HEIGHT: 28,
13 }
下面是初始化一个图形的核心流程,接下来我会一个一个拆分解释:
1 newGraphTree: function (me) {
2 var selectedItem = void 0;
3 //定义了一个缩略图,用来在图形较为复杂的时候,方便找到图形节点
4 let minimap = new Minimap({
5 size: [me.state.CANVAS_WIDTH / 8, me.state.CANVAS_HEIGHT / 8],
6 opacity: 0.5,
7 });
8 me.graph = new G6.TreeGraph({
9 container: "mountNode",
10 width: me.state.CANVAS_WIDTH,
11 height: me.state.CANVAS_HEIGHT,
12 plugins: [minimap],
13 zoom: 0.5,
14 modes: {
15 default: [{
16 type: "collapse-expand",
17 shouldUpdate: function (e) {
18 /* 点击 node 禁止展开收缩 */
19 if (e.target.get("className") !== "collapse-icon") {
20 return false;
21 }
22 return true;
23 },
24 onChange: function (item, collapsed) {
25 selectedItem = item;
26 var icon = item.get("group").findByClassName("collapse-icon");
27 if (collapsed) {
28 icon.attr("symbol", me.EXPAND_ICON);
29 } else {
30 icon.attr("symbol", me.COLLAPSE_ICON);
31 }
32 },
33 animate: {
34 callback: function callback() {
35 debugger;
36 this.graph.focusItem(selectedItem);
37 }
38 }
39 }, "double-finger-drag-canvas", "three-finger-drag-canvas",
40 {
41 type: "tooltip",
42 formatText: function formatText(data) {
43 return "<div style='border: 1px solid #e3e6e8;background: white;padding:10px 10px;border-radius:3px;opacity:0.8'>" +
44 "<div style='font-size: 13px;font-weight:bold;margin-top: 2px'>应用信息</div>" +
45 "<div style='margin-top: 2px'>应用:" + data.name + "</div>" +
46 "<div style='margin-top: 2px'>服务:" + data.keyInfo + "</div>" +
47 "</div>";
48 }
49 },
50 {
51 type: "drag-canvas",
52 shouldUpdate: function shouldUpdate() {
53 return false;
54 },
55 shouldEnd: function shouldUpdate() {
56 return false;
57 }
58 }]
59 },
60 //anchorPoints是锚点,即可以链接的位置
61 defaultNode: {
62 shape: me.state.TREE_NODE,
63 anchorPoints: [[0, 0.5], [1, 0.5]]
64 },
65 defaultEdge: {
66 shape: "tree-edge"
67 },
68 edgeStyle: {
69 default: {
70 stroke: "#A3B1BF"
71 }
72 },
73 /**
74 * 布局方式
75 * getWidth
76 */
77 layout: function layout(data) {
78 var result = Hierarchy.compactBox(data, {
79 direction: "LR",
80 getId: function getId(d) {
81 return d.id;
82 },
83 getWidth: function getWidth() {
84 return 243;
85 },
86 getVGap: function getVGap() {
87 return 24;
88 },
89 getHGap: function getHGap() {
90 return 50;
91 }
92 });
93 return result;
94 }
95 });
96
97 },
在这里,我在TreeGraph定义了若干配置项,包括:
1.container,width,height,plugins,zoom这些必要的配置项
2.modes,在这里配置多种交互模式及其包含的交互事件。
3.defaultNode,默认情况下全局节点的配置项,包括样式属性和其他属性。这里的node样式选择自定义的treeNode,并且设置了连接点的位置anchorPoints。
4.defaultEdge, 默认情况下全局边的配置项
5.edgeStyle,除默认状态外的其他状态下边的样式配置
6.layout 布局样式
在Graph里面有很多的布局样式,但是由于树图的特殊性,需要把同一深度的节点放在同一层,所以这里用到了紧凑树的布局,Hierarchy.compactBox。
1.direction:'TB' | 'BT' | 'LR' | 'RL' | 'H' | 'V' 代表树的朝向,即跟节点和孙子节点的相对位置。LR就是根节点在左,往右布局。
2.getId 这里返回了节点数据的id属性值
3.getHeight,getWidth 设置了节点的高度和宽度
4.getVGap,getHGap 设置了节点纵向和横向的间距
5.radial 是否按照辐射状布局,这里没有设置,默认为false。
大家最好奇的应该就是树上的每一个节点是怎么生成的,理解了原理才能画出自己想要的节点样式
1.矩形框:
1createNodeBox: function (opacity, group, config, width, height, isRoot) {
2 /**
3 *最外面的大矩形,作为节点的大背景,不填充颜色
4 */
5 var container = group.addShape("rect", {
6 attrs: {
7 x: 0,
8 y: 0,
9 width: width,
10 height: height
11 // fill: '#FFF',
12 // stroke: '#000',
13 }
14 });
15 if (!isRoot) {
16 /**
17 * 左边的小圆点,位置,半径
18 */
19 group.addShape("circle", {
20 attrs: {
21 x: 3,
22 y: height / 2,
23 r: 6,
24 fill: config.basicColor,
25 opacity: opacity
26 }
27 });
28 }
29 /**
30 * 可视化的矩形
31 */
32 group.addShape("rect", {
33 attrs: {
34 x: 3,
35 y: 0,
36 width: width - 19,
37 height: height,
38 fill: config.bgColor,
39 stroke: config.borderColor,
40 radius: 2,
41 cursor: "pointer",
42 opacity: opacity
43 }
44 });
45
46 /**
47 * 左边的粗线
48 */
49 group.addShape("rect", {
50 attrs: {
51 x: 3,
52 y: 0,
53 width: 3,
54 height: height,
55 fill: config.basicColor,
56 radius: 1.5,
57 opacity: opacity
58 }
59 });
60 return container;
61 },
Group类似于svg中的<g>标签,用来组合图形对象的容器,在 group 上添加属性(例如颜色、位置等)会被其所有的子元素继承。此外, group 可以多层嵌套使用,因此可以用来定义复杂的对象。
思路:
1.这里首先是定义了一个大的矩形,作为背景板,颜色是透明的,相当于html我们画了个div作为容器一样。attrs中的x,y是相对位置,"rect"指的是矩形。
2.在背景板中画内置的主体矩形,设置其边框、背景颜色。
3.样式增强,在矩形左侧画半球和粗线,作为链接的标记。如果当前节点是根节点,就取消左侧半球。
2.展开/收缩标记:
后面的展开/收缩标记,是需要根据参数在圆圈中展示“+”或“-”:
1createNodeMarker: function (group, collapsed, x, y) {
2 group.addShape("circle", {
3 attrs: {
4 x: x,
5 y: y,
6 r: 13,
7 fill: "rgba(47, 84, 235, 0.05)",
8 opacity: 0,
9 zIndex: -2
10 },
11 className: "collapse-icon-bg"
12 });
13 group.addShape("marker", {
14 attrs: {
15 x: x,
16 y: y,
17 radius: 7,
18 symbol: collapsed ? this.EXPAND_ICON : this.COLLAPSE_ICON,
19 stroke: "rgba(0,0,0,0.25)",
20 fill: "rgba(0,0,0,0)",
21 lineWidth: 1,
22 cursor: "pointer"
23 },
24 className: "collapse-icon"
25 });
26 },
这里需要提一下svg的语法:
M: moveTo
L: lineTo
H: horizontal lineTo
A: elliptical Arc
V: vertical lineTo
...
大写表示绝对位置,小写表示相对位置。
那么下面两个方法就是分别定义了如何画出展开/收缩的语法。
1/**
2 * 收缩的icon
3 * svg中的语法,a
4 * 椭圆横轴半径
5 * 椭圆竖轴半径
6 * 椭圆横轴相对于CanvasX轴的偏移角度
7 * 弧度大小
8 * sweep-flag 取值0表示绘制逆时针方向的圆弧,取值1表示绘制顺时针方向的圆弧。
9 * 目标 相对位置
10 */
11 COLLAPSE_ICON: function (x, y, r) {
12 return [["M", x - r, y],
13 ["a", r, r, 0, 1, 0, r * 2, 0],
14 ["a", r, r, 0, 1, 0, -r * 2, 0],
15 ["M", x - r + 4, y],
16 ["L", x - r + 2 * r - 4, y]
17 ];
18 },
19 /**
20 * 展开的icon
21 */
22 EXPAND_ICON: function (x, y, r) {
23 return [["M", x - r, y],
24 ["a", r, r, 0, 1, 0, r * 2, 0],
25 ["a", r, r, 0, 1, 0, -r * 2, 0],
26 ["M", x - r + 4, y],
27 ["L", x - r + 2 * r - 4, y],
28 ["M", x - r + r, y - r + 4],
29 ["L", x, y + r - 4]
30 ];
31 },
了解了svg之后,就可以画一些个性化的图形了。
传送门:https://blog.csdn.net/cuixiping/article/details/79663611
3.注册节点方法
1 /**
2 * 注册复杂节点节点
3 */
4 registerNode: function () {
5 let me = this;
6 G6.registerNode(me.state.TREE_NODE, {
7 //cfg是每个节点对象,group是群组类,继承于图项Node
8 //graph读取数据之后,会自动调用drawShape;然后再调用afterDraw
9 drawShape: function (cfg, group) {
10 //获取颜色配置
11 var config = me.getNodeConfig(cfg);
12 var isRoot = cfg.type === "root";
13 var data = cfg;
14 var nodeError = data.nodeError;
15 /* 最外面的大矩形 */
16 var container = me.createNodeBox(data.opacity, group, config, 243, 64, isRoot);
17 //非根节点
18 if (data.type !== "root") {
19 //矩形上边的类型
20 group.addShape("text", {
21 ...
22 });
23 }
24
25 /* (调用比率)+应用名称 */
26 let ratioAndname = "(" + data.rootRate + "%)" + data.name;
27 var nameText = group.addShape("text", {
28 attrs: {
29 text: me.fittingString(ratioAndname, 224, 12),
30 ...
31 }
32 });
33
34 /* 调用的服务 */
35 var remarkText = group.addShape("text", {
36 attrs: {
37 text: me.fittingString(data.keyInfo, 204, 12),
38 ...
39 }
40 });
41 /* 如果有错误的节点,添加一个图形标记 */
42 if (nodeError) {
43 group.addShape("image", {
44 ...
45 }
46 });
47 }
48 /* 如果当前节点有子孙节点,添加圆圈来收起和展开 */
49 var hasChildren = cfg.children && cfg.children.length > 0;
50 if (hasChildren) {
51 me.createNodeMarker(group, cfg.collapsed, 236, 32);
52 }
53 return container;
54 },
55 afterDraw: function (cfg, group) {
56 /* 操作 marker 的背景色显示隐藏 */
57 var icon = group.findByClassName("collapse-icon");
58 if (icon) {
59 var bg = group.findByClassName("collapse-icon-bg");
60 icon.on("mouseenter", function () {
61 bg.attr("opacity", 1);
62 me.graph.get("canvas").draw();
63 });
64 icon.on("mouseleave", function () {
65 bg.attr("opacity", 0);
66 me.graph.get("canvas").draw();
67 });
68 }
69
70 ...
71
72 }
73 },
74 setState: function (name, value, item) {
75 var hasOpacityClass = ["collapse-icon-bg"];
76 var group = item.getContainer();
77 var childrens = group.get("children");
78 me.graph.setAutoPaint(false);
79 if (name === "emptiness") {
80 if (value) {
81 childrens.forEach(function (shape) {
82 if (hasOpacityClass.indexOf(shape.get("className")) > -1) {
83 return;
84 }
85 shape.attr("opacity", 0.4);
86 });
87 } else {
88 childrens.forEach(function (shape) {
89 if (hasOpacityClass.indexOf(shape.get("className")) > -1) {
90 return;
91 }
92 shape.attr("opacity", 1);
93 });
94 }
95 }
96 me.graph.setAutoPaint(true);
97 },
98 }, "single-shape");
99
100
101 },
在这里注册了三个方法:
drawShape:
这个方法就是生成一个group的过程,也就是上面说的一个节点的各个部分(shape)的组合。
afterDraw:
这个方法是绘制完成以后的操作。用户可继承现有的节点或边,做一些延伸,比如定义鼠标的悬浮事件
setState:
设置元素的状态,主要是交互状态 。
用户在交互一张图时,可能由于意图不同而存在不同的交互模式, 而每个交互模式包含多种交互行为。不同的模式可以通过setMode() 的方式自由切换,比如从default切换到edit模式。这里只设置了default模式。
1模式 -> 行为 -> 事件
1.collapse-expand行为:这个是针对树图场景常见的展开/收缩交互,TreeGraph提供了专有 Behavior。这里设置了三个事件:
shouldUpdate:判断触发展开/收缩事件的条件,这里是必须要点击到collapse-icon才会触发。
onChange:重新布局刷新视图前的事件,根据传入的参数来展现icon的形状。
animate:动画事件
1{
2 type: "collapse-expand",
3 shouldUpdate: function (e) {
4 /* 点击 node 禁止展开收缩 */
5 if (e.target.get("className") !== "collapse-icon") {
6 return false;
7 }
8 return true;
9 },
10 onChange: function (item, collapsed) {
11 console.log("collapsed", collapsed);
12 console.log("item", item);
13
14 selectedItem = item;
15 var icon = item.get("group").findByClassName("collapse-icon");
16 if (collapsed) {
17 icon.attr("symbol", me.EXPAND_ICON);
18 } else {
19 icon.attr("symbol", me.COLLAPSE_ICON);
20 }
21 },
22 animate: {
23 callback: function callback() {
24 debugger;
25 this.graph.focusItem(selectedItem);
26 }
27 }
28 }
2."double-finger-drag-canvas", "three-finger-drag-canvas":设置的手指拖拽行为。
3.设置的tooltip行为,节点的悬浮样式
1{
2 type: "tooltip",
3 formatText: function formatText(data) {
4 return "<div style='border: 1px solid #e3e6e8;background: white;padding:10px 10px;border-radius:3px;opacity:0.8'>" +
5 "<div style='font-size: 13px;font-weight:bold;margin-top: 2px'>应用信息</div>" +
6 "<div style='margin-top: 2px'>应用:" + data.name + "</div>" +
7 "<div style='margin-top: 2px'>服务:" + data.keyInfo + "</div>" +
8 "</div>";
9 }
10 },
4.画布拖拽的行为,这里没有定义具体事件
1 {
2 type: "drag-canvas",
3 shouldUpdate: function shouldUpdate() {
4 return false;
5 },
6 shouldEnd: function shouldUpdate() {
7 return false;
8 }
9 }
场景:
展示链路图的时候,当按一些条件查询的时候,会把满足条件的节点高亮显示。
官方文档本身是有提供高亮的api的,但是script的链接已经失效了。所以只能自己设计一种方式了。
思路:
1.给节点数据加opacity属性,通过改变改属性,来把未选中的节点透明度降低,反衬出选择节点的高亮。
代码:
1 /**
2 * 过滤节点,设置opacity的值,筛选出的进行高亮,其他置灰
3 */
4 onFilterNode: function () {
5 let me = this;
6 let nodeName = me.field.getValue("linkServerName");
7 let nodeNameIdObj = me.state.nodeNameIdObj;
8 console.log("nodeName", nodeName);
9 let linkData = me.state.data;
10 var recursiveSetOpacity = function recursiveSetOpacity(linkData, nodeName) {
11 if (linkData === null || linkData === undefined) {
12 return;
13 }
14 let linkName = linkData.name;
15 if (nodeName === null || nodeName === undefined || nodeName === '') {
16 console.log("------1213---------");
17 linkData.opacity = 1;
18 } else if (linkName !== nodeName) {
19 linkData.opacity = 0.3;
20 } else {
21 console.log("------1213---------");
22 linkData.opacity = 1;
23 }
24 if (linkData.children) {
25 linkData.children.forEach(function (item) {
26 recursiveSetOpacity(item, nodeName);
27 });
28 }
29 };
30 recursiveSetOpacity(linkData, nodeName);
31 console.log("linkData", linkData);
32 me.setState({
33 data: linkData
34 });
35 me.graph.data(linkData);
36 me.graph.render();
37 //找到nodeName对应的第一个id,定位到该位置
38 console.log("nodeNameIdObj", nodeNameIdObj);
39 if (nodeNameIdObj[nodeName] && nodeNameIdObj[nodeName].length > 0) {
40 console.log("nodeNameIdObj find nodeName");
41 let item = me.graph.findById(nodeNameIdObj[nodeName][0]);
42 me.handleNodePositioning(item);
43 me.graph.zoomTo(0.75);
44 }
45 },
Antv的g6包功能还是很强大的,只是有些文档没有更新了,需要自己去踩一些坑,或者设计一些方法。本文通过实际的项目历程,讲解了一些G6的用法,希望在项目中对图形有需求的同学可以快速的理解和使用G6。