Reputation: 169
I had the same question as this one. The solution works, however, I cannot seem to space out the nodes and make them appear in a circular format with my dataset. I have around 30 nodes in total that are color-coded.
The nodes of the same color are overlapping instead of being clustered in a circular format/more concentric.
I used the code in the question above, and tried all radii values possible but cannot seem to make the nodes of the same color cluster in a circle.
Code:
import networkx
import numpy as np
import matplotlib.pyplot as plt
nodesWithGroup = {'A':'#7a8eff', 'B': '#7a8eff', 'C': '#eb2c30', 'D':'#eb2c30', 'E': '#eb2c30', 'F':'#730a15', 'G': '#730a15'}
# Set up graph, adding nodes and edges
G = nx.Graph()
G.add_nodes_from(nodesWithGroup.keys())
# Create a dictionary mapping color to a list of nodes
nodes_by_color = {}
for k, v in nodesWithGroup.items():
if v not in nodes_by_color:
nodes_by_color[v] = [k]
else:
nodes_by_color[v].append(k)
# Create initial circular layout
pos = nx.circular_layout(RRR)
# Get list of colors
colors2 = list(nodes_by_color.keys())
# clustering
angs = np.linspace(0, 2*np.pi, 1+len(colors))
repos = []
rad = 13
for ea in angs:
if ea > 0:
repos.append(np.array([rad*np.cos(ea), rad*np.sin(ea)]))
for color, nodes in nodes_by_color.items():
posx = colors.index(color)
for node in nodes:
pos[node] += repos[posx]
# Plot graph
fig,ax = plt.subplots(figsize=(5, 5))
# node colors
teamX = ['A', 'B']
teamY = ['C', 'D', 'E']
teamZ = ['F', 'G']
for n in G.nodes():
if n in teamX:
G.nodes[n]['color'] = '#7a8eff'
elif n in teamY:
G.nodes[n]['color'] = '#eb2c30'
else:
G.nodes[n]['color'] = '#730a15'
colors = [node[1]['color'] for node in G.nodes(data=True)]
# edges
zorder_edges = 3
zorder_nodes = 4
zorder_node_labels = 5
for edge in G.edges():
source, target = edge
rad = 0.15
node_color_dict = dict(G.nodes(data='color'))
if node_color_dict[source] == node_color_dict[target]:
arrowprops=dict(lw=G.edges[(source,target)]['weight'],
arrowstyle="-",
color='blue',
connectionstyle=f"arc3,rad={rad}",
linestyle= '-',
alpha=0.65, zorder=zorder_edges)
ax.annotate("",
xy=pos[source],
xytext=pos[target],
arrowprops=arrowprops
)
else:
arrowprops=dict(lw=G.edges[(source,target)]['weight'],
arrowstyle="-",
color='purple',
connectionstyle=f"arc3,rad={rad}",
linestyle= '-',
alpha=0.65, zorder=zorder_edges)
ax.annotate("",
xy=pos[source],
xytext=pos[target],
arrowprops=arrowprops
)
# drawing
node_labels_dict = nx.draw_networkx_labels(G, pos, font_size=5, font_family="monospace", font_color='white', font_weight='bold')
for color, nodes in nodes_by_color.items():
nodes_draw = nx.draw_networkx_nodes(G, pos=pos, nodelist=nodes, node_color=color, edgecolors=[(0,0,0,1)])
nodes_draw.set_zorder(zorder_nodes)
for node_labels_draw in node_labels_dict.values():
node_labels_draw.set_zorder(zorder_node_labels)
plt.show()
I'm getting the following output:
Desired output (as in the solution):
Upvotes: 6
Views: 1354
Reputation: 13031
As @willcrack suggested, slightly adapting this answer works well.
You can adjust the node overlap by changing the ratio
parameter in partition_layout
.
#!/usr/bin/env python
import numpy as np
import matplotlib.pyplot as plt
import networkx as nx
NODE_LAYOUT = nx.circular_layout
COMMUNITY_LAYOUT = nx.circular_layout
def partition_layout(g, partition, ratio=0.3):
"""
Compute the layout for a modular graph.
Arguments:
----------
g -- networkx.Graph or networkx.DiGraph instance
network to plot
partition -- dict mapping node -> community or None
Network partition, i.e. a mapping from node ID to a group ID.
ratio: 0 < float < 1.
Controls how tightly the nodes are clustered around their partition centroid.
If 0, all nodes of a partition are at the centroid position.
if 1, nodes are positioned independently of their partition centroid.
Returns:
--------
pos -- dict mapping int node -> (float x, float y)
node positions
"""
pos_communities = _position_communities(g, partition)
pos_nodes = _position_nodes(g, partition)
pos_nodes = {k : ratio * v for k, v in pos_nodes.items()}
# combine positions
pos = dict()
for node in g.nodes():
pos[node] = pos_communities[node] + pos_nodes[node]
return pos
def _position_communities(g, partition, **kwargs):
# create a weighted graph, in which each node corresponds to a community,
# and each edge weight to the number of edges between communities
between_community_edges = _find_between_community_edges(g, partition)
communities = set(partition.values())
hypergraph = nx.DiGraph()
hypergraph.add_nodes_from(communities)
for (ci, cj), edges in between_community_edges.items():
hypergraph.add_edge(ci, cj, weight=len(edges))
# find layout for communities
pos_communities = COMMUNITY_LAYOUT(hypergraph, **kwargs)
# set node positions to position of community
pos = dict()
for node, community in partition.items():
pos[node] = pos_communities[community]
return pos
def _find_between_community_edges(g, partition):
edges = dict()
for (ni, nj) in g.edges():
ci = partition[ni]
cj = partition[nj]
if ci != cj:
try:
edges[(ci, cj)] += [(ni, nj)]
except KeyError:
edges[(ci, cj)] = [(ni, nj)]
return edges
def _position_nodes(g, partition, **kwargs):
"""
Positions nodes within communities.
"""
communities = dict()
for node, community in partition.items():
if community in communities:
communities[community] += [node]
else:
communities[community] = [node]
pos = dict()
for community, nodes in communities.items():
subgraph = g.subgraph(nodes)
pos_subgraph = NODE_LAYOUT(subgraph, **kwargs)
pos.update(pos_subgraph)
return pos
def _layout(networkx_graph):
edge_list = [edge for edge in networkx_graph.edges]
node_list = [node for node in networkx_graph.nodes]
pos = circular_layout(edge_list)
# NB: some nodes might not be connected and hence will not be in the edge list.
# Assuming a [0, 0, 1, 1] canvas, we assign random positions on the periphery
# of the existing node positions.
# We define the periphery as the region outside the circle that covers all
# existing node positions.
xy = list(pos.values())
centroid = np.mean(xy, axis=0)
delta = xy - centroid[np.newaxis, :]
distance = np.sqrt(np.sum(delta**2, axis=1))
radius = np.max(distance)
connected_nodes = set(_flatten(edge_list))
for node in node_list:
if not (node in connected_nodes):
pos[node] = _get_random_point_on_a_circle(centroid, radius)
return pos
def _flatten(nested_list):
return [item for sublist in nested_list for item in sublist]
def _get_random_point_on_a_circle(origin, radius):
x0, y0 = origin
random_angle = 2 * np.pi * np.random.random()
x = x0 + radius * np.cos(random_angle)
y = y0 + radius * np.sin(random_angle)
return np.array([x, y])
def test():
# create test data
cliques = 8
clique_size = 7
g = nx.connected_caveman_graph(cliques, clique_size)
partition = {ii : np.int(ii/clique_size) for ii in range(cliques * clique_size)}
pos = partition_layout(g, partition, ratio=0.2)
nx.draw(g, pos, node_color=list(partition.values()))
plt.show()
def test2():
# create test data
cliques = 8
clique_size = 7
g = nx.connected_caveman_graph(cliques, clique_size)
partition = {ii : np.int(ii/clique_size) for ii in range(cliques * clique_size)}
# add additional between-clique edges
total_nodes = cliques*clique_size
for ii in range(cliques):
start = ii*clique_size + int(clique_size/2)
stop = (ii+cliques/2)*clique_size % total_nodes + int(clique_size/2)
g.add_edge(start, stop)
pos = partition_layout(g, partition, ratio=0.2)
nx.draw(g, pos, node_color=list(partition.values()))
plt.show()
if __name__ == '__main__':
test()
test2()
Example with additional inter-cluster edges as requested in comments:
Upvotes: 3