1

I want to draw a layered graph If you can look at the graph below, it can be ordered the following way

 95   85
 21  31
 42  52  62  22 32
 13
 14

I can provide hints which node in which layer goes if needed.

In the past I had similar need, but it does not work in this case : NetworkX: draw graph in layers

I tried all standard layouts and all graph_viz layouts : ‘dot’, ‘twopi’, ‘fdp’, ‘sfdp’, ‘circo’

enter image description here


By layered I mean where every group of nodes are on the same layer (vertical coordinates : Y)

Like I've ordered them above ...


this my first crack on it ...now i have to do the x-axis more clearly based on edges. thanks mathfux

def layered_layout(nodes, base):
    nodes = np.array(nodes)

    ncols = np.unique( nodes % base, return_counts=True)[1].max()                                                                                                                      
    nlayers = np.unique(nodes % base).size
    ys = np.linspace(0, 1, nlayers)
    xs = np.linspace(0, 1, ncols)
    pos = {}
    for j,b in enumerate(np.unique(nodes % base)):
        ixs = np.where(nodes % base == b)[0]
    
        for i,x in enumerate(ixs) :
            node = nodes[x]
            pos[node] = (xs[i], ys[j])

    return pos      
sten
  • 7,028
  • 9
  • 41
  • 63

1 Answers1

3

In case you can't find any default layout, you can define your custom layout. It seems not that complicated for layered manner.

def layered_layout(layers, stretch=0, alignment='c'):
    sizes = np.array([len(n) for n in layers])
    lfill = [np.linspace(0, 1, n) for n in sizes]
    scales = (sizes - 1)/(max(sizes) - 1)
    if alignment == 'l':
        x_coord_levels = [(x - x[0]) * (s + (1 - s) * stretch) for x, s in zip(lfill, scales)]
    elif alignment == 'r':
        x_coord_levels = [(x - x[-1]) * (s + (1 - s) * stretch) for x, s in zip(lfill, scales)]
    elif alignment == 'c':
        x_coord_levels = [(x - (x[0]+x[-1])/2) * (s + (1 - s) * stretch) for x, s in zip(lfill, scales)]
    else:
        raise AttributeError('invalid alignment attribute')
    y_coord_levels = [np.repeat(-y, times) for y, times in zip(np.arange(len(sizes)), sizes)]
    pos_levels = [dict(zip(l, zip(*p))) for l,p in zip(layers, zip(x_coord_levels, y_coord_levels))]
    pos = {k: v for d in pos_levels for k, v in d.items()}
    return pos

After you create a graph :

import networkx as nx
import matplotlib.pyplot as plt
import numpy as np
G = nx.DiGraph()
G.add_edges_from([(21, 42), (52, 13), (42, 13), (95, 21), (31, 52), (31, 62), (95, 31), (62, 13), (32, 95), (13, 22), (85, 31), (22, 14), (14, 32)])
nx.set_edge_attributes(G, {(21, 42): 0.20, (52, 13): 0.52, (42, 13): 0.49, (95, 21): 0.15, (31, 52): 0.52, (31, 62): 0.42, (95, 31): 0.47, (62, 13): 0.42, (32, 95): 0.42, (13, 22): 0.71, (85, 31): -0.00, (22, 14): 0.74, (14, 32): 0.74}, 'w')

layered_layout should be used to define values of pos parameter:

layers = [[95, 85], [21, 31, 42], [52, 62, 22, 32], [13], [14]]
pos = layered_layout(layers, stretch=..., alignment=...)

Example of usage:

layers = [[95, 85], [21, 31, 42], [52, 62, 22, 32], [13], [14]]
fig = plt.figure(figsize=(20,10))
for i in range(3):
    for j in range(3):
        ax = fig.add_subplot(3, 3, 1+3*j+i)
        ax.set_title(f'stretch = {[0, 0.5, 1][j]}, alignment={"lcr"[i]}')
        pos = layered_layout(layers, stretch = [0, 0.5, 1][j], alignment='lcr'[i])
        nx.draw_networkx(G, pos, with_labels=True, font_size=7, node_size=100)
        nx.draw_networkx_edge_labels(G, pos, edge_labels = nx.get_edge_attributes(G,'w'), font_size=7)
        plt.axis('on'); plt.tick_params(left=True, bottom=True, labelleft=True, labelbottom=True)
plt.show()

enter image description here

mathfux
  • 5,759
  • 1
  • 14
  • 34
  • seem complex to me :) tring to figure it out , is there a place where it is explained what is the expected 'pos' format.. I would prefer where I can set the 'Layer' based on the node-id. The nodes are modulo-numbers (in this case mod10), so I can group them and calculate in advance how many layers i would need. – sten Aug 23 '20 at 17:24
  • Alright, so for me personally, the hardest part is to implement it but an algorithm can be visible from a visual view of layout itself (and this what I mean saying "not too complex"). The most important thing is to understand that each level of layout consists of interval `[0, 1]` padded by a certain number of points. The next job is to implement a proper way to shift and scale each of these levels. I'll document this a little bit later. – mathfux Aug 23 '20 at 19:00
  • All you need for now is to know that `pos` is a dictionary of nodes and corresponding coordinates and what `stretch` and `alignment` parameters of `layered_layout` are responsible for. – mathfux Aug 23 '20 at 19:02
  • thanks.. i got the linspace and the format now {node: (x,y) ..}.. what is the range of x,y coords ... [0,1] or i have to scale them to some predefined size – sten Aug 23 '20 at 20:53
  • 1
    `y` coordinatess are not so important because `plt.show` scales them automatically. And `x` coordinates are set up to `[0,1]` at the beginning then shifted so that startpoint, midpoint or endpoint goes to zero (it depends on `alignment` parameter) and then streched. If `stretch = 0`, horizontal spaces are equal, if `stretch = 1`, left and right margins hold, otherwise spacing depends on `stretch` parameter in a linear way. Are these attributes enough for you to customise your layout? – mathfux Aug 23 '20 at 21:37
  • seems so.. i see that X is the hard to do ....otherwise some time edges overlap if not spaced correctly .. will look at your stretch and align.. – sten Aug 24 '20 at 00:04