paulbrodersen / netgraph

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

(Un)directed mixed graph #82

Open lcastri opened 5 months ago

lcastri commented 5 months ago

Hi there! Is there the possibility to generate a graph with some edges that are directed (with arrow) and some undirected?

Thank you, Luca

paulbrodersen commented 5 months ago

It's not a feature that is currently supported, but I will add it to the TODO list.

The only workaround I can see is to plot the network twice, first all edges with arrow heads and then the remaining ones (or vice-versa).

Figure_1

import matplotlib.pyplot as plt
import netgraph

edges = [(0, 1), (1, 2), (2, 0)]

# set or precompute node positions
pos = {
    0 : (0.1, 0.1),
    1 : (0.9, 0.1),
    2 : (0.5, 0.7),
}

fig, ax = plt.subplots()
netgraph.Graph(edges[:-1], nodes=[0, 1, 2], node_layout=pos, arrows=True, ax=ax)
netgraph.Graph(edges[-1:], nodes=[0, 1, 2], node_layout=pos, arrows=False, ax=ax)
plt.show()

If the desired node_alpha is less than one, one of the two function calls to Graph should be with node_alpha set to zero. Note that the interactive variants won't work properly with this workaround (at least I would be surprised if they did!).

paulbrodersen commented 5 months ago

Actually, I came up with a better way that doesn't break interactive Graph variants.

Similarly, to issue #83, you can simply instantiate the Graph object (or any of its derived classes), and then manipulate the edge artists individually, and either add or remove arrows by setting the head_width and head_length attributes appropriately.

Figure_1

import matplotlib.pyplot as plt

from netgraph import Graph

fig, ax = plt.subplots()
g = Graph([(0, 1), (1, 2), (2, 0)], arrows=True, node_labels=True, ax=ax)
edge_artist = g.edge_artists[(0, 1)]
edge_artist.head_width = 1e-12 # don't set them to zero as this can cause numerical issues 
edge_artist.head_length = 1e-12
edge_artist._update_path()
plt.show()
lcastri commented 5 months ago

Hi,

Thank you for the solutions; the second one seems to be the best.

I tried to play around with the library and managed to achieve a similar result with minimal modifications.

I defined a dictionary called "arrows":

arrows = {
(0, 1)  : False, 
(1, 2)  : True,
(2, 0) : True
}

Then, I used the dictionary in the graph definition:

fig, ax = plt.subplots()
g = Graph([(0, 1), (1, 2), (2, 0)], arrows=arrows, node_labels=True, ax=ax)
plt.show()

To implement this, I only needed to modify the "draw_edges" method in the "_main.py" script as follows:

if arrows[edge]:
    head_length = 2 * edge_width[edge]
    head_width = 3 * edge_width[edge]
else:
    head_length = 0
    head_width = 0

instead of

if arrows:
    head_length = 2 * edge_width[edge]
    head_width = 3 * edge_width[edge]
else:
    head_length = 0
    head_width = 0

However, I haven't extensively tested this modification for all types of graphs, so I do not know what else it can affects.

Luca

paulbrodersen commented 5 months ago

Hi Luca, your proposal is very similar to what I have in mind. Basically, the plan is to support 3 cases:

  1. arrows is a just a boolean. Everything remains as before to retain backwards compatibility.
  2. arrows is a dict. a) arrows is a dict mapping edge IDs to a boolean (= your suggestion). b) arrows is a dict mapping edge IDs to a tuple (head length, head width) (= solution for issue #83).

Would that work for you?

lcastri commented 5 months ago

Yes absolutely :)