- 启动vue项目,执行以下命令安装dagre、graphlib、jointjs、svg-pan-zoom。
npm install dagre graphlib jointjs svg-pan-zoom --save
- 新建vue文件,为svg准备父节点,以及部分初始化数据。
<template>
<div class="container">
<div id="paper"></div>
</div>
</template>
<script>
import dagre from "dagre";
import graphlib from "graphlib";
import * as joint from "jointjs";
import '/node_modules/jointjs/dist/joint.css';
import svgPanZoom from 'svg-pan-zoom';
export default {
data(){
return {
graph: null,
paper: null,
/** 原始数据:节点 */
nodes: [
{ id: 1, label: 'node1' },
{ id: 2, label: 'node2' },
{ id: 3, label: 'node3' },
],
/** 原始数据:连线 */
links: [
{ from: 1, to: 2 },
{ from: 1, to: 3 }
],
/** 处理后生成的节点 */
nodeList: [],
/** 处理后生成的连线 */
linkList: []
}
},
methods: {
/** 页面初始化 */
init(){
/** 此处是下面依次写的几个函数执行 */
}
},
mounted(){
this.init();
}
</script>
- 初始化画布,完成画布的初始化。
/** 初始化画布,按照joint文档来就可以,具体画布的尺寸和颜色自定义 */
initGraph() {
let paper = document.getElementById('paper');
this.graph = new joint.dia.Graph();
this.paper = new joint.dia.Paper({
dagre: dagre,
graphlib: graphlib,
el: paper,
model: this.graph,
width: '100%',
height: 'calc(100vh - 100px)',
background: {
color: '#f5f5f5'
},
/** 是否需要显示单元格以及单元格大小(px) */
// drawGrid: true,
// gridSize: 20,
});
}
将initGraph方法放入init执行后,页面上应当出现了一个浅灰色的画布( ̄▽ ̄)~*。
- 创建完画布之后,在画布上绘制节点。
/** 创建节点 */
createNode(){
/** 遍历节点原始数据,通过joint.shapes.standard.Rectangle(joint内置shape)创建节点对象。 */
this.nodes.forEach(ele => {
let node = new joint.shapes.standard.Rectangle({
id: ele.id,
size: {
width: 100,
height: 50
},
attrs: {
body: {
fill: '#ddd',
stroke: 'none'
},
text: {
text: ele.label
}
}
});
/** 创建的节点对象放入list */
this.nodeList.push(node);
})
/** 通过graph的addCell方法向画布批量添加一个list */
this.graph.addCell(this.nodeList);
}
执行完createNode方法,页面上出现了3个矩形覆盖在一起,可以拖动,是吧ಠᴗಠ。
- 把之前几个叠罗汉的矩形通过线连接起来。
遍历links列表,通过joint.shapes.standard.Link创建节点间的连接关系。
/** 创建连线 */
createLink(){
this.links.forEach(ele => {
let link = new joint.shapes.standard.Link({
source: {
id: ele.from
},
target: {
id: ele.to
},
attrs: {
line: {
stroke: '#aaa',
strokeWidth: 1
}
}
});
/** 创建好的连线push进数组 */
this.linkList.push(link);
})
/** 通过graph.addCell向画布批量添加连线 */
this.graph.addCell(this.linkList);
}
发现执行完之后,页面上节点和连线都缩在一起聊天了(ㅍ_ㅍ)。。。可以拖动分散开,会看到隐藏的连线,不要慌,下面给它布个局分散开就行了~~
注意:必须先创建节点再创建连线,连线的数据可以看出是跟节点息息相关的,没有节点,也就没有从节点a指向节点b这条连线了,页面上会出现找不到节点的报错。
- 节点是可以通过position属性指定渲染位置的,例如: position: { x: 100, y: 200 }。连线是根据节点的位置计算来的。
但是一般得到的数据大概率不会给你每个节点的具体坐标,所以自动布局是很有必要的。布局算法其实是dagre实现的,要是有兴趣可以去查查昂。
/** 画布节点自动布局,通过joint.layout.DirectedGraph.layout实现 */
randomLayout(){
joint.layout.DirectedGraph.layout(this.graph, {
dagre: dagre,
graphlib: graphlib,
/** 布局方向 TB | BT | LR | RL */
rankDir: "LR",
/** 表示列之间间隔的像素数 */
rankSep: 200,
/** 相同列中相邻接点之间的间隔的像素数 */
nodeSep: 80,
/** 同一列中相临边之间间隔的像素数 */
edgeSep: 50
});
}
执行完后,关系图已经是我们想要的样子了。但是图的位置在左上角,并且整个画布不可拖动,不是很灵活。所以使用svg-pan-zoom来优化动作。
- svg-pan-zoom实现画布拖拽缩放等操作。
/** svgpanzoom 画布拖拽、缩放 */
svgPanZoom(){
/** 判断是否有节点需要渲染,否则svg-pan-zoom会报错。 */
if(this.nodes.length){
let svgZoom = svgPanZoom('#paper svg', {
/** 是否可拖拽 */
panEnabled: true,
/** 是否可缩放 */
zoomEnabled: true,
/** 双击放大 */
dblClickZoomEnabled: false,
/** 可缩小至的最小倍数 */
minZoom: 0.01,
/** 可放大至的最大倍数 */
maxZoom: 100,
/** 是否自适应画布尺寸 */
fit: true,
/** 图是否居中 */
center: true
})
/** 手动设置缩放敏感度 */
svgZoom.setZoomScaleSensitivity(0.5);
}
}
由于设置了fit:true导致图会自适应画布大小,节点少的话会导致图过分放大,如不需要自适应画布,可以设置为false。也可以在fit:true基础上天添加以下代码进行优化。
/** fit:true 元素数量较少时,会引起元素过度放大,当缩放率大于1时,将图像缩小为1;小于等于1时,为体现出边距更显美观,整体缩放至0.9 */
let {sx, sy} = this.paper.scale();
if(sx > 1){
svgZoom.zoom(1/sx);
} else {
svgZoom.zoom(0.9);
}
可以看到图已经非常靠近我们想要的样子了ヽ(゚∀゚)メ(゚∀゚)ノ ,但还是有美中不足的地方,在拖拽节点时,会发现连着画布一起移动了,并且节点还哆哆嗦嗦的,这明显不太行。
没有使用svg-pan-zoom时节点是可以单独拖拽的,使用了之后,svg-pan-zoom影响了jointjs的节点拖拽事件。也就是说svg-pan-zoom影响了jointjs的节点拖拽事件。
解决这种情况,只需要在svg-pan-zoom判断是否拖拽的节点,并不触发相应事件即可。
- svg-pan-zoom有beforePan方法的配置:
beforePan will be called with 2 attributes:
- oldPan
- newPan
Each of these objects has two attributes (x and y) representing current pan (on X and Y axes).
If beforePan will return false or an object {x: true, y: true} then panning will be halted. If you want to prevent panning only on one axis then return an object of type {x: true, y: false}. You can alter panning on X and Y axes by providing alternative values through return {x: 10, y: 20}.
可以看到在beforePan里返回false 或者 { x: true, y: true } 即可停止拖拽。
ps: 但是我试了{ x: true, y: true }不得行ヽ(ー_ー)ノ,但是{ x: false, y: false }是可以的。
- 首先确定当前拖拽的是节点, 为paper添加事件,判断当前点击并拖拽的是节点
/** 给paper添加事件 */ paperEvent(){ /** 确认点击的是节点 */ this.paper.on('element:pointerdown', (cellView, evt, x, y) => { this.currCell = cellView; }) /** 在鼠标抬起时恢复currCell为null */ this.paper.on('cell:pointerup blank:pointerup', (cellView, evt, x, y) => { this.currCell = null; }) }
- 同时在svgPanZoom的配置里增加以下属性:
/** 判断是否是节点的拖拽 */ beforePan: (oldPan, newPan) => { if(this.currCell){ return false; } }
现在这个效果是我们想要的了,d=====( ̄▽ ̄*)b
- 整个页面完整代码如下:
<template>
<div class="container">
<div id="paper"></div>
</div>
</template>
<script>
import dagre from "dagre";
import graphlib from "graphlib";
import * as joint from "jointjs";
import '/node_modules/jointjs/dist/joint.css';
import svgPanZoom from 'svg-pan-zoom';
export default {
data(){
return {
graph: null,
paper: null,
/** 原始数据:节点 */
nodes: [
{ id: 1, label: 'node1' },
{ id: 2, label: 'node2' },
{ id: 3, label: 'node3' }
],
/** 原始数据:连线 */
links: [
{ from: 1, to: 2 },
{ from: 1, to: 3 }
],
/** 处理后生成的节点 */
nodeList: [],
/** 处理后生成的连线 */
linkList: [],
/** 当前单元格,joint的拖动和svgpanzoom会冲突造成抖动 */
currCell: null,
}
},
methods: {
init(){
this.initGraph();
this.createNode();
this.createLink();
this.randomLayout();
this.svgPanZoom();
this.paperEvent();
},
/** 初始化画布 */
initGraph() {
this.nodeList = [];
this.linkList = [];
let paper = document.getElementById('paper');
this.graph = new joint.dia.Graph();
this.paper = new joint.dia.Paper({
dagre: dagre,
graphlib: graphlib,
el: paper,
model: this.graph,
width: '100%',
height: 'calc(100vh - 100px)',
background: {
color: '#f5f5f5'
},
// drawGrid: true,
// gridSize: 20,
});
},
/** 创建节点 */
createNode(){
this.nodes.forEach(ele => {
let node = new joint.shapes.standard.Rectangle({
id: ele.id,
size: {
width: 100,
height: 50
},
attrs: {
body: {
fill: '#ddd',
stroke: 'none'
},
text: {
text: ele.label
}
}
});
this.nodeList.push(node);
})
this.graph.addCell(this.nodeList);
},
/** 创建连线 */
createLink(){
this.links.forEach(ele => {
let link = new joint.shapes.standard.Link({
source: {
id: ele.from
},
target: {
id: ele.to
},
attrs: {
line: {
stroke: '#aaa',
strokeWidth: 1
}
}
});
this.linkList.push(link);
})
this.graph.addCell(this.linkList);
},
/** 画布节点自动布局 */
randomLayout(){
joint.layout.DirectedGraph.layout(this.graph, {
dagre: dagre,
graphlib: graphlib,
/** 布局方向 TB | BT | LR | RL */
rankDir: "LR",
/** 表示列之间间隔的像素数 */
rankSep: 200,
/** 相同列中相邻接点之间的间隔的像素数 */
nodeSep: 80,
/** 同一列中相临边之间间隔的像素数 */
edgeSep: 50
});
},
/** svgpanzoom 画布拖拽、缩放 */
svgPanZoom(){
if(this.nodes.length){
let svgZoom = svgPanZoom('#paper svg', {
/** 是否可拖拽 */
panEnabled: true,
/** 是否可缩放 */
zoomEnabled: true,
/** 双击放大 */
dblClickZoomEnabled: false,
/** 可缩小至的最小倍数 */
minZoom: 0.01,
/** 可放大至的最大倍数 */
maxZoom: 100,
/** 是否自适应画布尺寸 */
fit: true,
/** 图是否居中 */
center: true,
/** 判断是否是节点的拖拽 */
beforePan: (oldPan, newPan) => {
if(this.currCell){
return false;
}
}
})
svgZoom.setZoomScaleSensitivity(0.5);
/** fit:true 元素数量较少时,会引起元素过度放大,当缩放率大于1时,将图像缩小为1;小于等于1时,为体现出边距更显美观,整体缩放至0.9 */
let {sx, sy} = this.paper.scale();
if(sx > 1){
svgZoom.zoom(1/sx);
} else {
svgZoom.zoom(0.9);
}
}
},
paperEvent(){
this.paper.on('element:pointerdown', (cellView, evt, x, y) => {
this.currCell = cellView;
})
this.paper.on('cell:pointerup blank:pointerup', (cellView, evt, x, y) => {
this.currCell = null;
})
},
},
mounted(){
this.init();
}
}
</script>