3D力导向树插件 3d-force-graph

发布时间 2023-09-25 17:18:51作者: 混名汪小星

3d-force-graph是什么?

一个 Web 组件,使用强制导向的迭代布局来表示 3 维空间中的图形数据结构。使用ThreeJS /WebGL 进行 3D 渲染,使用d3-force-3dngraph作为底层物理引擎。

 

3d-force-graph可以做些什么?

参考以下效果:

哔哩哔哩:https://www.bilibili.com/video/BV1WS4y1s7st/?spm_id_from=333.999.0.0&vd_source=cad25e75f283bed20b688da5f217b5d6

 

 

 

人物关系图:https://trails-game.com/relations/

 

3d-force-graph如何用?

引入

安装 npm install 3d-force-graph
import ForceGraph3D from '3d-force-graph';
//或者 const ForceGraph3D = require('3d-force-graph');

使用版本  "3d-force-graph": "^1.70.14",

容器

<div id="3d-graph" class="graph-3d"></div>

 画布初始化

this.Graph = ForceGraph3D()(document.getElementById('3d-graph'))
        .height(1000)  //画布高度
        .backgroundColor('rgba(0,0,0,0)')  //画布背景色
        .showNavInfo(false)   //禁用页脚

数据准备阶段

数据结构  选取部分数据仅作为参考,不可作为demo数据引入

this.highlightNodes = new Set()
this.highlightLinks = new Set()
this.hoverNode = null
this.nodes = [{
    "id": 2,    //节点id
    "labels": [
        "N2"   //自定义层级
    ],
    "property": {
        "name": "二十大"  //节点文本
    },
    "name": "二十大",  //节点文本
    "group": 0
},{
    "id": 3,
    "labels": [
        "N3"
    ],
    "property": {
        "name": "过去五年的工作和新时代十年的伟大变革"
    },
    "name": "过去五年的工作和新时代十年的伟大变革",
    "color": [   //节点颜色
        "rgba(215,255,171,1)",   //高亮时的颜色
        "rgba(215,255,171,0.2)"   //
    ],
    "group": 1
},{
    "id": 4,
    "labels": [
        "N4"
    ],
    "property": {
        "name": "过去五年的工作和新时代十年的伟大变革"
    },
    "name": "过去五年的工作和新时代十年的伟大变革",
    "typeNum": 0,
    "group": 2,
    "color": [
        "rgba(215,255,171,1)",
        "rgba(215,255,171,0.2)"
    ]
},{
    "id": 5,
    "labels": [
        "N4"
    ],
    "property": {
        "name": "五年的重大成就"
    },
    "name": "五年的重大成就",
    "typeNum": 0,
    "group": 2,
    "color": [
        "rgba(215,255,171,1)",
        "rgba(215,255,171,0.2)"
    ]
},
...
]
this.links = [
        {
          id: 2,   //线id
          start: 2,  //连线开始节点 id
          end: 3,   //连线结束节点的id
          type: '内容明细',  //线上文字
          identity: null,
          property: {},
          source: 2, //连线开始节点 id
          target: 3, //连线开始节点 id
          isShow: true,  //是否显示线
          group: 1,
          typeNum: 0
        },
        {
          id: 27,
          start: 2,
          end: 28,
          type: '内容明细',
          identity: null,
          property: {},
          source: 2,
          target: 28,
          isShow: true,
          group: 1,
          typeNum: 1
        },
        {
          id: 3,
          start: 3,
          end: 4,
          type: '解读',
          identity: null,
          property: {},
          source: 3,
          target: 4,
          isShow: true,
          typeNum: 0,
          group: 2
        },
        {
          id: 4,
          start: 3,
          end: 5,
          type: '解读',
          identity: null,
          property: {},
          source: 3,
          target: 5,
          isShow: true,
          typeNum: 0,
          group: 2
        },
        {
          id: 5,
          start: 3,
          end: 6,
          type: '解读',
          identity: null,
          property: {},
          source: 3,
          target: 6,
          isShow: true,
          typeNum: 0,
          group: 2
        }
      ]

 

加入数据  

this.Graph.graphData({
        nodes: this.nodes,
        links: this.links
      })
node 节点,添加周围的节点和线
this.buildNeighboursAndTestPos({
        nodes: this.nodes,
        links: this.links
      })
buildNeighboursAndTestPos(data) {
      return data.links.forEach(link => {
        const a = data.nodes.find(item => item.id == link.start)
        const b = data.nodes.find(item => item.id == link.end)
        !a.neighbors && (a.neighbors = [])
        !b.neighbors && (b.neighbors = [])
        a.neighbors.push(b)
        b.neighbors.push(a)
        !a.links && (a.links = [])
        !b.links && (b.links = [])
        a.links.push(link)
        b.links.push(link)
      })
    },

 

节点文字使用  three-spritetext

import SpriteText from 'three-spritetext'

使用版本  "three-spritetext": "^1.6.5"

渲染节点

   node() {
      this.Graph.nodeThreeObject(node => {
        const sprite = new SpriteText(`${node.name}`)
        sprite.color = 'rgba(255, 255, 255)'
        sprite.textHeight = 1.5
        sprite.fontSize = 120
        sprite.fontWeight = 'bold'
        sprite.position.set(0, 13, 0)
        return sprite
      })
        .nodeThreeObjectExtend(true)
        // 节点是否隐藏
        // .nodeVisibility(node => {
        //   if (node.id == 0) return true
        //   return node.show ? true : false
        // })
        .nodeOpacity(1)
        .nodeColor(node => {
          if (!node.color) return
          if (!this.highlightNodes.size) return node.color[0]
          return this.highlightNodes.has(node) ? node.color[0] : node.color[1]
        })
        .nodeRelSize(8)
        // 节点文字
        .linkThreeObjectExtend(true)
    },
线和线上文字、箭头
   // 线和线上文字
    linkText() {
      this.Graph.linkThreeObjectExtend(true)
        .linkThreeObject(link => {
          const sprite = new SpriteText(`${link.type}`)
          sprite.color = 'rgba(255, 255, 255)'
          sprite.textHeight = 1.5
          return sprite
        })
        .linkPositionUpdate((sprite, {start, end}) => {
          const middlePos = Object.assign(
            ...['x', 'y', 'z'].map(c => ({
              [c]: start[c] + (end[c] - start[c]) / 2 // calc middle point
            }))
          )
          // Position sprite
          Object.assign(sprite.position, middlePos)
        })
        // 曲线
        .linkCurvature(0)
        // 线颜色
        .linkColor(link => {
          if (!this.highlightLinks.size) {
            return this.nodes.find(item => item.id == link.end).color[0]
          }
          return this.highlightLinks.has(link)
            ? this.nodes.find(item => item.id == link.end).color[0]
            : this.nodes.find(item => item.id == link.end).color[1]
        })
        // 线宽度
        // .linkWidth(link => 0.3)
        .linkWidth(link => (this.highlightLinks.has(link) ? 1 : 0.3))
        .linkOpacity(0.7)
    },
    // 箭头
    linkArrow() {
      this.Graph.linkDirectionalArrowLength(2).linkDirectionalArrowRelPos(1)
    },

 

问题总结:

1.节点过多或增加连线曲度,画布卡顿

问题原因:

  SpriteText 渲染文字,GPU负荷过大导致卡顿,改为透明度,来区分选中项文字高亮

//SpriteText 渲染文字
    this.Graph.linkThreeObjectExtend(true)
        .linkThreeObject(link => {
          const sprite = new SpriteText(`${link.type}`)
          // if (!this.highlightLinks.size) {
          //   sprite.color = 'rgba(255, 255, 255)'
          // } else {
          //   this.highlightLinks.has(link) ? (sprite.color = 'rgba(255, 255, 255)') : (sprite.color = 'rgba(255, 255, 255,0.1)')
          // }
          sprite.color = 'rgba(255, 255, 255)'
          sprite.textHeight = 1.5
          return sprite
        })
  //更新节点文字
updateHighlight() {
      this.links.forEach(link => {
        if (this.hoverNode && !this.highlightLinks.has(link)) {
          link.__lineObj.visible = false
          link.__arrowObj.visible = false
        } else {
          link.__lineObj.visible = true
          link.__arrowObj.visible = true
        }
      })
      this.nodes.forEach(node => {
        if (this.hoverNode && !this.highlightNodes.has(node)) {
          node.__threeObj.children[0].material.opacity = 0.1
        } else {
          node.__threeObj.children[0].material.opacity = 1
        }
      })
      this.Graph.nodeColor(this.Graph.nodeColor())
      // this.Graph.nodeThreeObject(this.Graph.nodeThreeObject())
      // this.Graph.nodeColor(this.Graph.nodeColor()).linkColor(this.Graph.linkColor())  
      // this.Graph.linkThreeObject(this.Graph.linkThreeObject()) //此方法对gpu性能消耗较高
      // this.Graph.linkWidth(this.Graph.linkWidth())
    },

2.旋转动画,开始与结束会有失帧

 解决问题:

开启动画时,无法获取当前坐标,只能曲线救国

data(){
return {
    angle: 0,
      distance: 1400,
      amiTimer: null
}
}

watch: {
//是否开启动画
    isRotationActive(newVal) {
      if (newVal) {
        let node = this.nodes.find(ii => ii.name == '十四五规划')
        this.Graph.cameraPosition(
          {
            x: this.distance * Math.sin(this.angle),
            z: this.distance * Math.cos(this.angle)
          },
          node,
          300
        )
        setTimeout(() => {
          clearInterval(this.amiTimer)
          this.amiTimer = setInterval(() => {
            this.Graph.cameraPosition(
              {
                x: this.distance * Math.sin(this.angle),
                z: this.distance * Math.cos(this.angle)
              },
              node
            )
              .enableNodeDrag(false)
              .onNodeHover(() => {})
              .enableNavigationControls(false)
            this.angle += Math.PI / 300
          }, 10)
        }, 300)
      } else {
        clearInterval(this.amiTimer)
        setTimeout(() => {
          this.Graph.enableNavigationControls(true)
          this.nodeHover()
          this.amiTimer = null
        }, 10)
      }
    }
  },

3.旋转方向不能统一

暂未解决。

相关资源:

文档:https://github.com/vasturiano/3d-force-graph

人物关系图demo:https://github.com/trails-game/relation-graph-3d-force

 参考:https://www.jianshu.com/p/15f5ca09ad69