Reputation: 131
I want to create a multi-layered graph (like in the attached image), by connecting the two graphs written with the following code, using networkx
#Graph1
g1 = nx.read_edgelist('sample.txt', nodetype=str)
pos = nx.shell_layout(g)
plt.figure(figsize=(10, 10))
nx.draw_networkx_edges(g, pos, edge_color='khaki', alpha=1)
nx.draw_networkx_nodes(g,pos,node_color='r',alpha=0.5,node_size=1000)
nx.draw_networkx_labels(g, pos, font_size=10,font_family='IPAexGothic')
plt.axis('off')
#Graph2
g2 = nx.read_edgelist('sample2.txt', nodetype=str)
pos = nx.shell_layout(g)
plt.figure(figsize=(10, 10))
nx.draw_networkx_edges(g, pos, edge_color='khaki', alpha=1)
nx.draw_networkx_nodes(g,pos,node_color='r',alpha=0.5,node_size=1000)
nx.draw_networkx_labels(g, pos, font_size=10,font_family='IPAexGothic')
plt.axis('off')
Upvotes: 13
Views: 9390
Reputation: 1703
The above solution did not work for me on the latest networkx. I created a gist that provides a flat networkx-ified version.
import proplot as plt, cmasher as cmr, pandas as pd, numpy as np, os, sys, networkx as nx, warnings
def multilayer_layout(
G: nx.Graph,
subset_key="layer",
layout=nx.spring_layout,
separation: float = 2.0,
) -> dict:
# set positions
layers = {}
for node, layer in nx.get_node_attributes(G, subset_key).items():
layers[layer] = layers.get(layer, []) + [node]
# set layout within each layer
pos = {}
for layer, nodes in layers.items():
subgraph = G.subgraph(nodes)
layer_pos = {
node: node_pos + separation * np.array([0, int(layer)])
for node, node_pos in layout(subgraph).items()
}
pos.update(layer_pos)
return pos
def draw_multilayer_layout(
G,
subset_key="layer",
ax=None,
layout=nx.spring_layout,
separation=2.0,
node_kwargs=dict(node_size=12),
within_edge_kwargs=dict(style="solid", alpha=0.05),
between_edge_kwargs=dict(style="dashed", alpha=0.65),
cmap="Pastel2",
):
# get the layout
pos = multilayer_layout(
G,
subset_key=subset_key,
layout=layout,
separation=separation,
)
# find connections between and plot them differently
connectors = set()
others = set()
for node in G.nodes():
for neighbor in G.neighbors(node):
if G.nodes[node][subset_key] != G.nodes[neighbor][subset_key]:
connectors.add((node, neighbor))
else:
others.add((node, neighbor))
# draw the graph
if ax is None:
fig, ax = plt.subplots()
attr = set(nx.get_node_attributes(G, subset_key).values())
color_space = np.linspace(0, 1, len(attr), 0)
cmap = cmr.pride(color_space)
node_colors = [cmap[G.nodes[node]["layer"]] for node in G.nodes()]
nx.draw_networkx_nodes(G, pos, node_color=node_colors, **node_kwargs)
nx.draw_networkx_edges(G, pos, edgelist=others, **within_edge_kwargs)
nx.draw_networkx_edges(G, pos, edgelist=connectors, **between_edge_kwargs)
return ax
def disjoint_union_all(Gs: list[nx.Graph]) -> nx.Graph:
G = Gs[0]
for Gi in Gs[1:]:
G = nx.disjoint_union(G, Gi)
return G
if __name__ == "__main__":
graphs = []
for layer in range(3):
g = nx.erdos_renyi_graph(100, 0.2)
nx.set_node_attributes(g, layer, "layer")
graphs.append(g)
g = disjoint_union_all(graphs)
from random import sample
for ni in range(100):
edge = sample(list(g.nodes()), 2)
if not g.has_edge(*edge):
g.add_edge(*edge)
fig, ax = plt.subplots()
draw_multilayer_layout(g, ax=ax)
ax.axis("equal")
ax.grid(False)
plt.show(block=1)
Upvotes: 0
Reputation: 13041
There is no functionality within networkx
that currently supports a layered layout, much less a visualization as shown. So we need to roll our own.
The following implementation LayeredNetworkGraph
assumes that you have a list of graphs [g1, g2, ..., gn]
that represent the different layers. Within a layer, the corresponding (sub-) graph defines the connectivity. Between layers, nodes in subsequent layers are connected if they have the same node ID.
As there are no layout functions (AFAIK) that would compute node positions in three dimensions with the planarity constraint imposed on nodes within a layer, we use a small hack: we create a graph composition across all layers, compute the positions in two dimensions, and then apply these positions to nodes in all layers. One could compute a true force directed layout with the planarity constraints, but that would be a lot of work and since your example only used a shell layout (which would be unaffected), I haven't bothered. The differences would be small in many cases.
If you want to change aspects of the visualisation (sizes, widths, colours), have a look at the draw
method. Most changes that you might require can probably be made there.
#!/usr/bin/env python
"""
Plot multi-graphs in 3D.
"""
import numpy as np
import matplotlib.pyplot as plt
import networkx as nx
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d.art3d import Line3DCollection
class LayeredNetworkGraph(object):
def __init__(self, graphs, node_labels=None, layout=nx.spring_layout, ax=None):
"""Given an ordered list of graphs [g1, g2, ..., gn] that represent
different layers in a multi-layer network, plot the network in
3D with the different layers separated along the z-axis.
Within a layer, the corresponding graph defines the connectivity.
Between layers, nodes in subsequent layers are connected if
they have the same node ID.
Arguments:
----------
graphs : list of networkx.Graph objects
List of graphs, one for each layer.
node_labels : dict node ID : str label or None (default None)
Dictionary mapping nodes to labels.
If None is provided, nodes are not labelled.
layout_func : function handle (default networkx.spring_layout)
Function used to compute the layout.
ax : mpl_toolkits.mplot3d.Axes3d instance or None (default None)
The axis to plot to. If None is given, a new figure and a new axis are created.
"""
# book-keeping
self.graphs = graphs
self.total_layers = len(graphs)
self.node_labels = node_labels
self.layout = layout
if ax:
self.ax = ax
else:
fig = plt.figure()
self.ax = fig.add_subplot(111, projection='3d')
# create internal representation of nodes and edges
self.get_nodes()
self.get_edges_within_layers()
self.get_edges_between_layers()
# compute layout and plot
self.get_node_positions()
self.draw()
def get_nodes(self):
"""Construct an internal representation of nodes with the format (node ID, layer)."""
self.nodes = []
for z, g in enumerate(self.graphs):
self.nodes.extend([(node, z) for node in g.nodes()])
def get_edges_within_layers(self):
"""Remap edges in the individual layers to the internal representations of the node IDs."""
self.edges_within_layers = []
for z, g in enumerate(self.graphs):
self.edges_within_layers.extend([((source, z), (target, z)) for source, target in g.edges()])
def get_edges_between_layers(self):
"""Determine edges between layers. Nodes in subsequent layers are
thought to be connected if they have the same ID."""
self.edges_between_layers = []
for z1, g in enumerate(self.graphs[:-1]):
z2 = z1 + 1
h = self.graphs[z2]
shared_nodes = set(g.nodes()) & set(h.nodes())
self.edges_between_layers.extend([((node, z1), (node, z2)) for node in shared_nodes])
def get_node_positions(self, *args, **kwargs):
"""Get the node positions in the layered layout."""
# What we would like to do, is apply the layout function to a combined, layered network.
# However, networkx layout functions are not implemented for the multi-dimensional case.
# Futhermore, even if there was such a layout function, there probably would be no straightforward way to
# specify the planarity requirement for nodes within a layer.
# Therefor, we compute the layout for the full network in 2D, and then apply the
# positions to the nodes in all planes.
# For a force-directed layout, this will approximately do the right thing.
# TODO: implement FR in 3D with layer constraints.
composition = self.graphs[0]
for h in self.graphs[1:]:
composition = nx.compose(composition, h)
pos = self.layout(composition, *args, **kwargs)
self.node_positions = dict()
for z, g in enumerate(self.graphs):
self.node_positions.update({(node, z) : (*pos[node], z) for node in g.nodes()})
def draw_nodes(self, nodes, *args, **kwargs):
x, y, z = zip(*[self.node_positions[node] for node in nodes])
self.ax.scatter(x, y, z, *args, **kwargs)
def draw_edges(self, edges, *args, **kwargs):
segments = [(self.node_positions[source], self.node_positions[target]) for source, target in edges]
line_collection = Line3DCollection(segments, *args, **kwargs)
self.ax.add_collection3d(line_collection)
def get_extent(self, pad=0.1):
xyz = np.array(list(self.node_positions.values()))
xmin, ymin, _ = np.min(xyz, axis=0)
xmax, ymax, _ = np.max(xyz, axis=0)
dx = xmax - xmin
dy = ymax - ymin
return (xmin - pad * dx, xmax + pad * dx), \
(ymin - pad * dy, ymax + pad * dy)
def draw_plane(self, z, *args, **kwargs):
(xmin, xmax), (ymin, ymax) = self.get_extent(pad=0.1)
u = np.linspace(xmin, xmax, 10)
v = np.linspace(ymin, ymax, 10)
U, V = np.meshgrid(u ,v)
W = z * np.ones_like(U)
self.ax.plot_surface(U, V, W, *args, **kwargs)
def draw_node_labels(self, node_labels, *args, **kwargs):
for node, z in self.nodes:
if node in node_labels:
ax.text(*self.node_positions[(node, z)], node_labels[node], *args, **kwargs)
def draw(self):
self.draw_edges(self.edges_within_layers, color='k', alpha=0.3, linestyle='-', zorder=2)
self.draw_edges(self.edges_between_layers, color='k', alpha=0.3, linestyle='--', zorder=2)
for z in range(self.total_layers):
self.draw_plane(z, alpha=0.2, zorder=1)
self.draw_nodes([node for node in self.nodes if node[1]==z], s=300, zorder=3)
if self.node_labels:
self.draw_node_labels(self.node_labels,
horizontalalignment='center',
verticalalignment='center',
zorder=100)
if __name__ == '__main__':
# define graphs
n = 5
g = nx.erdos_renyi_graph(4*n, p=0.1)
h = nx.erdos_renyi_graph(3*n, p=0.2)
i = nx.erdos_renyi_graph(2*n, p=0.4)
node_labels = {nn : str(nn) for nn in range(4*n)}
# initialise figure and plot
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
LayeredNetworkGraph([g, h, i], node_labels=node_labels, ax=ax, layout=nx.spring_layout)
ax.set_axis_off()
plt.show()
Upvotes: 23