Reputation: 9854
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
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:
Here is briefly the idea behind it:
If this sounds too complicated, I hope the image makes it clear: Visualization of anchors
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
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