背景&需求

     现有的项目工程,在改造之后需要一种更好的展示链路的方式。当前的项目框架提供的图形有限,没有足够的API支持。所以,在调研了一些开源的图形工具之后,选择了蚂蚁的G6包,作为链路图形展示的基础。

     G6的文档相对来说不够详细,对一些细节没有解释,同时一些提供的包都已过期,需要用户重新设计。本文以链路图的设计代码为例,讲解了G6的一些使用方法和原理,希望能够帮助到一些对图形有需要的同学。

 

一、G6简介

G6是一个由纯 JavaScript 编写的关系图基础技术框架,是解决流程图和关系分析的图表库,集成了大量的交互,可以轻松的进行动态流程图和关系网络的开发,让用户获得关系数据的直接体验。

 

二、安装&引用

  官方文档提供了两种方式,一种是npm引入包,一种是html直接引入script方式。

1.npm

   首先需要在前端工程项目下执行命令下载g6包:

1npm install --save @antv/g6

   然后在js文件中引入g6包:

1import G6 from '@antv/g6'

 

2.HTML

   这种就是更直接的引入方式,在项目框架中,在前端关联的后端工程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.画布

1 <div className="button_style" id="mountNode"></div>

    在前端放的就是一个画布,“mountNode”对应graph设置的container名称。

2.图形初始化

    首先介绍一些关键字段,放在后端的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  布局样式

 

1.布局

   在Graph里面有很多的布局样式,但是由于树图的特殊性,需要把同一深度的节点放在同一层,所以这里用到了紧凑树的布局,Hierarchy.compactBox。

    1.direction:'TB' | 'BT' | 'LR' | 'RL' | 'H' | 'V'  代表树的朝向,即跟节点和孙子节点的相对位置。LR就是根节点在左,往右布局。

    2.getId   这里返回了节点数据的id属性值

    3.getHeight,getWidth   设置了节点的高度和宽度

    4.getVGap,getHGap  设置了节点纵向和横向的间距

    5.radial 是否按照辐射状布局,这里没有设置,默认为false。

2.注册节点

   大家最好奇的应该就是树上的每一个节点是怎么生成的,理解了原理才能画出自己想要的节点样式

      

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:

   设置元素的状态,主要是交互状态 。 

3.交互模式

  用户在交互一张图时,可能由于意图不同而存在不同的交互模式, 而每个交互模式包含多种交互行为。不同的模式可以通过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                }

3.特殊效果

1.高亮

   场景: 

     展示链路图的时候,当按一些条件查询的时候,会把满足条件的节点高亮显示。

     官方文档本身是有提供高亮的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。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐