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.

504 lines
14 KiB

  1. ;(function(undefined) {
  2. 'use strict';
  3. if (typeof sigma === 'undefined')
  4. throw 'sigma is not declared';
  5. // Initialize package:
  6. sigma.utils.pkg('sigma.plugins');
  7. // Add custom graph methods:
  8. /**
  9. * This methods returns an array of nodes that are adjacent to a node.
  10. *
  11. * @param {string} id The node id.
  12. * @return {array} The array of adjacent nodes.
  13. */
  14. if (!sigma.classes.graph.hasMethod('adjacentNodes'))
  15. sigma.classes.graph.addMethod('adjacentNodes', function(id) {
  16. if (typeof id !== 'string')
  17. throw 'adjacentNodes: the node id must be a string.';
  18. var target,
  19. nodes = [];
  20. for(target in this.allNeighborsIndex[id]) {
  21. nodes.push(this.nodesIndex[target]);
  22. }
  23. return nodes;
  24. });
  25. /**
  26. * This methods returns an array of edges that are adjacent to a node.
  27. *
  28. * @param {string} id The node id.
  29. * @return {array} The array of adjacent edges.
  30. */
  31. if (!sigma.classes.graph.hasMethod('adjacentEdges'))
  32. sigma.classes.graph.addMethod('adjacentEdges', function(id) {
  33. if (typeof id !== 'string')
  34. throw 'adjacentEdges: the node id must be a string.';
  35. var a = this.allNeighborsIndex[id],
  36. eid,
  37. target,
  38. edges = [];
  39. for(target in a) {
  40. for(eid in a[target]) {
  41. edges.push(a[target][eid]);
  42. }
  43. }
  44. return edges;
  45. });
  46. /**
  47. * Sigma Filter
  48. * =============================
  49. *
  50. * @author Sébastien Heymann <seb@linkurio.us> (Linkurious)
  51. * @version 0.1
  52. */
  53. var _g = undefined,
  54. _s = undefined,
  55. _chain = [], // chain of wrapped filters
  56. _keysIndex = Object.create(null),
  57. Processors = {}; // available predicate processors
  58. /**
  59. * Library of processors
  60. * ------------------
  61. */
  62. /**
  63. *
  64. * @param {function} fn The predicate.
  65. */
  66. Processors.nodes = function nodes(fn) {
  67. var n = _g.nodes(),
  68. ln = n.length,
  69. e = _g.edges(),
  70. le = e.length;
  71. // hide node, or keep former value
  72. while(ln--)
  73. n[ln].hidden = !fn.call(_g, n[ln]) || n[ln].hidden;
  74. while(le--)
  75. if (_g.nodes(e[le].source).hidden || _g.nodes(e[le].target).hidden)
  76. e[le].hidden = true;
  77. };
  78. /**
  79. *
  80. * @param {function} fn The predicate.
  81. */
  82. Processors.edges = function edges(fn) {
  83. var e = _g.edges(),
  84. le = e.length;
  85. // hide edge, or keep former value
  86. while(le--)
  87. e[le].hidden = !fn.call(_g, e[le]) || e[le].hidden;
  88. };
  89. /**
  90. *
  91. * @param {string} id The center node.
  92. */
  93. Processors.neighbors = function neighbors(id) {
  94. var n = _g.nodes(),
  95. ln = n.length,
  96. e = _g.edges(),
  97. le = e.length,
  98. neighbors = _g.adjacentNodes(id),
  99. nn = neighbors.length,
  100. no = {};
  101. while(nn--)
  102. no[neighbors[nn].id] = true;
  103. while(ln--)
  104. if (n[ln].id !== id && !(n[ln].id in no))
  105. n[ln].hidden = true;
  106. while(le--)
  107. if (_g.nodes(e[le].source).hidden || _g.nodes(e[le].target).hidden)
  108. e[le].hidden = true;
  109. };
  110. /**
  111. * This function adds a filter to the chain of filters.
  112. *
  113. * @param {function} fn The filter (i.e. predicate processor).
  114. * @param {function} p The predicate.
  115. * @param {?string} key The key to identify the filter.
  116. */
  117. function register(fn, p, key) {
  118. if (key != undefined && typeof key !== 'string')
  119. throw 'The filter key "'+ key.toString() +'" must be a string.';
  120. if (key != undefined && !key.length)
  121. throw 'The filter key must be a non-empty string.';
  122. if (typeof fn !== 'function')
  123. throw 'The predicate of key "'+ key +'" must be a function.';
  124. if ('undo' === key)
  125. throw '"undo" is a reserved key.';
  126. if (_keysIndex[key])
  127. throw 'The filter "' + key + '" already exists.';
  128. if (key)
  129. _keysIndex[key] = true;
  130. _chain.push({
  131. 'key': key,
  132. 'processor': fn,
  133. 'predicate': p
  134. });
  135. };
  136. /**
  137. * This function removes a set of filters from the chain.
  138. *
  139. * @param {object} o The filter keys.
  140. */
  141. function unregister (o) {
  142. _chain = _chain.filter(function(a) {
  143. return !(a.key in o);
  144. });
  145. for(var key in o)
  146. delete _keysIndex[key];
  147. };
  148. /**
  149. * Filter Object
  150. * ------------------
  151. * @param {sigma} s The related sigma instance.
  152. */
  153. function Filter(s) {
  154. _s = s;
  155. _g = s.graph;
  156. };
  157. /**
  158. * This method is used to filter the nodes. The method must be called with
  159. * the predicate, which is a function that takes a node as argument and
  160. * returns a boolean. It may take an identifier as argument to undo the
  161. * filter later. The method wraps the predicate into an anonymous function
  162. * that looks through each node in the graph. When executed, the anonymous
  163. * function hides the nodes that fail a truth test (predicate). The method
  164. * adds the anonymous function to the chain of filters. The filter is not
  165. * executed until the apply() method is called.
  166. *
  167. * > var filter = new sigma.plugins.filter(s);
  168. * > filter.nodesBy(function(n) {
  169. * > return this.degree(n.id) > 0;
  170. * > }, 'degreeNotNull');
  171. *
  172. * @param {function} fn The filter predicate.
  173. * @param {?string} key The key to identify the filter.
  174. * @return {sigma.plugins.filter} Returns the instance.
  175. */
  176. Filter.prototype.nodesBy = function(fn, key) {
  177. // Wrap the predicate to be applied on the graph and add it to the chain.
  178. register(Processors.nodes, fn, key);
  179. return this;
  180. };
  181. /**
  182. * This method is used to filter the edges. The method must be called with
  183. * the predicate, which is a function that takes a node as argument and
  184. * returns a boolean. It may take an identifier as argument to undo the
  185. * filter later. The method wraps the predicate into an anonymous function
  186. * that looks through each edge in the graph. When executed, the anonymous
  187. * function hides the edges that fail a truth test (predicate). The method
  188. * adds the anonymous function to the chain of filters. The filter is not
  189. * executed until the apply() method is called.
  190. *
  191. * > var filter = new sigma.plugins.filter(s);
  192. * > filter.edgesBy(function(e) {
  193. * > return e.size > 1;
  194. * > }, 'edgeSize');
  195. *
  196. * @param {function} fn The filter predicate.
  197. * @param {?string} key The key to identify the filter.
  198. * @return {sigma.plugins.filter} Returns the instance.
  199. */
  200. Filter.prototype.edgesBy = function(fn, key) {
  201. // Wrap the predicate to be applied on the graph and add it to the chain.
  202. register(Processors.edges, fn, key);
  203. return this;
  204. };
  205. /**
  206. * This method is used to filter the nodes which are not direct connections
  207. * of a given node. The method must be called with the node identifier. It
  208. * may take an identifier as argument to undo the filter later. The filter
  209. * is not executed until the apply() method is called.
  210. *
  211. * > var filter = new sigma.plugins.filter(s);
  212. * > filter.neighborsOf('n0');
  213. *
  214. * @param {string} id The node id.
  215. * @param {?string} key The key to identify the filter.
  216. * @return {sigma.plugins.filter} Returns the instance.
  217. */
  218. Filter.prototype.neighborsOf = function(id, key) {
  219. if (typeof id !== 'string')
  220. throw 'The node id "'+ id.toString() +'" must be a string.';
  221. if (!id.length)
  222. throw 'The node id must be a non-empty string.';
  223. // Wrap the predicate to be applied on the graph and add it to the chain.
  224. register(Processors.neighbors, id, key);
  225. return this;
  226. };
  227. /**
  228. * This method is used to execute the chain of filters and to refresh the
  229. * display.
  230. *
  231. * > var filter = new sigma.plugins.filter(s);
  232. * > filter
  233. * > .nodesBy(function(n) {
  234. * > return this.degree(n.id) > 0;
  235. * > }, 'degreeNotNull')
  236. * > .apply();
  237. *
  238. * @return {sigma.plugins.filter} Returns the instance.
  239. */
  240. Filter.prototype.apply = function() {
  241. for (var i = 0, len = _chain.length; i < len; ++i) {
  242. _chain[i].processor(_chain[i].predicate);
  243. };
  244. if (_chain[0] && 'undo' === _chain[0].key) {
  245. _chain.shift();
  246. }
  247. _s.refresh();
  248. return this;
  249. };
  250. /**
  251. * This method undoes one or several filters, depending on how it is called.
  252. *
  253. * To undo all filters, call "undo" without argument. To undo a specific
  254. * filter, call it with the key of the filter. To undo multiple filters, call
  255. * it with an array of keys or multiple arguments, and it will undo each
  256. * filter, in the same order. The undo is not executed until the apply()
  257. * method is called. For instance:
  258. *
  259. * > var filter = new sigma.plugins.filter(s);
  260. * > filter
  261. * > .nodesBy(function(n) {
  262. * > return this.degree(n.id) > 0;
  263. * > }, 'degreeNotNull');
  264. * > .edgesBy(function(e) {
  265. * > return e.size > 1;
  266. * > }, 'edgeSize')
  267. * > .undo();
  268. *
  269. * Other examples:
  270. * > filter.undo();
  271. * > filter.undo('myfilter');
  272. * > filter.undo(['myfilter1', 'myfilter2']);
  273. * > filter.undo('myfilter1', 'myfilter2');
  274. *
  275. * @param {?(string|array|*string))} v Eventually one key, an array of keys.
  276. * @return {sigma.plugins.filter} Returns the instance.
  277. */
  278. Filter.prototype.undo = function(v) {
  279. var q = Object.create(null),
  280. la = arguments.length;
  281. // find removable filters
  282. if (la === 1) {
  283. if (Object.prototype.toString.call(v) === '[object Array]')
  284. for (var i = 0, len = v.length; i < len; i++)
  285. q[v[i]] = true;
  286. else // 1 filter key
  287. q[v] = true;
  288. } else if (la > 1) {
  289. for (var i = 0; i < la; i++)
  290. q[arguments[i]] = true;
  291. }
  292. else
  293. this.clear();
  294. unregister(q);
  295. function processor() {
  296. var n = _g.nodes(),
  297. ln = n.length,
  298. e = _g.edges(),
  299. le = e.length;
  300. while(ln--)
  301. n[ln].hidden = false;
  302. while(le--)
  303. e[le].hidden = false;
  304. };
  305. _chain.unshift({
  306. 'key': 'undo',
  307. 'processor': processor
  308. });
  309. return this;
  310. };
  311. // fast deep copy function
  312. function deepCopy(o) {
  313. var copy = Object.create(null);
  314. for (var i in o) {
  315. if (typeof o[i] === "object" && o[i] !== null) {
  316. copy[i] = deepCopy(o[i]);
  317. }
  318. else if (typeof o[i] === "function" && o[i] !== null) {
  319. // clone function:
  320. eval(" copy[i] = " + o[i].toString());
  321. //copy[i] = o[i].bind(_g);
  322. }
  323. else
  324. copy[i] = o[i];
  325. }
  326. return copy;
  327. };
  328. function cloneChain(chain) {
  329. // Clone the array of filters:
  330. var copy = chain.slice(0);
  331. for (var i = 0, len = copy.length; i < len; i++) {
  332. copy[i] = deepCopy(copy[i]);
  333. if (typeof copy[i].processor === "function")
  334. copy[i].processor = 'filter.processors.' + copy[i].processor.name;
  335. };
  336. return copy;
  337. }
  338. /**
  339. * This method is used to empty the chain of filters.
  340. * Prefer the undo() method to reset filters.
  341. *
  342. * > var filter = new sigma.plugins.filter(s);
  343. * > filter.clear();
  344. *
  345. * @return {sigma.plugins.filter} Returns the instance.
  346. */
  347. Filter.prototype.clear = function() {
  348. _chain.length = 0; // clear the array
  349. _keysIndex = Object.create(null);
  350. return this;
  351. };
  352. /**
  353. * This method clones the filter chain and return the copy.
  354. *
  355. * > var filter = new sigma.plugins.filter(s);
  356. * > var chain = filter.export();
  357. *
  358. * @return {object} The cloned chain of filters.
  359. */
  360. Filter.prototype.export = function() {
  361. var c = cloneChain(_chain);
  362. return c;
  363. };
  364. /**
  365. * This method sets the chain of filters with the specified chain.
  366. *
  367. * > var filter = new sigma.plugins.filter(s);
  368. * > var chain = [
  369. * > {
  370. * > key: 'my-filter',
  371. * > predicate: function(n) {...},
  372. * > processor: 'filter.processors.nodes'
  373. * > }, ...
  374. * > ];
  375. * > filter.import(chain);
  376. *
  377. * @param {array} chain The chain of filters.
  378. * @return {sigma.plugins.filter} Returns the instance.
  379. */
  380. Filter.prototype.import = function(chain) {
  381. if (chain === undefined)
  382. throw 'Wrong arguments.';
  383. if (Object.prototype.toString.call(chain) !== '[object Array]')
  384. throw 'The chain" must be an array.';
  385. var copy = cloneChain(chain);
  386. for (var i = 0, len = copy.length; i < len; i++) {
  387. if (copy[i].predicate === undefined || copy[i].processor === undefined)
  388. throw 'Wrong arguments.';
  389. if (copy[i].key != undefined && typeof copy[i].key !== 'string')
  390. throw 'The filter key "'+ copy[i].key.toString() +'" must be a string.';
  391. if (typeof copy[i].predicate !== 'function')
  392. throw 'The predicate of key "'+ copy[i].key +'" must be a function.';
  393. if (typeof copy[i].processor !== 'string')
  394. throw 'The processor of key "'+ copy[i].key +'" must be a string.';
  395. // Replace the processor name by the corresponding function:
  396. switch(copy[i].processor) {
  397. case 'filter.processors.nodes':
  398. copy[i].processor = Processors.nodes;
  399. break;
  400. case 'filter.processors.edges':
  401. copy[i].processor = Processors.edges;
  402. break;
  403. case 'filter.processors.neighbors':
  404. copy[i].processor = Processors.neighbors;
  405. break;
  406. default:
  407. throw 'Unknown processor ' + copy[i].processor;
  408. }
  409. };
  410. _chain = copy;
  411. return this;
  412. };
  413. /**
  414. * Interface
  415. * ------------------
  416. *
  417. * > var filter = new sigma.plugins.filter(s);
  418. */
  419. var filter = null;
  420. /**
  421. * @param {sigma} s The related sigma instance.
  422. */
  423. sigma.plugins.filter = function(s) {
  424. // Create filter if undefined
  425. if (!filter) {
  426. filter = new Filter(s);
  427. }
  428. return filter;
  429. };
  430. }).call(this);