GraphVis的使用

GraphVis官网:GraphVisGraphVis节点的属性和事件 (yuque.com)

官方示例:

<template>
  <div style="height: 100%;">

    <div class="graph-nav">
        <el-row :gutter="20">
          <el-col :span="8"><div class="grid-content bg-purple">图谱名称:{{knowlegeInfo.name}}</div></el-col>
          <el-col :span="8"><div class="grid-content bg-purple">图谱标识:{{knowlegeInfo.tag}}</div></el-col>
          <el-col :span="8"><div class="grid-content bg-purple">图谱描述:{{knowlegeInfo.desc}}</div></el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="8"><div class="grid-content bg-purple">实体数量:{{knowlegeInfo.nodeNum}}</div></el-col>
          <el-col :span="8"><div class="grid-content bg-purple">关系数量:{{knowlegeInfo.relationNum}}</div></el-col>
          <el-col :span="8"><div class="grid-content bg-purple">图谱权限:{{knowlegeInfo.permission}}</div></el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="8">
            <div class="grid-content bg-purple">属性展开:
                <el-switch
                  v-model="properShow"
                  active-color="#13ce66"
                  active-text="开"
                  inactive-text="关"
                  @change="changeProperShow()">
                </el-switch>
            </div>
          </el-col>
        </el-row>
    </div >

    <!--图显示区域-->
    <div class="graph-area" v-loading="loading">
      <!-- 绘图面板区域 -->
      <div id="graph-panel" style="width: 100%;height: 100%;"></div>

      <!-- 图例区域 -->
      <div id="graph-legend" class="legend-wrap">
          <div v-for="(item,index) in graphLegend" :key="index" class="legend-item">
            <div class="item-dot" :style="{'background-color': item.show ? item.color : 'rgb(210,210,210)' }" @click="showOrHideType(index)"></div>
            <div class="item-label" :style="{'color': item.show ? '' : 'rgb(210,210,210)' }">{{ item.type }}</div>
          </div>
      </div>

      <!-- 工具栏 -->
      <div class="toolbar">
          <div class="toolbar-item" @click="toolBarEvent('layout','fastFR')" title="网络布局"><i class="el-icon-cpu"></i></div>
          <div class="toolbar-item" @click="toolBarEvent('layout','hubsize')" title="层次布局"><i class="el-icon-share"></i></div>

          <div class="toolbar-item" @click="toolBarEvent('zoom','zoomOut')" title="放大"><i class="el-icon-zoom-in"></i></div>
          <div class="toolbar-item" @click="toolBarEvent('zoom','auto')" title="适中"><i class="el-icon-help"></i></div>
          <div class="toolbar-item" @click="toolBarEvent('zoom','zoomIn')" title="缩小"><i class="el-icon-zoom-out"></i></div>
      </div>
    </div>

    <!-- 节点右键操作菜单 -->
    <div id="nodeRightMenuPanel" class="right-menu-layer">
      <button @click="showNodeDeteail()"><i class="el-icon-notebook-2"></i>节点属性</button>
      <button @click="hideNode()"><i class="el-icon-s-release"></i>隐藏</button>
    </div>

    <!-- 连线右键操作对话栏 -->
    <div id="linkRightMenuPanel" class="right-menu-layer">
      <button @click="showLinkDetail()"><i class="el-icon-notebook-2"></i>关系属性</button>
      <button @click="hideLink()"><i class="el-icon-s-release"></i>隐藏</button>
    </div>

    <!-- 节点属性信息显示弹出层 -->
    <el-drawer title="节点属性信息" :modal-append-to-body="false" :wrapperClosable="false" :visible.sync="showNodeInfoDialog" size="380px" ref="drawer" >
      <div style="padding:5px 10px;">
        <div class="box-card">
          <el-form :model="currentNode" label-width="80px" size="mini">
            <el-form-item label="节点ID">
              <el-input type="text" v-model="currentNode.id" label="节点ID" disabled></el-input>
            </el-form-item>
            <el-form-item label="节点名称">
              <el-input type="text" v-model="currentNode.label" label="节点名称" disabled></el-input>
            </el-form-item>
            <el-form-item label="节点类型">
              <el-input type="text" v-model="currentNode.type" label="节点类型" disabled></el-input>
            </el-form-item>
            <el-form-item label="颜色(rgb)">
               <el-color-picker v-model="currentNode.color" :color-format="'rgb'" :show-alpha="false" disabled></el-color-picker>
            </el-form-item>
          </el-form>
        </div>

        <div class="box-card">
            <el-table
               :data="attrbutes"
               border
               size="small"
               style="width:100%;">
               <el-table-column prop="name" label="属性"></el-table-column>
               <el-table-column prop="type"  label="数据类型" width="100"></el-table-column>
               <el-table-column prop="unit" label="单位" width="100"></el-table-column>
            </el-table>
        </div>
      </div>
    </el-drawer>

    <!-- 属性节点显示弹出层 -->
    <el-drawer title="属性信息" :modal-append-to-body="false" :wrapperClosable="false" :visible.sync="showProperNodeInfo" size="380px" ref="drawer" >
      <div style="padding:5px 10px;">
        <div class="box-card">
          <el-form :model="properNode" label-width="80px" size="mini">
            <el-form-item label="名称">
              <el-input type="text" v-model="properNode.name" label="名称" disabled></el-input>
            </el-form-item>
            <el-form-item label="数据类型">
              <el-input type="text" v-model="properNode.type" label="数据类型" disabled></el-input>
            </el-form-item>
            <el-form-item label="单位">
              <el-input type="text" v-model="properNode.unit" label="单位" disabled></el-input>
            </el-form-item>
          </el-form>
        </div>
      </div>
    </el-drawer>

    <!-- 连线属性信息显示弹出层 -->
    <el-drawer title="关系属性信息" :modal-append-to-body="false" :wrapperClosable="false" :visible.sync="showLinkInfoDialog" size="380px" ref="drawer" >
      <div style="padding:5px 10px;">
        <div class="box-card">
          <el-form :model="currentLink" label-width="80px" size="mini">
            <el-form-item label="关系ID">
              <el-input type="text" v-model="currentLink.id" label="关系ID" disabled></el-input>
            </el-form-item>
            <el-form-item label="关系类型">
              <el-input type="text" v-model="currentLink.type" label="关系类型" disabled></el-input>
            </el-form-item>
          </el-form>
        </div>

        <div class="box-card">
            <el-table
               :data="attrbutes"
               border
               size="small"
               style="width:100%;">
               <el-table-column prop="name" label="属性"></el-table-column>
               <el-table-column prop="type"  label="数据类型" width="100"></el-table-column>
               <el-table-column prop="unit" label="单位" width="100"></el-table-column>
            </el-table>
        </div>
      </div>
    </el-drawer>

    <!-- 节点或连线属性提示 -->
    <div id="tip-layer" class="tip-wrap" v-show="tipLayer.show">
      <div class="tip-header">{{tipLayer.header}}</div>
      <div class="tip-body">
        <el-table
           :data="tipLayer.data"
           border
           size="small"
           style="width:100%;">
           <el-table-column prop="name" label="属性"></el-table-column>
           <el-table-column prop="type"  label="数据类型" width="100"></el-table-column>
           <el-table-column prop="unit" label="单位" width="100"></el-table-column>
         </el-table>
      </div>
    </div>

  </div>

</template>

<script>
import VisGraph from '@/assets/js/graphvis.20230812.js'
import LayoutFactory from '@/assets/js/graphvis.layout.min.js'
import { config } from '@/assets/defaultConfig.js'
import { demoData } from '@/assets/demo2.js'
export default {
  data () {
    return {
      // visGraph实例对象
      visGraph: null,
      visLayout: null, // 布局对象
      layoutLoopName: null, // 布局循环对象
      // visGraph可视化交互配置
      config,
      // 示例数据
      demoData,
      circleBgImage: require('@/assets/images/circle.png'), // 节点背景图片
      // visGraph创建节点和连线数据集
      graphData: {
        nodes: [],
        links: []
      },
      graphLegend: [
        // {type:'图例一',color:'rgb(233,120,120)',show:true}
      ], // 图例数组,根据数据的节点类型生成
      knowlegeInfo: {// 图谱信息,需要服务端查询后赋值
        name: '公司网络图谱',
        tag: '公司',
        desc: '某某公司图谱信息',
        nodeNum: 7,
        relationNum: 9,
        permission: '私有'
      },
      properShow: false, // 属性展示配置
      tipLayer: { // 提示层配置
        show: false, // 是否显示提示层
        header: '提示信息', // 提示表头
        data: [] // 提示内部的数据
      },
      currentNode: {}, // 选中的节点对象
      attrbutes: [], // 选中节点或连线的属性列表
      currentLink: {}, // 选中的连线对象
      // 节点详细信息弹框
      showNodeInfoDialog: false,
      // 关系连线信息弹层
      showLinkInfoDialog: false,
      // 属性节点详细弹层控制开关
      showProperNodeInfo: false,
      properNode: {}, // 属性节点
      currentLayoutType: 'fastFR', // 当前布局类型,用于区分是否可以拖动
      loading: true
    }
  },
  methods: {
    // 获取所有的知识节点信息
    async drawGraphData (id) {
      this.graphData = this.demoData
      if (this.visGraph === null) {
        this.createGraph()
        this.refreshGraphData()
      } else {
        this.refreshGraphData()
      }
      this.loading = false
    },
    // 创建全局绘图客户端对象
    createGraph () {
      this.visGraph = new VisGraph(document.getElementById('graph-panel'), this.config)
    },
    // 刷新知识图谱数据
    refreshGraphData () {
      this.visGraph.drawData(this.graphData)
      this.visGraph.moveCenter() // 移动至中心位置
      this.generateLegend()// 生成图例

      this.reLayout()
    },
    generateLegend () { // 生成图例
      var legendMap = new Map()
      this.graphLegend.forEach((item) => {
        legendMap.set(item.type, item)
      })

      this.visGraph.nodes.forEach((node) => {
        if (!legendMap.get(node.type)) {
          legendMap.set(node.type, {
            type: node.type,
            color: `rgb(${node.fillColor})`,
            show: true
          })
        }
      })

      this.graphLegend = []
      for (var legend of legendMap.values()) {
        this.graphLegend.push(legend) // 加入图例记录
      }
    },
    changeProperShow () { // 属性展示开关
      if (this.properShow) {
        this.showProperNodes()
      } else {
        this.hideProperNodes()
      }
    },
    showProperNodes () { // 展示属性节点
      var that = this
      var properNodes = []// 属性节点定义
      var relations = [] // 关系定义
      that.visGraph.nodes.forEach((node) => {
        if (node.visible) {
          var attributes = node.properties.attributes || []// 节点属性

          attributes.forEach((attr, index) => {
            properNodes.push({
              id: `${node.id}-${index}`, // 虚拟节点
              label: attr.name,
              type: 'proper-virtual', // 定义一个虚拟类型,用于后续处理
              x: node.x,
              y: node.y,
              color: '240,240,240', // 属性节点的颜色
              fontColor: '50,50,50', // 属性节点的字体颜色
              borderWidth: 4, // 属性节点边框宽度
              borderColor: '110,110,240', // 属性节点的边框颜色
              properties: {
                attr: attr // 自定义属性定义
              }
            })

            relations.push({
              id: `edge-${node.id}-${index}`,
              source: node.id,
              target: `${node.id}-${index}`,
              label: '属性',
              type: 'proper-virtual',
              lineDash: [3, 5], // 属性设置虚线
              properties: {
                type: 'virtual' // 自定义属性定义
              }
            })
          })
        }
      })

      that.visGraph.addNodes(properNodes)// 添加属性节点
      that.visGraph.addEdges(relations)// 添加属性关系

      that.reLayout()
    },
    hideProperNodes () { // 隐藏属性节点
      var that = this
      var properNodes = that.visGraph.nodes.filter((node) => {
        return node.type == 'proper-virtual'
      })

      if (properNodes.length > 0) {
        that.visGraph.deleteNodes(properNodes)
      }
    },
    // 执行布局算法
    reLayout (alpha) {
      var that = this
      if (alpha == null) {
        that.visLayout = null
        that.visLayout = new LayoutFactory(this.visGraph.getGraphData()).createLayout('fastFR')
        that.visLayout.resetConfig({
          friction: 0.8,
          linkDistance: 200,
          linkStrength: 0.2,
          charge: -250,
          gravity: 0.01,
          noverlap: false,
          size: [that.visGraph.stage.width, that.visGraph.stage.height]
        })
      } else {
        that.visLayout.alpha += (alpha > 1 ? 0.2 : alpha) // 继续运动
      }

      runLayout()// 开始继续动画执行

      // 通过动画帧控制控制布局算法的执行,有动画效果
      function runLayout () {
        cancelAnimationFrame(that.layoutLoopName)// 停止动画控制
        that.visLayout.runLayout() // 运行布局算法
        that.visGraph.refresh()
        if (that.visLayout.alpha > 0.05) {
          that.layoutLoopName = requestAnimationFrame(runLayout)
        } else {
          if (that.visGraph.currentNode && that.visGraph.currentNode.isDragging) {
            that.visLayout.alpha = 0.1 // 继续运动
            that.layoutLoopName = requestAnimationFrame(runLayout)
          } else {
            that.visLayout.alpha = 0 // 停止运动
            cancelAnimationFrame(that.layoutLoopName)
          }
        }
      }
    },
    // 显示节点信息
    showNodeDeteail () {
      if (this.currentNode.type == 'proper-virtual') {
        this.properNode = this.currentNode.properties.attr || {}
        this.showProperNodeInfo = true// 显示属性节点信息
      } else {
        this.showNodeInfoDialog = true// 显示实体节点信息
      }
      this.cancelNodeRightMenu()
    },
    showLinkDetail () {
      this.showLinkInfoDialog = true
      this.cancelLinkRightMenu()
    },
    // 关闭节点提示工具栏
    cancelNodeRightMenu () {
      let nodeRightMenuLayer = document.getElementById('nodeRightMenuPanel')
      nodeRightMenuLayer.style.display = 'none'
    },
    // 关闭知识联系操作栏
    cancelLinkRightMenu () {
      let linkRightMenuLayer = document.getElementById('linkRightMenuPanel')
      linkRightMenuLayer.style.display = 'none'
    },
    showOrHideType (itemIndex) { // 图例的点击事件
      var legend = this.graphLegend[itemIndex]
      if (legend != null) {
        if (legend.show) { // 需要隐藏类型节点
          legend.show = false
        } else { // 需要显示类型节点
          legend.show = true
        }
        this.graphLegend[itemIndex] = legend

        this.renderNodesByLegend()// 重新渲染节点图例
      }
    },
    renderNodesByLegend () { // 根据节点图例渲染
      var legendMap = new Map()
      this.graphLegend.forEach((item) => {
        legendMap.set(item.type, item)
      })

      var that = this
      var legend
      var hideNodes = []
      this.visGraph.nodes.forEach((node) => {
        legend = legendMap.get(node.type) || {show: true}
        that.resetNodesVisible(node, true)
        if (!legend.show) {
          hideNodes.push(node)
        }
      })
      hideNodes.forEach((node) => {
        that.resetNodesVisible(node, false)
      })
    },
    resetNodesVisible (node, visible = true) { // 显示或隐藏节点及其关系
      node.visible = visible;
      (node.inLinks || []).forEach((link) => {
        link.visible = visible
      });
      (node.outLinks || []).forEach((link) => {
        link.visible = visible

        // 如果是虚拟属性节点,则跟着实体节点显示或隐藏
        if (link.target.type == 'proper-virtual') {
          link.target.visible = visible
        }
      })
      this.visGraph.refresh()
    },
    showTipLayer (event) { // 显示提示层
      this.tipLayer.show = true

      const tipDom = document.getElementById('tip-layer')
      tipDom.style.top = event.clientY + 5 + 'px'
      tipDom.style.left = event.clientX + 10 + 'px'
    },
    hideNode () { // 隐藏节点(画布上的删除)
      var properNodes = (this.visGraph.currentNode.outLinks || []).map(function (link) {
        return link.target.type == 'proper-virtual' ? link.target : null
      }).filter((node) => {
        return node != null
      })

      var that = this
      if (properNodes.length > 0) {
        that.visGraph.deleteNodes(properNodes)// 删除属性节点
      }
      this.visGraph.deleteNode(this.visGraph.currentNode)// 删除当前节点

      this.cancelNodeRightMenu()
    },
    hideLink () { // 隐藏连线
      this.visGraph.deleteLink(this.visGraph.currentLink)
      this.cancelLinkRightMenu()
    },
    toolBarEvent (eventType, type) { // 工具栏事件
      if (eventType == 'zoom') {
        this.visGraph.setZoom(type)
      } else if (eventType == 'layout') {
        cancelAnimationFrame(this.layoutLoopName)
        if (type == 'hubsize') {
          this.sortLayout()
        } else {
          this.forceLayout()
        }
      }
    },
    sortLayout () { // 树形结构布局
      this.currentLayoutType = 'hubsize'// 设置当前布局类型
      var tempLayout = new LayoutFactory(this.visGraph.getGraphData()).createLayout('hubsize')
      tempLayout.resetConfig({
        'layerDistance': 150,
        'nodeDistance': 100,
        'sortMethod': 'hubsize',
        'direction': 'LR'
      })
      tempLayout.boolTransition = false
      tempLayout.runLayout()

      this.visGraph.nodes.forEach(function (node) {
        node.fixed = true
      })
      this.visGraph.moveCenter()
    },
    forceLayout () { // 力导向布局
      this.currentLayoutType = 'fastFR'// 设置当前布局类型
      this.visGraph.nodes.forEach((node) => {
        node.fixed = false
      })
      var tempLayout = new LayoutFactory(this.visGraph.getGraphData()).createLayout('fastFR')
      tempLayout.resetConfig({
        friction: 0.8,
        linkDistance: 200,
        linkStrength: 0.2,
        charge: -250,
        gravity: 0.01,
        noverlap: false,
        size: [this.visGraph.stage.width, this.visGraph.stage.height]
      })

      var count = 0
      while (count++ <= 100) {
        tempLayout.runLayout()
      }
      this.visGraph.moveCenter()
    },
    expandNode (node) { // 双击扩展节点数据
      // TODO 需要从服务端查询节点数据,数据结构如demo2格式,以下为构造数据示例
      var newNodes = [], newRelations = []
      for (var i = 1; i <= 10; i++) {
        var tempId = Math.round(Math.random() * 999999999)
        newNodes.push({
          id: 'expandnode-' + tempId,
          label: '扩展节点-' + i,
          type: '扩展节点类型'
          // color: this.randomColor()
        })

        newRelations.push({
          id: 'exedge-' + tempId,
          label: '扩展',
          source: node.id,
          target: 'expandnode-' + tempId
        })
      }

      // 计算新节点的旋转坐标
      this.visGraph.incremaNodesCodinate(newNodes)
      this.visGraph.addNodes(newNodes)
      this.visGraph.addEdges(newRelations)

      this.reLayout()// 执行动态布局计算
    },
    contractChildNode (node) { // 收起节点的叶子节点
      var leafNodes = [], relations = [];
      (node.outLinks || []).forEach(function (l) {
      	if ((l.target.outLinks || []).length == 0 && (l.target.inLinks || []).length == 1) {
      		leafNodes.push(l.target)
      		relations.push(l)
      	}
      })

      var that = this
      that.visGraph.deleteNodes(leafNodes)// 删除叶子节点
      that.visGraph.deleteLinks(relations)// 删除叶子连线

      this.reLayout()// 执行动态布局计算
    },
    randomColor () {
      return Math.floor(255 * Math.random()) + ',' +
      	Math.floor(180 * Math.random()) + ',' +
      	Math.floor(255 * Math.random())
    },
    drawDefinedNode (node) { // 绘制自定义节点
      node.drawNode = function (ctx) { // 绘制自定义节点
        if (!this.openAnimation) { // 通过openAnimation控制是否绘制自定义
          this.drawOriginalNode(ctx)// 系统内置绘制方法
        } else {
          // 以下部分为自定义绘制部分,支持动画效果
          this.animate = this.animate > 50 ? 10 : this.animate
          ctx.save()
          ctx.beginPath()
          this.paintShadow(ctx)
          ctx.arc(0, 0, this.radius, 0, 2 * Math.PI)
          ctx.fillStyle = `rgba(${this.fillColor},${this.alpha})`
          ctx.fill()
          ctx.strokeStyle = `rgba(${this.borderColor},${this.alpha})`
          ctx.stroke()
          ctx.closePath()
          ctx.restore()

          if (this.image) { // 有图片的场景,需要绘制图片
            	var b = ctx.globalAlpha
            	ctx.save()
            	ctx.globalAlpha = this.alpha
            	this.drawNodeImg(ctx, this.image, -this.radius, -this.radius, this.radius)
            	ctx.globalAlpha = b
            	ctx.restore()
          }

          // 内环 - 间隔实线环
          this.angleStart = (this.angleStart == null || this.angleStart >= 0.51) ? 0 : this.angleStart += 0.01
          ctx.save()
          ctx.strokeStyle = `rgba(${this.selectedBorderColor},${this.alpha})`
          for (var i = 0; i < 4; i++) {
            ctx.beginPath()
            ctx.arc(0, 0, this.radius + 3, Math.PI * (0.5 * i), Math.PI * (this.angleStart + (0.5 * i)))
            ctx.stroke()
            ctx.closePath()
          }
          ctx.restore()

          // 外环-虚线圆环
          ctx.save()
          ctx.beginPath()
          ctx.arc(0, 0, this.radius + 8, 0, 2 * Math.PI)
          ctx.strokeStyle = `rgba(${this.selectedBorderColor},${this.alpha})`
          ctx.setLineDash([3, 2])
          ctx.lineDashOffset = this.animate++
          ctx.stroke()
          ctx.closePath()
          ctx.restore()

          // 绘制文字,调用系统内置绘制文字方法
          this.paintText(ctx)
        }
      }
    },
    drawAnimateCircleNode (node) { // 绘制旋转背景图
      var that = this

      // 实例化图片,作为背景图
      if (node.circleBgImage == null) {
        var circleImg = new Image()
        circleImg.setAttribute('crossOrigin', 'Anonymous')
        circleImg.src = that.circleBgImage // 图片资源
        circleImg.onload = function () {
          node.circleBgImage = circleImg // 图片加载完成赋值
        }
      }

      // 绘制自定义节点方法重写
      node.drawNode = function (ctx) {
        if (this.openAnimation) { // 通过openAnimation控制是否绘制自定义
          this.animate = this.animate > 360 ? 0 : this.animate += 0.02
           	var globleAlpha = ctx.globalAlpha
           	ctx.save()
          ctx.rotate(this.animate) // 图片旋转动画控制
           	ctx.globalAlpha = this.alpha
           	this.drawNodeImg(ctx, this.circleBgImage, -this.radius - 15, -this.radius - 15, this.radius + 15)// 绘制背景图
           	ctx.globalAlpha = globleAlpha
           	ctx.restore()
        }

        this.drawOriginalNode(ctx)// 调用系统内置绘制方法
      }
    }
  },
  created () {
    var that = this

    // 节点的点击事件
    this.config.node.onClick = function (event, node) {
      node.color = 'rgb(' + node.fillColor + ')'
      that.currentNode = node
      that.tipLayer.header = node.label || ''
      that.tipLayer.data = node.properties.attributes || [] // 节点属性列表
      that.attrbutes = node.properties.attributes || [] // 节点属性列表
      that.showTipLayer(event)
    }

    // 节点的双击事件
    this.config.node.ondblClick = function (event, node) {
      if (!node.isExpand) {
        node.fixed = true // 固定位置
        node.isExpand = true // 展开标识
        node.openAnimation = true // 启用节点动画特效

        that.expandNode(node)// 节点双击展开

        that.drawDefinedNode(node) // 方案1:绘制自定义动画
        // that.drawAnimateCircleNode(node); //方案2:绘制指定背景图

        that.visGraph.switchAnimate(true) // 开启全局动画开关(耗性能,按需开启和关闭)
      } else {
        node.fixed = false // 解除锁定
        node.isExpand = false // 展开标识关闭
        that.contractChildNode(node)// 节点双击收起

        node.openAnimation = false // 关闭节点动画特效
        node.drawNode = null // 去掉自定义节点绘制
        that.visGraph.switchAnimate(false) // 关闭全局动画(减小性能开销)
      }

      that.tipLayer.show = false // 关闭提示层
      that.generateLegend()// 重新渲染图例类型
    }

    // 节点鼠标弹起事件
    this.config.node.onMouseUp = function (event, node) {
      if (that.currentLayoutType == 'fastFR') { // 如果当前为力导向布局时才可触发力导计算
        that.reLayout(0.2)
      }
    }

    // 节点拖拽事件
    this.config.node.onMousedrag = function (event, node) {
      that.tipLayer.show = false // 关闭提示层
      if (that.currentLayoutType == 'fastFR') { // 如果当前为力导向布局时才可触发力导计算
        that.reLayout(0.05)
      }
    }

    // 连线的点击事件
    this.config.link.onClick = function (event, link) { // 节点点击事件回调
      that.currentLink = link
      that.tipLayer.header = link.label || ''
      that.tipLayer.data = link.properties.attributes || [] // 关系属性列表
      that.attrbutes = link.properties.attributes || [] // 关系属性列表

      that.showTipLayer(event)
    }

    // 空白处的点击事件
    this.config.noElementClick = function (event) {
      that.currentNode = {}
      that.currentLink = {}
      that.tipLayer.show = false // 关闭提示层
    }

    // 右键配置
    this.config.rightMenu = {
      nodeMenu: { // 节点右键菜单
        nodeRightMenuLayer: null,
        init: function (_graphvis) {
          if (!that.nodeRightMenuLayer) {
            that.nodeRightMenuLayer = document.getElementById('nodeRightMenuPanel')
          }
        },
        show: function (event, _graphvis) {
          this.init()
          if (that.nodeRightMenuLayer) {
            that.nodeRightMenuLayer.style.left = (event.clientX + 10) + 'px'
            that.nodeRightMenuLayer.style.top = (event.clientY - 5) + 'px'
            that.nodeRightMenuLayer.style.display = 'block'
          }
          var node = _graphvis.currentNode
          node.color = 'rgb(' + node.fillColor + ')'
          that.currentNode = node
          that.attrbutes = node.properties.attributes || []
        },
        hide: function () {
          if (that.nodeRightMenuLayer) {
            that.nodeRightMenuLayer.style.display = 'none'
          }
        }
      },
      linkMenu: { // 连线右键菜单
        linkDialog: null,
        init: function (_graphvis) {
          if (!that.linkDialog) {
            that.linkDialog = document.getElementById('linkRightMenuPanel')
          }
        },
        show: function (event, _graphvis) {
          this.init()
          if (that.linkDialog) {
            that.linkDialog.style.left = (event.clientX + 10) + 'px'
            that.linkDialog.style.top = (event.clientY - 5) + 'px'
            that.linkDialog.style.display = 'block'
          }
          var link = _graphvis.currentLink
          that.currentLink = link
          that.attrbutes = link.properties.attributes || []
        },
        hide: function () {
          if (that.linkDialog) {
            that.linkDialog.style.display = 'none'
          }
        }
      }
    }
  },
  mounted () {
    // 初始化加载绘图
    this.drawGraphData()
  },
  destroyed () {
    cancelAnimationFrame(this.layoutLoopName)
  }
}
</script>

<style scoped>
  /*****页面主要布局样式定义******/
  .graph-nav{
    height: 80px;
    background-color: #fff;
    line-height: 26px;
    font-size: 13px;
    padding: 10px 20px;
  }
  .graph-area {
    position: relative;
    height: calc(100% - 105px);
    margin: 0 5px 5px 5px;
    padding: 0;
    background-color: #fafafa;
    border: 1px solid #ddd;
  }

  /******工具栏*******/
  .toolbar{
    position: absolute;
    right: 0px;
    top: 0px;
    background: #efefef;
  }

  .toolbar .toolbar-item{
     width:40px;
     height:40px;
     line-height:40px;
     font-size:18px;
     text-align:center;
     color:#888;
     border-bottom:1px solid #ddd;
     cursor: pointer;
  }

  .toolbar .toolbar-item:hover{
     color:deepskyblue;
  }

  /*******图例区域样式定义*****/
  .legend-wrap{
    position: absolute;
    left:10px;
    bottom: 10px;
  }

  .legend-wrap > .legend-item{
     height:24px;
     line-height: 24px;
     font-size: 12px;
  }

  .legend-item > .item-dot{
    float: left;
    display: inline-block;
    height:20px;
    width: 20px;
    border-radius: 10px;
    background-color: rgb(210,210,210);
    cursor: pointer;
  }

  .legend-item > .item-label{
    display: inline-block;
    float: left;
    margin-left: 5px;
    max-width: 120px;
  }

  /*****节点弹出鼠标提示层样式*****/
  .tip-wrap{
    position: absolute;
    width: 350px;
    height: auto;
    min-height: 150px;
    background: #fff;
    box-shadow: 0px 0px 10px #999;
    font-size: 14px;
  }
  .tip-wrap > .tip-header{
    height:30px;
    line-height: 30px;
    padding: 5px 10px;
    border-bottom: 1px solid #ddd;
  }
  .tip-wrap > .tip-body{
     padding: 0 10px 10px;
  }

  /*****右键菜单样式******/
  .right-menu-layer {
    position: absolute;
    width: 100px;
    z-index: 5;
    display: none;
    border-radius: 3px;
    overflow: hidden;
    background: #fafafa;
    border: 1px solid #e1e2e2;
    box-shadow: 0 0 5px #ddd;
    padding: 5px 3px;
  }
  .right-menu-layer button {
    display: block;
    height:24px;
    line-height: 24px;
    background: transparent;
    border: none;
    color: #444;
    text-align:center;
    cursor: pointer;
  }
  .right-menu-layer button > i{
     margin-right: 5px;
  }
  .right-menu-layer button:hover {
    color: slateblue;
  }
  .right-menu-layer button:focus {
    outline:0;
  }

  .box-card{
    box-shadow: 0 0 3px #d4c8c8;
    padding: 10px;
    margin-bottom: 10px;
  }

</style>

js配件:graphvis.20230812.js 和 graphvis.layout.min.js 官网下载

默认defaultConfig配置文件:

export const config = {
  node: { // 节点的默认配置
    label: { // 标签配置
      show: true, // 是否显示
      color: '50,50,50', // 字体颜色
      font: '12px 微软雅黑', // 字体大小及类型
      wrapText: false, // 节点包裹文字 (节点大小由文字大小决定,绘制较耗性能)
      textPosition: 'Bottom_Center',// 文字位置 Middle_Center,Bottom_Center
      //textOffsetY:8, //文字竖向偏移距离
      //background:'250,250,250', //文字背景颜色
      //borderColor:'100,120,220' //背景边框
    },
    shape: 'circle', // 节点形状 circle
    color: '50,155,230', // 节点颜色
    borderColor: '255,255,255', // 边框颜色
    borderWidth: 0, // 边框宽度,
    lineDash: [0], // 边框虚线间隔,borderWidth>0时生效
    alpha: 1, // 节点透明度
    size: 66, // 节点大小
    selected: { // 选中时的样式设置
      borderColor: '50,120,230', // 选中时边框颜色
      borderAlpha: 1, // 选中时的边框透明度
      borderWidth: 4, // 选中是的边框宽度
      showShadow: true, // 是否展示阴影
      shadowBlur:30, //阴影范围大小
      shadowColor: '50,80,250'// 选中是的阴影颜色
    }
  },
  link: { // 连线的默认配置
    label: { // 连线标签
      show: true, // 是否显示
      color: '50,50,50', // 字体颜色
      font: '12px 微软雅黑',// 字体大小及类型
      background:'250,250,250',
      borderColor:'250,250,250'
    },
    lineType: 'direct', // 连线类型,direct,curver,
    arrowType:'thired',
    colorType: 'defined', // 连线颜色类型 source:继承source颜色,target:继承target颜色 both:用双边颜色,defined:自定义
    color: '145,145,155', // 连线颜色
    alpha: 1, // 连线透明度
    lineWidth: 2, // 连线宽度
    lineDash: [0], // 虚线间隔样式如:[5,8]
    showArrow: true, // 显示箭头
    selected: { // 选中时的样式设置
      color: '50,120,230', // 选中时的颜色
      alpha: 1,
      showShadow: false, // 是否展示阴影
      shadowColor: '250,40,30'// 选中连线时的阴影颜色
    }
  },
  highLightNeiber: true, // 相邻节点高亮开关
  wheelZoom: 0.8// 滚轮缩放开关,不使用时不设置[0,1]
}

demo2数据:

export const demoData = {
    "nodes":[
        {
            "id":"123456",
            "label":"爱妃术有限公司",
            "color":"rgb(233,120,120)",
            "type":"公司",
            "properties":{
               "attributes":[
                  {"name":"属性一","type":"字符型","unit":"项"},
                  {"name":"属性二","type":"数值型","unit":"个"},
                  {"name":"属性三","type":"字符型","unit":"次"}
               ]
            }
        },
        {
            "id":"2222",
            "label":"山东暗访十年有限公司",
            "color":"rgb(233,120,120)",
            "type":"公司",
            "properties":{
               "attributes":[
                  {"name":"属性一","type":"字符型","unit":"项"},
                  {"name":"属性二","type":"数值型","unit":"个"},
                  {"name":"属性三","type":"字符型","unit":"次"}
               ]
            }
        },
        {
            "id":"3333",
            "label":"山东阿萨德集团有限公司",
            "color":"rgb(233,120,120)",
            "type":"公司",
            "properties":{
               "attributes":[
                  {"name":"属性一","type":"字符型","unit":"项"},
                  {"name":"属性二","type":"数值型","unit":"个"},
                  {"name":"属性三","type":"字符型","unit":"次"}
               ]
            }
        },
        {
            "id":"4444",
            "label":"贾墀臭",
            "color":"rgb(120,120,233)",
            "type":"公司员工",
            "properties":{
               "attributes":[
                  {"name":"属性一","type":"字符型","unit":"项"}
               ]
            }
        },
        {
            "id":"5555",
            "label":"邓愍摸",
            "color":"rgb(120,233,120)",
            "type":"公司高管",
            "properties":{
               "attributes":[
                  {"name":"属性一","type":"字符型","unit":"项"},
                  {"name":"属性二","type":"数值型","unit":"个"}
               ]
            }
        },
        {
            "id":"6666",
            "label":"邓哿薪",
            "color":"rgb(120,233,120)",
            "type":"公司高管",
            "properties":{
               "attributes":[
                  {"name":"属性一","type":"字符型","unit":"项"},
                  {"name":"属性二","type":"数值型","unit":"个"}
               ]
            }
        },
        {
            "id":"7777",
            "label":"孟断簏",
            "color":"rgb(120,120,233)",
            "type":"公司员工",
            "properties":{
               "attributes":[
                  {"name":"属性一","type":"字符型","unit":"项"}
               ]
            }
        }
    ],
    "links":[
        {
            "id":"e111",
            "source":"3333",
            "target":"123456",
            "type":"子公司",
            "properties":{
               "attributes":[
                  {"name":"关系属性","type":"字符型","unit":"项"}
               ]
            }
        },
        {
            "id":"e222",
            "source":"3333",
            "target":"4444",
            "type":"雇员",
            "properties":{
               "attributes":[
                  {"name":"关系属性","type":"字符型","unit":"项"}
               ]
            }
        },
        {
            "id":"e333",
            "source":"2222",
            "target":"7777",
            "type":"员工",
            "properties":{
               "attributes":[
                  {"name":"关系属性","type":"字符型","unit":"项"}
               ]
            }
        },
        {
            "id":"e444",
            "source":"3333",
            "target":"7777",
            "type":"员工",
            "properties":{
               "attributes":[
                  {"name":"关系属性","type":"字符型","unit":"项"}
               ]
            }
        },
        {
            "id":"e555",
            "source":"3333",
            "target":"2222",
            "type":"投资",
            "properties":{
               "attributes":[
                  {"name":"关系属性","type":"字符型","unit":"项"}
               ]
            }
        },
        {
            "id":"e666",
            "source":"2222",
            "target":"3333",
            "type":"子公司"
        },
        {
            "id":"e777",
            "source":"2222",
            "target":"6666",
            "type":"股东"
        },
        {
            "id":"e888",
            "source":"3333",
            "target":"6666",
            "type":"董事长",
            "properties":{
               "attributes":[
                  {"name":"关系属性","type":"字符型","unit":"项"}
               ]
            }
        },
        {
            "id":"e999",
            "source":"2222",
            "target":"5555",
            "type":"总经理"
        }
    ]
};
相关推荐
耶啵奶膘9 分钟前
uniapp-是否删除
linux·前端·uni-app
王哈哈^_^2 小时前
【数据集】【YOLO】【目标检测】交通事故识别数据集 8939 张,YOLO道路事故目标检测实战训练教程!
前端·人工智能·深度学习·yolo·目标检测·计算机视觉·pyqt
cs_dn_Jie2 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic3 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿3 小时前
webWorker基本用法
前端·javascript·vue.js
cy玩具3 小时前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
customer084 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
清灵xmf4 小时前
TypeScript 类型进阶指南
javascript·typescript·泛型·t·infer
小白学大数据4 小时前
JavaScript重定向对网络爬虫的影响及处理
开发语言·javascript·数据库·爬虫
qq_390161774 小时前
防抖函数--应用场景及示例
前端·javascript