如何在 Python 中的 Matplotlib 线上绘制外边缘的轮廓?
How to plot the outline of the outer edges on a Matplotlib line in Python?
我正在尝试在 networkx
边上绘制轮廓 (linestyle=":"
)。我似乎无法弄清楚如何对 matplotlib
patch
对象执行此操作? 现在有人知道如何操作这些 patch
对象来绘制这些 "edges" 的轮廓吗? 如果这不可能,有人知道如何获取线数据到单独使用 ax.plot(x,y,linestyle=":")
来做到这一点?
import networkx as nx
import numpy as np
from collections import *
# Graph data
G = {'y1': OrderedDict([('y2', OrderedDict([('weight', 0.8688325076457851)])), (1, OrderedDict([('weight', 0.13116749235421485)]))]), 'y2': OrderedDict([('y3', OrderedDict([('weight', 0.29660515972204304)])), ('y4', OrderedDict([('weight', 0.703394840277957)]))]), 'y3': OrderedDict([(4, OrderedDict([('weight', 0.2858185316736193)])), ('y5', OrderedDict([('weight', 0.7141814683263807)]))]), 4: OrderedDict(), 'input': OrderedDict([('y1', OrderedDict([('weight', 1.0)]))]), 'y4': OrderedDict([(3, OrderedDict([('weight', 0.27847763084646443)])), (5, OrderedDict([('weight', 0.7215223691535356)]))]), 3: OrderedDict(), 5: OrderedDict(), 'y5': OrderedDict([(6, OrderedDict([('weight', 0.5733512797415756)])), (2, OrderedDict([('weight', 0.4266487202584244)]))]), 6: OrderedDict(), 1: OrderedDict(), 2: OrderedDict()}
G = nx.from_dict_of_dicts(G)
G_scaffold = {'input': OrderedDict([('y1', OrderedDict())]), 'y1': OrderedDict([('y2', OrderedDict()), (1, OrderedDict())]), 'y2': OrderedDict([('y3', OrderedDict()), ('y4', OrderedDict())]), 1: OrderedDict(), 'y3': OrderedDict([(4, OrderedDict()), ('y5', OrderedDict())]), 'y4': OrderedDict([(3, OrderedDict()), (5, OrderedDict())]), 4: OrderedDict(), 'y5': OrderedDict([(6, OrderedDict()), (2, OrderedDict())]), 3: OrderedDict(), 5: OrderedDict(), 6: OrderedDict(), 2: OrderedDict()}
G_scaffold = nx.from_dict_of_dicts(G_scaffold)
G_sem = {'y1': OrderedDict([('y2', OrderedDict([('weight', 0.046032370518141796)])), (1, OrderedDict([('weight', 0.046032370518141796)]))]), 'y2': OrderedDict([('y3', OrderedDict([('weight', 0.08764771571290508)])), ('y4', OrderedDict([('weight', 0.08764771571290508)]))]), 'y3': OrderedDict([(4, OrderedDict([('weight', 0.06045928834718992)])), ('y5', OrderedDict([('weight', 0.06045928834718992)]))]), 4: OrderedDict(), 'input': OrderedDict([('y1', OrderedDict([('weight', 0.0)]))]), 'y4': OrderedDict([(3, OrderedDict([('weight', 0.12254141747735424)])), (5, OrderedDict([('weight', 0.12254141747735425)]))]), 3: OrderedDict(), 5: OrderedDict(), 'y5': OrderedDict([(6, OrderedDict([('weight', 0.11700701511079069)])), (2, OrderedDict([('weight', 0.11700701511079069)]))]), 6: OrderedDict(), 1: OrderedDict(), 2: OrderedDict()}
G_sem = nx.from_dict_of_dicts(G_sem)
# Edge info
edge_input = ('input', 'y1')
weights_sem = np.array([G_sem[u][v]['weight']for u,v in G_sem.edges()]) * 256
# Layout
pos = nx.nx_agraph.graphviz_layout(G_scaffold, prog="dot", root="input")
# Plotting graph
pad = 10
with plt.style.context("ggplot"):
fig, ax = plt.subplots(figsize=(8,8))
linecollection = nx.draw_networkx_edges(G_sem, pos, alpha=0.5, edges=G_sem.edges(), arrowstyle="-", edge_color="#000000", width=weights_sem)
x = np.stack(pos.values())[:,0]
y = np.stack(pos.values())[:,1]
ax.set(xlim=(x.min()-pad,x.max()+pad), ylim=(y.min()-pad, y.max()+pad))
for path, lw in zip(linecollection.get_paths(), linecollection.get_linewidths()):
x = path.vertices[:,0]
y = path.vertices[:,1]
w = lw/4
theta = np.arctan2(y[-1] - y[0], x[-1] - x[0])
# ax.plot(x, y, color="blue", linestyle=":")
ax.plot((x-np.sin(theta)*w), y+np.cos(theta)*w, color="blue", linestyle=":")
ax.plot((x+np.sin(theta)*w), y-np.cos(theta)*w, color="blue", linestyle=":")
经过几次思想实验,我意识到我需要计算角度然后相应地调整垫子:
例如,如果线是完全垂直的(在 90 或 -90),则 y 坐标根本不会移动,而 x 坐标会移动。角度为 0 或 180 的线会发生相反的情况。
不过,还是有点偏。
我怀疑这是相关的:
matplotlib - Expand the line with specified width in data unit?
我不认为 linewidth
直接转换为数据 space
或者,如果这些线集合可以转换为矩形对象,那么它也是可能的。
LineCollection中的物体没有明显的边色和面色。通过尝试设置线型,您会影响整个线段的样式。我发现使用一系列补丁更容易创建所需的效果。每个补丁代表图形的一条边。面片的边色、线型、线宽和面色可以单独操作。诀窍是构建一个函数,将边转换为旋转的矩形面片。
import matplotlib.path as mpath
import matplotlib.patches as mpatches
import numpy as np
from matplotlib import pyplot as plt
import networkx as nx
G = nx.Graph()
for i in range(10):
G.add_node(i)
for i in range(9):
G.add_edge(9, i)
# make a square figure so the rectangles look nice
plt.figure(figsize=(10,10))
plt.xlim(-1.1, 1.1)
plt.ylim(-1.1, 1.1)
def create_patch(startx, starty, stopx, stopy, width, w=.1):
# Check if lower right corner is specified.
direction = 1
if startx > stopx:
direction = -1
length = np.sqrt((stopy-starty)**2 + (stopx-startx)**2)
theta = np.arctan((stopy-starty)/(stopx-startx))
complement = np.pi/2 - theta
patch = mpatches.Rectangle(
(startx+np.cos(complement)*width/2, starty-np.sin(complement)*width/2),
direction * length,
width,
angle=180/np.pi*theta,
facecolor='#000000',
linestyle=':',
linewidth=width*10,
edgecolor='k',
alpha=.3
)
return patch
# Create layout before building edge patches
pos = nx.circular_layout(G)
for i, edge in enumerate(G.edges()):
startx, starty = pos[edge[0]]
stopx, stopy = pos[edge[1]]
plt.gca().add_patch(create_patch(startx, starty, stopx, stopy, (i+1)/10))
plt.show()
在您的示例中,您注意到我们可以使用边的 X 和 Y 位置来找到旋转角度。我们在这里使用相同的技巧。另请注意,有时矩形长度的大小为负。 Rectangle Patch 假定 x 和 y 输入指的是矩形的左下角。我们 运行 快速检查以确保这是真的。如果为假,我们首先指定顶部。在这种情况下,我们沿着相同的角度向后绘制矩形。
另一个问题:在创建补丁之前 运行 您的布局算法很重要。指定 pos
后,我们可以使用边查找开始和停止位置。
改进机会:您可以使用 PatchCollection 并批量操作补丁,而不是在生成补丁时绘制每个补丁。文档声称 PatchCollection 更快,但它可能不适合所有用例。由于您表示希望独立设置每个补丁的属性,因此该集合可能无法提供您需要的灵活性。
一定宽度的线被另一条线包围的问题是线是用数据坐标定义的,而线宽是物理单位,即点。这通常是可取的,因为它允许线宽独立于数据范围、缩放级别等。它还确保线的末端始终垂直于线,与轴方向无关。
因此线条的轮廓始终处于混合坐标系中,在使用渲染器绘制实际线条之前并不确定最终的外观。因此,对于考虑(可能变化的)坐标的解决方案,需要确定图形当前状态的轮廓。
一种选择是使用新艺术家,它将现有 LineCollection
作为输入,并根据线条在像素 space 中的当前位置创建新的变换。
在下面我选择了一个PatchCollection
。从一个矩形开始,我们可以缩放和旋转它,然后将它平移到原始直线的位置。
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection, PatchCollection
import matplotlib.transforms as mtrans
class OutlineCollection(PatchCollection):
def __init__(self, linecollection, ax=None, **kwargs):
self.ax = ax or plt.gca()
self.lc = linecollection
assert np.all(np.array(self.lc.get_segments()).shape[1:] == np.array((2,2)))
rect = plt.Rectangle((-.5, -.5), width=1, height=1)
super().__init__((rect,), **kwargs)
self.set_transform(mtrans.IdentityTransform())
self.set_offsets(np.zeros((len(self.lc.get_segments()),2)))
self.ax.add_collection(self)
def draw(self, renderer):
segs = self.lc.get_segments()
n = len(segs)
factor = 72/self.ax.figure.dpi
lws = self.lc.get_linewidth()
if len(lws) <= 1:
lws = lws*np.ones(n)
transforms = []
for i, (lw, seg) in enumerate(zip(lws, segs)):
X = self.lc.get_transform().transform(seg)
mean = X.mean(axis=0)
angle = np.arctan2(*np.squeeze(np.diff(X, axis=0))[::-1])
length = np.sqrt(np.sum(np.diff(X, axis=0)**2))
trans = mtrans.Affine2D().scale(length,lw/factor).rotate(angle).translate(*mean)
transforms.append(trans.get_matrix())
self._transforms = transforms
super().draw(renderer)
请注意如何仅在 draw
时计算实际变换。这确保他们考虑像素 space 中的实际位置。
用法可能类似于
verts = np.array([[[5,10],[5,5]], [[5,5],[8,2]], [[5,5],[1,4]], [[1,4],[2,0]]])
plt.rcParams["axes.xmargin"] = 0.1
fig, (ax1, ax2) = plt.subplots(ncols=2, sharex=True, sharey=True)
lc1 = LineCollection(verts, color="k", alpha=0.5, linewidth=20)
ax1.add_collection(lc1)
olc1 = OutlineCollection(lc1, ax=ax1, linewidth=2,
linestyle=":", edgecolor="black", facecolor="none")
lc2 = LineCollection(verts, color="k", alpha=0.3, linewidth=(10,20,40,15))
ax2.add_collection(lc2)
olc2 = OutlineCollection(lc2, ax=ax2, linewidth=3,
linestyle="--", edgecolors=["r", "b", "gold", "indigo"],
facecolor="none")
for ax in (ax1,ax2):
ax.autoscale()
plt.show()
现在的想法当然是使用问题中的 linecollection
对象而不是上面的 lc1
对象。这应该很容易在代码中替换。
我正在尝试在 networkx
边上绘制轮廓 (linestyle=":"
)。我似乎无法弄清楚如何对 matplotlib
patch
对象执行此操作? 现在有人知道如何操作这些 patch
对象来绘制这些 "edges" 的轮廓吗? 如果这不可能,有人知道如何获取线数据到单独使用 ax.plot(x,y,linestyle=":")
来做到这一点?
import networkx as nx
import numpy as np
from collections import *
# Graph data
G = {'y1': OrderedDict([('y2', OrderedDict([('weight', 0.8688325076457851)])), (1, OrderedDict([('weight', 0.13116749235421485)]))]), 'y2': OrderedDict([('y3', OrderedDict([('weight', 0.29660515972204304)])), ('y4', OrderedDict([('weight', 0.703394840277957)]))]), 'y3': OrderedDict([(4, OrderedDict([('weight', 0.2858185316736193)])), ('y5', OrderedDict([('weight', 0.7141814683263807)]))]), 4: OrderedDict(), 'input': OrderedDict([('y1', OrderedDict([('weight', 1.0)]))]), 'y4': OrderedDict([(3, OrderedDict([('weight', 0.27847763084646443)])), (5, OrderedDict([('weight', 0.7215223691535356)]))]), 3: OrderedDict(), 5: OrderedDict(), 'y5': OrderedDict([(6, OrderedDict([('weight', 0.5733512797415756)])), (2, OrderedDict([('weight', 0.4266487202584244)]))]), 6: OrderedDict(), 1: OrderedDict(), 2: OrderedDict()}
G = nx.from_dict_of_dicts(G)
G_scaffold = {'input': OrderedDict([('y1', OrderedDict())]), 'y1': OrderedDict([('y2', OrderedDict()), (1, OrderedDict())]), 'y2': OrderedDict([('y3', OrderedDict()), ('y4', OrderedDict())]), 1: OrderedDict(), 'y3': OrderedDict([(4, OrderedDict()), ('y5', OrderedDict())]), 'y4': OrderedDict([(3, OrderedDict()), (5, OrderedDict())]), 4: OrderedDict(), 'y5': OrderedDict([(6, OrderedDict()), (2, OrderedDict())]), 3: OrderedDict(), 5: OrderedDict(), 6: OrderedDict(), 2: OrderedDict()}
G_scaffold = nx.from_dict_of_dicts(G_scaffold)
G_sem = {'y1': OrderedDict([('y2', OrderedDict([('weight', 0.046032370518141796)])), (1, OrderedDict([('weight', 0.046032370518141796)]))]), 'y2': OrderedDict([('y3', OrderedDict([('weight', 0.08764771571290508)])), ('y4', OrderedDict([('weight', 0.08764771571290508)]))]), 'y3': OrderedDict([(4, OrderedDict([('weight', 0.06045928834718992)])), ('y5', OrderedDict([('weight', 0.06045928834718992)]))]), 4: OrderedDict(), 'input': OrderedDict([('y1', OrderedDict([('weight', 0.0)]))]), 'y4': OrderedDict([(3, OrderedDict([('weight', 0.12254141747735424)])), (5, OrderedDict([('weight', 0.12254141747735425)]))]), 3: OrderedDict(), 5: OrderedDict(), 'y5': OrderedDict([(6, OrderedDict([('weight', 0.11700701511079069)])), (2, OrderedDict([('weight', 0.11700701511079069)]))]), 6: OrderedDict(), 1: OrderedDict(), 2: OrderedDict()}
G_sem = nx.from_dict_of_dicts(G_sem)
# Edge info
edge_input = ('input', 'y1')
weights_sem = np.array([G_sem[u][v]['weight']for u,v in G_sem.edges()]) * 256
# Layout
pos = nx.nx_agraph.graphviz_layout(G_scaffold, prog="dot", root="input")
# Plotting graph
pad = 10
with plt.style.context("ggplot"):
fig, ax = plt.subplots(figsize=(8,8))
linecollection = nx.draw_networkx_edges(G_sem, pos, alpha=0.5, edges=G_sem.edges(), arrowstyle="-", edge_color="#000000", width=weights_sem)
x = np.stack(pos.values())[:,0]
y = np.stack(pos.values())[:,1]
ax.set(xlim=(x.min()-pad,x.max()+pad), ylim=(y.min()-pad, y.max()+pad))
for path, lw in zip(linecollection.get_paths(), linecollection.get_linewidths()):
x = path.vertices[:,0]
y = path.vertices[:,1]
w = lw/4
theta = np.arctan2(y[-1] - y[0], x[-1] - x[0])
# ax.plot(x, y, color="blue", linestyle=":")
ax.plot((x-np.sin(theta)*w), y+np.cos(theta)*w, color="blue", linestyle=":")
ax.plot((x+np.sin(theta)*w), y-np.cos(theta)*w, color="blue", linestyle=":")
经过几次思想实验,我意识到我需要计算角度然后相应地调整垫子:
例如,如果线是完全垂直的(在 90 或 -90),则 y 坐标根本不会移动,而 x 坐标会移动。角度为 0 或 180 的线会发生相反的情况。
不过,还是有点偏。
我怀疑这是相关的: matplotlib - Expand the line with specified width in data unit?
我不认为 linewidth
直接转换为数据 space
或者,如果这些线集合可以转换为矩形对象,那么它也是可能的。
LineCollection中的物体没有明显的边色和面色。通过尝试设置线型,您会影响整个线段的样式。我发现使用一系列补丁更容易创建所需的效果。每个补丁代表图形的一条边。面片的边色、线型、线宽和面色可以单独操作。诀窍是构建一个函数,将边转换为旋转的矩形面片。
import matplotlib.path as mpath
import matplotlib.patches as mpatches
import numpy as np
from matplotlib import pyplot as plt
import networkx as nx
G = nx.Graph()
for i in range(10):
G.add_node(i)
for i in range(9):
G.add_edge(9, i)
# make a square figure so the rectangles look nice
plt.figure(figsize=(10,10))
plt.xlim(-1.1, 1.1)
plt.ylim(-1.1, 1.1)
def create_patch(startx, starty, stopx, stopy, width, w=.1):
# Check if lower right corner is specified.
direction = 1
if startx > stopx:
direction = -1
length = np.sqrt((stopy-starty)**2 + (stopx-startx)**2)
theta = np.arctan((stopy-starty)/(stopx-startx))
complement = np.pi/2 - theta
patch = mpatches.Rectangle(
(startx+np.cos(complement)*width/2, starty-np.sin(complement)*width/2),
direction * length,
width,
angle=180/np.pi*theta,
facecolor='#000000',
linestyle=':',
linewidth=width*10,
edgecolor='k',
alpha=.3
)
return patch
# Create layout before building edge patches
pos = nx.circular_layout(G)
for i, edge in enumerate(G.edges()):
startx, starty = pos[edge[0]]
stopx, stopy = pos[edge[1]]
plt.gca().add_patch(create_patch(startx, starty, stopx, stopy, (i+1)/10))
plt.show()
在您的示例中,您注意到我们可以使用边的 X 和 Y 位置来找到旋转角度。我们在这里使用相同的技巧。另请注意,有时矩形长度的大小为负。 Rectangle Patch 假定 x 和 y 输入指的是矩形的左下角。我们 运行 快速检查以确保这是真的。如果为假,我们首先指定顶部。在这种情况下,我们沿着相同的角度向后绘制矩形。
另一个问题:在创建补丁之前 运行 您的布局算法很重要。指定 pos
后,我们可以使用边查找开始和停止位置。
改进机会:您可以使用 PatchCollection 并批量操作补丁,而不是在生成补丁时绘制每个补丁。文档声称 PatchCollection 更快,但它可能不适合所有用例。由于您表示希望独立设置每个补丁的属性,因此该集合可能无法提供您需要的灵活性。
一定宽度的线被另一条线包围的问题是线是用数据坐标定义的,而线宽是物理单位,即点。这通常是可取的,因为它允许线宽独立于数据范围、缩放级别等。它还确保线的末端始终垂直于线,与轴方向无关。
因此线条的轮廓始终处于混合坐标系中,在使用渲染器绘制实际线条之前并不确定最终的外观。因此,对于考虑(可能变化的)坐标的解决方案,需要确定图形当前状态的轮廓。
一种选择是使用新艺术家,它将现有 LineCollection
作为输入,并根据线条在像素 space 中的当前位置创建新的变换。
在下面我选择了一个PatchCollection
。从一个矩形开始,我们可以缩放和旋转它,然后将它平移到原始直线的位置。
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection, PatchCollection
import matplotlib.transforms as mtrans
class OutlineCollection(PatchCollection):
def __init__(self, linecollection, ax=None, **kwargs):
self.ax = ax or plt.gca()
self.lc = linecollection
assert np.all(np.array(self.lc.get_segments()).shape[1:] == np.array((2,2)))
rect = plt.Rectangle((-.5, -.5), width=1, height=1)
super().__init__((rect,), **kwargs)
self.set_transform(mtrans.IdentityTransform())
self.set_offsets(np.zeros((len(self.lc.get_segments()),2)))
self.ax.add_collection(self)
def draw(self, renderer):
segs = self.lc.get_segments()
n = len(segs)
factor = 72/self.ax.figure.dpi
lws = self.lc.get_linewidth()
if len(lws) <= 1:
lws = lws*np.ones(n)
transforms = []
for i, (lw, seg) in enumerate(zip(lws, segs)):
X = self.lc.get_transform().transform(seg)
mean = X.mean(axis=0)
angle = np.arctan2(*np.squeeze(np.diff(X, axis=0))[::-1])
length = np.sqrt(np.sum(np.diff(X, axis=0)**2))
trans = mtrans.Affine2D().scale(length,lw/factor).rotate(angle).translate(*mean)
transforms.append(trans.get_matrix())
self._transforms = transforms
super().draw(renderer)
请注意如何仅在 draw
时计算实际变换。这确保他们考虑像素 space 中的实际位置。
用法可能类似于
verts = np.array([[[5,10],[5,5]], [[5,5],[8,2]], [[5,5],[1,4]], [[1,4],[2,0]]])
plt.rcParams["axes.xmargin"] = 0.1
fig, (ax1, ax2) = plt.subplots(ncols=2, sharex=True, sharey=True)
lc1 = LineCollection(verts, color="k", alpha=0.5, linewidth=20)
ax1.add_collection(lc1)
olc1 = OutlineCollection(lc1, ax=ax1, linewidth=2,
linestyle=":", edgecolor="black", facecolor="none")
lc2 = LineCollection(verts, color="k", alpha=0.3, linewidth=(10,20,40,15))
ax2.add_collection(lc2)
olc2 = OutlineCollection(lc2, ax=ax2, linewidth=3,
linestyle="--", edgecolors=["r", "b", "gold", "indigo"],
facecolor="none")
for ax in (ax1,ax2):
ax.autoscale()
plt.show()
现在的想法当然是使用问题中的 linecollection
对象而不是上面的 lc1
对象。这应该很容易在代码中替换。