近期工作中接触到vis.js,使用了他的network功能,感觉与d3有点儿像,但是操作起来要比d3方便,以下是我的使用随笔。
vis.js 下载地址(https://almende.github.io/vis),官网上的API是纯英文的,可能对我这种英文一般的人来说使用起来就有点差强人意,无意间在别人的博文中看到一篇译文(https://blog.csdn.net/ipinki1218/article/details/83651961)
本文主要讲一下vis.js如何实现右键操作,实际效果图如下所示
前期准备
我前台用的是vue + element-UI
1.下载vis.js
2.下载vue.js
3.下载elment-ui插件
4.下载lodash.min.js
HTML页面
首先新建一个topo.html页面,引入vis.js,vue.js等
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../css/font-awesome.min.css">
<link rel="stylesheet" href="../element-ui/v2.10.1/theme-chalk/index.css">
<link href="../visjs/vis.min.css" rel="stylesheet" type="text/css" />
<style>
body {
margin: 0px;
}
#mynetwork {
min-height: 500px;
width: 100%;
}
.btn {
position: absolute;
top: 10px;
right: 10px;
}
.custom-menu {
position: absolute;
padding:5px 0px;
background-color: rgb(84, 92, 100);
top:100px;
left:100px;
z-index:9999;
display:none;
}
.custom-menu div {
padding:5px 0px;
text-align: center;
}
.custom-menu div a{
font-family:"微软雅黑";
font-size:14px;
color: rgb(255, 255, 255);
}
.custom-menu div a:HOVER, .custom-menu div a:ACTIVE{
cursor: pointer;
color: orange;
}
</style>
</head>
<body>
<div id="app" v-cloak>
<template>
<div class="custom-menu" style="width:80px">
<div><a @click="addNode">新增</a></div>
<div><a @click="updateNode">修改</a></div>
<div><a @click="deleteNode">删除</a></div>
</div>
<div id="mynetwork"></div>
<div class="btn">
<el-button type="success" icon="el-icon-refresh" circle @click="reloadNodeData" title="刷新"></el-button>
</div>
</template>
</div>
</body>
<script src="../jquery.min.js"></script>
<script src="../lodash.min.js"></script>
<script src="../vuejs/v2.6.10/vue.min.js"></script>
<script src="../element-ui/v2.10.1/index.js"></script>
<script src="../visjs/vis.min.js"></script>
<script src="../topo.js?_${.now?long}"></script>
</html>
JS代码
新建topo.js,代码如下
var network;
var vm = new Vue({
el : '#app',
data : {
nodes : new vis.DataSet(),
edges : new vis.DataSet(),
currNodeId : null // 当前操作的节点(鼠标右键操作用)
},
methods : {
//加载网络拓扑数据,首先从缓存中获取数据,如果没有则重新加载数据
loadNetworkData : function(){
var _this = this;
$.getJSON('/topo/data/load', {
success: function(r){
if(r && r.code == 0 && r.data && r.data.nodes && r.data.nodes.length>0 && r.data.edges && r.data.edges.length > 0){
r.data.nodes.forEach(function(node){
_this.nodes.add(node);
});
r.data.edges.forEach(function(edge){
_this.edges.add(edge);
});
_this.initNetwork();
}else{
_this.reloadNodeData();
}
}
});
},
/**
* 构建数据
*/
buildData: function(){
var rootId = _.uniqueId();
var root = {
id: rootId,
label: '根节点',
title: '根节点',
group: 'root',
};
_this.nodes.add(root);
for (var i=0; i<10; i++){
var _id = _.uniqueId();
var name = 'node'+i;
var node = {
id: _id,
label: name,
title: name,
group: 'group',
};
_this.nodes.add(node);
_this.edges.add({
from : rootId,
to : _id
});
}
},
//重新加载所有节点
reloadNodeData : function(){
this.nodes.clear();
this.edges.clear();
if (!!network) {
network.destroy();
network = null;
}
this.buildData();
this.initNetwork();
},
//初始化网络拓扑
initNetwork : function() {
var _this = this;
var container = document.getElementById('mynetwork');
var data = {
'nodes' : this.nodes,
'edges' : this.edges
};
var options = {
locale: 'cn',
physics: {
stabilization: false,
barnesHut: {
//gravitationalConstant: -3000,//(默认值 : -2000)引力:值越大节点越集中,反之值越小节点越离散
springConstant: 0.01,//(default: 0.04)弹簧:值越大弹性越强
//springLength: 50//(default: 95)弹簧长度
},
minVelocity: 5 //(default: 1)一旦达到所有节点的最小速度,我们假设网络已经稳定,布局停止。
},
interaction : {
navigationButtons : true,
keyboard : true
},
// 自定义节点样式
groups : {
root : {
shape : 'icon',
icon : {
face : 'FontAwesome',
code : '\uf015',
size : 50,
color : '#0066FF'
}
},
group : {
shape : 'icon',
icon : {
face : 'FontAwesome',
code : '\uf0c0',
size : 50,
color : '#00CCFF'
}
}
}
};
network = new vis.Network(container, data, options);
var loading;
// 稳定启动时触发
network.on("startStabilizing", function (params) {
loading = _this.$loading({
lock: true,
text: '数据加载中',
background: 'rgba(0, 0, 0, 0.8)'
});
});
//在Network稳定或调用stopSimulation()时触发
network.on("stabilized", function (params) {
loading.close();
network.fit();
saveNetwork();
});
// 双击鼠标触发
network.on("doubleClick", function (params) {
// 双击时params.nodes.length不为0,拖动时该事件也会触发,但length=0
if (params.nodes.length>0){
var node = _this.nodes._data[params.nodes[0]];
// 这里可以实现点击加载节点
}
});
// 单击鼠标触发
network.on("click", function (params) {
var nodeId = this.getNodeAt(params.pointer.DOM);
if(nodeId){
var options = {
scale: 1.0,
offset: {x:0,y:0},
animation: {
duration: 1000,
easingFunction: 'easeInOutQuad'
}
};
network.focus(nodeId, options); //定位交点
}
if (!$(".custom-menu").is(':hidden')){
$(".custom-menu").hide();
}
});
// 单击鼠标右键触发
network.on("oncontext", function (params) {
var nodeId = this.getNodeAt(params.pointer.DOM);
if (nodeId){
params.event.preventDefault();
$(".custom-menu").finish().toggle(100);
$(".custom-menu").css({
top: params.pointer.DOM.y + "px",
left: params.pointer.DOM.x + "px"
});
_this.currNodeId = nodeId;
}
});
var saveNetwork = function(){
var positions = network.getPositions();
var data = {
'nodes': _this.nodes.get().map(function(node){
return _.extend({}, node, positions[node.id]);
}),
'edges': _this.edges.get()
};
$.post('/topo/data/save', data, {
success: function(r){
}
});
};
},
addNode: function(){
// 添加节点
var _pid = _this.currNodeId;
var _id = _.uniqueId(node.id + "_");
// 根据节点的children数,计算_id
var children = _this.edges.get({
filter: function (item) {
return item.from == _pid;
},
fields:["to"]
});
if (children){
_.sortBy(children, function(a, b){
return b.repalce(/_/g, "")-a.repalce(/_/g, "");
});
_id = parseInt(children[0].substring(_pid.length+1))+ 1;
}
var label = node.label+_id.substring(_pid.length);
_this.nodes.add({
id: _id,
label: label,
group: 'group',
title: label,
});
_this.edges.add({
from : _pid,
to : _id
});
},
updateNode: function(){
// 修改节点
var node = _this.nodes.get(_this.currNodeId);
node.title = "我正在执行修改操作";
_this.nodes.update(node);
},
deleteNode: function(){
var _this = this;
if (_this.currNodeId){
var node = _this.nodes.get(_this.currNodeId);
_this.nodes.remove(_this.currNodeId);
// 删除所有连线
var edgeIds = _this.edges.get({
filter: function (item) {
return item.to == _this.currNodeId;
},
fields:["id"]
});
_this.edges.remove(edgeIds);
_this.currNodeId = null;
}
// 删除节点之后隐藏操作菜单
if (!$(".custom-menu").is(':hidden')){
$(".custom-menu").hide();
}
}
},
mounted : function() {
//页面初始入口
$('#mynetwork').height($(top).innerHeight()-100);
if (window.ActiveXObject || "ActiveXObject" in window){
//为IE浏览器设置宽度
$('#mynetwork').width($(top).innerWidth());
}
this.loadNetworkData();
}
});
JAVA代码
创建TopoController.java,将加载完成的拓扑结构保存到缓存里,再次加载时可以直接从缓存获取数据及结构(位置会和上次查询完全一样),这样做的好处有点儿类似快照,保留上次浏览结果,也可提升再次访问的加载速度。
@Controller
@RequestMapping("/topo")
public class AssetTopoController {
@Autowired
private RedisUtils redisUtils;
/**
* 保存拓扑数据到redis中
* @param data
* @return
*/
@PostMapping("/data/save")
@ResponseBody
public void save(@RequestBody Map<String,Object> data) {
redisUtils.set("my-network", data, 60*60); //设置过期时长为1小时
}
/**
* 从redis中获取拓扑数据
* @return
*/
@GetMapping("/data/load")
@ResponseBody
public Map<String, Object> load() {
Object data = redisUtils.get("my-network");
Map<String, Object> map = new HashMap<String, Object>();
map.put("data", JSONUtil.parseObj(data));
return map;
}
}
实现右键菜单只需要两步就可以,第一找到vis.js network的右键事件,第二自定义一个菜单(或者div),在某个节点触发右键事件时定位到该节点上
第一步 找到vis.js network 的右键事件,获取当前节点的位置,用于确定右键菜单的位置
第二步 定义右键div 确定定位通过CSS样式来控制
这样就实现鼠标右键功能了。
另外有一点需要需注意,vis.js有自带的右键事件,使用时需要先关闭他自身的事件(关闭代码params.event.preventDefault()),然后再打开自定义菜单(显示自定义custom div),通过this.getNodeAt(params.pointer.DOM); 来获取当前选中节点。实现的过程中,我发现不管鼠标放在那里都可以调用右键事件,但是this.getNodeAt(params.pointer.DOM)只有放在节点上时才不会为空,所以可以通过this.getNodeAt(params.pointer.DOM)的返回值来判断当前鼠标的位置。我的做法是当鼠标选中某个节点时,触发鼠标右键弹出自定义菜单,离开选中节点单击鼠标关闭菜单,未选中节点时继续显示默认右键功能。
分享几篇别人的博文:
vis.js介绍
https://blog.csdn.net/DCX_abc/article/details/78143635
vis.js 小记
https://blog.csdn.net/qq_39759115/article/details/78594831
Vis.js–Network中文教程
https://blog.csdn.net/ipinki1218/article/details/83651961