1
|
/*!
|
2
|
Copyright (c) The Cytoscape Consortium
|
3
|
|
4
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
5
|
this software and associated documentation files (the “Software”), to deal in
|
6
|
the Software without restriction, including without limitation the rights to
|
7
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
8
|
of the Software, and to permit persons to whom the Software is furnished to do
|
9
|
so, subject to the following conditions:
|
10
|
|
11
|
The above copyright notice and this permission notice shall be included in all
|
12
|
copies or substantial portions of the Software.
|
13
|
|
14
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
20
|
SOFTWARE.
|
21
|
*/
|
22
|
|
23
|
;(function(){ 'use strict';
|
24
|
|
25
|
// registers the extension on a cytoscape lib ref
|
26
|
var register = function( cytoscape, dagre ){
|
27
|
if( !cytoscape || !dagre ){ return; } // can't register if cytoscape unspecified
|
28
|
|
29
|
var isFunction = function(o){ return typeof o === 'function'; };
|
30
|
|
31
|
// default layout options
|
32
|
var defaults = {
|
33
|
// dagre algo options, uses default value on undefined
|
34
|
nodeSep: undefined, // the separation between adjacent nodes in the same rank
|
35
|
edgeSep: undefined, // the separation between adjacent edges in the same rank
|
36
|
rankSep: undefined, // the separation between adjacent nodes in the same rank
|
37
|
rankDir: undefined, // 'TB' for top to bottom flow, 'LR' for left to right
|
38
|
minLen: function( edge ){ return 1; }, // number of ranks to keep between the source and target of the edge
|
39
|
edgeWeight: function( edge ){ return 1; }, // higher weight edges are generally made shorter and straighter than lower weight edges
|
40
|
|
41
|
// general layout options
|
42
|
fit: true, // whether to fit to viewport
|
43
|
padding: 30, // fit padding
|
44
|
animate: false, // whether to transition the node positions
|
45
|
animationDuration: 500, // duration of animation in ms if enabled
|
46
|
animationEasing: undefined, // easing of animation if enabled
|
47
|
boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
|
48
|
ready: function(){}, // on layoutready
|
49
|
stop: function(){} // on layoutstop
|
50
|
};
|
51
|
|
52
|
// constructor
|
53
|
// options : object containing layout options
|
54
|
function DagreLayout( options ){
|
55
|
var opts = this.options = {};
|
56
|
for( var i in defaults ){ opts[i] = defaults[i]; }
|
57
|
for( var i in options ){ opts[i] = options[i]; }
|
58
|
}
|
59
|
|
60
|
// runs the layout
|
61
|
DagreLayout.prototype.run = function(){
|
62
|
var options = this.options;
|
63
|
var layout = this;
|
64
|
|
65
|
var cy = options.cy; // cy is automatically populated for us in the constructor
|
66
|
var eles = options.eles;
|
67
|
|
68
|
var getVal = function( ele, val ){
|
69
|
return isFunction(val) ? val.apply( ele, [ ele ] ) : val;
|
70
|
};
|
71
|
|
72
|
var bb = options.boundingBox || { x1: 0, y1: 0, w: cy.width(), h: cy.height() };
|
73
|
if( bb.x2 === undefined ){ bb.x2 = bb.x1 + bb.w; }
|
74
|
if( bb.w === undefined ){ bb.w = bb.x2 - bb.x1; }
|
75
|
if( bb.y2 === undefined ){ bb.y2 = bb.y1 + bb.h; }
|
76
|
if( bb.h === undefined ){ bb.h = bb.y2 - bb.y1; }
|
77
|
|
78
|
var g = new dagre.graphlib.Graph({
|
79
|
multigraph: true,
|
80
|
compound: true
|
81
|
});
|
82
|
|
83
|
var gObj = {};
|
84
|
var setGObj = function( name, val ){
|
85
|
if( val != null ){
|
86
|
gObj[ name ] = val;
|
87
|
}
|
88
|
};
|
89
|
|
90
|
setGObj( 'nodesep', options.nodeSep );
|
91
|
setGObj( 'edgesep', options.edgeSep );
|
92
|
setGObj( 'ranksep', options.rankSep );
|
93
|
setGObj( 'rankdir', options.rankDir );
|
94
|
|
95
|
g.setGraph( gObj );
|
96
|
|
97
|
g.setDefaultEdgeLabel(function() { return {}; });
|
98
|
g.setDefaultNodeLabel(function() { return {}; });
|
99
|
|
100
|
// add nodes to dagre
|
101
|
var nodes = eles.nodes();
|
102
|
for( var i = 0; i < nodes.length; i++ ){
|
103
|
var node = nodes[i];
|
104
|
var nbb = node.boundingBox();
|
105
|
|
106
|
g.setNode( node.id(), {
|
107
|
width: nbb.w,
|
108
|
height: nbb.h,
|
109
|
name: node.id()
|
110
|
} );
|
111
|
|
112
|
// console.log( g.node(node.id()) );
|
113
|
}
|
114
|
|
115
|
// set compound parents
|
116
|
for( var i = 0; i < nodes.length; i++ ){
|
117
|
var node = nodes[i];
|
118
|
|
119
|
if( node.isChild() ){
|
120
|
g.setParent( node.id(), node.parent().id() );
|
121
|
}
|
122
|
}
|
123
|
|
124
|
// add edges to dagre
|
125
|
var edges = eles.edges().stdFilter(function( edge ){
|
126
|
return !edge.source().isParent() && !edge.target().isParent(); // dagre can't handle edges on compound nodes
|
127
|
});
|
128
|
for( var i = 0; i < edges.length; i++ ){
|
129
|
var edge = edges[i];
|
130
|
|
131
|
g.setEdge( edge.source().id(), edge.target().id(), {
|
132
|
minlen: getVal( edge, options.minLen ),
|
133
|
weight: getVal( edge, options.edgeWeight ),
|
134
|
name: edge.id()
|
135
|
}, edge.id() );
|
136
|
|
137
|
// console.log( g.edge(edge.source().id(), edge.target().id(), edge.id()) );
|
138
|
}
|
139
|
|
140
|
dagre.layout( g );
|
141
|
|
142
|
var gNodeIds = g.nodes();
|
143
|
for( var i = 0; i < gNodeIds.length; i++ ){
|
144
|
var id = gNodeIds[i];
|
145
|
var n = g.node( id );
|
146
|
|
147
|
cy.getElementById(id).scratch().dagre = n;
|
148
|
}
|
149
|
|
150
|
var dagreBB;
|
151
|
|
152
|
if( options.boundingBox ){
|
153
|
dagreBB = { x1: Infinity, x2: -Infinity, y1: Infinity, y2: -Infinity };
|
154
|
nodes.forEach(function( node ){
|
155
|
var dModel = node.scratch().dagre;
|
156
|
|
157
|
dagreBB.x1 = Math.min( dagreBB.x1, dModel.x );
|
158
|
dagreBB.x2 = Math.max( dagreBB.x2, dModel.x );
|
159
|
|
160
|
dagreBB.y1 = Math.min( dagreBB.y1, dModel.y );
|
161
|
dagreBB.y2 = Math.max( dagreBB.y2, dModel.y );
|
162
|
});
|
163
|
|
164
|
dagreBB.w = dagreBB.x2 - dagreBB.x1;
|
165
|
dagreBB.h = dagreBB.y2 - dagreBB.y1;
|
166
|
} else {
|
167
|
dagreBB = bb;
|
168
|
}
|
169
|
|
170
|
var constrainPos = function( p ){
|
171
|
if( options.boundingBox ){
|
172
|
var xPct = dagreBB.w === 0 ? 0 : (p.x - dagreBB.x1) / dagreBB.w;
|
173
|
var yPct = dagreBB.h === 0 ? 0 : (p.y - dagreBB.y1) / dagreBB.h;
|
174
|
|
175
|
return {
|
176
|
x: bb.x1 + xPct * bb.w,
|
177
|
y: bb.y1 + yPct * bb.h
|
178
|
};
|
179
|
} else {
|
180
|
return p;
|
181
|
}
|
182
|
};
|
183
|
|
184
|
nodes.layoutPositions(layout, options, function( ele ){
|
185
|
ele = typeof ele === "object" ? ele : this;
|
186
|
var dModel = ele.scratch().dagre;
|
187
|
|
188
|
return constrainPos({
|
189
|
x: dModel.x,
|
190
|
y: dModel.y
|
191
|
});
|
192
|
});
|
193
|
|
194
|
return this; // chaining
|
195
|
};
|
196
|
|
197
|
cytoscape('layout', 'dagre', DagreLayout);
|
198
|
|
199
|
};
|
200
|
|
201
|
if( typeof module !== 'undefined' && module.exports ){ // expose as a commonjs module
|
202
|
module.exports = function( cytoscape, dagre ){
|
203
|
register( cytoscape, dagre || require('dagre') );
|
204
|
};
|
205
|
} else if( typeof define !== 'undefined' && define.amd ){ // expose as an amd/requirejs module
|
206
|
define('cytoscape-dagre', function(){
|
207
|
return register;
|
208
|
});
|
209
|
}
|
210
|
|
211
|
if( typeof cytoscape !== 'undefined' && typeof dagre !== 'undefined' ){ // expose to global cytoscape (i.e. window.cytoscape)
|
212
|
register( cytoscape, dagre );
|
213
|
}
|
214
|
|
215
|
})();
|