如何在Python中使用networkx和matplotlib在同一图中绘制不同形状的节点和bbox的面色?

How to plot nodes of different shapes and facecolor of bbox in the same graph using networkx and matplotlib in Python?

我想使用 networkx 绘制一个简单的图形。有两个带有标签 Python 和 Programming 的节点。代码和图表如下:

import networkx as nx

G = nx.DiGraph()
G.add_node(0)
G.add_node(1)
G.add_edge(0,1)

nx.draw_networkx(G,
                pos = {0:(0, 0), 1: (1, 0)},
                node_size = [1000, 2000],
                node_color = ["red","green"],
                node_shape = "s",
                )

我可以在两个节点中分别得到红色和绿色两种不同的颜色,并在每个节点中设置不同的标签。但是,我也想获得两种不同形状的节点。我想要原样的第一个节点和菱形的第二个节点,可以使用 node_shape 中的 d 获得。我尝试以 ["s","d"] 的形式传递列表,并在 node_shape.

中以 {0:"s", 1:"d"} 的形式传递字典

但是,两者return都出错了。我怎样才能在这里得到不同形状的节点?是否可以使用任何其他库,如 graphviz,如何?

在另一步中,我希望将标签包含在节点中。所以我将 node_size 设置为 0 并使用 bbox,如下所示。

nx.draw_networkx(G,
                pos = {0:(0, 0), 1: (1, 0)},
                node_size = 0,
                 arrows = True,
                 labels = {0:"Python",1:"Programming"},
                 bbox = dict(facecolor = "skyblue")
                )

我可以将标签包含在 bbox 中,但是箭头没有隐藏,并且两个节点中 bbox 的面色相同。在这种情况下,如何使面部颜色不同?解决这个问题的替代方法是什么?

Networkx 不支持在一个函数调用中使用多个节点形状,因为节点是使用 matplotlib 的 scatter 绘制的,不支持在同一个函数调用中绘制不同的标记。但是,您可以多次调用 nx.draw_networkx_nodes,每个调用具有不同的节点和形状子集。

在节点中包含标签是一件棘手的事情。 matplotlib 中的文本框尺寸不能为 pre-calculated,因为它支持多个渲染器。因此,您必须绘制一些虚拟文本对象,确定文本框的尺寸,然后逆向调整为所需的尺寸。 networkx 不包含此类功能。

如果您愿意使用其他库,那么您所需的大部分功能都可以在 netgraph 中轻松实现,这是我编写和维护的库。 支持在一个函数调用中绘制不同的节点形状。箭头应保持可见。更改节点标签背景颜色相当容易。

关于字体大小,在netgraph中,如果没有明确设置节点标签的字体大小,它会计算出最大字体大小,使所有文本框都适合相应的节点艺术家。正如您在下面的示例中看到的那样,该计算存在一个小问题,因为当前的实现假设一个圆形节点形状,因此第一个节点的文本框不太适合方形节点。我会尽快解决这个问题;与此同时,您将不得不使用更多的圆形节点形状,或者在单独的调用中调整字体大小,如下所示。

#!/usr/bin/env python
import numpy as np
import matplotlib.pyplot as plt
import networkx as nx

from netgraph import Graph # pip install netgraph

graph_data = nx.DiGraph([(0, 1)]) # edge lists or igraph Graph objects are also supported

g = Graph(graph_data,
          node_layout = {0: (0, 0), 1: (1, 0)},
          node_size = {0 : 10, 1 : 20},
          node_color = {0 : "red", 1 : "green"},
          node_labels = {0:"Python", 1:"Programming"},
          node_label_fontdict = {'backgroundcolor' : 'lightgray'},
          node_shape = {0 : "s", 1 : "d"},
          arrows=True,
)

# Netgraph currently does not support multiple values for label backgroundcolors.
# However, all artists are exposed in simple to query dictionaries.
# As node label artists are matplotlib text objects,
# we can vary the node label background colors by using matplotlib.text.Text methods:
g.node_label_artists[0].set_backgroundcolor('salmon')
g.node_label_artists[1].set_backgroundcolor('lightgreen')

# Netgraph assumes circular node shapes when computing fontsizes.
# We hence have to manually adjust the node label fontsizes
# by the ratio of the diagonal to the width in a square.
for node, label in g.node_label_artists.items():
    fontsize = label.get_fontsize()
    label.set_fontsize(fontsize * 1./np.sqrt(2))

plt.show()

我找到了一种使用 graphviz 包的方法。 NetworkX 包的局限性在于它不允许节点具有不同的形状,尽管可以以列表的形式设置不同的大小或颜色。另外,如果使用边界框(bbox),那么它的属性在节点之间是统一的。

但是,graphviz 在这种情况下提供了更多的灵活性。每个节点的形状、大小和颜色都可以单独调整。 我是这样做的:

import graphviz

G = graphviz.Digraph()

G.node("Python", style = 'filled', color = "red", shape = "box")
G.node("Programming", style = "filled", color = "green", shape = "ellipse")

G.edge("Python","Programming")

G.view()

结果,我得到了如图所示的情节: 也可以进一步自定义图形,例如节点的位置、边的颜色等

我正在测试 Python 库,这些库允许以更直观和用户友好的方式绘制图表。我考虑了标记节点的正确连接、不同样式的标签、可以用鼠标拖动的元素以及其他类型的交互性。 netgraph 在大多数这些功能中看起来很有前途,但不幸的是它不支持矩形节点和边缘末端位置的动态偏移,这对于能够以所需方式显示有向边缘至关重要。事实上,节点和边在 netgraph 中是 matplotlib path objects and labels are matplotlib text objects,这意味着即使您重写它的任何方法,矩形文本标签和文本包围的正确连接也会变得复杂。

另一方面,如果您使用 matplotlib annotations 连接节点标签,这些问题可以在 networkx 中解决。在这种情况下,您只需要 networkx.Graph 和注释其节点的自定义艺术家:

import matplotlib.pyplot as plt
import networkx as nx

G = nx.DiGraph()
G.add_edges_from([(0, 1)])
G.add_nodes_from([0, 1])
pos = {0:(0.1, 0.9), 1: (0.1, 0.1)}

fig, ax = plt.subplots()

annotations = {0: ax.annotate('Python', xy=pos[0], xycoords='data', size=20,
                ha="center", va="center", bbox=dict(facecolor = "blue")),
            1:ax.annotate('programming', xy=pos[1], xycoords='data',
                ha="center", va="center", bbox=dict(facecolor = "red"))}

for A, B in G.edges:
    ax.annotate("", xy=pos[B], xycoords='data', xytext=pos[A], textcoords='data',
                          arrowprops=dict(arrowstyle="->", color="0.5",  # shrinkA=85, shrinkB=85,
                                          patchA=annotations[A],
                                          patchB=annotations[B],
                                          connectionstyle='arc3'))

plt.axis('off')
plt.show()

另一个灵活性

此外,matplotlib 允许更多花哨的节点标记样式:

import matplotlib.pyplot as plt
from matplotlib.offsetbox import AnnotationBbox, TextArea, VPacker
import networkx as nx


G = nx.DiGraph()
G.add_edges_from([(0,1), (1,2), (0, 2)])
G.add_nodes_from([0, 1, 2])
pos = {0:(0.1, 0.9), 1: (0.1, 0.1), 2: (0.9, 0.5)}

fig, ax = plt.subplots()

textobjs = [TextArea('IS', textprops=dict(color="blue", size=18, ha='left',va='baseline')),
            TextArea('FUN!', textprops=dict(color="red", size=18, ha='left',va='baseline'))]
ybox = VPacker(children=textobjs, pad=0, sep=0)
anbox = AnnotationBbox(ybox, pos[2], frameon=True, box_alignment=(0,.5),
                       bboxprops=dict(facecolor='yellow', boxstyle='round'))

annotations = {0: ax.annotate('Python', xy=pos[0], xycoords='data',
                ha="center", va="center", bbox=dict(facecolor = "blue")),
            1:ax.annotate('programming', xy=pos[1], xycoords='data',
                ha="center", va="center", bbox=dict(facecolor = "red")),
            2:ax.add_artist(anbox)}

annotations[2].draggable()

for A, B in G.edges:
    ax.annotate("", xy=pos[B], xycoords='data', xytext=pos[A], textcoords='data',
                          arrowprops=dict(arrowstyle="->", color="0.5",  # shrinkA=85, shrinkB=85,
                                          patchA=annotations[A],
                                          patchB=annotations[B],
                                          connectionstyle='arc3'))

plt.axis('off')
plt.show()