/* 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 */ (function() { 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; }); return Object.keys(set); } function obj_values(obj) { var vals = []; for (var key in obj) { if (obj.hasOwnProperty(key)) { vals.push(obj[key]); } } return vals; } 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; } function get_neighbours_of_node(graph, node) { if (typeof graph._assoc_mat[node] == 'undefined') return []; var neighbours = Object.keys(graph._assoc_mat[node]); return neighbours; } function get_edge_weight(graph, node1, node2) { return graph._assoc_mat[node1] ? graph._assoc_mat[node1][node2] : undefined; } function get_graph_size(graph) { var size = 0; graph.edges.forEach(function(edge) { size += edge.weight; }); return size; } function add_edge_to_graph(graph, edge) { update_assoc_mat(graph, edge); var edge_index = graph.edges .map(function(d) { return d.source + '_' + d.target; }) .indexOf(edge.source + '_' + edge.target); if (edge_index != -1) graph.edges[edge_index].weight = edge.weight; else graph.edges.push(edge); } 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; }); return mat; } 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 clone(obj) { if (obj == null || typeof obj != 'object') return obj; var temp = obj.constructor(); for (var key in obj) temp[key] = clone(obj[key]); return temp; } //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'; } if (part[neighbour] == com) { if (neighbour == node) { inc += weight; } else { inc += weight / 2.0; } } }); status.internals[com] = (status.internals[com] || 0) + inc; }); } } function __modularity(status) { var links = status.total_weight; var result = 0.0; var communities = make_set(obj_values(status.nodes_to_com)); 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); } }); 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, nb_pass_done = 0, cur_mod = __modularity(status), 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; }; })();