目录
- JointJS:JavaScript 流程图绘制框架
- JointJS 简介
- JointJS Hello world
- 前后端分离架构
- 其他
- 自动布局 Automatic layout
- 使用 HTML 定制元素
JointJS:JavaScript 流程图绘制框架
最近调研了js画流程图的框架,最后选择了Joint。配合上 dagre 可以画出像模像样的流程图。
JointJS 简介
JointJS 是一个开源前端框架,支持绘制各种各样的流程图、工作流图等。Rappid 是 Joint 的商业版,提供了一些更强的插件。JointJS 的特点有下面几条,摘自官网:
- 能够实时地渲染上百(或者上千)个元素和连接
- 支持多种形状(矩形、圆、文本、图像、路径等)
- 高度事件驱动,用户可自定义任何发生在 paper 下的事件响应
- 元素间连接简单
- 可定制的连接和关系图
- 连接平滑(基于贝塞尔插值 bezier interpolation)& 智能路径选择
- 基于 SVG 的可定制、可编程的图形渲染
- NodeJS 支持
- 通过 JSON 进行序列化和反序列化
总之 JoingJS 是一款很强的流程图制作框架,开源版本已经足够日常使用了。
一些常用地址:
API: https://resources.jointjs.com/docs/jointjs/v1.1/joint.html
Tutorials: https://resources.jointjs.com/tutorial
JointJS Hello world
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/jointjs/2.1.0/joint.css" />
</head>
<body>
<!-- content -->
<div id="myholder"></div>
<!-- dependencies 通过CDN加载依赖-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.3.3/backbone.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jointjs/2.1.0/joint.js"></script>
<!-- code -->
<script type="text/javascript">
var graph = new joint.dia.Graph;
var paper = new joint.dia.Paper({
el: document.getElementById('myholder'),
model: graph,
width: 600,
height: 100,
gridSize: 1
});
var rect = new joint.shapes.standard.Rectangle();
rect.position(100, 30);
rect.resize(100, 40);
rect.attr({
body: {
fill: 'blue'
},
label: {
text: 'Hello',
fill: 'white'
}
});
rect.addTo(graph);
var rect2 = rect.clone();
rect2.translate(300, 0);
rect2.attr('label/text', 'World!');
rect2.addTo(graph);
var link = new joint.shapes.standard.Link();
link.source(rect);
link.target(rect2);
link.addTo(graph);
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/jointjs/2.1.0/joint.css" />
</head>
<body>
<!-- content -->
<div id="myholder"></div>
<!-- dependencies 通过CDN加载依赖-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.3.3/backbone.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jointjs/2.1.0/joint.js"></script>
<!-- code -->
<script type="text/javascript">
var graph = new joint.dia.Graph;
var paper = new joint.dia.Paper({
el: document.getElementById('myholder'),
model: graph,
width: 600,
height: 100,
gridSize: 1
});
var rect = new joint.shapes.standard.Rectangle();
rect.position(100, 30);
rect.resize(100, 40);
rect.attr({
body: {
fill: 'blue'
},
label: {
text: 'Hello',
fill: 'white'
}
});
rect.addTo(graph);
var rect2 = rect.clone();
rect2.translate(300, 0);
rect2.attr('label/text', 'World!');
rect2.addTo(graph);
var link = new joint.shapes.standard.Link();
link.source(rect);
link.target(rect2);
link.addTo(graph);
</script>
</body>
</html>
hello world 代码没什么好说的。要注意这里的图形并没有自动排版,而是通过移动第二个 rect 实现的手动排版。
前后端分离架构
既然支持 NodeJs,那就可以把繁重的图形绘制任务交给服务器,再通过 JSON 序列化在 HTTP 上传输对象,这样减轻客户端的压力。
NodeJS 后端
var express = require('express');
var joint = require('jointjs');
var app = express();
function get_graph(){
var graph = new joint.dia.Graph();
var rect = new joint.shapes.standard.Rectangle();
rect.position(100, 30);
rect.resize(100, 40);
rect.attr({
body: {
fill: 'blue'
},
label: {
text: 'Hello',
fill: 'white'
}
});
rect.addTo(graph);
var rect2 = rect.clone();
rect2.translate(300, 0);
rect2.attr('label/text', 'World!');
rect2.addTo(graph);
var link = new joint.shapes.standard.Link();
link.source(rect);
link.target(rect2);
link.addTo(graph);
return graph.toJSON();
}
app.all('*', function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "X-Requested-With");
res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
next();
});
app.get('/graph', function(req, res){
console.log('[+] send graph json to client')
res.send(get_graph());
});
app.listen(8071);
var express = require('express');
var joint = require('jointjs');
var app = express();
function get_graph(){
var graph = new joint.dia.Graph();
var rect = new joint.shapes.standard.Rectangle();
rect.position(100, 30);
rect.resize(100, 40);
rect.attr({
body: {
fill: 'blue'
},
label: {
text: 'Hello',
fill: 'white'
}
});
rect.addTo(graph);
var rect2 = rect.clone();
rect2.translate(300, 0);
rect2.attr('label/text', 'World!');
rect2.addTo(graph);
var link = new joint.shapes.standard.Link();
link.source(rect);
link.target(rect2);
link.addTo(graph);
return graph.toJSON();
}
app.all('*', function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "X-Requested-With");
res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
next();
});
app.get('/graph', function(req, res){
console.log('[+] send graph json to client')
res.send(get_graph());
});
app.listen(8071);
HTML 前端
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/jointjs/2.1.0/joint.css" />
</head>
<body>
<!-- content -->
<div id="myholder"></div>
<!-- dependencies 通过CDN加载依赖-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.3.3/backbone.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jointjs/2.1.0/joint.js"></script>
<!-- code -->
<script type="text/javascript">
var graph = new joint.dia.Graph;
var paper = new joint.dia.Paper({
el: document.getElementById('myholder'),
model: graph,
width: 600,
height: 100,
gridSize: 1
});
$.get('http://192.168.237.128:8071/graph', function(data, statue){
graph.fromJSON(data);
});
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/jointjs/2.1.0/joint.css" />
</head>
<body>
<!-- content -->
<div id="myholder"></div>
<!-- dependencies 通过CDN加载依赖-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.3.3/backbone.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jointjs/2.1.0/joint.js"></script>
<!-- code -->
<script type="text/javascript">
var graph = new joint.dia.Graph;
var paper = new joint.dia.Paper({
el: document.getElementById('myholder'),
model: graph,
width: 600,
height: 100,
gridSize: 1
});
$.get('http://192.168.237.128:8071/graph', function(data, statue){
graph.fromJSON(data);
});
</script>
</body>
</html>
其他
自动布局 Automatic layout
JointJS 内置了插件进行自动排版,原理是调用 Dagre 库。官方 api 中有样例。
使用方法:
var graphBBox = joint.layout.DirectedGraph.layout(graph, {
nodeSep: 50,
edgeSep: 80,
rankDir: "TB"
});
配置参数 | 注释 |
nodeSep | 相同rank的邻接节点的距离 |
edgeSep | 相同rank的邻接边的距离 |
rankSep | 不同 rank 元素之间的距离 |
rankDir | 布局方向 ( |
marginX | number of pixels to use as a margin around the left and right of the graph. |
marginY | number of pixels to use as a margin around the top and bottom of the graph. |
ranker | 排序算法。 Possible values: |
resizeClusters | set to |
clusterPadding | A gap between the parent element and the boundary of its embedded children. It could be a number or an object e.g. |
setPosition(element, position) | a function that will be used to set the position of elements at the end of the layout. This is useful if you don't want to use the default |
setVertices(link, vertices) | If set to |
setLabels(link, labelPosition, points) | If set to |
dagre | 默认情况下,dagre 应该在全局命名空间当中,不过你也可以当作参数传进去 |
graphlib | 默认情况下,graphlib 应该在全局命名空间当中,不过你也可以当作参数传进去 |
我们来试一下。NodeJS 后端
var express = require('express');
var joint = require('jointjs');
var dagre = require('dagre')
var graphlib = require('graphlib');
var app = express();
function get_graph(){
var graph = new joint.dia.Graph();
var rect = new joint.shapes.standard.Rectangle();
rect.position(100, 30);
rect.resize(100, 40);
rect.attr({
body: {
fill: 'blue'
},
label: {
text: 'Hello',
fill: 'white'
}
});
rect.addTo(graph);
var rect2 = rect.clone();
rect2.translate(300, 0);
rect2.attr('label/text', 'World!');
rect2.addTo(graph);
for(var i=0; i<10; i++){
var cir = new joint.shapes.standard.Circle();
cir.resize(100, 100);
cir.position(10, 10);
cir.attr('root/title', 'joint.shapes.standard.Circle');
cir.attr('label/text', 'Circle' + i);
cir.attr('body/fill', 'lightblue');
cir.addTo(graph);
var ln = new joint.shapes.standard.Link();
ln.source(cir);
ln.target(rect2);
ln.addTo(graph);
}
var link = new joint.shapes.standard.Link();
link.source(rect);
link.target(rect2);
link.addTo(graph);
//auto layout
joint.layout.DirectedGraph.layout(graph, {
nodeSep: 50,
edgeSep: 50,
rankDir: "TB",
dagre: dagre,
graphlib: graphlib
});
return graph.toJSON();
}
app.all('*', function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "X-Requested-With");
res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
next();
});
app.get('/graph', function(req, res){
console.log('[+] send graph json to client')
res.send(get_graph());
});
app.listen(8071);
var express = require('express');
var joint = require('jointjs');
var dagre = require('dagre')
var graphlib = require('graphlib');
var app = express();
function get_graph(){
var graph = new joint.dia.Graph();
var rect = new joint.shapes.standard.Rectangle();
rect.position(100, 30);
rect.resize(100, 40);
rect.attr({
body: {
fill: 'blue'
},
label: {
text: 'Hello',
fill: 'white'
}
});
rect.addTo(graph);
var rect2 = rect.clone();
rect2.translate(300, 0);
rect2.attr('label/text', 'World!');
rect2.addTo(graph);
for(var i=0; i<10; i++){
var cir = new joint.shapes.standard.Circle();
cir.resize(100, 100);
cir.position(10, 10);
cir.attr('root/title', 'joint.shapes.standard.Circle');
cir.attr('label/text', 'Circle' + i);
cir.attr('body/fill', 'lightblue');
cir.addTo(graph);
var ln = new joint.shapes.standard.Link();
ln.source(cir);
ln.target(rect2);
ln.addTo(graph);
}
var link = new joint.shapes.standard.Link();
link.source(rect);
link.target(rect2);
link.addTo(graph);
//auto layout
joint.layout.DirectedGraph.layout(graph, {
nodeSep: 50,
edgeSep: 50,
rankDir: "TB",
dagre: dagre,
graphlib: graphlib
});
return graph.toJSON();
}
app.all('*', function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "X-Requested-With");
res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
next();
});
app.get('/graph', function(req, res){
console.log('[+] send graph json to client')
res.send(get_graph());
});
app.listen(8071);
HTML 前端
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/jointjs/2.1.0/joint.css" />
</head>
<body>
<!-- content -->
<div id="myholder"></div>
<!-- dependencies 通过CDN加载依赖-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.3.3/backbone.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jointjs/2.1.0/joint.js"></script>
<!-- code -->
<script type="text/javascript">
var graph = new joint.dia.Graph;
var paper = new joint.dia.Paper({
el: document.getElementById('myholder'),
model: graph,
width: 2000,
height: 2000,
gridSize: 1
});
$.get('http://192.168.237.128:8071/graph', function(data, statue){
graph.fromJSON(data);
});
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/jointjs/2.1.0/joint.css" />
</head>
<body>
<!-- content -->
<div id="myholder"></div>
<!-- dependencies 通过CDN加载依赖-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.3.3/backbone.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jointjs/2.1.0/joint.js"></script>
<!-- code -->
<script type="text/javascript">
var graph = new joint.dia.Graph;
var paper = new joint.dia.Paper({
el: document.getElementById('myholder'),
model: graph,
width: 2000,
height: 2000,
gridSize: 1
});
$.get('http://192.168.237.128:8071/graph', function(data, statue){
graph.fromJSON(data);
});
</script>
</body>
</html>
结果:
使用 HTML 定制元素
流程图中的每个点,也就是是元素,都可以自定义,直接编写 html 代码能添加按钮、输入框、代码块等。
我的一个代码块 demo,搭配 highlight.js 可以达到类似 IDA 控制流图的效果。这个 feature 可玩度很高。
joint.shapes.BBL = {};
joint.shapes.BBL.Element = joint.shapes.basic.Rect.extend({
defaults: joint.util.deepSupplement({
type: 'BBL.Element',
attrs: {
rect: { stroke: 'none', 'fill-opacity': 0 }
}
}, joint.shapes.basic.Rect.prototype.defaults)
});
// Create a custom view for that element that displays an HTML div above it.
// -------------------------------------------------------------------------
joint.shapes.BBL.ElementView = joint.dia.ElementView.extend({
template: [
'<div class="html-element" data-collapse>',
'<label></label><br/>',
'<div class="hljs"><pre><code></code></pre></span></div>',
'</div>'
].join(''),
initialize: function() {
_.bindAll(this, 'updateBox');
joint.dia.ElementView.prototype.initialize.apply(this, arguments);
this.$box = $(_.template(this.template)());
// Prevent paper from handling pointerdown.
this.$box.find('h3').on('mousedown click', function(evt) {
evt.stopPropagation();
});
// Update the box position whenever the underlying model changes.
this.model.on('change', this.updateBox, this);
// Remove the box when the model gets removed from the graph.
this.model.on('remove', this.removeBox, this);
this.updateBox();
},
render: function() {
joint.dia.ElementView.prototype.render.apply(this, arguments);
this.paper.$el.prepend(this.$box);
this.updateBox();
return this;
},
updateBox: function() {
// Set the position and dimension of the box so that it covers the JointJS element.
var bbox = this.model.getBBox();
// Example of updating the HTML with a data stored in the cell model.
this.$box.find('label').text(this.model.get('label'));
this.$box.find('code').html(this.model.get('code'));
var color = this.model.get('color');
this.$box.css({
width: bbox.width,
height: bbox.height,
left: bbox.x,
top: bbox.y,
background: color,
"border-color": color
});
},
removeBox: function(evt) {
this.$box.remove();
}
});
joint.shapes.BBL = {};
joint.shapes.BBL.Element = joint.shapes.basic.Rect.extend({
defaults: joint.util.deepSupplement({
type: 'BBL.Element',
attrs: {
rect: { stroke: 'none', 'fill-opacity': 0 }
}
}, joint.shapes.basic.Rect.prototype.defaults)
});
// Create a custom view for that element that displays an HTML div above it.
// -------------------------------------------------------------------------
joint.shapes.BBL.ElementView = joint.dia.ElementView.extend({
template: [
'<div class="html-element" data-collapse>',
'<label></label><br/>',
'<div class="hljs"><pre><code></code></pre></span></div>',
'</div>'
].join(''),
initialize: function() {
_.bindAll(this, 'updateBox');
joint.dia.ElementView.prototype.initialize.apply(this, arguments);
this.$box = $(_.template(this.template)());
// Prevent paper from handling pointerdown.
this.$box.find('h3').on('mousedown click', function(evt) {
evt.stopPropagation();
});
// Update the box position whenever the underlying model changes.
this.model.on('change', this.updateBox, this);
// Remove the box when the model gets removed from the graph.
this.model.on('remove', this.removeBox, this);
this.updateBox();
},
render: function() {
joint.dia.ElementView.prototype.render.apply(this, arguments);
this.paper.$el.prepend(this.$box);
this.updateBox();
return this;
},
updateBox: function() {
// Set the position and dimension of the box so that it covers the JointJS element.
var bbox = this.model.getBBox();
// Example of updating the HTML with a data stored in the cell model.
this.$box.find('label').text(this.model.get('label'));
this.$box.find('code').html(this.model.get('code'));
var color = this.model.get('color');
this.$box.css({
width: bbox.width,
height: bbox.height,
left: bbox.x,
top: bbox.y,
background: color,
"border-color": color
});
},
removeBox: function(evt) {
this.$box.remove();
}
});