vis-network绘制拓扑图


前言

我自己了解到的绘制拓扑图的开源库有vis和阿里的G6,G6在项目中没机会使用,只用到的vis,有时间再玩一下G6,G6好像比vis功能强大一些,下面主要说一下vis。

vis与vis-network认识

我接手拓扑项目时,项目中使用的是vis。现场反馈一个需求,说是要自定义线条的箭头。我在官网上看到可以进行自定义箭头,但是一使用就是报错,很是郁闷不理解。

在查阅大量资料后,发现有demo竟然没有完全引入vis,单独引入了vis框架中的一个vis-network,也可以成功绘制拓扑图。紧接着去研究vis与vis-network的区别,研究发现vis-network是vis框架中的一个功能组件,主要是用来绘制拓扑图,而且拓扑项目中也只使用到了vis框架中的绘制拓扑图功能。于是在项目中将vis更换成最新版本的vis-network,并进行自定义箭头的尝试,发现可以进行自定义箭头,满足项目的需求。

因此我建议如果项目中只使用到了绘制拓扑图能力时,只引入vis-network即可,这样引入的包体积减小,可使用的功能更多。

npm install vis-network@9.1.0  (安装的是 ^9.1.0 版本) 当时network的最新版本
// 这里最好使用ESModule的模块引入方式,这里我不修改了,有使用到自己研究修改
const vis = require("vis-network/dist/vis-network.min.js"); // vis-network 引入方式

vis-network使用排坑

  • 使用vis-network不要使用vis,首先是vis是多个功能板块的集合,项目中只会使用到其中的vis-network;还有虽然vis-network包含在vis中,但是有些功能在vis中无法使用,使用了就会报错。如线条的箭头图标改为image类型。
  • vis-network绘制出来后,激活拓扑图会自带outline,但是找不到哪个元素添加的outline,需添加以下代码:* { outline: none; }
  • nodes节点模块,使用title时,需自己手调样式,否则页面上不会展示。
    ::v-deep .vis-tooltip {position: absolute;visibility: visible;}
  • 可视化操作模块manipulation,在vis-network中使用会报错,目前不知道原因,可能是版本问题。在vis中可以使用,需要自己调样式,而且感觉这个可视化操作不怎么好用,用处不大。
  • 放大缩小按钮等需自己手调样式才会展示,都是拓扑图中自带的,只是因为样式问题没有显示出来。
  • 拓扑图分层级时,同层级从左往右的节点顺序:①当节点无x轴坐标时,与edges线条数组中的连接顺序有关。②当有x轴坐标时,以坐标值大小为准。

vis-network配置

配置有很多,直接写一个小案例放上配置吧。

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

<script>
const vis = require("vis-network/dist/vis-network.min.js");
export default {
  name: 'HelloWorld',
  props: {
    msg: String
  },
  data() {
    return {
      nodes: null,
      edges: null,
      options: null,
      network: null,
    }
  },
  created() {
    this.nodes = new vis.DataSet([  // nodes是节点
      {id: 1, label: 'Node 1',level: 1},
      {id: 2, label: 'Node 2',level: 2},
      {id: 8, label: 'Node 8',level: 2},
      {id: 9, label: 'Node 9',level: 2},
      {id: 10, label: 'Node 10',level: 2},
      {id: 3, label: 'Node 3',level: 2},
      {id: 4, label: 'Node 4',level: 3},
      {id: 5, label: 'Node 5',level: 3},
      {id: 6, label: 'Node 6',level: 4},
      {id: 7, label: 'Node 7',level: 5},
    ]);
    this.edges = new vis.DataSet([  // edges是线
      {from: 1, to: 2},  // 决定了节点从左往右的顺序
      {from: 1, to: 3},
      {from: 1, to: 8},
      {from: 1, to: 9},
      {from: 1, to: 10},
      {from: 2, to: 4},
      {from: 2, to: 5},
      {from: 5, to: 6},
      {from: 6, to: 7},
    ]);
  },
  mounted() {
    this.init()
  },
  methods: {
    init() {
      const container = this.$refs.mynetwork;
      const data = {
        nodes: this.nodes,
        edges: this.edges
      }
      this.options = {
        autoResize: true, // 默认true,自动调整容器的大小
        height: '100%', // 默认值
        width: '100%',  // 默认值
        locale: 'cn',   // 选择语言,默认英文en,cn为汉语
        locales: {      // 语言格式化对象
          cn: {
            edit: '编辑',
            del: '删除',
            back: '返回',
            addNode: '添加节点',
            addEdge: '添加连线',
            editNode: '编辑节点',
            editEdge: '编辑连线',
            addDescription: '点击空白区域添加节点',
            edgeDescription: 'Click on a node and drag the edge to another node to connect them.',
            editEdgeDescription: 'Click on the control points and drag them to a node to connect to it.',
            createEdgeError: 'Cannot link edges to a cluster.',
            deleteClusterError: 'Clusters cannot be deleted.',
            editClusterError: 'Clusters cannot be edited.',
          },
        },
        // 配置模块
        configure: {
          enabled: false, // false时不会在界面上出现各种配置项 
        },
        // 节点模块
        nodes: {
          chosen: true, // 对选择节点做出反应
          color: {
            border: '#2B7CE9',
            background: '#97C2FD',
            highlight: {
              border: '#2B7CE9',
              background: '#FFEC8B'
            },
            hover: {
              border: '#2B7CE9',
              background: '#FFC125'
            }
          },
          font: {
            align: 'left',
            color: '#FFC125',
            size: 12
            // vadjust: 10, // 标签文本的垂直位置,值越大离节点越远
          },
          labelHighlightBold: false,
          // hidden: true, // 为true不会显示节点。但仍是物理模拟的一部分
          shape: 'image',
          image: {    // 路径问题要注意,一定要存储在静态文件夹中
            unselected: '/static/images/icon_normal.svg',
            selected: '/static/images/icon_selected.svg',
          },
          size: 25, // 节点大小
          physics: false, // 关闭物理引擎
          title: '哈哈哈', // 用户悬停在节点上时显示的标题,可以是HTML元素或包含纯文本或HTML的字符串
          widthConstraint: { // 节点的最小宽度与最大宽度
            // maximum: 100,
          }
        },
        // 边模块
        edges: {
          // label: '哈哈哈',
          physics: false,
          smooth: {
            enabled: true,
            type: 'curvedCCW', // 平滑曲线的类型
            forceDirection: 'horizontal' // 用于分层布局的配置项,可选值有: ['horizontal', 'vertical', 'none']
          },
          // arrows: {  // 这里可以用来自定义箭头,type为image类型即可
            // middle: { enabled: true, type: 'image', imageHeight: 12, imageWidth: 12, src: getOpticalRed() }
          // },
        },
        // 交互模块
        interaction: {
          hover: true, // 启用鼠标悬停
          hoverConnectedEdges: false, // 鼠标悬停在节点上时,与其连接的边不高亮显示
          hideEdgesOnDrag: false, // true时拖动视图时不会绘制边。这会加快拖动时的响应速度
          hideNodesOnDrag: false, // true时拖动视图时不会绘制节点
          navigationButtons: true,
          selectConnectedEdges: false, // 选中节点时,与其连接的边不高亮
          multiselect: false, // true时长时间单击(或触摸)以及控件单击将添加到选择中
          tooltipDelay: 100,
        },
        // 可视化操作: 没起作用,不知道是不是版本的问题
        manipulation: {
          enabled: false,
          initiallyActive: true,
          addNode: true,
          addEdge: true,
          // editNode: undefined,
          editEdge: true,
          deleteNode: true,
          deleteEdge: true,
          controlNodeStyle:{
            // all node options are valid.
          }
        },
        // 物理引擎
        physics: {
          enabled: true,
          barnesHut: {
            gravitationalConstant: -20000, // 斥力
            springLength: 20,   // 弹簧长度
            avoidOverlap: 1,
          },
          maxVelocity: 50,
          minVelocity: 1,
          stabilization: {
            iterations: 500, // 最大迭代次数
            enabled: true,
            // fit: true,
            fit: false,   // 值为false时,点击刷新后可回到刷新前的页面
          },
        },
        // 布局
        layout: {
          randomSeed: 2000,
          hierarchical: {
            enabled: true,
            levelSeparation: 100, // 层级之间的距离,太小的话箭头会盖住标签字
            nodeSpacing: 100,     // 节点之间的距离
            treeSpacing: 100,     // 树之间的距离 
            sortMethod: 'directed',
          },
        },
      }
      this.network = new vis.Network(container, data, this.options);
      setTimeout(() => {
        this.nodes.update({id: 9, label: '更新后的9'})
      },10000)
    }
  }
}
</script>

<style scoped lang="less">
#mynetwork {
  width: 80%;
  height: 60%;
  border: none;
  ::v-deep .vis-navigation {
    position: absolute;
    z-index: 2;
    right: 12px;
    top: 47.8%;
    .vis-zoomIn,
    .vis-zoomOut {
      width: 24px;
      height: 24px;
      margin: 4px;
      cursor: pointer;
      border: 1px solid #c0c0c0;
      border-radius: 2px;
      border-radius: 2px;
      &:hover {
        border-color: #ccc;
        color: #4d4d4d;
      }
    }
  }
  ::v-deep .vis-tooltip {
    position: absolute;
    visibility: visible;
  }
}
</style>

开发时注意的功能点

1.性能问题

拓扑图性能问题很严重,官网上明确说了最多只支持几千个节点的同时展示,我试了一下,展示的节点很多时,页面操作非常卡顿。

所以在前期设计时要格外考虑性能问题,后面因为性能问题重写了好几版代码结构,都是泪。

2.是否有环路情况

我做的项目在定位时是不考虑环路的情况的,所以当时做了树状图和星状图是用的一套数据。后面听说加了要支持环路的需求,那之前的设计根本没法玩了,要全部推倒。我也不负责此拓扑项目了,不清楚后续如何设计的,所以在前端设计时也要考虑好自己项目的定位与应用场景。

3.位置保存还是状态保持

所谓的位置保存就是将每个节点的x与y轴值传给后端进行数据库保存,每次渲染时节点都会出现在相同的位置。

状态保持是不进行位置保存,只是在前端进行维护一份位置数据,保证在用户关闭浏览器之前,每个节点的相对顺序是保持不变的。

当时主要考虑到进行位置保存时,节点的首次渲染与新增节点渲染处理逻辑较为复杂,尤其在树状图模式下,位置没有任何意义了,就使用了状态保持的处理逻辑。

4.展开收缩

当使用树状图时,层级很深时很难分析拓扑状态,最好是可以增加展示收缩节点功能,可以进行缩放与展开层级,此功能很好用,也可以解决一下性能问题,使页面上展示的节点数不至于很多,进行性能优化。展开收缩的实现也不难,就是初始做一个判断,在需要进行展开收缩的节点上绘制一个展开收缩按钮,并加上自己封装的展开收缩功能。

5.分页器

有展开收缩功能,那当然少不了左右翻页的分页器了,实现逻辑和展开收缩类型,很好实现。

6.节点是否可拖动

当节点可拖动时,要考虑到分页器的绘制了,分页器要始终绘制在最左侧和最右侧的节点旁边的。
好在节点拖动时拓扑图是不断重绘的(就是canvas的重绘),可以封装一个公共函数进行分页器的绘制。

7.编辑拓扑关系

只有展示拓扑关系的功能显示是不够的,还要具有可编辑性,具体的编辑功能还要看如何进行设计的,需要什么样的场景。

总结

还有太多的点就暂时不说了,只是说了一些拓扑绘制实现思路,没有说实现的详细代码,有兴趣的可以一起讨论如何用代码进行实现,我感觉我编写的代码需要优化,不怎么好,还是经验不足。

  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值