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.
 
 
 
 

859 lines
26 KiB

;(function(undefined) {
'use strict';
var _methods = Object.create(null),
_indexes = Object.create(null),
_initBindings = Object.create(null),
_methodBindings = Object.create(null),
_methodBeforeBindings = Object.create(null),
_defaultSettings = {
immutable: true,
clone: true
},
_defaultSettingsFunction = function(key) {
return _defaultSettings[key];
};
/**
* The graph constructor. It initializes the data and the indexes, and binds
* the custom indexes and methods to its own scope.
*
* Recognized parameters:
* **********************
* Here is the exhaustive list of every accepted parameters in the settings
* object:
*
* {boolean} clone Indicates if the data have to be cloned in methods
* to add nodes or edges.
* {boolean} immutable Indicates if nodes "id" values and edges "id",
* "source" and "target" values must be set as
* immutable.
*
* @param {?configurable} settings Eventually a settings function.
* @return {graph} The new graph instance.
*/
var graph = function(settings) {
var k,
fn,
data;
/**
* DATA:
* *****
* Every data that is callable from graph methods are stored in this "data"
* object. This object will be served as context for all these methods,
* and it is possible to add other type of data in it.
*/
data = {
/**
* SETTINGS FUNCTION:
* ******************
*/
settings: settings || _defaultSettingsFunction,
/**
* MAIN DATA:
* **********
*/
nodesArray: [],
edgesArray: [],
/**
* GLOBAL INDEXES:
* ***************
* These indexes just index data by ids.
*/
nodesIndex: Object.create(null),
edgesIndex: Object.create(null),
/**
* LOCAL INDEXES:
* **************
* These indexes refer from node to nodes. Each key is an id, and each
* value is the array of the ids of related nodes.
*/
inNeighborsIndex: Object.create(null),
outNeighborsIndex: Object.create(null),
allNeighborsIndex: Object.create(null),
inNeighborsCount: Object.create(null),
outNeighborsCount: Object.create(null),
allNeighborsCount: Object.create(null)
};
// Execute bindings:
for (k in _initBindings)
_initBindings[k].call(data);
// Add methods to both the scope and the data objects:
for (k in _methods) {
fn = __bindGraphMethod(k, data, _methods[k]);
this[k] = fn;
data[k] = fn;
}
};
/**
* A custom tool to bind methods such that function that are bound to it will
* be executed anytime the method is called.
*
* @param {string} methodName The name of the method to bind.
* @param {object} scope The scope where the method must be executed.
* @param {function} fn The method itself.
* @return {function} The new method.
*/
function __bindGraphMethod(methodName, scope, fn) {
var result = function() {
var k,
res;
// Execute "before" bound functions:
for (k in _methodBeforeBindings[methodName])
_methodBeforeBindings[methodName][k].apply(scope, arguments);
// Apply the method:
res = fn.apply(scope, arguments);
// Execute bound functions:
for (k in _methodBindings[methodName])
_methodBindings[methodName][k].apply(scope, arguments);
// Return res:
return res;
};
return result;
}
/**
* This custom tool function removes every pair key/value from an hash. The
* goal is to avoid creating a new object while some other references are
* still hanging in some scopes...
*
* @param {object} obj The object to empty.
* @return {object} The empty object.
*/
function __emptyObject(obj) {
var k;
for (k in obj)
if (!('hasOwnProperty' in obj) || obj.hasOwnProperty(k))
delete obj[k];
return obj;
}
/**
* This global method adds a method that will be bound to the futurly created
* graph instances.
*
* Since these methods will be bound to their scope when the instances are
* created, it does not use the prototype. Because of that, methods have to
* be added before instances are created to make them available.
*
* Here is an example:
*
* > graph.addMethod('getNodesCount', function() {
* > return this.nodesArray.length;
* > });
* >
* > var myGraph = new graph();
* > console.log(myGraph.getNodesCount()); // outputs 0
*
* @param {string} methodName The name of the method.
* @param {function} fn The method itself.
* @return {object} The global graph constructor.
*/
graph.addMethod = function(methodName, fn) {
if (
typeof methodName !== 'string' ||
typeof fn !== 'function' ||
arguments.length !== 2
)
throw 'addMethod: Wrong arguments.';
if (_methods[methodName] || graph[methodName])
throw 'The method "' + methodName + '" already exists.';
_methods[methodName] = fn;
_methodBindings[methodName] = Object.create(null);
_methodBeforeBindings[methodName] = Object.create(null);
return this;
};
/**
* This global method returns true if the method has already been added, and
* false else.
*
* Here are some examples:
*
* > graph.hasMethod('addNode'); // returns true
* > graph.hasMethod('hasMethod'); // returns true
* > graph.hasMethod('unexistingMethod'); // returns false
*
* @param {string} methodName The name of the method.
* @return {boolean} The result.
*/
graph.hasMethod = function(methodName) {
return !!(_methods[methodName] || graph[methodName]);
};
/**
* This global methods attaches a function to a method. Anytime the specified
* method is called, the attached function is called right after, with the
* same arguments and in the same scope. The attached function is called
* right before if the last argument is true, unless the method is the graph
* constructor.
*
* To attach a function to the graph constructor, use 'constructor' as the
* method name (first argument).
*
* The main idea is to have a clean way to keep custom indexes up to date,
* for instance:
*
* > var timesAddNodeCalled = 0;
* > graph.attach('addNode', 'timesAddNodeCalledInc', function() {
* > timesAddNodeCalled++;
* > });
* >
* > var myGraph = new graph();
* > console.log(timesAddNodeCalled); // outputs 0
* >
* > myGraph.addNode({ id: '1' }).addNode({ id: '2' });
* > console.log(timesAddNodeCalled); // outputs 2
*
* The idea for calling a function before is to provide pre-processors, for
* instance:
*
* > var colorPalette = { Person: '#C3CBE1', Place: '#9BDEBD' };
* > graph.attach('addNode', 'applyNodeColorPalette', function(n) {
* > n.color = colorPalette[n.category];
* > }, true);
* >
* > var myGraph = new graph();
* > myGraph.addNode({ id: 'n0', category: 'Person' });
* > console.log(myGraph.nodes('n0').color); // outputs '#C3CBE1'
*
* @param {string} methodName The name of the related method or
* "constructor".
* @param {string} key The key to identify the function to attach.
* @param {function} fn The function to bind.
* @param {boolean} before If true the function is called right before.
* @return {object} The global graph constructor.
*/
graph.attach = function(methodName, key, fn, before) {
if (
typeof methodName !== 'string' ||
typeof key !== 'string' ||
typeof fn !== 'function' ||
arguments.length < 3 ||
arguments.length > 4
)
throw 'attach: Wrong arguments.';
var bindings;
if (methodName === 'constructor')
bindings = _initBindings;
else {
if (before) {
if (!_methodBeforeBindings[methodName])
throw 'The method "' + methodName + '" does not exist.';
bindings = _methodBeforeBindings[methodName];
}
else {
if (!_methodBindings[methodName])
throw 'The method "' + methodName + '" does not exist.';
bindings = _methodBindings[methodName];
}
}
if (bindings[key])
throw 'A function "' + key + '" is already attached ' +
'to the method "' + methodName + '".';
bindings[key] = fn;
return this;
};
/**
* Alias of attach(methodName, key, fn, true).
*/
graph.attachBefore = function(methodName, key, fn) {
return this.attach(methodName, key, fn, true);
};
/**
* This methods is just an helper to deal with custom indexes. It takes as
* arguments the name of the index and an object containing all the different
* functions to bind to the methods.
*
* Here is a basic example, that creates an index to keep the number of nodes
* in the current graph. It also adds a method to provide a getter on that
* new index:
*
* > sigma.classes.graph.addIndex('nodesCount', {
* > constructor: function() {
* > this.nodesCount = 0;
* > },
* > addNode: function() {
* > this.nodesCount++;
* > },
* > dropNode: function() {
* > this.nodesCount--;
* > }
* > });
* >
* > sigma.classes.graph.addMethod('getNodesCount', function() {
* > return this.nodesCount;
* > });
* >
* > var myGraph = new sigma.classes.graph();
* > console.log(myGraph.getNodesCount()); // outputs 0
* >
* > myGraph.addNode({ id: '1' }).addNode({ id: '2' });
* > console.log(myGraph.getNodesCount()); // outputs 2
*
* @param {string} name The name of the index.
* @param {object} bindings The object containing the functions to bind.
* @return {object} The global graph constructor.
*/
graph.addIndex = function(name, bindings) {
if (
typeof name !== 'string' ||
Object(bindings) !== bindings ||
arguments.length !== 2
)
throw 'addIndex: Wrong arguments.';
if (_indexes[name])
throw 'The index "' + name + '" already exists.';
var k;
// Store the bindings:
_indexes[name] = bindings;
// Attach the bindings:
for (k in bindings)
if (typeof bindings[k] !== 'function')
throw 'The bindings must be functions.';
else
graph.attach(k, name, bindings[k]);
return this;
};
/**
* This method adds a node to the graph. The node must be an object, with a
* string under the key "id". Except for this, it is possible to add any
* other attribute, that will be preserved all along the manipulations.
*
* If the graph option "clone" has a truthy value, the node will be cloned
* when added to the graph. Also, if the graph option "immutable" has a
* truthy value, its id will be defined as immutable.
*
* @param {object} node The node to add.
* @return {object} The graph instance.
*/
graph.addMethod('addNode', function(node) {
// Check that the node is an object and has an id:
if (Object(node) !== node || arguments.length !== 1)
throw 'addNode: Wrong arguments.';
if (typeof node.id !== 'string' && typeof node.id !== 'number')
throw 'The node must have a string or number id.';
if (this.nodesIndex[node.id])
throw 'The node "' + node.id + '" already exists.';
var k,
id = node.id,
validNode = Object.create(null);
// Check the "clone" option:
if (this.settings('clone')) {
for (k in node)
if (k !== 'id')
validNode[k] = node[k];
} else
validNode = node;
// Check the "immutable" option:
if (this.settings('immutable'))
Object.defineProperty(validNode, 'id', {
value: id,
enumerable: true
});
else
validNode.id = id;
// Add empty containers for edges indexes:
this.inNeighborsIndex[id] = Object.create(null);
this.outNeighborsIndex[id] = Object.create(null);
this.allNeighborsIndex[id] = Object.create(null);
this.inNeighborsCount[id] = 0;
this.outNeighborsCount[id] = 0;
this.allNeighborsCount[id] = 0;
// Add the node to indexes:
this.nodesArray.push(validNode);
this.nodesIndex[validNode.id] = validNode;
// Return the current instance:
return this;
});
/**
* This method adds an edge to the graph. The edge must be an object, with a
* string under the key "id", and strings under the keys "source" and
* "target" that design existing nodes. Except for this, it is possible to
* add any other attribute, that will be preserved all along the
* manipulations.
*
* If the graph option "clone" has a truthy value, the edge will be cloned
* when added to the graph. Also, if the graph option "immutable" has a
* truthy value, its id, source and target will be defined as immutable.
*
* @param {object} edge The edge to add.
* @return {object} The graph instance.
*/
graph.addMethod('addEdge', function(edge) {
// Check that the edge is an object and has an id:
if (Object(edge) !== edge || arguments.length !== 1)
throw 'addEdge: Wrong arguments.';
if (typeof edge.id !== 'string' && typeof edge.id !== 'number')
throw 'The edge must have a string or number id.';
if ((typeof edge.source !== 'string' && typeof edge.source !== 'number') ||
!this.nodesIndex[edge.source])
throw 'The edge source must have an existing node id.';
if ((typeof edge.target !== 'string' && typeof edge.target !== 'number') ||
!this.nodesIndex[edge.target])
throw 'The edge target must have an existing node id.';
if (this.edgesIndex[edge.id])
throw 'The edge "' + edge.id + '" already exists.';
var k,
validEdge = Object.create(null);
// Check the "clone" option:
if (this.settings('clone')) {
for (k in edge)
if (k !== 'id' && k !== 'source' && k !== 'target')
validEdge[k] = edge[k];
} else
validEdge = edge;
// Check the "immutable" option:
if (this.settings('immutable')) {
Object.defineProperty(validEdge, 'id', {
value: edge.id,
enumerable: true
});
Object.defineProperty(validEdge, 'source', {
value: edge.source,
enumerable: true
});
Object.defineProperty(validEdge, 'target', {
value: edge.target,
enumerable: true
});
} else {
validEdge.id = edge.id;
validEdge.source = edge.source;
validEdge.target = edge.target;
}
// Add the edge to indexes:
this.edgesArray.push(validEdge);
this.edgesIndex[validEdge.id] = validEdge;
if (!this.inNeighborsIndex[validEdge.target][validEdge.source])
this.inNeighborsIndex[validEdge.target][validEdge.source] =
Object.create(null);
this.inNeighborsIndex[validEdge.target][validEdge.source][validEdge.id] =
validEdge;
if (!this.outNeighborsIndex[validEdge.source][validEdge.target])
this.outNeighborsIndex[validEdge.source][validEdge.target] =
Object.create(null);
this.outNeighborsIndex[validEdge.source][validEdge.target][validEdge.id] =
validEdge;
if (!this.allNeighborsIndex[validEdge.source][validEdge.target])
this.allNeighborsIndex[validEdge.source][validEdge.target] =
Object.create(null);
this.allNeighborsIndex[validEdge.source][validEdge.target][validEdge.id] =
validEdge;
if (validEdge.target !== validEdge.source) {
if (!this.allNeighborsIndex[validEdge.target][validEdge.source])
this.allNeighborsIndex[validEdge.target][validEdge.source] =
Object.create(null);
this.allNeighborsIndex[validEdge.target][validEdge.source][validEdge.id] =
validEdge;
}
// Keep counts up to date:
this.inNeighborsCount[validEdge.target]++;
this.outNeighborsCount[validEdge.source]++;
this.allNeighborsCount[validEdge.target]++;
this.allNeighborsCount[validEdge.source]++;
return this;
});
/**
* This method drops a node from the graph. It also removes each edge that is
* bound to it, through the dropEdge method. An error is thrown if the node
* does not exist.
*
* @param {string} id The node id.
* @return {object} The graph instance.
*/
graph.addMethod('dropNode', function(id) {
// Check that the arguments are valid:
if ((typeof id !== 'string' && typeof id !== 'number') ||
arguments.length !== 1)
throw 'dropNode: Wrong arguments.';
if (!this.nodesIndex[id])
throw 'The node "' + id + '" does not exist.';
var i, k, l;
// Remove the node from indexes:
delete this.nodesIndex[id];
for (i = 0, l = this.nodesArray.length; i < l; i++)
if (this.nodesArray[i].id === id) {
this.nodesArray.splice(i, 1);
break;
}
// Remove related edges:
for (i = this.edgesArray.length - 1; i >= 0; i--)
if (this.edgesArray[i].source === id || this.edgesArray[i].target === id)
this.dropEdge(this.edgesArray[i].id);
// Remove related edge indexes:
delete this.inNeighborsIndex[id];
delete this.outNeighborsIndex[id];
delete this.allNeighborsIndex[id];
delete this.inNeighborsCount[id];
delete this.outNeighborsCount[id];
delete this.allNeighborsCount[id];
for (k in this.nodesIndex) {
delete this.inNeighborsIndex[k][id];
delete this.outNeighborsIndex[k][id];
delete this.allNeighborsIndex[k][id];
}
return this;
});
/**
* This method drops an edge from the graph. An error is thrown if the edge
* does not exist.
*
* @param {string} id The edge id.
* @return {object} The graph instance.
*/
graph.addMethod('dropEdge', function(id) {
// Check that the arguments are valid:
if ((typeof id !== 'string' && typeof id !== 'number') ||
arguments.length !== 1)
throw 'dropEdge: Wrong arguments.';
if (!this.edgesIndex[id])
throw 'The edge "' + id + '" does not exist.';
var i, l, edge;
// Remove the edge from indexes:
edge = this.edgesIndex[id];
delete this.edgesIndex[id];
for (i = 0, l = this.edgesArray.length; i < l; i++)
if (this.edgesArray[i].id === id) {
this.edgesArray.splice(i, 1);
break;
}
delete this.inNeighborsIndex[edge.target][edge.source][edge.id];
if (!Object.keys(this.inNeighborsIndex[edge.target][edge.source]).length)
delete this.inNeighborsIndex[edge.target][edge.source];
delete this.outNeighborsIndex[edge.source][edge.target][edge.id];
if (!Object.keys(this.outNeighborsIndex[edge.source][edge.target]).length)
delete this.outNeighborsIndex[edge.source][edge.target];
delete this.allNeighborsIndex[edge.source][edge.target][edge.id];
if (!Object.keys(this.allNeighborsIndex[edge.source][edge.target]).length)
delete this.allNeighborsIndex[edge.source][edge.target];
if (edge.target !== edge.source) {
delete this.allNeighborsIndex[edge.target][edge.source][edge.id];
if (!Object.keys(this.allNeighborsIndex[edge.target][edge.source]).length)
delete this.allNeighborsIndex[edge.target][edge.source];
}
this.inNeighborsCount[edge.target]--;
this.outNeighborsCount[edge.source]--;
this.allNeighborsCount[edge.source]--;
this.allNeighborsCount[edge.target]--;
return this;
});
/**
* This method destroys the current instance. It basically empties each index
* and methods attached to the graph.
*/
graph.addMethod('kill', function() {
// Delete arrays:
this.nodesArray.length = 0;
this.edgesArray.length = 0;
delete this.nodesArray;
delete this.edgesArray;
// Delete indexes:
delete this.nodesIndex;
delete this.edgesIndex;
delete this.inNeighborsIndex;
delete this.outNeighborsIndex;
delete this.allNeighborsIndex;
delete this.inNeighborsCount;
delete this.outNeighborsCount;
delete this.allNeighborsCount;
});
/**
* This method empties the nodes and edges arrays, as well as the different
* indexes.
*
* @return {object} The graph instance.
*/
graph.addMethod('clear', function() {
this.nodesArray.length = 0;
this.edgesArray.length = 0;
// Due to GC issues, I prefer not to create new object. These objects are
// only available from the methods and attached functions, but still, it is
// better to prevent ghost references to unrelevant data...
__emptyObject(this.nodesIndex);
__emptyObject(this.edgesIndex);
__emptyObject(this.nodesIndex);
__emptyObject(this.inNeighborsIndex);
__emptyObject(this.outNeighborsIndex);
__emptyObject(this.allNeighborsIndex);
__emptyObject(this.inNeighborsCount);
__emptyObject(this.outNeighborsCount);
__emptyObject(this.allNeighborsCount);
return this;
});
/**
* This method reads an object and adds the nodes and edges, through the
* proper methods "addNode" and "addEdge".
*
* Here is an example:
*
* > var myGraph = new graph();
* > myGraph.read({
* > nodes: [
* > { id: 'n0' },
* > { id: 'n1' }
* > ],
* > edges: [
* > {
* > id: 'e0',
* > source: 'n0',
* > target: 'n1'
* > }
* > ]
* > });
* >
* > console.log(
* > myGraph.nodes().length,
* > myGraph.edges().length
* > ); // outputs 2 1
*
* @param {object} g The graph object.
* @return {object} The graph instance.
*/
graph.addMethod('read', function(g) {
var i,
a,
l;
a = g.nodes || [];
for (i = 0, l = a.length; i < l; i++)
this.addNode(a[i]);
a = g.edges || [];
for (i = 0, l = a.length; i < l; i++)
this.addEdge(a[i]);
return this;
});
/**
* This methods returns one or several nodes, depending on how it is called.
*
* To get the array of nodes, call "nodes" without argument. To get a
* specific node, call it with the id of the node. The get multiple node,
* call it with an array of ids, and it will return the array of nodes, in
* the same order.
*
* @param {?(string|array)} v Eventually one id, an array of ids.
* @return {object|array} The related node or array of nodes.
*/
graph.addMethod('nodes', function(v) {
// Clone the array of nodes and return it:
if (!arguments.length)
return this.nodesArray.slice(0);
// Return the related node:
if (arguments.length === 1 &&
(typeof v === 'string' || typeof v === 'number'))
return this.nodesIndex[v];
// Return an array of the related node:
if (
arguments.length === 1 &&
Object.prototype.toString.call(v) === '[object Array]'
) {
var i,
l,
a = [];
for (i = 0, l = v.length; i < l; i++)
if (typeof v[i] === 'string' || typeof v[i] === 'number')
a.push(this.nodesIndex[v[i]]);
else
throw 'nodes: Wrong arguments.';
return a;
}
throw 'nodes: Wrong arguments.';
});
/**
* This methods returns the degree of one or several nodes, depending on how
* it is called. It is also possible to get incoming or outcoming degrees
* instead by specifying 'in' or 'out' as a second argument.
*
* @param {string|array} v One id, an array of ids.
* @param {?string} which Which degree is required. Values are 'in',
* 'out', and by default the normal degree.
* @return {number|array} The related degree or array of degrees.
*/
graph.addMethod('degree', function(v, which) {
// Check which degree is required:
which = {
'in': this.inNeighborsCount,
'out': this.outNeighborsCount
}[which || ''] || this.allNeighborsCount;
// Return the related node:
if (typeof v === 'string' || typeof v === 'number')
return which[v];
// Return an array of the related node:
if (Object.prototype.toString.call(v) === '[object Array]') {
var i,
l,
a = [];
for (i = 0, l = v.length; i < l; i++)
if (typeof v[i] === 'string' || typeof v[i] === 'number')
a.push(which[v[i]]);
else
throw 'degree: Wrong arguments.';
return a;
}
throw 'degree: Wrong arguments.';
});
/**
* This methods returns one or several edges, depending on how it is called.
*
* To get the array of edges, call "edges" without argument. To get a
* specific edge, call it with the id of the edge. The get multiple edge,
* call it with an array of ids, and it will return the array of edges, in
* the same order.
*
* @param {?(string|array)} v Eventually one id, an array of ids.
* @return {object|array} The related edge or array of edges.
*/
graph.addMethod('edges', function(v) {
// Clone the array of edges and return it:
if (!arguments.length)
return this.edgesArray.slice(0);
// Return the related edge:
if (arguments.length === 1 &&
(typeof v === 'string' || typeof v === 'number'))
return this.edgesIndex[v];
// Return an array of the related edge:
if (
arguments.length === 1 &&
Object.prototype.toString.call(v) === '[object Array]'
) {
var i,
l,
a = [];
for (i = 0, l = v.length; i < l; i++)
if (typeof v[i] === 'string' || typeof v[i] === 'number')
a.push(this.edgesIndex[v[i]]);
else
throw 'edges: Wrong arguments.';
return a;
}
throw 'edges: Wrong arguments.';
});
/**
* EXPORT:
* *******
*/
if (typeof sigma !== 'undefined') {
sigma.classes = sigma.classes || Object.create(null);
sigma.classes.graph = graph;
} else if (typeof exports !== 'undefined') {
if (typeof module !== 'undefined' && module.exports)
exports = module.exports = graph;
exports.graph = graph;
} else
this.graph = graph;
}).call(this);