G6官网:
ps://g6.antv.antgroup.com/exampleshttps://g6.antv.antgroup.com/examples
什么是G6
G6是一款基于JavaScript的图形可视化引擎,由阿里巴巴集团开发和维护。它提供了丰富的图形绘制、布局和交互功能,用于构建各种类型的图形化应用,包括流程图、关系图、组织结构图等。
G6的主要优势包括
1. 强大的图形绘制能力:G6提供了丰富的图形绘制功能,支持绘制各种形状的节点和边,以及自定义样式和标签。
2. 灵活的布局算法:G6内置了多种常用的布局算法,如树形布局、力导向布局、圆形布局等,可以帮助用户自动排列和调整图形元素的位置。
3. 交互丰富:G6支持多种交互方式,包括拖拽、缩放、平移、选中、连线等,用户可以通过交互操作来编辑和探索图形。
4. 扩展性强:G6提供了丰富的扩展机制,用户可以根据自己的需求自定义节点、边、布局、交互等,以满足特定的应用场景。
5. 跨平台支持:G6可以在浏览器和Node.js环境下运行,支持主流的桌面和移动设备,可以方便地集成到各种前端框架和项目中。
总之,G6是一个功能强大、灵活易用的图形可视化引擎,适用于构建各种图形化应用,并且具有良好的扩展性和跨平台支持。
业务需求,点击二级再调用接口获取后面的数据进行渲染。
业务要求
使用G6实现拓扑图,需要先加载前面的一二级,点击二级节点动态加载数据,绘制树图。鼠标悬浮展示节点对应的详情信息。
图片展示
代码实现
<template>
<div class="container">
<div id="treeContainer" />
</div>
</template>
<script>
import { getG6Cdn, uniqueId } from '@/utils/utils'
import { drawShape } from './registerNode'
export default {
name: 'ApprovalList',
components: {},
data() {
return {
mediaMap: {},
treeContainer: null,
graph: null,
trackParams: {
userId: 0,
distinctId: '',
deviceId: '',
promoteAssets: ''
},
list: {
nodeName: '用户',
nodeType: 'tree-node',
id: uniqueId(),
children: []
}
}
},
async mounted() {
await this.getMediaMap()
await this.initDate()
getG6Cdn().then(() => {
this.treeContainer = document.querySelector('#treeContainer')
this.initG6()
this.render()
})
},
methods: {
//不需要管,这里是业务需要的第五级所对应的数据
async getMediaMap() {
await this.$axios
.post(this.$transformUrl('/media/get_details_list'), { id: '' })
.then((res) => {
res.data.forEach((media) => {
delete media.configList
this.mediaMap[media.id] = media
})
})
},
//初始化数据,加载第二层数据
async initDate() {
this.initList = Object.assign({}, this.list)
Object.assign(this.trackParams, this.$route.query)
await this.$axios
.post(
this.$transformUrl('/xxx'),
this.trackParams
)
.then((res) => {
this.initList.children = res.data.map((item) => {
return {
nodeName: item,
id: uniqueId(),
nodeType: 'class1'
}
})
})
},
initG6() {
// treeNode类型节点
G6.registerNode('tree-node', { drawShape: drawShape }, 'single-node')
const width = this.treeContainer.scrollWidth
const height = this.treeContainer.scrollHeight || 800
//设置提示框
//G6设置提示框官网链接https://g6.antv.antgroup.com/zh/examples/tool/tooltip/#tooltipPluginLocal
const tooltip = new G6.Tooltip({
getContent(e) {
const detail = e.item.getModel().extraData || {}
const outDiv = document.createElement('div')
outDiv.style.width = '180px'
//这里循环遍历展示对象(注意是写原生,之前有点迷糊,在里面写了v-for和v-if
outDiv.innerHTML = `
<h4 class="db-title" >详情</h4>
<div class="content">
${Object.keys(detail)
.map((key) => `<p>${key}: ${detail[key]}</p>`)
.join('')}
</div>`
return outDiv
},
shouldBegin: (e) => {
return (
e.item.getModel().extraData ||
Object.keys(e.item.getModel().extraData || {}).length
)
},
itemTypes: ['node']
})
this.graph = new G6.TreeGraph({
container: 'treeContainer',
width,
height,
modes: {
default: ['drag-canvas', 'zoom-canvas']
},
plugins: [tooltip],
defaultNode: {
type: 'tree-node',
anchorPoints: [
[0, 0.5],
[1, 0.5]
]
},
defaultEdge: {
type: 'cubic-horizontal',
style: {
stroke: '#A3B1BF'
}
},
layout: {
type: 'compactBox',
direction: 'H',
getId: function getId(d) {
return d.nodeName
},
getHeight: function getHeight() {
return 16
},
getWidth: function getWidth() {
return 50
},
getVGap: function getVGap() {
// 设置兄弟节点垂直方向的间距
return 30
},
getHGap: function getHGap() {
// 设置兄弟节点水平方向的间距
return 80
},
getSide: () => {
return 'right'
}
}
})
this.graph.edge(function () {
return {
shape: 'cubic-horizontal',
color: '#A3B1BF'
}
})
this.graph.on('node:click', async (evt) => {
const item = evt.item
const model = item.getModel()
const children = model.children
if (!children?.length) {
if (model.nodeType === 'class1') {
this.trackParams.promoteAssets = model.nodeName
await this.getTrackData(this.trackParams, model)
this.render()//这里也可以只写this.graph.render() this.graph.fitCenter() 必须都写,前者不写加载的节点出不来,后者不加页面布局会不在正中心,点击后不知道会跑到哪里去
}
}
})
},
render() {
this.graph.data(this.list)//设置图初始化数据
this.graph.render()//根据提供的数据渲染视图
this.graph.fitCenter()// 在渲染和动画完成后调用 v3.5.1 后支持 平移图到中心将对齐到画布中心,但不缩放。优先级低于 fitView
if (typeof window !== 'undefined') {
window.addEventListener('resize', this.graphResize)
}
},
// 3 4 5级数据
async getTrackData(params, model) {
await this.$axios
.post(
this.$transformUrl('/xxx'),
params
)
.then(async (res) => {
const list = res.data || []
const children = []
for await (const child of list) {
const dataObj = {
extraData: child,
nodeName: child.event,
id: uniqueId(),
nodeType: 'class2',
children: []
}
const media = {
extraData: this.mediaMap[child.mediaId],
nodeName: this.mediaMap[child.mediaId].mediaName,
id: uniqueId(),
nodeType: 'class4',
children: []
}
let tableObj = {
nodeType: 'class3',
id: uniqueId(),
children: [media],
extraData: null,
nodeName: '点击数据或安装数据为空'
}
if (child?.notifyId) {
const { data } = await this.$axios.post(
this.$transformUrl('/xxx'),
{
tableEnName: child.tableEnName,
id: child?.notifyId
}
)
Object.assign(tableObj, {
extraData: data,
nodeName: '点击数据'
})
} else if (child?.installRegId) {
const { data } = await this.$axios.post(
this.$transformUrl('/xxx'),
{
tableEnName: child.installTableName,
id: child?.installRegId
}
)
tableObj = {
extraData: data,
nodeName: child.installTableName,
children: [media]
}
Object.assign(tableObj, {
extraData: data,
nodeName: '安装数据'
})
}
dataObj.children.push(tableObj)
children.push(dataObj)
}
this.initList.children = this.cycleSource(
this.initList.children,
model,
children
)
})
},
// 拼装原数据
cycleSource(data, model, pushData) {
data.map((node) => {
if (node.id === model.id) {
node.children = []
node.children = [...pushData]
} else if (node.children?.length) {
this.cycleSource(node.children, model, pushData)
}
return node
})
return data
},
//监听resize事件
graphResize() {
if (!this.graph || this.graph.get('destroyed')) return
if (
!this.treeContainer ||
!this.treeContainer.scrollWidth ||
!this.treeContainer.scrollHeight
) {
return
}
this.graph.changeSize(
this.treeContainer.scrollWidth,
this.treeContainer.scrollHeight
)
}
}
}
</script>
<style scoped lang="less">
.container {
position: relative;
width: 100%;
}
</style>
//cdn动态加载g6
export const getG6Cdn = () => {
return new Promise((resolve, reject) => {
const scriptElement = document.createElement('script')
scriptElement.src =
'https://xxxx/npm/@antv/[email protected]/dist/g6.min.js'
document.body.appendChild(scriptElement)
scriptElement.onload = () => {
resolve('')
}
})
}
export const uniqueId = () => Math.random().toString(36).slice(2)
export const drawShape = (cfg, group) => {
// 用来存放对照颜色
const color = {
'tree-node': '#0f88f7',
class1: '#0bdba8',
class2: 'pink',
class3: 'coral',
class4: 'yellow'
}
const grey = '#CED4D9'
// 逻辑不应该在这里判断
const rectConfig = {
width: 180,
height: 40,
lineWidth: 1,
fontSize: 12,
fill: '#fff',
radius: 4,
stroke: grey,
opacity: 1
}
const nodeOrigin = {
x: -rectConfig.width / 2,
y: -rectConfig.height / 2
}
const textY = 25 + nodeOrigin.y
const rect = group?.addShape('rect', {
attrs: {
x: nodeOrigin.x,
y: nodeOrigin.y,
...rectConfig
}
})
const rectBBox = rect?.getBBox()
//设置文字
group?.addShape('text', {
attrs: {
x: 5 + nodeOrigin.x,
y: textY + 5,
text: cfg.nodeName,//与数据里面想展示的字段对应
fontSize: 12,
opacity: 0.85,
fill: '#000',
cursor: 'pointer'
},
name: 'name-shape'
})
group?.addShape('rect', {
attrs: {
x: nodeOrigin.x,
y: rectBBox.maxY - 4,
width: rectBBox.width,
height: 4,
radius: [0, 0, rectConfig.radius, rectConfig.radius],
fill: cfg?.nodeType === null ? '#898B8E' : color[cfg?.nodeType]//设置边框的底色
}
})
if (rect) {
return rect
} else {
throw new Error('Invalid arguments')
}
}
遇见的错误
1.鼠标单击节点左键,控制台会输出TypeError: this.graph.findDataById is not a function,且点击节点布局会来回进行改变
处理:点击节点出现局部变动且id不唯一的问题是因为在modes里写了collapse-expand行为,这是树图布局的行为,用在一般布局里会报错。删掉就好了
2.重新处理数据进行渲染出现问题,添加children视图不会发生改变(数据加上去了)
打印发现数据是加上去了,但是缺少了g6内部添加的一些属性,用 this.graph.render()重新渲染一下,底层会把这个数据进行处理,加上对应的联系anchorPoint和x,y等属性。
注意this.graph.fitCenter()也要加上,不然点击后视图会发生变动,移动到别的地方去了。
第一次初始化数据打印出来的数据
处理过程中的数据
3.vue2中要多考虑this指向问题,特别是在this调用对象里面又用this的情况
如果这里写的不是箭头函数,那么里面的this指向就会有问题,找不到对应的值
4处理tooltip部分时候插件问题
在new G6.Tooltip({})中配置shouldBegin方法,返回boolean值来决定tooltip插件是否使用。在getContent函数中使用模版字符串里写vue模版一些特殊写法是不生效的。
5.使用forEach和map等函数+async/await循环遍历的时候执行顺序问题。
具体可以看我之前的博客
forEach或map循环遍历使用async/await执行顺序问题-CSDN博客
业务处理数据
所以这里在处理3,4,5级数据的时候(将3,4,5级数据加到2级数据的children里面)
使用map遍历加async和await
// 3 4 5级数据
async getTrackData(params, model) {
await this.$axios
.post(
this.$transformUrl('/xxx'),
params
)
.then(async (res) => {
const list = res.data || []
const children = []
list.map(async (child) => {
const dataObj = {
extraData: child,
nodeName: child.event,
id: uniqueId(),
nodeType: 'class2',
children: []
}
const media = {
extraData: this.mediaMap[child.mediaId],
nodeName: this.mediaMap[child.mediaId].mediaName,
id: uniqueId(),
nodeType: 'class4',
children: []
}
let tableObj = {
nodeType: 'class3',
id: uniqueId(),
children: [media]
}
if (child?.notifyId) {
const { data } = await this.$axios.post(
this.$transformUrl('/xxx'),
{
tableEnName: 'event_track_app_install',
id: '47818613'
}
)
Object.assign(tableObj, {
extraData: data,
nodeName: '点击数据'
})
console.log(data, '111')
} else if (child?.installRegId) {
const { data } = await this.$axios.post(
this.$transformUrl('/xxx'),
{
tableEnName: child.installTableName,
id: child?.installRegId
}
)
tableObj = {
extraData: data,
nodeName: child.installTableName,
children: [media]
}
Object.assign(tableObj, {
extraData: data,
nodeName: '点击数据'
})
} else {
tableObj = {
extraData: null,
nodeName: '点击数据或安装数据为空'
}
}
dataObj.children.push(tableObj)
children.push(dataObj)
console.log(children, '0')
})
console.log(children, '1')
this.list.children = this.cycleSource(
this.list.children,
model,
children
)
console.log(this.list.children, '2')
})
},
如果使用的是使用map遍历加async和awiat,会出现先执行下面的同步函数,后执行map遍历,导致数据没加上去。
Promise.all+map遍历加async和awiat写法
为了解决这种问题,可以结合Promise.all来实现,将循环放到promise.all里面,执行完之后.then执行后续对数据的操作。
// 3 4 5级数据
async getTrackData(params, model) {
await this.$axios
.post(
this.$transformUrl('/xxx'),
params
)
.then(async (res) => {
const list = res.data || []
const children = []
Promise.all(
list.map(async (child) => {
const dataObj = {
extraData: child,
nodeName: child.event,
id: uniqueId(),
nodeType: 'class2',
children: []
}
const media = {
extraData: this.mediaMap[child.mediaId],
nodeName: this.mediaMap[child.mediaId].mediaName,
id: uniqueId(),
nodeType: 'class4',
children: []
}
let tableObj = {
nodeType: 'class3',
id: uniqueId(),
children: [media]
}
if (child?.notifyId) {
const { data } = await this.$axios.post(
this.$transformUrl('/xxx'),
{
tableEnName: 'event_track_app_install',
id: '47818613'
}
)
Object.assign(tableObj, {
extraData: data,
nodeName: '点击数据'
})
console.log(data, '111')
} else if (child?.installRegId) {
const { data } = await this.$axios.post(
this.$transformUrl('/xxx'),
{
tableEnName: child.installTableName,
id: child?.installRegId
}
)
tableObj = {
extraData: data,
nodeName: child.installTableName,
children: [media]
}
Object.assign(tableObj, {
extraData: data,
nodeName: '点击数据'
})
} else {
tableObj = {
extraData: null,
nodeName: '点击数据或安装数据为空'
}
}
dataObj.children.push(tableObj)
children.push(dataObj)
console.log(children, '0')
})
).then(() => {
this.list.children = this.cycleSource(
this.list.children,
model,
children
)
console.log(this.list.children, '2')
this.render()
})
console.log(children, '1')
})
},
for await of写法
// 3 4 5级数据
async getTrackData(params, model) {
await this.$axios
.post(
this.$transformUrl('/xxx'),
params
)
.then(async (res) => {
const list = res.data || []
const children = []
for await (const child of list) {
const dataObj = {
extraData: child,
nodeName: child.event,
id: uniqueId(),
nodeType: 'class2',
children: []
}
const media = {
extraData: this.mediaMap[child.mediaId],
nodeName: this.mediaMap[child.mediaId].mediaName,
id: uniqueId(),
nodeType: 'class4',
children: []
}
let tableObj = {
nodeType: 'class3',
id: uniqueId(),
children: [media]
}
if (child?.notifyId) {
const { data } = await this.$axios.post(
this.$transformUrl('/xxx'),
{
tableEnName: 'event_track_app_install',
id: child?.notifyId
}
)
Object.assign(tableObj, {
extraData: data,
nodeName: '点击数据'
})
} else if (child?.installRegId) {
const { data } = await this.$axios.post(
this.$transformUrl('/xxx'),
{
tableEnName: child.installTableName,
id: child?.installRegId
}
)
tableObj = {
extraData: data,
nodeName: child.installTableName,
children: [media]
}
Object.assign(tableObj, {
extraData: data,
nodeName: '点击数据'
})
} else {
tableObj = {
extraData: null,
nodeName: '点击数据或安装数据为空'
}
}
dataObj.children.push(tableObj)
children.push(dataObj)
}
})
},