From 5097e7ff8bbf65c5f77f48b1a93d692f1f5abb61 Mon Sep 17 00:00:00 2001 From: pissang Date: Fri, 23 Jun 2017 16:44:23 +0800 Subject: [PATCH] Change algorithm to gephi modularity. Release 1.1.0 --- README.md | 7 +- dist/echarts-graph-modularity.js | 1930 +++++++++++++++++++++----- dist/echarts-graph-modularity.min.js | 2 +- example/example.html | 4 +- package.json | 5 +- src/main.js | 15 +- src/modularity.js | 398 ------ 7 files changed, 1617 insertions(+), 744 deletions(-) delete mode 100644 src/modularity.js diff --git a/README.md b/README.md index 8213be0..6267352 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# [ECharts](https://github.com/ecomfe/echarts) graph modularity extension based on [jLouvain](https://github.com/upphiminn/jLouvain) +# [ECharts](https://github.com/ecomfe/echarts) graph modularity extension logo @@ -40,6 +40,11 @@ setOption({ // and assign each different color. modularity: true + // Specify resolution. Higher resolution will produce less communities + modularity: { + resolution: 5 + } + ... }] }) diff --git a/dist/echarts-graph-modularity.js b/dist/echarts-graph-modularity.js index aa7211a..736949e 100644 --- a/dist/echarts-graph-modularity.js +++ b/dist/echarts-graph-modularity.js @@ -7,7 +7,7 @@ exports["echarts-graph-modularity"] = factory(require("echarts")); else root["echarts-graph-modularity"] = factory(root["echarts"]); -})(this, function(__WEBPACK_EXTERNAL_MODULE_3__) { +})(this, function(__WEBPACK_EXTERNAL_MODULE_9__) { return /******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; @@ -83,8 +83,9 @@ module.exports = __webpack_require__(1); /* 1 */ /***/ (function(module, exports, __webpack_require__) { -var modularity = __webpack_require__(2); -var echarts = __webpack_require__(3); +var Modularity = __webpack_require__(2); +var echarts = __webpack_require__(9); +var createNGraph = __webpack_require__(10); function createModularityVisual(chartType) { return function (ecModel, api) { @@ -94,13 +95,16 @@ function createModularityVisual(chartType) { if (modularityOpt) { var graph = seriesModel.getGraph(); var idIndexMap = {}; - var nodeDataArr = graph.data.mapArray(function (idx) { + var ng = createNGraph(); + graph.data.each(function (idx) { var node = graph.getNodeByIndex(idx); idIndexMap[node.id] = idx; + ng.addNode(node.id); return node.id; }); - var edgeDataArr = graph.edgeData.mapArray('value', function (val, idx) { + graph.edgeData.each('value', function (val, idx) { var edge = graph.getEdgeByIndex(idx); + ng.addLink(edge.node1.id, edge.node2.id); return { source: edge.node1.id, target: edge.node2.id, @@ -108,8 +112,9 @@ function createModularityVisual(chartType) { }; }); - var community = modularity().nodes(nodeDataArr).edges(edgeDataArr).partition_init(); - var result = community(); + var modularity = new Modularity(seriesModel.get('modularity.resolution') || 1); + var result = modularity.execute(ng); + console.log(result); for (var id in result) { var comm = result[id]; @@ -144,412 +149,1665 @@ echarts.registerVisual(echarts.PRIORITY.VISUAL.CHART + 1, createModularityVisual /***/ }), /* 2 */ +/***/ (function(module, exports, __webpack_require__) { + +/* + Copyright 2008-2011 Gephi + Authors : Patick J. McSweeney , Sebastien Heymann + Website : http://www.gephi.org + + This file is part of Gephi. + + DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + + Copyright 2011 Gephi Consortium. All rights reserved. + + The contents of this file are subject to the terms of either the GNU + General Public License Version 3 only ("GPL") or the Common + Development and Distribution License("CDDL") (collectively, the + "License"). You may not use this file except in compliance with the + License. You can obtain a copy of the License at + http://gephi.org/about/legal/license-notice/ + or /cddl-1.0.txt and /gpl-3.0.txt. See the License for the + specific language governing permissions and limitations under the + License. When distributing the software, include this License Header + Notice in each file and include the License files at + /cddl-1.0.txt and /gpl-3.0.txt. If applicable, add the following below the + License Header, with the fields enclosed by brackets [] replaced by + your own identifying information: + "Portions Copyrighted [year] [name of copyright owner]" + + If you wish your version of this file to be governed by only the CDDL + or only the GPL Version 3, indicate your decision by adding + "[Contributor] elects to include this software in this distribution + under the [CDDL or GPL Version 3] license." If you do not indicate a + single choice of license, a recipient has the option to distribute + your version of this file under either the CDDL, the GPL Version 3 or + to extend the choice of license to its licensees as provided above. + However, if you add GPL Version 3 code and therefore, elected the GPL + Version 3 license, then the option applies only if the new code is + made subject to such option by the copyright holder. + + Contributor(s): Thomas Aynaud + + Portions Copyrighted 2011 Gephi Consortium. + */ +var CommunityStructure = __webpack_require__(3) + , centrality = __webpack_require__(6) + ; + +/** + * @constructor + */ +function Modularity (resolution, useWeight) { + this.isRandomized = false; + this.useWeight = useWeight; + this.resolution = resolution || 1.; + /** + * @type {CommunityStructure} + */ + this.structure = null; +} + +/** + * @param {IGraph} graph + */ +Modularity.prototype.execute = function (graph/*, AttributeModel attributeModel*/) { + + + this.structure = new CommunityStructure(graph, this.useWeight); + + var comStructure = new Array(graph.getNodesCount()); + + var computedModularityMetrics = this.computeModularity( + graph + , this.structure + , comStructure + , this.resolution + , this.isRandomized + , this.useWeight + ); + + var result = {}; + this.structure.map.forEach(function (i, node) { + result[node] = comStructure[i]; + }); + + return result; + +}; + + +/** + * + * @param {IGraph} graph + * @param {CommunityStructure} theStructure + * @param {Array.} comStructure + * @param {Number} currentResolution + * @param {Boolean} randomized + * @param {Boolean} weighted + * @returns {Object.} + */ +Modularity.prototype.computeModularity = function(graph, theStructure, comStructure, currentResolution, randomized, weighted) { + + + function getRandomInt(min, max) { + return Math.floor(Math.random() * (max - min)) + min; + } + + var totalWeight = theStructure.graphWeightSum; + var nodeDegrees = theStructure.weights.slice(); + + + var /** @type {Object.} */ results = Object.create(null); + + + var someChange = true; + + while (someChange) { + someChange = false; + var localChange = true; + while (localChange) { + localChange = false; + var start = 0; + if (randomized) { + //start = Math.abs(rand.nextInt()) % theStructure.N; + start = getRandomInt(0,theStructure.N); + } + var step = 0; + for (var i = start; step < theStructure.N; i = (i + 1) % theStructure.N) { + step++; + var bestCommunity = this.updateBestCommunity(theStructure, i, currentResolution); + if ((theStructure.nodeCommunities[i] != bestCommunity) && (bestCommunity != null)) { + theStructure.moveNodeTo(i, bestCommunity); + localChange = true; + } + + } + + someChange = localChange || someChange; + + } + + if (someChange) { + theStructure.zoomOut(); + } + } + + this.fillComStructure(graph, theStructure, comStructure); + + /* + //TODO: uncomment when finalQ will be implemented + var degreeCount = this.fillDegreeCount(graph, theStructure, comStructure, nodeDegrees, weighted); + + + var computedModularity = this._finalQ(comStructure, degreeCount, graph, theStructure, totalWeight, 1., weighted); + var computedModularityResolution = this._finalQ(comStructure, degreeCount, graph, theStructure, totalWeight, currentResolution, weighted); + + results["modularity"] = computedModularity; + results["modularityResolution"] = computedModularityResolution; + */ + + return results; +}; + + +/** + * @param {CommunityStructure} theStructure + * @param {Number} i + * @param {Number} currentResolution + * @returns {Community} + */ +Modularity.prototype.updateBestCommunity = function(theStructure, i, currentResolution) { + var best = this.q(i, theStructure.nodeCommunities[i], theStructure, currentResolution); + var bestCommunity = theStructure.nodeCommunities[i]; + //var /*Set*/ iter = theStructure.nodeConnectionsWeight[i].keySet(); + theStructure.nodeConnectionsWeight[i].forEach(function (_$$val, com) { + + var qValue = this.q(i, com, theStructure, currentResolution); + if (qValue > best) { + best = qValue; + bestCommunity = com; + } + + }, this); + return bestCommunity; +}; + +/** + * + * @param {IGraph} graph + * @param {CommunityStructure} theStructure + * @param {Array.} comStructure + * @returns {Array.} + */ +Modularity.prototype.fillComStructure = function(graph, theStructure, comStructure) { + + var count = 0; + + theStructure.communities.forEach(function (com) { + + com.nodes.forEach(function (node) { + + var hidden = theStructure.invMap.get(node); + hidden.nodes.forEach( function (nodeInt){ + comStructure[nodeInt] = count; + }); + + }); + count++; + + }); + + + return comStructure; +}; + +/** + * @param {IGraph} graph + * @param {CommunityStructure} theStructure + * @param {Array.} comStructure + * @param {Array.} nodeDegrees + * @param {Boolean} weighted + * @returns {Array.} + */ +Modularity.prototype.fillDegreeCount = function(graph, theStructure, comStructure, nodeDegrees, weighted) { + + var degreeCount = new Array(theStructure.communities.length); + var degreeCentrality = centrality.degree(graph); + + graph.forEachNode(function(node){ + + var index = theStructure.map.get(node); + if (weighted) { + degreeCount[comStructure[index]] += nodeDegrees[index]; + } else { + degreeCount[comStructure[index]] += degreeCentrality[node.id]; + } + + }); + return degreeCount; + +}; + + +/** + * + * @param {Array.} struct + * @param {Array.} degrees + * @param {IGraph} graph + * @param {CommunityStructure} theStructure + * @param {Number} totalWeight + * @param {Number} usedResolution + * @param {Boolean} weighted + * @returns {Number} + */ +Modularity.prototype._finalQ = function(struct, degrees, graph, theStructure, totalWeight, usedResolution, weighted) { + + //TODO: rewrite for wighted version of algorithm + throw new Error("not implemented properly"); + var res = 0; + var internal = new Array(degrees.length); + + graph.forEachNode(function(n){ + var n_index = theStructure.map.get(n); + + graph.forEachLinkedNode(n.id, function(neighbor){ + if (n == neighbor) { + return; + } + var neigh_index = theStructure.map.get(neighbor); + if (struct[neigh_index] == struct[n_index]) { + if (weighted) { + //throw new Error("weighted aren't implemented"); + //internal[struct[neigh_index]] += graph.getEdge(n, neighbor).getWeight(); + } else { + internal[struct[neigh_index]]++; + } + } + }.bind(this), false); + + }.bind(this)); + + for (var i = 0; i < degrees.length; i++) { + internal[i] /= 2.0; + res += usedResolution * (internal[i] / totalWeight) - Math.pow(degrees[i] / (2 * totalWeight), 2);//HERE + } + return res; +}; + + + +/** + * + * @param {Number} nodeId + * @param {Community} community + * @param {CommunityStructure} theStructure + * @param {Number} currentResolution + * @returns {Number} + */ +Modularity.prototype.q = function(nodeId, community, theStructure, currentResolution) { + + var edgesToFloat = theStructure.nodeConnectionsWeight[nodeId].get(community); + var edgesTo = 0; + if (edgesToFloat != null) { + edgesTo = edgesToFloat; + } + var weightSum = community.weightSum; + var nodeWeight = theStructure.weights[nodeId]; + var qValue = currentResolution * edgesTo - (nodeWeight * weightSum) / (2.0 * theStructure.graphWeightSum); + if ((theStructure.nodeCommunities[nodeId] == community) && (theStructure.nodeCommunities[nodeId].size() > 1)) { + qValue = currentResolution * edgesTo - (nodeWeight * (weightSum - nodeWeight)) / (2.0 * theStructure.graphWeightSum); + } + if ((theStructure.nodeCommunities[nodeId] == community) && (theStructure.nodeCommunities[nodeId].size() == 1)) { + qValue = 0.; + } + return qValue; + +}; + +module.exports = Modularity; + +/***/ }), +/* 3 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +var Community = __webpack_require__(4) + , ModEdge = __webpack_require__(5) + ; + +/** + * + * @param {IGraph} graph + * @param useWeight + * @param {CommunityStructure} structure + * @constructor + */ +function CommunityStructure(graph, useWeight) { + + //this.graph = graph; + this.N = graph.getNodesCount(); + this.graphWeightSum = 0; + this.structure = this; + + /** @type {Map.} */ + this.invMap = new Map(); + + /** @type {Array.< Map. >} */ + this.nodeConnectionsWeight = new Array(this.N); + + /** @type {Array.< Map. >} */ + this.nodeConnectionsCount = new Array(this.N); + + /** @type {Array.} */ + this.nodeCommunities = new Array(this.N); + + /** @type {Map.} */ + this.map = new Map(); + + /** @type {Array.< Array. >} */ + this.topology = new Array(this.N); + for (var i = 0; i < this.N; i++) this.topology[i] = []; + + /** @type {Array.} */ + this.communities = []; + + /**@type {Array.} */ + this.weights = new Array(this.N); + + var index = 0; + + graph.forEachNode(function (node) { + + this.map.set(node.id, index); + this.nodeCommunities[index] = new Community(this); + this.nodeConnectionsWeight[index] = new Map(); + this.nodeConnectionsCount[index] = new Map(); + this.weights[index] = 0; + this.nodeCommunities[index].seed(index); + var hidden = new Community(this); + hidden.nodes.add(index); + this.invMap.set(index, hidden); + this.communities.push(this.nodeCommunities[index]); + index++; + + }.bind(this)); + + + graph.forEachLink(function (link) { + + var node_index = this.map.get(link.fromId) + , neighbor_index = this.map.get(link.toId) + , weight = 1 + ; + + if (node_index === neighbor_index) { + return; + } + + if (useWeight) { + weight = link.data.weight; + } + + this.setUpLink(node_index, neighbor_index, weight); + this.setUpLink(neighbor_index, node_index, weight); + + + }.bind(this)); + + + this.graphWeightSum /= 2.0; +} + + +CommunityStructure.prototype.setUpLink = function (node_index, neighbor_index, weight) { + + this.weights[node_index] += weight; + var /** @type {ModEdge} */ me = new ModEdge(node_index, neighbor_index, weight); + this.topology[node_index].push(me); + var /** @type {Community} **/ adjCom = this.nodeCommunities[neighbor_index]; + this.nodeConnectionsWeight[node_index].set(adjCom, weight); + this.nodeConnectionsCount[node_index].set(adjCom, 1); + this.nodeCommunities[node_index].connectionsWeight.set(adjCom, weight); + this.nodeCommunities[node_index].connectionsCount.set(adjCom, 1); + this.nodeConnectionsWeight[neighbor_index].set(this.nodeCommunities[node_index], weight); + this.nodeConnectionsCount[neighbor_index].set(this.nodeCommunities[node_index], 1); + this.nodeCommunities[neighbor_index].connectionsWeight.set(this.nodeCommunities[node_index], weight); + this.nodeCommunities[neighbor_index].connectionsCount.set(this.nodeCommunities[node_index], 1); + this.graphWeightSum += weight; + +}; + +/** + * @param {Number} node + * @param {Community} to + */ +CommunityStructure.prototype.addNodeTo = function (node, to) { + + to.add(node); + this.nodeCommunities[node] = to; + + var nodeTopology = this.topology[node]; + for (var topologyKey in nodeTopology) { + + //noinspection JSUnfilteredForInLoop + var /** @type {ModEdge} */ e = nodeTopology[topologyKey]; + + var neighbor = e.target; + + + //Remove Node Connection to this community + var neighEdgesTo = this.nodeConnectionsWeight[neighbor].get(to); + if (neighEdgesTo === undefined) { + this.nodeConnectionsWeight[neighbor].set(to, e.weight); + } else { + this.nodeConnectionsWeight[neighbor].set(to, neighEdgesTo + e.weight); + } + + var neighCountEdgesTo = this.nodeConnectionsCount[neighbor].get(to); + if (neighCountEdgesTo === undefined) { + this.nodeConnectionsCount[neighbor].set(to, 1); + } else { + this.nodeConnectionsCount[neighbor].set(to, neighCountEdgesTo + 1); + } + + + var /** @type {Community} */ adjCom = this.nodeCommunities[neighbor]; + var wEdgesto = adjCom.connectionsWeight.get(to); + if (wEdgesto === undefined) { + adjCom.connectionsWeight.set(to, e.weight); + } else { + adjCom.connectionsWeight.set(to, wEdgesto + e.weight); + } + + var cEdgesto = adjCom.connectionsCount.get(to); + if (cEdgesto === undefined) { + adjCom.connectionsCount.set(to, 1); + } else { + adjCom.connectionsCount.set(to, cEdgesto + 1); + } + + var nodeEdgesTo = this.nodeConnectionsWeight[node].get(adjCom); + if (nodeEdgesTo === undefined) { + this.nodeConnectionsWeight[node].set(adjCom, e.weight); + } else { + this.nodeConnectionsWeight[node].set(adjCom, nodeEdgesTo + e.weight); + } + + var nodeCountEdgesTo = this.nodeConnectionsCount[node].get(adjCom); + if (nodeCountEdgesTo === undefined) { + this.nodeConnectionsCount[node].set(adjCom, 1); + } else { + this.nodeConnectionsCount[node].set(adjCom, nodeCountEdgesTo + 1); + } + + if (to != adjCom) { + var comEdgesto = to.connectionsWeight.get(adjCom); + if (comEdgesto === undefined) { + to.connectionsWeight.set(adjCom, e.weight); + } else { + to.connectionsWeight.set(adjCom, comEdgesto + e.weight); + } + + var comCountEdgesto = to.connectionsCount.get(adjCom); + if (comCountEdgesto === undefined) { + to.connectionsCount.set(adjCom, 1); + } else { + to.connectionsCount.set(adjCom, comCountEdgesto + 1); + } + + } + } +}; + +/** + * @param {Number} node + * @param {Community} source + */ +CommunityStructure.prototype.removeNodeFrom = function (node, source) { + + var community = this.nodeCommunities[node]; + + + var nodeTopology = this.topology[node]; + for (var topologyKey in nodeTopology) { + + //noinspection JSUnfilteredForInLoop + var /** @type {ModEdge} */ e = nodeTopology[topologyKey]; + + var neighbor = e.target; + + //Remove Node Connection to this community + var edgesTo = this.nodeConnectionsWeight[neighbor].get(community); + var countEdgesTo = this.nodeConnectionsCount[neighbor].get(community); + + if ((countEdgesTo - 1) == 0) { + this.nodeConnectionsWeight[neighbor].delete(community); + this.nodeConnectionsCount[neighbor].delete(community); + } else { + this.nodeConnectionsWeight[neighbor].set(community, edgesTo - e.weight); + this.nodeConnectionsCount[neighbor].set(community, countEdgesTo - 1); + } + + + //Remove Adjacency Community's connection to this community + var adjCom = this.nodeCommunities[neighbor]; + var oEdgesto = adjCom.connectionsWeight.get(community); + var oCountEdgesto = adjCom.connectionsCount.get(community); + if ((oCountEdgesto - 1) == 0) { + adjCom.connectionsWeight.delete(community); + adjCom.connectionsCount.delete(community); + } else { + adjCom.connectionsWeight.set(community, oEdgesto - e.weight); + adjCom.connectionsCount.set(community, oCountEdgesto - 1); + } + + if (node == neighbor) { + continue; + } + + if (adjCom != community) { + + var comEdgesto = community.connectionsWeight.get(adjCom); + var comCountEdgesto = community.connectionsCount.get(adjCom); + + if (comCountEdgesto - 1 == 0) { + community.connectionsWeight.delete(adjCom); + community.connectionsCount.delete(adjCom); + } else { + community.connectionsWeight.set(adjCom, comEdgesto - e.weight); + community.connectionsCount.set(adjCom, comCountEdgesto - 1); + } + + } + + var nodeEdgesTo = this.nodeConnectionsWeight[node].get(adjCom); + var nodeCountEdgesTo = this.nodeConnectionsCount[node].get(adjCom); + + if ((nodeCountEdgesTo - 1) == 0) { + this.nodeConnectionsWeight[node].delete(adjCom); + this.nodeConnectionsCount[node].delete(adjCom); + } else { + this.nodeConnectionsWeight[node].set(adjCom, nodeEdgesTo - e.weight); + this.nodeConnectionsCount[node].set(adjCom, nodeCountEdgesTo - 1); + } + + } + + source.remove(node); +}; + +/** + * @param {Number} node + * @param {Community} to + */ +CommunityStructure.prototype.moveNodeTo = function (node, to) { + + var source = this.nodeCommunities[node]; + this.removeNodeFrom(node, source); + this.addNodeTo(node, to); + +}; + + +CommunityStructure.prototype.zoomOut = function () { + var realCommunities = this.communities.reduce(function (arr, value) { + arr.push(value); + return arr; + }, []); + var M = realCommunities.length; // size + var /** @type Array.< Array. > */ newTopology = new Array(M); + var index = 0; + + this.nodeCommunities = new Array(M); + this.nodeConnectionsWeight = new Array(M); + this.nodeConnectionsCount = new Array(M); + + var /** @type Map.*/ newInvMap = new Map(); + realCommunities.forEach(function (com) { + + var weightSum = 0; + this.nodeConnectionsWeight[index] = new Map(); + this.nodeConnectionsCount[index] = new Map(); + newTopology[index] = []; + this.nodeCommunities[index] = new Community(com); + //var iter = com.connectionsWeight.keySet(); + + var hidden = new Community(this.structure); + + com.nodes.forEach(function (nodeInt) { + + var oldHidden = this.invMap.get(nodeInt); + oldHidden.nodes.forEach(hidden.nodes.add.bind(hidden.nodes)); + + }, this); + + newInvMap.set(index, hidden); + com.connectionsWeight.forEach(function (weight, adjCom) { + + var target = realCommunities.indexOf(adjCom); + if (!~target) return; + if (target == index) { + weightSum += 2. * weight; + } else { + weightSum += weight; + } + var e = new ModEdge(index, target, weight); + newTopology[index].push(e); + + }, this); + + this.weights[index] = weightSum; + this.nodeCommunities[index].seed(index); + + index++; + + }.bind(this)); + + this.communities = []; + + for (var i = 0; i < M; i++) { + var com = this.nodeCommunities[i]; + this.communities.push(com); + for (var ei in newTopology[i]) { + //noinspection JSUnfilteredForInLoop + var e = newTopology[i][ei]; + this.nodeConnectionsWeight[i].set(this.nodeCommunities[e.target], e.weight); + this.nodeConnectionsCount[i].set(this.nodeCommunities[e.target], 1); + com.connectionsWeight.set(this.nodeCommunities[e.target], e.weight); + com.connectionsCount.set(this.nodeCommunities[e.target], 1); + } + + } + + this.N = M; + this.topology = newTopology; + this.invMap = newInvMap; + +}; + +module.exports = CommunityStructure; + +/***/ }), +/* 4 */ /***/ (function(module, exports) { -/* - Author: Corneliu S. (github.com/upphiminn) - This is a javascript implementation of the Louvain - community detection algorithm (http://arxiv.org/abs/0803.0476) - Based on https://bitbucket.org/taynaud/python-louvain/overview - */ -jLouvain = function () { - //Constants - var __PASS_MAX = -1; - var __MIN = 0.0000001; - - //Local vars - var original_graph_nodes; - var original_graph_edges; - var original_graph = {}; - var partition_init; - - //Helpers - function make_set(array) { - var set = {}; - array.forEach(function (d, i) { - set[d] = true; - }); +/** + * @param {CommunityStructure|Community} com + * @constructor + */ +function Community(com) { + + /** @type {CommunityStructure} */ + this.structure = com.structure ? com.structure : com; + + /** @type {Map.} */ + this.connectionsWeight = new Map(); + + /** @type {Map.} */ + this.connectionsCount = new Map(); + + /** @type {Set.} */ + this.nodes = new Set; + + this.weightSum = 0; + + +} + +/** + * @public + * @returns {Number} + */ +Community.prototype.size = function() { + return this.nodes.size; +}; + + +/** + * @param {Number} node + */ +Community.prototype.seed = function(node) { + + this.nodes.add(node); + this.weightSum += this.structure.weights[node]; + +}; + +/** + * @param {Number} nodeId + * @returns {boolean} + */ +Community.prototype.add = function(nodeId) { + + this.nodes.add(nodeId); + this.weightSum += this.structure.weights[nodeId]; + return true; + +}; + +/** + * @param {Number} node + * @returns {boolean} + */ +Community.prototype.remove = function(node) { + + var result = this.nodes.delete(node); + + this.weightSum -= this.structure.weights[node]; + if (!this.nodes.size) { + var index = this.structure.communities.indexOf(this); + delete this.structure.communities[index]; + } + + return result; +}; + +module.exports = Community; - return Object.keys(set); - } - function obj_values(obj) { - var vals = []; - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - vals.push(obj[key]); - } - } +/***/ }), +/* 5 */ +/***/ (function(module, exports) { - return vals; - } +/** + * + * @param s + * @param t + * @param w + * @constructor + */ +function ModEdge(s, t, w) { + /** @type {Number} */ + this.source = s; + /** @type {Number} */ + this.target = t; + /** @type {Number} */ + this.weight = w; +} + +module.exports = ModEdge; - function get_degree_for_node(graph, node) { - var neighbours = graph._assoc_mat[node] ? Object.keys(graph._assoc_mat[node]) : []; - var weight = 0; - neighbours.forEach(function (neighbour, i) { - var value = graph._assoc_mat[node][neighbour] || 1; - if (node === neighbour) { - value *= 2; - } - weight += value; - }); - return weight; - } +/***/ }), +/* 6 */ +/***/ (function(module, exports, __webpack_require__) { - function get_neighbours_of_node(graph, node) { - if (typeof graph._assoc_mat[node] === 'undefined') { - return []; - } +module.exports.degree = __webpack_require__(7); +module.exports.betweenness = __webpack_require__(8); - var neighbours = Object.keys(graph._assoc_mat[node]); - return neighbours; - } +/***/ }), +/* 7 */ +/***/ (function(module, exports) { +module.exports = degree; + +/** + * Calculates graph nodes degree centrality (in/out or both). + * + * @see http://en.wikipedia.org/wiki/Centrality#Degree_centrality + * + * @param {ngraph.graph} graph object for which we are calculating centrality. + * @param {string} [kind=both] What kind of degree centrality needs to be calculated: + * 'in' - calculate in-degree centrality + * 'out' - calculate out-degree centrality + * 'inout' - (default) generic degree centrality is calculated + */ +function degree(graph, kind) { + var getNodeDegree, + sortedDegrees = [], + result = Object.create(null), + nodeDegree; + + kind = (kind || 'both').toLowerCase(); + if (kind === 'both' || kind === 'inout') { + getNodeDegree = inoutDegreeCalculator; + } else if (kind === 'in') { + getNodeDegree = inDegreeCalculator; + } else if (kind === 'out') { + getNodeDegree = outDegreeCalculator; + } else { + throw new Error('Expected centrality degree kind is: in, out or both'); + } + + graph.forEachNode(calculateNodeDegree); + + return result; + + function calculateNodeDegree(node) { + var links = graph.getLinks(node.id); + result[node.id] = getNodeDegree(links, node.id); + } +} - function get_edge_weight(graph, node1, node2) { - return graph._assoc_mat[node1] ? graph._assoc_mat[node1][node2] : undefined; - } +function inDegreeCalculator(links, nodeId) { + var total = 0; + if (!links) return total; - function get_graph_size(graph) { - var size = 0; - graph.edges.forEach(function (edge) { - size += edge.weight; - }); + for (var i = 0; i < links.length; i += 1) { + total += (links[i].toId === nodeId) ? 1 : 0; + } + return total; +} - return size; - } +function outDegreeCalculator(links, nodeId) { + var total = 0; + if (!links) return total; - function add_edge_to_graph(graph, edge) { - update_assoc_mat(graph, edge); + for (var i = 0; i < links.length; i += 1) { + total += (links[i].fromId === nodeId) ? 1 : 0; + } + return total; +} - var edge_index = graph.edges.map(function (d) { - return d.source + '_' + d.target; - }).indexOf(edge.source + '_' + edge.target); +function inoutDegreeCalculator(links) { + if (!links) return 0; - if (edge_index !== -1) { - graph.edges[edge_index].weight = edge.weight; - } else { - graph.edges.push(edge); - } + return links.length; +} + + +/***/ }), +/* 8 */ +/***/ (function(module, exports) { + +module.exports = betweennes; + +/** + * I'm using http://www.inf.uni-konstanz.de/algo/publications/b-vspbc-08.pdf + * as a reference for this implementation + */ +function betweennes(graph, oriented) { + var Q = [], + S = []; // Queue and Stack + // list of predcessors on shorteest paths from source + var pred = Object.create(null); + // distance from source + var dist = Object.create(null); + // number of shortest paths from source to key + var sigma = Object.create(null); + // dependency of source on key + var delta = Object.create(null); + + var currentNode; + var centrality = Object.create(null); + + graph.forEachNode(setCentralityToZero); + graph.forEachNode(calculateCentrality); + + if (!oriented) { + // The centrality scores need to be divided by two if the graph is not oriented, + // since all shortest paths are considered twice + Object.keys(centrality).forEach(divideByTwo); + } + + return centrality; + + function divideByTwo(key) { + centrality[key] /= 2; + } + + function setCentralityToZero(node) { + centrality[node.id] = 0; + } + + function calculateCentrality(node) { + currentNode = node.id; + singleSourceShortestPath(currentNode); + accumulate(); + } + + function accumulate() { + graph.forEachNode(setDeltaToZero); + while (S.length) { + var w = S.pop(); + var coeff = (1 + delta[w])/sigma[w]; + var predcessors = pred[w]; + for (var idx = 0; idx < predcessors.length; ++idx) { + var v = predcessors[idx]; + delta[v] += sigma[v] * coeff; + } + if (w !== currentNode) { + centrality[w] += delta[w]; + } + } + } + + function setDeltaToZero(node) { + delta[node.id] = 0; + } + + function singleSourceShortestPath(source) { + graph.forEachNode(initNode); + dist[source] = 0; + sigma[source] = 1; + Q.push(source); + + while (Q.length) { + var v = Q.shift(); + var dedup = Object.create(null); + S.push(v); + graph.forEachLinkedNode(v, toId, oriented); } - function make_assoc_mat(edge_list) { - var mat = {}; - edge_list.forEach(function (edge, i) { - mat[edge.source] = mat[edge.source] || {}; - mat[edge.source][edge.target] = edge.weight; - mat[edge.target] = mat[edge.target] || {}; - mat[edge.target][edge.source] = edge.weight; - }); + function toId(otherNode) { + // NOTE: This code will also consider multi-edges, which are often + // ignored by popular software (Gephi/NetworkX). Depending on your use + // case this may not be desired and deduping needs to be performed. To + // save memory I'm not deduping here... + processNode(otherNode.id); + } - return mat; + function initNode(node) { + var nodeId = node.id; + pred[nodeId] = []; // empty list + dist[nodeId] = -1; + sigma[nodeId] = 0; } - function update_assoc_mat(graph, edge) { - graph._assoc_mat[edge.source] = graph._assoc_mat[edge.source] || {}; - graph._assoc_mat[edge.source][edge.target] = edge.weight; - graph._assoc_mat[edge.target] = graph._assoc_mat[edge.target] || {}; - graph._assoc_mat[edge.target][edge.source] = edge.weight; + function processNode(w) { + // path discovery + if (dist[w] === -1) { + // Node w is found for the first time + dist[w] = dist[v] + 1; + Q.push(w); + } + // path counting + if (dist[w] === dist[v] + 1) { + // edge (v, w) on a shortest path + sigma[w] += sigma[v]; + pred[w].push(v); + } } + } +} + - function clone(obj) { - if (obj === null || typeof(obj) !== 'object') - return obj; +/***/ }), +/* 9 */ +/***/ (function(module, exports) { - var temp = obj.constructor(); +module.exports = __WEBPACK_EXTERNAL_MODULE_9__; - for (var key in obj) { - temp[key] = clone(obj[key]); - } +/***/ }), +/* 10 */ +/***/ (function(module, exports, __webpack_require__) { + +/** + * @fileOverview Contains definition of the core graph object. + */ + +/** + * @example + * var graph = require('ngraph.graph')(); + * graph.addNode(1); // graph has one node. + * graph.addLink(2, 3); // now graph contains three nodes and one link. + * + */ +module.exports = createGraph; - return temp; +var eventify = __webpack_require__(11); + +/** + * Creates a new graph + */ +function createGraph(options) { + // Graph structure is maintained as dictionary of nodes + // and array of links. Each node has 'links' property which + // hold all links related to that node. And general links + // array is used to speed up all links enumeration. This is inefficient + // in terms of memory, but simplifies coding. + options = options || {}; + if (options.uniqueLinkId === undefined) { + // Request each link id to be unique between same nodes. This negatively + // impacts `addLink()` performance (O(n), where n - number of edges of each + // vertex), but makes operations with multigraphs more accessible. + options.uniqueLinkId = true; + } + + var nodes = typeof Object.create === 'function' ? Object.create(null) : {}, + links = [], + // Hash of multi-edges. Used to track ids of edges between same nodes + multiEdges = {}, + nodesCount = 0, + suspendEvents = 0, + + forEachNode = createNodeIterator(), + createLink = options.uniqueLinkId ? createUniqueLink : createSingleLink, + + // Our graph API provides means to listen to graph changes. Users can subscribe + // to be notified about changes in the graph by using `on` method. However + // in some cases they don't use it. To avoid unnecessary memory consumption + // we will not record graph changes until we have at least one subscriber. + // Code below supports this optimization. + // + // Accumulates all changes made during graph updates. + // Each change element contains: + // changeType - one of the strings: 'add', 'remove' or 'update'; + // node - if change is related to node this property is set to changed graph's node; + // link - if change is related to link this property is set to changed graph's link; + changes = [], + recordLinkChange = noop, + recordNodeChange = noop, + enterModification = noop, + exitModification = noop; + + // this is our public API: + var graphPart = { + /** + * Adds node to the graph. If node with given id already exists in the graph + * its data is extended with whatever comes in 'data' argument. + * + * @param nodeId the node's identifier. A string or number is preferred. + * @param [data] additional data for the node being added. If node already + * exists its data object is augmented with the new one. + * + * @return {node} The newly added node or node with given id if it already exists. + */ + addNode: addNode, + + /** + * Adds a link to the graph. The function always create a new + * link between two nodes. If one of the nodes does not exists + * a new node is created. + * + * @param fromId link start node id; + * @param toId link end node id; + * @param [data] additional data to be set on the new link; + * + * @return {link} The newly created link + */ + addLink: addLink, + + /** + * Removes link from the graph. If link does not exist does nothing. + * + * @param link - object returned by addLink() or getLinks() methods. + * + * @returns true if link was removed; false otherwise. + */ + removeLink: removeLink, + + /** + * Removes node with given id from the graph. If node does not exist in the graph + * does nothing. + * + * @param nodeId node's identifier passed to addNode() function. + * + * @returns true if node was removed; false otherwise. + */ + removeNode: removeNode, + + /** + * Gets node with given identifier. If node does not exist undefined value is returned. + * + * @param nodeId requested node identifier; + * + * @return {node} in with requested identifier or undefined if no such node exists. + */ + getNode: getNode, + + /** + * Gets number of nodes in this graph. + * + * @return number of nodes in the graph. + */ + getNodesCount: function() { + return nodesCount; + }, + + /** + * Gets total number of links in the graph. + */ + getLinksCount: function() { + return links.length; + }, + + /** + * Gets all links (inbound and outbound) from the node with given id. + * If node with given id is not found null is returned. + * + * @param nodeId requested node identifier. + * + * @return Array of links from and to requested node if such node exists; + * otherwise null is returned. + */ + getLinks: getLinks, + + /** + * Invokes callback on each node of the graph. + * + * @param {Function(node)} callback Function to be invoked. The function + * is passed one argument: visited node. + */ + forEachNode: forEachNode, + + /** + * Invokes callback on every linked (adjacent) node to the given one. + * + * @param nodeId Identifier of the requested node. + * @param {Function(node, link)} callback Function to be called on all linked nodes. + * The function is passed two parameters: adjacent node and link object itself. + * @param oriented if true graph treated as oriented. + */ + forEachLinkedNode: forEachLinkedNode, + + /** + * Enumerates all links in the graph + * + * @param {Function(link)} callback Function to be called on all links in the graph. + * The function is passed one parameter: graph's link object. + * + * Link object contains at least the following fields: + * fromId - node id where link starts; + * toId - node id where link ends, + * data - additional data passed to graph.addLink() method. + */ + forEachLink: forEachLink, + + /** + * Suspend all notifications about graph changes until + * endUpdate is called. + */ + beginUpdate: enterModification, + + /** + * Resumes all notifications about graph changes and fires + * graph 'changed' event in case there are any pending changes. + */ + endUpdate: exitModification, + + /** + * Removes all nodes and links from the graph. + */ + clear: clear, + + /** + * Detects whether there is a link between two nodes. + * Operation complexity is O(n) where n - number of links of a node. + * NOTE: this function is synonim for getLink() + * + * @returns link if there is one. null otherwise. + */ + hasLink: getLink, + + /** + * Gets an edge between two nodes. + * Operation complexity is O(n) where n - number of links of a node. + * + * @param {string} fromId link start identifier + * @param {string} toId link end identifier + * + * @returns link if there is one. null otherwise. + */ + getLink: getLink + }; + + // this will add `on()` and `fire()` methods. + eventify(graphPart); + + monitorSubscribers(); + + return graphPart; + + function monitorSubscribers() { + var realOn = graphPart.on; + + // replace real `on` with our temporary on, which will trigger change + // modification monitoring: + graphPart.on = on; + + function on() { + // now it's time to start tracking stuff: + graphPart.beginUpdate = enterModification = enterModificationReal; + graphPart.endUpdate = exitModification = exitModificationReal; + recordLinkChange = recordLinkChangeReal; + recordNodeChange = recordNodeChangeReal; + + // this will replace current `on` method with real pub/sub from `eventify`. + graphPart.on = realOn; + // delegate to real `on` handler: + return realOn.apply(graphPart, arguments); + } + } + + function recordLinkChangeReal(link, changeType) { + changes.push({ + link: link, + changeType: changeType + }); + } + + function recordNodeChangeReal(node, changeType) { + changes.push({ + node: node, + changeType: changeType + }); + } + + function addNode(nodeId, data) { + if (nodeId === undefined) { + throw new Error('Invalid node identifier'); } - //Core-Algorithm Related - function init_status(graph, status, part) { - status['nodes_to_com'] = {}; - status['total_weight'] = 0; - status['internals'] = {}; - status['degrees'] = {}; - status['gdegrees'] = {}; - status['loops'] = {}; - status['total_weight'] = get_graph_size(graph); - - if (typeof part === 'undefined') { - graph.nodes.forEach(function (node, i) { - status.nodes_to_com[node] = i; - var deg = get_degree_for_node(graph, node); - - if (deg < 0) - throw 'Bad graph type, use positive weights!'; - - status.degrees[i] = deg; - status.gdegrees[node] = deg; - status.loops[node] = get_edge_weight(graph, node, node) || 0; - status.internals[i] = status.loops[node]; - }); - } else { - graph.nodes.forEach(function (node, i) { - var com = part[node]; - status.nodes_to_com[node] = com; - var deg = get_degree_for_node(graph, node); - status.degrees[com] = (status.degrees[com] || 0) + deg; - status.gdegrees[node] = deg; - var inc = 0.0; - - var neighbours = get_neighbours_of_node(graph, node); - neighbours.forEach(function (neighbour, i) { - var weight = graph._assoc_mat[node][neighbour]; - - if (weight <= 0) { - throw "Bad graph type, use positive weights"; - } + enterModification(); - if (part[neighbour] === com) { - if (neighbour === node) { - inc += weight; - } else { - inc += weight / 2.0; - } - } - }); - status.internals[com] = (status.internals[com] || 0) + inc; - }); - } + var node = getNode(nodeId); + if (!node) { + node = new Node(nodeId); + nodesCount++; + recordNodeChange(node, 'add'); + } else { + recordNodeChange(node, 'update'); } - function __modularity(status) { - var links = status.total_weight; - var result = 0.0; - var communities = make_set(obj_values(status.nodes_to_com)); + node.data = data; - communities.forEach(function (com, i) { - var in_degree = status.internals[com] || 0; - var degree = status.degrees[com] || 0; - if (links > 0) { - result = result + in_degree / links - Math.pow((degree / (2.0 * links)), 2); - } - }); + nodes[nodeId] = node; + + exitModification(); + return node; + } + + function getNode(nodeId) { + return nodes[nodeId]; + } - return result; + function removeNode(nodeId) { + var node = getNode(nodeId); + if (!node) { + return false; } - function __neighcom(node, graph, status) { - // compute the communities in the neighb. of the node, with the graph given by - // node_to_com - var weights = {}; - var neighboorhood = get_neighbours_of_node(graph, node);//make iterable; - - neighboorhood.forEach(function (neighbour, i) { - if (neighbour !== node) { - var weight = graph._assoc_mat[node][neighbour] || 1; - var neighbourcom = status.nodes_to_com[neighbour]; - weights[neighbourcom] = (weights[neighbourcom] || 0) + weight; - } - }); + enterModification(); - return weights; + if (node.links) { + while (node.links.length) { + var link = node.links[0]; + removeLink(link); + } } - function __insert(node, com, weight, status) { - //insert node into com and modify status - status.nodes_to_com[node] = +com; - status.degrees[com] = (status.degrees[com] || 0) + (status.gdegrees[node] || 0); - status.internals[com] = (status.internals[com] || 0) + weight + (status.loops[node] || 0); + delete nodes[nodeId]; + nodesCount--; + + recordNodeChange(node, 'remove'); + + exitModification(); + + return true; + } + + + function addLink(fromId, toId, data) { + enterModification(); + + var fromNode = getNode(fromId) || addNode(fromId); + var toNode = getNode(toId) || addNode(toId); + + var link = createLink(fromId, toId, data); + + links.push(link); + + // TODO: this is not cool. On large graphs potentially would consume more memory. + addLinkToNode(fromNode, link); + if (fromId !== toId) { + // make sure we are not duplicating links for self-loops + addLinkToNode(toNode, link); } - function __remove(node, com, weight, status) { - //remove node from com and modify status - status.degrees[com] = ((status.degrees[com] || 0) - (status.gdegrees[node] || 0)); - status.internals[com] = ((status.internals[com] || 0) - weight - (status.loops[node] || 0)); - status.nodes_to_com[node] = -1; + recordLinkChange(link, 'add'); + + exitModification(); + + return link; + } + + function createSingleLink(fromId, toId, data) { + var linkId = makeLinkId(fromId, toId); + return new Link(fromId, toId, data, linkId); + } + + function createUniqueLink(fromId, toId, data) { + // TODO: Get rid of this method. + var linkId = makeLinkId(fromId, toId); + var isMultiEdge = multiEdges.hasOwnProperty(linkId); + if (isMultiEdge || getLink(fromId, toId)) { + if (!isMultiEdge) { + multiEdges[linkId] = 0; + } + var suffix = '@' + (++multiEdges[linkId]); + linkId = makeLinkId(fromId + suffix, toId + suffix); } - function __renumber(dict) { - var count = 0; - var ret = clone(dict); //deep copy :) - var new_values = {}; - var dict_keys = Object.keys(dict); - dict_keys.forEach(function (key) { - var value = dict[key]; - var new_value = typeof new_values[value] === 'undefined' ? -1 : new_values[value]; - if (new_value === -1) { - new_values[value] = count; - new_value = count; - count = count + 1; - } - ret[key] = new_value; - }); + return new Link(fromId, toId, data, linkId); + } - return ret; + function getLinks(nodeId) { + var node = getNode(nodeId); + return node ? node.links : null; + } + + function removeLink(link) { + if (!link) { + return false; + } + var idx = indexOfElementInArray(link, links); + if (idx < 0) { + return false; } - function __one_level(graph, status) { - //Compute one level of the Communities Dendogram. - var modif = true; - var nb_pass_done = 0; - var cur_mod = __modularity(status); - var new_mod = cur_mod; - - while (modif && nb_pass_done !== __PASS_MAX) { - cur_mod = new_mod; - modif = false; - nb_pass_done += 1 - - graph.nodes.forEach(function (node, i) { - var com_node = status.nodes_to_com[node]; - var degc_totw = (status.gdegrees[node] || 0) / (status.total_weight * 2.0); - var neigh_communities = __neighcom(node, graph, status); - __remove(node, com_node, (neigh_communities[com_node] || 0.0), status); - var best_com = com_node; - var best_increase = 0; - var neigh_communities_entries = Object.keys(neigh_communities);//make iterable; - - neigh_communities_entries.forEach(function (com, i) { - var incr = neigh_communities[com] - (status.degrees[com] || 0.0) * degc_totw; - if (incr > best_increase) { - best_increase = incr; - best_com = com; - } - }); + enterModification(); - __insert(node, best_com, neigh_communities[best_com] || 0, status); + links.splice(idx, 1); - if (best_com !== com_node) { - modif = true; - } - }); - new_mod = __modularity(status); - if (new_mod - cur_mod < __MIN) { - break; - } - } + var fromNode = getNode(link.fromId); + var toNode = getNode(link.toId); + + if (fromNode) { + idx = indexOfElementInArray(link, fromNode.links); + if (idx >= 0) { + fromNode.links.splice(idx, 1); + } } - function induced_graph(partition, graph) { - var ret = {nodes: [], edges: [], _assoc_mat: {}}; - var w_prec, weight; - //add nodes from partition values - var partition_values = obj_values(partition); - ret.nodes = ret.nodes.concat(make_set(partition_values)); //make set - graph.edges.forEach(function (edge, i) { - weight = edge.weight || 1; - var com1 = partition[edge.source]; - var com2 = partition[edge.target]; - w_prec = (get_edge_weight(ret, com1, com2) || 0); - var new_weight = (w_prec + weight); - add_edge_to_graph(ret, {'source': com1, 'target': com2, 'weight': new_weight}); - }); + if (toNode) { + idx = indexOfElementInArray(link, toNode.links); + if (idx >= 0) { + toNode.links.splice(idx, 1); + } + } + + recordLinkChange(link, 'remove'); + + exitModification(); + + return true; + } - return ret; + function getLink(fromNodeId, toNodeId) { + // TODO: Use sorted links to speed this up + var node = getNode(fromNodeId), + i; + if (!node || !node.links) { + return null; } - function partition_at_level(dendogram, level) { - var partition = clone(dendogram[0]); - for (var i = 1; i < level + 1; i++) { - Object.keys(partition).forEach(function (key, j) { - var node = key; - var com = partition[key]; - partition[node] = dendogram[i][com]; - }); - } + for (i = 0; i < node.links.length; ++i) { + var link = node.links[i]; + if (link.fromId === fromNodeId && link.toId === toNodeId) { + return link; + } + } - return partition; + return null; // no link. + } + + function clear() { + enterModification(); + forEachNode(function(node) { + removeNode(node.id); + }); + exitModification(); + } + + function forEachLink(callback) { + var i, length; + if (typeof callback === 'function') { + for (i = 0, length = links.length; i < length; ++i) { + callback(links[i]); + } } + } + function forEachLinkedNode(nodeId, callback, oriented) { + var node = getNode(nodeId); - function generate_dendogram(graph, part_init) { - if (graph.edges.length === 0) { - var part = {}; - graph.nodes.forEach(function (node, i) { - part[node] = node; - }); - return part; + if (node && node.links && typeof callback === 'function') { + if (oriented) { + return forEachOrientedLink(node.links, nodeId, callback); + } else { + return forEachNonOrientedLink(node.links, nodeId, callback); + } + } + } + + function forEachNonOrientedLink(links, nodeId, callback) { + var quitFast; + for (var i = 0; i < links.length; ++i) { + var link = links[i]; + var linkedNodeId = link.fromId === nodeId ? link.toId : link.fromId; + + quitFast = callback(nodes[linkedNodeId], link); + if (quitFast) { + return true; // Client does not need more iterations. Break now. + } + } + } + + function forEachOrientedLink(links, nodeId, callback) { + var quitFast; + for (var i = 0; i < links.length; ++i) { + var link = links[i]; + if (link.fromId === nodeId) { + quitFast = callback(nodes[link.toId], link); + if (quitFast) { + return true; // Client does not need more iterations. Break now. } - var status = {}; - - init_status(original_graph, status, part_init); - var mod = __modularity(status); - var status_list = []; - __one_level(original_graph, status); - var new_mod = __modularity(status); - var partition = __renumber(status.nodes_to_com); - status_list.push(partition); - mod = new_mod; - var current_graph = induced_graph(partition, original_graph); - init_status(current_graph, status); - - while (true) { - __one_level(current_graph, status); - new_mod = __modularity(status); - if (new_mod - mod < __MIN) { - break; - } + } + } + } + + // we will not fire anything until users of this library explicitly call `on()` + // method. + function noop() {} + + // Enter, Exit modification allows bulk graph updates without firing events. + function enterModificationReal() { + suspendEvents += 1; + } + + function exitModificationReal() { + suspendEvents -= 1; + if (suspendEvents === 0 && changes.length > 0) { + graphPart.fire('changed', changes); + changes.length = 0; + } + } + + function createNodeIterator() { + // Object.keys iterator is 1.3x faster than `for in` loop. + // See `https://github.com/anvaka/ngraph.graph/tree/bench-for-in-vs-obj-keys` + // branch for perf test + return Object.keys ? objectKeysIterator : forInIterator; + } + + function objectKeysIterator(callback) { + if (typeof callback !== 'function') { + return; + } - partition = __renumber(status.nodes_to_com); - status_list.push(partition); + var keys = Object.keys(nodes); + for (var i = 0; i < keys.length; ++i) { + if (callback(nodes[keys[i]])) { + return true; // client doesn't want to proceed. Return. + } + } + } - mod = new_mod; - current_graph = induced_graph(partition, current_graph); - init_status(current_graph, status); - } + function forInIterator(callback) { + if (typeof callback !== 'function') { + return; + } + var node; - return status_list; + for (node in nodes) { + if (callback(nodes[node])) { + return true; // client doesn't want to proceed. Return. + } } + } +} - var core = function () { - var status = {}; - var dendogram = generate_dendogram(original_graph, partition_init); +// need this for old browsers. Should this be a separate module? +function indexOfElementInArray(element, array) { + if (!array) return -1; - return partition_at_level(dendogram, dendogram.length - 1); - }; + if (array.indexOf) { + return array.indexOf(element); + } - core.nodes = function (nds) { - if (arguments.length > 0) { - original_graph_nodes = nds; - } + var len = array.length, + i; - return core; - }; + for (i = 0; i < len; i += 1) { + if (array[i] === element) { + return i; + } + } - core.edges = function (edgs) { - if (typeof original_graph_nodes === 'undefined') - throw 'Please provide the graph nodes first!'; - - if (arguments.length > 0) { - original_graph_edges = edgs; - var assoc_mat = make_assoc_mat(edgs); - original_graph = { - 'nodes': original_graph_nodes, - 'edges': original_graph_edges, - '_assoc_mat': assoc_mat - }; - } + return -1; +} - return core; +/** + * Internal structure to represent node; + */ +function Node(id) { + this.id = id; + this.links = null; + this.data = null; +} - }; +function addLinkToNode(node, link) { + if (node.links) { + node.links.push(link); + } else { + node.links = [link]; + } +} - core.partition_init = function (prttn) { - if (arguments.length > 0) { - partition_init = prttn; - } - return core; - }; +/** + * Internal structure to represent links; + */ +function Link(fromId, toId, data, id) { + this.fromId = fromId; + this.toId = toId; + this.data = data; + this.id = id; +} - return core; +function hashCode(str) { + var hash = 0, i, chr, len; + if (str.length == 0) return hash; + for (i = 0, len = str.length; i < len; i++) { + chr = str.charCodeAt(i); + hash = ((hash << 5) - hash) + chr; + hash |= 0; // Convert to 32bit integer + } + return hash; +} + +function makeLinkId(fromId, toId) { + return hashCode(fromId.toString() + '👉 ' + toId.toString()); } -module.exports = jLouvain; /***/ }), -/* 3 */ +/* 11 */ /***/ (function(module, exports) { -module.exports = __WEBPACK_EXTERNAL_MODULE_3__; +module.exports = function(subject) { + validateSubject(subject); + + var eventsStorage = createEventsStorage(subject); + subject.on = eventsStorage.on; + subject.off = eventsStorage.off; + subject.fire = eventsStorage.fire; + return subject; +}; + +function createEventsStorage(subject) { + // Store all event listeners to this hash. Key is event name, value is array + // of callback records. + // + // A callback record consists of callback function and its optional context: + // { 'eventName' => [{callback: function, ctx: object}] } + var registeredEvents = Object.create(null); + + return { + on: function (eventName, callback, ctx) { + if (typeof callback !== 'function') { + throw new Error('callback is expected to be a function'); + } + var handlers = registeredEvents[eventName]; + if (!handlers) { + handlers = registeredEvents[eventName] = []; + } + handlers.push({callback: callback, ctx: ctx}); + + return subject; + }, + + off: function (eventName, callback) { + var wantToRemoveAll = (typeof eventName === 'undefined'); + if (wantToRemoveAll) { + // Killing old events storage should be enough in this case: + registeredEvents = Object.create(null); + return subject; + } + + if (registeredEvents[eventName]) { + var deleteAllCallbacksForEvent = (typeof callback !== 'function'); + if (deleteAllCallbacksForEvent) { + delete registeredEvents[eventName]; + } else { + var callbacks = registeredEvents[eventName]; + for (var i = 0; i < callbacks.length; ++i) { + if (callbacks[i].callback === callback) { + callbacks.splice(i, 1); + } + } + } + } + + return subject; + }, + + fire: function (eventName) { + var callbacks = registeredEvents[eventName]; + if (!callbacks) { + return subject; + } + + var fireArguments; + if (arguments.length > 1) { + fireArguments = Array.prototype.splice.call(arguments, 1); + } + for(var i = 0; i < callbacks.length; ++i) { + var callbackInfo = callbacks[i]; + callbackInfo.callback.apply(callbackInfo.ctx, fireArguments); + } + + return subject; + } + }; +} + +function validateSubject(subject) { + if (!subject) { + throw new Error('Eventify cannot use falsy object as events subject'); + } + var reservedWords = ['on', 'fire', 'off']; + for (var i = 0; i < reservedWords.length; ++i) { + if (subject.hasOwnProperty(reservedWords[i])) { + throw new Error("Subject cannot be eventified, since it already has property '" + reservedWords[i] + "'"); + } + } +} + /***/ }) /******/ ]); diff --git a/dist/echarts-graph-modularity.min.js b/dist/echarts-graph-modularity.min.js index e530f62..745a974 100644 --- a/dist/echarts-graph-modularity.min.js +++ b/dist/echarts-graph-modularity.min.js @@ -1 +1 @@ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("echarts")):"function"==typeof define&&define.amd?define(["echarts"],t):"object"==typeof exports?exports["echarts-graph-modularity"]=t(require("echarts")):e["echarts-graph-modularity"]=t(e.echarts)}(this,function(e){return function(e){function t(o){if(r[o])return r[o].exports;var n=r[o]={i:o,l:!1,exports:{}};return e[o].call(n.exports,n,n.exports,t),n.l=!0,n.exports}var r={};return t.m=e,t.c=r,t.d=function(e,r,o){t.o(e,r)||Object.defineProperty(e,r,{configurable:!1,enumerable:!0,get:o})},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,"a",r),r},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=0)}([function(e,t,r){e.exports=r(1)},function(e,t,r){function o(e){return function(t,r){var o={};t.eachSeriesByType(e,function(e){if(e.get("modularity")){var t=e.getGraph(),r={},a=t.data.mapArray(function(e){var o=t.getNodeByIndex(e);return r[o.id]=e,o.id}),s=t.edgeData.mapArray("value",function(e,r){var o=t.getEdgeByIndex(r);return{source:o.node1.id,target:o.node2.id,value:e}}),c=n().nodes(a).edges(s).partition_init(),i=c();for(var u in i){var f=i[u];t.data.setItemVisual(r[u],"color",e.getColorFromPalette(f,o))}t.edgeData.each(function(e){var r=t.edgeData.getItemModel(e),o=t.getEdgeByIndex(e),n=r.get("lineStyle.normal.color");switch(n){case"source":n=o.node1.getVisual("color");break;case"target":n=o.node2.getVisual("color")}null!=n&&o.setVisual("color",n)})}})}}var n=r(2),a=r(3);a.registerVisual(a.PRIORITY.VISUAL.CHART+1,o("graph")),a.registerVisual(a.PRIORITY.VISUAL.CHART+1,o("graphGL"))},function(e,t){jLouvain=function(){function e(e){var t={};return e.forEach(function(e,r){t[e]=!0}),Object.keys(t)}function t(e){var t=[];for(var r in e)e.hasOwnProperty(r)&&t.push(e[r]);return t}function r(e,t){var r=e._assoc_mat[t]?Object.keys(e._assoc_mat[t]):[],o=0;return r.forEach(function(r,n){var a=e._assoc_mat[t][r]||1;t===r&&(a*=2),o+=a}),o}function o(e,t){return void 0===e._assoc_mat[t]?[]:Object.keys(e._assoc_mat[t])}function n(e,t,r){return e._assoc_mat[t]?e._assoc_mat[t][r]:void 0}function a(e){var t=0;return e.edges.forEach(function(e){t+=e.weight}),t}function s(e,t){i(e,t);var r=e.edges.map(function(e){return e.source+"_"+e.target}).indexOf(t.source+"_"+t.target);-1!==r?e.edges[r].weight=t.weight:e.edges.push(t)}function c(e){var t={};return e.forEach(function(e,r){t[e.source]=t[e.source]||{},t[e.source][e.target]=e.weight,t[e.target]=t[e.target]||{},t[e.target][e.source]=e.weight}),t}function i(e,t){e._assoc_mat[t.source]=e._assoc_mat[t.source]||{},e._assoc_mat[t.source][t.target]=t.weight,e._assoc_mat[t.target]=e._assoc_mat[t.target]||{},e._assoc_mat[t.target][t.source]=t.weight}function u(e){if(null===e||"object"!=typeof e)return e;var t=e.constructor();for(var r in e)t[r]=u(e[r]);return t}function f(e,t,s){t.nodes_to_com={},t.total_weight=0,t.internals={},t.degrees={},t.gdegrees={},t.loops={},t.total_weight=a(e),void 0===s?e.nodes.forEach(function(o,a){t.nodes_to_com[o]=a;var s=r(e,o);if(s<0)throw"Bad graph type, use positive weights!";t.degrees[a]=s,t.gdegrees[o]=s,t.loops[o]=n(e,o,o)||0,t.internals[a]=t.loops[o]}):e.nodes.forEach(function(n,a){var c=s[n];t.nodes_to_com[n]=c;var i=r(e,n);t.degrees[c]=(t.degrees[c]||0)+i,t.gdegrees[n]=i;var u=0;o(e,n).forEach(function(t,r){var o=e._assoc_mat[n][t];if(o<=0)throw"Bad graph type, use positive weights";s[t]===c&&(u+=t===n?o:o/2)}),t.internals[c]=(t.internals[c]||0)+u})}function g(r){var o=r.total_weight,n=0;return e(t(r.nodes_to_com)).forEach(function(e,t){var a=r.internals[e]||0,s=r.degrees[e]||0;o>0&&(n=n+a/o-Math.pow(s/(2*o),2))}),n}function d(e,t,r){var n={};return o(t,e).forEach(function(o,a){if(o!==e){var s=t._assoc_mat[e][o]||1,c=r.nodes_to_com[o];n[c]=(n[c]||0)+s}}),n}function h(e,t,r,o){o.nodes_to_com[e]=+t,o.degrees[t]=(o.degrees[t]||0)+(o.gdegrees[e]||0),o.internals[t]=(o.internals[t]||0)+r+(o.loops[e]||0)}function _(e,t,r,o){o.degrees[t]=(o.degrees[t]||0)-(o.gdegrees[e]||0),o.internals[t]=(o.internals[t]||0)-r-(o.loops[e]||0),o.nodes_to_com[e]=-1}function l(e){var t=0,r=u(e),o={};return Object.keys(e).forEach(function(n){var a=e[n],s=void 0===o[a]?-1:o[a];-1===s&&(o[a]=t,s=t,t+=1),r[n]=s}),r}function v(e,t){for(var r=!0,o=0,n=g(t),a=n;r&&o!==x&&(n=a,r=!1,o+=1,e.nodes.forEach(function(o,n){var a=t.nodes_to_com[o],s=(t.gdegrees[o]||0)/(2*t.total_weight),c=d(o,e,t);_(o,a,c[a]||0,t);var i=a,u=0;Object.keys(c).forEach(function(e,r){var o=c[e]-(t.degrees[e]||0)*s;o>u&&(u=o,i=e)}),h(o,i,c[i]||0,t),i!==a&&(r=!0)}),!((a=g(t))-n0&&(w=e),I},I.edges=function(e){if(void 0===w)throw"Please provide the graph nodes first!";if(arguments.length>0){E=e;var t=c(e);O={nodes:w,edges:E,_assoc_mat:t}}return I},I.partition_init=function(e){return arguments.length>0&&(b=e),I},I},e.exports=jLouvain},function(t,r){t.exports=e}])}); \ No newline at end of file +!function(t,n){"object"==typeof exports&&"object"==typeof module?module.exports=n(require("echarts")):"function"==typeof define&&define.amd?define(["echarts"],n):"object"==typeof exports?exports["echarts-graph-modularity"]=n(require("echarts")):t["echarts-graph-modularity"]=n(t.echarts)}(this,function(t){return function(t){function n(o){if(e[o])return e[o].exports;var i=e[o]={i:o,l:!1,exports:{}};return t[o].call(i.exports,i,i.exports,n),i.l=!0,i.exports}var e={};return n.m=t,n.c=e,n.d=function(t,e,o){n.o(t,e)||Object.defineProperty(t,e,{configurable:!1,enumerable:!0,get:o})},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,n){return Object.prototype.hasOwnProperty.call(t,n)},n.p="",n(n.s=0)}([function(t,n,e){t.exports=e(1)},function(t,n,e){function o(t){return function(n,e){var o={};n.eachSeriesByType(t,function(t){if(t.get("modularity")){var n=t.getGraph(),e={},r=s();n.data.each(function(t){var o=n.getNodeByIndex(t);return e[o.id]=t,r.addNode(o.id),o.id}),n.edgeData.each("value",function(t,e){var o=n.getEdgeByIndex(e);return r.addLink(o.node1.id,o.node2.id),{source:o.node1.id,target:o.node2.id,value:t}});var u=new i(t.get("modularity.resolution")||1),c=u.execute(r);console.log(c);for(var h in c){var a=c[h];n.data.setItemVisual(e[h],"color",t.getColorFromPalette(a,o))}n.edgeData.each(function(t){var e=n.edgeData.getItemModel(t),o=n.getEdgeByIndex(t),i=e.get("lineStyle.normal.color");switch(i){case"source":i=o.node1.getVisual("color");break;case"target":i=o.node2.getVisual("color")}null!=i&&o.setVisual("color",i)})}})}}var i=e(2),r=e(9),s=e(10);r.registerVisual(r.PRIORITY.VISUAL.CHART+1,o("graph")),r.registerVisual(r.PRIORITY.VISUAL.CHART+1,o("graphGL"))},function(t,n,e){function o(t,n){this.isRandomized=!1,this.useWeight=n,this.resolution=t||1,this.structure=null}var i=e(3),r=e(6);o.prototype.execute=function(t){this.structure=new i(t,this.useWeight);var n=new Array(t.getNodesCount()),e=(this.computeModularity(t,this.structure,n,this.resolution,this.isRandomized,this.useWeight),{});return this.structure.map.forEach(function(t,o){e[o]=n[t]}),e},o.prototype.computeModularity=function(t,n,e,o,i,r){for(var s=(n.graphWeightSum,n.weights.slice(),Object.create(null)),u=!0;u;){u=!1;for(var c=!0;c;){c=!1;var h=0;i&&(h=function(t,n){return Math.floor(Math.random()*(n-t))+t}(0,n.N));for(var a=0,d=h;ao&&(o=u,i=s)},this),i},o.prototype.fillComStructure=function(t,n,e){var o=0;return n.communities.forEach(function(t){t.nodes.forEach(function(t){n.invMap.get(t).nodes.forEach(function(t){e[t]=o})}),o++}),e},o.prototype.fillDegreeCount=function(t,n,e,o,i){var s=new Array(n.communities.length),u=r.degree(t);return t.forEachNode(function(t){var r=n.map.get(t);s[e[r]]+=i?o[r]:u[t.id]}),s},o.prototype._finalQ=function(t,n,e,o,i,r,s){throw new Error("not implemented properly")},o.prototype.q=function(t,n,e,o){var i=e.nodeConnectionsWeight[t].get(n),r=0;null!=i&&(r=i);var s=n.weightSum,u=e.weights[t],c=o*r-u*s/(2*e.graphWeightSum);return e.nodeCommunities[t]==n&&e.nodeCommunities[t].size()>1&&(c=o*r-u*(s-u)/(2*e.graphWeightSum)),e.nodeCommunities[t]==n&&1==e.nodeCommunities[t].size()&&(c=0),c},t.exports=o},function(t,n,e){"use strict";function o(t,n){this.N=t.getNodesCount(),this.graphWeightSum=0,this.structure=this,this.invMap=new Map,this.nodeConnectionsWeight=new Array(this.N),this.nodeConnectionsCount=new Array(this.N),this.nodeCommunities=new Array(this.N),this.map=new Map,this.topology=new Array(this.N);for(var e=0;e=0&&e.links.splice(n,1),o&&(n=i(t,o.links))>=0&&o.links.splice(n,1),U(t,"remove"),V(),!0}function v(t,n){var e,o=c(t);if(!o||!o.links)return null;for(e=0;e0&&(P.fire("changed",z),z.length=0)}function E(t){if("function"==typeof t)for(var n=Object.keys(O),e=0;e1&&(i=Array.prototype.splice.call(arguments,1));for(var r=0;r 0) { - result = result + in_degree / links - Math.pow((degree / (2.0 * links)), 2); - } - }); - - return result; - } - - function __neighcom(node, graph, status) { - // compute the communities in the neighb. of the node, with the graph given by - // node_to_com - var weights = {}; - var neighboorhood = get_neighbours_of_node(graph, node);//make iterable; - - neighboorhood.forEach(function (neighbour, i) { - if (neighbour !== node) { - var weight = graph._assoc_mat[node][neighbour] || 1; - var neighbourcom = status.nodes_to_com[neighbour]; - weights[neighbourcom] = (weights[neighbourcom] || 0) + weight; - } - }); - - return weights; - } - - function __insert(node, com, weight, status) { - //insert node into com and modify status - status.nodes_to_com[node] = +com; - status.degrees[com] = (status.degrees[com] || 0) + (status.gdegrees[node] || 0); - status.internals[com] = (status.internals[com] || 0) + weight + (status.loops[node] || 0); - } - - function __remove(node, com, weight, status) { - //remove node from com and modify status - status.degrees[com] = ((status.degrees[com] || 0) - (status.gdegrees[node] || 0)); - status.internals[com] = ((status.internals[com] || 0) - weight - (status.loops[node] || 0)); - status.nodes_to_com[node] = -1; - } - - function __renumber(dict) { - var count = 0; - var ret = clone(dict); //deep copy :) - var new_values = {}; - var dict_keys = Object.keys(dict); - dict_keys.forEach(function (key) { - var value = dict[key]; - var new_value = typeof new_values[value] === 'undefined' ? -1 : new_values[value]; - if (new_value === -1) { - new_values[value] = count; - new_value = count; - count = count + 1; - } - ret[key] = new_value; - }); - - return ret; - } - - function __one_level(graph, status) { - //Compute one level of the Communities Dendogram. - var modif = true; - var nb_pass_done = 0; - var cur_mod = __modularity(status); - var new_mod = cur_mod; - - while (modif && nb_pass_done !== __PASS_MAX) { - cur_mod = new_mod; - modif = false; - nb_pass_done += 1 - - graph.nodes.forEach(function (node, i) { - var com_node = status.nodes_to_com[node]; - var degc_totw = (status.gdegrees[node] || 0) / (status.total_weight * 2.0); - var neigh_communities = __neighcom(node, graph, status); - __remove(node, com_node, (neigh_communities[com_node] || 0.0), status); - var best_com = com_node; - var best_increase = 0; - var neigh_communities_entries = Object.keys(neigh_communities);//make iterable; - - neigh_communities_entries.forEach(function (com, i) { - var incr = neigh_communities[com] - (status.degrees[com] || 0.0) * degc_totw; - if (incr > best_increase) { - best_increase = incr; - best_com = com; - } - }); - - __insert(node, best_com, neigh_communities[best_com] || 0, status); - - if (best_com !== com_node) { - modif = true; - } - }); - new_mod = __modularity(status); - if (new_mod - cur_mod < __MIN) { - break; - } - } - } - - function induced_graph(partition, graph) { - var ret = {nodes: [], edges: [], _assoc_mat: {}}; - var w_prec, weight; - //add nodes from partition values - var partition_values = obj_values(partition); - ret.nodes = ret.nodes.concat(make_set(partition_values)); //make set - graph.edges.forEach(function (edge, i) { - weight = edge.weight || 1; - var com1 = partition[edge.source]; - var com2 = partition[edge.target]; - w_prec = (get_edge_weight(ret, com1, com2) || 0); - var new_weight = (w_prec + weight); - add_edge_to_graph(ret, {'source': com1, 'target': com2, 'weight': new_weight}); - }); - - return ret; - } - - function partition_at_level(dendogram, level) { - var partition = clone(dendogram[0]); - for (var i = 1; i < level + 1; i++) { - Object.keys(partition).forEach(function (key, j) { - var node = key; - var com = partition[key]; - partition[node] = dendogram[i][com]; - }); - } - - return partition; - } - - - function generate_dendogram(graph, part_init) { - if (graph.edges.length === 0) { - var part = {}; - graph.nodes.forEach(function (node, i) { - part[node] = node; - }); - return part; - } - var status = {}; - - init_status(original_graph, status, part_init); - var mod = __modularity(status); - var status_list = []; - __one_level(original_graph, status); - var new_mod = __modularity(status); - var partition = __renumber(status.nodes_to_com); - status_list.push(partition); - mod = new_mod; - var current_graph = induced_graph(partition, original_graph); - init_status(current_graph, status); - - while (true) { - __one_level(current_graph, status); - new_mod = __modularity(status); - if (new_mod - mod < __MIN) { - break; - } - - partition = __renumber(status.nodes_to_com); - status_list.push(partition); - - mod = new_mod; - current_graph = induced_graph(partition, current_graph); - init_status(current_graph, status); - } - - return status_list; - } - - var core = function () { - var status = {}; - var dendogram = generate_dendogram(original_graph, partition_init); - - return partition_at_level(dendogram, dendogram.length - 1); - }; - - core.nodes = function (nds) { - if (arguments.length > 0) { - original_graph_nodes = nds; - } - - return core; - }; - - core.edges = function (edgs) { - if (typeof original_graph_nodes === 'undefined') - throw 'Please provide the graph nodes first!'; - - if (arguments.length > 0) { - original_graph_edges = edgs; - var assoc_mat = make_assoc_mat(edgs); - original_graph = { - 'nodes': original_graph_nodes, - 'edges': original_graph_edges, - '_assoc_mat': assoc_mat - }; - } - - return core; - - }; - - core.partition_init = function (prttn) { - if (arguments.length > 0) { - partition_init = prttn; - } - return core; - }; - - return core; -} - -module.exports = jLouvain; \ No newline at end of file