pilomuuuu
pilomuuuu

Reputation: 131

Multi-layer graph in networkx

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')

enter image description here

enter image description here

enter image description here

Upvotes: 13

Views: 9390

Answers (2)

cvanelteren
cvanelteren

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

Paul Brodersen
Paul Brodersen

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.

Plot of multi-layered network

#!/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

Related Questions