Skip to content

Commit

Permalink
Merge pull request #234 from amineitji/issue_weights_width_size
Browse files Browse the repository at this point in the history
Updating vizualisation of communities
  • Loading branch information
GiulioRossetti authored Feb 18, 2024
2 parents d5ac19d + da1e877 commit 4c8ed87
Show file tree
Hide file tree
Showing 2 changed files with 168 additions and 16 deletions.
14 changes: 12 additions & 2 deletions cdlib/test/test_viz_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ def test_nx_cluster(self):

coms = algorithms.demon(g, 0.25)
pos = nx.spring_layout(g)
viz.plot_network_clusters(g, coms, pos, plot_labels=True, plot_overlaps=True)
viz.plot_network_clusters(g, coms, pos,
plot_labels=True,
plot_overlaps=True,
show_edge_weights=True,
show_edge_widths=True,
show_node_sizes=True)

plt.savefig("cluster.pdf")
os.remove("cluster.pdf")
Expand All @@ -33,7 +38,12 @@ def test_community_graph(self):
os.remove("cg.pdf")

coms = algorithms.demon(g, 0.25)
viz.plot_community_graph(g, coms, plot_overlaps=True, plot_labels=True)
viz.plot_community_graph(g, coms,
plot_labels=True,
plot_overlaps=True,
show_edge_weights=True,
show_edge_widths=True,
show_node_sizes=True)

plt.savefig("cg.pdf")
os.remove("cg.pdf")
170 changes: 156 additions & 14 deletions cdlib/viz/networks.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from cdlib import NodeClustering
from cdlib.utils import convert_graph_formats
from community import community_louvain
from typing import Union

__all__ = ["plot_network_clusters", "plot_community_graph"]

Expand Down Expand Up @@ -43,12 +44,15 @@ def plot_network_clusters(
partition: NodeClustering,
position: dict = None,
figsize: tuple = (8, 8),
node_size: int = 200,
node_size: Union[int, dict] = 200, # 200 default value
plot_overlaps: bool = False,
plot_labels: bool = False,
cmap: object = None,
top_k: int = None,
min_size: int = None,
show_edge_widths: bool = False,
show_edge_weights: bool = False,
show_node_sizes: bool = False,
) -> object:
"""
Plot a graph with node color coding for communities.
Expand All @@ -57,12 +61,15 @@ def plot_network_clusters(
:param partition: NodeClustering object
:param position: A dictionary with nodes as keys and positions as values. Example: networkx.fruchterman_reingold_layout(G). By default, uses nx.spring_layout(g)
:param figsize: the figure size; it is a pair of float, default (8, 8)
:param node_size: int, default 200
:param node_size: The size of nodes. It can be an integer or a dictionary mapping nodes to sizes. Default is 200.
:param plot_overlaps: bool, default False. Flag to control if multiple algorithms memberships are plotted.
:param plot_labels: bool, default False. Flag to control if node labels are plotted.
:param cmap: str or Matplotlib colormap, Colormap(Matplotlib colormap) for mapping intensities of nodes. If set to None, original colormap is used.
:param top_k: int, Show the top K influential communities. If set to zero or negative value indicates all.
:param min_size: int, Exclude communities below the specified minimum size.
:param show_edge_widths: Flag to control if edge widths are shown. Default is False.
:param show_edge_weights: Flag to control if edge weights are shown. Default is False.
:param show_node_sizes: Flag to control if node sizes are shown. Default is False.
Example:
Expand All @@ -71,7 +78,7 @@ def plot_network_clusters(
>>> g = nx.karate_club_graph()
>>> coms = algorithms.louvain(g)
>>> pos = nx.spring_layout(g)
>>> viz.plot_network_clusters(g, coms, pos)
>>> viz.plot_network_clusters(g, coms, pos, edge_weights=edge_weights)
"""
if not isinstance(cmap, (type(None), str, matplotlib.colors.Colormap)):
raise TypeError(
Expand Down Expand Up @@ -112,24 +119,71 @@ def plot_network_clusters(
graph.edges(),
)
)
fig = nx.draw_networkx_nodes(
graph, position, node_size=node_size, node_color="w", nodelist=filtered_nodelist
)
fig.set_edgecolor("k")
nx.draw_networkx_edges(graph, position, alpha=0.5, edgelist=filtered_edgelist)
if isinstance(node_size, int):
fig = nx.draw_networkx_nodes(
graph, position, node_size=node_size, node_color="w", nodelist=filtered_nodelist
)
fig.set_edgecolor("k")

filtered_edge_widths = [1] * len(filtered_edgelist)

if show_edge_widths:
edge_widths = nx.get_edge_attributes(graph, "weight")
filtered_edge_widths = [weight for (edge, weight) in edge_widths.items() if edge[0] != edge[1]]

min_weight = min(filtered_edge_widths)
max_weight = max(filtered_edge_widths)

filtered_edge_widths = np.interp(filtered_edge_widths, (min_weight, max_weight), (1, 5))

nx.draw_networkx_edges(graph, position, alpha=0.5, edgelist=filtered_edgelist, width=filtered_edge_widths)

if show_edge_weights:
edge_weights = nx.get_edge_attributes(graph, "weight")
filtered_edge_weights = [{edge: weight} for edge, weight in edge_weights.items() if edge[0] != edge[1]]

for edge_weight in filtered_edge_weights:
nx.draw_networkx_edge_labels(
graph,
position,
edge_labels=edge_weight,
font_color="red",
label_pos=0.5,
font_size=8,
font_weight='bold',
)

if plot_labels:
nx.draw_networkx_labels(
graph,
position,
font_color=".8",
labels={node: str(node) for node in filtered_nodelist},
)

if isinstance(node_size, dict) and show_node_sizes:
# Extract values from the node_size dictionary
node_values = list(node_size.values())

# Interpolate node_size values to be between 200 and 500
min_node_size = min(node_values) if node_values else 1
max_node_size = max(node_values) if node_values else 1
node_size = {key: np.interp(value, (min_node_size, max_node_size), (200, 1000)) for key, value in node_size.items()}
else:
node_size = 200

for i in range(n_communities):
if len(partition[i]) > 0:
if plot_overlaps:
size = (n_communities - i) * node_size
if isinstance(node_size, dict):
size = (n_communities - i) * node_size[i] # Use interpolated size from dictionary
else:
size = (n_communities - i) * node_size # Use fixed size
else:
size = node_size
if isinstance(node_size, dict):
size = node_size[i] # Use interpolated size from dictionary
else:
size = node_size # Use fixed size
fig = nx.draw_networkx_nodes(
graph,
position,
Expand All @@ -138,6 +192,7 @@ def plot_network_clusters(
node_color=[cmap(_norm(i))],
)
fig.set_edgecolor("k")

if plot_labels:
for i in range(n_communities):
if len(partition[i]) > 0:
Expand All @@ -147,33 +202,109 @@ def plot_network_clusters(
font_color=fontcolors[i],
labels={node: str(node) for node in partition[i]},
)

return fig

def calculate_cluster_edge_weights(graph, node_to_com):
"""
Calculate edge weights between different clusters.
This function calculates the edge weights between nodes belonging to different clusters.
It iterates through the edges of the graph, identifies the communities of the source and target nodes,
and increments the edge weight for the corresponding cluster pair.
:param graph: NetworkX/igraph graph
:param node_to_com: Dictionary mapping nodes to their community IDs
"""
cluster_edge_weights = {}

for edge in graph.edges():
source, target = edge
source_com = node_to_com.get(source, None)
target_com = node_to_com.get(target, None)

if source_com is not None and target_com is not None and source_com != target_com:
# Nodes belong to different communities
cluster_pair = (source_com, target_com)

# Check if edge data is not empty
edge_data = graph.get_edge_data(source_com, target_com)

# Check if edge data is None, empty or not
if edge_data is None:
edge_weight = 0
elif edge_data == {}:
edge_weight = 1
else: # edge_data contains an element 'weight' : int
edge_weight = edge_data['weight']

if cluster_pair not in cluster_edge_weights:
cluster_edge_weights[cluster_pair] = edge_weight
else:
cluster_edge_weights[cluster_pair] += edge_weight

cluster_edge_weights_array = [(source, target, weight) for (source, target), weight in cluster_edge_weights.items()]
graph.add_weighted_edges_from(cluster_edge_weights_array)

def calculate_cluster_sizes(partition: NodeClustering) -> Union[int, dict]:
"""
Calculate the total weight of all nodes in each cluster.
:param partition: The partition of the graph into clusters.
:type partition: NodeClustering
:return: If all clusters have the same size, return the size as an integer.
Otherwise, return a dictionary mapping cluster ID to the number of nodes in the cluster.
:rtype: Union[int, dict]
"""
cluster_sizes = {}
unique_values = set()

for cid, com in enumerate(partition.communities):
total_weight = 0

#print("cluster: ", cid)
for node in com:
if 'weight' in partition.graph.nodes[node]: # If node data contains a 'weight' attribute
total_weight += partition.graph.nodes[node]['weight']
else: # If node data is empty
total_weight += 1 # Default weight is 1

cluster_sizes[cid] = total_weight

if len(unique_values) == 1:
return int(unique_values.pop()) # All elements have the same value, return that value as an integer
else:
return cluster_sizes # Elements have different values, return the dictionary


def plot_community_graph(
graph: object,
partition: NodeClustering,
figsize: tuple = (8, 8),
node_size: int = 200,
node_size: Union[int, dict] = 200,
plot_overlaps: bool = False,
plot_labels: bool = False,
cmap: object = None,
top_k: int = None,
min_size: int = None,
show_edge_weights: bool = True,
show_edge_widths: bool = True,
show_node_sizes: bool = True,
) -> object:
"""
Plot a algorithms-graph with node color coding for communities.
Plot a algorithms-graph with node color coding for communities, where nodes represent clusters obtained from a community detection algorithm.
:param graph: NetworkX/igraph graph
:param partition: NodeClustering object
:param figsize: the figure size; it is a pair of float, default (8, 8)
:param node_size: int, default 200
:param node_size: The size of nodes. It can be an integer or a dictionary mapping nodes to sizes. Default is 200.
:param plot_overlaps: bool, default False. Flag to control if multiple algorithms memberships are plotted.
:param plot_labels: bool, default False. Flag to control if node labels are plotted.
:param cmap: str or Matplotlib colormap, Colormap(Matplotlib colormap) for mapping intensities of nodes. If set to None, original colormap is used..
:param top_k: int, Show the top K influential communities. If set to zero or negative value indicates all.
:param min_size: int, Exclude communities below the specified minimum size.
:param show_edge_widths: Flag to control if edge widths are shown. Default is True.
:param show_edge_weights: Flag to control if edge weights are shown. Default is True.
:param show_node_sizes: Flag to control if node sizes are shown. Default is True.
Example:
Expand Down Expand Up @@ -205,6 +336,14 @@ def plot_community_graph(
c_graph = community_louvain.induced_graph(node_to_com, s)
node_cms = [[node] for node in c_graph.nodes()]

# Calculate edge weights and edge widths for each cluster
if(show_edge_weights or show_edge_widths):
calculate_cluster_edge_weights(graph, node_to_com)

if show_node_sizes:
# Calculate cluster sizes for adjusting node sizes
node_size = calculate_cluster_sizes(partition)

return plot_network_clusters(
c_graph,
NodeClustering(node_cms, None, ""),
Expand All @@ -214,4 +353,7 @@ def plot_community_graph(
plot_overlaps=plot_overlaps,
plot_labels=plot_labels,
cmap=cmap,
show_edge_weights=show_edge_weights,
show_edge_widths=show_edge_widths,
show_node_sizes=show_node_sizes,
)

0 comments on commit 4c8ed87

Please sign in to comment.