Hi. I have a circular layout graph with 12 nodes outside of the layout (by design).
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)
I want to make a bezier path (or similar to bezier) for each of the (straight) edges of the outer 12 nodes, that will stay outside of the nodes in the circular layout until reaching the circular layout target node, by spiraling in (I don't want the edge to leap away from the graph like what happens when you increase the midpoint of the edge too much).
Currently I only have the bezier curve math worked out for the inner circular layout edges:
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)
This solution creates splines between two points that are routed around a central origin. For each spline, the distances of its interior points to the origin are interpolated between the distances of the start and end points of the spline to the same origin, resulting in spiral-like appearance.
This solution also selects the shortest path around the origin (rather than always wrapping around counter-clockwise).
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):
# determine vectors from origin to end points
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 vectors from origin to control points
dx = np.cos(angles) * magnitudes
dy = np.sin(angles) * magnitudes
v = np.c_[dx, dy]
points = np.vstack((source, origin[np.newaxis, :] + v, 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()