python-3.xnetworkxgraphvizgraph-visualizationpydot

Compact graph-vizualization using pydot


I would like to visualize a "linear" directed graph with the layout like that:

compact visualization

All the in- and out-degrees are 1 (except the first and last, of course). The length of the labels are different, so I can't calculate easily, how many nodes will fit in one row or the other. The code I have so far is this.

import networkx as nx
from networkx.drawing.nx_pydot import to_pydot

G = nx.DiGraph()
G.add_node("XYZ 1.0")
for i in range(1, 20):
    G.add_node(f'XYZ 1.{i}', style='filled', fillcolor='skyblue')
    G.add_edge(f'XYZ 1.{i-1}', f'XYZ 1.{i}')

# set defaults
G.graph['graph'] = {'rankdir': 'LR'}
G.graph['node'] = {'shape': 'rectangle'}
G.graph['edges'] = {'arrowsize': '4.0'}

pydt = to_pydot(G)
prog = 'dot'
file_name = f'nx_graph_{prog}.png'
pydt.write(file_name, prog=prog, format="png")

So far I use networkx in a project that needs to be run in a Python docker container, so I would like to use pydot and Networkx, if it is possible.

In some of the graphviz programs I can set coordinates if I understand correctly, but for setting coordinates I should know the widths of the boxes to avoid overlapping boxes.


Solution

  • I managed to find a way to do this with pydot. We can create a dot file with the coordinates with the write_dot function. Reading it back, we can get the coordinates that dot program created (and also the widths, heights). We can somehow calculate the new coordinates and modify them in the networkx Digraph. Converting again to pydot.Dot object, and at the end, we can use neato with the -n option to create the graph, that way we use the coordinates we have set. A working code can be seen below.

    import networkx as nx
    from networkx.drawing.nx_pydot import to_pydot
    import pydot
    from typing import List
    
    G = nx.DiGraph()
    G.add_node(0, label="XYZ 1.0")
    for i in range(1, 20):
        G.add_node(i, label=f'XYZ 1.{i}')
        G.add_edge(i - 1, i)
    
    # set defaults
    G.graph['graph'] = {'rankdir': 'LR'}
    G.graph['node'] = {'shape': 'rectangle'}
    G.graph['edges'] = {'arrowsize': '4.0'}
    
    pydt = to_pydot(G)
    dot_data = pydt.create_dot()
    
    pydt2 = pydot.graph_from_dot_data(dot_data.decode('utf-8'))[0]
    
    
    def get_position(node):
        pydot_node = pydt2.get_node(str(node))[0]
        return [float(i) for i in pydot_node.get_attributes().get("pos")[1:-1].split(',')]
    
    
    def fix_position(position: List, w: float = 1000, shift: float = 80):
        x_orig, y_orig = position
        n = int(x_orig / w)
        y = y_orig - n * shift
        remain_x = x_orig - n * w
        if n % 2 == 0:
            x = remain_x
        else:
            x = w - remain_x
        return x, y
    
    
    def refresh_coordinates_using_x():
        for node in G.nodes:
            position = get_position(node)
            x, y = fix_position(position)
            pos = f'"{x},{y}!"'
            G.nodes[node]['pos'] = pos
    
    
    refresh_coordinates_using_x()
    
    pydt3 = to_pydot(G)
    file_name = f'nx_graph_neato.png'
    pydt3.write(file_name, prog=["neato", "-n"], format="png")
    

    If you want to calculate the position of the nodes based on the widths, you need to know, that while the coordinates are in points, the widths are in inches. 1 inch is 72 points.

    The result will be similar to this one.

    enter image description here