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" rel="external nofollow" rel="external nofollow" rel="external nofollow" /> </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);
HTML 前端
<!DOCTYPE html> <html> <head> <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/jointjs/2.1.0/joint.css" rel="external nofollow" rel="external nofollow" rel="external nofollow" /> </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" });
配置参数
注释
我们来试一下。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);
HTML 前端
<!DOCTYPE html> <html> <head> <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/jointjs/2.1.0/joint.css" rel="external nofollow" rel="external nofollow" rel="external nofollow" /> </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(); } });
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持易盾网络。