Skip to content Skip to sidebar Skip to footer

Label Nodes Outside With Minimum Overlap With Other Nodes/edges In Networkx

I am trying to create a graph with node labels printed outside of nodes. I am able to generate 'offset' as shown below that solve the purpose. However, Sometimes the labels overlap

Solution 1:

I have previously attempted something similar with the main idea being to keep out of the way of the edges mostly.

Assuming that the edges are straight lines, there are two simple and similar ways to achieve this:

  1. On the basis of the angles that the edges of a node's neighbourhood are making with respect to the node itself.

  2. On the basis of the centroid of the neighbourhood's nodes.

So, find the angles that the edges departing from a node towards its neighbourhood form and try to position the label AWAY from the majority of the edges; OR estimate the centroid of a node's neighbourhood and position the label along the opposite direction.

The first solution can be a little bit problematic, primarily because of the way that the atan2 function operates (which essentially determines the edge angles) but it does provide some flexibility in terms of positioning the label.

The second solution is the simplest and works as follows:

import networkx as nx
import matplotlib.pyplot as plt

#Build the graph#Please note, the code here is as per the original post
G=nx.Graph()
G = nx.complete_graph(5)
mapping = {0:'aaaaaaa',1:'bbbbbbb',2:'ccccccc', 3:'dddddddd', 4:'eeeeeeeee'}
G = nx.relabel_nodes(G,mapping)

plt.figure(figsize=(10,10), facecolor="w", frameon=False)
#Get a graph layout
pos = nx.graphviz_layout(G, prog="fdp") #calculate position (x,y) coordinates#Here is an alternative layout, please see below.#pos = nx.layout.spring_layout(G)
nx.draw_networkx_nodes(G,pos,node_size=1200,node_shape='^',node_color='0.75')
nx.draw_networkx_edges(G,pos, width=2,edge_color='r')
#Show the original position of the labels using a Green colour.
nx.draw_networkx_labels(G,pos,font_color='g')

#Please note, the code below uses the original idea of re-calculating a dictionary of adjusted label positions per node.
label_ratio = 1.0/8.0
pos_labels = {} 
#For each node in the Graphfor aNode in G.nodes():
    #Get the node's position from the layout
    x,y = pos[aNode]
    #Get the node's neighbourhood
    N = G[aNode]
    #Find the centroid of the neighbourhood. The centroid is the average of the Neighbourhood's node's x and y coordinates respectively.#Please note: This could be optimised further
    cx = sum(map(lambda x:pos[x][0], N)) / len(pos)
    cy = sum(map(lambda x:pos[x][1], N)) / len(pos)
    #Get the centroid's 'direction' or 'slope'. That is, the direction TOWARDS the centroid FROM aNode.
    slopeY = (y-cy)
    slopeX = (x-cx)
    #Position the label at some distance along this line. Here, the label is positioned at about 1/8th of the distance.
    pos_labels[aNode] = (x+slopeX*label_ratio, y+slopeY*label_ratio)

#Finally, redraw the labels at their new position.
nx.draw_networkx_labels(G,pos=pos_labels,fontsize=2)
#Show the figure
plt.show()

This works, mostly, for nodes that are largely in the periphery of the Graph but is challenging for nodes that are positioned towards the centre of the graph because the centroid will not provide a reliable direction that avoids the majority of the edges.

Here is the output for graphviz's fdp layout...

graphviz fdp output

...and here is the output for networkx' spring layout.

networkx spring layout

Please note the proximity of the green and black coloured labels on the second figure. Essentially, the centroid of ddddddd's neighbourhood is relatively close to the node's actual position.

For a more complex solution, you might want to check more complex algorithms such as the one that is used by Wordle in order to adapt the initial position of the label if it intersects an edge.

Hope this helps.

Solution 2:

The approaches outlined by @A_A are built on good intuitions and are decent first approximations. However, apart from the problems already mentioned by @A_A, there are some additional issues with both approaches.

  1. Both approaches only reduce label-edge overlaps if all edges in the (Euclidean) vicinity of a node also belong to that node. However, if the graph is large or dense, the majority of the edges in the vicinity of a node can belong to other nodes, which neither approach takes into account.

  2. Even though both approaches typically reduce label-edge overlaps in small and sparse graphs, neither approach addresses label-node and label-label overlaps.

There is a conceptually simple approach that also resolves label-node and label-label overlaps: draw a circle around each node being labelled. On each circle, find the point that is furthest away from everything else (nodes, edges, other labels). This ensures that this position on the circle has the most empty canvas around it, and is thus a good place to put a label.

This can be done in the following way:

  1. Approximate each edge with a series of points densely sampled along the edge. In practice, 10-20 points seem to work well enough, but even 100-1000 points are computationally easily tractable. Make sure to include the start and end points of the edge, i.e. the node positions.

  2. For each label, compute a second set of points sampled along a circle around the corresponding node. Again, 35 points (one point for every 10 degrees) are usually more than enough, but there is no substantial harm in using more points, say 100.

  3. For each circle, find the point on the circle whose nearest Euclidean neighbour is maximally far away (while excluding points on the same circle). Place the label there.

Step 3 can be further refined to use the maximum average distance of the nearest two neighbours. This resolves ties, which can occur when the node is on the periphery of the graph, such that the nearest neighbour for a large section of the circle is the node being labelled.

All of this may sound horrific from a numerical perspective. However, nearest neighbour distances can be computed very efficiently by using KD-trees, as demonstrated below (using 100 points to approximate each edge and circle).

enter image description here

This approach is implemented in netgraph, a python library for visualising networks (I am the author). The library is fully compatible with most common graph data formats, including networkx and igraphGraph objects, so it should be easy and fast to make great looking graphs of graphs. At least that is the idea.

Code to reproduce the animation (mouse movements not included):

#!/usr/bin/env pythonimport matplotlib.pyplot as plt
import networkx as nx
from netgraph import InteractiveGraph # pip install netgraph

g = InteractiveGraph(nx.complete_graph(10), node_size=2, edge_width=0.5,
                     node_labels=dict(zip(range(10), 'abcdefghij')), node_label_offset=0.05,
                     node_label_fontdict=dict(size=20, fontweight='bold'))
plt.show()

Post a Comment for "Label Nodes Outside With Minimum Overlap With Other Nodes/edges In Networkx"