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