pceccon
pceccon

Reputation: 9854

Show self loops with networkx - Python

I'm using the DiGraph class from networkx, which, by the docs, should allow self loops. However, when plotting with Matplotlib, I just cannot see any self loop, no matter if

print(G.nodes_with_selfloops())

returns a list of nodes with self loops. I'm wondering how to display these self loops.

I'm using these functions to draw:

nx.draw_networkx_edge_labels(G,pos,edge_labels=edge_labels)
nx.draw_networkx(G,pos,font_color='k',node_size=500, edge_color='b', alpha=0.5)

Upvotes: 2

Views: 6089

Answers (2)

Vladimir Shitov
Vladimir Shitov

Reputation: 46

I have faced the same issue when trying to draw a chord diagram using networkx. On an older version of networkx (2.5) self-loops were drawn with one dot behind the node (which means you don't see them at all). On the newer version (2.6.2), self-loops are drawn in the same direction as on the image below

Self-loops in the networkx 2.6.2

If this is enough for you, try to update networkx. Looks like this problem is solved. At least, the documentation has some info about it

However, if this is not enough for you (as it was for me), you can write a custom code to draw self-loops nicer. I created a repository for that task. It allows to draw self-loop with different directions, looking away from the center:

Self-loops with my code

Here is briefly the idea behind it:

  1. You know the start and the end coordinates of the loop. These are just the coordinates of your node
  2. You need 2 more points to draw a self-loop with Bézier curve. You want them to be further away from the center, than the node of the graph. You also want them to be further from the node in the orthogonal direction to the vector from the center to the node

If this sounds too complicated, I hope the image makes it clear: Visualization of anchors

  1. When we obtained the anchors, we can draw a Bézier curve through them. This is how it looks: Self-loops

Here is code:

from typing import Optional

import matplotlib.pyplot as plt
from matplotlib.path import Path as MplPath  # To avoid collisions with pathlib.Path
import matplotlib.patches as patches
import networkx as nx
import numpy as np


# Some useful functions
def normalize_vector(vector: np.array, normalize_to: float) -> np.array:
    """Make `vector` norm equal to `normalize_to`
    
    vector: np.array
        Vector with 2 coordinates
    
    normalize_to: float
        A norm of the new vector
        
    Returns
    -------
    Vector with the same direction, but length normalized to `normalize_to`
    """
    
    vector_norm = np.linalg.norm(vector)
    
    return vector * normalize_to / vector_norm


def orthogonal_vector(point: np.array, width: float, normalize_to: Optional[float] = None) -> np.array:
    """Get orthogonal vector to a `point`

    point: np.array
        Vector with x and y coordinates of a point

    width: float
        Distance of the x-coordinate of the new vector from the `point` (in orthogonal direction)

    normalize_to: Optional[float] = None
        If a number is provided, normalize a new vector length to this number
    
    Returns
    -------
    Array with x and y coordinates of the vector, which is orthogonal to the vector from (0, 0) to `point` 
    """
    EPSILON = 0.000001

    x = width
    y = -x * point[0] / (point[1] + EPSILON)

    ort_vector = np.array([x, y])

    if normalize_to is not None:
        ort_vector = normalize_vector(ort_vector, normalize_to)

    return ort_vector


def draw_self_loop(
    point: np.array,
    ax: Optional[plt.Axes] = None,
    padding: float = 1.5,
    width: float = 0.3,
    plot_size: int = 10,
    linewidth = 0.2,
    color: str = "pink",
    alpha: float = 0.5
) -> plt.Axes:
    """Draw a loop from `point` to itself

    !Important! By "center" we assume a (0, 0) point. If your data is centered around a different points,
    it is strongly recommended to center it around zero. Otherwise, you will probably get ugly plots

    Parameters
    ----------
    point: np.array
        1D array with 2 coordinates of the point. Loop will be drawn from and to these coordinates.
    padding: float = 1.5
        Controls how the distance of the loop from the center. If `padding` > 1, the loop will be
        from the outside of the `point`. If `padding` < 1, the loop will be closer to the center
    width: float = 0.3
        Controls the width of the loop
    linewidth: float = 0.2
        Width of the line of the loop
    ax: Optional[matplotlib.pyplot.Axes]:
        Axis on which to draw a plot. If None, a new Axis is generated
    plot_size: int = 7
        Size of the plot sides in inches. Ignored if `ax` is provided    
    color: str = "pink"
        Color of the arrow
    alpha: float = 0.5
        Opacity of the edge
    
    Returns
    -------
    Matplotlib axes with the self-loop drawn
    """

    if ax is None:
        fig, ax = plt.subplots(figsize=(plot_size, plot_size))
    
    point_with_padding = padding * point

    ort_vector = orthogonal_vector(point, width, normalize_to=width)

    first_anchor = ort_vector + point_with_padding
    second_anchor = -ort_vector + point_with_padding

    verts = [point, first_anchor, second_anchor, point]
    codes = [MplPath.MOVETO, MplPath.CURVE4, MplPath.CURVE4, MplPath.CURVE4]

    path = MplPath(verts, codes)

    patch = patches.FancyArrowPatch(
        path=path,
        facecolor='none',
        lw=linewidth,
        arrowstyle="-|>",
        color=color,
        alpha=alpha,
        mutation_scale=30  # arrowsize in draw_networkx_edges()
    )
    ax.add_patch(patch)

    return ax

Code example with drawing a plot:

fig, ax = plt.subplots(figsize=(6, 6))

graph = nx.DiGraph(
    np.array([
        [1, 1, 1, 1, 1],
        [1, 0, 1, 0, 0],
        [1, 1, 1, 0, 1],
        [0, 0, 1, 0, 1],
        [1, 1, 1, 1, 1]
    ])
)

pos = nx.circular_layout(graph, center=(0, 0))

nx.draw_networkx_nodes(graph, pos, ax=ax)
nx.draw_networkx_edges(graph, pos, ax=ax)

for node in graph.nodes:
    if (node, node) in graph.edges:
        draw_self_loop(point=pos[node], ax=ax, color="k", alpha=1, linewidth=1)
        
ax.set_xlim(-1.5, 1.5)
ax.set_ylim(-1.5, 1.5)

Result: chord diagram

You can find more examples and functions to draw a beautiful chord diagram in my repository

Upvotes: 2

Vladimir S.
Vladimir S.

Reputation: 548

https://networkx.github.io/documentation/networkx-1.10/reference/drawing.html

In the future, graph visualization functionality may be removed from NetworkX or only available as an add-on package.

We highly recommend that people visualize their graphs with tools dedicated to that task.

Link above provides many alternatives to built-in visualization. Do consider alternatives they provide to save yourself A LOT of time down the road.

Personally I use cytoscape, which accepts files in .graphml format. Exporting your graph to .graphml is very easy:

nx.write_graphml(graph, path_to_file)

Upvotes: 0

Related Questions