[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

relation.gif
  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>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容