这次征文可以说是缘分了,因为最近两个多月,我一直在做echarts,antvg6,cesium这些可视化大屏以及流程图的东西,自己也在博客上做了一些总结。g6是已经总结过一部分的,当时处于项目调研期间,之后从调研转入开发阶段后的深度使用,没有总结出来,但是也只是对功能的进一步使用,需要用到的东西还是这些。我调研期间写了一个demo,当时处于摸索阶段,虽然有很多问题,但是可以参考结构和使用。
个人博客总结:encaik.top/blog/g6.htm…
g6小demo:github.com/Encaik/G6-l…
介绍
G6 是一个简单、易用、完备的图可视化引擎,它在高定制能力的基础上,提供了一系列设计优雅、便于使用的图可视化解决方案。能帮助开发者搭建属于自己的图可视化、图分析、或图编辑器应用。
安装
-
CDN引入
// version <= 3.2 <script src="https://gw.alipayobjects.com/os/antv/pkg/_antv.g6-{$version}/build/g6.js"></script> // version >= 3.3 <script src="https://gw.alipayobjects.com/os/antv/pkg/_antv.g6-{$version}/dist/g6.min.js"></script>
在 {$version} 中填写版本号,例如 3.4.7;
最新版可以在 NPM 查看最新版本及版本号;
详情参考 Github 分支:github.com/antvis/g6/t…
- NPM安装
-
使用npm安装依赖
npm install --save @antv/g6
-
在需要使用的地方引入
import G6 from '@antv/g6';
-
Vue中使用
- 在需要使用G6的文件中引入/在Vue中全局引入
- 在methods中创建初始化图的方法,详细配置参考文档
- 在mounted或created中调用初始化方法,并使用nextTick立即更改DOM
- 如果有behavior、绑定监听等交互事件,则需要在初始化之前声明
代码示例:
//此处为引入G6,以及引入我自己定义的行为和节点
import G6, { Graph } from "@antv/g6";
import { initBehavors } from "./behavior";
import { initItems } from "./Item";
//此处调用初始化方法
export default {
created() {
this.$nextTick(() => {
this.init();
});
},
methods:{
init() {
/* 图初始化 */
this.graph = new G6.Graph({
container: "mountNode", //这里写绑定标签的id,G6画布会插入到这个位置
width: this.width, //图宽度
height: this.height, //图高度
...
});
}
}
}
如果要做到自适应窗口改变,需要同时改变canvas及g6的宽高配置才可以
概念
画布/布局
G6中画布为canvas,需要插入在div上,包括画布的插件也是一样。因为在初始化时需要声明插入div的class及尺寸。布局则是画布元素分布规则,设置后元素将根据分布规则固定自己的位置。
元素
G6中节点,边,分组都是元素,结构公用。此类对象都定义在图数据中,被render方法渲染在画布上。如果没有交互事件配合,元素将只是通过定义生成在画布上的图形。
状态
元素的样式状态,可以通过交互行为和事件绑定变更状态,然后展示出在初始化时配置的不同状态的样式效果。
交互
G6中交互对象主要有画布和元素,可以响应常见的所有交互事件。需要按照文档的behavior写法定义,并注册。初始化前注册可以写在初始化中,初始化后注册则通过方法加入。事件也可以单独使用,绑定监听。
动画
G6中可添加动画,定义好动画并注册到动画组件中,就可以在画布上显示。主要分为全局动画,节点动画和边动画。
使用
实现节点/边/combo添加(删改同理)
-
使用G6的API,通过addItem方法,标明元素类型,携带节点配置创建。这种方法创建的节点只能通过API获取数据。
this.graph.addItem("node", { id: this.newNode.id, type: this.newNode.type, label: this.newNode.label, size: nodeSize, x: parseInt(this.newNode.x), y: parseInt(this.newNode.y), comboId: null });
-
在data中定义图数据,然后通过push方法添加到节点数组中,通过API读取数据重新渲染添加节点。这种方法创建的节点可以在data中直接获取操作。
this.ghdata.nodes.push({ id: this.newNode.id, type: this.newNode.type, label: this.newNode.label, size: nodeSize, x: parseInt(this.newNode.x), y: parseInt(this.newNode.y), comboId: null }); this.graph.read(this.ghdata); this.graph.render();
实现行为绑定/事件监听
-
通过registerBehavior方法注册行为,参数为行为名称及行为配置, getEvents方法为说明事件与方法对应关系。注册后需要在图中配置行为参与的模式,否则不生效。
G6.registerBehavior("before-edge", { getEvents() { return { "node:mousedown": "onMouseDown" }; }, onMouseDown(e) { const graph = this.graph; if ("index" in e.shape.attrs) { if (e.shape.attrs.index == "node") { console.log("节点:拖动关键图形"); } else { console.log("节点:拖动锚点"); if (e.item) { const point = e.item.getContainer().get("children")[ parseInt(e.shape.attrs.index) + 1 ]; point.attr("fill", "#fff"); point.attr("stroke", "#000"); point.attr("r", 2.5); } const uid = Math.round(Math.random() * 100 + 100); graph.setMode("addedge"); newEdge = graph.addItem("edge", { id: uid, type: "cubic", source: e.item.getModel().id, sourceAnchor: e.shape.attrs.index, target: { x: e.x, y: e.y } }); newEdge.toBack(); } } else { console.log("节点:拖动原生图形"); } } }); this.graph = new G6.Graph({ modes: { default: [ "before-edge" ] }, })
-
通过on/off方法添加或移除事件监听,参数为监听事件及处理方法。事件监听只允许监听触发一个事件,但行为可以同时监听触发多个事件。
this.graph.on("node:click", e => { this.select = { ...e.item.getModel() }; });
实现状态变换
-
先在样式配置中声明状态样式,有以下两种方法:
- 初始化默认全局配置
this.graph = new G6.Graph({ defaultEdge: { style: { stroke: "#000", endArrow: true } }, edgeStateStyles: { hover: { stroke: "#66C4FF" } }, })
- 添加节点单独配置
this.graph.addItem("node", { ..., style: { stroke: "#000", endArrow: true } });
-
然后在代码中通过setItemState方法转换元素状态,G6高版本中支持多值转换,即一个状态种类多种状态样式。
const graph = this.graph; const item = e.item; graph.setItemState(item, "hover", true);
实现自定义节点/边
-
通过registerNode/ registerEdge方法注册节点/边,参数为节点/边名以及自定义生命周期对象。
G6.registerNode( 'nodeName', { options: { style: {}, stateStyles: { hover: {}, selected: {}, }, }, /** * 绘制节点,包含文本 * @param {Object} cfg 节点的配置项 * @param {G.Group} group 节点的容器 * @return {G.Shape} 返回一个绘制的图形作为 keyShape,通过 node.get('keyShape') 可以获取。 * 关于 keyShape 可参考文档 核心概念-节点/边/Combo-图形 Shape 与 keyShape */ draw(cfg, group) {}, /** * 绘制后的附加操作,默认没有任何操作 * @param {Object} cfg 节点的配置项 * @param {G.Group} group 节点的容器 */ afterDraw(cfg, group) {}, /** * 更新节点,包含文本 * @override * @param {Object} cfg 节点的配置项 * @param {Node} node 节点 */ update(cfg, node) {}, /** * 更新节点后的操作,一般同 afterDraw 配合使用 * @override * @param {Object} cfg 节点的配置项 * @param {Node} node 节点 */ afterUpdate(cfg, node) {}, /** * 响应节点的状态变化。 * 在需要使用动画来响应状态变化时需要被复写,其他样式的响应参见下文提及的 [配置状态样式] 文档 * @param {String} name 状态名称 * @param {Object} value 状态值 * @param {Node} node 节点 */ setState(name, value, node) {}, /** * 获取锚点(相关边的连入点) * @param {Object} cfg 节点的配置项 * @return {Array|null} 锚点(相关边的连入点)的数组,如果为 null,则没有控制点 */ getAnchorPoints(cfg) {}, }, // 继承内置节点类型的名字,例如基类 'single-node',或 'circle', 'rect' 等 // 当不指定该参数则代表不继承任何内置节点类型 extendedNodeName, );
- 如果不从任何现有的节点或从 'single-node' 扩展新节点时,draw 方法是必须的;
- 节点内部所有图形使用相对于节点自身的坐标系,即 (0, 0) 是该节点的中心。而节点的坐标是相对于画布的,由该节点 group 上的矩阵控制,自定义节点中不需要用户感知。若在自定义节点内增加 rect 图形,要注意让它的 x 与 y 各减去其长与宽的一半。详见例子 从无到有定义节点;
- update 方法可以不定义:
- 当 update 未定义:若指定了 registerNode 的第三个参数 extendedNodeName(即代表继承指定的内置节点类型),则节点更新时将执行被继承的内置节点类型的 update 逻辑;若未指定 registerNode 的第三个参数,则节点更新时会执行 draw 方法,所有图形清除重绘;
- 当定义了 update 方法,则不论是否指定 registerNode 的第三个参数,在节点更新时都会执行复写的 update 函数逻辑。
- afterDraw,afterUpdate 方法一般用于扩展已有的节点,例如:在矩形节点上附加图片,圆节点增加动画等;
- setState 只有在需要使用动画的方式来响应状态变化时需要复写,一般的样式响应状态变化可以通过 配置状态样式 实现;
- getAnchorPoints 方法仅在需要限制与边的连接点时才需要复写,也可以在数据中直接指定。
-
除了draw是必须要有的图形绘制方法,其他的方法根据需求添加(未复写的方法会继承基类)。Draw的参数cfg为创建配置,group为图形组合,该方法返回一个图形组合,就是最终样式。
G6.registerNode("ownrect", { draw(cfg, group) { group.addShape("rect", { attrs: { width: cfg.size[0], height: cfg.size[1], stroke: "block", fill: "white", index: "node" }, draggable: true }); const points = cfg.anchorPoints; for (let index = 0; index < points.length; index++) { group.addShape("circle", { attrs: { x: cfg.size[0] * points[index][0], y: cfg.size[1] * points[index][1], r: 2.5, stroke: "block", fill: "white", index } }); } if (cfg.label) { group.addShape("text", { attrs: { x: cfg.size[0] / 2, y: cfg.size[1] / 2, textAlign: "center", textBaseline: "middle", text: cfg.label, fill: "#666", index: "node" }, draggable: true }); } return group; } });
实现拖拽生成节点
-
设置HTML标签为可拖拽draggable,然后添加拖拽结束后的方法dragend。
<span draggable @dragend="handleDragEnd"> 拖动节点 </span>
-
在dragend中设定生成坐标x,y以及节点配置,然后通过addItem添加节点到画布。
handleDragEnd(e) { let str = Math.round(Math.random() * 100).toString(); let point = this.graph.getPointByClient(e.clientX, e.clientY); this.graph.addItem("node", { id: str, type: "ownrect", label: str, size: [80, 80], x: parseInt(point.x - 40), y: parseInt(point.y - 40), comboId: null }); },
实现拖拽生成边
-
先处理拖拽添加边和拖拽移动节点的逻辑,拖动节点行为放在默认mode中,添加边行为放在自定义mode中。通过node:mousedown(节点上按下鼠标)事件可以监听鼠标是在锚点按下还是在节点按下,如果是锚点则切换至自定义mode同时创建边,如果是节点则不用管。
G6.registerBehavior("before-edge", { getEvents() { return { "node:mousedown": "onMouseDown" }; }, onMouseDown(e) { const graph = this.graph; if ("index" in e.shape.attrs) { if (e.shape.attrs.index == "node") { console.log("节点:拖动关键图形"); } else { console.log("节点:拖动锚点"); if (e.item) { const point = e.item.getContainer().get("children")[ parseInt(e.shape.attrs.index) + 1 ]; point.attr("fill", "#fff"); point.attr("stroke", "#000"); point.attr("r", 2.5); } const uid = Math.round(Math.random() * 100 + 100); graph.setMode("addedge"); newEdge = graph.addItem("edge", { id: uid, type: "cubic", source: e.item.getModel().id, sourceAnchor: e.shape.attrs.index, target: { x: e.x, y: e.y } }); newEdge.toBack(); } } else { console.log("节点:拖动原生图形"); } } });
-
切换到自定义mode后监听mousemove(全画布鼠标移动)事件,通过获取鼠标位置持续更新边的target配置,做出鼠标拉着边移动的效果。然后在mouseup(全画布鼠标抬起)事件触发时,获取松开时所指节点及锚点信息,更新边完成创建。如果松开鼠标时不在节点上,则直接销毁边。
G6.registerBehavior("add-edge", { getEvents() { return { mousemove: "onMouseMove", mouseup: "onMouseUp" }; }, onMouseMove(e) { const graph = this.graph; console.log("画布:鼠标移动"); if (newEdge) { graph.updateItem(newEdge, { target: { x: e.x, y: e.y } }); } }, onMouseUp(e) { const graph = this.graph; if (!e.item.getModel()) { console.log("节点:松开在画布"); graph.removeItem(newEdge); graph.setMode("default"); newEdge = {}; return; } if ("index" in e.shape.attrs) { if (e.shape.attrs.index == "node") { console.log("节点:松开在关键图形"); } else { console.log("节点:松开在锚点"); graph.updateItem(newEdge, { target: e.item.getModel().id, targetAnchor: e.shape.attrs.index }); graph.setMode("default"); newEdge = {}; } } else { console.log("节点:松开在原生图形"); } } });
最后
这是我第一次写文章,所以有很多欠缺的地方,希望大家批评指正,谢谢~