1

I can draw a directed network graph using Matplotlib. Now I want to be able to respond to mouse events so that a user can interact with the network. For example, a node could change its colour when the user clicks on it. This is just a noddy example but it illustrates the point. I'd also like to know which node (label) has been clicked on; I'm not really interested in its x,y coordinates in space.

Here's my code so far:

import matplotlib.pyplot as plt
from matplotlib.collections import PathCollection
import networkx as nx

def createDiGraph():
    G = nx.DiGraph()

    # Add nodes:
    nodes = ['A', 'B', 'C', 'D', 'E']
    G.add_nodes_from(nodes)

    # Add edges or links between the nodes:
    edges = [('A','B'), ('B','C'), ('B', 'D'), ('D', 'E')]
    G.add_edges_from(edges)
    return G

G = createDiGraph()

# Get a layout for the nodes according to some algorithm.
pos = nx.layout.spring_layout(G, random_state=779)

node_size = 300
nodes = nx.draw_networkx_nodes(G, pos, node_size=node_size, node_color=(0,0,0.9),
                                edgecolors='black')
# nodes is a matplotlib.collections.PathCollection object
nodes.set_picker(5)
#nodes.pick('button_press_event')
#print(nodes.get_offsets())

nx.draw_networkx_edges(G, pos, node_size=node_size, arrowstyle='->',
                                arrowsize=15, edge_color='black', width=1)

nx.draw_networkx_labels(G, pos, font_color='red', font_family='arial',
                                font_size=10)

def onpick(event):
    #print(event.mouseevent)

    if isinstance(event.artist, PathCollection):
        #nodes = event.artist
        print (event.ind)

fig = plt.gcf()

# Bind our onpick() function to pick events:
fig.canvas.mpl_connect('pick_event', onpick)

# Hide the axes:
plt.gca().set_axis_off()
plt.show()

The network looks like this when plotted:

enter image description here

If I click with my mouse on node C for example, the program prints out [1]; or [3] for node E. Notice the index doesn't correspond to 0 for A, 1 for B, 2 for C, and so on, even though the original nodes were added to the networkx digraph in that order.

So how do I get the value 'C', when I click on node C? And how do I get hold of the object representing C in the figure so I can change its colour?

I've tried playing around with PathCollection.pick but I'm not sure what to pass into it and I'm not sure that's the right method to use anyway.

snark
  • 2,462
  • 3
  • 32
  • 63

2 Answers2

2

I eventually arrived at this solution after helpful comments from @apogalacticon.

import matplotlib.pyplot as plt
from matplotlib.collections import PathCollection
import networkx as nx

def createDiGraph():
    G = nx.DiGraph()

    # Add nodes:
    nodes = ['A', 'B', 'C', 'D', 'E']
    G.add_nodes_from(nodes)

    # Add edges or links between the nodes:
    edges = [('A','B'), ('B','C'), ('B', 'D'), ('D', 'E')]
    G.add_edges_from(edges)
    return G

G = createDiGraph()

# Get a layout for the nodes according to some algorithm.
pos = nx.layout.spring_layout(G, random_state=779)

node_size = 300
nodes = nx.draw_networkx_nodes(G, pos, node_size=node_size, node_color=(0,0,0.9),
                                edgecolors='black')
# nodes is a matplotlib.collections.PathCollection object
nodes.set_picker(5)

nx.draw_networkx_edges(G, pos, node_size=node_size, arrowstyle='->',
                                arrowsize=15, edge_color='black', width=1)

nx.draw_networkx_labels(G, pos, font_color='red', font_family='arial',
                                font_size=10)

def onpick(event):

    if isinstance(event.artist, PathCollection):
        all_nodes = event.artist
        ind = event.ind[0] # event.ind is a single element array.
        this_node_name = pos.keys()[ind]
        print(this_node_name)

        # Set the colours for all the nodes, highlighting the picked node with
        # a different colour:
        colors = [(0, 0, 0.9)] * len(pos)
        colors[ind]=(0, 0.9, 0)
        all_nodes.set_facecolors(colors)

        # Update the plot to show the change:
        fig.canvas.draw() # plt.draw() also works.
        #fig.canvas.flush_events() # Not required? See https://stackoverflow.com/a/4098938/1843329

fig = plt.gcf()

# Bind our onpick() function to pick events:
fig.canvas.mpl_connect('pick_event', onpick)

# Hide the axes:
plt.gca().set_axis_off()
plt.show()

When you click on a node its face colour changes to green whilst the other nodes are set to blue:

enter image description here

Unfortunately it seems the nodes in the network are represented by a single matplotlib.artist.Artist object, which happens to be a collection of paths without any artist children. This means you can't get hold of a single node as such to alter its properties. Instead you're forced to update all the nodes, just making sure the properties for the picked node - colour in this case - are different to the others.

snark
  • 2,462
  • 3
  • 32
  • 63
1

It looks like G does not retain the order of your nodes when it is created, however, the order of the nodes appears to be stored in pos. I would recommend the following:

Add to your import statement:

from ast import literal_eval

Define label in nx.draw_networkx_nodes and create the plot within a function update_plot which takes G, pos, and color as arguments:

def update_plot(pos, G, colors):
    # the keys of the pos dictionary contains the labels that you are interested in
    # or label = [*pos]
    nodes = nx.draw_networkx_nodes(G, pos, node_size=node_size, node_color=colors,edgecolors='black', label=list(pos.keys()))
    # nodes is a matplotlib.collections.PathCollection object
    nodes.set_picker(5)
    nx.draw_networkx_edges(G, pos, node_size=node_size, arrowstyle='->', arrowsize=15, edge_color='black', width=1)
    nx.draw_networkx_labels(G, pos, font_color='red', font_family='arial', font_size=10)

Your onpick function should then be:

def onpick(event):
    if isinstance(event.artist, PathCollection):
        #index of event
        ind = event.ind[0]
        #convert the label list from a string back to a list
        label_list = literal_eval(event.artist.get_label())
        print(label_list[ind])
        colors = [(0, 0, 0.9)] * len(pos)
        colors[ind]=(0.9,0,0)
        update_plot(pos, G, colors)

The body of your code is then simply:

G = createDiGraph()
# Get a layout for the nodes according to some algorithm.
pos = nx.layout.spring_layout(G, random_state=779)

node_size = 300
colors=[(0,0,0.9)]*len(pos)
update_plot(pos, G, colors)
apogalacticon
  • 709
  • 1
  • 9
  • 19
  • Thanks, that's helpful because that's a good spot that `pos` holds the correct order. However your line with `draw_networkx_nodes` generates a syntax error at the `*` of `label=[*pos]`. The `label` argument is for the legend anyway, which I don't want. However now that I know `pos` has what I want I might be able to get the corresponding label now. I also still need to get hold of the graphical object representing the node that's been clicked on to change its colour. I'll play around some more... – snark Apr 20 '18 at 10:27
  • Yes, if all I do compared to my original code is change `print (event.ind)` inside `onpick()` to `print(pos.keys()[event.ind[0]])` I can make it print out the correct label when I click on each node. So that solves half the problem... – snark Apr 20 '18 at 10:40
  • Still struggling to see how I can get hold of a single node in the network to change its colour. It seems the PathCollection just sees the network as a collection of paths rather than as a bunch of child artist objects. `nodes_get_children()` returns an empty list! Is it not possible to get hold of an individual node so I can modify its properties? I can only think to modify the colourmap next. Or I could go back to square one and draw the network in a different way so that it is a collection of artist objects. – snark Apr 20 '18 at 13:00
  • The use of the `label` argument can be used to create legends, but is also used to pass the label of each node to the `onpick` function, which is a bit cleaner than accessing `pos` from within the function itself. I'm not sure why `label=[*pos]` produces a syntax error for you. You could also try `label=list(pos.keys())` instead. I didn't notice the second part of your question. I will edit my answer shortly to address this. – apogalacticon Apr 20 '18 at 15:57
  • Thanks! I'll look at this again next week hopefully; unfortunately something more important has come up in the meantime... – snark Apr 21 '18 at 10:28
  • Many thanks for updating your answer but I'm afraid I still couldn't get it to work. The colours wouldn't update and each time I clicked on a node I would trigger 1, 2, 4, 8,... pick events. See my posted answer... – snark Apr 24 '18 at 14:43