由于一个项目需要绘制流程图。在做JS元素的选型时,看到 JointJS 的介绍,感觉应该可以满足我们的需要。不过还是得试试才知道。所以我打算按照以下顺序去试用 JointJS

  • 快速了解如何使用 JointJS
  • 试验如何用 JointJS 实现我们的关键需求点
    • 自定义元素
    • 自定义元素属性
    • 与后端交互

JointJS速览

入门

Hello World

下面时官方给出的入门demo例子,可以看到 JointJS 依赖jquerylodashbackbone,使用 JointJS 只需要引入joint.cssjoint.js即可。定义一个div用来盛放绘制的图形。

<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="css/joint.css" />
</head>
<body>
<!-- content -->
<div id="myholder"></div>

<!-- dependencies -->
<script src="js/jquery.js"></script>
<script src="js/lodash.js"></script>
<script src="js/backbone.js"></script>
<script src="js/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>

代码解析

在上面的js代码中,有以下几类对象:

paperJointJS中最外层的对象,用来连接内外:内部装载的是哪个graph,外部是渲染在哪个div中。gridSize属性是用来指明元素对齐的网格的大小。影响元素移动的粒度。paper在渲染过后,可以使用paper.scale()去实现缩放,使用paper.translate()去实现整体位移。paper的完整属性可以参看这里

graph:用来承载各个元素的画布。

Rectangle:矩形元素。一个元素可以是通过构造器,如new joint.shapes.standard.Rectangle(),来实例化,也可以通过clone方法去得到。shapes.standard下提供了十几种常见的图形元素,比如圆形、椭圆、带标题的矩形等等。官网提到,可以通过继承Element对象,来自定义自己的元素。Element最常用的几个方法如下:

  • element.position() - 设置元素原点(左上角)相对于paper坐标系的位置(考虑paper缩放和其他变换)。
  • element.resize() - 设置元素的尺寸。
  • element.clone() - 克隆现有元素,包括其位置,尺寸和属性。
  • element.translate() - 沿两个坐标轴移动元素指定的距离。还有缩放和旋转元素的方法。
  • element.addTo() - 将元素添加到graph中以便可以呈现它。

每个元素都可以通过element.attr方法来重写属性。这个元素支持哪些属性,可以通过joint.js中查看代码来得知,比如Rectangle元素在joint.js中的定义代码如下:

Element.define('standard.Rectangle', {
attrs: {
body: {
refWidth: '100%',
refHeight: '100%',
strokeWidth: 2,
stroke: '#000000',
fill: '#FFFFFF'
},
label: {
textVerticalAnchor: 'middle',
textAnchor: 'middle',
refX: '50%',
refY: '50%',
fontSize: 14,
fill: '#333333'
}
}
}, {
markup: [{
tagName: 'rect',
selector: 'body',
}, {
tagName: 'text',
selector: 'label'
}]
});

改变Element的属性在上例html中演示了两种写法rect,rect2,个人感觉第二种更直观。

Link:线条元素。指明线条的两个端点即实现了连线。Link和普通的元素没有很大区别。

中级

特殊属性

相对维度

使用SVG时最常见的请求之一是相对设置SVGElements的维度。 JointJS允许您使用一组ref属性来实现。这些属性允许您将子元素的大小调整为形状模型的尺寸的百分比。此外,由于所有计算都是程序化的,并且不依赖于浏览器的bbox测量,因此使用这些属性不会影响应用程序的性能。

  • refWidth和refHeight - 设置子元素相对于模型bbox的宽度。
  • refX和refY - 设置子元素左上角相对于模型bbox左上角的坐标。百分比是相对于模型bbox的。
  • refCx和refCy - 设置圆/椭圆中心的坐标。百分比是相对于模型bbox的。可以与refX / refY一起使用。
  • refRx和refRy - 设置椭圆相对于模型bbox尺寸的半径。百分比是相对于模型bbox的。
  • refR - 设置圆的半径相对于模型bbox的短边的长度。百分比是相对于模型bbox的。
  • refRCircumscribed - 设置圆的半径相对于模型bbox的最长对角线。

基于文本的相对维度

暂不关心细节,略过。

Link箭头

可以通过SourceMarkerTargetMarkerLink自定义箭头,如下面的示例代码为其定义了一个矩形箭头和圆形箭尾:

link.attr({
line: {
sourceMarker: {
'type': 'rect',
'width': 50,
'height': 10,
'y': -5,
'fill': 'rgba(255,0,0,0.3)',
'stroke': 'black'
},
targetMarker: {
'type': 'circle',
'r': 10,
'cx': 10,
'fill': 'rgba(0,255,0,0.3)',
'stroke': 'black'
}
}
});

Link的相对位置

Link的文本标签

可以通过扩展Link的方式为其添加文本标签。暂不关心细节,略过。

事件

paper的内建事件

Paper提供了常见的事件捕捉,如单元格/元素/线条/空白处被点击,鼠标移动,线条连接元素,线条取消连接元素等等,详细的可参见。事件的回调写法如下:

paper.on('blank:pointerdown', function(evt, x, y) {
alert('pointerdown on a blank area in the paper.')
})

连续的一组事件,可以共享数据,写法如下:

// Create a new link by dragging
paper.on({
'blank:pointerdown': function(evt, x, y) {
var link = new joint.dia.Link();
link.set('source', { x: x, y: y });
link.set('target', { x: x, y: y });
link.addTo(this.model);
evt.data = { link: link, x: x, y: y };
},
'blank:pointermove': function(evt, x, y) {
evt.data.link.set('target', { x: x, y: y });
},
'blank:pointerup': function(evt) {
var target = evt.data.link.get('target');
if (evt.data.x === target.x && evt.data.y === target.y) {
// remove zero-length links
evt.data.link.remove();
}
}
});

graph的内建事件

graph提供了一些捕捉element位置变化、大小变化的事情,如change:position是监测element的位置发生了变化,change:target是监测link的指向发生了变化,示例代码如下:

graph.on('change:position', function(cell) {
var center = cell.getBBox().center();
var label = center.toString();
cell.attr('label/text', label);
});

graph.on('change:target', function(cell) {
var target = new g.Point(cell.target());
var label = target.toString();
cell.label(0, {
attrs: {
label: {
text: label
}
}
});
});

子元素的事件监听

在下面的代码中定义了一个新的元素,他由一个矩形+一个按钮组成。现在我们想监听按钮被点击的事情,该怎么处理?注意97行代码,我们需要通过子元素的event属性,告知上层event是需要监听的。然后通过paper.on来捕捉,并定义响应。

<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="css/joint.css" />
</head>
<body>
<!-- content -->
<div id="paper"></div>

<!-- dependencies -->
<script src="js/jquery.js"></script>
<script src="js/lodash.js"></script>
<script src="js/backbone.js"></script>
<script src="js/joint.js"></script>

<!-- code -->
<script type="text/javascript">

var graph = new joint.dia.Graph;

var paper = new joint.dia.Paper({
el: document.getElementById('paper'),
model: graph,
width: 600,
height: 100,
gridSize: 10,
drawGrid: true,
background: {
color: 'rgba(0, 255, 0, 0.3)'
}
});


var CustomElement = joint.dia.Element.define('examples.CustomElement', {
attrs: {
body: {
refWidth: '100%',
refHeight: '100%',
strokeWidth: 2,
stroke: 'black',
fill: 'white'
},
label: {
textVerticalAnchor: 'middle',
textAnchor: 'middle',
refX: '50%',
refY: '50%',
fontSize: 14,
fill: 'black'
},
button: {
cursor: 'pointer',
ref: 'buttonLabel',
refWidth: '150%',
refHeight: '150%',
refX: '-25%',
refY: '-25%'
},
buttonLabel: {
pointerEvents: 'none',
refX: '100%',
refY: 0,
textAnchor: 'middle',
textVerticalAnchor: 'middle'
}
}
}, {
markup: [{
tagName: 'rect',
selector: 'body',
}, {
tagName: 'text',
selector: 'label'
}, {
tagName: 'rect',
selector: 'button'
}, {
tagName: 'text',
selector: 'buttonLabel'
}]
});

var element = new CustomElement();
element.position(250, 30);
element.resize(100, 40);
element.attr({
label: {
pointerEvents: 'none',
visibility: 'visible',
text: 'Element'
},
body: {
cursor: 'default',
visibility: 'visible'
},
button: {
event: 'element:button:pointerdown',
fill: 'orange',
stroke: 'black',
strokeWidth: 2
},
buttonLabel: {
text: '_', // fullwidth underscore
fill: 'black',
fontSize: 8,
fontWeight: 'bold'
}
});
element.addTo(graph);

paper.on('element:button:pointerdown', function(elementView, evt) {
evt.stopPropagation(); // stop any further actions with the element view (e.g. dragging)

var model = elementView.model;

if (model.attr('body/visibility') === 'visible') {
model.attr('body/visibility', 'hidden');
model.attr('label/visibility', 'hidden');
model.attr('buttonLabel/text', '+'); // fullwidth plus

} else {
model.attr('body/visibility', 'visible');
model.attr('label/visibility', 'visible');
model.attr('buttonLabel/text', '_'); // fullwidth underscore
}
});

</script>
</body>
</html>

自定义视图事件

对于更高级的事件自定义,我们可以通过重写paper的默认view来实现,下面是示例代码。在下面的例子中,我们禁用了默认的视图,通过elementviewlinkview重写了事件。新的效果是,当某个元素或者线条被双击时会删除它本身。

var paper = new joint.dia.Paper({
el: document.getElementById('paper-custom-view-events'),
model: graph,
width: 600,
height: 100,
gridSize: 1,
background: {
color: 'white'
},
interactive: false, // disable default interaction (e.g. dragging)
elementView: joint.dia.ElementView.extend({
pointerdblclick: function(evt, x, y) {
this.model.remove();
}
}),
linkView: joint.dia.LinkView.extend({
pointerdblclick: function(evt, x, y) {
this.model.remove();
}
})
});

序列化

joint.js序列化比较简单,调用graph.toJSON()即可,如本文第一个示例代码的graph,序列化后如下:

{
"cells": [
{
"type": "standard.Rectangle",
"position": {
"x": 100,
"y": 30
},
"size": {
"width": 100,
"height": 40
},
"angle": 0,
"id": "7428fe92-c29d-4658-9146-d7594de8a5d9",
"z": 1,
"attrs": {
"body": {
"fill": "blue"
},
"label": {
"fill": "white",
"text": "Hello"
}
}
},
{
"type": "standard.Rectangle",
"position": {
"x": 400,
"y": 30
},
"size": {
"width": 100,
"height": 40
},
"angle": 0,
"id": "f42d2d4b-8c8f-46de-8143-d2c8b833549f",
"z": 1,
"attrs": {
"body": {
"fill": "blue"
},
"label": {
"fill": "white",
"text": "World!"
}
}
},
{
"type": "standard.Link",
"source": {
"id": "7428fe92-c29d-4658-9146-d7594de8a5d9"
},
"target": {
"id": "f42d2d4b-8c8f-46de-8143-d2c8b833549f"
},
"id": "998611ac-31af-4f37-b90b-f796217c788e",
"z": 2,
"attrs": {}
}
]
}

反序列化,可以逆向调用graph.fromJSON()即可。重要的是要记住这两个函数适用于JSON对象 - 而不是JSON字符串。但是,如有必要,也可以使用JSON.stringify()JSON.parse()函数轻松地来回转换。

var graph1 = new joint.dia.Graph();
var jsonObject = graph1.toJSON();
var jsonString = JSON.stringify(jsonObject);

// transmission of `jsonString` across network etc.

var graph2 = new joint.dia.Graph(); // new empty graph
graph2.fromJSON(JSON.parse(jsonString));

在序列化和反序列化时,也可以指定元素来进行,这为将来的元素复用提供了入口。

var graph = new joint.dia.Graph();
graph.fromJSON({
cells: [{
id: 1,
type: 'standard.Rectangle',
position: {
x: 100,
y: 100
},
size: {
width: 100,
height: 100
}
}]
});
var graph = new joint.dia.Graph();
graph.fromJSON({ cells: [] });

自定义元素

可以先看一下源码中是如何定义一个Rectangle Element的,代码如下:

joint.dia.Element.define('standard.Rectangle', {
attrs: {
body: {
refWidth: '100%',
refHeight: '100%',
strokeWidth: 2,
stroke: '#000000',
fill: '#FFFFFF'
},
label: {
textVerticalAnchor: 'middle',
textAnchor: 'middle',
refX: '50%',
refY: '50%',
fontSize: 14,
fill: '#333333'
}
}
}, {
markup: [{
tagName: 'rect',
selector: 'body',
}, {
tagName: 'text',
selector: 'label'
}]
});
  • 通过Element.define函数去声明这是一个Element的定义。
  • standard.Rectangle是所定义Element的名称,应是唯一的。这里隐藏了joint.shapes,所以实际上的全名是joint.shapes.standard.Rectangle
  • markup是定义子元素的地方。tagName指明了子元素的名字rect->SVGRectElement ,text->SVGTextElement selector给出了两个子元素的在本元素类的名称。可以看出joint.js最底层是svg对象。
  • attrs设置默认属性。使用在markup中定义的selector标识符,为各个子元素指定属性。可以看到这里子元素的大小都是通过相对维度去设定的。

这里还没有提到的是构造函数。在上面的元素定义中,还可以增加一个构造函数段,如下:

{
createRandom: function() {

var rectangle = new this();

var fill = '#' + ('000000' + Math.floor(Math.random() * 16777215).toString(16)).slice(-6);
var stroke = '#' + ('000000' + Math.floor(Math.random() * 16777215).toString(16)).slice(-6);
var strokeWidth = Math.floor(Math.random() * 6);
var strokeDasharray = Math.floor(Math.random() * 6) + ' ' + Math.floor(Math.random() * 6);
var radius = Math.floor(Math.random() * 21);

rectangle.attr({
body: {
fill: fill,
stroke: stroke,
strokeWidth: strokeWidth,
strokeDasharray: strokeDasharray,
rx: radius,
ry: radius
},
label: { // ensure visibility on dark backgrounds
fill: 'black',
stroke: 'white',
strokeWidth: 1,
fontWeight: 'bold'
}
});

return rectangle;
}

这样,使用的时候,就可以通过joint.shapes.standard.Rectangle.createRandom();来调用构造函数去创建元素对象。

自定义属性

自定义属性需要在使用时通过以下方法进行:

var rect3 = new joint.shapes.lefer.Rectangle()
rect3.attr('bussiness/title', 'lefer');

需要注意的是,自定义的业务属性需要在自定义元素时没有出现过才能成功。

使用port

前面的示例中是在渲染时就指明了图形的连接关系,还有一种很常见的情况是由用户来拖拽连线。这个时候需要用到joint.shapes.devs.js。下面是示例代码

WORKING WITH PORTS
Many diagramming applications deal with elements with ports. Ports are usually displayed as circles inside diagram elements and are used not only as "sticky" points for connected links but they also further structure the linking information. It is common that certain elements have lists of input and output ports. A link might then point not to the element as a whole but to a certain port instead.

JointJS has a built-in support for elements with ports, linking between ports and a facility for defining what connections are allowed and what not. This is useful if you, for example, want to restrict linking in between input ports, or output ports or between a certain port of an element A and a certain port of an element B. This tutorial shows you how you can do all that.

Creating elements with ports
The easiest way to start with elements with ports is using the joint.shapes.devs plugin. Search for joint.shapes.devs.js file. This plugin defines one important shape, the joint.shapes.devs.Model*. You can just instantiate that shape and pass the inPorts and outPorts arrays as parameters. You can further set the coloring of the ports and label for your element as you can see in the example below. Moreover, JointJS takes care of preparing the view and the magnets** for UI interaction. That's why you can already click and drag a port and JointJS automatically creates a link coming out of that port.

JointJS and the joint.shapes.devs.Model also makes it easy to change ports. Simply set the inPorts/outPorts arrays of your element:

element.set('inPorts', ['newIn1', 'newIn2', 'newIn3']);
element.set('outPorts', ['newOut1', 'newOut2']);
*DEVS is an abbreviation for Discrete EVent System specification and is a formalism for modeling and analyzing general systems. This formalism uses two types of models (Atomic and Coupled) both having a set of input and output ports.

**Magnets in JointJS are SVG sub-elements that serve as sticky points for links. If you use the joint.shapes.devs plugin, you don't have to define your magnets yourself, instead the joint.shapes.devs.Model shape does it for you.

(function() {

var graph = new joint.dia.Graph;
var paper = new joint.dia.Paper({ el: $('#paper-create'), width: 650, height: 200, gridSize: 1, model: graph });

var m1 = new joint.shapes.devs.Model({
position: { x: 50, y: 50 },
size: { width: 90, height: 90 },
inPorts: ['in1','in2'],
outPorts: ['out'],
ports: {
groups: {
'in': {
attrs: {
'.port-body': {
fill: '#16A085'
}
}
},
'out': {
attrs: {
'.port-body': {
fill: '#E74C3C'
}
}
}
}
},
attrs: {
'.label': { text: 'Model', 'ref-x': .5, 'ref-y': .2 },
rect: { fill: '#2ECC71' }
}
});
graph.addCell(m1);

}());
Linking elements with ports
Now when you have your elements with ports created, you can start observing what port is connected with a link to what other port. This is easy to do thanks to JointJS storing the information about ports in the link models themselves once the links are created via the UI. The following example shows you how you can get the linking information. Try to connect a port of one element to another port of another element.

(function() {

var graph = new joint.dia.Graph;
var paper = new joint.dia.Paper({ el: $('#paper-link'), width: 650, height: 200, gridSize: 1, model: graph });

var m1 = new joint.shapes.devs.Model({
position: { x: 50, y: 50 },
size: { width: 90, height: 90 },
inPorts: ['in1','in2'],
outPorts: ['out'],
ports: {
groups: {
'in': {
attrs: {
'.port-body': {
fill: '#16A085'
}
}
},
'out': {
attrs: {
'.port-body': {
fill: '#E74C3C'
}
}
}
}
},
attrs: {
'.label': { text: 'Model', 'ref-x': .5, 'ref-y': .2 },
rect: { fill: '#2ECC71' }
}
});
graph.addCell(m1);

var m2 = m1.clone().translate(300, 0).attr('.label/text', 'Model 2');
graph.addCell(m2);

graph.on('change:source change:target', function(link) {
var sourcePort = link.get('source').port;
var sourceId = link.get('source').id;
var targetPort = link.get('target').port;
var targetId = link.get('target').id;

var m = [
'The port <b>' + sourcePort,
'</b> of element with ID <b>' + sourceId,
'</b> is connected to port <b>' + targetPort,
'</b> of elemnt with ID <b>' + targetId + '</b>'
].join('');

out(m);
});

function out(m) {
$('#paper-link-out').html(m);
}

}());
Linking restrictions
Now you know how to create elements with ports and how to get the linking information. Another practical functionality related to elements with ports and their links is restricting certain connections. Say you want links to never start in input ports and never end in output ports. This is the most usual case. However, all kinds of restrictions are possible and application specific. JointJS doesn't limit you. Instead, it allows you to define a function that simply returns true if a connection between a source magnet of a source element and a target magnet of a target element is allowed, and false otherwise. If the connection is not allowed JointJS does not connect the magnets (and associated ports). Furthermore, you can mark certain magnets as "passive" in which case JointJS treats these magnets in a way that they can never become a source of a link. For further information, please see the list of options that you can pass to the joint.dia.Paper in the API reference page, especially the two related functions: validateConnection() and validateMagnet().

(function() {

var graph = new joint.dia.Graph;
var paper = new joint.dia.Paper({
el: $('#paper-restrict'),
width: 650, height: 200, gridSize: 1,
model: graph,
defaultLink: new joint.dia.Link({
attrs: { '.marker-target': { d: 'M 10 0 L 0 5 L 10 10 z' } }
}),
validateConnection: function(cellViewS, magnetS, cellViewT, magnetT, end, linkView) {
// Prevent linking from input ports.
if (magnetS && magnetS.getAttribute('port-group') === 'in') return false;
// Prevent linking from output ports to input ports within one element.
if (cellViewS === cellViewT) return false;
// Prevent linking to input ports.
return magnetT && magnetT.getAttribute('port-group') === 'in';
},
validateMagnet: function(cellView, magnet) {
// Note that this is the default behaviour. Just showing it here for reference.
// Disable linking interaction for magnets marked as passive (see below `.inPorts circle`).
return magnet.getAttribute('magnet') !== 'passive';
}
});

var m1 = new joint.shapes.devs.Model({
position: { x: 50, y: 50 },
size: { width: 90, height: 90 },
inPorts: ['in1','in2'],
outPorts: ['out'],
ports: {
groups: {
'in': {
attrs: {
'.port-body': {
fill: '#16A085',
magnet: 'passive'
}
}
},
'out': {
attrs: {
'.port-body': {
fill: '#E74C3C'
}
}
}
}
},
attrs: {
'.label': { text: 'Model', 'ref-x': .5, 'ref-y': .2 },
rect: { fill: '#2ECC71' }
}
});
graph.addCell(m1);

var m2 = m1.clone();
m2.translate(300, 0);
graph.addCell(m2);
m2.attr('.label/text', 'Model 2');

}());
Link snapping
To improve user experience little bit you might want to enable the link snapping. While the user is dragging a link, it searches for the closest port in the given radius. Once a suitable port is found (it meets requirements specified in validateConnection()) the link automatically connects to it. You can try this functionality in the example below.

(function() {

var graph = new joint.dia.Graph;
var paper = new joint.dia.Paper({
el: $('#paper-link-snapping'),
width: 650, height: 200, gridSize: 1,
model: graph,
defaultLink: new joint.dia.Link({
attrs: { '.marker-target': { d: 'M 10 0 L 0 5 L 10 10 z' } }
}),
validateConnection: function(cellViewS, magnetS, cellViewT, magnetT, end, linkView) {
// Prevent loop linking
return (magnetS !== magnetT);
},
// Enable link snapping within 75px lookup radius
snapLinks: { radius: 75 }
});

var m1 = new joint.shapes.devs.Model({
position: { x: 50, y: 50 },
size: { width: 90, height: 90 },
inPorts: ['in1','in2'],
outPorts: ['out'],
ports: {
groups: {
'in': {
attrs: {
'.port-body': {
fill: '#16A085',
magnet: 'passive'
}
}
},
'out': {
attrs: {
'.port-body': {
fill: '#E74C3C'
}
}
}
}
},
attrs: {
'.label': { text: 'Model', 'ref-x': .5, 'ref-y': .2 },
rect: { fill: '#2ECC71' }
}
});
graph.addCell(m1);

var m2 = m1.clone();
m2.translate(300, 0);
graph.addCell(m2);
m2.attr('.label/text', 'Model 2');

})();
Marking available magnets
Another way how to make user's life easier can be to offer him all magnets he can connect to while he is dragging a link. To achieve this you have to enable markAvailable option on the paper and add some css rules into your stylesheet like in the example bellow.

/* port styling */
.available-magnet {
fill: yellow;
}

/* element styling */
.available-cell rect {
stroke-dasharray: 5, 2;
}
(function() {

var graph = new joint.dia.Graph;
var paper = new joint.dia.Paper({
el: $('#paper-mark-available'),
width: 650, height: 200, gridSize: 1,
model: graph,
defaultLink: new joint.dia.Link({
attrs: { '.marker-target': { d: 'M 10 0 L 0 5 L 10 10 z' } }
}),
validateConnection: function(cellViewS, magnetS, cellViewT, magnetT, end, linkView) {
// Prevent linking from input ports.
if (magnetS && magnetS.getAttribute('port-group') === 'in') return false;
// Prevent linking from output ports to input ports within one element.
if (cellViewS === cellViewT) return false;
// Prevent linking to input ports.
return magnetT && magnetT.getAttribute('port-group') === 'in';
},
// Enable marking available cells & magnets
markAvailable: true
});

var m1 = new joint.shapes.devs.Model({
position: { x: 50, y: 50 },
size: { width: 90, height: 90 },
inPorts: ['in1','in2'],
outPorts: ['out'],
ports: {
groups: {
'in': {
attrs: {
'.port-body': {
fill: '#16A085',
magnet: 'passive'
}
}
},
'out': {
attrs: {
'.port-body': {
fill: '#E74C3C'
}
}
}
}
},
attrs: {
'.label': { text: 'Model', 'ref-x': .5, 'ref-y': .2 },
rect: { fill: '#2ECC71' }
}
}).addTo(graph);

var m2 = m1.clone().translate(300, 0).attr('.label/text', 'Model 2').addTo(graph);

})();