Repository: networkx/networkx

Issue:
draw_network_edges allows curved edges or dashed edges, but not both.
Hi folks,

I would very much like to create curved, dashed  edges for either directed or undirected graphs.

However, it seems I can only have curved, **but not dashed** edges for directed graphs, even though I specify `style="dashed"`
```python
G = nx.DiGraph([('X','Y')])
pos = nx.spring_layout( G )
nx.draw_networkx_nodes(G, pos,
                       nodelist=list(G.nodes))
nx.draw_networkx_edges(G, pos,
                       edgelist=list(G.edges),
                       style='dashed',
                       connectionstyle="arc3,rad=0.3",
                      )
plt.savefig('solid-curved-directed-edge.png')
```

![solid-curved-directed-edge](https://user-images.githubusercontent.com/144210/56476144-5354cc80-6448-11e9-94ef-28c311ad9dac.png)


Furthermore, it seems I can only get dashed, **but not curved**, edges for undirected graphs. 
even though I specify `connectionstyle="arc3,rad=0.3"`

```python
G = nx.Graph([('X','Y')])
pos = nx.spring_layout( G )
nx.draw_networkx_nodes(G, pos,
                       nodelist=list(G.nodes))
nx.draw_networkx_edges(G, pos,
                       edgelist=list(G.edges),
                       style='dashed',
                       connectionstyle="arc3,rad=0.3",
                      )
plt.savefig('dashed-straight-undirected-edge.png')
```

![dashed-straight-undirected-edge](https://user-images.githubusercontent.com/144210/56476149-610a5200-6448-11e9-9dfd-061270f0c235.png)

Please advise.

Thanks so much,

Jeremy


Discussion:

Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 13930673a..cbdeb8b48 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -54,6 +54,7 @@ jobs:
             pip install --upgrade pip wheel setuptools
             pip install -r requirements.txt
             pip install -r requirements/extra.txt
+            pip install -r requirements/example-requirements.txt
             pip install -r requirements/example.txt
             pip install -U -r requirements/doc.txt
 
diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml
index 146b16959..27c5c891a 100644
--- a/.github/workflows/deploy-docs.yml
+++ b/.github/workflows/deploy-docs.yml
@@ -30,7 +30,7 @@ jobs:
     - name: Mayavi setup
       run: |
         sudo apt-get --no-install-recommends install -y libxkbcommon-x11-0 optipng libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 xcb libxcb-xfixes0 libxcb-xinerama0 libxcb-shape0
-        echo "export DISPLAY=:99" >> $BASH_ENV
+        export DISPLAY=:99
         /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1400x200x24 -ac +extension GLX +render -noreset;
 
     - name: Install packages
@@ -38,6 +38,7 @@ jobs:
         pip install --upgrade pip wheel setuptools
         pip install -r requirements.txt
         pip install -r requirements/extra.txt
+        pip install -r requirements/example-requirements.txt
         pip install -r requirements/example.txt
         pip install -U -r requirements/doc.txt
         export CPLUS_INCLUDE_PATH=/usr/include/gdal
@@ -48,9 +49,7 @@ jobs:
 
     - name: Build docs
       run: |
-        make -C doc/ html
-        make -C doc/ latexpdf LATEXOPTS="-file-line-error -halt-on-error"
-        cp -a doc/build/latex/networkx_reference.pdf doc/build/html/_downloads/.
+        echo "To do"
 
     - name: Deploy docs
       run: |
diff --git a/.travis.yml b/.travis.yml
index 7e6bd2d34..607435f61 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -50,6 +50,7 @@ after_success:
       pip install --upgrade pip wheel setuptools;
       pip install -r requirements.txt;
       pip install -r requirements/extra.txt;
+      pip install -r requirements/example-requirements.txt;
       pip install -r requirements/example.txt;
       pip install -U -r requirements/doc.txt;
       export CPLUS_INCLUDE_PATH=/usr/include/gdal;
diff --git a/doc/developer/deprecations.rst b/doc/developer/deprecations.rst
index 22bbf5372..ad647a00a 100644
--- a/doc/developer/deprecations.rst
+++ b/doc/developer/deprecations.rst
@@ -59,8 +59,9 @@ Version 3.0
 * In ``linalg/algebraicconnectivity.py`` remove ``_CholeskySolver`` and related code.
 * In ``convert_matrix.py`` remove ``to_numpy_matrix`` and ``from_numpy_matrix``.
 * In ``readwrite/json_graph/cytoscape.py``, change function signature for
-  ``cytoscape_graph`` and ``cytoscape_data`` to replace the ``attrs`` keyword
+  ``cytoscape_graph`` and ``cytoscape_data`` to replace the ``attrs`` keyword.
   argument with explicit ``name`` and ``ident`` keyword args.
 * Remove ``networkx/readwrite/nx_yaml.py`` and related tests.
-* Remove ``networkx/readwrite/gpickle.py`` and related tests. 
-* Remove ``networkx/readwrite/nx_shp.py`` and related tests (add info in alternatives). 
+* Remove ``networkx/readwrite/gpickle.py`` and related tests.
+* Remove ``networkx/readwrite/nx_shp.py`` and related tests (add info in alternatives).
+* Remove ``copy`` method in the coreview Filtered-related classes and related tests.
diff --git a/doc/release/release_dev.rst b/doc/release/release_dev.rst
index 02ecea975..c6622ad37 100644
--- a/doc/release/release_dev.rst
+++ b/doc/release/release_dev.rst
@@ -36,6 +36,14 @@ API Changes
   In `to_*_array/matrix`, nodes in nodelist but not in G now raise an exception.
   Use G.add_nodes_from(nodelist) to add them to G before converting.
 
+- [`#4360  <https://github.com/networkx/networkx/pull/4360>`_]
+  Internally `.nx_pylab.draw_networkx_edges` now always generates a
+  list of `matplotlib.patches.FancyArrowPatch` rather than using
+  a `matplotlib.collections.LineCollection` for un-directed graphs.  This
+  unifies interface for all types of graphs.  In
+  addition to the API change this may cause a performance regression for
+  large graphs.
+
 Deprecations
 ------------
 
@@ -53,6 +61,9 @@ Deprecations
   Deprecate ``read_gpickle`` and ``write_gpickle``.
 - [`#4298 <https://github.com/networkx/networkx/pull/4298>`_]
   Deprecate ``read_shp``, ``edges_from_line``, and ``write_shp``.
+- [`#4355 <https://github.com/networkx/networkx/pull/4355>`_]
+  Deprecate ``copy`` method in the coreview Filtered-related classes.
+
 
 Contributors to this release
 ----------------------------
diff --git a/doc/tutorial.rst b/doc/tutorial.rst
index 1de851a7f..4d4fd2972 100644
--- a/doc/tutorial.rst
+++ b/doc/tutorial.rst
@@ -131,6 +131,24 @@ At this stage the graph ``G`` consists of 8 nodes and 3 edges, as can be seen by
     >>> G.number_of_edges()
     3
 
+.. note:: 
+   
+   The order of adjacency reporting (e.g., G.adj, G.successors,
+   G.predecessors) is the order of edge addition. However, 
+   the order of G.edges is the order of the adjacencies
+   which includes both the order of the nodes and each 
+   node's adjacencies. See example below:
+
+.. nbplot::
+
+    >>> G = nx.DiGraph()
+    >>> G.add_edge(2, 1)   # adds the nodes in order 2, 1
+    >>> G.add_edge(1, 3)
+    >>> G.add_edge(2, 4)
+    >>> G.add_edge(1, 2)
+    >>> assert list(G.successors(2)) == [1, 4]
+    >>> assert list(G.edges) == [(2, 1), (2, 4), (1, 3), (1, 2)]
+
 Examining elements of a graph
 -----------------------------
 
diff --git a/networkx/algorithms/triads.py b/networkx/algorithms/triads.py
index cf48932b3..8269b3f7d 100644
--- a/networkx/algorithms/triads.py
+++ b/networkx/algorithms/triads.py
@@ -130,16 +130,19 @@ def _tricode(G, v, u, w):
 
 
 @not_implemented_for("undirected")
-def triadic_census(G):
+def triadic_census(G, nodelist=None):
     """Determines the triadic census of a directed graph.
 
     The triadic census is a count of how many of the 16 possible types of
-    triads are present in a directed graph.
+    triads are present in a directed graph. If a list of nodes is passed, then
+    only those triads are taken into account which have elements of nodelist in them.
 
     Parameters
     ----------
     G : digraph
        A NetworkX DiGraph
+    nodelist : list
+        List of nodes for which you want to calculate triadic census
 
     Returns
     -------
@@ -168,7 +171,9 @@ def triadic_census(G):
     n = len(G)
     # m = dict(zip(G, range(n)))
     m = {v: i for i, v in enumerate(G)}
-    for v in G:
+    if nodelist is None:
+        nodelist = list(G.nodes())
+    for v in nodelist:
         vnbrs = set(G.pred[v]) | set(G.succ[v])
         for u in vnbrs:
             if m[u] <= m[v]:
@@ -186,11 +191,24 @@ def triadic_census(G):
                 ):
                     code = _tricode(G, v, u, w)
                     census[TRICODE_TO_NAME[code]] += 1
-    # null triads = total number of possible triads - all found triads
-    #
-    # Use integer division here, since we know this formula guarantees an
-    # integral value.
-    census["003"] = ((n * (n - 1) * (n - 2)) // 6) - sum(census.values())
+
+    if len(nodelist) != len(G):
+        census["003"] = 0
+        for v in nodelist:
+            vnbrs = set(G.pred[v]) | set(G.succ[v])
+            not_vnbrs = G.nodes() - vnbrs - set(v)
+            triad_003_count = 0
+            for u in not_vnbrs:
+                unbrs = set(set(G.succ[u]) | set(G.pred[u])) - vnbrs
+                triad_003_count += len(not_vnbrs - unbrs) - 1
+            triad_003_count //= 2
+            census["003"] += triad_003_count
+    else:
+        # null triads = total number of possible triads - all found triads
+        #
+        # Use integer division here, since we know this formula guarantees an
+        # integral value.
+        census["003"] = ((n * (n - 1) * (n - 2)) // 6) - sum(census.values())
     return census
 
 
diff --git a/networkx/classes/coreviews.py b/networkx/classes/coreviews.py
index e22f6855a..f824e4539 100644
--- a/networkx/classes/coreviews.py
+++ b/networkx/classes/coreviews.py
@@ -1,5 +1,6 @@
 """
 """
+import warnings
 from collections.abc import Mapping
 
 __all__ = [
@@ -283,7 +284,17 @@ class FilterAtlas(Mapping):  # nodedict, nbrdict, keydict
             return self._atlas[key]
         raise KeyError(f"Key {key} not found")
 
+    # FIXME should this just be removed? we don't use it, but someone might
     def copy(self):
+        warnings.warn(
+            (
+                "FilterAtlas.copy is deprecated.\n"
+                "It will be removed in NetworkX 3.0.\n"
+                "Please open an Issue on https://github.com/networkx/networkx/issues\n"
+                "if you use this feature. We think that no one does use it."
+            ),
+            DeprecationWarning,
+        )
         try:  # check that NODE_OK has attr 'nodes'
             node_ok_shorter = 2 * len(self.NODE_OK.nodes) < len(self._atlas)
         except AttributeError:
@@ -326,7 +337,17 @@ class FilterAdjacency(Mapping):  # edgedict
             return FilterAtlas(self._atlas[node], new_node_ok)
         raise KeyError(f"Key {node} not found")
 
+    # FIXME should this just be removed? we don't use it, but someone might
     def copy(self):
+        warnings.warn(
+            (
+                "FilterAdjacency.copy is deprecated.\n"
+                "It will be removed in NetworkX 3.0.\n"
+                "Please open an Issue on https://github.com/networkx/networkx/issues\n"
+                "if you use this feature. We think that no one does use it."
+            ),
+            DeprecationWarning,
+        )
         try:  # check that NODE_OK has attr 'nodes'
             node_ok_shorter = 2 * len(self.NODE_OK.nodes) < len(self._atlas)
         except AttributeError:
@@ -384,7 +405,17 @@ class FilterMultiInner(FilterAdjacency):  # muliedge_seconddict
             return FilterAtlas(self._atlas[nbr], new_node_ok)
         raise KeyError(f"Key {nbr} not found")
 
+    # FIXME should this just be removed? we don't use it, but someone might
     def copy(self):
+        warnings.warn(
+            (
+                "FilterMultiInner.copy is deprecated.\n"
+                "It will be removed in NetworkX 3.0.\n"
+                "Please open an Issue on https://github.com/networkx/networkx/issues\n"
+                "if you use this feature. We think that no one does use it."
+            ),
+            DeprecationWarning,
+        )
         try:  # check that NODE_OK has attr 'nodes'
             node_ok_shorter = 2 * len(self.NODE_OK.nodes) < len(self._atlas)
         except AttributeError:
@@ -412,7 +443,17 @@ class FilterMultiAdjacency(FilterAdjacency):  # multiedgedict
             return FilterMultiInner(self._atlas[node], self.NODE_OK, edge_ok)
         raise KeyError(f"Key {node} not found")
 
+    # FIXME should this just be removed? we don't use it, but someone might
     def copy(self):
+        warnings.warn(
+            (
+                "FilterMultiAdjacency.copy is deprecated.\n"
+                "It will be removed in NetworkX 3.0.\n"
+                "Please open an Issue on https://github.com/networkx/networkx/issues\n"
+                "if you use this feature. We think that no one does use it."
+            ),
+            DeprecationWarning,
+        )
         try:  # check that NODE_OK has attr 'nodes'
             node_ok_shorter = 2 * len(self.NODE_OK.nodes) < len(self._atlas)
         except AttributeError:
diff --git a/networkx/drawing/nx_pylab.py b/networkx/drawing/nx_pylab.py
index 4b28baea7..212d00e70 100644
--- a/networkx/drawing/nx_pylab.py
+++ b/networkx/drawing/nx_pylab.py
@@ -25,6 +25,7 @@ from networkx.drawing.layout import (
     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)
diff --git a/networkx/utils/misc.py b/networkx/utils/misc.py
index 74cca6dd1..c3b1f0240 100644
--- a/networkx/utils/misc.py
+++ b/networkx/utils/misc.py
@@ -62,7 +62,7 @@ def flatten(obj, result=None):
             result.append(item)
         else:
             flatten(item, result)
-    return obj.__class__(result)
+    return tuple(result)
 
 
 def make_list_of_ints(sequence):
@@ -333,19 +333,18 @@ def create_random_state(random_state=None):
 
 
 class PythonRandomInterface:
-    try:
-
-        def __init__(self, rng=None):
+    def __init__(self, rng=None):
+        try:
             import numpy
+        except ImportError:
+            msg = "numpy not found, only random.random available."
+            warnings.warn(msg, ImportWarning)
 
-            if rng is None:
-                self._rng = numpy.random.mtrand._rand
+        if rng is None:
+            self._rng = numpy.random.mtrand._rand
+        else:
             self._rng = rng
 
-    except ImportError:
-        msg = "numpy not found, only random.random available."
-        warnings.warn(msg, ImportWarning)
-
     def random(self):
         return self._rng.random_sample()
 
diff --git a/requirements/example-requirements.txt b/requirements/example-requirements.txt
new file mode 100644
index 000000000..a55fb87db
--- /dev/null
+++ b/requirements/example-requirements.txt
@@ -0,0 +1,3 @@
+# For mayavi
+vtk>=9.0
+pyqt5>=5.15
diff --git a/requirements/example.txt b/requirements/example.txt
index 107696620..a1118222f 100644
--- a/requirements/example.txt
+++ b/requirements/example.txt
@@ -8,6 +8,4 @@ geopandas>=0.8
 osmnx>=0.16
 pysal>=2.3
 # For mayavi
-vtk>=9.0
-pyqt5>=5.15
 mayavi>=4.7


Test patch:
diff --git a/networkx/algorithms/shortest_paths/generic.py b/networkx/algorithms/shortest_paths/generic.py
index 97d3a530e..5a277e363 100644
--- a/networkx/algorithms/shortest_paths/generic.py
+++ b/networkx/algorithms/shortest_paths/generic.py
@@ -159,7 +159,7 @@ def shortest_path(G, source=None, target=None, weight=None, method="dijkstra"):
             if method == "unweighted":
                 paths = nx.bidirectional_shortest_path(G, source, target)
             elif method == "dijkstra":
-                paths = nx.dijkstra_path(G, source, target, weight)
+                _, paths = nx.bidirectional_dijkstra(G, source, target, weight)
             else:  # method == 'bellman-ford':
                 paths = nx.bellman_ford_path(G, source, target, weight)
     return paths
diff --git a/networkx/algorithms/tests/test_triads.py b/networkx/algorithms/tests/test_triads.py
index 9ad198d66..e0e45dfe1 100644
--- a/networkx/algorithms/tests/test_triads.py
+++ b/networkx/algorithms/tests/test_triads.py
@@ -137,3 +137,35 @@ def test_random_triad():
     G = G.to_directed()
     for i in range(100):
         assert nx.is_triad(nx.random_triad(G))
+
+
+def test_triadic_census_nodelist():
+    """Tests the triadic_census function."""
+    G = nx.DiGraph()
+    G.add_edges_from(["01", "02", "03", "04", "05", "12", "16", "51", "56", "65"])
+    expected = {
+        "030T": 2,
+        "120C": 1,
+        "210": 0,
+        "120U": 0,
+        "012": 9,
+        "102": 3,
+        "021U": 0,
+        "111U": 0,
+        "003": 8,
+        "030C": 0,
+        "021D": 9,
+        "201": 0,
+        "111D": 1,
+        "300": 0,
+        "120D": 0,
+        "021C": 2,
+    }
+    actual = {k: 0 for k in expected}
+    for node in G.nodes():
+        node_triad_census = nx.triadic_census(G, nodelist=[node])
+        for triad_key in expected:
+            actual[triad_key] += node_triad_census[triad_key]
+    # Divide the total count of 003 triads by 3, since we are counting them thrice
+    actual["003"] //= 3
+    assert expected == actual
diff --git a/networkx/classes/tests/test_coreviews.py b/networkx/classes/tests/test_coreviews.py
index c9b259a09..2d5fe82ee 100644
--- a/networkx/classes/tests/test_coreviews.py
+++ b/networkx/classes/tests/test_coreviews.py
@@ -315,10 +315,10 @@ class TestUnionMultiAdjacency(TestUnionAdjacency):
 class TestFilteredGraphs:
     def setup(self):
         self.Graphs = [nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph]
-        self.SubGraphs = [nx.graphviews.subgraph_view] * 4
 
     def test_hide_show_nodes(self):
-        for Graph, SubGraph in zip(self.Graphs, self.SubGraphs):
+        SubGraph = nx.graphviews.subgraph_view
+        for Graph in self.Graphs:
             G = nx.path_graph(4, Graph)
             SG = G.subgraph([2, 3])
             RG = SubGraph(G, nx.filters.hide_nodes([0, 1]))
@@ -330,7 +330,8 @@ class TestFilteredGraphs:
             assert SGC.edges == RGC.edges
 
     def test_str_repr(self):
-        for Graph, SubGraph in zip(self.Graphs, self.SubGraphs):
+        SubGraph = nx.graphviews.subgraph_view
+        for Graph in self.Graphs:
             G = nx.path_graph(4, Graph)
             SG = G.subgraph([2, 3])
             RG = SubGraph(G, nx.filters.hide_nodes([0, 1]))
@@ -344,13 +345,86 @@ class TestFilteredGraphs:
             repr(RG.adj[2])
 
     def test_copy(self):
-        for Graph, SubGraph in zip(self.Graphs, self.SubGraphs):
+        SubGraph = nx.graphviews.subgraph_view
+        for Graph in self.Graphs:
             G = nx.path_graph(4, Graph)
             SG = G.subgraph([2, 3])
             RG = SubGraph(G, nx.filters.hide_nodes([0, 1]))
+            RsG = SubGraph(G, nx.filters.show_nodes([2, 3]))
             assert G.adj.copy() == G.adj
             assert G.adj[2].copy() == G.adj[2]
             assert SG.adj.copy() == SG.adj
             assert SG.adj[2].copy() == SG.adj[2]
             assert RG.adj.copy() == RG.adj
             assert RG.adj[2].copy() == RG.adj[2]
+            assert RsG.adj.copy() == RsG.adj
+            assert RsG.adj[2].copy() == RsG.adj[2]
+
+    def test_filtered_copy(self):
+        # TODO: This function can be removed when filtered.copy()
+        # deprecation expires
+        SubGraph = nx.graphviews.subgraph_view
+        for Graph in self.Graphs:
+            G = nx.path_graph(4, Graph)
+            SG = G.subgraph([2, 3])
+            RG = SubGraph(G, nx.filters.hide_nodes([0, 1]))
+            RsG = SubGraph(G, nx.filters.show_nodes([2, 3]))
+            # test FilterAtlas & co in these subgraphs
+            assert SG._node.copy() == SG._node
+            assert SG.adj._atlas.copy() == SG.adj._atlas
+            assert SG.adj[2]._atlas.copy() == SG.adj[2]._atlas
+            assert SG.adj[2]._atlas[3].copy() == SG.adj[2]._atlas[3]
+            assert RG.adj._atlas.copy() == RG.adj._atlas
+            assert RG.adj[2]._atlas.copy() == RG.adj[2]._atlas
+            assert RG.adj[2]._atlas[3].copy() == RG.adj[2]._atlas[3]
+            assert RG._node.copy() == RG._node
+            assert RsG.adj._atlas.copy() == RsG.adj._atlas
+            assert RsG.adj[2]._atlas.copy() == RsG.adj[2]._atlas
+            assert RsG.adj[2]._atlas[3].copy() == RsG.adj[2]._atlas[3]
+            assert RsG._node.copy() == RsG._node
+            # test MultiFilterInner
+            if G.is_multigraph():
+                assert SG.adj[2]._atlas[3][0].copy() == SG.adj[2]._atlas[3][0]
+                assert RG.adj[2]._atlas[3][0].copy() == RG.adj[2]._atlas[3][0]
+                assert RsG.adj[2]._atlas[3][0].copy() == RsG.adj[2]._atlas[3][0]
+
+            # test deprecation
+            # FilterAtlas.copy()
+            pytest.deprecated_call(SG._node.copy)
+            # FilterAdjacency.copy()
+            pytest.deprecated_call(SG.adj._atlas.copy)
+            # FilterMultiAdjacency.copy()
+            if G.is_multigraph():
+                pytest.deprecated_call(SG.adj._atlas.copy)
+            # FilterMultiInner.copy()
+            if G.is_multigraph():
+                pytest.deprecated_call(SG.adj[2]._atlas.copy)
+
+            SSG = SG.subgraph([2])
+            assert list(SSG) == [2]
+
+            # check case when node_ok is small
+            G = nx.complete_graph(9, Graph)
+            SG = G.subgraph([2, 3])
+            RG = SubGraph(G, nx.filters.hide_nodes([0, 1]))
+            RsG = SubGraph(G, nx.filters.show_nodes([2, 3, 4, 5, 6, 7, 8]))
+            assert SG.adj._atlas.copy() == SG.adj._atlas
+            assert SG.adj[2]._atlas.copy() == SG.adj[2]._atlas
+            assert SG.adj[2]._atlas[3].copy() == SG.adj[2]._atlas[3]
+            assert SG._node.copy() == SG._node
+            assert RG.adj._atlas.copy() == RG.adj._atlas
+            assert RG.adj[2]._atlas.copy() == RG.adj[2]._atlas
+            assert RG.adj[2]._atlas[3].copy() == RG.adj[2]._atlas[3]
+            assert RG._node.copy() == RG._node
+            assert RsG.adj._atlas.copy() == RsG.adj._atlas
+            assert RsG.adj[2]._atlas.copy() == RsG.adj[2]._atlas
+            assert RsG.adj[2]._atlas[3].copy() == RsG.adj[2]._atlas[3]
+            assert RsG._node.copy() == RsG._node
+            # test MultiFilterInner
+            if G.is_multigraph():
+                assert SG.adj[2][3]._atlas.copy() == SG.adj[2][3]._atlas
+                assert RG.adj[2][3]._atlas.copy() == RG.adj[2][3]._atlas
+                assert RsG.adj[2][3]._atlas.copy() == RsG.adj[2][3]._atlas
+
+            SSG = SG.subgraph([2])
+            assert list(SSG) == [2]
diff --git a/networkx/drawing/tests/test_pylab.py b/networkx/drawing/tests/test_pylab.py
index 00b3dddf4..245c3363b 100644
--- a/networkx/drawing/tests/test_pylab.py
+++ b/networkx/drawing/tests/test_pylab.py
@@ -205,7 +205,7 @@ class TestPylab:
 
     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 @@ class TestPylab:
         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):
diff --git a/networkx/utils/tests/test_misc.py b/networkx/utils/tests/test_misc.py
index 3ecf2ef0b..b20648cb1 100644
--- a/networkx/utils/tests/test_misc.py
+++ b/networkx/utils/tests/test_misc.py
@@ -1,3 +1,5 @@
+from copy import copy
+
 import pytest
 import networkx as nx
 import random
@@ -8,6 +10,7 @@ from networkx.utils import (
     dict_to_numpy_array,
     dict_to_numpy_array1,
     dict_to_numpy_array2,
+    flatten,
     is_string_like,
     iterable,
     groups,
@@ -19,6 +22,56 @@ from networkx.utils import (
     to_tuple,
 )
 
+nested_depth = (
+    1,
+    2,
+    (
+        3,
+        4,
+        (
+            (5, 6, (7,), (8, (9, 10), 11), (12, 13, (14, 15)), 16),
+            17,
+        ),
+        18,
+        19,
+    ),
+    20,
+)
+
+nested_set = {
+    (1, 2, 3, 4),
+    (5, 6, 7, 8, 9),
+    (10, 11, (12, 13, 14), (15, 16, 17, 18)),
+    19,
+    20,
+}
+
+nested_mixed = [
+    1,
+    (2, 3, {4, (5, 6), 7}, [8, 9]),
+    {10: "foo", 11: "bar", (12, 13): "baz"},
+    {(14, 15): "qwe", 16: "asd"},
+    (17, (18, "19"), 20),
+]
+
+
+@pytest.mark.parametrize("result", [None, [], ["existing"], ["existing1", "existing2"]])
+@pytest.mark.parametrize(
+    "nested",
+    [nested_depth, nested_mixed, nested_set],
+)
+def test_flatten(nested, result):
+    if result is None:
+        val = flatten(nested, result)
+        assert len(val) == 20
+    else:
+        _result = copy(result)  # because pytest passes parameters as is
+        nexisting = len(_result)
+        val = flatten(nested, _result)
+        assert len(val) == len(_result) == 20 + nexisting
+
+    assert issubclass(type(val), tuple)
+
 
 def test_is_string_like():
     assert is_string_like("aaaa")