Repository: networkx/networkx

Issue:
Error Attempting to Iterate Over LineCollection
This is less a bug with the code base and more a bug in the examples, but as a result a number of pieces of example code do not work any longer.

I have cobbled together code that uses the **Directed Graph** ([https://networkx.github.io/documentation/stable/auto_examples/drawing/plot_directed.html#](url)) example code to implement color maps, line alphas, and a colorbar.  Apparently matplotlib.collections.LineCollection objects are no longer iterable, and thus the code

```
for i, arc in enumerate(edges):
    arc.set_alpha(edge_alphas[i])
```

does not work (nor does using something like `for e in edges:` or `for i in range(len(edges)):`

The problem also arises in trying to create a colorbar legend by passing the `edges` object to `mpl.collections.PatchCollection()` as this too raise the error that LineCollection objects are not iterable.

Versions:

Python - 3.6
Networkx - 2.2
Matplotlib - 3.0.2

Here is the full code (assuming you have a graph `G` already):

```
pos = nx.circular_layout(G)

M = G.number_of_edges()
node_sizes = [3 + 10 * i for i in range(len(G))]
edge_colors = range(2, M + 2)
edge_alphas = [(5 + i) / (M + 4) for i in range(M)]

nodes = nx.draw_networkx_nodes(G, pos, node_color='red')
edges = nx.draw_networkx_edges(G, pos, edge_color=edge_colors,edge_cmap=plt.cm.coolwarm, width=2)

nx.draw_networkx_labels(G,pos,node_labels,font_size=16)

# set alpha value for each edge
for i, arc in enumerate(edges):
    arc.set_alpha(edge_alphas[i])

pc = mpl.collections.PatchCollection(edges, cmap=plt.cm.Blues)
pc.set_array(edge_colors)
plt.colorbar(pc)

plt.axis('off')
plt.show()
```


Discussion:
The example code you point to doesn't have a list that iterates over the edges.
Both stable and latest version of documentation uses the following idiom to set the alpha values:

    # set alpha value for each edge
    for i in range(M):
        edges[i].set_alpha(edge_alphas[i])

Try the code at [this documentation link](https://networkx.github.io/documentation/stable/auto_examples/drawing/plot_directed.html)
Thanks for the reply.  I have tried this way just now and it does not work either.

This might be an issue with a loss of functionality in a new version of **matplotlib**, what version of **matplotlib** are you using?  I am on 3.0.2, which apparently does not support indexing on LineCollection objects.
Hmmm...  I think 3.0.2 does allow indexing on LineCollection. I have used it with 3.0.3 so I know it works there, and it worked with 2.x.

This is an example session. Figure shows up fine -- no errors.

    Python 3.6.8 |Anaconda, Inc.| (default, Dec 29 2018, 19:04:46) 
    Type 'copyright', 'credits' or 'license' for more information
    IPython 6.4.0 -- An enhanced Interactive Python. Type '?' for help.
    
    In [1]: import matplotlib as mpl

    In [2]: mpl.__version__
    Out[2]: '3.0.3'

    In [3]: from  __future__ import division
       ...: import matplotlib as mpl
       ...: import matplotlib.pyplot as plt
       ...: import networkx as nx
       ...: 
       ...: G = nx.generators.directed.random_k_out_graph(10, 3, 0.5)
       ...: pos = nx.layout.spring_layout(G)
       ...: 
       ...: node_sizes = [3 + 10 * i for i in range(len(G))]
       ...: M = G.number_of_edges()
       ...: edge_colors = range(2, M + 2)
       ...: edge_alphas = [(5 + i) / (M + 4) for i in range(M)]
       ...: 
       ...: nodes = nx.draw_networkx_nodes(G, pos, node_size=node_sizes, node_color='blue')
       ...: edges = nx.draw_networkx_edges(G, pos, node_size=node_sizes, arrowstyle='->',
       ...:                                arrowsize=10, edge_color=edge_colors,
       ...:                                edge_cmap=plt.cm.Blues, width=2)
       ...: # set alpha value for each edge
       ...: for i in range(M):
       ...:     edges[i].set_alpha(edge_alphas[i])
       ...: 
       ...: pc = mpl.collections.PatchCollection(edges, cmap=plt.cm.Blues)
       ...: pc.set_array(edge_colors)
       ...: plt.colorbar(pc)
       ...: 
       ...: ax = plt.gca()
       ...: ax.set_axis_off()
       ...: plt.show()
       ...: 

Hello everyone :) In the [matplotlib documentation about Collections API](https://matplotlib.org/3.1.1/api/collections_api.html) they state (somehow, it's not very clear) that the Collections act as a whole. I've talked with the developers of matplotlib and they explained me that it's a design choice not to let the users iterate this monolithic structure. So it's not a bug.

As to the alpha channels of the edges, there's a trick. Not very elegant but, at least, relies fully on the public API and without any bad workaround. It works by setting the alpha values in the RGBA notation.
```python
edges = nx.draw_networkx_edges(G, ... )
edges.set_alpha(None) # needed up to Networkx 3.6
edges.set_color( RGBAList )
```
Where `RGBA_List` is the list of the colors (with alpha channel) we want to assign to the edges. Unfortunately every other effort seems to make use of the internals of the `LineCollection` of matplotlib and, therefore, it's not reliable.

I hope it can help others in future :)
I don't think that Matplotlib's Collection object have ever been iterable.  Internally we do a fair amount of implicit broadcasting / zipping / colormapping that would be very difficult to keep in sync if we also let users reach in and touch individual elements (the number of notional elements is not ever really fixed if you set the offsets). 

It looks like in nx21 (via https://github.com/networkx/networkx/pull/2760) nx changed to sometimes returning a `LineCollection` and sometimes returning a list of `FancyArrowPatch` objects so in some cases you can iterate over the object returned by `draw_networkx_edges` and sometimes not.

Not sure what the best path out of this is though.

Resolving patch:
diff --git a/networkx/drawing/nx_pylab.py b/networkx/drawing/nx_pylab.py
--- a/networkx/drawing/nx_pylab.py
+++ b/networkx/drawing/nx_pylab.py
@@ -25,6 +25,7 @@
     random_layout,
     planar_layout,
 )
+import warnings
 
 __all__ = [
     "draw",
@@ -480,13 +481,13 @@ def draw_networkx_edges(
     edge_color="k",
     style="solid",
     alpha=None,
-    arrowstyle="-|>",
+    arrowstyle=None,
     arrowsize=10,
     edge_cmap=None,
     edge_vmin=None,
     edge_vmax=None,
     ax=None,
-    arrows=True,
+    arrows=None,
     label=None,
     node_size=300,
     nodelist=None,
@@ -536,11 +537,15 @@ def draw_networkx_edges(
        Draw the graph in the specified Matplotlib axes.
 
     arrows : bool, optional (default=True)
-       For directed graphs, if True draw arrowheads.
+       For directed graphs, if True draw arrowheads by default.  Ignored
+       if *arrowstyle* is passed.
+
        Note: Arrows will be the same color as edges.
 
-    arrowstyle : str, optional (default='-|>')
-       For directed graphs, choose the style of the arrow heads.
+    arrowstyle : str, optional (default=None)
+       For directed graphs and *arrows==True* defaults to ``'-|>'`` otherwise
+       defaults to ``'-'``.
+
        See :py:class: `matplotlib.patches.ArrowStyle` for more
        options.
 
@@ -566,19 +571,17 @@ def draw_networkx_edges(
 
     Returns
     -------
-    matplotlib.collection.LineCollection
-        `LineCollection` of the edges
-
     list of matplotlib.patches.FancyArrowPatch
         `FancyArrowPatch` instances of the directed edges
 
-    Depending whether the drawing includes arrows or not.
-
     Notes
     -----
     For directed graphs, arrows are drawn at the head end.  Arrows can be
-    turned off with keyword arrows=False. Be sure to include `node_size` as a
-    keyword argument; arrows are drawn considering the size of nodes.
+    turned off with keyword arrows=False or by passing an arrowstyle without
+    an arrow on the end.
+
+    Be sure to include `node_size` as a keyword argument; arrows are
+    drawn considering the size of nodes.
 
     Examples
     --------
@@ -602,13 +605,27 @@ def draw_networkx_edges(
     draw_networkx_nodes()
     draw_networkx_labels()
     draw_networkx_edge_labels()
+
     """
     import matplotlib.pyplot as plt
     from matplotlib.colors import colorConverter, Colormap, Normalize
-    from matplotlib.collections import LineCollection
-    from matplotlib.patches import FancyArrowPatch
+    from matplotlib.patches import FancyArrowPatch, ConnectionStyle
+    from matplotlib.path import Path
     import numpy as np
 
+    if arrowstyle is not None and arrows is not None:
+        warnings.warn(
+            f"You passed both arrowstyle={arrowstyle} and "
+            f"arrows={arrows}.  Because you set a non-default "
+            "*arrowstyle*, arrows will be ignored."
+        )
+
+    if arrowstyle is None:
+        if G.is_directed() and arrows:
+            arrowstyle = "-|>"
+        else:
+            arrowstyle = "-"
+
     if ax is None:
         ax = plt.gca()
 
@@ -616,10 +633,7 @@ def draw_networkx_edges(
         edgelist = list(G.edges())
 
     if len(edgelist) == 0:  # no edges!
-        if not G.is_directed() or not arrows:
-            return LineCollection(None)
-        else:
-            return []
+        return []
 
     if nodelist is None:
         nodelist = list(G.nodes())
@@ -649,108 +663,118 @@ def draw_networkx_edges(
         color_normal = Normalize(vmin=edge_vmin, vmax=edge_vmax)
         edge_color = [edge_cmap(color_normal(e)) for e in edge_color]
 
-    if not G.is_directed() or not arrows:
-        edge_collection = LineCollection(
-            edge_pos,
-            colors=edge_color,
-            linewidths=width,
-            antialiaseds=(1,),
-            linestyle=style,
-            transOffset=ax.transData,
-            alpha=alpha,
-        )
-
-        edge_collection.set_cmap(edge_cmap)
-        edge_collection.set_clim(edge_vmin, edge_vmax)
+    # Note: Waiting for someone to implement arrow to intersection with
+    # marker.  Meanwhile, this works well for polygons with more than 4
+    # sides and circle.
 
-        edge_collection.set_zorder(1)  # edges go behind nodes
-        edge_collection.set_label(label)
-        ax.add_collection(edge_collection)
-
-        return edge_collection
-
-    arrow_collection = None
+    def to_marker_edge(marker_size, marker):
+        if marker in "s^>v<d":  # `large` markers need extra space
+            return np.sqrt(2 * marker_size) / 2
+        else:
+            return np.sqrt(marker_size) / 2
 
-    if G.is_directed() and arrows:
-        # Note: Waiting for someone to implement arrow to intersection with
-        # marker.  Meanwhile, this works well for polygons with more than 4
-        # sides and circle.
+    # Draw arrows with `matplotlib.patches.FancyarrowPatch`
+    arrow_collection = []
+    mutation_scale = arrowsize  # scale factor of arrow head
 
-        def to_marker_edge(marker_size, marker):
-            if marker in "s^>v<d":  # `large` markers need extra space
-                return np.sqrt(2 * marker_size) / 2
-            else:
-                return np.sqrt(marker_size) / 2
-
-        # Draw arrows with `matplotlib.patches.FancyarrowPatch`
-        arrow_collection = []
-        mutation_scale = arrowsize  # scale factor of arrow head
-
-        # FancyArrowPatch doesn't handle color strings
-        arrow_colors = colorConverter.to_rgba_array(edge_color, alpha)
-        for i, (src, dst) in enumerate(edge_pos):
-            x1, y1 = src
-            x2, y2 = dst
-            shrink_source = 0  # space from source to tail
-            shrink_target = 0  # space from  head to target
-            if np.iterable(node_size):  # many node sizes
-                source, target = edgelist[i][:2]
-                source_node_size = node_size[nodelist.index(source)]
-                target_node_size = node_size[nodelist.index(target)]
-                shrink_source = to_marker_edge(source_node_size, node_shape)
-                shrink_target = to_marker_edge(target_node_size, node_shape)
-            else:
-                shrink_source = shrink_target = to_marker_edge(node_size, node_shape)
-
-            if shrink_source < min_source_margin:
-                shrink_source = min_source_margin
-
-            if shrink_target < min_target_margin:
-                shrink_target = min_target_margin
-
-            if len(arrow_colors) == len(edge_pos):
-                arrow_color = arrow_colors[i]
-            elif len(arrow_colors) == 1:
-                arrow_color = arrow_colors[0]
-            else:  # Cycle through colors
-                arrow_color = arrow_colors[i % len(arrow_colors)]
-
-            if np.iterable(width):
-                if len(width) == len(edge_pos):
-                    line_width = width[i]
-                else:
-                    line_width = width[i % len(width)]
-            else:
-                line_width = width
-
-            arrow = FancyArrowPatch(
-                (x1, y1),
-                (x2, y2),
-                arrowstyle=arrowstyle,
-                shrinkA=shrink_source,
-                shrinkB=shrink_target,
-                mutation_scale=mutation_scale,
-                color=arrow_color,
-                linewidth=line_width,
-                connectionstyle=connectionstyle,
-                linestyle=style,
-                zorder=1,
-            )  # arrows go behind nodes
-
-            # There seems to be a bug in matplotlib to make collections of
-            # FancyArrowPatch instances. Until fixed, the patches are added
-            # individually to the axes instance.
-            arrow_collection.append(arrow)
-            ax.add_patch(arrow)
-
-    # update view
+    # compute view
     minx = np.amin(np.ravel(edge_pos[:, :, 0]))
     maxx = np.amax(np.ravel(edge_pos[:, :, 0]))
     miny = np.amin(np.ravel(edge_pos[:, :, 1]))
     maxy = np.amax(np.ravel(edge_pos[:, :, 1]))
-
     w = maxx - minx
     h = maxy - miny
+
+    if connectionstyle is not None:
+        base_connection_style = ConnectionStyle(connectionstyle)
+
+        def _connectionstyle(posA, posB, *args, **kwargs):
+            # check if we need to do a self-loop
+            if np.all(posA == posB):
+                # this is called with _screen space_ values so covert back
+                # to data space
+                data_loc = ax.transData.inverted().transform(posA)
+                v_shift = 0.1 * h
+                h_shift = v_shift * 0.5
+                # put the top of the loop first so arrow is not hidden by node
+                path = [
+                    # 1
+                    data_loc + np.asarray([0, v_shift]),
+                    # 4 4 4
+                    data_loc + np.asarray([h_shift, v_shift]),
+                    data_loc + np.asarray([h_shift, 0]),
+                    data_loc,
+                    # 4 4 4
+                    data_loc + np.asarray([-h_shift, 0]),
+                    data_loc + np.asarray([-h_shift, v_shift]),
+                    data_loc + np.asarray([0, v_shift]),
+                ]
+
+                ret = Path(ax.transData.transform(path), [1, 4, 4, 4, 4, 4, 4])
+            # if not, fall back to the user specified behavior
+            else:
+                ret = base_connection_style(posA, posB, *args, **kwargs)
+
+            return ret
+
+    else:
+        _connectionstyle = connectionstyle
+
+    # FancyArrowPatch doesn't handle color strings
+    arrow_colors = colorConverter.to_rgba_array(edge_color, alpha)
+    for i, (src, dst) in enumerate(edge_pos):
+        x1, y1 = src
+        x2, y2 = dst
+        shrink_source = 0  # space from source to tail
+        shrink_target = 0  # space from  head to target
+        if np.iterable(node_size):  # many node sizes
+            source, target = edgelist[i][:2]
+            source_node_size = node_size[nodelist.index(source)]
+            target_node_size = node_size[nodelist.index(target)]
+            shrink_source = to_marker_edge(source_node_size, node_shape)
+            shrink_target = to_marker_edge(target_node_size, node_shape)
+        else:
+            shrink_source = shrink_target = to_marker_edge(node_size, node_shape)
+
+        if shrink_source < min_source_margin:
+            shrink_source = min_source_margin
+
+        if shrink_target < min_target_margin:
+            shrink_target = min_target_margin
+
+        if len(arrow_colors) == len(edge_pos):
+            arrow_color = arrow_colors[i]
+        elif len(arrow_colors) == 1:
+            arrow_color = arrow_colors[0]
+        else:  # Cycle through colors
+            arrow_color = arrow_colors[i % len(arrow_colors)]
+
+        if np.iterable(width):
+            if len(width) == len(edge_pos):
+                line_width = width[i]
+            else:
+                line_width = width[i % len(width)]
+        else:
+            line_width = width
+
+        arrow = FancyArrowPatch(
+            (x1, y1),
+            (x2, y2),
+            arrowstyle=arrowstyle,
+            shrinkA=shrink_source,
+            shrinkB=shrink_target,
+            mutation_scale=mutation_scale,
+            color=arrow_color,
+            linewidth=line_width,
+            connectionstyle=_connectionstyle,
+            linestyle=style,
+            zorder=1,
+        )  # arrows go behind nodes
+
+        arrow_collection.append(arrow)
+        ax.add_patch(arrow)
+
+    # update view
     padx, pady = 0.05 * w, 0.05 * h
     corners = (minx - padx, miny - pady), (maxx + padx, maxy + pady)
     ax.update_datalim(corners)


Test patch:
diff --git a/networkx/drawing/tests/test_pylab.py b/networkx/drawing/tests/test_pylab.py
--- a/networkx/drawing/tests/test_pylab.py
+++ b/networkx/drawing/tests/test_pylab.py
@@ -205,7 +205,7 @@ def test_empty_graph(self):
 
     def test_draw_empty_nodes_return_values(self):
         # See Issue #3833
-        from matplotlib.collections import PathCollection, LineCollection
+        from matplotlib.collections import PathCollection
 
         G = nx.Graph([(1, 2), (2, 3)])
         DG = nx.DiGraph([(1, 2), (2, 3)])
@@ -213,16 +213,11 @@ def test_draw_empty_nodes_return_values(self):
         assert isinstance(nx.draw_networkx_nodes(G, pos, nodelist=[]), PathCollection)
         assert isinstance(nx.draw_networkx_nodes(DG, pos, nodelist=[]), PathCollection)
 
-        # drawing empty edges either return an empty LineCollection or empty list.
-        assert isinstance(
-            nx.draw_networkx_edges(G, pos, edgelist=[], arrows=True), LineCollection
-        )
-        assert isinstance(
-            nx.draw_networkx_edges(G, pos, edgelist=[], arrows=False), LineCollection
-        )
-        assert isinstance(
-            nx.draw_networkx_edges(DG, pos, edgelist=[], arrows=False), LineCollection
-        )
+        # drawing empty edges used to return an empty LineCollection or empty list.
+        # Now it is always an empty list (because edges are now lists of FancyArrows)
+        assert nx.draw_networkx_edges(G, pos, edgelist=[], arrows=True) == []
+        assert nx.draw_networkx_edges(G, pos, edgelist=[], arrows=False) == []
+        assert nx.draw_networkx_edges(DG, pos, edgelist=[], arrows=False) == []
         assert nx.draw_networkx_edges(DG, pos, edgelist=[], arrows=True) == []
 
     def test_multigraph_edgelist_tuples(self):