Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using arrows in draw_networkx_edges removes legend labels. #7054

Open
hulsed opened this issue Oct 24, 2023 · 6 comments
Open

Using arrows in draw_networkx_edges removes legend labels. #7054

hulsed opened this issue Oct 24, 2023 · 6 comments
Labels
Visualization Related to graph visualization or layout

Comments

@hulsed
Copy link

hulsed commented Oct 24, 2023

When using the arrows argument in networkx.draw_networkx_edges, legend labels are removed from the object so they don't show up in the legend.

Current Behavior

Passing arrows=True removes the edge from the legend.

Expected Behavior

If a legend label is passed, it should be shown if you use plt.legend().

Steps to Reproduce

import networkx as nx
from matplotlib import pyplot as plt

g = nx.Graph()
g.add_nodes_from((1,2,3))
g.add_edges_from(((1,2),(2,3)))
pos = nx.spring_layout(g)
nx.draw_networkx_edges(g, pos, [(1,2)], label="edge1", style = 'dashed',
                       arrows=True, arrowstyle='-|>')
nx.draw_networkx_edges(g, pos, [(2,3)], label="edge2", style = 'solid')
nx.draw_networkx_nodes(g, pos, label="nodes")
plt.legend()

Gives the following plot with broken legend:
image

This is what would have been shown if arrows=True was not passed:
image

Environment

Anaconda Python (Windows)

Python version: 3.10.9
NetworkX version: 3.1

@dschult
Copy link
Member

dschult commented Oct 24, 2023

Thanks for the report.
I'm not sure how to make this work. I suspect it is related to the fact that the arrows are patches, while the non-arrows are a line collection. But, I suspect this SO link might have a solution. It looks like you/we need to add the arrow patch to the list of handles in the legend. Can you get it to work for you?
If so, please report back how you got it working? We could add that feature, or provide an example of how to do it.

@hulsed
Copy link
Author

hulsed commented Oct 24, 2023

So, here are my two workarounds:

  • If you set the label of one of the FancyArrowPatch afterward it gives you something like this:
import networkx as nx
from matplotlib import pyplot as plt
import matplotlib.lines as mlines

g = nx.Graph()
g.add_nodes_from((1,2,3))
g.add_edges_from(((1,2),(2,3)))
pos = nx.spring_layout(g)
b = nx.draw_networkx_edges(g, pos, [(1,2)], label="edge1", style = 'dashed',
                           arrows=True, arrowstyle='-|>')
b[0].set_label("edge1")
c = nx.draw_networkx_edges(g, pos, [(2,3)], label="edge2", style = 'solid')
nx.draw_networkx_nodes(g, pos, label="nodes")
plt.legend()

Which gives you something like this:
image

  • You can also create and add it to the legend handles (which is my current workaround):
import networkx as nx
from matplotlib import pyplot as plt
import matplotlib.lines as mlines

g = nx.Graph()
g.add_nodes_from((1,2,3))
g.add_edges_from(((1,2),(2,3)))
pos = nx.spring_layout(g)
b = nx.draw_networkx_edges(g, pos, [(1,2)], label="edge1", style = 'dashed',
                           arrows=True, arrowstyle='-|>')
lin = mlines.Line2D([], [], color='black', linestyle='dashed', label='edge1')

c = nx.draw_networkx_edges(g, pos, [(2,3)], label="edge2", style = 'solid')
nx.draw_networkx_nodes(g, pos, label="nodes")
plt.legend()
ax = plt.gca()
handles, labels = ax.get_legend_handles_labels()
plt.legend(handles=[lin]+handles)

which gives you this:
image

Neither strikes me as particularly elegant. Changing the FancyArrowPatch label is easy, but the line labels are ugly/don't really match the lines (perhaps that is an upstream issue for matplotlib?). And I'm not sure if/how you would to the second approach at the library level without it being your own implementation of legend().

@rossbar rossbar added the Visualization Related to graph visualization or layout label Oct 25, 2023
@rossbar
Copy link
Contributor

rossbar commented Oct 25, 2023

Neither strikes me as particularly elegant. Changing the FancyArrowPatch label is easy, but the line labels are ugly/don't really match the lines (perhaps that is an upstream issue for matplotlib?). And I'm not sure if/how you would to the second approach at the library level without it being your own implementation of legend().

Yeah I don't know if there's a way to do this generally in matplotlib. My (admittedly limited) understanding is that not all matplotlib artists are supported as legend handles. I suspect that FancyArrowPatches fall into this category, since the visual properties can vary for each patch instance. In this case, I think the only thing that can be done is to create/modify the legends directly, as suggested in the workarounds above or the matplotlib docs.

Assuming the above is correct (again, I'm not 100% sure that patches can't be legend handles --- it's worth investigating further) my vote would be to add an example for this to the networkx gallery.

@dschult
Copy link
Member

dschult commented Oct 25, 2023

This info was quite helpful for me -- thank you!
I have an idea for how it could work within nx.draw_networkx_edges, but I'm not sure it will work.

I discovered:

  • It turns out that FancyArrowPatches can be used in legends, but their legend symbol is a patch -- not a line. And our homemade "collection" of FancyArrowPatches is just a list of them. And we don't want an entry in the legend for each patch -- just one entry for all the edges drawn in one call. (one entry per "collection", not one per patch).

  • Currently when the FancyArrowPatches are drawn, we don't set their label for the legend even though it is an input to the function. We can set the label of a FancyArrowPatch. But maybe instead we should set the label of a line Artist created just to provide the legend.

Here's my idea:

  • draw the fancy arriws as currently
  • create a Line2D handle (just like you do above, only use the info from the FancyArrowPatches):
lin = mlines.Line2D([], [], color=b[0].get_edgecolor(), linestyle=b[0].get_linestyle(), label=label)

I have written this as if the list b is already created so the first patch is b[0]. And I've used the input argument label here to set the label, instead of setting the label of each patch via patch.set_label(label) which is also possible.

  • add the Line2D Artist to the axes: ax.add_artist(lin). You can check that this worked by using ax.get_legend_handles_labels().

Then when the user does ax.legend() or plt.legend(). It should "just work". Of course, there may be parts of this I am not taking into account. But it is definitely worth trying this approach. Here's a proof of concept code based on the code posted above:

plt.clf()
g=nx.path_graph(4)
pos = {i: (i,i) for i in g}
ax=plt.gca()
n = nx.draw_networkx_nodes(g, pos, label="nodes")
c = nx.draw_networkx_edges(g, pos, [(0, 1)], style='solid', label="edge2")

b = nx.draw_networkx_edges(g, pos, [(1,2), (2,3)], style = 'dashed', edge_color="blue", arrows=True, arrowstyle='-|>')
lin = mlines.Line2D([], [], color=b[0].get_edgecolor(), linestyle=b[0].get_linestyle(), label='edge1')

# user does this
ax.add_artist(lin)

@VashitvaR
Copy link

VashitvaR commented Jan 4, 2024

import networkx as nx
import matplotlib.pyplot as plt
import matplotlib.lines as mlines

g = nx.Graph()
g.add_nodes_from((1, 2, 3))
g.add_edges_from(((1, 2), (2, 3)))
pos = nx.spring_layout(g)

# Draw edges with arrows and labels
nx.draw_networkx_edges(g, pos, [(1, 2)], label="edge1", style='dashed', arrows=True, arrowstyle='-|>')
nx.draw_networkx_edges(g, pos, [(2, 3)], label="edge2", style='solid')
nx.draw_networkx_nodes(g, pos, label="nodes")

# Create sets for legend handles and labels
legend_handles = set()
legend_labels = set()

# Iterate over edges and add legend entries dynamically
for edge in g.edges():
    edge_label = f"edge_{edge[0]}_{edge[1]}"  # You can modify this based on your requirements
    if edge_label not in legend_labels:
        linestyle = 'dashed' if edge in [(1, 2)] else 'solid'  # Choose linestyle based on conditions
        lin_edge = mlines.Line2D([], [], color='black', linestyle=linestyle, label=edge_label)
        legend_handles.add(lin_edge)
        legend_labels.add(edge_label)

# Extract existing legend handles and labels
ax = plt.gca()
existing_handles, existing_labels = ax.get_legend_handles_labels()

# Add existing handles and labels to the sets
legend_handles.update(existing_handles)
legend_labels.update(existing_labels)

# Create a new legend with the combined set
plt.legend(handles=legend_handles, labels=legend_labels)

# Display the plot
plt.show()

@dschult
Copy link
Member

dschult commented Jan 4, 2024

I think the handles containers need to be sequences or the order isn't always preserved. so the legend_* vars should be something like lists.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Visualization Related to graph visualization or layout
Development

No branches or pull requests

4 participants