Need to control pydot edge length

3.4k views Asked by At

Is there a way to use pydot to generate a directed tree graph that looks like "normal tree graphs" where every node of the same depth displays at the same depth, and every edge is at some angle (not straight down)?

When I use these defaults:

graph = pydot.Dot(graph_type='digraph', nodesep=.75)
graph.set_node_defaults(style="filled", fillcolor="grey")
graph.set_edge_defaults(color="blue", arrowhead="vee", weight="0")

The length of the edge between nodes 2 and 1 is too long. See:
enter image description here

I can correct the length of the node using an edge weight of "1" like:

graph.set_edge_defaults(color="blue", arrowhead="vee", weight="1")

But now two of the edges are pointing straight down between 2 and 3, and 7 and 6. See: enter image description here

Edges pointing straight down turns out to be a real problem. If both left and right siblings are not drawn, then it is not clear which node (left or right) is drawn.

PS: In my previous question on this same project I asked how to control the angle of edges so a pydot generated tree graph would avoid edges pointing straight down. That question was answered very well by "Guy". After implementing Guy's solution using invisible nodes, a new side effect has emerged: The length of some edges on some graphs have lengths that are too long. Here is a link to the prior question for reference: Is there a way to control line angle when producing graphs with pyDot

1

There are 1 answers

0
AudioBubble On

To control a pydot digraph's node position and edge length properly so that edges are parallel to each other when pointing in the same direction, no edges point straight down, and all nodes drawn to their appropriate level - invisible nodes must be used.

In the previous SO Q/A session on this topic "how to control line angles..." I learned that invisible nodes are used to fill out missing leaf nodes. This works for some trees but there were several side effects.

Now, I have learned to add a third invisible node to each node with both left and right nodes. The invisible nodes are not added to the tree, mind you, but are just drawn into the graph. So the tree can still be used for searching, insertion, deletion, and other methods.

Here is a graphviz example demonstrating the solution:

digraph {
  nodesep=0.35
  ordering=out
  node[style="filled", fillcolor="grey"]
  edge[color="blue", arrowhead="vee"]
  { node[shape=point style=invis] m5 m2 m8 m6 m4 }

  5 ->  2
  5 ->  m5 [weight=100 style=invis]
  5 ->  8
  2 -> 1
  2 -> m2 [weight=100 style=invis]
  2 -> 4
  8 -> 6
  8 -> m8 [weight=100 style=invis]
  4 -> 3
  4 -> m4 [weight=100 style=invis]
  6 -> m6 [weight=100 style=invis]
  6 ->  7
}

Here is a snippet of my updated python code to automate this process with a typical tree class:

    vT = visualizeTree(0, fileDir, 'bst_graph','.png',1) # instantiate the visualizeTree Object
    graph = pydot.Dot(graph_type='digraph', nodesep=.5, pad=.3, size="19.2, 10.1")
    graph.set_node_defaults(style="filled", fillcolor="grey")
    graph.set_edge_defaults(color="blue", arrowhead="vee")
    vT.searchTree(root, sketchTree)
    vT.updateGraph()

class visualizeTree(object):
    # more code and comments located at project home
    def __init__(self, fileCount, fileDir, fileName, fileExt, vidFrames):

    def sketchTree(node, stack, find=None, draw=None):
        if node.getLeftBranch():
            draw(str(node), str(node.getLeftBranch()))
            stack.append(node.getLeftBranch()) 
            if node.getRightBranch():
                # insert invisible third node in-between left and right nodes
                draw(str(node), ":"+str(node), style_type="invisible")
        elif node.getRightBranch():
            # draw any missing left branches as invisible nodes/edges with dummy unique labels 
            draw(str(node), ":"+str(node), style_type="invisible")
        if node.getRightBranch():
            draw(str(node), str(node.getRightBranch()))
            stack.append(node.getRightBranch())      
        elif node.getLeftBranch():
            # draw any missing right branches as invisible nodes/edges with dummy unique labels 
            draw(str(node), ";"+str(node), style_type="invisible")

    def draw(self, parent_name, child_name, fill_color="grey", style_type='filled'):                  
        if style_type=="invisible":
            # save original edge defaults
            weight_ = "100"
            saveEdgeDefaults = graph.get_edge_defaults()[0]
            graph.set_edge_defaults(style=style_type, color="white", arrowhead="none") 
        else:
            weight_ = "3"
        edge = pydot.Edge(parent_name, child_name, style=style_type, weight=weight_)
        graph.add_edge(edge)  
        if style_type=="invisible":
            graph.set_edge_defaults(**saveEdgeDefaults)        
        if not self.nodeNames:
            self.nodeNames[parent_name] = pydot.Node(parent_name, label=parent_name, fillcolor=fill_color, style=style_type)
            graph.add_node(self.nodeNames[parent_name]) 
        if (parent_name not in self.nodeNames):    
            self.nodeNames[parent_name] = pydot.Node(parent_name, label=parent_name, fillcolor=fill_color, style=style_type)
            graph.add_node(self.nodeNames[parent_name])
        if child_name not in self.nodeNames:
            self.nodeNames[child_name] = pydot.Node(child_name, label=child_name, fillcolor=fill_color, style=style_type)
            graph.add_node(self.nodeNames[child_name])

Full source code can be found at the project's home: http://www.embeddedcomponents.com/blogs/2013/12/visualizing-software-tree-structures/

Typical small images now draw as I wanted in the first place: binary search tree

Even large trees can be drawn with similar geometry: larger binary search tree with ordered search trail