嗨。我有一个圆形布局图,布局外部有 12 个节点(按设计)。
num_miR_nodes = len(miR_nodes['nodes'])
angle_increment = 2*math.pi / num_miR_nodes
miR_radius = 1.5
for i, node in enumerate(miR_nodes['nodes']):
angle = i * angle_increment
x = miR_radius * math.cos(angle)
y = miR_radius * math.sin(angle)
pos[node] = (x, y)
G.add_node(node)
我想为外部 12 个节点的每个(直)边创建一条贝塞尔路径(或类似于贝塞尔曲线),该路径将保持在圆形布局中的节点之外,直到到达圆形布局目标节点,通过螺旋(我不希望边缘跳离图形,就像当您将边缘的中点增加太多时会发生的情况一样)。
目前我只计算了内圆形布局边缘的贝塞尔曲线数学:
def draw_curved_edges2(G, pos, ax, alpha):
for u, v, d in G.edges(data=True):
edge_color = d['edge_color']
weight = d['width']
pos_u = pos[u]
pos_v = pos[v]
x_u, y_u = pos_u
x_v, y_v = pos_v
if 'miR' not in u:
# midpoint of the edge
x_mid = 0 * (x_u + x_v)
y_mid = 0 * (y_u + y_v)
# control point for Bezier
x_ctrl = 0.25 * (x_mid + 0.5 * (x_u + x_v))
y_ctrl = 0.25 * (y_mid + 0.5 * (y_u + y_v))
# Bezier curve path
bezier_path = Path([(x_u, y_u), (x_ctrl, y_ctrl), (x_v, y_v)], [Path.MOVETO, Path.CURVE3, Path.CURVE3])
width = G[u][v]['width']# for u, v in G.edges()]
#patch = PathPatch(bezier_path, facecolor='none', edgecolor=edge_color, linewidth=width, alpha=alpha)
#ax.add_patch(patch)
arrow = FancyArrowPatch(path=bezier_path, color=edge_color, linewidth=width, alpha=alpha,
arrowstyle="->, head_length=6, head_width=2, widthA=1.0, widthB=1.0, lengthA=0.4, lengthB=0.4")
ax.add_patch(arrow)
draw_curved_edges2(G, pos, ax, alpha=0.4)
此解决方案在围绕中心原点路由的两个点之间创建样条线。对于每个样条线,其内部点到原点的距离插值在样条线的起点和终点到同一原点的距离之间,从而产生螺旋状外观。
此解决方案还选择围绕原点的最短路径(而不是始终逆时针环绕)。
import numpy as np
import matplotlib.pyplot as plt
from scipy.interpolate import BSpline
def _get_unit_vector(vector):
"""Returns the unit vector of the vector."""
return vector / np.linalg.norm(vector)
def _get_interior_angle_between(v1, v2, radians=True):
"""Returns the interior angle between vectors v1 and v2.
Parameters
----------
v1, v2 : numpy.array
The vectors in question.
radians : bool, default False
If True, return the angle in radians (otherwise it is in degrees).
Returns
-------
angle : float
The interior angle between two vectors.
Examples
--------
>>> angle_between((1, 0, 0), (0, 1, 0))
1.5707963267948966
>>> angle_between((1, 0, 0), (1, 0, 0))
0.0
>>> angle_between((1, 0, 0), (-1, 0, 0))
3.141592653589793
Notes
-----
Adapted from https://stackoverflow.com/a/13849249/2912349
"""
v1_u = _get_unit_vector(v1)
v2_u = _get_unit_vector(v2)
angle = np.arccos(np.clip(np.dot(v1_u, v2_u), -1.0, 1.0))
if radians:
return angle
else:
return angle * 360 / (2 * np.pi)
def _get_signed_angle_between(v1, v2, radians=True):
"""Returns the signed angle between vectors v1 and v2.
Parameters
----------
v1, v2 : numpy.array
The vectors in question.
radians : bool, default False
If True, return the angle in radians (otherwise it is in degrees).
Returns
-------
angle : float
The signed angle between two vectors.
Notes
-----
Adapted from https://stackoverflow.com/a/16544330/2912349
"""
x1, y1 = v1
x2, y2 = v2
dot = x1*x2 + y1*y2
det = x1*y2 - y1*x2
angle = np.arctan2(det, dot)
if radians:
return angle
else:
return angle * 360 / (2 * np.pi)
def _bspline(cv, n=100, degree=5, periodic=False):
"""Calculate n samples on a bspline.
Parameters
----------
cv : numpy.array
Array of (x, y) control vertices.
n : int
Number of samples to return.
degree : int
Curve degree
periodic : bool, default True
If True, the curve is closed.
Returns
-------
numpy.array
Array of (x, y) spline vertices.
Notes
-----
Adapted from https://stackoverflow.com/a/35007804/2912349
"""
cv = np.asarray(cv)
count = cv.shape[0]
# Closed curve
if periodic:
kv = np.arange(-degree,count+degree+1)
factor, fraction = divmod(count+degree+1, count)
cv = np.roll(np.concatenate((cv,) * factor + (cv[:fraction],)),-1,axis=0)
degree = np.clip(degree,1,degree)
# Opened curve
else:
degree = np.clip(degree,1,count-1)
kv = np.clip(np.arange(count+degree+1)-degree,0,count-degree)
# Return samples
max_param = count - (degree * (1-periodic))
spl = BSpline(kv, cv, degree)
return spl(np.linspace(0,max_param,n))
def get_path_around_origin(source, target, origin):
v1 = source - origin
v2 = target - origin
# determine control point angles
delta_angle = 10 # angle between control points in degrees
interior_angle = _get_interior_angle_between(v1, v2) # in radians
total_control_points = int(interior_angle / (2 * np.pi) * 360 / delta_angle)
a1 = _get_signed_angle_between(np.array([1, 0]), v1) # start angle
a2 = _get_signed_angle_between(np.array([1, 0]), v2) # stop angle
# angles = np.linspace(a1, a2, total_control_points + 1)[1:] # always counter-clockwise
if np.isclose(interior_angle, _get_signed_angle_between(v1, v2)):
angles = a1 + np.linspace(0, 1, total_control_points+1)[1:] * interior_angle
else: # go the other way
angles = a1 - np.linspace(0, 1, total_control_points+1)[1:] * interior_angle
# determine control point magnitudes
m1 = np.linalg.norm(v1)
m2 = np.linalg.norm(v2)
# magnitudes = np.linspace(m1, m2, total_control_points+1)[1:] # very shallow approach
magnitudes = np.linspace(m1, m2 + 0.25 * (m1 - m2), total_control_points+1)[1:] # for a more perpendicular approach to the target
# determine control points
dx = np.cos(angles) * magnitudes
dy = np.sin(angles) * magnitudes
points = np.vstack((source, origin[np.newaxis, :] + np.c_[dx, dy], target))
return _bspline(points) # interpolate & smooth
if __name__ == "__main__":
fig, ax = plt.subplots()
origin = np.array([0, 0])
radius = 1
ax.add_patch(plt.Circle(origin, radius, alpha=0.1))
source = np.array([-1.25, 0])
for target in [np.array([0, 1]), np.array([1, 0]), np.array([0, -1])]:
vertices = get_path_around_origin(source, target, origin)
ax.plot(*vertices.T, color="tab:red")
ax.axis([-1.5, 1.5, -1.5, 1.5])
ax.set_aspect("equal")
plt.show()