Basic Graph Drawing

By Matthew Henderson in graph-theory

May 2, 2014

Drawing the Petersen Graph with NetworkX

Graph drawing in NetworkX is primarily handled by the draw function. Layouts are generated through functions like circular_layout and spring_layout, providing coordinate mappings that integrate with the draw function, enabling the rendering of visually appealing graphs.

Each layout function also has a convenience function counterpart. For instance, the circular layout method can be easily accessed using the draw_circular function, while the spring layout method is readily available through the draw_spring function. These functions provide a user-friendly approach to utilize specific layout options for graph visualization.

In this section, we will showcase the integration of the draw function with various layout functions, demonstrating how to effectively utilize them for graph visualization.

As with all work involving NetworkX, begin by importing the package.

import networkx as nx

NetworkX makes many named graphs available. For example, the Petersen graph can be generated by the petersen_graph function.

G = nx.petersen_graph()

To visualize the Petersen graph with a circular layout, you can employ the draw function by providing it with vertex coordinates generated from the circular_layout function. This layout function takes the graph as its parameter and outputs the coordinates of the vertices, arranging them in a circular pattern. By passing these coordinates to the draw function, the Petersen graph will be rendered in a circular layout, resulting in a visually appealing representation.

nx.draw(G, nx.circular_layout(G))
A drawing of the Petersen graph with a circular layout.

Figure 1: The Petersen graph drawn with a circular layout.

Instead of using the draw function in conjunction with the circular_layout function we can use the draw_circular convenience function.

The draw function and all of the draw_ functions can be configured to produce graph drawings with different visual properties. Keyword parameters can be specified to customise things like node colours, edge widths and whether the graph is displayed with labels or not.

nx.draw_circular(G, with_labels=False, node_color='black')
A drawing of the Petersen graph with a circular layout with black nodes.

Figure 2: The Petersen graph drawn with a circular layout and black nodes.

To enhance reusability across multiple graphs, consolidating all configuration options into a dictionary and subsequently passing this dictionary as a parameter to the relevant layout function proves advantageous. This approach streamlines the process of managing and applying configuration settings for various graphs efficiently.

options = {
 'with_labels': False,
 'node_color': 'black',
 'node_size': 200,
 'width': 3,
}

nx.draw_circular(G, **options)
A drawing of the Petersen graph with a circular layout with small black nodes and thick edges.

Figure 3: The Petersen graph drawn with a circular layout and some style options.

This drawing is not easily recognised as a drawing of the Petersen graph. A more familiar drawing shows one of the five cycles in the shape of a regular pentagon with another five cycle as a star in the interior joined to the outer cycle by five spokes. To achieve this layout in NetworkX use the shell layout algorithm.

The draw_shell function takes an optional nlist keyword argument. The elements of the list are lists of vertices. The vertices will be arranged in concentric shells according to the elements of these lists. Figuring out which vertices lie in the outer shell and which are in the inner shell, along with their respective orderings, requires a little experimentation.

nx.draw_shell(G, nlist=[range(5, 10), range(5)], **options)
A drawing of the Petersen graph with a layout consisting of two concentric shells of nodes.

Figure 4: The Petersen graph drawn with a shell layout.

NetworkX has the following layout algorithms.

  • Circular
  • Shell
  • Spring
  • Spectral
  • Random
  • Graphviz (through pygraphviz)

We have seen already examples of the circular and shell layouts. The next figure reproduces the shell and circular drawings of the Petersen graph we have already seen along with examples of spring and spectral layouts.

plt.subplot(221)
nx.draw_circular(G, **options)

plt.subplot(222)
nx.draw_shell(G, nlist=[range(5, 10), range(5)], **options)

plt.subplot(223)
nx.draw_spectral(G, **options)

plt.subplot(224)
nx.draw_spring(G, **options)
Four drawings of the Petersen graph with different layouts.

Figure 5: The Petersen graph drawn with four different layouts.

The plt.subplot command takes an integer argument whose first two digits are interpreted as a number of rows and columns in a rectangular grid layout. The third argument is interpreted as the position of the current figure in the grid. Grid numbering starts in the top-left corner and proceeds right and down to the bottom-right corner.

Drawing Graphs with More Nodes

The options we have chosen so far have been suitable for graphs with a few nodes. When drawing larger graphs it makes sense to choose smaller node sizes and thinner edges so that the various elements of the graph can be easily distinguished.

options_1 = {
 'with_labels': False,
 'node_color': 'black',
 'node_size': 50,
 'width': 1,
}

As with the earlier drawing of the Petersen graph the nodes for different layers of the drawing of the dodecahedral graph below were found through experimentation.

G = nx.dodecahedral_graph()
nx.draw_shell(G, nlist = [[2,3,4,5,6],[8,1,0,19,18,17,16,15,14,7],[9,10,11,12,13]], **options_1)
The dodecahedral graph drawn with nodes in three concentric shells of 5, 10 and 5 nodes.

Figure 6: The dodecahedral graph drawn with a shell layout.

Drawing Graphs with Many Nodes

If a graph has many nodes then there is a risk, in particular with circular or shell layouts, of node boundaries overlapping. Choosing small node sizes is a natural remedy. When choosing very small nodes edges must also be made very thin so that node-edge intersections can be seen. As thin edges appear grey and it makes sense choose grey nodes to match.

options_2 = {
 'with_labels': False,
 'node_color': 'grey',
 'node_size': 10,
 'linewidths': 0,
 'width': 0.1,
}
G = nx.barabasi_albert_graph(100, 3)
nx.draw_circular(G, **options_2)
A random graph on 100 nodes drawn with a circular layout.

Figure 7: A random graph on 100 nodes drawn with a circular layout.

Drawing Graphs with Many Edges

Drawing graphs with many edges requires further tinkering with layout options. When there are many edges it can be difficult to distinguish different edges and see which vertices are the ends of a particular edge. Increasing transparency by lowering alpha may have some benefit.

options_3 = {
 'with_labels': False,
 'node_color': 'grey',
 'node_size': 10,
 'linewidths': 0,
 'width': 0.1,
 'alpha': 0.3
}

G = nx.complete_bipartite_graph(25, 26)
nx.draw_shell(G, nlist=[range(0, 25), range(25, 51)], **options_3)
A drawing of a bipartite graph with nodes in two concentric shells.

Figure 8: A shell layout drawing of a bipartite graph on 51 vertices.

Admittedly, this drawing conveys little information about the graph. It is hard even to see which nodes in the inner shell are connected to nodes in the outer shell or if there are any connections between nodes in the inner shell. About the only information we can interpret is that nodes in the outer shell appear not to be joined to other vertices in the outer shell. But it serves as an example of the kind of issues that can arise when drawing graphs with many edges.

Importing Layouts from Gephi

Naturally, some of the problems encountered above are due to poorly chosen layouts. Better layouts can reduce problems of identifying nodes and edges. Even better layouts can help to highlight structural properties and symmetries of graphs.

G = nx.random_lobster(100, 0.9, 0.9)
nx.draw_spring(G, iterations=10000, **options_2)
A drawing of a random lobster graph on 100 vertices.

Figure 9: A random lobster graph on 100 vertices drawn with a spring layout.

In a earlier post we talked about how to use Gephi to find nice layouts of large lobster graphs. As an alternative to using NetworkX’s layout algorithms we can export our graph, use Gephi to find a suitable layout and then import the graph data back (now augmented with coordinate information) and use the raw drawing ability of NetworkX to render the graph with this layout.

G = nx.read_graphml('lobster.graphml')
pos = dict([(v,(G.node[v]['x'], G.node[v]['y'])) for v in G.nodes()])

NetworkX loads the graph as a directed graph and will draw directed edges with arrows unless we set the arrows keyword argument to False.

nx.draw(G, pos, arrows=False, **options_2)
A drawing of a random lobster graph on 100 vertices.

Figure 10: A random lobster graph on 100 vertices drawn with a custom layout created in Gephi.

This drawing is an improvement on the previous one. The chosen layout almost succeeds in conveying both the planarity and lobsterity of the graph. With a little manual adjustment we could probably eliminate all edge-crossings. Other nice properties of this layout are that there are only a few different edge lengths and nodes are evenly distributed over a symmetrical area.

Code

https://github.com/MHenderson/graph-drawing

Posted on:
May 2, 2014
Length:
6 minute read, 1274 words
Categories:
graph-theory
Tags:
graph-drawing python networkx
See Also:
Greedy Edge Colouring of Small Graphs
More on Moore Graphs
Chromatic Polynomials