[vue2 + jointjs + svg-pan-zoom] 節點自動布局渲染 + 拖拽縮放


  1. 啟動vue項目,執行以下命令安裝dagre、graphlib、jointjs、svg-pan-zoom。
  npm install dagre graphlib jointjs svg-pan-zoom --save
  1. 新建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>
  1. 初始化畫布,完成畫布的初始化。
/** 初始化畫布,按照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執行后,頁面上應當出現了一個淺灰色的畫布( ̄▽ ̄)~*。

  1. 創建完畫布之后,在畫布上繪制節點。
/** 創建節點 */
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個矩形覆蓋在一起,可以拖動,是吧ಠᴗಠ。

  1. 把之前幾個疊羅漢的矩形通過線連接起來。
    遍歷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這條連線了,頁面上會出現找不到節點的報錯。

  1. 節點是可以通過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來優化動作。

  1. 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判斷是否拖拽的節點,並不觸發相應事件即可。

  1. 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

  1. 整個頁面完整代碼如下:
<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>


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM