- 啟動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>