JointJS:JavaScript 流程图绘制框架
<h1 id="jointjsjavascript-流程图绘制框架">JointJS:JavaScript 流程图绘制框架</h1><blockquote>
<p>最近调研了js画流程图的框架,最后选择了Joint。配合上 dagre 可以画出像模像样的流程图。</p>
</blockquote>
<p></p><div class="toc"><div class="toc-container-header">目录</div><ul><li>JointJS:JavaScript 流程图绘制框架<ul><li>JointJS 简介</li><li>JointJS Hello world</li><li>前后端分离架构</li><li>其他<ul><li>自动布局 Automatic layout</li><li>使用 HTML 定制元素</li></ul></li></ul></li></ul></div><p></p>
<h2 id="jointjs-简介">JointJS 简介</h2>
<p>JointJS 是一个开源前端框架,支持绘制各种各样的流程图、工作流图等。Rappid 是 Joint 的商业版,提供了一些更强的插件。JointJS 的特点有下面几条,摘自官网:</p>
<ul>
<li>能够实时地渲染上百(或者上千)个元素和连接</li>
<li>支持多种形状(矩形、圆、文本、图像、路径等)</li>
<li>高度事件驱动,用户可自定义任何发生在 paper 下的事件响应</li>
<li>元素间连接简单</li>
<li>可定制的连接和关系图</li>
<li>连接平滑(基于贝塞尔插值 bezier interpolation)& 智能路径选择</li>
<li>基于 SVG 的可定制、可编程的图形渲染</li>
<li>NodeJS 支持</li>
<li>通过 JSON 进行序列化和反序列化</li>
</ul>
<p>总之 JoingJS 是一款很强的流程图制作框架,开源版本已经足够日常使用了。</p>
<p>一些常用地址:</p>
<p>API: https://resources.jointjs.com/docs/jointjs/v1.1/joint.html</p>
<p>Tutorials: https://resources.jointjs.com/tutorial</p>
<h2 id="jointjs-hello-world">JointJS Hello world</h2>
<pre><code class="language-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>
</code></pre>
<p>hello world 代码没什么好说的。要注意这里的图形并没有自动排版,而是通过移动第二个 rect 实现的手动排版。</p>
<p><img src="https://img2018.cnblogs.com/blog/761792/201908/761792-20190814204022500-1136219124.png"></p>
<h2 id="前后端分离架构">前后端分离架构</h2>
<p>既然支持 NodeJs,那就可以把繁重的图形绘制任务交给服务器,再通过 JSON 序列化在 HTTP 上传输对象,这样减轻客户端的压力。</p>
<p>NodeJS 后端</p>
<pre><code class="language-javascript">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);
</code></pre>
<p>HTML 前端</p>
<pre><code class="language-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>
</code></pre>
<h2 id="其他">其他</h2>
<h3 id="自动布局-automatic-layout">自动布局 Automatic layout</h3>
<p>JointJS 内置了插件进行自动排版,原理是调用 Dagre 库。官方 api 中有样例。</p>
<p>使用方法:</p>
<blockquote>
<p>var graphBBox = joint.layout.DirectedGraph.layout(graph, {<br>
nodeSep: 50,<br>
edgeSep: 80,<br>
rankDir: "TB"<br>
});</p>
</blockquote>
<table>
<thead>
<tr>
<th style="text-align: left">配置参数</th>
<th>注释</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left">nodeSep</td>
<td>相同rank的邻接节点的距离</td>
</tr>
<tr>
<td style="text-align: left">edgeSep</td>
<td>相同rank的邻接边的距离</td>
</tr>
<tr>
<td style="text-align: left">rankSep</td>
<td>不同 rank 元素之间的距离</td>
</tr>
<tr>
<td style="text-align: left">rankDir</td>
<td>布局方向 ( <code>"TB"</code> (top-to-bottom) / <code>"BT"</code> (bottom-to-top) / <code>"LR"</code> (left-to-right) / <code>"RL"</code> (right-to-left))</td>
</tr>
<tr>
<td style="text-align: left">marginX</td>
<td>number of pixels to use as a margin around the left and right of the graph.</td>
</tr>
<tr>
<td style="text-align: left">marginY</td>
<td>number of pixels to use as a margin around the top and bottom of the graph.</td>
</tr>
<tr>
<td style="text-align: left">ranker</td>
<td>排序算法。 Possible values: <code>'network-simplex'</code> (default), <code>'tight-tree'</code> or <code>'longest-path'</code>.</td>
</tr>
<tr>
<td style="text-align: left">resizeClusters</td>
<td>set to <code>false</code> if you don't want parent elements to stretch in order to fit all their embedded children. Default is <code>true</code>.</td>
</tr>
<tr>
<td style="text-align: left">clusterPadding</td>
<td>A gap between the parent element and the boundary of its embedded children. It could be a number or an object e.g. <code>{ left: 10, right: 10, top: 30, bottom: 10 }</code>. It defaults to <code>10</code>.</td>
</tr>
<tr>
<td style="text-align: left">setPosition(element, position)</td>
<td>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 <code>element.set('position', position)</code> but want to set the position in an animated fashion via transitions.</td>
</tr>
<tr>
<td style="text-align: left">setVertices(link, vertices)</td>
<td>If set to <code>true</code> the layout will adjust the links by setting their vertices. It defaults to <code>false</code>. If the option is defined as a function it will be used to set the vertices of links at the end of the layout. This is useful if you don't want to use the default <code>link.set('vertices', vertices)</code> but want to set the vertices in an animated fashion via transitions.</td>
</tr>
<tr>
<td style="text-align: left">setLabels(link, labelPosition, points)</td>
<td>If set to <code>true</code> the layout will adjust the labels by setting their position. It defaults to <code>false</code>. If the option is defined as a function it will be used to set the labels of links at the end of the layout. <em><strong>Note:</strong> Only the first label (link.label(0)😉 is positioned by the layout.</em></td>
</tr>
<tr>
<td style="text-align: left">dagre</td>
<td>默认情况下,dagre 应该在全局命名空间当中,不过你也可以当作参数传进去</td>
</tr>
<tr>
<td style="text-align: left">graphlib</td>
<td>默认情况下,graphlib 应该在全局命名空间当中,不过你也可以当作参数传进去</td>
</tr>
</tbody>
</table>
<p>我们来试一下。NodeJS 后端</p>
<pre><code class="language-javascript">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);
//autolayout
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);
</code></pre>
<p>HTML 前端</p>
<pre><code class="language-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>
</code></pre>
<p>结果:</p>
<p><img src="https://img2018.cnblogs.com/blog/761792/201908/761792-20190814204120938-1779921677.png"></p>
<h3 id="使用-html-定制元素">使用 HTML 定制元素</h3>
<p>流程图中的每个点,也就是是元素,都可以自定义,直接编写 html 代码能添加按钮、输入框、代码块等。</p>
<p>我的一个代码块 demo,搭配 highlight.js 可以达到类似 IDA 控制流图的效果。这个 feature 可玩度很高。</p>
<pre><code class="language-javascript">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();
}
});
</code></pre><br><br>
来源:https://www.cnblogs.com/helica/p/11354577.html
頁:
[1]