56

I am trying to display a tree graph of my class hierarchy using networkx. I have it all graphed correctly, and it displays fine. But as a circular graph with crossing edges, it is a pure hierarchy, and it seems I ought to be able to display it as a tree.

I have googled this extensively, and every solution offered involves using pygraphviz... but PyGraphviz does not work with Python 3 (documentation from the pygraphviz site).

Has anyone been able to get a tree graph display in Python 3?

12
  • With networkx you should be able to use DIGraph with the dot layout. This should display a tree graph. Commented Apr 12, 2015 at 9:34
  • The development version of pygraphviz does work with Python 3. Commented Apr 12, 2015 at 12:34
  • You might try using the spring layout, networkx.spring_layout() Commented Apr 12, 2015 at 12:35
  • I tried spring layout -- what displays is still circular, with overlapping edges. Commented Apr 12, 2015 at 14:06
  • I've provided an answer, but it won't look particularly nice if the tree has some branches that are very "wide". I think this is where a lot of the effort of pygraphviz happens. Let me know if it works for you. If not, let me know what looks bad about it and I'll see if it's an easy fix. Commented Apr 13, 2015 at 6:32

9 Answers 9

109

[scroll down a bit to see what kind of output the code produces]

edit (7 Nov 2019) I've put a more refined version of this into a package I've been writing: https://epidemicsonnetworks.readthedocs.io/en/latest/_modules/EoN/auxiliary.html#hierarchy_pos. The main difference between the code here and the version there is that the code here gives all children of a given node the same horizontal space, while the code following that link also considers how many descendants a node has when deciding how much space to allocate it.

edit (19 Jan 2019) I have updated the code to be more robust: It now works for directed and undirected graphs without any modification, no longer requires the user to specify the root, and it tests that the graph is a tree before it runs (without the test it would have infinite recursion - see user2479115's answer for a way to handle non-trees).

edit (27 Aug 2018) If you want to create a plot with the nodes appearing as rings around the root node, the code right at the bottom shows a simple modification to do this

edit (17 Sept 2017) I believe the trouble with pygraphviz that OP was having should be fixed by now. So pygraphviz is likely to be a better solution that what I've got below.


Here is a simple recursive program to define the positions. The recursion happens in _hierarchy_pos, which is called by hierarchy_pos. The main role of hierarcy_pos is to do a bit of testing to make sure the graph is appropriate before entering the recursion:

import networkx as nx
import random

    
def hierarchy_pos(G, root=None, width=1., vert_gap = 0.2, vert_loc = 0, xcenter = 0.5):

    '''
    From Joel's answer at https://stackoverflow.com/a/29597209/2966723.  
    Licensed under Creative Commons Attribution-Share Alike 
    
    If the graph is a tree this will return the positions to plot this in a 
    hierarchical layout.
    
    G: the graph (must be a tree)
    
    root: the root node of current branch 
    - if the tree is directed and this is not given, 
      the root will be found and used
    - if the tree is directed and this is given, then 
      the positions will be just for the descendants of this node.
    - if the tree is undirected and not given, 
      then a random choice will be used.
    
    width: horizontal space allocated for this branch - avoids overlap with other branches
    
    vert_gap: gap between levels of hierarchy
    
    vert_loc: vertical location of root
    
    xcenter: horizontal location of root
    '''
    if not nx.is_tree(G):
        raise TypeError('cannot use hierarchy_pos on a graph that is not a tree')

    if root is None:
        if isinstance(G, nx.DiGraph):
            root = next(iter(nx.topological_sort(G)))  #allows back compatibility with nx version 1.11
        else:
            root = random.choice(list(G.nodes))

    def _hierarchy_pos(G, root, width=1., vert_gap = 0.2, vert_loc = 0, xcenter = 0.5, pos = None, parent = None):
        '''
        see hierarchy_pos docstring for most arguments

        pos: a dict saying where all nodes go if they have been assigned
        parent: parent of this branch. - only affects it if non-directed

        '''
    
        if pos is None:
            pos = {root:(xcenter,vert_loc)}
        else:
            pos[root] = (xcenter, vert_loc)
        children = list(G.neighbors(root))
        if not isinstance(G, nx.DiGraph) and parent is not None:
            children.remove(parent)  
        if len(children)!=0:
            dx = width/len(children) 
            nextx = xcenter - width/2 - dx/2
            for child in children:
                nextx += dx
                pos = _hierarchy_pos(G,child, width = dx, vert_gap = vert_gap, 
                                    vert_loc = vert_loc-vert_gap, xcenter=nextx,
                                    pos=pos, parent = root)
        return pos

            
    return _hierarchy_pos(G, root, width, vert_gap, vert_loc, xcenter)

and an example usage:

import matplotlib.pyplot as plt
import networkx as nx
G=nx.Graph()
G.add_edges_from([(1,2), (1,3), (1,4), (2,5), (2,6), (2,7), (3,8), (3,9), (4,10),
                  (5,11), (5,12), (6,13)])
pos = hierarchy_pos(G,1)    
nx.draw(G, pos=pos, with_labels=True)
plt.savefig('hierarchy.png')

enter image description here

Ideally this should rescale the horizontal separation based on how wide things will be beneath it. I'm not attempting that but this version does: https://epidemicsonnetworks.readthedocs.io/en/latest/_modules/EoN/auxiliary.html#hierarchy_pos

Radial expansion

Let's say you want the plot to look like:

enter image description here

Here's the code for that:

pos = hierarchy_pos(G, 0, width = 2*math.pi, xcenter=0)
new_pos = {u:(r*math.cos(theta),r*math.sin(theta)) for u, (theta, r) in pos.items()}
nx.draw(G, pos=new_pos, node_size = 50)
nx.draw_networkx_nodes(G, pos=new_pos, nodelist = [0], node_color = 'blue', node_size = 200)

edit - thanks to Deepak Saini for noting an error that used to appear in directed graphs

Sign up to request clarification or add additional context in comments.

9 Comments

Needs neighbors = list(G.neighbors(root)) for python 3.
@typingduck Can you check if neighbors = G.neighbors(root) and then later if neighbors: rather than if len(neighbors)!=0: works correctly?
What if there is a loop, can we show it by above graph? Example: For this data [(1,2), (1,3), (1,4), (2,5), (2,6), (2,7), (3,8), (3,9), (4,10),(5,11), (5,12), (6,13),(13,1)]
Maybe it's only me but if you care about the (lexicographic) ordering of the child nodes, add the line children.sort() below children = list(G.neighbors(root))
Thanks for this code. We should get it into Networkx. A problem I notice with all of the solutions is that if the tree is very large the nodes meld into one another. I have openned an issue here github.com/springer-math/Mathematics-of-Epidemics-on-Networks/… and that has a screenshot which shows a rendering.
|
19

Here is a solution for large trees. It is a modification of Joel's recursive approach that evenly spaces nodes at each level.

def hierarchy_pos(G, root, levels=None, width=1., height=1.):
    '''If there is a cycle that is reachable from root, then this will see infinite recursion.
       G: the graph
       root: the root node
       levels: a dictionary
               key: level number (starting from 0)
               value: number of nodes in this level
       width: horizontal space allocated for drawing
       height: vertical space allocated for drawing'''
    TOTAL = "total"
    CURRENT = "current"
    def make_levels(levels, node=root, currentLevel=0, parent=None):
        """Compute the number of nodes for each level
        """
        if not currentLevel in levels:
            levels[currentLevel] = {TOTAL : 0, CURRENT : 0}
        levels[currentLevel][TOTAL] += 1
        neighbors = G.neighbors(node)
        for neighbor in neighbors:
            if not neighbor == parent:
                levels =  make_levels(levels, neighbor, currentLevel + 1, node)
        return levels

    def make_pos(pos, node=root, currentLevel=0, parent=None, vert_loc=0):
        dx = 1/levels[currentLevel][TOTAL]
        left = dx/2
        pos[node] = ((left + dx*levels[currentLevel][CURRENT])*width, vert_loc)
        levels[currentLevel][CURRENT] += 1
        neighbors = G.neighbors(node)
        for neighbor in neighbors:
            if not neighbor == parent:
                pos = make_pos(pos, neighbor, currentLevel + 1, node, vert_loc-vert_gap)
        return pos
    if levels is None:
        levels = make_levels({})
    else:
        levels = {l:{TOTAL: levels[l], CURRENT:0} for l in levels}
    vert_gap = height / (max([l for l in levels])+1)
    return make_pos({})

Joel's example will look like this: enter image description here

And this is a more complex graph (rendered using plotly):enter image description here

3 Comments

This would seem to be something that should be easy out-of-the-box. I teach CS, and I would love to use this package to create b-trees, red-black trees, etc.... But it is a little cumbersome right now.
Note that you have to replace neighbors = G.neighbors(node) with neighbors = list(G.neighbors(node)) for this to work in Python 3.
Thanks, I have updated the code now (the problem was due to an old version of networkx).
15

The simplest way to get a nice-looking tree graph display in Python 2 or 3 without PyGraphviz is to use PyDot (https://pypi.python.org/pypi/pydot). Whereas PyGraphviz provides an interface to the whole of Graphviz, PyDot only provides an interface to Graphviz's Dot tool, which is the only one you need if what you're after is a hierarchical graph / a tree. If you want to create your graph in NetworkX rather than PyDot, you can use NetworkX to export a PyDot graph, as in the following:

import networkx as nx

g=nx.DiGraph()
g.add_edges_from([(1,2), (1,3), (1,4), (2,5), (2,6), (2,7), (3,8), (3,9),
                  (4,10), (5,11), (5,12), (6,13)])
p=nx.drawing.nx_pydot.to_pydot(g)
p.write_png('example.png')

Note that Graphviz and PyDot need to be installed for the above to work correctly.

enter image description here

Warning: I have experienced problems when using PyDot to draw graphs with node attribute dictionaries exported from NetworkX - sometimes the dictionaries seem to be exported with quotation marks missing from strings, which causes the write method to crash. This can be avoided by leaving out the dictionaries.

3 Comments

I've been searching since 2 days for simple answer without graphviz! Thanks a ton!
Unfortunately, pydot is no longer maintained regularly, so this is no longer a good option. Trying it, networkx gives this warning: DeprecationWarning: nx.nx_pydot.to_pydot depends on the pydot package, which hasknown issues and is not actively maintained. See https://github.com/networkx/networkx/issues/5723
I am not sure if it was reinstated but I don't get such warning and github repo had last update recently. Thumbs up to the author, I kicked out the attribute and that sorted out my error.
10

I modified slightly so that it would not infinitely recurse.

def hierarchy_pos_no_recur(
    G, root, width=1.0, vert_gap=0.2, vert_loc=0, xcenter=0.5
):
    """If there is a cycle that is reachable from root, then result will not be a hierarchy.

    G: the graph
    root: the root node of current branch
    width: horizontal space allocated for this branch - avoids overlap with other branches
    vert_gap: gap between levels of hierarchy
    vert_loc: vertical location of root
    xcenter: horizontal location of root
    """

    def h_recur(
        G,
        root,
        width=1.0,
        vert_gap=0.2,
        vert_loc=0,
        xcenter=0.5,
        pos=None,
        parent=None,
        parsed=[],
    ):
        if root not in parsed:
            parsed.append(root)
            if pos == None:
                pos = {root: (xcenter, vert_loc)}
            else:
                pos[root] = (xcenter, vert_loc)
            neighbors = list(G.neighbors(root))
            if parent != None and parent in neighbors:
                neighbors.remove(parent)
            if len(neighbors) > 0:
                dx = width / len(neighbors)
                nextx = xcenter - width / 2 - dx / 2
                for neighbor in neighbors:
                    nextx += dx
                    pos = h_recur(
                        G,
                        neighbor,
                        width=dx,
                        vert_gap=vert_gap,
                        vert_loc=vert_loc - vert_gap,
                        xcenter=nextx,
                        pos=pos,
                        parent=root,
                        parsed=parsed,
                    )
        return pos

    return h_recur(G, root, width=1.0, vert_gap=0.2, vert_loc=0, xcenter=0.5)

Comments

8

There is a networkx-only solution to this question that is in the documentation. See https://networkx.org/documentation/stable/auto_examples/graph/plot_dag_layout.html.

Here's a slightly modified version of the code appearing on that case, specializing from the case of a DAG flowing from left to right to a tree falling from top to bottom.

import networkx as nx
import matplotlib.pyplot as plt


G = nx.DiGraph(
    [
        ("f", "a"),
        ("a", "b"),
        ("b", "d"),
        ("d", "e"),
        ("f", "c"),
        ("f", "g"),
        ("h", "f"),
    ]
)

for layer, nodes in enumerate(reversed(tuple(nx.topological_generations(G)))):
    # `multipartite_layout` expects the layer as a node attribute, so add the
    # numeric layer value as a node attribute
    for node in nodes:
        G.nodes[node]["layer"] = layer

# Compute the multipartite_layout using the "layer" node attribute
pos = nx.multipartite_layout(G, subset_key="layer", align='horizontal')

fig, ax = plt.subplots()
nx.draw_networkx(G, pos=pos, ax=ax)
ax.set_title("Tree layout in topological order")
fig.tight_layout()
plt.show()

This generates: enter image description here

I'd prefer b, d, and e to fall directly below a, but this is at least close to what you want with no additional dependencies.

Comments

5

I used grandalf for a python-only solution that uses neither graphviz nor pygraphviz.

Also, this type of visualization is called a layered graph drawing or Sugiyama-style graph drawing, which can display many kinds of graphs, including non-trees.

import networkx as nx
import grandalf
from grandalf.layouts import SugiyamaLayout

G = nx.DiGraph()

# Build your networkx graph here
[G.add_node(data) for data in range(10)]
X = [(0,1),(0,2),(1,3),(2,3),(1,4),(4,5),(5,6),(3,6),(3,7),(6,8),(7,8),(8,9),(5,9)]
for x in X:
    G.add_edge(*x)

g = grandalf.utils.convert_nextworkx_graph_to_grandalf(G)  # undocumented function

class defaultview(object): # see README of grandalf's github
    w, h = 10, 10


for v in g.C[0].sV:
    v.view = defaultview()

sug = SugiyamaLayout(g.C[0])
sug.init_all() # roots=[V[0]])
sug.draw()
# This is a bit of a misnomer, as grandalf doesn't actually come with any visualization methods.
# This method instead calculates positions

poses = {v.data: (v.view.xy[0], v.view.xy[1]) for v in g.C[0].sV} # Extracts the positions
nx.draw(G, pos=poses, with_labels=True)
import matplotlib.pyplot as plt
plt.show()

2 Comments

several typos to correct
Sure, fixed 2 typos. Do note that "nextworkx" (sic) is correct -- that one is a typo from grandalf.
4

Very simple hacky topology-based heirachical plot. Only works with DiGraphs. Offsetting is helpful if you have long labels:

def topo_pos(G):
    """Display in topological order, with simple offsetting for legibility"""
    pos_dict = {}
    for i, node_list in enumerate(nx.topological_generations(G)):
        x_offset = len(node_list) / 2
        y_offset = 0.1
        for j, name in enumerate(node_list):
            pos_dict[name] = (j - x_offset, -i + j * y_offset)

    return pos_dict

# Same example data as top answer, but directed
G=nx.DiGraph()
G.add_edges_from([
    (1,2), (1,3), (1,4), (2,5), (2,6), (2,7),
    (3,8), (3,9), (4,10), (5,11), (5,12), (6,13)])
pos = topo_pos(G)

nx.draw(G, pos)
nx.draw_networkx_labels(G, pos, horizontalalignment="left")

enter image description here

Comments

2

For a directed graph, Since neighbors(x) include only the succesors(x), so you have to remove the lines:

if parent != None:
        neighbors.remove(parent)

Also, a better option would be this:

pos=nx.graphviz_layout(G,prog='dot')

Comments

1

I extend Joel's answer for directed acyclic graphs (DAGs) with exactly one root, and dynamically allocate space based on children's widths.

My answer uses networkx only. The steps are:

  1. Perform a topological sort, separating graph G into a tree T and a list of forward-like edges. This extends the layout to single-root DAGs and is visually appealing.

  2. Assigns each node a width based on their subtree size:

    • For leaf node: leaf_width
    • For internal node: sum of widths of children
  3. Run a modification of Joel's answer using dynamic node widths.

Usage:

T, nontree_edges = spanning_tree_by_topo_sort(G)
pos = hierarchy_pos_dyn(T, root=root)
# transpose, because text is LTR
for k, v in pos.items():
    pos[k] = (-v[1], v[0])
nx.draw_networkx_nodes(G, pos)
nx.draw_networkx_edges(G, pos)

I greatly prefer my proposed strategy, especially for larger graphs. I find it the most visually appealing with fewest edge crossings. My solution generates the below:

proposed

In comparison, here is Joel's original:

joel orig

burubum's equidistant:

equi

Jagerber48's multipartite_layout:

multipartite

I target networkx-only solutions in my comparison.

My proposed layout still has room for improvement. It may be further condensed (for example, see how the extra space around "English" is unnecessary.) However, it is a cautious estimate: the layout produced is very readable. It may assign more space than needed, but it should guarantee that each node gets enough space, with relatively few edge crossings.

Code:

import networkx as nx
def spanning_tree_by_topo_sort(G: nx.DiGraph) -> tuple[nx.DiGraph, list[tuple]]:
    """
    Construct spanning tree (T) and non-tree edges from a DAG G
    by performing a topological sort. When adding edges, always connect
    a child to its youngest parent (the one with the greatest depth).

    Returns:
        T: A directed tree (DiGraph) containing the spanning tree edges
        nontree_edges: List of tuples (u, v, edge_data)
    """
    T = nx.DiGraph()
    nontree_edges = []
    depth = {}
    
    for node in nx.topological_sort(G):
        parents = list(G.predecessors(node))
        
        if not parents:
            # This is a root node
            T.add_node(node, depth=0)
            depth[node] = 0
        else:
            # parents MUST already be in T because of topo sort
            # Choose the deepest parent
            deepest_parent = max(parents, key=lambda p: depth[p])
            node_depth = depth[deepest_parent] + 1
            
            T.add_node(node, depth=node_depth)
            depth[node] = node_depth
            T.add_edge(deepest_parent, node, **G[deepest_parent][node])
            
            # All other edges from parents are non-tree edges that "strictly increase" depth
            for parent in parents:
                if parent != deepest_parent:
                    nontree_edges.append((parent, node, G[parent][node]))
    
    return T, nontree_edges


def hierarchy_pos_dyn(G, root=None, width=1., vert_gap = 0.2, vert_loc = 0, xcenter = 0.5, leaf_width=10):
    """
    Modified from Joel's answer at https://stackoverflow.com/a/29597209. 
    Licensed under CC BY-SA 4.0

    Assigns each node a width based on their subtree size:
    1. For leaf node: leaf_width
    2. For internal node: sum of widths of children

    Nodes are positioned proportionally to their width rather than evenly spaced.

    :param G: the graph (must be a tree)
    :param root: the root node of current branch
    :param width: horizontal space allocated for this branch - avoids overlap with other branches
    :param vert_gap: gap between levels of hierarchy
    :param vert_loc: vertical location of root
    :param xcenter: horizontal location of root
    :param leaf_width: width allocated to each leaf node (default 0.1)
    """
    if not nx.is_tree(G):
        raise TypeError('cannot use hierarchy_pos on a graph that is not a tree')

    if root is None:
        if isinstance(G, nx.DiGraph):
            root = next(iter(nx.topological_sort(G)))  #allows back compatibility with nx version 1.11
        else:
            root = random.choice(list(G.nodes))

    # First pass: calculate width for each node
    def _calculate_widths(G, node, parent=None):
        """
        Calculate the width of each node's subtree.
        Leaf nodes get leaf_width, internal nodes get sum of children widths.
        Returns dict mapping node -> width
        """
        children = list(G.neighbors(node))
        if not isinstance(G, nx.DiGraph) and parent is not None:
            children.remove(parent)
        
        if len(children) == 0:
            # Leaf node
            return {node: leaf_width}
        else:
            # Internal node: recursively calculate children widths
            widths = {}
            total_width = 0
            for child in children:
                child_widths = _calculate_widths(G, child, parent=node)
                widths.update(child_widths)
                total_width += child_widths[child]
            widths[node] = total_width
            return widths
    
    node_widths = _calculate_widths(G, root)

    def _hierarchy_pos(G, root, width=1., vert_gap = 0.2, vert_loc = 0, xcenter = 0.5, pos = None, parent = None):
        """
        see hierarchy_pos docstring for most arguments

        pos: a dict saying where all nodes go if they have been assigned
        parent: parent of this branch. - only affects it if non-directed
        """
        if pos is None:
            pos = {root:(xcenter,vert_loc)}
        else:
            pos[root] = (xcenter, vert_loc)
        children = list(G.neighbors(root))
        if not isinstance(G, nx.DiGraph) and parent is not None:
            children.remove(parent)  
        if len(children)!=0:
            # Calculate total width of all children
            total_child_width = sum(node_widths[child] for child in children)
            
            # Position children proportionally to their widths
            nextx = xcenter - width/2
            for child in children:
                # Width allocated to this child proportional to its subtree width
                child_proportion = node_widths[child] / total_child_width
                child_width = width * child_proportion
                # Center of this child's allocated space
                child_center = nextx + child_width/2
                
                pos = _hierarchy_pos(G, child, width = child_width, vert_gap = vert_gap, 
                                    vert_loc = vert_loc-vert_gap, xcenter=child_center,
                                    pos=pos, parent = root)
                nextx += child_width
        return pos

            
    return _hierarchy_pos(G, root, width, vert_gap, vert_loc, xcenter)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.