4

If you know how to use Matplotlib, you are probably able to answer without knowing about NetworkX.

I have 2 networks to draw using NetworkX, and I'd like to draw side by side in a single graphic, showing the axes of each. Essentially, it's a matter of creating 2 subplots in Matplotlib (which is the library that NetworkX uses to draw graphs).

The positions of each node of the networks are distributed in an area of [0, area_size], but usually there is no point with coordinates x = 0.0 or y = area_size. That is, points appear inside an area of [0, area_size] or smaller. Not bigger.

The proportions of each subgraph should be 0.8 x 1.0, separated by an area with proportions of 0.16 x 1.0.

In essence, it should look like this (assuming area_size = 100).

graphs with proportions

Then I have to draw lines between the 2 plots, so I need some way to get back to the positions in each graph, in order to connect them.

Node positions are generated, stored and assigned like this

# generate and store node positions
positions = {}
for node_id in G.nodes():
    pos_x = # generate pos_x in [0.0, area_size]
    pos_y = # generate pos_y in [0.0, area_size]

    positions[node_id]['x'] = pos_x
    positions[node_id]['y'] = pos_y

    G.node[node_id]['x'] = pos_x
    G.node[node_id]['y'] = pos_y

These positions are then stored in a dictionary pos = {node_id: (x, y), ...}. NetworkX gets this structure to draw nodes in the correct positions nx.draw_network(G, pos=positions).

Right now, I do the following:

  • calculate the positions of the first network on the [0, area_size], then stretch them to [0, area_size*0.8]
  • calculate the positions of the second network in the same way
  • shift the positions of the second network to the right, summing area_size*0.8 + area_size*0.16 to the x coordinates
  • set the figure size (in inches) plt.figure(figsize=(h, w), dpi=100)
  • set the x axis plt.xlim(0.0, area_size*0.8*2 + area_size*0.16)
  • set the y axis plt.ylim(0.0, area_size)
  • draw the first network (passing its positions)
  • draw the second network (passing its positions)
  • draw lines between the two networks

The plot I get has the right proportions, but the axis does not display the right information. Should I hide the axis, and draw dummy ones?

I probably need some better procedure to draw proper subplots.

I also noticed that the figure size I set is not always respected when I export to pdf.

The function NetworkX uses to draw a graph are these, in particular, I'm using draw_networkx.

I don't really mind drawing the separate axes, if they give too much trouble.

Agostino
  • 2,723
  • 9
  • 48
  • 65
  • Pass in the axes object you want to plot to as the `ax` kwarg to `draw_networkx`. – tacaswell Mar 18 '15 at 19:07
  • I'm quite new to matplotlib. I can act directly on the main plot to modify the axis, but I don't understand how I could use `ax` to draw the 2 networks as I described. Plus there are the lines between the 2 nets. – Agostino Mar 18 '15 at 21:44

1 Answers1

5

I find it takes a bit of tweaking to get Matplotlib to produce precise proportions. Usually it involves doing some simple calculations to figure out the ratios that you want (e.g. you want the height of each subplot to be 1.25 its width, by your measurements).

As for the PDF not respecting the figure size, it might be because you are specifying the DPI. There's no need to specify the DPI, since PDFs are saved in vector format. Or maybe you are using the plt.tight_layout function, which can be useful, but which alters the figure size.

Drawing lines between subplots is a bit of a nightmare. First, you have to convert from the axis coordinate system to the figure coordinate system, using transformations. Then you can draw the lines directly onto the figure. This post may help.

The code below should help. You will need to tweak it in a variety of ways, but hopefully it will help you produce the figure you want.

import networkx as nx
import matplotlib.pyplot as plt
import matplotlib.lines as lines

# Generate some random graphs and positions for demonstrative purposes.
G1 = nx.gnp_random_graph(10, 0.2)
G2 = nx.gnp_random_graph(10, 0.2)
pos1 = nx.spring_layout(G1)
pos2 = nx.spring_layout(G2)

# Set up the figure with two subplots and a figure size of 6.5 by 4 inches.
fig, ax = plt.subplots(1, 2, figsize=(6.5, 4))

# Set the x and y limits for each axis, leaving a 0.2 margin te ensure
# that nodes near the edges of the graph are not clipped.
ax[0].set_xlim([-0.2, 1.2])
ax[0].set_ylim([-0.2, 1.2])
ax[1].set_xlim([-0.2, 1.2])
ax[1].set_ylim([-0.2, 1.2])

# Stretch the subplots up, so that each unit in the y direction appears 1.25
# times taller than the width of each unit in the x direction.
ax[0].set_aspect(1.25)
ax[1].set_aspect(1.25)

# Remove the tick labels from the axes.
ax[0].xaxis.set_visible(False)
ax[0].yaxis.set_visible(False)
ax[1].xaxis.set_visible(False)
ax[1].yaxis.set_visible(False)

# Set the space between the subplots to be 0.2 times their width.
# Also reduce the margins of the figure.
plt.subplots_adjust(wspace=0.2, left=0.05, right=0.95, bottom=0.05, top=0.95)

# Draw the networks in each subplot
nx.draw_networkx(G1, pos1, ax=ax[0], node_color='r')
nx.draw_networkx(G2, pos2, ax=ax[1], node_color='b')

# Now suppose we want to draw a line between nodes 5 in each subplot. First, we need to
# be able to convert from the axes coordinates to the figure coordinates, like so.
# (Read the matplotlib transformations documentation for more detail).
def ax_to_fig(coordinates, axis):
    transFig = fig.transFigure.inverted()
    return transFig.transform((axis.transData.transform((coordinates))))

# Now we can get the figure coordinates for the nodes we want to connect.
line_start = ax_to_fig(pos1[5], ax[0])
line_end = ax_to_fig(pos2[5], ax[1])

# Create the line and draw it on the figure.
line = lines.Line2D((line_start[0], line_end[0]), (line_start[1], line_end[1]), transform=fig.transFigure)
fig.lines = [line]

# Save the figure.
plt.savefig('test_networks.pdf', format='pdf')

EDIT

The above code doesn't appear to draw the lines between the axes exactly between the centers of the corresponding nodes. Removing the ax.set_aspect functions fixes this, but now the proportions are wrong. To accommodate for this, you can manually change the y-positions of the nodes (which NetworkX makes far harder than it should be). Next, you will have to change the ax.set_ymin values to get the correct proportions, as follows:

# Manually rescale the positions of the nodes in the y-direction only.
for node in pos1:
    pos1[node][1] *= 1.35
for node in pos2:
    pos2[node][1] *= 1.35

# Set the x and y limits for each axis, leaving a 0.2 margin te ensure
# that nodes near the edges of the graph are not clipped.
ax[0].set_xlim([-0.2, 1.2])
ax[0].set_ylim([-0.2, 1.55])
ax[1].set_xlim([-0.2, 1.2])
ax[1].set_ylim([-0.2, 1.55])
Community
  • 1
  • 1
McMath
  • 6,862
  • 2
  • 28
  • 33
  • Thanks. The figures look nice, but the lines between them so not hit the exact center of the nodes. Commenting the lines `ax[...].set_aspect(...)` I see the centers get connected perfectly, while changing the aspect to `0.8` makes it worse. Perhaps there a bug? – Agostino Mar 19 '15 at 15:18
  • I've edited the post to address the problem. The solution is a bit fiddly. There may be an easier way of going about things, but at least it works. – McMath Mar 19 '15 at 18:39
  • Thanks. You probably meant `1.35`, or more generally, `y_scale`. I would also make a `margin` variable to hold the `0.2`. However, this solution does not allow to label the axes correctly. Maybe there's a way to go around this and draw fake axis? I guess drawing 2 empty subplots with the correct axes, without clearing the graph, could work. – Agostino Mar 19 '15 at 19:36
  • I think I'm a bit unclear on what you are trying to do. Maybe you could edit your question to include your code. Otherwise, I think it would be helpful to know a few things: 1) Do you need to display tick labels? 2) Are you manually generating the node positions or are you letting NetworkX do it for you? and 3) Do the positions of the nodes matter, or are they arbitrary? – McMath Mar 19 '15 at 20:10
  • Updated question. Thanks for your interest. Axis ticks would be nice to have. – Agostino Mar 19 '15 at 20:46