Search code examples
pythongraphvizdotpygraphviz

How to place nodes on top of others with Graphviz?


I am trying to design a very specific type of graph with certain colors and shapes but I am unable to tell if this is achievable with Graphviz.

My goal is to draw a directed graph where each node:

  • is composed of 3 smaller boxes aligned horizontally (striped rectangle or record, rounded)
  • contains 3 labels accordingly
  • is filled with a gradient
  • has a black square node located on top of the middle box

Example (all black boxes have been placed manually) :

enter image description here

I have made it to the third requirement but I am struggling implementing the last one. How would you place square nodes (rect, square, box, etc) on top of the first set of nodes (colored rectangles) so as they perfectly fit the dimensions of the middle boxes they are meant to cover ? Is that even possible with Graphviz ?

Example code (Python):

from graphviz import Digraph

# Dictionary storing relations between nodes
d = {0: set([1, 2, 3]), 
     1: set([4, 5, 6]), 
     2: set([7, 8, 9, 10]), 
     3: set([0]), 
     4: set([]), 
     5: set([]), 
     6: set([]), 
     7: set([]), 
     8: set([0]), 
     9: set([]), 
     10: set([])} 

# List of node labels (3 labels per node)
P = [('S', 'M', 'S'),
     ('M', 'S', 'L'),
     ('M', 'S', 'S'),
     ('M', 'S', 'M'),
     ('S', 'L', 'L'),
     ('S', 'L', 'M'),
     ('S', 'L', 'X'),
     ('S', 'S', 'S'),
     ('S', 'S', 'M'),
     ('S', 'S', 'L'),
     ('S', 'S', 'X')]

# Dictionary storing the colors corresponding to each label
c = {'S':'olivedrab1', 
     'M':'mediumturquoise', 
     'L':'deepskyblue', 
     'X':'palevioletred1'}

# Create a "directed graph" with general node and edge attributes
G = Digraph(node_attr={'shape':'record',  
                       'style':'rounded, filled',
                       'color':'white', 
                       'height':'0.1',
                       'fontcolor':'white'},
           
            edge_attr={'color':'grey', 
                       'arrowhead':'vee'} 
           )
       
G.attr('graph', bgcolor='transparent')

# 1st pass: create all nodes (0 to 10)
for k in d:
    l1, l2, l3 = P[k]
    
    # set specific attribute to each node (label & colors)
    G.attr('node', label='{} | {} | {}'.format(l1, ' ', l2), fillcolor='{}:{}'.format(c[l1], c[l2]))
    G.node(str(k))

# 2nd pass: create edges between nodes
for k in d:
    l1, l2, l3 = P[k]
    for i in d[k]:
        if i in d:
            G.edge(str(k), str(i))

# Then, how to overlap black square nodes ?

Solution

  • I don't think it's possible in dot/graphviz to force nodes to superimpose one node on another; I guess that would sort of defeat the whole point of rendering a graph visually.

    This code uses HTML(-like) shapes for the nodes; these describe a three-cell table with gradient colours on the left and right side, and solid black with white text in the centre.

    import graphviz
    
    # Dictionary storing relations between nodes
    d = {0: set([1, 2, 3]), 
         1: set([4, 5, 6]), 
         2: set([7, 8, 9, 10]), 
         3: set([0]), 
         4: set([]), 
         5: set([]), 
         6: set([]), 
         7: set([]), 
         8: set([0]), 
         9: set([]), 
         10: set([])} 
    
    # List of node labels (3 labels per node)
    P = [('S', 'M', 'S'),
         ('M', 'S', 'L'),
         ('M', 'S', 'S'),
         ('M', 'S', 'M'),
         ('S', 'L', 'L'),
         ('S', 'L', 'M'),
         ('S', 'L', 'X'),
         ('S', 'S', 'S'),
         ('S', 'S', 'M'),
         ('S', 'S', 'L'),
         ('S', 'S', 'X')]
    
    # Dictionary storing the colors corresponding to each label
    c = {'S':'olivedrab1:grey', 
         'M':'mediumturquoise:gray', 
         'L':'deepskyblue:gray', 
         'X':'palevioletred1:gray'}
    
    # Create a "directed graph" with general node and edge attributes
    G = graphviz.Digraph(
                format='svg'
                ,edge_attr={'color':'grey', 
                           'arrowhead':'vee'} 
               )
           
    G.attr('graph', bgcolor='transparent')
    
    # 1st pass: create all nodes (0 to 10)
    for k in d:
        l1, l2, l3 = P[k]
        
        G.node(name=str(k),shape='plain',label=f'''<
    <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" BGCOLOR="gray">
      <TR>
        <TD BGCOLOR="{c[l1]}" GRADIENTANGLE="45">{l1}</TD>
        <TD BGCOLOR="black"><FONT COLOR="white">{l2}</FONT></TD>
        <TD BGCOLOR="{c[l3]}" GRADIENTANGLE="135">{l3}</TD>
      </TR>
    </TABLE>
    >
    '''
    )
    
    # 2nd pass: create edges between nodes
    for k in d:
        l1, l2, l3 = P[k]
        for i in d[k]:
            if i in d:
                G.edge(str(k), str(i))
    
    G.view()
    

    Result:

    enter image description here

    Notes:

    • the node gets the shape of the HTML when shape is set to 'plain'
    • The colour gradient is specified in the cell <TD> as BGCOLOR=“colour1:colour2”
    • The angle of the gradient is also specified in the cell <TD> as e.g.GRADIENTANGLE=“45”

    More info about HTML nodes here: http://graphviz.org/doc/info/shapes.html#gradientangle