绘制networkx.Graph图形:如何更改节点位置而不是重置每个节点?

7

我正在做一个项目,需要创建一个预览nx.Graph()的功能,可以通过鼠标拖拽来改变节点的位置。我的当前代码能够在鼠标每次移动时立即重新绘制整个图形,但会显著增加延迟。我应该如何仅更新所需的艺术元素,也就是点击的节点、它的标签文本和相邻边缘,而不是刷新plt.subplots()的所有艺术元素?我能否至少得到需要重新定位的所有艺术元素的引用?

我从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

在下一步中,我更改了以前脚本的5行代码(取消注释4行并替换1行),并使用了 InteractiveGraph 实例使其变得交互式:
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

在此输入图片描述

相关资源:


你需要使用matplotlib吗?如果你在支持jupyter小部件的环境中(笔记本、实验室、voila),那么你可以使用https://ipycytoscape.readthedocs.io/en/latest/,这将为你提供免费的交互性。它支持直接从networkx对象创建。 - Ianhi
通常来说,如果您能够访问底层对象,matplotlib动画效果的实现可以通过调用这些对象的set_data方法来更新特定的元素,这将大大提高性能。 - Ianhi
我认为在ipycytoscape中所有这些都应该已经可以实现了,如果还没有的话,那么肯定应该被实现。(免责声明:我对ipycytoscape做出了很大的贡献) - Ianhi
1
我认为ipycytoscape最大的缺点是你只能在笔记本中显示。 - Ianhi
好的。如果您遇到任何问题,请毫不犹豫地在 GitHub 上打开一个问题:https://github.com/QuantStack/ipycytoscape - Ianhi
显示剩余4条评论
1个回答

4

补充一下我上面的评论,在 netgraph 中,你的示例可以使用以下方法重新创建:

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


关于您编写的代码,如果您拥有所有艺术家的句柄,则kd树是不必要的。通常,matplotlib艺术家具有contains方法,因此当您记录按钮按下事件时,可以简单地检查artist.contains(event)以找出按钮按下是否发生在艺术家上。当然,如果您使用networkx进行绘图,则无法以可查询的形式获取句柄(ax.get_children()也不是),因此这是不可能的。

谢谢你的回复,Paul。这很有用。 - mathfux
@mathfux 请告诉我这对你是否有效,或者你有哪些痛点。我正在重写代码库的某些部分(主要是内部),以便更容易编写其他功能,并乐意接受任何建议。此外,如果您有兴趣编写类似的内容,我很乐意合作。 - Paul Brodersen
我正在做一个小项目,可以绘制数学证明的图表。经过5天的编写脚本,我最终得到了一个相当令人满意的版本(https://github.com/loijord/neuromap)。感谢您分享的示例,我已经测试过了。这是一个非常好的参考,可以改进我的脚本。不幸的是,我的项目目前被困在旧版本中,但希望我能找到时间在回来时用`netgraph.InteractiveGraph`替换所有的`nx.Graph`。 - mathfux

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接