Bootstrap

ant V G6自定义树图实现动态加载数据

G6官网: 

ps://g6.antv.antgroup.com/examplesicon-default.png?t=N7T8https://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-CSDN博客使用srcipt标签动态加载cdnhttps://blog.csdn.net/Humanideal/article/details/132722161?spm=1001.2014.3001.5501

//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)
          }
        })
    },

;