目前github社区存在几款可用于设计流程图的绘图框架:
go.js( http://www.gojs.net/latest/index.html) :go.js 提供一整套的JS工具 ,支持各种交互式图表的创建;目前go.js 是闭源收费的
jsPlumb(https://jsplumbtoolkit.com/): jsPlumb是一套开源的流程图创建工具 ,小巧精悍,使用简单;jsPlumb 有社区版跟收费版,我们可使用的是社区版
JointJS(https://www.jointjs.com/):JointJS是一个开源的、基于JavaScript的图表库,可以用来创建静态图表、完全可交互的图表、 WEB在线流程图、应用程序
mxGraph(http://jgraph.github.io/mxgraph/):mxGraph是一个js绘图组件,适用在网页设计/编辑流程图、图表、网络图和普通图形的web应用程序,draw.io工具就是基于mxGraph开发的。缺点就是介绍框架简介以及API文档不全,社区问题也较少更新;
GG-Editor(https://g6.antv.vision/) :GG-Editor是基于 G6 和 React 的可视化图编辑器,G6 是一个图可视化引擎。它提供了图的绘制、布局、分析、交互、动画等图可视化的基础能力。
最终,我选择了jsPlumb,因为它完全开源,文档也较齐全,社区也比较活跃在开发过程中有问题也可以和其他开发者交流;mxGraph和JointJS也不错。大家可以根据自己的需要选择。后期也会着手调研mxGraph,因为基于它实现了功能非常全的draw.io,相信里面也会有非常多好的思路可以参考;
下面将对jsplumb进行介绍并结合案例进行使用
1.什么是jsplumb?
jsplumb是可以让你在网站上展示图表或者甚至在浏览器应用程序中使用图表的开发框架,该框架适用于必须绘制图表的Web应用程序,例如类似于Visio的应用程序或工作流程设计器等。由于图表项目和连接的所有参数都是非常精细可控的,因此你可以绘制你可以想到的任何类型的图表
2.jsplumb基础知识
官方文档地址:https://docs.jsplumbtoolkit.com/community/current/index.html
2.1 基本元素组成
Source: 源对象。jsPlumb 通过元素的 id 属性获取对象。
Target: 目标对象。jsPlumb 通过元素的 id 属性获取对象。Source 和 Target 都可以是任何元素,区别是,Source 是起点,Target 是终点。 例如,connector 中的箭头总是从 Source 指向 Target。
Anchor:锚点。是 Source 和 Target 对象上可以连接 Connector 的点。Anchor 并不是一个视觉概念,它是不可见的。
Connector: 连接线。
Endpoint: 端点。需要注意的是,箭头并不是一种端点样式,它是通过 overlay 添加的。
Overlay: 添加到连接线上的附件。例如箭头和标签。
3.开始使用:
3.1 安装
npm install jsplumb --save
3.2 准备节点元素
假设我们现在这里是一个 .vue 组件。首先我们创建一系列的元素作为 Source 和 Target。
<template>
<div id="wrapper">
<div class="line-wrap" style="margin-left: 70px;">
<div id="item-1" class="state-item">State 1</div>
<div id="item-2" class="state-item">State 2</div>
<div id="item-3" class="state-item">State 3</div>
</div>
<div class="line-wrap">
<div id="item-4" class="state-item">State 4</div>
<div id="item-5" class="state-item">State 5</div>
<div id="item-6" class="state-item">State 6</div>
<div id="item-7" class="state-item">State 7</div>
</div>
<div class="line-wrap" style="margin-left: 215px;">
<div id="item-8" class="state-item">State 8</div>
<div id="item-9" class="state-item">State 9</div>
</div>
</div>
</template>
然后我们为它写点 css:
<style >
#wrapper {
background:
radial-gradient(
ellipse at top left,
rgba(255, 255, 255, 1) 40%,
rgba(229, 229, 229, .9) 100%
);
height: 100vh;
padding: 60px 80px;
width: 100vw;
}
.state-item {
width: 80px;
height: 40px;
color: #606266;
background: #f6f6f6;
border: 2px solid rgba(0, 0, 0, 0.05);
text-align: center;
line-height: 40px;
font-family: sans-serif;
border-radius: 4px;
margin-right: 60px;
}
.line-wrap {
display: flex;
margin-bottom: 40px;
}
</style>
3.3 开始实例化jsPlumb
使用 jsPlumb.ready() 函数来初始化 jsPlumb。
<script>
import {jsPlumb} from 'jsplumb'
export default {
name: 'landing-page',
mounted () {
let plumbIns = jsPlumb.getInstance()
plumbIns.ready(function () {
plumbIns.connect({
// 对应上述基本概念
source: 'item-1',
target: 'item-2',
anchor: ['Left', 'Right', 'Top', 'Bottom', [0.3, 0, 0, -1], [0.7, 0, 0, -1], [0.3, 1, 0, 1], [0.7, 1, 0, 1]],
connector: ['StateMachine'],
endpoint: 'Blank',
overlays: [ ['Arrow', { width: 8, length: 8, location: 1 }] ], // overlay
// 添加样式
paintStyle: { stroke: '#909399', strokeWidth: 2 }, // connector
// endpointStyle: { fill: '#909399', outlineStroke: '#606266', outlineWidth: 1 } // endpoint
})
})
}
}
</script>
这样我们就在两个元素之间画了一条连接线。
注意:
1.我们应该把 jsPlumb.ready() 的代码写在 Vue 生命周期的 mounted 钩子中,而不是 created 中,因为 jsPlumb 是通过 DOM 元素来工作的,而在生命周期中的 created 阶段,Vue 实例还未挂载到页面元素上,这时候我们编写在组件中的元素是不存在的,因此 jsPlumb 无法起作用。
如果你的一些用于连线的元素放在子组件里,你还需要使用 vm.$nextTick() 来确保子组件也已经完成渲染,因为 mounted 并不能保证子组件完成渲染。
2.建议只使用 jsPlumb 的实例,而不是它本身,虽然在使用方法上二者是完全一样的。
在官方文档中,提到了这么一句话:
If you bind to the ready event after jsPlumb has already been initialized, your callback will be executed immediately.
也就是,如果 jsPlumb 已经被初始化了一次,那么 ready 函数中的代码会立即执行,而不是等待 ready 事件。
这会造成什么问题呢?我们知道,在 vue 的环境里,jsPlumb 是个全局变量,因为它直接引用自模块,而模块是不随着某个页面的消亡而消亡的。因此,当你重新路由进这个页面时,jsPlumb 就会认为你已经进行了初始化,因此不会等待 ready 事件,这就可能导致你的连线不能正常显示。
而如果采用 jsPlumb.getInstance() 创建独立的实例,这个实例就可以在页面摧毁时被回收,这样你下次路由进这个页面的时候,就不会发生异常了。总之,永远使用实例代替总没错。
4.jsPlumb配置项描述:
jsPlumb.ready() 是一个钩子函数,它会在 jsPlumb 准备完毕时执行。
连接线的建立是通过 jsPlumb.connect() 方法实现的。该方法接受一个对象作为配置项。其中包含了与上述概念一一对应的配置项,以及一些额外的样式。
source: 源对象,可以是对象的 id 属性、Element 对象或者 Endpoint 对象。
target: 目标对象,可以是对象的 id 属性、Element 对象或者 Endpoint 对象。
anchor: 是一个数组,数组中每一项定义一个锚点。
添加锚点有两种方式:通过预设字符串(Static Anchors)或者一个数组(Dinamic Anchors)。
Static Anchors:
Top (also aliased as TopCenter)
TopRight
Right (also aliased as RightMiddle)
BottomRight
Bottom (also aliased as BottomCenter)
BottomLeft
Left (also aliased as LeftMiddle)
TopLeft
Center
//定义了一个在底部中间的锚点位置
jsPlumb.connect({...., anchor:"Bottom", ... });
Dinamic Anchors:是一个表示 Anchor 的数组。[x, y, dx, dy]。
x-相对该锚点在x轴坐标比例(最大1)
y-相对该锚点y轴坐标比例(最大1)
dx-控制锚的方向
dy-同上
//定义了一个在底部中间的锚点位置
jsPlumb.connect({...., anchor:[ 0.5, 1, 0, 1 ], ... });
在上例中,我们定义了四个边的中点为 Anchors,然后还增加了上下边距离两端 0.3 的点作为 Anchors。
多边形锚:
Circle(圆)
Ellipse(椭圆)
Triangle(三角形)
Diamond(菱形)
Rectangle(矩形)
Square(正方形)
jsPlumb.addEndpoint("someElement", {
endpoint:"Dot",
anchor:[ "Perimeter", { shape:"Circle" } ]
});
如果锚点的宽高一样,该锚点位置为动态圆周。宽高不同为椭圆,类似正方形和矩形。
默认情况下,锚点个数为60,我们还可以手动指定:
eg(指定150个动态锚点):
jsPlumb.addEndpoint("someDiv", {
endpoint:"Dot",
anchor:[ "Perimeter", { shape:"Square", anchorCount:150 }]
});
默认定义: jsPlumb提供了一个动态锚 AutoDefault 选择从 前 , 右 , 底 和 左 :
jsPlumb.connect({...., anchor:"AutoDefault", ... });
connector: 连接线类型。此处我们选择了 StateMachine。类型的名称为 Connector 命名空间下的类名。可选值为:
Bezier
Flowchart
StateMachine
Straight
endpoint: 端点类型。此处我们选择了 Blank,也就是没有可见的端点。endpoint可选值为:
Blank
Dot
Image
Rectangle
overlays: 是一个数组,数组中的每一项定义一个 overlay。overlay 是附加于 connector 上的部件,例如箭头、标签等。每一个 overlay 定义使用一个数组,第一项为 overlay 的类型,第二项是一个对象,用来定义 overlay 的配置参数。不同类型的 overlay 其配置项也是不同的。这里以上例中的箭头为例:
overlays: [ ['Arrow', { width: 8, length: 8, location: 1 }] ],定义了一个 大小为 8 px,位于 connector 末端的箭头。可用的 overlay 类型有:
Arrow - 箭头
Label - 标签
PlainArrow - 平头箭头
Diamond - 菱形
Diamond(钻石):钻石箭头
Custom - 自定义
paintStyle: connector 样式(颜色和线宽)。
endpointStyle: endpoint 样式(填充颜色,边框颜色和边框宽度)。此处我使用的是 Blank 类型,不需要配置此参数,因此将其作为注释列在这里供大家参考。
默认配置:
我们在元素之间建立了一条连接线,事实上,我们常常需要画很多连接线,它们的样式是相同的,这时我们就可以使用 jsPlumb.connect() 的第二个参数。
第二个参数接收一个对象,作为 connect 的默认配置。它会自动附加到第一个参数的配置中去。
我们定义一个默认配置,然后为整个状态图建立对应的关系。
<script>
import {jsPlumb} from 'jsplumb'
export default {
name: 'landing-page',
mounted () {
let plumbIns = jsPlumb.getInstance()
let defaultConfig = {
// 对应上述基本概念
anchor: ['Left', 'Right', 'Top', 'Bottom', [0.3, 0, 0, -1], [0.7, 0, 0, -1], [0.3, 1, 0, 1], [0.7, 1, 0, 1]],
connector: ['StateMachine'],
endpoint: 'Blank',
// 添加样式
paintStyle: { stroke: '#909399', strokeWidth: 2 }, // connector
// endpointStyle: { fill: 'lightgray', outlineStroke: 'darkgray', outlineWidth: 2 } // endpoint
// 添加 overlay,如箭头
overlays: [ ['Arrow', { width: 8, length: 8, location: 1 }] ] // overlay
}
let relations = [
['item-4', 'item-1'],
['item-1', 'item-5'],
['item-5', 'item-2'],
['item-2', 'item-6'],
['item-6', 'item-3'],
['item-3', 'item-7'],
['item-7', 'item-9'],
['item-9', 'item-6'],
['item-6', 'item-8'],
['item-8', 'item-5'],
['item-3', 'item-9'],
['item-2', 'item-8'],
['item-1', 'item-4'],
['item-5', 'item-4']
]
plumbIns.ready(function () {
//在item-4节点上添加一个端点
let anEndpoint = plumbIns.addEndpoint('item-4', {
anchors: [ [0.7, 1, 0, 1] ],
endpoint: 'Blank'
})
relations.push(['item-8', anEndpoint])
for (let item of relations) {
plumbIns.connect({
source: item[0],
target: item[1]
}, defaultConfig)
}
})
}
}
</script>
在创建连接线时,jsPlumb 会从所有的 Anchors 中选择最合适的 Anchor 进行连接。
你也可以在 Source 或 Target 中直接指定 Endpoint 对象,来进行更精确的控制。
在上例中,我们通过 jsPlumb.addEndpoint() 创建了 Endpoint 对象。第一个参数是元素的 id 属性。第二个参数是此 Endpoint 的相关配置。完整的配置项请参考官方文档。
原本 State8 -> State4 的箭头是连接到 State4 右边的中点,
通过指定具体的 Endpoint,我们将它连接到了底部。
下面就让我们来练习一个可拽拽的拓扑图demo吧:
第一步:我们先设置好dom结构:
<template>
<div class="hello">
<h1 style="text-align:center;padding:50px">JsPlumb + D3js实现自定义节点,可拖拽节点,自动树状布局</h1>
<div id="relation-box">
<div
class="node"
v-for="item in nodeList"
:key="item.id"
:style="{left: item.left, top: item.top}"
:id="'node-'+ item.id"
>
{{ item.name }}
<div>detail...</div>
</div>
</div>
</div>
</template>
第二步:设置元素的css样式
<style scoped>
#relation-box {
position: relative;
}
.node {
position: absolute;
padding: 20px;
border: 1px solid #ccc;
border-radius: 20px;
text-align: center;
background-color: #f6f6f6;
}
</style>
第三步我们导入需要使用到的工具:
import { jsPlumb } from "jsplumb";
import * as D3 from "d3";
第四步:准备好需要的默认配置和数据
data () {
return {
jsPlumbInstance: "", //jsPlumb实例
// jsPlumb默认配置
jsPlumbSetting: {
// 动态锚点、位置自适应
Anchors: ['Top', 'TopCenter', 'TopRight', 'TopLeft', 'Right', 'RightMiddle', 'Bottom', 'BottomCenter', 'BottomRight', 'BottomLeft', 'Left', 'LeftMiddle'],
// 连线的样式 StateMachine、Flowchart,Bezier、Straight
Connector: ['Bezier', { curviness: 60 }],
// 鼠标是否拖动删除线
ConnectionsDetachable: false,
// 删除线的时候节点不删除
DeleteEndpointsOnDetach: false,
// 连线的两端端点类型:矩形 Rectangle;圆形Dot; eight: 矩形的高 ,idth: 矩形的宽
Endpoints: [['Dot', { radius: 2, }], ['Dot', { radius: 2 }]],
// 线端点的样式
EndpointStyle: { fill: 'skyblue', outlineWidth: 1 },
// 绘制连线
PaintStyle: {
stroke: '#000000',
strokeWidth: 1,
outlineStroke: 'transparent',
// 设定线外边的宽,单位px
outlineWidth: 10
},
// 绘制连线箭头
Overlays: [// 箭头叠加
['Arrow', {
width: 10, // 箭头尾部的宽度
length: 8, // 从箭头的尾部到头部的距离
location: 1, // 位置,建议使用0~1之间
direction: 1, // 方向,默认值为1(表示向前),可选-1(表示向后)
foldback: 0.623 // 折回,也就是尾翼的角度,默认0.623,当为1时,为正三角
}]
],
// 绘制图的模式 svg、canvas
RenderMode: 'svg',
DragOptions: { cursor: 'pointer', zIndex: 2000 },
// 鼠标滑过线的样式
HoverPaintStyle: { stroke: 'skyblue', strokeWidth: 3, cursor: 'pointer' },
},
// 连线的配置
jsPlumbConnectOptions: {
isSource: true,
isTarget: true,
// 动态锚点、提供了4个方向 Continuous、AutoDefault
anchor: "Continuous",
overlays: [['Arrow', { width: 8, length: 8, location: 1 }]] // overlay
},
commonLink: {
isSource: true,
isTarget: true,
anchor: ["Perimeter", { shape: "Circle" }],
connector: ['Bezier'],
endpoint: 'Dot',
// 不限制节点的连线数量
maxConnections: -1,
},
dataList: {
id: 1,
name: "中国",
children: [
{
id: 2,
name: "北京",
children: [
{
id: 6,
name: "海淀区"
},
{
id: 7,
name: "高新区"
}
]
},
{
id: 3,
name: "贵州",
children: [
{
id: 4,
name: "贵阳"
},
{
id: 5,
name: "黔西南"
},
{
id: 8,
name: "黔东南"
}
]
}
]
},
nodeList: [],
lineList: []
}
}
第五步:定义设置节点和设置连线的函数:
setNodeInfo (tree) {
// 参考D3API,这里会生成树形数据结构
const data = D3.hierarchy(tree);
// 使用D3 Tree自动布局, nodeSize控制节点x方向和y方向上的距离
const treeGenerator = D3.tree().nodeSize([200, 180]);
const treeData = treeGenerator(data);
// 获取自动布局后的节点信息
const nodes = treeData.descendants();
// 获取父子关系列表
const links = treeData.links();
// 设置节点位置信息
this.nodeList = nodes.map(item => {
return {
id: item.data.id,
name: item.data.name,
left: item.x + 900 + "px", // 900为初始X方向起点位置,默认为0
top: item.y + "px"
};
});
this.lineList = links.map(item => {
return {
source: `node-${item.source.data.id}`,
target: `node-${item.target.data.id}`,
overlays: [["Arrow", { width: 10, length: 10, location: 0.5 }]]
}
})
},
drawLines () {
this.$nextTick().then(() => {
jsPlumb.ready(() => {
// 创建jsPlumb实例
this.jsPlumbInstance = jsPlumb.getInstance();
// 导入准备好的jsPlumb配置
this.jsPlumbInstance.importDefaults(this.jsPlumbSetting);
// 开始节点间的连线
this.lineList.forEach((item) => {
this.jsPlumbInstance.connect(item, this.jsPlumbConnectOptions);
});
// 让每个节点都可以被拖拽
this.nodeList.forEach((item, index) => {
this.jsPlumbInstance.draggable("node-" + (index + 1))
})
// 给每个节点添加锚点
this.jsPlumbInstance.addEndpoint("node-" + (index + 1), {
anchor: ['Bottom', 'Top', 'Left', 'Right'],
Overlays: [
['Arrow', { width: 10, length: 8, location: 1, direction: 1, foldback: 0.623 }]]
}, this.commonLink)
})
this.jsPlumbInstance.repaintEverything(); // 重绘
})
}
第六步:在mounted生命周期中执行定义的函数:
mounted () { this.setNodeInfo(this.dataList); this.drawLines(); }
当我们执行setNodeInfo方法后
接着执行drawLines方法后:
this.jsPlumbInstance.draggable("node-" + (index + 1)) 该方法可以让每个节点都可被拖拽:
this.jsPlumbInstance.addEndpoint 该方法可以让每个节点都添加一个锚点,该锚点可以存在在四个方向,连线成功后jsPlumb会根据节点的位置动态变化锚点的位置
总结:
1.我们在使用时该注意我们需要将节点的容器设置相对定位,并且给节点设置绝对定位,因为jsPlumb是通过定位的方式来实现节点在容器中拖拽的
2.我们使用D3仅是让节点更好地快速地排布,我们也可以将节点的位置信息保存在dataList中,然后再设置每个节点的初始位置