使用networkx在两个节点之间绘制多条边
Posted
技术标签:
【中文标题】使用networkx在两个节点之间绘制多条边【英文标题】:Drawing multiple edges between two nodes with networkx 【发布时间】:2014-05-12 05:23:58 【问题描述】:我需要在两个节点之间绘制一个有多个边(具有不同权重)的有向图。也就是说,我有节点 A 和 B 以及长度为 2 的边 (A,B) 和长度为 3 的边 (B,A)。
我已经尝试过使用 G=nx.Digraph 和 G=nx.Multidigraph。当我绘制它时,我只能看到一个边缘和一个标签。 有什么办法吗?
【问题讨论】:
请参阅***.com/questions/15053686/… 和***.com/questions/14943439/… 了解信息。 【参考方案1】:尝试以下方法:
import networkx as nx
import matplotlib.pyplot as plt
G = nx.DiGraph() #or G = nx.MultiDiGraph()
G.add_node('A')
G.add_node('B')
G.add_edge('A', 'B', length = 2)
G.add_edge('B', 'A', length = 3)
pos = nx.spring_layout(G)
nx.draw(G, pos)
edge_labels=dict([((u,v,),d['length'])
for u,v,d in G.edges(data=True)])
nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels, label_pos=0.3, font_size=7)
plt.show()
这将返回这个带有两条边和边上显示的长度的图表:
【讨论】:
在可视化和阅读加权图时,这可能是最大的敌人。它很丑陋,不可读,并且在有向图中 - 地狱知道哪条边是哪条。【参考方案2】:对上面回复的改进是将connectionstyle 添加到nx.draw,这样可以在图中看到两条平行线:
import networkx as nx
import matplotlib.pyplot as plt
G = nx.DiGraph() #or G = nx.MultiDiGraph()
G.add_node('A')
G.add_node('B')
G.add_edge('A', 'B', length = 2)
G.add_edge('B', 'A', length = 3)
pos = nx.spring_layout(G)
nx.draw(G, pos, with_labels=True, connectionstyle='arc3, rad = 0.1')
edge_labels=dict([((u,v,),d['length'])
for u,v,d in G.edges(data=True)])
plt.show()
【讨论】:
也许你可以在同一篇文章中查看 Francesco Sgaramella 的回答,他还在情节中添加了标签。 感谢您的回答帮助。 Francesco Sgaramella 的回答有助于显示边缘的权重,但它仅显示 A -> B 的权重,而不显示 B-> A 的权重,有什么建议如何显示两者?你解决了你的问题吗? @mdexp @nooshinha 使用此答案中的connectionstyle
参数和 Francesco 答案中的 nx.draw_networkx_edge_label
对我来说效果很好。我不得不调整label_pos
参数,因为重量在中间重叠并且只显示了一个(但实际上有两个在另一个之上)。
不幸的是我没有设法将标签放在相应拱门的顶部,但我的解决方案足以解决我的问题
@mdexp 感谢您的解释。现在我明白重量标签之间的重叠是问题而不是值。虽然你的问题已经解决了,但是如果我解决了,我会在这里分享。【参考方案3】:
您可以使用您计算的节点位置直接使用 matplotlib。
G=nx.MultiGraph ([(1,2),(1,2),(1,2),(3,1),(3,2)])
pos = nx.random_layout(G)
nx.draw_networkx_nodes(G, pos, node_color = 'r', node_size = 100, alpha = 1)
ax = plt.gca()
for e in G.edges:
ax.annotate("",
xy=pos[e[0]], xycoords='data',
xytext=pos[e[1]], textcoords='data',
arrowprops=dict(arrowstyle="->", color="0.5",
shrinkA=5, shrinkB=5,
patchA=None, patchB=None,
connectionstyle="arc3,rad=rrr".replace('rrr',str(0.3*e[2])
),
),
)
plt.axis('off')
plt.show()
【讨论】:
如何为每个箭头添加边缘标签(文本)?【参考方案4】:在AMangipinto的解决方案中添加以下代码,在两个方向上添加边缘标签(图片见链接):
edge_labels = dict([((u, v,), f'd["length"]\n\nG.edges[(v,u)]["length"]')
for u, v, d in G.edges(data=True) if pos[u][0] > pos[v][0]])
nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels, font_color='red')
“if pos[u][0] > pos[v][0]”只在一个方向上添加了一个边标签。我们将两个长度都添加到单个标签,否则我们会将第一个标签覆盖在边缘上。 注意:如果节点的 x 位置相同,标签将不会显示。
【讨论】:
这仅适用于弧的曲率非常小的情况。【参考方案5】:在两个节点之间绘制双向边的常用方法有两种:
-
将两条边绘制为直线,每条边都平行于连接节点的直线,但略有偏移。
将两条边绘制为曲线;确保它们在不同的方向上弯曲。
在这两种情况下,标签都可以简单地放置在两条线的中心。
这两种方法都不能很好地适应 networkx
绘图实用程序的当前状态:
第一种方法需要在
平行边。其他库中的常见选择包括
平均边缘宽度或节点大小的三分之一。然而,节点
networkx
中的位置以数据坐标给出,而节点
尺寸和边缘宽度在显示坐标中给出。这使得
偏移量的计算很麻烦,而且——更重要的是——
如果调整图形大小,布局会中断(作为转换
从数据坐标到显示坐标的变化)。
正如其他答案中所述,networkx
可以通过
设置正确的connectionstyle
。然而,这个功能是
最近添加到networkx,因此功能
绘制标签仍采用直边。如果只有边缘
有一个非常小的弧(即仍然基本上是直的),那么
可以通过添加将标签捏造到近似正确的位置
换行符在标签的正确位置,如
@PaulMenzies 的回答证明了这一点。然而,这种方法
如果曲率是,通常会产生次优结果并中断
高。
如果您愿意使用基于matplotlib
构建的其他绘图实用程序,
我在我的模块中实现了这两种方法
netgraph。 netgraph
是
完全兼容 networkx 和 igraph 图形对象,所以它应该
轻松快速地生成美观的图表。
#!/usr/bin/env python
import numpy as np
import matplotlib.pyplot as plt
import networkx as nx
from netgraph import Graph # pip install netgraph
triangle = nx.DiGraph([('a', 'b'), ('a', 'c'), ('b', 'a'), ('c', 'b'), ('c', 'c')])
node_positions =
'a' : np.array([0.2, 0.2]),
'b' : np.array([0.8, 0.2]),
'c' : np.array([0.5, 0.8]),
edge_labels =
('a', 'b') : 3,
('a', 'c') : 'Lorem ipsum',
('b', 'a') : 4,
('c', 'b') : 'dolor sit',
('c', 'c') : r'$\pi$'
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14,14))
Graph(triangle, node_labels=True, edge_labels=edge_labels,
edge_label_fontdict=dict(size=12, fontweight='bold'),
node_layout=node_positions, edge_layout='straight',
node_size=6, edge_width=4, arrows=True, ax=ax1)
Graph(triangle, node_labels=True, edge_labels=edge_labels,
edge_label_fontdict=dict(size=12, fontweight='bold'),
node_layout=node_positions, edge_layout='curved',
node_size=6, edge_width=4, arrows=True, ax=ax2)
plt.show()
【讨论】:
【参考方案6】:以下是获得理想结果的方法,例如:
这种预期结果的一些特点是:
只有单边时,应该是直的。 标签应完美定位在边缘中间(适合发布)。 有些边缘比其他边缘更长(因此更宽)。 当前实现适用于有向图设置
以下几行是启动示例的初始代码
import matplotlib.pyplot as plt
import networkx as nx
G = nx.DiGraph()
edge_list = [(1,2,'w':'A1'),(2,1,'w':'A2'),(2,3,'w':'B'),(3,1,'w':'C'),
(3,4,'w':'D1'),(4,3,'w':'D2'),(1,5,'w':'E1'),(5,1,'w':'E2'),
(3,5,'w':'F'),(5,4,'w':'G')]
G.add_edges_from(edge_list)
pos=nx.spring_layout(G,seed=5)
fig, ax = plt.subplots()
nx.draw_networkx_nodes(G, pos, ax=ax)
nx.draw_networkx_labels(G, pos, ax=ax)
fig.savefig("1.png", bbox_inches='tight',pad_inches=0)
结果:
绘制边缘
NetworkX 的draw_networkx_edges
函数只能使用edgelist
参数绘制边的子集。为了使用它,我们将边缘分组为两个列表并分别绘制它们。感谢AMangipinto's answer connectionstyle='arc3, rad = 0.1'
。
curved_edges = [edge for edge in G.edges() if reversed(edge) in G.edges()]
straight_edges = list(set(G.edges()) - set(curved_edges))
nx.draw_networkx_edges(G, pos, ax=ax, edgelist=straight_edges)
arc_rad = 0.25
nx.draw_networkx_edges(G, pos, ax=ax, edgelist=curved_edges, connectionstyle=f'arc3, rad = arc_rad')
fig.savefig("2.png", bbox_inches='tight',pad_inches=0)
结果:
绘制边缘标签
NetworkX 的draw_networkx_edge_labels
函数假定边缘是直的,并且没有参数可以改变它。由于 NetworkX 是开源的,我复制了该函数并创建了一个修改后的my_draw_networkx_edge_labels
。这个函数在附录中。
假设您将此函数保存到名为 my_networkx.py 的文件中,您可以将边缘标签绘制为:
import my_networkx as my_nx
edge_weights = nx.get_edge_attributes(G,'w')
curved_edge_labels = edge: edge_weights[edge] for edge in curved_edges
straight_edge_labels = edge: edge_weights[edge] for edge in straight_edges
my_nx.my_draw_networkx_edge_labels(G, pos, ax=ax, edge_labels=curved_edge_labels,rotate=False,rad = arc_rad)
nx.draw_networkx_edge_labels(G, pos, ax=ax, edge_labels=straight_edge_labels,rotate=False)
fig.savefig("3.png", bbox_inches='tight',pad_inches=0)
我们再次将曲线与直线分开。结果是这个答案中的第一个数字。
附录
NetworkX 的函数draw_networkx_edge_labels
假设直线查找标签的位置:
(x, y) = (
x1 * label_pos + x2 * (1.0 - label_pos),
y1 * label_pos + y2 * (1.0 - label_pos),
)
要找到二次贝塞尔曲线的中点,我们可以使用以下代码。首先我们根据matplotlib中的定义找到贝塞尔曲线的中间控制点(代码中的ctrl_1
):
创建曲线以使中间控制点 (C1) 位于 距起点 (C0) 和终点 (C2) 的距离相同,并且 C1 到连接 C0-C2 的线的距离是弧度乘以 C0-C2的距离。
由于这个定义,函数my_draw_networkx_edge_labels
需要一个名为rad
的额外参数。
pos_1 = ax.transData.transform(np.array(pos[n1]))
pos_2 = ax.transData.transform(np.array(pos[n2]))
linear_mid = 0.5*pos_1 + 0.5*pos_2
d_pos = pos_2 - pos_1
rotation_matrix = np.array([(0,1), (-1,0)])
ctrl_1 = linear_mid + rad*rotation_matrix@d_pos
以“ax.transData”开头的函数是必需的,因为轴域中的 90 度角不对应于显示中的 90 度。所以我们必须在显示坐标系之间转换坐标。
bezier_mid
可以用贝塞尔曲线规则计算:
ctrl_mid_1 = 0.5*pos_1 + 0.5*ctrl_1
ctrl_mid_2 = 0.5*pos_2 + 0.5*ctrl_1
bezier_mid = 0.5*ctrl_mid_1 + 0.5*ctrl_mid_2
(x, y) = ax.transData.inverted().transform(bezier_mid)
完成my_draw_networkx_edge_labels
:
def my_draw_networkx_edge_labels(
G,
pos,
edge_labels=None,
label_pos=0.5,
font_size=10,
font_color="k",
font_family="sans-serif",
font_weight="normal",
alpha=None,
bbox=None,
horizontalalignment="center",
verticalalignment="center",
ax=None,
rotate=True,
clip_on=True,
rad=0
):
"""Draw edge labels.
Parameters
----------
G : graph
A networkx graph
pos : dictionary
A dictionary with nodes as keys and positions as values.
Positions should be sequences of length 2.
edge_labels : dictionary (default=)
Edge labels in a dictionary of labels keyed by edge two-tuple.
Only labels for the keys in the dictionary are drawn.
label_pos : float (default=0.5)
Position of edge label along edge (0=head, 0.5=center, 1=tail)
font_size : int (default=10)
Font size for text labels
font_color : string (default='k' black)
Font color string
font_weight : string (default='normal')
Font weight
font_family : string (default='sans-serif')
Font family
alpha : float or None (default=None)
The text transparency
bbox : Matplotlib bbox, optional
Specify text box properties (e.g. shape, color etc.) for edge labels.
Default is boxstyle='round', ec=(1.0, 1.0, 1.0), fc=(1.0, 1.0, 1.0).
horizontalalignment : string (default='center')
Horizontal alignment 'center', 'right', 'left'
verticalalignment : string (default='center')
Vertical alignment 'center', 'top', 'bottom', 'baseline', 'center_baseline'
ax : Matplotlib Axes object, optional
Draw the graph in the specified Matplotlib axes.
rotate : bool (deafult=True)
Rotate edge labels to lie parallel to edges
clip_on : bool (default=True)
Turn on clipping of edge labels at axis boundaries
Returns
-------
dict
`dict` of labels keyed by edge
Examples
--------
>>> G = nx.dodecahedral_graph()
>>> edge_labels = nx.draw_networkx_edge_labels(G, pos=nx.spring_layout(G))
Also see the NetworkX drawing examples at
https://networkx.org/documentation/latest/auto_examples/index.html
See Also
--------
draw
draw_networkx
draw_networkx_nodes
draw_networkx_edges
draw_networkx_labels
"""
import matplotlib.pyplot as plt
import numpy as np
if ax is None:
ax = plt.gca()
if edge_labels is None:
labels = (u, v): d for u, v, d in G.edges(data=True)
else:
labels = edge_labels
text_items =
for (n1, n2), label in labels.items():
(x1, y1) = pos[n1]
(x2, y2) = pos[n2]
(x, y) = (
x1 * label_pos + x2 * (1.0 - label_pos),
y1 * label_pos + y2 * (1.0 - label_pos),
)
pos_1 = ax.transData.transform(np.array(pos[n1]))
pos_2 = ax.transData.transform(np.array(pos[n2]))
linear_mid = 0.5*pos_1 + 0.5*pos_2
d_pos = pos_2 - pos_1
rotation_matrix = np.array([(0,1), (-1,0)])
ctrl_1 = linear_mid + rad*rotation_matrix@d_pos
ctrl_mid_1 = 0.5*pos_1 + 0.5*ctrl_1
ctrl_mid_2 = 0.5*pos_2 + 0.5*ctrl_1
bezier_mid = 0.5*ctrl_mid_1 + 0.5*ctrl_mid_2
(x, y) = ax.transData.inverted().transform(bezier_mid)
if rotate:
# in degrees
angle = np.arctan2(y2 - y1, x2 - x1) / (2.0 * np.pi) * 360
# make label orientation "right-side-up"
if angle > 90:
angle -= 180
if angle < -90:
angle += 180
# transform data coordinate angle to screen coordinate angle
xy = np.array((x, y))
trans_angle = ax.transData.transform_angles(
np.array((angle,)), xy.reshape((1, 2))
)[0]
else:
trans_angle = 0.0
# use default box of white with white border
if bbox is None:
bbox = dict(boxstyle="round", ec=(1.0, 1.0, 1.0), fc=(1.0, 1.0, 1.0))
if not isinstance(label, str):
label = str(label) # this makes "1" and 1 labeled the same
t = ax.text(
x,
y,
label,
size=font_size,
color=font_color,
family=font_family,
weight=font_weight,
alpha=alpha,
horizontalalignment=horizontalalignment,
verticalalignment=verticalalignment,
rotation=trans_angle,
transform=ax.transData,
bbox=bbox,
zorder=1,
clip_on=clip_on,
)
text_items[(n1, n2)] = t
ax.tick_params(
axis="both",
which="both",
bottom=False,
left=False,
labelbottom=False,
labelleft=False,
)
return text_items
【讨论】:
以上是关于使用networkx在两个节点之间绘制多条边的主要内容,如果未能解决你的问题,请参考以下文章