在Vue中使用3d-force-graph渲染neo4j图谱


最近用 3d-force-graph做了下neo4j的可视化,3D效果很好。并总结了下 3d-force-graph库在 vue.js下的的简单使用,如有描述错误的地方敬请指出。

    "neo4j-driver": "^4.2.2",
    "3d-force-graph": "^1.67.7",
  1. 创建template标签,创建一个渲染容器标签graph,对其引用。
<template>
  <div ref="graph" id="graph"></div>
</template>
  1. 在script中导入ForceGraph3D。
import ForceGraph3D from "3d-force-graph";
  1. 在vue中声明所需的变量
data() {
    return {
      myGraph: null,            		// 3D-graph对象
      graphData: null,					// 3D-graph加载的图数据
      db:{								// 数据库连接配置:
        uri : 'bolt://111.230.233.89',  // 		neo4j地址(不给端口默认7687)
        user : 'neo4j',                 // 		数据库用户名(默认neo4j,修改成自己的)
        password : 'neo4j'				// 		数据库密码(默认ne4oj,修改成自己的)
      },
    };
  1. 开始准备数据,从neo4j中读取数据。此处代码通用,读取所有属性和标签,无需手动修改。函数返回 {Promise<node_info, rel_info>}node_inforel_info都是字典,存储所有节点和边信息。key为neo4j数据库中节点的ID和边的ID。示例:
node_info[节点ID] = {
	labels: 节点的所有labels	// 数据类型是一个字符串,多个标签之间使用<逗号>隔开
	attrs:  节点的所有属性	// 数据类型是一个字典,包含节点的所有属性 
}
rel_info[ID] = {
	type:	边的名称/类型	// 数据类型是一个字符串,多个标签之间使用<逗号>隔开
	attrs:	边的所有属性		// 数据类型是一个字典,包含节点的所有属性 
	source:	边的首节点ID		
	target:	边的尾节点ID
}
  • 完整代码如下:
/**
     * 读取neo4j结果
     * @param limit_items 返回的条目数量
     * @returns {Promise<node_info, rel_info>}
     */
    async getCyperResult(limit_items) {
      const start = new Date()
      const neo4j = require('neo4j-driver')
      const driver = neo4j.driver(this.db.uri, neo4j.auth.basic(this.db.user, this.db.password))
      const session = driver.session()
      const result = await session.run(
          'MATCH (n)-[r]->(m) ' +
          'RETURN ' +
          'id(n) as source, labels(n) as source_labels, properties(n) as source_attrs, ' +
          'id(m) as target, labels(m) as target_labels, properties(m) as target_attrs, ' +
          'id(r) as link,     type(r) as r_type,          properties(r) as r_attrs ' +
          'LIMIT $limit_items ',
          {limit_items: neo4j.int(limit_items)}
      );

      /* 存储节点和边信息
       * node_info[节点ID] = {节点标签:list, 节点属性:dict}
       *   rel_info[边ID] = {  边类别:str,   边属性:dict}
       */
      const node_info = {}
      const rel_info = {}
      result.records.map(r => {
        node_info[r.get('source').toString()] = {
          labels: r.get('source_labels').toString(),
          attrs: r.get('source_attrs').toString()
        };
        node_info[r.get('target').toString()] = {
          labels: r.get('target_labels').toString(),
          attrs: r.get('target_attrs')
        }
        rel_info[r.get('link').toString()] = {
          type: r.get('r_type').toString(),
          attrs: r.get('r_attrs'),
          source: r.get('source').toString(),
          target: r.get('target').toString()
        }
      });
      console.log(Object.keys(node_info).length + " nodes loaded and " + Object.keys(rel_info).length + " links loaded in " + (new Date() - start) + " ms.")
      return {
        node_info,
        rel_info
      }
    }
  1. 构建插件渲染所需要的数据格式。3d-graph渲染的数据格式为一个字典,字典中包含一个nodes(包含所有节点),和一个links(包含所有边)。
    nodes是一个数组,每一个数组元素是一个节点字典(字典中至至少包含idid是节点的唯一标识)。如:nodes=[ {id:1}, {id:2}, {id:3}],字典中也可以存放其他属性,如:nodes=[ {id:1, labels:['Stu', 'Son'], age:13}, {id:2, labels:['Stu', 'Son'], age:14}]。 该属性在构建图动态渲染中可以通过回调函数引用。
    links是一个数组,每一个数组元素是一个边字典(字典中至少包含sourcetarget,sourcetarget分别代表首尾节点,sourcetarget要对应nodes中的id
    let graph_info = await this.getCyperResult(100000)
      /** 构造3D-Graph数据的边 */
      const links = Object.values(graph_info.rel_info);
      /** 构造3D-Graph数据的节点 */
      const nodes = Object.entries(graph_info.node_info).map(entry=>{
        return {id:entry[0], labels:entry[1].labels, attrs:entry[1].attrs}
      })
      this.graphData = {
        nodes: nodes, 
        links: links
      }
  1. 数据构建完之后就可以创建图。这里创建ForceGraph3D对象,并设置一些基本样式。本人这里列举一些简单常用的。标注有错误还请指出。
/********************************************** 1.创建图 **********************************************/
      this.myGraph = ForceGraph3D({
        controlType: "trackball",                                                       // orbit沿2d轨迹绕着拖动,fly 固定不动
        rendererConfig:{ antialias: true, alpha: true }
      })(this.$refs.graph)
        /*------------------------------------------- 画布配置 -------------------------------------------*/
        .backgroundColor("black")                                                       // 背景颜色,支持内置颜色和RGB
        .width(this.$refs.graph.parentElement.offsetWidth )                             // 画布宽度(充满父级容器)
        .height(this.$refs.graph.parentElement.offsetHeight+150)                        // 画布高度(充满父级容器)
        .showNavInfo(false)                                                             // 是否显示底部导航提示信息
        /*------------------------------------------- 节点配置 -------------------------------------------*/
        .nodeRelSize(1)                                                                 // 节点大小(支持数值)
        .nodeVal(node => node.size * 0.05)                                              // 节点大小(支持回调)
        .nodeAutoColorBy('id')                                                          // 节点颜色:根据属性划分(参数为graphData({nodes: nodes, links: links}))中nodes中每个node中的属性名称)
        .nodeAutoColorBy(node => node.id)                                               // 节点颜色:回调函数处理(功能同上)
        .nodeOpacity(1)                                                                 // 节点透明度:回调函数处理(根据label划分)
        .nodeLabel("labels")                                                            // 节点标签显示内容(鼠标滑到节点显示,支持直接写节点属性名称)
        .nodeLabel(node => node.labels+'<br>'+JSON.stringify(node.attrs)) 				// 节点标签显示内容(鼠标滑到节点显示,也可以使用回调函数)
        .onNodeHover(node => this.$refs.graph.style.cursor = node ? 'pointer' : null)	// 鼠标滑到节点上改变指针
        .onNodeClick(node => {														    // 点击节点事件(视角转移到该节点)
          // Aim at node from outside it
          const distance = 40;
          const distRatio = 1 + distance / Math.hypot(node.x, node.y, node.z);
          this.myGraph.cameraPosition(
              {x: node.x * distRatio, y: node.y * distRatio, z: node.z * distRatio},// new position
              node, // lookAt ({ x, y, z })
              3000  // ms transition duration)
          )
        })
        /*------------------------------------------- 边的配置 -------------------------------------------*/
        .linkVisibility(true)                                                 // 是否显示边
        .linkLabel(r => r.type)                                               // 边的标签显示(鼠标滑到边上显示)
        .linkDirectionalArrowLength(3.5)                                      // 边的指向箭头长度
        .linkDirectionalArrowRelPos(1)                                        // 边的标签显示(鼠标滑到边上显示)
        .linkCurvature(0.25)                                                  // 边的透明度
        .linkDirectionalParticles(5)                                          // 边粒子:数量
        .linkDirectionalParticleSpeed(1)                                      // 边粒子:移动速度
        .linkDirectionalParticleWidth(0.3)                                    // 边粒子:大小
        .linkColor(()=>'RGB(170,170,170)')                                    // 边颜色
        .linkAutoColorBy(r => r.type)                                         // 边颜色自动化分
        .linkOpacity(0.5)                                                     // 边透明度(越小越透明)
  1. 图构建完成后就可以加载处理好的数据,即可渲染图谱。
  /********************************************** 2.加载数据 **********************************************/
      let graph_info = await this.getCyperResult(100000)
      /** 构造3D-Graph数据的边 */
      const links = Object.values(graph_info.rel_info);
      /** 构造3D-Graph数据的节点 */
      const nodes = Object.entries(graph_info.node_info).map(entry=>{
        return {id:entry[0], labels:entry[1].labels, attrs:entry[1].attrs}
      })
      this.myGraph.graphData({
        nodes: nodes, links: links
      })
  1. 这里加入图的一些动态修改。比如修改图的边长,使图进行旋转等。非必须。(注意如果这里设置图旋转后,就无法使用鼠标对图进行放大缩小,因为每次的坐标都被还原)
  /********************************************** 3.动态设置 **********************************************/
      /*  修改边长度,同d3引擎用法  */
      this.myGraph.d3Force('link').distance(400);
      /*  设置图谱自动旋转  */
      const distance = 500;
      let angle = 0;
      setInterval(() => {
        this.myGraph.cameraPosition({
          x: distance * Math.sin(angle),
          y: distance * Math.sin(angle),
          z: distance * Math.cos(angle)
        });
        angle += Math.PI / 1000;
      }, 100);

完整代码如下:

<template>
  <div  ref="graph" id="graph"></div>
</template>

<script>
import ForceGraph3D from "3d-force-graph";
export default {
  name: "graph",
  data() {
    return {
      myGraph: null,
      graphData: null,
      db:{
        uri : this.$conf.neo4j.url,
        user : this.$conf.neo4j.username,
        password : this.$conf.neo4j.password
      },
    };
  },
  mounted() {
      this.initGraph ()
  },
  methods: {
    async initGraph() {
      /********************************************** 1.创建图 **********************************************/
      this.myGraph = ForceGraph3D({
        controlType: "trackball",                                                                 // orbit沿2d轨迹绕着拖动,fly 固定不动
        rendererConfig:{ antialias: true, alpha: true }
      })(this.$refs.graph)
        /*------------------------------------------- 画布配置 -------------------------------------------*/
        .backgroundColor("black")                                                           // 背景颜色,支持内置颜色和RGB
        .width(this.$refs.graph.parentElement.offsetWidth )                                       // 画布宽度(充满父级容器)
        .height(this.$refs.graph.parentElement.offsetHeight+150)                           // 画布高度(充满父级容器)
        .showNavInfo(false)                                                               // 是否显示底部导航提示信息
        /*------------------------------------------- 节点配置 -------------------------------------------*/
        .nodeRelSize(1)                                                                           // 节点大小(支持数值)
        .nodeVal(node => node.size * 0.05)                                                        // 节点大小(支持回调)
        .nodeAutoColorBy('id')                                                                    // 节点颜色:根据属性划分(参数为graphData({nodes: nodes, links: links}))中nodes中每个node中的属性名称)
        .nodeAutoColorBy(node => node.id)                                                         // 节点颜色:回调函数处理(功能同上)
        .nodeOpacity(1)                                                                           // 节点透明度:回调函数处理(根据label划分)
        .nodeLabel("labels")                                                          // 节点标签显示内容(鼠标滑到节点显示,支持直接写节点属性名称)
        .nodeLabel(node => node.labels+'<br>'+JSON.stringify(node.attrs))             // 节点标签显示内容(鼠标滑到节点显示,也可以使用回调函数)
        .onNodeHover(node => this.$refs.graph.style.cursor = node ? 'pointer' : null)     // 鼠标滑到节点上改变指针
        .onNodeClick(node => {                                                            // 点击节点事件(视角转移到该节点)
          // Aim at node from outside it
          const distance = 40;
          const distRatio = 1 + distance / Math.hypot(node.x, node.y, node.z);
          this.myGraph.cameraPosition(
              {x: node.x * distRatio, y: node.y * distRatio, z: node.z * distRatio}, // new position
              node, // lookAt ({ x, y, z })
              3000  // ms transition duration)
          )
        })
        /*------------------------------------------- 边的配置 -------------------------------------------*/
        .linkVisibility(true)                                                                    // 是否显示边
        .linkLabel(r => r.type)                                                      // 边的标签显示(鼠标滑到边上显示)
        .linkDirectionalArrowLength(3.5)                                                         // 边的指向箭头长度
        .linkDirectionalArrowRelPos(1)                                                           // 边的标签显示(鼠标滑到边上显示)
        .linkCurvature(0.25)                                                                     // 边的透明度
        .linkDirectionalParticles(5)                                                             // 边粒子:数量
        .linkDirectionalParticleSpeed(1)                                                         // 边粒子:移动速度
        .linkDirectionalParticleWidth(0.3)                                                       // 边粒子:大小
        .linkColor(()=>'RGB(170,170,170)')                                                       // 边颜色
        .linkAutoColorBy(r => r.type)                                                            // 边颜色自动化分
        .linkOpacity(0.5)                                                                        // 边透明度(越小越透明)

      /********************************************** 2.加载数据 **********************************************/
      let graph_info = await this.getCyperResult(100000)
      /** 构造3D-Graph数据的边 */
      const links = Object.values(graph_info.rel_info);
      /** 构造3D-Graph数据的节点 */
      const nodes = Object.entries(graph_info.node_info).map(entry=>{
        return {id:entry[0], labels:entry[1].labels, attrs:entry[1].attrs}
      })
      this.myGraph.graphData({
        nodes: nodes, links: links
      })
      /********************************************** 3.动态设置 **********************************************/
      /*  修改边长度,同d3引擎用法  */
      this.myGraph.d3Force('link').distance(400);
      /*  设置图谱自动旋转  */
      const distance = 500;
      let angle = 0;
      setInterval(() => {
        this.myGraph.cameraPosition({
          x: distance * Math.sin(angle),
          y: distance * Math.sin(angle),
          z: distance * Math.cos(angle)
        });
        angle += Math.PI / 1000;
      }, 100);
    },
    /**
     * 读取neo4j结果
     * @param limit_items
     * @returns {Promise<node_info, rel_info>}
     */
    async getCyperResult(limit_items) {
      const start = new Date()
      const neo4j = require('neo4j-driver')
      const driver = neo4j.driver(this.db.uri, neo4j.auth.basic(this.db.user, this.db.password))
      const session = driver.session()
      const result = await session.run(
          'MATCH (n)-[r]->(m) ' +
          'RETURN ' +
          'id(n) as source, labels(n) as source_labels, properties(n) as source_attrs, ' +
          'id(m) as target, labels(m) as target_labels, properties(m) as target_attrs, ' +
          'id(r) as link,     type(r) as r_type,          properties(r) as r_attrs ' +
          'LIMIT $limit_items ',
          {limit_items: neo4j.int(limit_items)}
      );

      /* 存储节点和边信息
       * node_info[节点ID] = {节点标签:list, 节点属性:dict}
       *   rel_info[边ID] = {  边类别:str,   边属性:dict}
       */
      const node_info = {}
      const rel_info = {}
      result.records.map(r => {
        node_info[r.get('source').toString()] = {
          labels: r.get('source_labels').toString(),
          attrs: r.get('source_attrs').toString()
        };
        node_info[r.get('target').toString()] = {
          labels: r.get('target_labels').toString(),
          attrs: r.get('target_attrs')
        }
        rel_info[r.get('link').toString()] = {
          type: r.get('r_type').toString(),
          attrs: r.get('r_attrs'),
          source: r.get('source').toString(),
          target: r.get('target').toString()
        }
      });
      console.log(Object.keys(node_info).length + " nodes loaded and " + Object.keys(rel_info).length + " links loaded in " + (new Date() - start) + " ms.")
      return {
        node_info,
        rel_info
      }
    },
  }
};
</script>

<style scoped>
#graph{
  background-color: rgba(0,0,0,1);
  padding: 1rem;
  height:100vh;
  /*min-width: 300px;*/
  width: 100%;
  border-radius: 5px;
}
</style>

Logo

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

更多推荐