Graph database Analysis of the Steam Network
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

408 lines
13 KiB

;(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);