;(function(undefined) { 'use strict'; if (typeof sigma === 'undefined') throw 'sigma is not declared'; // Initialize package: sigma.utils.pkg('sigma.plugins'); // Add custom graph methods: /** * This methods returns an array of nodes that are adjacent to a node. * * @param {string} id The node id. * @return {array} The array of adjacent nodes. */ if (!sigma.classes.graph.hasMethod('adjacentNodes')) sigma.classes.graph.addMethod('adjacentNodes', function(id) { if (typeof id !== 'string') throw 'adjacentNodes: the node id must be a string.'; var target, nodes = []; for(target in this.allNeighborsIndex[id]) { nodes.push(this.nodesIndex[target]); } return nodes; }); /** * This methods returns an array of edges that are adjacent to a node. * * @param {string} id The node id. * @return {array} The array of adjacent edges. */ if (!sigma.classes.graph.hasMethod('adjacentEdges')) sigma.classes.graph.addMethod('adjacentEdges', function(id) { if (typeof id !== 'string') throw 'adjacentEdges: the node id must be a string.'; var a = this.allNeighborsIndex[id], eid, target, edges = []; for(target in a) { for(eid in a[target]) { edges.push(a[target][eid]); } } return edges; }); /** * Sigma Filter * ============================= * * @author Sébastien Heymann (Linkurious) * @version 0.1 */ var _g = undefined, _s = undefined, _chain = [], // chain of wrapped filters _keysIndex = Object.create(null), Processors = {}; // available predicate processors /** * Library of processors * ------------------ */ /** * * @param {function} fn The predicate. */ Processors.nodes = function nodes(fn) { var n = _g.nodes(), ln = n.length, e = _g.edges(), le = e.length; // hide node, or keep former value while(ln--) n[ln].hidden = !fn.call(_g, n[ln]) || n[ln].hidden; while(le--) if (_g.nodes(e[le].source).hidden || _g.nodes(e[le].target).hidden) e[le].hidden = true; }; /** * * @param {function} fn The predicate. */ Processors.edges = function edges(fn) { var e = _g.edges(), le = e.length; // hide edge, or keep former value while(le--) e[le].hidden = !fn.call(_g, e[le]) || e[le].hidden; }; /** * * @param {string} id The center node. */ Processors.neighbors = function neighbors(id) { var n = _g.nodes(), ln = n.length, e = _g.edges(), le = e.length, neighbors = _g.adjacentNodes(id), nn = neighbors.length, no = {}; while(nn--) no[neighbors[nn].id] = true; while(ln--) if (n[ln].id !== id && !(n[ln].id in no)) n[ln].hidden = true; while(le--) if (_g.nodes(e[le].source).hidden || _g.nodes(e[le].target).hidden) e[le].hidden = true; }; /** * This function adds a filter to the chain of filters. * * @param {function} fn The filter (i.e. predicate processor). * @param {function} p The predicate. * @param {?string} key The key to identify the filter. */ function register(fn, p, key) { if (key != undefined && typeof key !== 'string') throw 'The filter key "'+ key.toString() +'" must be a string.'; if (key != undefined && !key.length) throw 'The filter key must be a non-empty string.'; if (typeof fn !== 'function') throw 'The predicate of key "'+ key +'" must be a function.'; if ('undo' === key) throw '"undo" is a reserved key.'; if (_keysIndex[key]) throw 'The filter "' + key + '" already exists.'; if (key) _keysIndex[key] = true; _chain.push({ 'key': key, 'processor': fn, 'predicate': p }); }; /** * This function removes a set of filters from the chain. * * @param {object} o The filter keys. */ function unregister (o) { _chain = _chain.filter(function(a) { return !(a.key in o); }); for(var key in o) delete _keysIndex[key]; }; /** * Filter Object * ------------------ * @param {sigma} s The related sigma instance. */ function Filter(s) { _s = s; _g = s.graph; }; /** * This method is used to filter the nodes. The method must be called with * the predicate, which is a function that takes a node as argument and * returns a boolean. It may take an identifier as argument to undo the * filter later. The method wraps the predicate into an anonymous function * that looks through each node in the graph. When executed, the anonymous * function hides the nodes that fail a truth test (predicate). The method * adds the anonymous function to the chain of filters. The filter is not * executed until the apply() method is called. * * > var filter = new sigma.plugins.filter(s); * > filter.nodesBy(function(n) { * > return this.degree(n.id) > 0; * > }, 'degreeNotNull'); * * @param {function} fn The filter predicate. * @param {?string} key The key to identify the filter. * @return {sigma.plugins.filter} Returns the instance. */ Filter.prototype.nodesBy = function(fn, key) { // Wrap the predicate to be applied on the graph and add it to the chain. register(Processors.nodes, fn, key); return this; }; /** * This method is used to filter the edges. The method must be called with * the predicate, which is a function that takes a node as argument and * returns a boolean. It may take an identifier as argument to undo the * filter later. The method wraps the predicate into an anonymous function * that looks through each edge in the graph. When executed, the anonymous * function hides the edges that fail a truth test (predicate). The method * adds the anonymous function to the chain of filters. The filter is not * executed until the apply() method is called. * * > var filter = new sigma.plugins.filter(s); * > filter.edgesBy(function(e) { * > return e.size > 1; * > }, 'edgeSize'); * * @param {function} fn The filter predicate. * @param {?string} key The key to identify the filter. * @return {sigma.plugins.filter} Returns the instance. */ Filter.prototype.edgesBy = function(fn, key) { // Wrap the predicate to be applied on the graph and add it to the chain. register(Processors.edges, fn, key); return this; }; /** * This method is used to filter the nodes which are not direct connections * of a given node. The method must be called with the node identifier. It * may take an identifier as argument to undo the filter later. The filter * is not executed until the apply() method is called. * * > var filter = new sigma.plugins.filter(s); * > filter.neighborsOf('n0'); * * @param {string} id The node id. * @param {?string} key The key to identify the filter. * @return {sigma.plugins.filter} Returns the instance. */ Filter.prototype.neighborsOf = function(id, key) { if (typeof id !== 'string') throw 'The node id "'+ id.toString() +'" must be a string.'; if (!id.length) throw 'The node id must be a non-empty string.'; // Wrap the predicate to be applied on the graph and add it to the chain. register(Processors.neighbors, id, key); return this; }; /** * This method is used to execute the chain of filters and to refresh the * display. * * > var filter = new sigma.plugins.filter(s); * > filter * > .nodesBy(function(n) { * > return this.degree(n.id) > 0; * > }, 'degreeNotNull') * > .apply(); * * @return {sigma.plugins.filter} Returns the instance. */ Filter.prototype.apply = function() { for (var i = 0, len = _chain.length; i < len; ++i) { _chain[i].processor(_chain[i].predicate); }; if (_chain[0] && 'undo' === _chain[0].key) { _chain.shift(); } _s.refresh(); return this; }; /** * This method undoes one or several filters, depending on how it is called. * * To undo all filters, call "undo" without argument. To undo a specific * filter, call it with the key of the filter. To undo multiple filters, call * it with an array of keys or multiple arguments, and it will undo each * filter, in the same order. The undo is not executed until the apply() * method is called. For instance: * * > var filter = new sigma.plugins.filter(s); * > filter * > .nodesBy(function(n) { * > return this.degree(n.id) > 0; * > }, 'degreeNotNull'); * > .edgesBy(function(e) { * > return e.size > 1; * > }, 'edgeSize') * > .undo(); * * Other examples: * > filter.undo(); * > filter.undo('myfilter'); * > filter.undo(['myfilter1', 'myfilter2']); * > filter.undo('myfilter1', 'myfilter2'); * * @param {?(string|array|*string))} v Eventually one key, an array of keys. * @return {sigma.plugins.filter} Returns the instance. */ Filter.prototype.undo = function(v) { var q = Object.create(null), la = arguments.length; // find removable filters if (la === 1) { if (Object.prototype.toString.call(v) === '[object Array]') for (var i = 0, len = v.length; i < len; i++) q[v[i]] = true; else // 1 filter key q[v] = true; } else if (la > 1) { for (var i = 0; i < la; i++) q[arguments[i]] = true; } else this.clear(); unregister(q); function processor() { var n = _g.nodes(), ln = n.length, e = _g.edges(), le = e.length; while(ln--) n[ln].hidden = false; while(le--) e[le].hidden = false; }; _chain.unshift({ 'key': 'undo', 'processor': processor }); return this; }; // fast deep copy function function deepCopy(o) { var copy = Object.create(null); for (var i in o) { if (typeof o[i] === "object" && o[i] !== null) { copy[i] = deepCopy(o[i]); } else if (typeof o[i] === "function" && o[i] !== null) { // clone function: eval(" copy[i] = " + o[i].toString()); //copy[i] = o[i].bind(_g); } else copy[i] = o[i]; } return copy; }; function cloneChain(chain) { // Clone the array of filters: var copy = chain.slice(0); for (var i = 0, len = copy.length; i < len; i++) { copy[i] = deepCopy(copy[i]); if (typeof copy[i].processor === "function") copy[i].processor = 'filter.processors.' + copy[i].processor.name; }; return copy; } /** * This method is used to empty the chain of filters. * Prefer the undo() method to reset filters. * * > var filter = new sigma.plugins.filter(s); * > filter.clear(); * * @return {sigma.plugins.filter} Returns the instance. */ Filter.prototype.clear = function() { _chain.length = 0; // clear the array _keysIndex = Object.create(null); return this; }; /** * This method clones the filter chain and return the copy. * * > var filter = new sigma.plugins.filter(s); * > var chain = filter.export(); * * @return {object} The cloned chain of filters. */ Filter.prototype.export = function() { var c = cloneChain(_chain); return c; }; /** * This method sets the chain of filters with the specified chain. * * > var filter = new sigma.plugins.filter(s); * > var chain = [ * > { * > key: 'my-filter', * > predicate: function(n) {...}, * > processor: 'filter.processors.nodes' * > }, ... * > ]; * > filter.import(chain); * * @param {array} chain The chain of filters. * @return {sigma.plugins.filter} Returns the instance. */ Filter.prototype.import = function(chain) { if (chain === undefined) throw 'Wrong arguments.'; if (Object.prototype.toString.call(chain) !== '[object Array]') throw 'The chain" must be an array.'; var copy = cloneChain(chain); for (var i = 0, len = copy.length; i < len; i++) { if (copy[i].predicate === undefined || copy[i].processor === undefined) throw 'Wrong arguments.'; if (copy[i].key != undefined && typeof copy[i].key !== 'string') throw 'The filter key "'+ copy[i].key.toString() +'" must be a string.'; if (typeof copy[i].predicate !== 'function') throw 'The predicate of key "'+ copy[i].key +'" must be a function.'; if (typeof copy[i].processor !== 'string') throw 'The processor of key "'+ copy[i].key +'" must be a string.'; // Replace the processor name by the corresponding function: switch(copy[i].processor) { case 'filter.processors.nodes': copy[i].processor = Processors.nodes; break; case 'filter.processors.edges': copy[i].processor = Processors.edges; break; case 'filter.processors.neighbors': copy[i].processor = Processors.neighbors; break; default: throw 'Unknown processor ' + copy[i].processor; } }; _chain = copy; return this; }; /** * Interface * ------------------ * * > var filter = new sigma.plugins.filter(s); */ var filter = null; /** * @param {sigma} s The related sigma instance. */ sigma.plugins.filter = function(s) { // Create filter if undefined if (!filter) { filter = new Filter(s); } return filter; }; }).call(this);