Skip to content

Instantly share code, notes, and snippets.

@mattkenefick
Created September 10, 2013 17:45
Show Gist options
  • Save mattkenefick/6512949 to your computer and use it in GitHub Desktop.
Save mattkenefick/6512949 to your computer and use it in GitHub Desktop.
Sigma Concatenated
// Define packages:
var sigma = {};
sigma.tools = {};
sigma.classes = {};
sigma.instances = {};
// Adding Array helpers, if not present yet:
(function() {
if (!Array.prototype.some) {
Array.prototype.some = function(fun /*, thisp*/) {
var len = this.length;
if (typeof fun != 'function') {
throw new TypeError();
}
var thisp = arguments[1];
for (var i = 0; i < len; i++) {
if (i in this &&
fun.call(thisp, this[i], i, this)) {
return true;
}
}
return false;
};
}
if (!Array.prototype.forEach) {
Array.prototype.forEach = function(fun /*, thisp*/) {
var len = this.length;
if (typeof fun != 'function') {
throw new TypeError();
}
var thisp = arguments[1];
for (var i = 0; i < len; i++) {
if (i in this) {
fun.call(thisp, this[i], i, this);
}
}
};
}
if (!Array.prototype.map) {
Array.prototype.map = function(fun /*, thisp*/) {
var len = this.length;
if (typeof fun != 'function') {
throw new TypeError();
}
var res = new Array(len);
var thisp = arguments[1];
for (var i = 0; i < len; i++) {
if (i in this) {
res[i] = fun.call(thisp, this[i], i, this);
}
}
return res;
};
}
if (!Array.prototype.filter) {
Array.prototype.filter = function(fun /*, thisp*/) {
var len = this.length;
if (typeof fun != 'function')
throw new TypeError();
var res = new Array();
var thisp = arguments[1];
for (var i = 0; i < len; i++) {
if (i in this) {
var val = this[i]; // in case fun mutates this
if (fun.call(thisp, val, i, this)) {
res.push(val);
}
}
}
return res;
};
}
if (!Object.keys) {
Object.keys = (function() {
var hasOwnProperty = Object.prototype.hasOwnProperty,
hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
dontEnums = [
'toString',
'toLocaleString',
'valueOf',
'hasOwnProperty',
'isPrototypeOf',
'propertyIsEnumerable',
'constructor'
],
dontEnumsLength = dontEnums.length;
return function(obj) {
if (typeof obj !== 'object' &&
typeof obj !== 'function' ||
obj === null
) {
throw new TypeError('Object.keys called on non-object');
}
var result = [];
for (var prop in obj) {
if (hasOwnProperty.call(obj, prop)) result.push(prop);
}
if (hasDontEnumBug) {
for (var i = 0; i < dontEnumsLength; i++) {
if (hasOwnProperty.call(obj, dontEnums[i])) {
result.push(dontEnums[i]);
}
}
}
return result;
}
})();
}
})();
/**
* A jQuery like properties management class. It works like jQuery .css()
* method: You can call it with juste one string to get the corresponding
* property, with a string and anything else to set the corresponding property,
* or directly with an object, and then each pair string / object (or any type)
* will be set in the properties.
* @constructor
* @this {sigma.classes.Cascade}
*/
sigma.classes.Cascade = function() {
/**
* This instance properties.
* @protected
* @type {Object}
*/
this.p = {};
/**
* The method to use to set/get any property of this instance.
* @param {(string|Object)} a1 If it is a string and if a2 is undefined,
* then it will return the corresponding
* property.
* If it is a string and if a2 is set, then it
* will set a2 as the property corresponding to
* a1, and return this.
* If it is an object, then each pair string /
* object (or any other type) will be set as a
* property.
* @param {*?} a2 The new property corresponding to a1 if a1 is
* a string.
* @return {(*|sigma.classes.Cascade)} Returns itself or the corresponding
* property.
*/
this.config = function(a1, a2) {
if (typeof a1 == 'string' && a2 == undefined) {
return this.p[a1];
} else {
var o = (typeof a1 == 'object' && a2 == undefined) ? a1 : {};
if (typeof a1 == 'string') {
o[a1] = a2;
}
for (var k in o) {
if (this.p[k] != undefined) {
this.p[k] = o[k];
}
}
return this;
}
};
};
/**
* sigma.js custom event dispatcher class.
* @constructor
* @this {sigma.classes.EventDispatcher}
*/
sigma.classes.EventDispatcher = function() {
/**
* An object containing all the different handlers bound to one or many
* events, indexed by these events.
* @private
* @type {Object.<string,Object>}
*/
var _h = {};
/**
* Represents "this", without the well-known scope issue.
* @private
* @type {sigma.classes.EventDispatcher}
*/
var _self = this;
/**
* Will execute the handler the next (and only the next) time that the
* indicated event (or the indicated events) will be triggered.
* @param {string} events The name of the event (or the events
* separated by spaces).
* @param {function(Object)} handler The handler to bind.
* @return {sigma.classes.EventDispatcher} Returns itself.
*/
function one(events, handler) {
if (!handler || !events) {
return _self;
}
var eArray = ((typeof events) == 'string') ? events.split(' ') : events;
eArray.forEach(function(event) {
if (!_h[event]) {
_h[event] = [];
}
_h[event].push({
'h': handler,
'one': true
});
});
return _self;
}
/**
* Will execute the handler everytime that the indicated event (or the
* indicated events) will be triggered.
* @param {string} events The name of the event (or the events
* separated by spaces).
* @param {function(Object)} handler The handler to bind.
* @return {sigma.classes.EventDispatcher} Returns itself.
*/
function bind(events, handler) {
if (!handler || !events) {
return _self;
}
var eArray = ((typeof events) == 'string') ? events.split(' ') : events;
eArray.forEach(function(event) {
if (!_h[event]) {
_h[event] = [];
}
_h[event].push({
'h': handler,
'one': false
});
});
return _self;
}
/**
* Unbinds the handler from a specified event (or specified events).
* @param {?string} events The name of the event (or the events
* separated by spaces). If undefined,
* then all handlers are unbound.
* @param {?function(Object)} handler The handler to unbind. If undefined,
* each handler bound to the event or the
* events will be unbound.
* @return {sigma.classes.EventDispatcher} Returns itself.
*/
function unbind(events, handler) {
if (!events) {
_h = {};
}
var eArray = typeof events == 'string' ? events.split(' ') : events;
if (handler) {
eArray.forEach(function(event) {
if (_h[event]) {
_h[event] = _h[event].filter(function(e) {
return e['h'] != handler;
});
}
if (_h[event] && _h[event].length == 0) {
delete _h[event];
}
});
}else {
eArray.forEach(function(event) {
delete _h[event];
});
}
return _self;
}
/**
* Executes each handler bound to the event
* @param {string} type The type of the event.
* @param {?Object} content The content of the event (optional).
* @return {sigma.classes.EventDispatcher} Returns itself.
*/
function dispatch(type, content) {
if (_h[type]) {
_h[type].forEach(function(e) {
e['h']({
'type': type,
'content': content,
'target': _self
});
});
_h[type] = _h[type].filter(function(e) {
return !e['one'];
});
}
return _self;
}
/* PUBLIC INTERFACE: */
this.one = one;
this.bind = bind;
this.unbind = unbind;
this.dispatch = dispatch;
};
(function() {
// Define local shortcut:
var id = 0;
// Define local package:
var local = {};
local.plugins = [];
sigma.init = function(dom) {
var inst = new Sigma(dom, (++id).toString());
sigma.instances[id] = new SigmaPublic(inst);
return sigma.instances[id];
};
/**
* The graph data model used in sigma.js.
* @constructor
* @extends sigma.classes.Cascade
* @extends sigma.classes.EventDispatcher
* @this {Graph}
*/
function Graph() {
sigma.classes.Cascade.call(this);
sigma.classes.EventDispatcher.call(this);
/**
* Represents "this", without the well-known scope issue.
* @private
* @type {Graph}
*/
var self = this;
/**
* The different parameters that determine how the nodes and edges should be
* translated and rescaled.
* @type {Object}
*/
this.p = {
minNodeSize: 0,
maxNodeSize: 0,
minEdgeSize: 0,
maxEdgeSize: 0,
// Scaling mode:
// - 'inside' (default)
// - 'outside'
scalingMode: 'inside',
nodesPowRatio: 0.5,
edgesPowRatio: 0
};
/**
* Contains the borders of the graph. These are useful to avoid the user to
* drag the graph out of the canvas.
* @type {Object}
*/
this.borders = {};
/**
* Inserts a node in the graph.
* @param {string} id The node's ID.
* @param {object} params An object containing the different parameters
* of the node.
* @return {Graph} Returns itself.
*/
function addNode(id, params) {
if (self.nodesIndex[id]) {
throw new Error('Node "' + id + '" already exists.');
}
params = params || {};
var n = {
// Numbers :
'x': 0,
'y': 0,
'size': 1,
'degree': 0,
'inDegree': 0,
'outDegree': 0,
// Flags :
'fixed': false,
'active': false,
'hidden': false,
'forceLabel': false,
// Strings :
'label': id.toString(),
'id': id.toString(),
// Custom attributes :
'attr': {}
};
for (var k in params) {
switch (k) {
case 'id':
break;
case 'x':
case 'y':
case 'size':
n[k] = +params[k];
break;
case 'fixed':
case 'active':
case 'hidden':
case 'forceLabel':
n[k] = !!params[k];
break;
case 'color':
case 'label':
n[k] = params[k];
break;
default:
n['attr'][k] = params[k];
}
}
self.nodes.push(n);
self.nodesIndex[id.toString()] = n;
return self;
};
/**
* Generates the clone of a node, to make it easier to be exported.
* @private
* @param {Object} node The node to clone.
* @return {Object} The clone of the node.
*/
function cloneNode(node) {
return {
'x': node['x'],
'y': node['y'],
'size': node['size'],
'degree': node['degree'],
'inDegree': node['inDegree'],
'outDegree': node['outDegree'],
'displayX': node['displayX'],
'displayY': node['displayY'],
'displaySize': node['displaySize'],
'label': node['label'],
'id': node['id'],
'color': node['color'],
'fixed': node['fixed'],
'active': node['active'],
'hidden': node['hidden'],
'forceLabel': node['forceLabel'],
'attr': node['attr']
};
};
/**
* Checks the clone of a node, and inserts its values when possible. For
* example, it is possible to modify the size or the color of a node, but it
* is not possible to modify its display values or its id.
* @private
* @param {Object} node The original node.
* @param {Object} copy The clone.
* @return {Graph} Returns itself.
*/
function checkNode(node, copy) {
for (var k in copy) {
switch (k) {
case 'id':
case 'attr':
case 'degree':
case 'inDegree':
case 'outDegree':
case 'displayX':
case 'displayY':
case 'displaySize':
break;
case 'x':
case 'y':
case 'size':
node[k] = +copy[k];
break;
case 'fixed':
case 'active':
case 'hidden':
case 'forceLabel':
node[k] = !!copy[k];
break;
case 'color':
case 'label':
node[k] = (copy[k] || '').toString();
break;
default:
node['attr'][k] = copy[k];
}
}
return self;
};
/**
* Deletes one or several nodes from the graph, and the related edges.
* @param {(string|Array.<string>)} v A string ID, or an Array of several
* IDs.
* @return {Graph} Returns itself.
*/
function dropNode(v) {
var a = (v instanceof Array ? v : [v]) || [];
a.forEach(function(id) {
if (self.nodesIndex[id]) {
var index = null;
self.nodes.some(function(n, i) {
if (n['id'] == id) {
index = i;
return true;
}
return false;
});
index != null && self.nodes.splice(index, 1);
delete self.nodesIndex[id];
var edgesToRemove = [];
self.edges = self.edges.filter(function(e) {
if (e['source']['id'] == id) {
delete self.edgesIndex[e['id']];
e['target']['degree']--;
e['target']['inDegree']--;
return false;
}else if (e['target']['id'] == id) {
delete self.edgesIndex[e['id']];
e['source']['degree']--;
e['source']['outDegree']--;
return false;
}
return true;
});
}else {
sigma.log('Node "' + id + '" does not exist.');
}
});
return self;
};
/**
* Inserts an edge in the graph.
* @param {string} id The edge ID.
* @param {string} source The ID of the edge source.
* @param {string} target The ID of the edge target.
* @param {object} params An object containing the different parameters
* of the edge.
* @return {Graph} Returns itself.
*/
function addEdge(id, source, target, params) {
if (self.edgesIndex[id]) {
throw new Error('Edge "' + id + '" already exists.');
}
if (!self.nodesIndex[source]) {
var s = 'Edge\'s source "' + source + '" does not exist yet.';
throw new Error(s);
}
if (!self.nodesIndex[target]) {
var s = 'Edge\'s target "' + target + '" does not exist yet.';
throw new Error(s);
}
params = params || {};
var e = {
'source': self.nodesIndex[source],
'target': self.nodesIndex[target],
'size': 1,
'weight': 1,
'displaySize': 0.5,
'label': id.toString(),
'id': id.toString(),
'hidden': false,
'attr': {}
};
e['source']['degree']++;
e['source']['outDegree']++;
e['target']['degree']++;
e['target']['inDegree']++;
for (var k in params) {
switch (k) {
case 'id':
case 'source':
case 'target':
break;
case 'hidden':
e[k] = !!params[k];
break;
case 'size':
case 'weight':
e[k] = +params[k];
break;
case 'color':
e[k] = params[k].toString();
break;
case 'type':
e[k] = params[k].toString();
break;
case 'label':
e[k] = params[k];
break;
default:
e['attr'][k] = params[k];
}
}
self.edges.push(e);
self.edgesIndex[id.toString()] = e;
return self;
};
/**
* Generates the clone of a edge, to make it easier to be exported.
* @private
* @param {Object} edge The edge to clone.
* @return {Object} The clone of the edge.
*/
function cloneEdge(edge) {
return {
'source': edge['source']['id'],
'target': edge['target']['id'],
'size': edge['size'],
'type': edge['type'],
'weight': edge['weight'],
'displaySize': edge['displaySize'],
'label': edge['label'],
'hidden': edge['hidden'],
'id': edge['id'],
'attr': edge['attr'],
'color': edge['color']
};
};
/**
* Checks the clone of an edge, and inserts its values when possible. For
* example, it is possible to modify the label or the type of an edge, but it
* is not possible to modify its display values or its id.
* @private
* @param {Object} edge The original edge.
* @param {Object} copy The clone.
* @return {Graph} Returns itself.
*/
function checkEdge(edge, copy) {
for (var k in copy) {
switch (k) {
case 'id':
case 'displaySize':
break;
case 'weight':
case 'size':
edge[k] = +copy[k];
break;
case 'source':
case 'target':
edge[k] = self.nodesIndex[k] || edge[k];
break;
case 'hidden':
edge[k] = !!copy[k];
break;
case 'color':
case 'label':
case 'type':
edge[k] = (copy[k] || '').toString();
break;
default:
edge['attr'][k] = copy[k];
}
}
return self;
};
/**
* Deletes one or several edges from the graph.
* @param {(string|Array.<string>)} v A string ID, or an Array of several
* IDs.
* @return {Graph} Returns itself.
*/
function dropEdge(v) {
var a = (v instanceof Array ? v : [v]) || [];
a.forEach(function(id) {
if (self.edgesIndex[id]) {
self.edgesIndex[id]['source']['degree']--;
self.edgesIndex[id]['source']['outDegree']--;
self.edgesIndex[id]['target']['degree']--;
self.edgesIndex[id]['target']['inDegree']--;
var index = null;
self.edges.some(function(n, i) {
if (n['id'] == id) {
index = i;
return true;
}
return false;
});
index != null && self.edges.splice(index, 1);
delete self.edgesIndex[id];
}else {
sigma.log('Edge "' + id + '" does not exist.');
}
});
return self;
};
/**
* Deletes every nodes and edges from the graph.
* @return {Graph} Returns itself.
*/
function empty() {
self.nodes = [];
self.nodesIndex = {};
self.edges = [];
self.edgesIndex = {};
return self;
};
/**
* Computes the display x, y and size of each node, relatively to the
* original values and the borders determined in the parameters, such as
* each node is in the described area.
* @param {number} w The area width (actually the width of the DOM
* root).
* @param {number} h The area height (actually the height of the
* DOM root).
* @param {boolean} parseNodes Indicates if the nodes have to be parsed.
* @param {boolean} parseEdges Indicates if the edges have to be parsed.
* @return {Graph} Returns itself.
*/
function rescale(w, h, parseNodes, parseEdges) {
var weightMax = 0, sizeMax = 0;
parseNodes && self.nodes.forEach(function(node) {
sizeMax = Math.max(node['size'], sizeMax);
});
parseEdges && self.edges.forEach(function(edge) {
weightMax = Math.max(edge['size'], weightMax);
});
sizeMax = sizeMax || 1;
weightMax = weightMax || 1;
// Recenter the nodes:
var xMin, xMax, yMin, yMax;
parseNodes && self.nodes.forEach(function(node) {
xMax = Math.max(node['x'], xMax || node['x']);
xMin = Math.min(node['x'], xMin || node['x']);
yMax = Math.max(node['y'], yMax || node['y']);
yMin = Math.min(node['y'], yMin || node['y']);
});
// First, we compute the scaling ratio, without considering the sizes
// of the nodes : Each node will have its center in the canvas, but might
// be partially out of it.
var scale = self.p.scalingMode == 'outside' ?
Math.max(w / Math.max(xMax - xMin, 1),
h / Math.max(yMax - yMin, 1)) :
Math.min(w / Math.max(xMax - xMin, 1),
h / Math.max(yMax - yMin, 1));
// Then, we correct that scaling ratio considering a margin, which is
// basically the size of the biggest node.
// This has to be done as a correction since to compare the size of the
// biggest node to the X and Y values, we have to first get an
// approximation of the scaling ratio.
var margin = (self.p.maxNodeSize || sizeMax) / scale;
xMax += margin;
xMin -= margin;
yMax += margin;
yMin -= margin;
scale = self.p.scalingMode == 'outside' ?
Math.max(w / Math.max(xMax - xMin, 1),
h / Math.max(yMax - yMin, 1)) :
Math.min(w / Math.max(xMax - xMin, 1),
h / Math.max(yMax - yMin, 1));
// Size homothetic parameters:
var a, b;
if (!self.p.maxNodeSize && !self.p.minNodeSize) {
a = 1;
b = 0;
}else if (self.p.maxNodeSize == self.p.minNodeSize) {
a = 0;
b = self.p.maxNodeSize;
}else {
a = (self.p.maxNodeSize - self.p.minNodeSize) / sizeMax;
b = self.p.minNodeSize;
}
var c, d;
if (!self.p.maxEdgeSize && !self.p.minEdgeSize) {
c = 1;
d = 0;
}else if (self.p.maxEdgeSize == self.p.minEdgeSize) {
c = 0;
d = self.p.minEdgeSize;
}else {
c = (self.p.maxEdgeSize - self.p.minEdgeSize) / weightMax;
d = self.p.minEdgeSize;
}
// Rescale the nodes:
parseNodes && self.nodes.forEach(function(node) {
node['displaySize'] = node['size'] * a + b;
if (!node['fixed']) {
node['displayX'] = (node['x'] - (xMax + xMin) / 2) * scale + w / 2;
node['displayY'] = (node['y'] - (yMax + yMin) / 2) * scale + h / 2;
}
});
parseEdges && self.edges.forEach(function(edge) {
edge['displaySize'] = edge['size'] * c + d;
});
return self;
};
/**
* Translates the display values of the nodes and edges relatively to the
* scene position and zoom ratio.
* @param {number} sceneX The x position of the scene.
* @param {number} sceneY The y position of the scene.
* @param {number} ratio The zoom ratio of the scene.
* @param {boolean} parseNodes Indicates if the nodes have to be parsed.
* @param {boolean} parseEdges Indicates if the edges have to be parsed.
* @return {Graph} Returns itself.
*/
function translate(sceneX, sceneY, ratio, parseNodes, parseEdges) {
var sizeRatio = Math.pow(ratio, self.p.nodesPowRatio);
parseNodes && self.nodes.forEach(function(node) {
if (!node['fixed']) {
node['displayX'] = node['displayX'] * ratio + sceneX;
node['displayY'] = node['displayY'] * ratio + sceneY;
}
node['displaySize'] = node['displaySize'] * sizeRatio;
});
sizeRatio = Math.pow(ratio, self.p.edgesPowRatio);
parseEdges && self.edges.forEach(function(edge) {
edge['displaySize'] = edge['displaySize'] * sizeRatio;
});
return self;
};
/**
* Determines the borders of the graph as it will be drawn. It is used to
* avoid the user to drag the graph out of the canvas.
*/
function setBorders() {
self.borders = {};
self.nodes.forEach(function(node) {
self.borders.minX = Math.min(
self.borders.minX == undefined ?
node['displayX'] - node['displaySize'] :
self.borders.minX,
node['displayX'] - node['displaySize']
);
self.borders.maxX = Math.max(
self.borders.maxX == undefined ?
node['displayX'] + node['displaySize'] :
self.borders.maxX,
node['displayX'] + node['displaySize']
);
self.borders.minY = Math.min(
self.borders.minY == undefined ?
node['displayY'] - node['displaySize'] :
self.borders.minY,
node['displayY'] - node['displaySize']
);
self.borders.maxY = Math.max(
self.borders.maxY == undefined ?
node['displayY'] - node['displaySize'] :
self.borders.maxY,
node['displayY'] - node['displaySize']
);
});
}
/**
* Checks which nodes are under the (mX, mY) points, representing the mouse
* position.
* @param {number} mX The mouse X position.
* @param {number} mY The mouse Y position.
* @return {Graph} Returns itself.
*/
function checkHover(mX, mY) {
var dX, dY, s, over = [], out = [];
self.nodes.forEach(function(node) {
if (node['hidden']) {
node['hover'] = false;
return;
}
dX = Math.abs(node['displayX'] - mX);
dY = Math.abs(node['displayY'] - mY);
s = node['displaySize'];
var oldH = node['hover'];
var newH = dX < s && dY < s && Math.sqrt(dX * dX + dY * dY) < s;
if (oldH && !newH) {
node['hover'] = false;
out.push(node.id);
} else if (newH && !oldH) {
node['hover'] = true;
over.push(node.id);
}
});
over.length && self.dispatch('overnodes', over);
out.length && self.dispatch('outnodes', out);
return self;
};
/**
* Applies a function to a clone of each node (or indicated nodes), and then
* tries to apply the modifications made on the clones to the original nodes.
* @param {function(Object)} fun The function to execute.
* @param {?Array.<string>} ids An Array of node IDs (optional).
* @return {Graph} Returns itself.
*/
function iterNodes(fun, ids) {
var a = ids ? ids.map(function(id) {
return self.nodesIndex[id];
}) : self.nodes;
var aCopies = a.map(cloneNode);
aCopies.forEach(fun);
a.forEach(function(n, i) {
checkNode(n, aCopies[i]);
});
return self;
};
/**
* Applies a function to a clone of each edge (or indicated edges), and then
* tries to apply the modifications made on the clones to the original edges.
* @param {function(Object)} fun The function to execute.
* @param {?Array.<string>} ids An Array of edge IDs (optional).
* @return {Graph} Returns itself.
*/
function iterEdges(fun, ids) {
var a = ids ? ids.map(function(id) {
return self.edgesIndex[id];
}) : self.edges;
var aCopies = a.map(cloneEdge);
aCopies.forEach(fun);
a.forEach(function(e, i) {
checkEdge(e, aCopies[i]);
});
return self;
};
/**
* Returns a specific node clone or an array of specified node clones.
* @param {(string|Array.<string>)} ids The ID or an array of node IDs.
* @return {(Object|Array.<Object>)} The clone or the array of clones.
*/
function getNodes(ids) {
var a = ((ids instanceof Array ? ids : [ids]) || []).map(function(id) {
return cloneNode(self.nodesIndex[id]);
});
return (ids instanceof Array ? a : a[0]);
};
/**
* Returns a specific edge clone or an array of specified edge clones.
* @param {(string|Array.<string>)} ids The ID or an array of edge IDs.
* @return {(Object|Array.<Object>)} The clone or the array of clones.
*/
function getEdges(ids) {
var a = ((ids instanceof Array ? ids : [ids]) || []).map(function(id) {
return cloneEdge(self.edgesIndex[id]);
});
return (ids instanceof Array ? a : a[0]);
};
empty();
this.addNode = addNode;
this.addEdge = addEdge;
this.dropNode = dropNode;
this.dropEdge = dropEdge;
this.iterEdges = iterEdges;
this.iterNodes = iterNodes;
this.getEdges = getEdges;
this.getNodes = getNodes;
this.empty = empty;
this.rescale = rescale;
this.translate = translate;
this.setBorders = setBorders;
this.checkHover = checkHover;
}
/**
* A class to monitor some local / global probes directly on an instance,
* inside a div DOM element.
* It executes different methods (called "probes") regularly, and displays
* the results on the element.
* @constructor
* @extends sigma.classes.Cascade
* @param {Sigma} instance The instance to monitor.
* @param {element} dom The div DOM element to draw write on.
* @this {Monitor}
*/
function Monitor(instance, dom) {
sigma.classes.Cascade.call(this);
/**
* Represents "this", without the well-known scope issue.
* @private
* @type {Monitor}
*/
var self = this;
/**
* {@link Sigma} instance owning this Monitor instance.
* @type {Sigma}
*/
this.instance = instance;
/**
* Determines if the monitoring is activated or not.
* @type {Boolean}
*/
this.monitoring = false;
/**
* The different parameters that define how this instance should work. It
* also contains the different probes.
* @see sigma.classes.Cascade
* @type {Object}
*/
this.p = {
fps: 40,
dom: dom,
globalProbes: {
'Time (ms)': sigma.chronos.getExecutionTime,
'Queue': sigma.chronos.getQueuedTasksCount,
'Tasks': sigma.chronos.getTasksCount,
'FPS': sigma.chronos.getFPS
},
localProbes: {
'Nodes count': function() { return self.instance.graph.nodes.length; },
'Edges count': function() { return self.instance.graph.edges.length; }
}
};
/**
* Activates the monitoring: Some texts describing some values about sigma.js
* or the owning {@link Sigma} instance will appear over the graph, but
* beneath the mouse sensible DOM element.
* @return {Monitor} Returns itself.
*/
function activate() {
if (!self.monitoring) {
self.monitoring = window.setInterval(routine, 1000 / self.p.fps);
}
return self;
}
/**
* Desactivates the monitoring: Will disappear, and stop computing the
* different probes.
* @return {Monitor} Returns itself.
*/
function desactivate() {
if (self.monitoring) {
window.clearInterval(self.monitoring);
self.monitoring = null;
self.p.dom.innerHTML = '';
}
return self;
}
/**
* The private method dedicated to compute the different values to observe.
* @private
* @return {Monitor} Returns itself.
*/
function routine() {
var s = '';
s += '<p>GLOBAL :</p>';
for (var k in self.p.globalProbes) {
s += '<p>' + k + ' : ' + self.p.globalProbes[k]() + '</p>';
}
s += '<br><p>LOCAL :</p>';
for (var k in self.p.localProbes) {
s += '<p>' + k + ' : ' + self.p.localProbes[k]() + '</p>';
}
self.p.dom.innerHTML = s;
return self;
}
this.activate = activate;
this.desactivate = desactivate;
}
/**
* This class listen to all the different mouse events, to normalize them and
* dispatch action events instead (from "startinterpolate" to "isdragging",
* etc).
* @constructor
* @extends sigma.classes.Cascade
* @extends sigma.classes.EventDispatcher
* @param {element} dom The DOM element to bind the handlers on.
* @this {MouseCaptor}
*/
function MouseCaptor(dom) {
sigma.classes.Cascade.call(this);
sigma.classes.EventDispatcher.call(this);
/**
* Represents "this", without the well-known scope issue.
* @private
* @type {MouseCaptor}
*/
var self = this;
/**
* The DOM element to bind the handlers on.
* @type {element}
*/
var dom = dom;
/**
* The different parameters that define how this instance should work.
* @see sigma.classes.Cascade
* @type {Object}
*/
this.p = {
minRatio: 1,
maxRatio: 32,
marginRatio: 1,
zoomDelta: 0.1,
dragDelta: 0.3,
zoomMultiply: 2,
directZooming: false,
blockScroll: true,
inertia: 1.1,
mouseEnabled: true
};
var oldMouseX = 0;
var oldMouseY = 0;
var startX = 0;
var startY = 0;
var oldStageX = 0;
var oldStageY = 0;
var oldRatio = 1;
var targetRatio = 1;
var targetStageX = 0;
var targetStageY = 0;
var lastStageX = 0;
var lastStageX2 = 0;
var lastStageY = 0;
var lastStageY2 = 0;
var progress = 0;
var isZooming = false;
this.stageX = 0;
this.stageY = 0;
this.ratio = 1;
this.mouseX = 0;
this.mouseY = 0;
this.isMouseDown = false;
/**
* Extract the local X position from a mouse event.
* @private
* @param {event} e A mouse event.
* @return {number} The local X value of the mouse.
*/
function getX(e) {
return e.offsetX != undefined && e.offsetX ||
e.layerX != undefined && e.layerX ||
e.clientX != undefined && e.clientX;
};
/**
* Extract the local Y position from a mouse event.
* @private
* @param {event} e A mouse event.
* @return {number} The local Y value of the mouse.
*/
function getY(e) {
return e.offsetY != undefined && e.offsetY ||
e.layerY != undefined && e.layerY ||
e.clientY != undefined && e.clientY;
};
/**
* Extract the wheel delta from a mouse event.
* @private
* @param {event} e A mouse event.
* @return {number} The wheel delta of the mouse.
*/
function getDelta(e) {
return e.wheelDelta != undefined && e.wheelDelta ||
e.detail != undefined && -e.detail;
};
/**
* The handler listening to the 'move' mouse event. It will set the mouseX
* and mouseY values as the mouse position values, prevent the default event,
* and dispatch a 'move' event.
* @private
* @param {event} event A 'move' mouse event.
*/
function moveHandler(event) {
oldMouseX = self.mouseX;
oldMouseY = self.mouseY;
self.mouseX = getX(event);
self.mouseY = getY(event);
self.isMouseDown && drag(event);
self.dispatch('move');
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
};
/**
* The handler listening to the 'up' mouse event. It will set the isMouseDown
* value as false, dispatch a 'mouseup' event, and trigger stopDrag().
* @private
* @param {event} event A 'up' mouse event.
*/
function upHandler(event) {
if (self.p.mouseEnabled && self.isMouseDown) {
self.isMouseDown = false;
self.dispatch('mouseup');
stopDrag();
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
}
};
/**
* The handler listening to the 'down' mouse event. It will set the
* isMouseDown value as true, dispatch a 'mousedown' event, and trigger
* startDrag().
* @private
* @param {event} event A 'down' mouse event.
*/
function downHandler(event) {
if (self.p.mouseEnabled) {
self.isMouseDown = true;
oldMouseX = self.mouseX;
oldMouseY = self.mouseY;
self.dispatch('mousedown');
startDrag();
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
}
};
/**
* The handler listening to the 'wheel' mouse event. It will trigger
* {@link startInterpolate} with the event delta as parameter.
* @private
* @param {event} event A 'wheel' mouse event.
*/
function wheelHandler(event) {
if (self.p.mouseEnabled) {
startInterpolate(
self.mouseX,
self.mouseY,
self.ratio * (getDelta(event) > 0 ?
self.p.zoomMultiply :
1 / self.p.zoomMultiply)
);
if (self.p['blockScroll']) {
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
}
}
};
/**
* Will start computing the scene X and Y, until {@link stopDrag} is
* triggered.
*/
function startDrag() {
oldStageX = self.stageX;
oldStageY = self.stageY;
startX = self.mouseX;
startY = self.mouseY;
lastStageX = self.stageX;
lastStageX2 = self.stageX;
lastStageY = self.stageY;
lastStageY2 = self.stageY;
self.dispatch('startdrag');
};
/**
* Stops computing the scene position.
*/
function stopDrag() {
if (oldStageX != self.stageX || oldStageY != self.stageY) {
startInterpolate(
self.stageX + self.p.inertia * (self.stageX - lastStageX2),
self.stageY + self.p.inertia * (self.stageY - lastStageY2)
);
}
};
/**
* Computes the position of the scene, relatively to the mouse position, and
* dispatches a "drag" event.
*/
function drag() {
var newStageX = self.mouseX - startX + oldStageX;
var newStageY = self.mouseY - startY + oldStageY;
if (newStageX != self.stageX || newStageY != self.stageY) {
lastStageX2 = lastStageX;
lastStageY2 = lastStageY;
lastStageX = newStageX;
lastStageY = newStageY;
self.stageX = newStageX;
self.stageY = newStageY;
self.dispatch('drag');
}
};
/**
* Will start computing the scene zoom ratio, until {@link stopInterpolate} is
* triggered.
* @param {number} x The new stage X.
* @param {number} y The new stage Y.
* @param {number} ratio The new zoom ratio.
*/
function startInterpolate(x, y, ratio) {
if (self.isMouseDown) {
return;
}
window.clearInterval(self.interpolationID);
isZooming = ratio != undefined;
oldStageX = self.stageX;
targetStageX = x;
oldStageY = self.stageY;
targetStageY = y;
oldRatio = self.ratio;
targetRatio = ratio || self.ratio;
targetRatio = Math.min(
Math.max(targetRatio, self.p.minRatio),
self.p.maxRatio
);
progress =
self.p.directZooming ?
1 - (isZooming ? self.p.zoomDelta : self.p.dragDelta) :
0;
if (
self.ratio != targetRatio ||
self.stageX != targetStageX ||
self.stageY != targetStageY
) {
interpolate();
self.interpolationID = window.setInterval(interpolate, 50);
self.dispatch('startinterpolate');
}
};
/**
* Stops the move interpolation.
*/
function stopInterpolate() {
var oldRatio = self.ratio;
if (isZooming) {
self.ratio = targetRatio;
self.stageX = targetStageX +
(self.stageX - targetStageX) *
self.ratio /
oldRatio;
self.stageY = targetStageY +
(self.stageY - targetStageY) *
self.ratio /
oldRatio;
}else {
self.stageX = targetStageX;
self.stageY = targetStageY;
}
self.dispatch('stopinterpolate');
};
/**
* Computes the interpolate ratio and the position of the scene, relatively
* to the last mouse event delta received, and dispatches a "interpolate"
* event.
*/
function interpolate() {
progress += (isZooming ? self.p.zoomDelta : self.p.dragDelta);
progress = Math.min(progress, 1);
var k = sigma.easing.quadratic.easeout(progress);
var oldRatio = self.ratio;
self.ratio = oldRatio * (1 - k) + targetRatio * k;
if (isZooming) {
self.stageX = targetStageX +
(self.stageX - targetStageX) *
self.ratio /
oldRatio;
self.stageY = targetStageY +
(self.stageY - targetStageY) *
self.ratio /
oldRatio;
} else {
self.stageX = oldStageX * (1 - k) + targetStageX * k;
self.stageY = oldStageY * (1 - k) + targetStageY * k;
}
self.dispatch('interpolate');
if (progress >= 1) {
window.clearInterval(self.interpolationID);
stopInterpolate();
}
};
/**
* Checks that there is always a part of the graph that is displayed, to
* avoid the user to drag the graph out of the stage.
* @param {Object} b An object containing the borders of the graph.
* @param {number} width The width of the stage.
* @param {number} height The height of the stage.
* @return {MouseCaptor} Returns itself.
*/
function checkBorders(b, width, height) {
// TODO : Find the good formula
/*if (!isNaN(b.minX) && !isNaN(b.maxX)) {
self.stageX = Math.min(
self.stageX = Math.max(
self.stageX,
(b.minX - width) * self.ratio +
self.p.marginRatio*(b.maxX - b.minX)
),
(b.maxX - width) * self.ratio +
width -
self.p.marginRatio*(b.maxX - b.minX)
);
}
if (!isNaN(b.minY) && !isNaN(b.maxY)) {
self.stageY = Math.min(
self.stageY = Math.max(
self.stageY,
(b.minY - height) * self.ratio +
self.p.marginRatio*(b.maxY - b.minY)
),
(b.maxY - height) * self.ratio +
height -
self.p.marginRatio*(b.maxY - b.minY)
);
}*/
return self;
};
// ADD CALLBACKS
dom.addEventListener('DOMMouseScroll', wheelHandler, true);
dom.addEventListener('mousewheel', wheelHandler, true);
dom.addEventListener('mousemove', moveHandler, true);
dom.addEventListener('mousedown', downHandler, true);
document.addEventListener('mouseup', upHandler, true);
this.checkBorders = checkBorders;
this.interpolate = startInterpolate;
}
/**
* This class draws the graph on the different canvas DOM elements. It just
* contains all the different methods to draw the graph, synchronously or
* pseudo-asynchronously.
* @constructor
* @param {CanvasRenderingContext2D} nodesCtx Context dedicated to draw nodes.
* @param {CanvasRenderingContext2D} edgesCtx Context dedicated to draw edges.
* @param {CanvasRenderingContext2D} labelsCtx Context dedicated to draw
* labels.
* @param {CanvasRenderingContext2D} hoverCtx Context dedicated to draw hover
* nodes labels.
* @param {Graph} graph A reference to the graph to
* draw.
* @param {number} w The width of the DOM root
* element.
* @param {number} h The width of the DOM root
* element.
* @extends sigma.classes.Cascade
* @this {Plotter}
*/
function Plotter(nodesCtx, edgesCtx, labelsCtx, hoverCtx, graph, w, h) {
sigma.classes.Cascade.call(this);
/**
* Represents "this", without the well-known scope issue.
* @private
* @type {Plotter}
*/
var self = this;
/**
* The different parameters that define how this instance should work.
* @see sigma.classes.Cascade
* @type {Object}
*/
this.p = {
// -------
// LABELS:
// -------
// Label color:
// - 'node'
// - default (then defaultLabelColor
// will be used instead)
labelColor: 'default',
defaultLabelColor: '#000',
// Label hover background color:
// - 'node'
// - default (then defaultHoverLabelBGColor
// will be used instead)
labelHoverBGColor: 'default',
defaultHoverLabelBGColor: '#fff',
// Label hover shadow:
labelHoverShadow: true,
labelHoverShadowColor: '#000',
// Label hover color:
// - 'node'
// - default (then defaultLabelHoverColor
// will be used instead)
labelHoverColor: 'default',
defaultLabelHoverColor: '#000',
// Label active background color:
// - 'node'
// - default (then defaultActiveLabelBGColor
// will be used instead)
labelActiveBGColor: 'default',
defaultActiveLabelBGColor: '#fff',
// Label active shadow:
labelActiveShadow: true,
labelActiveShadowColor: '#000',
// Label active color:
// - 'node'
// - default (then defaultLabelActiveColor
// will be used instead)
labelActiveColor: 'default',
defaultLabelActiveColor: '#000',
// Label size:
// - 'fixed'
// - 'proportional'
// Label size:
// - 'fixed'
// - 'proportional'
labelSize: 'fixed',
defaultLabelSize: 12, // for fixed display only
labelSizeRatio: 2, // for proportional display only
labelThreshold: 6,
font: 'Arial',
hoverFont: '',
activeFont: '',
fontStyle: '',
hoverFontStyle: '',
activeFontStyle: '',
// ------
// EDGES:
// ------
// Edge color:
// - 'source'
// - 'target'
// - default (then defaultEdgeColor or edge['color']
// will be used instead)
edgeColor: 'source',
defaultEdgeColor: '#aaa',
defaultEdgeType: 'line',
// ------
// NODES:
// ------
defaultNodeColor: '#aaa',
// HOVER:
// Node hover color:
// - 'node'
// - default (then defaultNodeHoverColor
// will be used instead)
nodeHoverColor: 'node',
defaultNodeHoverColor: '#fff',
// ACTIVE:
// Node active color:
// - 'node'
// - default (then defaultNodeActiveColor
// will be used instead)
nodeActiveColor: 'node',
defaultNodeActiveColor: '#fff',
// Node border color:
// - 'node'
// - default (then defaultNodeBorderColor
// will be used instead)
borderSize: 0,
nodeBorderColor: 'node',
defaultNodeBorderColor: '#fff',
// --------
// PROCESS:
// --------
edgesSpeed: 200,
nodesSpeed: 200,
labelsSpeed: 200
};
/**
* The canvas context dedicated to draw the nodes.
* @type {CanvasRenderingContext2D}
*/
var nodesCtx = nodesCtx;
/**
* The canvas context dedicated to draw the edges.
* @type {CanvasRenderingContext2D}
*/
var edgesCtx = edgesCtx;
/**
* The canvas context dedicated to draw the labels.
* @type {CanvasRenderingContext2D}
*/
var labelsCtx = labelsCtx;
/**
* The canvas context dedicated to draw the hover nodes.
* @type {CanvasRenderingContext2D}
*/
var hoverCtx = hoverCtx;
/**
* A reference to the graph to draw.
* @type {Graph}
*/
var graph = graph;
/**
* The width of the stage to draw on.
* @type {number}
*/
var width = w;
/**
* The height of the stage to draw on.
* @type {number}
*/
var height = h;
/**
* The index of the next edge to draw.
* @type {number}
*/
this.currentEdgeIndex = 0;
/**
* The index of the next node to draw.
* @type {number}
*/
this.currentNodeIndex = 0;
/**
* The index of the next label to draw.
* @type {number}
*/
this.currentLabelIndex = 0;
/**
* An atomic function to drawn the N next edges, with N as edgesSpeed.
* The counter is {@link this.currentEdgeIndex}.
* This function has been designed to work with {@link sigma.chronos}, that
* will insert frames at the middle of the calls, to make the edges drawing
* process fluid for the user.
* @see sigma.chronos
* @return {boolean} Returns true if all the edges are drawn and false else.
*/
function task_drawEdge() {
var c = graph.edges.length;
var s, t, i = 0;
while (i++< self.p.edgesSpeed && self.currentEdgeIndex < c) {
e = graph.edges[self.currentEdgeIndex];
s = e['source'];
t = e['target'];
if (e['hidden'] ||
s['hidden'] ||
t['hidden'] ||
(!self.isOnScreen(s) && !self.isOnScreen(t))) {
self.currentEdgeIndex++;
}else {
drawEdge(graph.edges[self.currentEdgeIndex++]);
}
}
return self.currentEdgeIndex < c;
};
/**
* An atomic function to drawn the N next nodes, with N as nodesSpeed.
* The counter is {@link this.currentEdgeIndex}.
* This function has been designed to work with {@link sigma.chronos}, that
* will insert frames at the middle of the calls, to make the nodes drawing
* process fluid for the user.
* @see sigma.chronos
* @return {boolean} Returns true if all the nodes are drawn and false else.
*/
function task_drawNode() {
var c = graph.nodes.length;
var i = 0;
while (i++< self.p.nodesSpeed && self.currentNodeIndex < c) {
if (!self.isOnScreen(graph.nodes[self.currentNodeIndex])) {
self.currentNodeIndex++;
}else {
drawNode(graph.nodes[self.currentNodeIndex++]);
}
}
return self.currentNodeIndex < c;
};
/**
* An atomic function to drawn the N next labels, with N as labelsSpeed.
* The counter is {@link this.currentEdgeIndex}.
* This function has been designed to work with {@link sigma.chronos}, that
* will insert frames at the middle of the calls, to make the labels drawing
* process fluid for the user.
* @see sigma.chronos
* @return {boolean} Returns true if all the labels are drawn and false else.
*/
function task_drawLabel() {
var c = graph.nodes.length;
var i = 0;
while (i++< self.p.labelsSpeed && self.currentLabelIndex < c) {
if (!self.isOnScreen(graph.nodes[self.currentLabelIndex])) {
self.currentLabelIndex++;
}else {
drawLabel(graph.nodes[self.currentLabelIndex++]);
}
}
return self.currentLabelIndex < c;
};
/**
* Draws one node to the corresponding canvas.
* @param {Object} node The node to draw.
* @return {Plotter} Returns itself.
*/
function drawNode(node) {
var size = Math.round(node['displaySize'] * 10) / 10;
var ctx = nodesCtx;
ctx.fillStyle = node['color'];
ctx.beginPath();
ctx.arc(node['displayX'],
node['displayY'],
size,
0,
Math.PI * 2,
true);
ctx.closePath();
ctx.fill();
node['hover'] && drawHoverNode(node);
return self;
};
/**
* Draws one edge to the corresponding canvas.
* @param {Object} edge The edge to draw.
* @return {Plotter} Returns itself.
*/
function drawEdge(edge) {
var x1 = edge['source']['displayX'];
var y1 = edge['source']['displayY'];
var x2 = edge['target']['displayX'];
var y2 = edge['target']['displayY'];
var color = edge['color'];
if (!color) {
switch (self.p.edgeColor) {
case 'source':
color = edge['source']['color'] ||
self.p.defaultNodeColor;
break;
case 'target':
color = edge['target']['color'] ||
self.p.defaultNodeColor;
break;
default:
color = self.p.defaultEdgeColor;
break;
}
}
var ctx = edgesCtx;
switch (edge['type'] || self.p.defaultEdgeType) {
case 'curve':
ctx.strokeStyle = color;
ctx.lineWidth = edge['displaySize'] / 3;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.quadraticCurveTo((x1 + x2) / 2 + (y2 - y1) / 4,
(y1 + y2) / 2 + (x1 - x2) / 4,
x2,
y2);
ctx.stroke();
break;
case 'line':
default:
ctx.strokeStyle = color;
ctx.lineWidth = edge['displaySize'] / 3;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
break;
}
return self;
};
/**
* Draws one label to the corresponding canvas.
* @param {Object} node The label to draw.
* @return {Plotter} Returns itself.
*/
function drawLabel(node) {
var ctx = labelsCtx;
if (node['displaySize'] >= self.p.labelThreshold || node['forceLabel']) {
var fontSize = self.p.labelSize == 'fixed' ?
self.p.defaultLabelSize :
self.p.labelSizeRatio * node['displaySize'];
ctx.font = self.p.fontStyle + fontSize + 'px ' + self.p.font;
ctx.fillStyle = self.p.labelColor == 'node' ?
(node['color'] || self.p.defaultNodeColor) :
self.p.defaultLabelColor;
ctx.fillText(
node['label'],
Math.round(node['displayX'] + node['displaySize'] * 1.5),
Math.round(node['displayY'] + fontSize / 2 - 3)
);
}
return self;
};
/**
* Draws one hover node to the corresponding canvas.
* @param {Object} node The hover node to draw.
* @return {Plotter} Returns itself.
*/
function drawHoverNode(node) {
var ctx = hoverCtx;
var fontSize = self.p.labelSize == 'fixed' ?
self.p.defaultLabelSize :
self.p.labelSizeRatio * node['displaySize'];
ctx.font = (self.p.hoverFontStyle || self.p.fontStyle || '') + ' ' +
fontSize + 'px ' +
(self.p.hoverFont || self.p.font || '');
ctx.fillStyle = self.p.labelHoverBGColor == 'node' ?
(node['color'] || self.p.defaultNodeColor) :
self.p.defaultHoverLabelBGColor;
// Label background:
ctx.beginPath();
if (self.p.labelHoverShadow) {
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.shadowBlur = 4;
ctx.shadowColor = self.p.labelHoverShadowColor;
}
sigma.tools.drawRoundRect(
ctx,
Math.round(node['displayX'] - fontSize / 2 - 2),
Math.round(node['displayY'] - fontSize / 2 - 2),
Math.round(ctx.measureText(node['label']).width +
node['displaySize'] * 1.5 +
fontSize / 2 + 4),
Math.round(fontSize + 4),
Math.round(fontSize / 2 + 2),
'left'
);
ctx.closePath();
ctx.fill();
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.shadowBlur = 0;
// Node border:
ctx.beginPath();
ctx.fillStyle = self.p.nodeBorderColor == 'node' ?
(node['color'] || self.p.defaultNodeColor) :
self.p.defaultNodeBorderColor;
ctx.arc(Math.round(node['displayX']),
Math.round(node['displayY']),
node['displaySize'] + self.p.borderSize,
0,
Math.PI * 2,
true);
ctx.closePath();
ctx.fill();
// Node:
ctx.beginPath();
ctx.fillStyle = self.p.nodeHoverColor == 'node' ?
(node['color'] || self.p.defaultNodeColor) :
self.p.defaultNodeHoverColor;
ctx.arc(Math.round(node['displayX']),
Math.round(node['displayY']),
node['displaySize'],
0,
Math.PI * 2,
true);
ctx.closePath();
ctx.fill();
// Label:
ctx.fillStyle = self.p.labelHoverColor == 'node' ?
(node['color'] || self.p.defaultNodeColor) :
self.p.defaultLabelHoverColor;
ctx.fillText(
node['label'],
Math.round(node['displayX'] + node['displaySize'] * 1.5),
Math.round(node['displayY'] + fontSize / 2 - 3)
);
return self;
};
/**
* Draws one active node to the corresponding canvas.
* @param {Object} node The active node to draw.
* @return {Plotter} Returns itself.
*/
function drawActiveNode(node) {
var ctx = hoverCtx;
if (!isOnScreen(node)) {
return self;
}
var fontSize = self.p.labelSize == 'fixed' ?
self.p.defaultLabelSize :
self.p.labelSizeRatio * node['displaySize'];
ctx.font = (self.p.activeFontStyle || self.p.fontStyle || '') + ' ' +
fontSize + 'px ' +
(self.p.activeFont || self.p.font || '');
ctx.fillStyle = self.p.labelHoverBGColor == 'node' ?
(node['color'] || self.p.defaultNodeColor) :
self.p.defaultActiveLabelBGColor;
// Label background:
ctx.beginPath();
if (self.p.labelActiveShadow) {
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.shadowBlur = 4;
ctx.shadowColor = self.p.labelActiveShadowColor;
}
sigma.tools.drawRoundRect(
ctx,
Math.round(node['displayX'] - fontSize / 2 - 2),
Math.round(node['displayY'] - fontSize / 2 - 2),
Math.round(ctx.measureText(node['label']).width +
node['displaySize'] * 1.5 +
fontSize / 2 + 4),
Math.round(fontSize + 4),
Math.round(fontSize / 2 + 2),
'left'
);
ctx.closePath();
ctx.fill();
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.shadowBlur = 0;
// Node border:
ctx.beginPath();
ctx.fillStyle = self.p.nodeBorderColor == 'node' ?
(node['color'] || self.p.defaultNodeColor) :
self.p.defaultNodeBorderColor;
ctx.arc(Math.round(node['displayX']),
Math.round(node['displayY']),
node['displaySize'] + self.p.borderSize,
0,
Math.PI * 2,
true);
ctx.closePath();
ctx.fill();
// Node:
ctx.beginPath();
ctx.fillStyle = self.p.nodeActiveColor == 'node' ?
(node['color'] || self.p.defaultNodeColor) :
self.p.defaultNodeActiveColor;
ctx.arc(Math.round(node['displayX']),
Math.round(node['displayY']),
node['displaySize'],
0,
Math.PI * 2,
true);
ctx.closePath();
ctx.fill();
// Label:
ctx.fillStyle = self.p.labelActiveColor == 'node' ?
(node['color'] || self.p.defaultNodeColor) :
self.p.defaultLabelActiveColor;
ctx.fillText(
node['label'],
Math.round(node['displayX'] + node['displaySize'] * 1.5),
Math.round(node['displayY'] + fontSize / 2 - 3)
);
return self;
};
/**
* Determines if a node is on the screen or not. The limits here are
* bigger than the actual screen, to avoid seeing labels disappear during
* the graph manipulation.
* @param {Object} node The node to check if it is on or out the screen.
* @return {boolean} Returns false if the node is hidden or not on the screen
* or true else.
*/
function isOnScreen(node) {
if (isNaN(node['x']) || isNaN(node['y'])) {
throw (new Error('A node\'s coordinate is not a ' +
'number (id: ' + node['id'] + ')')
);
}
return !node['hidden'] &&
(node['displayX'] + node['displaySize'] > -width / 3) &&
(node['displayX'] - node['displaySize'] < width * 4 / 3) &&
(node['displayY'] + node['displaySize'] > -height / 3) &&
(node['displayY'] - node['displaySize'] < height * 4 / 3);
};
/**
* Resizes this instance.
* @param {number} w The new width.
* @param {number} h The new height.
* @return {Plotter} Returns itself.
*/
function resize(w, h) {
width = w;
height = h;
return self;
}
this.task_drawLabel = task_drawLabel;
this.task_drawEdge = task_drawEdge;
this.task_drawNode = task_drawNode;
this.drawActiveNode = drawActiveNode;
this.drawHoverNode = drawHoverNode;
this.isOnScreen = isOnScreen;
this.resize = resize;
}
/**
* Sigma is the main class. It represents the core of any instance id sigma.js.
* It is private and can be initialized only from inside sigma.js. To see its
* public interface, see {@link SigmaPublic}.
* It owns its own {@link Graph}, {@link MouseCaptor}, {@link Plotter}
* and {@link Monitor}.
* @constructor
* @extends sigma.classes.Cascade
* @extends sigma.classes.EventDispatcher
* @param {element} root The DOM root of this instance (a div, for example).
* @param {string} id The ID of this instance.
* @this {Sigma}
*/
function Sigma(root, id) {
sigma.classes.Cascade.call(this);
sigma.classes.EventDispatcher.call(this);
/**
* Represents "this", without the well-known scope issue.
* @private
* @type {Sigma}
*/
var self = this;
/**
* The ID of the instance.
* @type {string}
*/
this.id = id.toString();
/**
* The different parameters that define how this instance should work.
* @see sigma.classes.Cascade
* @type {Object}
*/
this.p = {
auto: true,
drawNodes: 2,
drawEdges: 1,
drawLabels: 2,
lastNodes: 2,
lastEdges: 0,
lastLabels: 2,
drawHoverNodes: true,
drawActiveNodes: true
};
/**
* The root DOM element of this instance, containing every other elements.
* @type {element}
*/
this.domRoot = root;
/**
* The width of this instance - initially, the root's width.
* @type {number}
*/
this.width = this.domRoot.offsetWidth;
/**
* The height of this instance - initially, the root's height.
* @type {number}
*/
this.height = this.domRoot.offsetHeight;
/**
* The graph of this instance - initiallyempty.
* @type {Graph}
*/
this.graph = new Graph();
/**
* An object referencing every DOM elements used by this instance.
* @type {Object}
*/
this.domElements = {};
initDOM('edges', 'canvas');
initDOM('nodes', 'canvas');
initDOM('labels', 'canvas');
initDOM('hover', 'canvas');
initDOM('monitor', 'div');
initDOM('mouse', 'canvas');
/**
* The class dedicated to manage the drawing process of the graph of the
* different canvas.
* @type {Plotter}
*/
this.plotter = new Plotter(
this.domElements.nodes.getContext('2d'),
this.domElements.edges.getContext('2d'),
this.domElements.labels.getContext('2d'),
this.domElements.hover.getContext('2d'),
this.graph,
this.width,
this.height
);
/**
* The class dedicated to monitor different probes about the running
* processes or the data, such as the number of nodes or edges, or how
* many times the graph is drawn per second.
* @type {Monitor}
*/
this.monitor = new Monitor(
this,
this.domElements.monitor
);
/**
* The class dedicated to manage the different mouse events.
* @type {MouseCaptor}
*/
this.mousecaptor = new MouseCaptor(
this.domElements.mouse,
this.id
);
// Interaction listeners:
this.mousecaptor.bind('drag interpolate', function(e) {
self.draw(
self.p.auto ? 2 : self.p.drawNodes,
self.p.auto ? 0 : self.p.drawEdges,
self.p.auto ? 2 : self.p.drawLabels,
true
);
}).bind('stopdrag stopinterpolate', function(e) {
self.draw(
self.p.auto ? 2 : self.p.drawNodes,
self.p.auto ? 1 : self.p.drawEdges,
self.p.auto ? 2 : self.p.drawLabels,
true
);
}).bind('mousedown mouseup', function(e) {
var targeted = self.graph.nodes.filter(function(n) {
return !!n['hover'];
}).map(function(n) {
return n.id;
});
self.dispatch(
e['type'] == 'mousedown' ?
'downgraph' :
'upgraph'
);
if (targeted.length) {
self.dispatch(
e['type'] == 'mousedown' ?
'downnodes' :
'upnodes',
targeted
);
}
}).bind('move', function() {
self.domElements.hover.getContext('2d').clearRect(
0,
0,
self.domElements.hover.width,
self.domElements.hover.height
);
drawHover();
drawActive();
});
sigma.chronos.bind('startgenerators', function() {
if (sigma.chronos.getGeneratorsIDs().some(function(id) {
return !!id.match(new RegExp('_ext_' + self.id + '$', ''));
})) {
self.draw(
self.p.auto ? 2 : self.p.drawNodes,
self.p.auto ? 0 : self.p.drawEdges,
self.p.auto ? 2 : self.p.drawLabels
);
}
}).bind('stopgenerators', function() {
self.draw();
});
/**
* Resizes the element, and redraws the graph with the last settings.
* @param {?number} w The new width (if undefined, it will use the root
* width).
* @param {?number} h The new height (if undefined, it will use the root
* height).
* @return {Sigma} Returns itself.
*/
function resize(w, h) {
var oldW = self.width, oldH = self.height;
if (w != undefined && h != undefined) {
self.width = w;
self.height = h;
}else {
self.width = self.domRoot.offsetWidth;
self.height = self.domRoot.offsetHeight;
}
if (oldW != self.width || oldH != self.height) {
for (var k in self.domElements) {
self.domElements[k].setAttribute('width', self.width + 'px');
self.domElements[k].setAttribute('height', self.height + 'px');
}
self.plotter.resize(self.width, self.height);
self.draw(
self.p.lastNodes,
self.p.lastEdges,
self.p.lastLabels,
true
);
}
return self;
};
/**
* Kills every drawing task currently running. Basically, it stops this
* instance's drawing process.
* @return {Sigma} Returns itself.
*/
function clearSchedule() {
sigma.chronos.removeTask(
'node_' + self.id, 2
).removeTask(
'edge_' + self.id, 2
).removeTask(
'label_' + self.id, 2
).stopTasks();
return self;
};
/**
* Initialize a DOM element, that will be stores by this instance, to make
* automatic these elements resizing.
* @private
* @param {string} id The element's ID.
* @param {string} type The element's nodeName (Example : canvas, div, ...).
* @return {Sigma} Returns itself.
*/
function initDOM(id, type) {
self.domElements[id] = document.createElement(type);
self.domElements[id].style.position = 'absolute';
self.domElements[id].setAttribute('id', 'sigma_' + id + '_' + self.id);
self.domElements[id].setAttribute('class', 'sigma_' + id + '_' + type);
self.domElements[id].setAttribute('width', self.width + 'px');
self.domElements[id].setAttribute('height', self.height + 'px');
self.domRoot.appendChild(self.domElements[id]);
return self;
};
/**
* Starts the graph drawing process. The three first parameters indicate
* how the different layers have to be drawn:
* . -1: The layer is not drawn, but it is not erased.
* . 0: The layer is not drawn.
* . 1: The layer is drawn progressively.
* . 2: The layer is drawn directly.
* @param {?number} nodes Determines if and how the nodes must be drawn.
* @param {?number} edges Determines if and how the edges must be drawn.
* @param {?number} labels Determines if and how the labels must be drawn.
* @param {?boolean} safe If true, nothing will happen if any generator
* affiliated to this instance is currently running
* (an iterative layout, for example).
* @return {Sigma} Returns itself.
*/
function draw(nodes, edges, labels, safe) {
if (safe && sigma.chronos.getGeneratorsIDs().some(function(id) {
return !!id.match(new RegExp('_ext_' + self.id + '$', ''));
})) {
return self;
}
var n = (nodes == undefined) ? self.p.drawNodes : nodes;
var e = (edges == undefined) ? self.p.drawEdges : edges;
var l = (labels == undefined) ? self.p.drawLabels : labels;
var params = {
nodes: n,
edges: e,
labels: l
};
self.p.lastNodes = n;
self.p.lastEdges = e;
self.p.lastLabels = l;
// Remove tasks:
clearSchedule();
// Rescale graph:
self.graph.rescale(
self.width,
self.height,
n > 0,
e > 0
).setBorders();
self.mousecaptor.checkBorders(
self.graph.borders,
self.width,
self.height
);
self.graph.translate(
self.mousecaptor.stageX,
self.mousecaptor.stageY,
self.mousecaptor.ratio,
n > 0,
e > 0
);
self.dispatch(
'graphscaled'
);
// Clear scene:
for (var k in self.domElements) {
if (
self.domElements[k].nodeName.toLowerCase() == 'canvas' &&
(params[k] == undefined || params[k] >= 0)
) {
self.domElements[k].getContext('2d').clearRect(
0,
0,
self.domElements[k].width,
self.domElements[k].height
);
}
}
self.plotter.currentEdgeIndex = 0;
self.plotter.currentNodeIndex = 0;
self.plotter.currentLabelIndex = 0;
var previous = null;
var start = false;
if (n) {
if (n > 1) {
while (self.plotter.task_drawNode()) {}
}else {
sigma.chronos.addTask(
self.plotter.task_drawNode,
'node_' + self.id,
false
);
start = true;
previous = 'node_' + self.id;
}
}
if (l) {
if (l > 1) {
while (self.plotter.task_drawLabel()) {}
} else {
if (previous) {
sigma.chronos.queueTask(
self.plotter.task_drawLabel,
'label_' + self.id,
previous
);
} else {
sigma.chronos.addTask(
self.plotter.task_drawLabel,
'label_' + self.id,
false
);
}
start = true;
previous = 'label_' + self.id;
}
}
if (e) {
if (e > 1) {
while (self.plotter.task_drawEdge()) {}
}else {
if (previous) {
sigma.chronos.queueTask(
self.plotter.task_drawEdge,
'edge_' + self.id,
previous
);
}else {
sigma.chronos.addTask(
self.plotter.task_drawEdge,
'edge_' + self.id,
false
);
}
start = true;
previous = 'edge_' + self.id;
}
}
self.dispatch(
'draw'
);
self.refresh();
start && sigma.chronos.runTasks();
return self;
};
/**
* Draws the hover and active nodes labels.
* @return {Sigma} Returns itself.
*/
function refresh() {
self.domElements.hover.getContext('2d').clearRect(
0,
0,
self.domElements.hover.width,
self.domElements.hover.height
);
drawHover();
drawActive();
return self;
}
/**
* Draws the hover nodes labels. This method is applied directly, and does
* not use the pseudo-asynchronous tasks process.
* @return {Sigma} Returns itself.
*/
function drawHover() {
if (self.p.drawHoverNodes) {
self.graph.checkHover(
self.mousecaptor.mouseX,
self.mousecaptor.mouseY
);
self.graph.nodes.forEach(function(node) {
if (node.hover && !node.active) {
self.plotter.drawHoverNode(node);
}
});
}
return self;
}
/**
* Draws the active nodes labels. This method is applied directly, and does
* not use the pseudo-asynchronous tasks process.
* @return {Sigma} Returns itself.
*/
function drawActive() {
if (self.p.drawActiveNodes) {
self.graph.nodes.forEach(function(node) {
if (node.active) {
self.plotter.drawActiveNode(node);
}
});
}
return self;
}
// Apply plugins:
for (var i = 0; i < local.plugins.length; i++) {
local.plugins[i](this);
}
this.draw = draw;
this.resize = resize;
this.refresh = refresh;
this.drawHover = drawHover;
this.drawActive = drawActive;
this.clearSchedule = clearSchedule;
window.addEventListener('resize', function() {
self.resize();
});
}
function SigmaPublic(sigmaInstance) {
var s = sigmaInstance;
var self = this;
sigma.classes.EventDispatcher.call(this);
this._core = sigmaInstance;
this.kill = function() {
// TODO
};
this.getID = function() {
return s.id;
};
// Config:
this.configProperties = function(a1, a2) {
var res = s.config(a1, a2);
return res == s ? self : res;
};
this.drawingProperties = function(a1, a2) {
var res = s.plotter.config(a1, a2);
return res == s.plotter ? self : res;
};
this.mouseProperties = function(a1, a2) {
var res = s.mousecaptor.config(a1, a2);
return res == s.mousecaptor ? self : res;
};
this.graphProperties = function(a1, a2) {
var res = s.graph.config(a1, a2);
return res == s.graph ? self : res;
};
this.getMouse = function() {
return {
mouseX: s.mousecaptor.mouseX,
mouseY: s.mousecaptor.mouseY,
down: s.mousecaptor.isMouseDown
};
};
// Actions:
this.position = function(stageX, stageY, ratio) {
if (arguments.length == 0) {
return {
stageX: s.mousecaptor.stageX,
stageY: s.mousecaptor.stageY,
ratio: s.mousecaptor.ratio
};
}else {
s.mousecaptor.stageX = stageX != undefined ?
stageX :
s.mousecaptor.stageX;
s.mousecaptor.stageY = stageY != undefined ?
stageY :
s.mousecaptor.stageY;
s.mousecaptor.ratio = ratio != undefined ?
ratio :
s.mousecaptor.ratio;
return self;
}
};
this.goTo = function(stageX, stageY, ratio) {
s.mousecaptor.interpolate(stageX, stageY, ratio);
return self;
};
this.zoomTo = function(x, y, ratio) {
ratio = Math.min(
Math.max(s.mousecaptor.config('minRatio'), ratio),
s.mousecaptor.config('maxRatio')
);
if (ratio == s.mousecaptor.ratio) {
s.mousecaptor.interpolate(
x - s.width / 2 + s.mousecaptor.stageX,
y - s.height / 2 + s.mousecaptor.stageY
);
}else {
s.mousecaptor.interpolate(
(ratio * x - s.mousecaptor.ratio * s.width/2) /
(ratio - s.mousecaptor.ratio),
(ratio * y - s.mousecaptor.ratio * s.height/2) /
(ratio - s.mousecaptor.ratio),
ratio
);
}
return self;
};
this.resize = function(w, h) {
s.resize(w, h);
return self;
};
this.draw = function(nodes, edges, labels, safe) {
s.draw(nodes, edges, labels, safe);
return self;
};
this.refresh = function() {
s.refresh();
return self;
};
// Tasks methods:
this.addGenerator = function(id, task, condition) {
sigma.chronos.addGenerator(id + '_ext_' + s.id, task, condition);
return self;
};
this.removeGenerator = function(id) {
sigma.chronos.removeGenerator(id + '_ext_' + s.id);
return self;
};
// Graph methods:
this.addNode = function(id, params) {
s.graph.addNode(id, params);
return self;
};
this.addEdge = function(id, source, target, params) {
s.graph.addEdge(id, source, target, params);
return self;
}
this.dropNode = function(v) {
s.graph.dropNode(v);
return self;
};
this.dropEdge = function(v) {
s.graph.dropEdge(v);
return self;
};
this.pushGraph = function(object, safe) {
object.nodes && object.nodes.forEach(function(node) {
node['id'] && (!safe || !s.graph.nodesIndex[node['id']]) &&
self.addNode(node['id'], node);
});
var isEdgeValid;
object.edges && object.edges.forEach(function(edge) {
validID = edge['source'] && edge['target'] && edge['id'];
validID &&
(!safe || !s.graph.edgesIndex[edge['id']]) &&
self.addNode(
edge['id'],
edge['source'],
edge['target'],
edge
);
});
return self;
};
this.emptyGraph = function() {
s.graph.empty();
return self;
};
this.getNodesCount = function() {
return s.graph.nodes.length;
};
this.getEdgesCount = function() {
return s.graph.edges.length;
};
this.iterNodes = function(fun, ids) {
s.graph.iterNodes(fun, ids);
return self;
};
this.iterEdges = function(fun, ids) {
s.graph.iterEdges(fun, ids);
return self;
};
this.getNodes = function(ids) {
return s.graph.getNodes(ids);
};
this.getEdges = function(ids) {
return s.graph.getEdges(ids);
};
// Monitoring
this.activateMonitoring = function() {
return s.monitor.activate();
};
this.desactivateMonitoring = function() {
return s.monitor.desactivate();
};
// Events
s.bind('downnodes upnodes downgraph upgraph', function(e) {
self.dispatch(e.type, e.content);
});
s.graph.bind('overnodes outnodes', function(e) {
self.dispatch(e.type, e.content);
});
}
/**
* Add a function to the prototype of SigmaPublic, but with access to the
* Sigma class properties.
* @param {string} pluginName [description].
* @param {function} caller [description].
* @param {function(Sigma)} launcher [description].
*/
sigma.addPlugin = function(pluginName, caller, launcher) {
SigmaPublic.prototype[pluginName] = caller;
local.plugins.push(launcher);
};
/**
* sigma.chronos manages frames insertion to simulate asynchronous computing.
* It has been designed to make possible to execute heavy computing tasks
* for the browser, without freezing it.
* @constructor
* @extends sigma.classes.Cascade
* @extends sigma.classes.EventDispatcher
* @this {sigma.chronos}
*/
sigma.chronos = new (function() {
sigma.classes.EventDispatcher.call(this);
/**
* Represents "this", without the well-known scope issue.
* @private
* @type {sigma.chronos}
*/
var self = this;
/**
* Indicates whether any task is actively running or not.
* @private
* @type {boolean}
*/
var isRunning = false;
/**
* Indicates the FPS "goal", that will define the theoretical
* frame length.
* @private
* @type {number}
*/
var fpsReq = 80;
/**
* Stores the last computed FPS value (FPS is computed only when any
* task is running).
* @private
* @type {number}
*/
var lastFPS = 0;
/**
* The number of frames inserted since the last start.
* @private
* @type {number}
*/
var framesCount = 0;
/**
* The theoretical frame time.
* @private
* @type {number}
*/
var frameTime = 1000 / fpsReq;
/**
* The theoretical frame length, minus the last measured delay.
* @private
* @type {number}
*/
var correctedFrameTime = frameTime;
/**
* The measured length of the last frame.
* @private
* @type {number}
*/
var effectiveTime = 0;
/**
* The time passed since the last runTasks action.
* @private
* @type {number}
*/
var currentTime = 0;
/**
* The time when the last frame was inserted.
* @private
* @type {number}
*/
var startTime = 0;
/**
* The difference between the theoretical frame length and the
* last measured frame length.
* @private
* @type {number}
*/
var delay = 0;
/**
* The container of all active generators.
* @private
* @type {Object.<string, Object>}
*/
var generators = {};
/**
* The array of all the referenced and active tasks.
* @private
* @type {Array.<Object>}
*/
var tasks = [];
/**
* The array of all the referenced and queued tasks.
* @private
* @type {Array.<Object>}
*/
var queuedTasks = [];
/**
* The index of the next task to execute.
* @private
* @type {number}
*/
var taskIndex = 0;
/**
* Inserts a frame before executing the callback.
* @param {function()} callback The callback to execute after having
* inserted the frame.
* @return {sigma.chronos} Returns itself.
*/
function insertFrame(callback) {
window.setTimeout(callback, 0);
return self;
}
/**
* The local method that executes routine, and inserts frames when needed.
* It dispatches a "frameinserted" event after having inserted any frame,
* and an "insertframe" event before.
* @private
*/
function frameInserter() {
self.dispatch('frameinserted');
while (isRunning && tasks.length && routine()) {}
if (!isRunning || !tasks.length) {
stopTasks();
} else {
startTime = (new Date()).getTime();
framesCount++;
delay = effectiveTime - frameTime;
correctedFrameTime = frameTime - delay;
self.dispatch('insertframe');
insertFrame(frameInserter);
}
};
/**
* The local method that executes the tasks, and compares the current frame
* length to the ideal frame length.
* @private
* @return {boolean} Returns false if the current frame should be ended,
* and true else.
*/
function routine() {
taskIndex = taskIndex % tasks.length;
if (!tasks[taskIndex].task()) {
var n = tasks[taskIndex].taskName;
queuedTasks = queuedTasks.filter(function(e) {
(e.taskParent == n) && tasks.push({
taskName: e.taskName,
task: e.task
});
return e.taskParent != n;
});
self.dispatch('killed', tasks.splice(taskIndex--, 1)[0]);
}
taskIndex++;
effectiveTime = (new Date()).getTime() - startTime;
return effectiveTime <= correctedFrameTime;
};
/**
* Starts tasks execution.
* @return {sigma.chronos} Returns itself.
*/
function runTasks() {
isRunning = true;
taskIndex = 0;
framesCount = 0;
startTime = (new Date()).getTime();
currentTime = startTime;
self.dispatch('start');
self.dispatch('insertframe');
insertFrame(frameInserter);
return self;
};
/**
* Stops tasks execution, and dispatch a "stop" event.
* @return {sigma.chronos} Returns itself.
*/
function stopTasks() {
self.dispatch('stop');
isRunning = false;
return self;
};
/**
* A task is a function that will be executed continuously while it returns
* true. As soon as it return false, the task will be removed.
* If several tasks are present, they will be executed in parallele.
* This method will add the task to this execution process.
* @param {function(): boolean} task The task to add.
* @param {string} name The name of the worker, used for
* managing the different tasks.
* @param {boolean} autostart If true, sigma.chronos will start
* automatically if it is not working
* yet.
* @return {sigma.chronos} Returns itself.
*/
function addTask(task, name, autostart) {
if (typeof task != 'function') {
throw new Error('Task "' + name + '" is not a function');
}
tasks.push({
taskName: name,
task: task
});
isRunning = !!(isRunning || (autostart && runTasks()) || true);
return self;
};
/**
* Will add a task that will be start to be executed as soon as a task
* named as the parent will be removed.
* @param {function(): boolean} task The task to add.
* @param {string} name The name of the worker, used for
* managing the different tasks.
* @param {string} parent The name of the parent task.
* @return {sigma.chronos} Returns itself.
*/
function queueTask(task, name, parent) {
if (typeof task != 'function') {
throw new Error('Task "' + name + '" is not a function');
}
if (!tasks.concat(queuedTasks).some(function(e) {
return e.taskName == parent;
})) {
throw new Error(
'Parent task "' + parent + '" of "' + name + '" is not attached.'
);
}
queuedTasks.push({
taskParent: parent,
taskName: name,
task: task
});
return self;
};
/**
* Removes a task.
* @param {string} v If v is undefined, then every tasks will
* be removed. If not, each task named v will
* be removed.
* @param {number} queueStatus Determines the queued tasks behaviour. If 0,
* then nothing will happen. If 1, the tasks
* queued to any removed task will be triggered.
* If 2, the tasks queued to any removed task
* will be removed as well.
* @return {sigma.chronos} Returns itself.
*/
function removeTask(v, queueStatus) {
if (v == undefined) {
tasks = [];
if (queueStatus == 1) {
queuedTasks = [];
}else if (queueStatus == 2) {
tasks = queuedTasks;
queuedTasks = [];
}
stopTasks();
} else {
var n = (typeof v == 'string') ? v : '';
tasks = tasks.filter(function(e) {
if ((typeof v == 'string') ? e.taskName == v : e.task == v) {
n = e.taskName;
return false;
}
return true;
});
if (queueStatus > 0) {
queuedTasks = queuedTasks.filter(function(e) {
if (queueStatus == 1 && e.taskParent == n) {
tasks.push(e);
}
return e.taskParent != n;
});
}
}
isRunning = !!(!tasks.length || (stopTasks() && false));
return self;
};
/**
* A generator is a pair task/condition. The task will be executed
* while it returns true.
* When it returns false, the condition will be tested. If
* the condition returns true, the task will be executed
* again at the next process iteration. If not, the generator
* is removed.
* If several generators are present, they will be executed one
* by one: When the first stops, the second will start, etc. When
* they are all ended, then the conditions will be tested to know
* which generators have to be started again.
* @param {string} id The generators ID.
* @param {function(): boolean} task The generator's task.
* @param {function(): boolean} condition The generator's condition.
* @return {sigma.chronos} Returns itself.
*/
function addGenerator(id, task, condition) {
if (generators[id] != undefined) {
return self;
}
generators[id] = {
task: task,
condition: condition
};
getGeneratorsCount(true) == 0 && startGenerators();
return self;
};
/**
* Removes a generator. It means that the task will continue being eecuted
* until it returns false, but then the
* condition will not be tested.
* @param {string} id The generator's ID.
* @return {sigma.chronos} Returns itself.
*/
function removeGenerator(id) {
if (generators[id]) {
generators[id].on = false;
generators[id].del = true;
}
return self;
};
/**
* Returns the number of generators.
* @private
* @param {boolean} running If true, returns the number of active
* generators instead.
* @return {sigma.chronos} Returns itself.
*/
function getGeneratorsCount(running) {
return running ?
Object.keys(generators).filter(function(id) {
return !!generators[id].on;
}).length :
Object.keys(generators).length;
};
/**
* Returns the array of the generators IDs.
* @return {array.<string>} The array of IDs.
*/
function getGeneratorsIDs() {
return Object.keys(generators);
}
/**
* startGenerators is the method that manages which generator
* is the next to start when another one stops. It will dispatch
* a "stopgenerators" event if there is no more generator to start,
* and a "startgenerators" event else.
* @return {sigma.chronos} Returns itself.
*/
function startGenerators() {
if (!Object.keys(generators).length) {
self.dispatch('stopgenerators');
}else {
self.dispatch('startgenerators');
self.unbind('killed', onTaskEnded);
insertFrame(function() {
for (var k in generators) {
generators[k].on = true;
addTask(
generators[k].task,
k,
false
);
}
});
self.bind('killed', onTaskEnded).runTasks();
}
return self;
};
/**
* A callback triggered everytime the task of a generator stops, that will
* test the related generator's condition, and see if there is still any
* generator to start.
* @private
* @param {Object} e The sigma.chronos "killed" event.
*/
function onTaskEnded(e) {
if (generators[e['content'].taskName] != undefined) {
if (generators[e['content'].taskName].del ||
!generators[e['content'].taskName].condition()) {
delete generators[e['content'].taskName];
}else {
generators[e['content'].taskName].on = false;
}
if (getGeneratorsCount(true) == 0) {
startGenerators();
}
}
};
/**
* Either set or returns the fpsReq property. This property determines
* the number of frames that should be inserted per second.
* @param {?number} v The frequency asked.
* @return {(Chronos|number)} Returns the frequency if v is undefined, and
* itself else.
*/
function frequency(v) {
if (v != undefined) {
fpsReq = Math.abs(1 * v);
frameTime = 1000 / fpsReq;
framesCount = 0;
return self;
} else {
return fpsReq;
}
};
/**
* Returns the actual average number of frames that are inserted per
* second.
* @return {number} The actual average FPS.
*/
function getFPS() {
if (isRunning) {
lastFPS =
Math.round(
framesCount /
((new Date()).getTime() - currentTime) *
10000
) / 10;
}
return lastFPS;
};
/**
* Returns the number of tasks.
* @return {number} The number of tasks.
*/
function getTasksCount() {
return tasks.length;
}
/**
* Returns the number of queued tasks.
* @return {number} The number of queued tasks.
*/
function getQueuedTasksCount() {
return queuedTasks.length;
}
/**
* Returns how long sigma.chronos has active tasks running
* without interuption for, in ms.
* @return {number} The time chronos is running without interuption for.
*/
function getExecutionTime() {
return startTime - currentTime;
}
this.frequency = frequency;
this.runTasks = runTasks;
this.stopTasks = stopTasks;
this.insertFrame = insertFrame;
this.addTask = addTask;
this.queueTask = queueTask;
this.removeTask = removeTask;
this.addGenerator = addGenerator;
this.removeGenerator = removeGenerator;
this.startGenerators = startGenerators;
this.getGeneratorsIDs = getGeneratorsIDs;
this.getFPS = getFPS;
this.getTasksCount = getTasksCount;
this.getQueuedTasksCount = getQueuedTasksCount;
this.getExecutionTime = getExecutionTime;
return this;
})();
sigma.debugMode = 0;
sigma.log = function() {
if (sigma.debugMode == 1) {
for (var k in arguments) {
console.log(arguments[k]);
}
}else if (sigma.debugMode > 1) {
for (var k in arguments) {
throw new Error(arguments[k]);
}
}
return sigma;
};
sigma.easing = {
linear: {},
quadratic: {}
};
sigma.easing.linear.easenone = function(k) {
return k;
};
sigma.easing.quadratic.easein = function(k) {
return k * k;
};
sigma.easing.quadratic.easeout = function(k) {
return - k * (k - 2);
};
sigma.easing.quadratic.easeinout = function(k) {
if ((k *= 2) < 1) return 0.5 * k * k;
return - 0.5 * (--k * (k - 2) - 1);
};
sigma.tools.drawRoundRect = function(ctx, x, y, w, h, ellipse, corners) {
var e = ellipse ? ellipse : 0;
var c = corners ? corners : [];
c = ((typeof c) == 'string') ? c.split(' ') : c;
var tl = e && (c.indexOf('topleft') >= 0 ||
c.indexOf('top') >= 0 ||
c.indexOf('left') >= 0);
var tr = e && (c.indexOf('topright') >= 0 ||
c.indexOf('top') >= 0 ||
c.indexOf('right') >= 0);
var bl = e && (c.indexOf('bottomleft') >= 0 ||
c.indexOf('bottom') >= 0 ||
c.indexOf('left') >= 0);
var br = e && (c.indexOf('bottomright') >= 0 ||
c.indexOf('bottom') >= 0 ||
c.indexOf('right') >= 0);
ctx.moveTo(x, y + e);
if (tl) {
ctx.arcTo(x, y, x + e, y, e);
}else {
ctx.lineTo(x, y);
}
if (tr) {
ctx.lineTo(x + w - e, y);
ctx.arcTo(x + w, y, x + w, y + e, e);
}else {
ctx.lineTo(x + w, y);
}
if (br) {
ctx.lineTo(x + w, y + h - e);
ctx.arcTo(x + w, y + h, x + w - e, y + h, e);
}else {
ctx.lineTo(x + w, y + h);
}
if (bl) {
ctx.lineTo(x + e, y + h);
ctx.arcTo(x, y + h, x, y + h - e, e);
}else {
ctx.lineTo(x, y + h);
}
ctx.lineTo(x, y + e);
};
sigma.tools.getRGB = function(s, asArray) {
s = s.toString();
var res = {
'r': 0,
'g': 0,
'b': 0
};
if (s.length >= 3) {
if (s.charAt(0) == '#') {
var l = s.length - 1;
if (l == 6) {
res = {
'r': parseInt(s.charAt(1) + s.charAt(2), 16),
'g': parseInt(s.charAt(3) + s.charAt(4), 16),
'b': parseInt(s.charAt(5) + s.charAt(5), 16)
};
}else if (l == 3) {
res = {
'r': parseInt(s.charAt(1) + s.charAt(1), 16),
'g': parseInt(s.charAt(2) + s.charAt(2), 16),
'b': parseInt(s.charAt(3) + s.charAt(3), 16)
};
}
}
}
if (asArray) {
res = [
res['r'],
res['g'],
res['b']
];
}
return res;
};
sigma.tools.rgbToHex = function(R, G, B) {
return sigma.tools.toHex(R) + sigma.tools.toHex(G) + sigma.tools.toHex(B);
};
sigma.tools.toHex = function(n) {
n = parseInt(n, 10);
if (isNaN(n)) {
return '00';
}
n = Math.max(0, Math.min(n, 255));
return '0123456789ABCDEF'.charAt((n - n % 16) / 16) +
'0123456789ABCDEF'.charAt(n % 16);
};
sigma.publicPrototype = SigmaPublic.prototype;
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment