;(function(undefined) {
|
|
'use strict';
|
|
|
|
if (typeof sigma === 'undefined')
|
|
throw new Error('sigma is not declared');
|
|
|
|
// Initialize package:
|
|
sigma.utils.pkg('sigma.layout.noverlap');
|
|
|
|
/**
|
|
* Noverlap Layout
|
|
* ===============================
|
|
*
|
|
* Author: @apitts / Andrew Pitts
|
|
* Algorithm: @jacomyma / Mathieu Jacomy (originally contributed to Gephi and ported to sigma.js under the MIT license by @andpitts with permission)
|
|
* Acknowledgement: @sheyman / Sébastien Heymann (some inspiration has been taken from other MIT licensed layout algorithms authored by @sheyman)
|
|
* Version: 0.1
|
|
*/
|
|
|
|
var settings = {
|
|
speed: 3,
|
|
scaleNodes: 1.2,
|
|
nodeMargin: 5.0,
|
|
gridSize: 20,
|
|
permittedExpansion: 1.1,
|
|
rendererIndex: 0,
|
|
maxIterations: 500
|
|
};
|
|
|
|
var _instance = {};
|
|
|
|
/**
|
|
* Event emitter Object
|
|
* ------------------
|
|
*/
|
|
var _eventEmitter = {};
|
|
|
|
/**
|
|
* Noverlap Object
|
|
* ------------------
|
|
*/
|
|
function Noverlap() {
|
|
var self = this;
|
|
|
|
this.init = function (sigInst, options) {
|
|
options = options || {};
|
|
|
|
// Properties
|
|
this.sigInst = sigInst;
|
|
this.config = sigma.utils.extend(options, settings);
|
|
this.easing = options.easing;
|
|
this.duration = options.duration;
|
|
|
|
if (options.nodes) {
|
|
this.nodes = options.nodes;
|
|
delete options.nodes;
|
|
}
|
|
|
|
if (!sigma.plugins || typeof sigma.plugins.animate === 'undefined') {
|
|
throw new Error('sigma.plugins.animate is not declared');
|
|
}
|
|
|
|
// State
|
|
this.running = false;
|
|
};
|
|
|
|
/**
|
|
* Single layout iteration.
|
|
*/
|
|
this.atomicGo = function () {
|
|
if (!this.running || this.iterCount < 1) return false;
|
|
|
|
var nodes = this.nodes || this.sigInst.graph.nodes(),
|
|
nodesCount = nodes.length,
|
|
i,
|
|
n,
|
|
n1,
|
|
n2,
|
|
xmin = Infinity,
|
|
xmax = -Infinity,
|
|
ymin = Infinity,
|
|
ymax = -Infinity,
|
|
xwidth,
|
|
yheight,
|
|
xcenter,
|
|
ycenter,
|
|
grid,
|
|
row,
|
|
col,
|
|
minXBox,
|
|
maxXBox,
|
|
minYBox,
|
|
maxYBox,
|
|
adjacentNodes,
|
|
subRow,
|
|
subCol,
|
|
nxmin,
|
|
nxmax,
|
|
nymin,
|
|
nymax;
|
|
|
|
this.iterCount--;
|
|
this.running = false;
|
|
|
|
for (i=0; i < nodesCount; i++) {
|
|
n = nodes[i];
|
|
n.dn.dx = 0;
|
|
n.dn.dy = 0;
|
|
|
|
//Find the min and max for both x and y across all nodes
|
|
xmin = Math.min(xmin, n.dn_x - (n.dn_size*self.config.scaleNodes + self.config.nodeMargin) );
|
|
xmax = Math.max(xmax, n.dn_x + (n.dn_size*self.config.scaleNodes + self.config.nodeMargin) );
|
|
ymin = Math.min(ymin, n.dn_y - (n.dn_size*self.config.scaleNodes + self.config.nodeMargin) );
|
|
ymax = Math.max(ymax, n.dn_y + (n.dn_size*self.config.scaleNodes + self.config.nodeMargin) );
|
|
|
|
}
|
|
|
|
xwidth = xmax - xmin;
|
|
yheight = ymax - ymin;
|
|
xcenter = (xmin + xmax) / 2;
|
|
ycenter = (ymin + ymax) / 2;
|
|
xmin = xcenter - self.config.permittedExpansion*xwidth / 2;
|
|
xmax = xcenter + self.config.permittedExpansion*xwidth / 2;
|
|
ymin = ycenter - self.config.permittedExpansion*yheight / 2;
|
|
ymax = ycenter + self.config.permittedExpansion*yheight / 2;
|
|
|
|
grid = {}; //An object of objects where grid[row][col] is an array of node ids representing nodes that fall in that grid. Nodes can fall in more than one grid
|
|
|
|
for(row = 0; row < self.config.gridSize; row++) {
|
|
grid[row] = {};
|
|
for(col = 0; col < self.config.gridSize; col++) {
|
|
grid[row][col] = [];
|
|
}
|
|
}
|
|
|
|
//Place nodes in grid
|
|
for (i=0; i < nodesCount; i++) {
|
|
n = nodes[i];
|
|
|
|
nxmin = n.dn_x - (n.dn_size*self.config.scaleNodes + self.config.nodeMargin);
|
|
nxmax = n.dn_x + (n.dn_size*self.config.scaleNodes + self.config.nodeMargin);
|
|
nymin = n.dn_y - (n.dn_size*self.config.scaleNodes + self.config.nodeMargin);
|
|
nymax = n.dn_y + (n.dn_size*self.config.scaleNodes + self.config.nodeMargin);
|
|
|
|
minXBox = Math.floor(self.config.gridSize* (nxmin - xmin) / (xmax - xmin) );
|
|
maxXBox = Math.floor(self.config.gridSize* (nxmax - xmin) / (xmax - xmin) );
|
|
minYBox = Math.floor(self.config.gridSize* (nymin - ymin) / (ymax - ymin) );
|
|
maxYBox = Math.floor(self.config.gridSize* (nymax - ymin) / (ymax - ymin) );
|
|
for(col = minXBox; col <= maxXBox; col++) {
|
|
for(row = minYBox; row <= maxYBox; row++) {
|
|
grid[row][col].push(n.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
adjacentNodes = {}; //An object that stores the node ids of adjacent nodes (either in same grid box or adjacent grid box) for all nodes
|
|
|
|
for(row = 0; row < self.config.gridSize; row++) {
|
|
for(col = 0; col < self.config.gridSize; col++) {
|
|
grid[row][col].forEach(function(nodeId) {
|
|
if(!adjacentNodes[nodeId]) {
|
|
adjacentNodes[nodeId] = [];
|
|
}
|
|
for(subRow = Math.max(0, row - 1); subRow <= Math.min(row + 1, self.config.gridSize - 1); subRow++) {
|
|
for(subCol = Math.max(0, col - 1); subCol <= Math.min(col + 1, self.config.gridSize - 1); subCol++) {
|
|
grid[subRow][subCol].forEach(function(subNodeId) {
|
|
if(subNodeId !== nodeId && adjacentNodes[nodeId].indexOf(subNodeId) === -1) {
|
|
adjacentNodes[nodeId].push(subNodeId);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
//If two nodes overlap then repulse them
|
|
for (i=0; i < nodesCount; i++) {
|
|
n1 = nodes[i];
|
|
adjacentNodes[n1.id].forEach(function(nodeId) {
|
|
var n2 = self.sigInst.graph.nodes(nodeId);
|
|
var xDist = n2.dn_x - n1.dn_x;
|
|
var yDist = n2.dn_y - n1.dn_y;
|
|
var dist = Math.sqrt(xDist*xDist + yDist*yDist);
|
|
var collision = (dist < ((n1.dn_size*self.config.scaleNodes + self.config.nodeMargin) + (n2.dn_size*self.config.scaleNodes + self.config.nodeMargin)));
|
|
if(collision) {
|
|
self.running = true;
|
|
if(dist > 0) {
|
|
n2.dn.dx += xDist / dist * (1 + n1.dn_size);
|
|
n2.dn.dy += yDist / dist * (1 + n1.dn_size);
|
|
} else {
|
|
n2.dn.dx += xwidth * 0.01 * (0.5 - Math.random());
|
|
n2.dn.dy += yheight * 0.01 * (0.5 - Math.random());
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
for (i=0; i < nodesCount; i++) {
|
|
n = nodes[i];
|
|
if(!n.fixed) {
|
|
n.dn_x = n.dn_x + n.dn.dx * 0.1 * self.config.speed;
|
|
n.dn_y = n.dn_y + n.dn.dy * 0.1 * self.config.speed;
|
|
}
|
|
}
|
|
|
|
if(this.running && this.iterCount < 1) {
|
|
this.running = false;
|
|
}
|
|
|
|
return this.running;
|
|
};
|
|
|
|
this.go = function () {
|
|
this.iterCount = this.config.maxIterations;
|
|
|
|
while (this.running) {
|
|
this.atomicGo();
|
|
};
|
|
|
|
this.stop();
|
|
};
|
|
|
|
this.start = function() {
|
|
if (this.running) return;
|
|
|
|
var nodes = this.sigInst.graph.nodes();
|
|
|
|
var prefix = this.sigInst.renderers[self.config.rendererIndex].options.prefix;
|
|
|
|
this.running = true;
|
|
|
|
// Init nodes
|
|
for (var i = 0; i < nodes.length; i++) {
|
|
nodes[i].dn_x = nodes[i][prefix + 'x'];
|
|
nodes[i].dn_y = nodes[i][prefix + 'y'];
|
|
nodes[i].dn_size = nodes[i][prefix + 'size'];
|
|
nodes[i].dn = {
|
|
dx: 0,
|
|
dy: 0
|
|
};
|
|
}
|
|
_eventEmitter[self.sigInst.id].dispatchEvent('start');
|
|
this.go();
|
|
};
|
|
|
|
this.stop = function() {
|
|
var nodes = this.sigInst.graph.nodes();
|
|
|
|
this.running = false;
|
|
|
|
if (this.easing) {
|
|
_eventEmitter[self.sigInst.id].dispatchEvent('interpolate');
|
|
sigma.plugins.animate(
|
|
self.sigInst,
|
|
{
|
|
x: 'dn_x',
|
|
y: 'dn_y'
|
|
},
|
|
{
|
|
easing: self.easing,
|
|
onComplete: function() {
|
|
self.sigInst.refresh();
|
|
for (var i = 0; i < nodes.length; i++) {
|
|
nodes[i].dn = null;
|
|
nodes[i].dn_x = null;
|
|
nodes[i].dn_y = null;
|
|
}
|
|
_eventEmitter[self.sigInst.id].dispatchEvent('stop');
|
|
},
|
|
duration: self.duration
|
|
}
|
|
);
|
|
}
|
|
else {
|
|
// Apply changes
|
|
for (var i = 0; i < nodes.length; i++) {
|
|
nodes[i].x = nodes[i].dn_x;
|
|
nodes[i].y = nodes[i].dn_y;
|
|
}
|
|
|
|
this.sigInst.refresh();
|
|
|
|
for (var i = 0; i < nodes.length; i++) {
|
|
nodes[i].dn = null;
|
|
nodes[i].dn_x = null;
|
|
nodes[i].dn_y = null;
|
|
}
|
|
_eventEmitter[self.sigInst.id].dispatchEvent('stop');
|
|
}
|
|
};
|
|
|
|
this.kill = function() {
|
|
this.sigInst = null;
|
|
this.config = null;
|
|
this.easing = null;
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Interface
|
|
* ----------
|
|
*/
|
|
|
|
/**
|
|
* Configure the layout algorithm.
|
|
|
|
* Recognized options:
|
|
* **********************
|
|
* Here is the exhaustive list of every accepted parameter in the settings
|
|
* object:
|
|
*
|
|
* {?number} speed A larger value increases the convergence speed at the cost of precision
|
|
* {?number} scaleNodes The ratio to scale nodes by - a larger ratio will lead to more space around larger nodes
|
|
* {?number} nodeMargin A fixed margin to apply around nodes regardless of size
|
|
* {?number} maxIterations The maximum number of iterations to perform before the layout completes.
|
|
* {?integer} gridSize The number of rows and columns to use when partioning nodes into a grid for efficient computation
|
|
* {?number} permittedExpansion A permitted expansion factor to the overall size of the network applied at each iteration
|
|
* {?integer} rendererIndex The index of the renderer to use for node co-ordinates. Defaults to zero.
|
|
* {?(function|string)} easing Either the name of an easing in the sigma.utils.easings package or a function. If not specified, the
|
|
* quadraticInOut easing from this package will be used instead.
|
|
* {?number} duration The duration of the animation. If not specified, the "animationsTime" setting value of the sigma instance will be used instead.
|
|
*
|
|
*
|
|
* @param {object} config The optional configuration object.
|
|
*
|
|
* @return {sigma.classes.dispatcher} Returns an event emitter.
|
|
*/
|
|
sigma.prototype.configNoverlap = function(config) {
|
|
|
|
var sigInst = this;
|
|
|
|
if (!config) throw new Error('Missing argument: "config"');
|
|
|
|
// Create instance if undefined
|
|
if (!_instance[sigInst.id]) {
|
|
_instance[sigInst.id] = new Noverlap();
|
|
|
|
_eventEmitter[sigInst.id] = {};
|
|
sigma.classes.dispatcher.extend(_eventEmitter[sigInst.id]);
|
|
|
|
// Binding on kill to clear the references
|
|
sigInst.bind('kill', function() {
|
|
_instance[sigInst.id].kill();
|
|
_instance[sigInst.id] = null;
|
|
_eventEmitter[sigInst.id] = null;
|
|
});
|
|
}
|
|
|
|
_instance[sigInst.id].init(sigInst, config);
|
|
|
|
return _eventEmitter[sigInst.id];
|
|
};
|
|
|
|
/**
|
|
* Start the layout algorithm. It will use the existing configuration if no
|
|
* new configuration is passed.
|
|
|
|
* Recognized options:
|
|
* **********************
|
|
* Here is the exhaustive list of every accepted parameter in the settings
|
|
* object
|
|
*
|
|
* {?number} speed A larger value increases the convergence speed at the cost of precision
|
|
* {?number} scaleNodes The ratio to scale nodes by - a larger ratio will lead to more space around larger nodes
|
|
* {?number} nodeMargin A fixed margin to apply around nodes regardless of size
|
|
* {?number} maxIterations The maximum number of iterations to perform before the layout completes.
|
|
* {?integer} gridSize The number of rows and columns to use when partioning nodes into a grid for efficient computation
|
|
* {?number} permittedExpansion A permitted expansion factor to the overall size of the network applied at each iteration
|
|
* {?integer} rendererIndex The index of the renderer to use for node co-ordinates. Defaults to zero.
|
|
* {?(function|string)} easing Either the name of an easing in the sigma.utils.easings package or a function. If not specified, the
|
|
* quadraticInOut easing from this package will be used instead.
|
|
* {?number} duration The duration of the animation. If not specified, the "animationsTime" setting value of the sigma instance will be used instead.
|
|
*
|
|
*
|
|
*
|
|
* @param {object} config The optional configuration object.
|
|
*
|
|
* @return {sigma.classes.dispatcher} Returns an event emitter.
|
|
*/
|
|
|
|
sigma.prototype.startNoverlap = function(config) {
|
|
|
|
var sigInst = this;
|
|
|
|
if (config) {
|
|
this.configNoverlap(sigInst, config);
|
|
}
|
|
|
|
_instance[sigInst.id].start();
|
|
|
|
return _eventEmitter[sigInst.id];
|
|
};
|
|
|
|
/**
|
|
* Returns true if the layout has started and is not completed.
|
|
*
|
|
* @return {boolean}
|
|
*/
|
|
sigma.prototype.isNoverlapRunning = function() {
|
|
|
|
var sigInst = this;
|
|
|
|
return !!_instance[sigInst.id] && _instance[sigInst.id].running;
|
|
};
|
|
|
|
}).call(this);
|