我自己了解到的绘制拓扑图的开源库有vis和阿里的G6,G6在项目中没机会使用,只用到的vis,有时间再玩一下G6,G6好像比vis功能强大一些,下面主要说一下vis。
我接手拓扑项目时,项目中使用的是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 引入方式
配置有很多,直接写一个小案例放上配置吧。
<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>
拓扑图性能问题很严重,官网上明确说了最多只支持几千个节点的同时展示,我试了一下,展示的节点很多时,页面操作非常卡顿。
所以在前期设计时要格外考虑性能问题,后面因为性能问题重写了好几版代码结构,都是泪。
我做的项目在定位时是不考虑环路的情况的,所以当时做了树状图和星状图是用的一套数据。后面听说加了要支持环路的需求,那之前的设计根本没法玩了,要全部推倒。我也不负责此拓扑项目了,不清楚后续如何设计的,所以在前端设计时也要考虑好自己项目的定位与应用场景。
所谓的位置保存就是将每个节点的x与y轴值传给后端进行数据库保存,每次渲染时节点都会出现在相同的位置。
状态保持是不进行位置保存,只是在前端进行维护一份位置数据,保证在用户关闭浏览器之前,每个节点的相对顺序是保持不变的。
当时主要考虑到进行位置保存时,节点的首次渲染与新增节点渲染处理逻辑较为复杂,尤其在树状图模式下,位置没有任何意义了,就使用了状态保持的处理逻辑。
当使用树状图时,层级很深时很难分析拓扑状态,最好是可以增加展示收缩节点功能,可以进行缩放与展开层级,此功能很好用,也可以解决一下性能问题,使页面上展示的节点数不至于很多,进行性能优化。展开收缩的实现也不难,就是初始做一个判断,在需要进行展开收缩的节点上绘制一个展开收缩按钮,并加上自己封装的展开收缩功能。
有展开收缩功能,那当然少不了左右翻页的分页器了,实现逻辑和展开收缩类型,很好实现。
当节点可拖动时,要考虑到分页器的绘制了,分页器要始终绘制在最左侧和最右侧的节点旁边的。
好在节点拖动时拓扑图是不断重绘的(就是canvas的重绘),可以封装一个公共函数进行分页器的绘制。
只有展示拓扑关系的功能显示是不够的,还要具有可编辑性,具体的编辑功能还要看如何进行设计的,需要什么样的场景。
还有太多的点就暂时不说了,只是说了一些拓扑绘制实现思路,没有说实现的详细代码,有兴趣的可以一起讨论如何用代码进行实现,我感觉我编写的代码需要优化,不怎么好,还是经验不足。