Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Plotting networkx.Graph: how to change node position instead of resetting every node?

I'm working on a project where I need to create a preview of nx.Graph() which allows to change position of nodes dragging them with a mouse. My current code is able to redraw whole figure immediately after each motion of mouse if it's clicked on specific node. However, this increases latency significantly. How can I update only artists needed, it is, clicked node, its label text and adjacent edges instead of refreshing every artist of plt.subplots()? Can I at least get a reference to all the artists that need to be relocated?

I started from a standard way of displaying a graph in networkx:

import networkx as nx
import matplotlib.pyplot as plt
import numpy as np
import scipy.spatial

def refresh(G):
    plt.axis((-4, 4, -1, 3))
    nx.draw_networkx_labels(G, pos = nx.get_node_attributes(G, 'pos'),
                                bbox = dict(fc="lightgreen", ec="black", boxstyle="square", lw=3))
    nx.draw_networkx_edges(G, pos = nx.get_node_attributes(G, 'pos'), width=1.0, alpha=0.5)
    plt.show()

nodes = np.array(['A', 'B', 'C', 'D', 'E', 'F', 'G'])
edges = np.array([['A', 'B'], ['A', 'C'], ['B', 'D'], ['B', 'E'], ['C', 'F'], ['C', 'G']])
pos = np.array([[0, 0], [-2, 1], [2, 1], [-3, 2], [-1, 2], [1, 2], [3, 2]])

G = nx.Graph()
# IG = InteractiveGraph(G) #>>>>> add this line in the next step
G.add_nodes_from(nodes)
G.add_edges_from(edges)
nx.set_node_attributes(G, dict(zip(G.nodes(), pos.astype(float))), 'pos')

fig, ax = plt.subplots()
# fig.canvas.mpl_connect('button_press_event', lambda event: IG.on_press(event))
# fig.canvas.mpl_connect('motion_notify_event', lambda event: IG.on_motion(event))
# fig.canvas.mpl_connect('button_release_event', lambda event: IG.on_release(event))
refresh(G) # >>>>> replace it with IG.refresh() in the next step

enter image description here

In the next step I changed 5 line of previous script (4 is uncommented and 1 replaced) plus used InteractiveGraph instance to make it interactive:

class InteractiveGraph:
    def __init__(self, G, node_pressed=None, xydata=None):
        self.G = G
        self.node_pressed = node_pressed
        self.xydata = xydata

    def refresh(self, show=True):
        plt.clf()
        nx.draw_networkx_labels(self.G, pos = nx.get_node_attributes(self.G, 'pos'),
                                bbox = dict(fc="lightgreen", ec="black", boxstyle="square", lw=3))
        nx.draw_networkx_edges(self.G, pos = nx.get_node_attributes(self.G, 'pos'), width=1.0, alpha=0.5)
        plt.axis('off')
        plt.axis((-4, 4, -1, 3))
        fig.patch.set_facecolor('white')
        if show:
            plt.show()

    def on_press(self, event):
        if event.inaxes is not None and len(self.G.nodes()) > 0:
            nodelist, coords = zip(*nx.get_node_attributes(self.G, 'pos').items())
            kdtree = scipy.spatial.KDTree(coords)
            self.xydata = np.array([event.xdata, event.ydata])
            close_idx = kdtree.query_ball_point(self.xydata, np.sqrt(0.1))
            i = close_idx[0]
            self.node_pressed = nodelist[i]

    def on_motion(self, event):
        if event.inaxes is not None and self.node_pressed:
            new_xydata = np.array([event.xdata, event.ydata])
            self.xydata += new_xydata - self.xydata
            #print(d_xy, self.G.nodes[self.node_pressed])
            self.G.nodes[self.node_pressed]['pos'] = self.xydata
            self.refresh(show=False)
            event.canvas.draw()

    def on_release(self, event):
        self.node_pressed = None

enter image description here

Related sources:

  • Event handling
  • Optimized removal of closest node
like image 310
mathfux Avatar asked Sep 10 '20 21:09

mathfux


People also ask

How to view nodes and edges in a NetworkX graph?

You can view the nodes and edges in a Networkx Graph using the attributes midsummer.nodes and midsummer.edges. We then need to get the positions for the nodes in the graph. There are a few different layouts to choose from. I thought the spring layout looked the best.

How to plot a NetworkX graph in Python using matplotlib?

In this article, we will be discussing how to plot a graph generated by NetworkX in Python using Matplotlib. NetworkX is not a graph visualizing package but basic drawing with Matplotlib is included in the software package. Step 1 : Import networkx and matplotlib.pyplot in the project file. Step 2 : Generate a graph using networkx.

How to add numbering in the node in NetworkX drawing?

Below is the Python code: To add numbering in the node add one argument with_labels=True in draw () function. Different graph types and plotting can be done using networkx drawing and matplotlib. Note** : Here keywords is referred to optional keywords that we can mention use to format the graph plotting. Some of the general graph layouts are :

Can NetworkX be used for graph visualization?

NetworkX provides basic functionality for visualizing graphs, but its main goal is to enable graph analysis rather than perform graph visualization. In the future, graph visualization functionality may be removed from NetworkX or only available as an add-on package.


1 Answers

To expand on my comment above, in netgraph, your example can be reproduced with

import numpy as np
import matplotlib.pyplot as plt; plt.ion()
import networkx as nx
import netgraph

nodes = np.array(['A', 'B', 'C', 'D', 'E', 'F', 'G'])
edges = np.array([['A', 'B'], ['A', 'C'], ['B', 'D'], ['B', 'E'], ['C', 'F'], ['C', 'G']])
pos = np.array([[0, 0], [-2, 1], [2, 1], [-3, 2], [-1, 2], [1, 2], [3, 2]])

G = nx.Graph()
G.add_nodes_from(nodes)
G.add_edges_from(edges)

I = netgraph.InteractiveGraph(G,
                              node_positions=dict(zip(nodes, pos)),
                              node_labels=dict(zip(nodes,nodes)),
                              node_label_bbox=dict(fc="lightgreen", ec="black", boxstyle="square", lw=3),
                              node_size=12,
)

# move stuff with mouse

enter image description here


Regarding the code you wrote, the kd-tree is unnecessary if you have handles of all the artists. In general, matplotlib artists have a contains method, such that when you log button press events, you can simply check artist.contains(event) to find out if the button press occurred over the artist. Of course, if you use networkx to do the plotting, you can't get the handles in a nice, query-able form (ax.get_children() is neither) so that is not possible.

like image 172
Paul Brodersen Avatar answered Sep 24 '22 20:09

Paul Brodersen