Plotting and Animating NetworkX graphs
Modifying neworkX graph using Matplotlib
Let's start with plotting a simple graph using nx.draw
:
import networkx as nx
G = nx.Graph()
G.add_nodes_from([1, 2, 3, 4, 5, 6, 7, 8, 9])
G.add_edges_from([(1,2), (3,4), (2,5), (4,5), (6,7), (8,9), (4,7), (1,7), (3,5), (2,7), (5,8), (2,9), (5,7)])
nx.draw(G)
In my case I get something like this:
{% img http://s19.postimg.org/5zl0vjk0j/figure_1.png %}
You might get something different because networkX internally uses
matplotlib.scatter
which randomly generates the position of the nodes.
Now let's dive deeper into how does networkX internally draws graphs. Taking a quick look of all the children of the plot.
fig = plt.gcf()
axes = plt.gca()
axes.get_children()
Out[14]:
[<matplotlib.axis.XAxis at 0x1bed4d0>,
<matplotlib.axis.YAxis at 0x30457d0>,
<matplotlib.text.Text at 0x30613d0>,
<matplotlib.text.Text at 0x3061310>,
<matplotlib.text.Text at 0x3061550>,
<matplotlib.text.Text at 0x30615d0>,
<matplotlib.text.Text at 0x3061650>,
<matplotlib.text.Text at 0x30616d0>,
<matplotlib.text.Text at 0x3061750>,
<matplotlib.text.Text at 0x30617d0>,
<matplotlib.text.Text at 0x3061890>,
<matplotlib.collections.PathCollection at 0x305d4d0>,
<matplotlib.collections.LineCollection at 0x305dc50>,
<matplotlib.text.Text at 0x30514d0>,
<matplotlib.patches.Rectangle at 0x3051550>,
<matplotlib.spines.Spine at 0x303ec90>,
<matplotlib.spines.Spine at 0x303e990>,
<matplotlib.spines.Spine at 0x303eb10>,
<matplotlib.spines.Spine at 0x303e7d0>]
Here we see there are 9 matplotlib.text.Text
objects which are the labels
on the nodes. Let's see some of the properties of one of the text objects.
text_object = axes.get_children()[2]
text_object.get_text()
Out[13]: '1'
text_object.get_size()
Out[14]: 12.0
text_object.get_color()
Out[15]: 'k'
For the complete list of properties we can call the properties
method.
Example: text_object.properties()
.
Next object we see is the matplotlib.collections.PathCollection
object which
is the collection object of all the nodes. Now again we can see some properties
and modify them.
nodes = axes.get_children()[11] # Selecting the PathCollection object
In [25]: nodes.get_pickradius()
Out[25]: 5.0
In [26]: nodes.get_label()
Out[26]: '_collection0'
In [27]: nodes.get_facecolor()
Out[27]: array([[ 1., 0., 0., 1.]])
In [28]: nodes.get_edgecolors()
Out[28]: array([[ 0., 0., 0., 1.]])
In [29]: nodes.get_linewidth()
Out[29]: (1.0,)
We can change the properties:
nodes.set_linewidth(2)
nodes.set_pickradius(8)
plt.draw()
Now with the offsets
property we can change the position of the nodes.
In [32]: offsets = nodes.get_offsets()
In [33]: offsets
Out[33]:
array([[ 1. , 0.31732594],
[ 0.60075321, 0.28045667],
[ 0.04382142, 0.98124087],
[ 0.38063134, 0.99245427],
[ 0.27586167, 0.61542286],
[ 0.9961299 , 0.91637658],
[ 0.70916575, 0.66473314],
[ 0. , 0.23580501],
[ 0.28996686, 0. ]])
In [35]: offsets[0][0] = 0.5
In [37]: offsets[0][1] = 0.5
In [38]: plt.draw()
Now we get this output:
{% img http://s19.postimg.org/s01dc62oj/figure_2.png %}
We see here that only the node has moved and the label and the edges are
at their original position. We will now move the edges to the new node
position.
NetworkX uses matplotlib.scatter
to draw nodes which creates a
collections.PathCollection
object and then draws the edges
which is a matplotlib.collections.LineCollection
object.
We can modify many properties of the lines. But for now let's just move the two edges to the new position of their node.
edges_object = axes.get_children()[12] #selecting the LineCollection object
edges_object.get_paths()
Out[34]:
[Path([[ 0.17693707 0. ]
[ 0.29792 0.44577014]], None),
Path([[ 0.17693707 0. ]
[ 0.43349969 0.12392229]], None),
Path([[ 0.29792 0.44577014]
[ 0.30217028 0.87528811]], None),
Path([[ 0.29792 0.44577014]
[ 0.70125136 0.42163912]], None),
Path([[ 0.29792 0.44577014]
[ 0.43349969 0.12392229]], None),
Path([[ 1. 0.3245961 ]
[ 0.8526191 0.04511872]], None),
Path([[ 1. 0.3245961 ]
[ 0.70125136 0.42163912]], None),
Path([[ 0.8526191 0.04511872]
[ 0.70125136 0.42163912]], None),
Path([[ 0.8526191 0.04511872]
[ 0.43349969 0.12392229]], None),
Path([[ 0.70125136 0.42163912]
[ 0.65698815 0.85722426]], None),
Path([[ 0.70125136 0.42163912]
[ 0.43349969 0.12392229]], None),
Path([[ 0. 0.24864231]
[ 0.43349969 0.12392229]], None),
Path([[ 0.65698815 0.85722426]
[ 0.30217028 0.87528811]], None)]
We see the 13 edges. The initial position of the moved node was [0.176... 0. ]
and we see here that the first and second paths have the starting point at
that position. So, we need to move the starting point of those two edges
to [0.5 0.5 ]
.
first_path = edges_object.get_paths()[0]
first_path.vertices
Out[41]:
array([[ 0.17693707, 0. ],
[ 0.29792 , 0.44577014]])
first_path.vertices[0][0] = 0.5
first_path.vertices[0][1] = 0.5
second_path = edges_object.get_paths()[1]
second_path.vertices
Out[48]:
array([[ 0.17693707, 0. ],
[ 0.43349969, 0.12392229]])
second_path.vertices[0][0] = 0.5
second_path.vertices[0][1] = 0.5
plt.draw()
We can now see the graph to be something like:
Now let's move the label to this position:
label_object = axes.get_children()[2]
label_object.get_text()
Out[54]: '1'
label_object.get_position()
Out[55]: (0.17693707256025257, 0.0)
label_object.set_position((0.5, 0.5))
plt.draw()
We can now see the graph to be like:
{% img http://s19.postimg.org/se7r8vt3n/figure_4.png %}
So, finally we have moved the node to a new position. For having better
control over the properties I have rewritten the nx_pyplot
module which
adds many functions to manipulate the graph. You can check out the
github repo.
Basic Animation
Let's start with a very simple color changing animation in which we will draw a graph whose nodes will change color. Here in each iteration we are drawing a new graph over the previous ones with different node colors. This is a very bad approach but let's just start with this. I will write about better ways to do it in the next post.
I will be using networkX
for drawing the graphs and matplotlib
for
animation.
import networkx as nx
import matplotlib.animation as animation
import matplotlib.pyplot as plt
import random
# Graph initialization
G = nx.Graph()
G.add_nodes_from([1, 2, 3, 4, 5, 6, 7, 8, 9])
G.add_edges_from([(1,2), (3,4), (2,5), (4,5), (6,7), (8,9), (4,7), (1,7), (3,5), (2,7), (5,8), (2,9), (5,7)])
# Animation funciton
define animate(i):
colors = ['r', 'b', 'g', 'y', 'w', 'm']
nx.draw_circular(G, node_color=[random.choice(colors) for j in range(9)]
nx.draw_circular(G)
fig = plt.gcf()
# Animator call
anim = animation.FuncAnimation(fig, animate, frames=20, interval=20, blit=True)
Let's step through and see what's happening. In the first four lines we are
importing networkX
, matplotlib.animation
, matplotlib.pyplot
and random
modules.
In the next few lines we create a graph using networkX:
G = nx.Graph()
G.add_nodes_from([1, 2, 3, 4, 5, 6, 7, 8, 9])
G.add_edges_from([(1,2), (3,4), (2,5), (4,5), (6,7), (8,9), (4,7), (1,7), (3,5), (2,7), (5,8), (2,9), (5,7)])
First we initialize an empty graph G
. Then we add 9 nodes and 13 edges to it.
This next piece is the animation function which takes a single parameter i
which is the frame number of the animation.
def animate(i):
colors = ['r', 'b', 'g', 'y', 'w', 'm']
nx.draw_circular(G, node_color=[random.choice(colors) for j in range(9)]
Here the colors
list is a list of colors from which we will be randomly
picking up colors for our nodes.
nx.draw_circular
draws the graph keeping the nodes in a circular pattern.
anim = animation.FuncAnimation(fig, animate, frames=20, interval=20, blit=True)
animation.FuncAnimation
repeatedly calls the animate fucntion incrementing
i
in each iteration.
frames
define the number of times animate function is called,
interval
is the inverval between each call.
blit=True
defines to draw only those parts which have changed.