圆形布局之外的螺旋贝塞尔路径

问题描述 投票:0回答:1

嗨。我有一个圆形布局图,布局外部有 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)
python matplotlib networkx bezier
1个回答
0
投票

此解决方案在围绕中心原点路由的两个点之间创建样条线。对于每个样条线,其内部点到原点的距离插值在样条线的起点和终点到同一原点的距离之间,从而产生螺旋状外观。

此解决方案还选择围绕原点的最短路径(而不是始终逆时针环绕)。

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()

© www.soinside.com 2019 - 2024. All rights reserved.