paulbrodersen / netgraph

Publication-quality network visualisations in python
GNU General Public License v3.0
660 stars 39 forks source link

Strange Axis BoundingBox "diamond" #79

Open rtbs-dev opened 7 months ago

rtbs-dev commented 7 months ago

Running into a pretty consistent "diamond" bound on my node layouts for some reason. I've tried playing with many layout settings, but so far it only changes the sizes within this odd diamond.

Here's a min. example:

from netgraph import Graph
Graph(G) # pre-existing NX graph
plt.show()

image

Adjusting the scale doesn't seem to do anything to the boundary:

Graph(G, scale=(2,2))

image

Replicating in networkx:

nx.draw_networkx_nodes(
    G, pos=(testpos:=nx.fruchterman_reingold_layout(G)), 
    node_color='white', edgecolors='k', 
    node_size=50, linewidths=0.5,
)
nx.draw_networkx_edges(
    G, pos=testpos
)

image

So I don't think my networkx layouts are reaching some kind of border.

Similarly, the diamond starts appearing for the karate graph:

Graph(nx.karate_club_graph(), scale=(2,2))

image

vs:

nx.draw_networkx_nodes(
    K:=nx.karate_club_graph(), pos=(testpos:=nx.fruchterman_reingold_layout(K)), 
    node_color='white', edgecolors='k', 
    node_size=100, linewidths=0.5,
)
nx.draw_networkx_edges(
    K, pos=testpos
)

image

paulbrodersen commented 7 months ago

Hi, thanks for raising the issue. This is clearly not the intended behaviour.

I think there are two things happening here.

  1. The attractive force acting along the edges isn't enough to counter the repulsion between all nodes. Hence many nodes are pushed to the bounding box, which is a square by default. NetworkX doesn't run into that problem, because their implementation of the FR algorithm differs significantly from the algorithm presented in the FR paper.
  2. After computing a layout, Netgraph determines the major axis of the node positions and then rotates the graph such that this major axis matches the largest extent of the bounding box (width or height, whichever is larger; width if tied). This creates the diamond.

The solution is to increase the spring constant k, e.g.:

Graph(G, node_layout="spring", node_layout_kwargs=dict(k=0.1))

I am unsure why the default value for k is currently so small but I will investigate. In the meantime, please just set it explicitly. Do let me know if that doesn't fix the issue on your end.

rtbs-dev commented 7 months ago

Thanks! Let me see here:

k not set: image

k=0.01 image

k = 0.1 image

k = 0.2 image

k = 0.5 image

Super interesting. setting scale=(2,2)

k=0.1 looks great image

k=0.5 actually goes back to the diamond? image

Reading that issue, there's a super interesting conclusion at the end

So we are in a bit of a dilemma here: If we have no frame, then examples like the ball and chain are not good. If we add a frame, then the ball and chain can be good. However this makes many other layouts bad. To fix that we have to reduce the optimal edge length (e.g. by reducing C), however this reintroduces the original issue.

So in this case I would propose to do the following:

  • implement a frame with inelastic boundaries that is optional and disabled by default.
  • add the constant C as a possible parameter, defaulting to 1.
  • keep the force-calculation and temperature as it was in the original implementation.

So if I'm honest, I nearly always fall in the "not ball and chain" category... stuff like social and semantic (sparse) networks. Border frames...not so useful? Is there a chance the frame could be disabled and avoid this node-size/spring-constant iteration? It's going to make automating my publication plots a bit of a headache :sweat_smile:

Loving this API, btw, so glad to see a more capable plotting library!

paulbrodersen commented 7 months ago

Border frames...not so useful? Is there a chance the frame could be disabled and avoid this node-size/spring-constant iteration?

Border frames are quite useful in other cases but I do agree that there maybe should be an option to use the NetworkX variant instead of the original FR algorithm. I will add it to the list of planned features, but it will be a while until I get to it. In the meantime, if you prefer the NetworkX spring layout, you can simply pre-compute the positions with NetworkX and then pass the node position dictionary to Netgraph:

import matplotlib.pyplot as plt
import networkx as nx
from netgraph import Graph

G = nx.karate_club_graph()
node_positions = nx.spring_layout(G)
Graph(G, node_layout=node_positions)
plt.show()

Loving this API, btw, so glad to see a more capable plotting library!

Glad you like it!