Is it possible to achieve this type of design?

Hi again,

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

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 covering ? Is that even possible with Graphviz ?

Example code (Python):

# 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 ?
digraph G {
 node [shape=plaintext]
 t [label=<
 <TABLE border="1" cellborder="1" cellspacing="0" cellpadding="0" style="rounded"> 
  <TR>
    <TD WIDTH="50" BGCOLOR="red:blue" ><FONT COLOR="white">00</FONT></TD>
    <TD WIDTH="50" BGCOLOR="black"><FONT COLOR="white">02</FONT></TD>
    <TD WIDTH="50" BGCOLOR="pink:green"><FONT COLOR="white">03</FONT></TD>    
  </TR>
</TABLE>>];
}

Giving:
doneWithHtml

1 Like

Hi, thank you for the pointer !

The problem with this approach is that:

  • there is 1 gradient per cell instead of 1 filling all three at once
  • gradients are filling the inner cells, not the larger node.
  • the overall outline is not sharp/clear because of the overlapping of multiple cell borders (inner+outer)

Illustration:

example

When removing the inner cells’ borders for a cleaner outline, it becomes apparent that the gradients are not filling the main node.

Instead, is there a way to place a rect on top of the middle cell of a record node, just like in my previous example picture ?

Very fiddly, but seemingly what you want.

  • Start with dot to position the nodes
  • Output to dot output format (dot -Tdot) (see DOT | Graphviz)
  • Use post-processing to add center node (see script below),
  • Finally run through neato -n (FAQ | Graphviz)
    Demo graph:
digraph G {
 node [shape=Mrecord fontcolor="white"]
 triple [style=filled fillcolor="red:blue" label="<f0>AA|<f1>BB|<f2>CC"];
}

I used gvpr to add the center node overlay (see https://graphviz.org/pdf/gvpr.1.pdf), but seemingly python (or others) could be used

My script:

f=gradientOverlay.gv;
T=dot;
F=`basename $f .gv`;
/usr/bin/dot -T$T $f |
/usr/bin/gvpr -c 'BEGIN{int Z=0;node_t aNode}N{Z++;aNode=node($G,"__top_" + (string)Z);aNode.width=.05+(float)$.width/3.;aNode.height=$.height;aNode.pos=$.pos;aNode.shape="rect";aNode.style="filled";aNode.fillcolor="black";aNode.label=html($G,"<u>4&#960;</u><BR/>8");}'
|/usr/bin/neato -n -Tpng >O.png

Giving:
O

The post-processing creates new, overlay nodes, using the pos of the original node. It sets attributes of the new node as needed, setting width to ~original width/3 and height to ~original height.

1 Like

Thank you so much! I’ll see if I can implement this in Python.

I am having a difficult time finding a way to do the same with Python. Am I correct that gv.3python is the right extension to load in this case ? If so, where can I find it ?

If not, would you mind sharing a transcription of your example in Graphviz Python ? That would be very helpful.

[Note: I am not a Python coder and have never used the PyGraphviz interface. Take my suggestions with a ton of salt]

My best guess:

  • I don’t think you need to worry about gv.3python
  1. Add the labels for the “overlay” nodes to the existing nodes using a new, made-up attribute name. I suggest something like centerLabel. It is completely legal to add new attribute name/value pairs to Graphviz files. Graphviz produces no error messages, but just passes them downstream.
    [after the code presented above]
  2. G.layout to add positions to the graph
  3. iterate through all the nodes. For each node with a centerLabel attribute (see above):
    3.1. use node command to create the overlay node (using a new, unique node name)
    3.2 set the overlay node pos equal to the old node pos
    3.3 set the overlay node shape to “rect”
    3.4 set the overlay node style to “filled”
    3.5 set the overlay node fillcolor to “black”
    3.6 set the overlay node height to ~ old node height - .02
    3.7 set the overlay node width to ~ .05 + (old node width/3)
    3.8 set the overlay node label to the old node centerLabel value
  4. G.layout (again) but using the neato engine and -n arg
  5. G.draw - I assume this produces the output

This is “best guess”. I hope it helps in some way.