Repository: ManimCommunity/manim

Issue:
`self.play(Create(G))` connects edges to the centre of vertices unlike `self.add(G)` for Graphs
## Description of bug / unexpected behavior
<!-- Add a clear and concise description of the problem you encountered. -->
The edges connect to the center of vertices in `self.play(Create(G))` 

## Expected behavior
<!-- Add a clear and concise description of what you expected to happen. -->
I would expect it to output a graph similar to what happens in `self.add(G)`

## How to reproduce the issue
<!-- Provide a piece of code illustrating the undesired behavior. -->

<details><summary>Code for reproducing the problem</summary>

```py
from manim import *

class bugreport_expected(Scene):
    def construct(self):
        edges = [(1, 2), (1, 3), (2, 4), (2, 5), (2, 9), (3, 7), (3, 8), (4, 9), (9, 10), (5, 6), (6, 7), (6, 8), (7, 8), (8, 10), (10, 1)]
        vertices = list(range(1,11))
        vertex_config = {}
        for i in vertices:
            vertex_config[i] = {'radius': 0.2, 'color': WHITE}
        edge_config = {}
        for i in edges:
            edge_config[i] = {'stroke_width': 3, 'tip_length': 0.2, 'color': WHITE}
        labels = {}
        labels_text = "abcdefghij"
        for i in vertices:
            labels[i] = Tex(r''+labels_text[i-1], color=BLACK)
        G = Graph(vertices, edges, layout="circular", vertex_config=vertex_config, edge_config=edge_config, edge_type=Arrow, labels = labels)
        self.add(G)

class bugreport_actual(Scene):
    def construct(self):
        edges = [(1, 2), (1, 3), (2, 4), (2, 5), (2, 9), (3, 7), (3, 8), (4, 9), (9, 10), (5, 6), (6, 7), (6, 8), (7, 8), (8, 10), (10, 1)]
        vertices = list(range(1,11))
        vertex_config = {}
        for i in vertices:
            vertex_config[i] = {'radius': 0.2, 'color': WHITE}
        edge_config = {}
        for i in edges:
            edge_config[i] = {'stroke_width': 3, 'tip_length': 0.2, 'color': WHITE}
        labels = {}
        labels_text = "abcdefghij"
        for i in vertices:
            labels[i] = Tex(r''+labels_text[i-1], color=BLACK)
        G = Graph(vertices, edges, layout="circular", vertex_config=vertex_config, edge_config=edge_config, edge_type=Arrow, labels = labels)
        self.play(Create(G))
```

</details>


## Additional media files
<!-- Paste in the files manim produced on rendering the code above. -->

<details><summary>Images/GIFs</summary>

<!-- PASTE MEDIA HERE -->
![bugreport_ManimCE_v0 15 2](https://user-images.githubusercontent.com/5823408/174984366-4dc7d747-ab25-4b62-82ec-391828e968ee.png)


https://user-images.githubusercontent.com/5823408/174984342-1fbc91c4-5f1d-4f95-afa8-23dccc8f24f7.mp4



</details>


## Logs
<details><summary>Terminal output</summary>
<!-- Add "-v DEBUG" when calling manim to generate more detailed logs -->

```
PASTE HERE OR PROVIDE LINK TO https://pastebin.com/ OR SIMILAR
```

<!-- Insert screenshots here (only when absolutely necessary, we prefer copy/pasted output!) -->

</details>


## System specifications

<details><summary>System Details</summary>

- OS (with version, e.g., Windows 10 v2004 or macOS 10.15 (Catalina)): Mac OS 12
- RAM: 8GB
- Python version (`python/py/python3 --version`): 3.10
- Installed modules (provide output from `pip list`): 
```
PASTE HERE
```
</details>

<details><summary>LaTeX details</summary>

+ LaTeX distribution (e.g. TeX Live 2020): TeX Live 2021
+ Installed LaTeX packages:
<!-- output of `tlmgr list --only-installed` for TeX Live or a screenshot of the Packages page for MikTeX -->
</details>

<details><summary>FFMPEG</summary>

Output of `ffmpeg -version`:

```
PASTE HERE
```
</details>

## Additional comments
<!-- Add further context that you think might be relevant for this issue here. -->



Discussion:
This is a problem with how the graph is updated so that if vertices are moved, the endpoints of the edges move too -- if you do not care about moving vertices, you can run `G.clear_updaters()` before creating the graph, that should help.
Thank you @behackl for the solution, it works.
But I don't understand where the vertices have moved during the construction of the graph.
Is it perhaps that the vertices are added at the center as usual when running `Create(G)` while `add(G)` directly adds it at the desired position, and thus resulting in a discrepancy in the endpoint location for the edges?
They did not move: the `Graph` has an updater function attached which ensures that the endpoints of the edges match the vertices. This function is not called when you render a static image, but when rendering a video, the function is called at the end of every rendered frame. 
Thank you @noamzaks for the fix. I didn't expect it to be just a function call difference. But it seems like there are issues with the tests related to the 3D view of the vertices. Have you figured out any solution?
Hi. Sorry, not yet. I'll try again today. I tried to create the supposed npz result and compare the framed, but without success
Hey, small update; I found out some "workaround", but if you need this for something right now, you can try to use my WIP fix and make sure it looks good in your case
Thank you. But I have already finished my presentation with a static graph, so not in any immediate need. I will wait for the next release.

Resolving patch:
diff --git a/manim/mobject/graph.py b/manim/mobject/graph.py
--- a/manim/mobject/graph.py
+++ b/manim/mobject/graph.py
@@ -4,6 +4,7 @@
 
 __all__ = [
     "Graph",
+    "DiGraph",
 ]
 
 import itertools as it
@@ -13,21 +14,20 @@
 import networkx as nx
 import numpy as np
 
+from manim.animation.composition import AnimationGroup
+from manim.animation.creation import Create, Uncreate
 from manim.mobject.geometry.arc import Dot, LabeledDot
 from manim.mobject.geometry.line import Line
+from manim.mobject.mobject import Mobject, override_animate
 from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
 from manim.mobject.opengl.opengl_mobject import OpenGLMobject
 from manim.mobject.text.tex_mobject import MathTex
-
-from ..animation.composition import AnimationGroup
-from ..animation.creation import Create, Uncreate
-from ..utils.color import BLACK
-from .mobject import Mobject, override_animate
-from .types.vectorized_mobject import VMobject
+from manim.mobject.types.vectorized_mobject import VMobject
+from manim.utils.color import BLACK
 
 
 def _determine_graph_layout(
-    nx_graph: nx.classes.graph.Graph,
+    nx_graph: nx.classes.graph.Graph | nx.classes.digraph.DiGraph,
     layout: str | dict = "spring",
     layout_scale: float = 2,
     layout_config: dict | None = None,
@@ -106,7 +106,7 @@ def _determine_graph_layout(
 
 
 def _tree_layout(
-    T: nx.classes.graph.Graph,
+    T: nx.classes.graph.Graph | nx.classes.digraph.DiGraph,
     root_vertex: Hashable | None,
     scale: float | tuple | None = 2,
     vertex_spacing: tuple | None = None,
@@ -213,18 +213,27 @@ def slide(v, dx):
     return {v: (np.array([x, y, 0]) - center) * sf for v, (x, y) in pos.items()}
 
 
-class Graph(VMobject, metaclass=ConvertToOpenGL):
-    """An undirected graph (that is, a collection of vertices connected with edges).
+class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
+    """Abstract base class for graphs (that is, a collection of vertices
+    connected with edges).
 
     Graphs can be instantiated by passing both a list of (distinct, hashable)
     vertex names, together with list of edges (as tuples of vertex names). See
-    the examples below for details.
+    the examples for concrete implementations of this class for details.
 
     .. note::
 
         This implementation uses updaters to make the edges move with
         the vertices.
 
+
+    See also
+    --------
+
+    :class:`.Graph`
+    :class:`.DiGraph`
+
+
     Parameters
     ----------
 
@@ -285,211 +294,6 @@ class Graph(VMobject, metaclass=ConvertToOpenGL):
         to the class specified via ``edge_type``, or a dictionary whose
         keys are the edges, and whose values are dictionaries containing
         keyword arguments for the mobject related to the corresponding edge.
-
-    Examples
-    --------
-
-    First, we create a small graph and demonstrate that the edges move
-    together with the vertices.
-
-    .. manim:: MovingVertices
-
-        class MovingVertices(Scene):
-            def construct(self):
-                vertices = [1, 2, 3, 4]
-                edges = [(1, 2), (2, 3), (3, 4), (1, 3), (1, 4)]
-                g = Graph(vertices, edges)
-                self.play(Create(g))
-                self.wait()
-                self.play(g[1].animate.move_to([1, 1, 0]),
-                          g[2].animate.move_to([-1, 1, 0]),
-                          g[3].animate.move_to([1, -1, 0]),
-                          g[4].animate.move_to([-1, -1, 0]))
-                self.wait()
-
-    There are several automatic positioning algorithms to choose from:
-
-    .. manim:: GraphAutoPosition
-        :save_last_frame:
-
-        class GraphAutoPosition(Scene):
-            def construct(self):
-                vertices = [1, 2, 3, 4, 5, 6, 7, 8]
-                edges = [(1, 7), (1, 8), (2, 3), (2, 4), (2, 5),
-                         (2, 8), (3, 4), (6, 1), (6, 2),
-                         (6, 3), (7, 2), (7, 4)]
-                autolayouts = ["spring", "circular", "kamada_kawai",
-                               "planar", "random", "shell",
-                               "spectral", "spiral"]
-                graphs = [Graph(vertices, edges, layout=lt).scale(0.5)
-                          for lt in autolayouts]
-                r1 = VGroup(*graphs[:3]).arrange()
-                r2 = VGroup(*graphs[3:6]).arrange()
-                r3 = VGroup(*graphs[6:]).arrange()
-                self.add(VGroup(r1, r2, r3).arrange(direction=DOWN))
-
-    Vertices can also be positioned manually:
-
-    .. manim:: GraphManualPosition
-        :save_last_frame:
-
-        class GraphManualPosition(Scene):
-            def construct(self):
-                vertices = [1, 2, 3, 4]
-                edges = [(1, 2), (2, 3), (3, 4), (4, 1)]
-                lt = {1: [0, 0, 0], 2: [1, 1, 0], 3: [1, -1, 0], 4: [-1, 0, 0]}
-                G = Graph(vertices, edges, layout=lt)
-                self.add(G)
-
-    The vertices in graphs can be labeled, and configurations for vertices
-    and edges can be modified both by default and for specific vertices and
-    edges.
-
-    .. note::
-
-        In ``edge_config``, edges can be passed in both directions: if
-        ``(u, v)`` is an edge in the graph, both ``(u, v)`` as well
-        as ``(v, u)`` can be used as keys in the dictionary.
-
-    .. manim:: LabeledModifiedGraph
-        :save_last_frame:
-
-        class LabeledModifiedGraph(Scene):
-            def construct(self):
-                vertices = [1, 2, 3, 4, 5, 6, 7, 8]
-                edges = [(1, 7), (1, 8), (2, 3), (2, 4), (2, 5),
-                         (2, 8), (3, 4), (6, 1), (6, 2),
-                         (6, 3), (7, 2), (7, 4)]
-                g = Graph(vertices, edges, layout="circular", layout_scale=3,
-                          labels=True, vertex_config={7: {"fill_color": RED}},
-                          edge_config={(1, 7): {"stroke_color": RED},
-                                       (2, 7): {"stroke_color": RED},
-                                       (4, 7): {"stroke_color": RED}})
-                self.add(g)
-
-    You can also lay out a partite graph on columns by specifying
-    a list of the vertices on each side and choosing the partite layout.
-
-    .. note::
-
-        All vertices in your graph which are not listed in any of the partitions
-        are collected in their own partition and rendered in the rightmost column.
-
-    .. manim:: PartiteGraph
-        :save_last_frame:
-
-        import networkx as nx
-
-        class PartiteGraph(Scene):
-            def construct(self):
-                G = nx.Graph()
-                G.add_nodes_from([0, 1, 2, 3])
-                G.add_edges_from([(0, 2), (0,3), (1, 2)])
-                graph = Graph(list(G.nodes), list(G.edges), layout="partite", partitions=[[0, 1]])
-                self.play(Create(graph))
-
-    The representation of a linear artificial neural network is facilitated
-    by the use of the partite layout and defining partitions for each layer.
-
-    .. manim:: LinearNN
-        :save_last_frame:
-
-        class LinearNN(Scene):
-            def construct(self):
-                edges = []
-                partitions = []
-                c = 0
-                layers = [2, 3, 3, 2]  # the number of neurons in each layer
-
-                for i in layers:
-                    partitions.append(list(range(c + 1, c + i + 1)))
-                    c += i
-                for i, v in enumerate(layers[1:]):
-                        last = sum(layers[:i+1])
-                        for j in range(v):
-                            for k in range(last - layers[i], last):
-                                edges.append((k + 1, j + last + 1))
-
-                vertices = np.arange(1, sum(layers) + 1)
-
-                graph = Graph(
-                    vertices,
-                    edges,
-                    layout='partite',
-                    partitions=partitions,
-                    layout_scale=3,
-                    vertex_config={'radius': 0.20},
-                )
-                self.add(graph)
-
-    The custom tree layout can be used to show the graph
-    by distance from the root vertex. You must pass the root vertex
-    of the tree.
-
-    .. manim:: Tree
-
-        import networkx as nx
-
-        class Tree(Scene):
-            def construct(self):
-                G = nx.Graph()
-
-                G.add_node("ROOT")
-
-                for i in range(5):
-                    G.add_node("Child_%i" % i)
-                    G.add_node("Grandchild_%i" % i)
-                    G.add_node("Greatgrandchild_%i" % i)
-
-                    G.add_edge("ROOT", "Child_%i" % i)
-                    G.add_edge("Child_%i" % i, "Grandchild_%i" % i)
-                    G.add_edge("Grandchild_%i" % i, "Greatgrandchild_%i" % i)
-
-                self.play(Create(
-                    Graph(list(G.nodes), list(G.edges), layout="tree", root_vertex="ROOT")))
-
-    The following code sample illustrates the use of the ``vertex_spacing``
-    layout parameter specific to the ``"tree"`` layout. As mentioned
-    above, setting ``vertex_spacing`` overrides the specified value
-    for ``layout_scale``, and as such it is harder to control the size
-    of the mobject. However, we can adjust the captured frame and
-    zoom out by using a :class:`.MovingCameraScene`::
-
-        class LargeTreeGeneration(MovingCameraScene):
-            DEPTH = 4
-            CHILDREN_PER_VERTEX = 3
-            LAYOUT_CONFIG = {"vertex_spacing": (0.5, 1)}
-            VERTEX_CONF = {"radius": 0.25, "color": BLUE_B, "fill_opacity": 1}
-
-            def expand_vertex(self, g, vertex_id: str, depth: int):
-                new_vertices = [f"{vertex_id}/{i}" for i in range(self.CHILDREN_PER_VERTEX)]
-                new_edges = [(vertex_id, child_id) for child_id in new_vertices]
-                g.add_edges(
-                    *new_edges,
-                    vertex_config=self.VERTEX_CONF,
-                    positions={
-                        k: g.vertices[vertex_id].get_center() + 0.1 * DOWN for k in new_vertices
-                    },
-                )
-                if depth < self.DEPTH:
-                    for child_id in new_vertices:
-                        self.expand_vertex(g, child_id, depth + 1)
-
-                return g
-
-            def construct(self):
-                g = Graph(["ROOT"], [], vertex_config=self.VERTEX_CONF)
-                g = self.expand_vertex(g, "ROOT", 1)
-                self.add(g)
-
-                self.play(
-                    g.animate.change_layout(
-                        "tree",
-                        root_vertex="ROOT",
-                        layout_config=self.LAYOUT_CONFIG,
-                    )
-                )
-                self.play(self.camera.auto_zoom(g, margin=1), run_time=0.5)
     """
 
     def __init__(
@@ -511,7 +315,7 @@ def __init__(
     ) -> None:
         super().__init__()
 
-        nx_graph = nx.Graph()
+        nx_graph = self._empty_networkx_graph()
         nx_graph.add_nodes_from(vertices)
         nx_graph.add_edges_from(edges)
         self._graph = nx_graph
@@ -564,48 +368,51 @@ def __init__(
         # build edge_config
         if edge_config is None:
             edge_config = {}
+        default_tip_config = {}
         default_edge_config = {}
         if edge_config:
+            default_tip_config = edge_config.pop("tip_config", {})
             default_edge_config = {
                 k: v
                 for k, v in edge_config.items()
-                if k not in edges and k[::-1] not in edges
+                if not isinstance(
+                    k, tuple
+                )  # everything that is not an edge is an option
             }
         self._edge_config = {}
+        self._tip_config = {}
         for e in edges:
             if e in edge_config:
+                self._tip_config[e] = edge_config[e].pop(
+                    "tip_config", copy(default_tip_config)
+                )
                 self._edge_config[e] = edge_config[e]
-            elif e[::-1] in edge_config:
-                self._edge_config[e] = edge_config[e[::-1]]
             else:
+                self._tip_config[e] = copy(default_tip_config)
                 self._edge_config[e] = copy(default_edge_config)
 
         self.default_edge_config = default_edge_config
-        self.edges = {
-            (u, v): edge_type(
-                self[u].get_center(),
-                self[v].get_center(),
-                z_index=-1,
-                **self._edge_config[(u, v)],
-            )
-            for (u, v) in edges
-        }
+        self._populate_edge_dict(edges, edge_type)
 
         self.add(*self.vertices.values())
         self.add(*self.edges.values())
 
-        def update_edges(graph):
-            for (u, v), edge in graph.edges.items():
-                edge.put_start_and_end_on(graph[u].get_center(), graph[v].get_center())
+        self.add_updater(self.update_edges)
+
+    @staticmethod
+    def _empty_networkx_graph():
+        """Return an empty networkx graph for the given graph type."""
+        raise NotImplementedError("To be implemented in concrete subclasses")
 
-        self.add_updater(update_edges)
+    def _populate_edge_dict(
+        self, edges: list[tuple[Hashable, Hashable]], edge_type: type[Mobject]
+    ):
+        """Helper method for populating the edges of the graph."""
+        raise NotImplementedError("To be implemented in concrete subclasses")
 
     def __getitem__(self: Graph, v: Hashable) -> Mobject:
         return self.vertices[v]
 
-    def __repr__(self: Graph) -> str:
-        return f"Graph on {len(self.vertices)} vertices and {len(self.edges)} edges"
-
     def _create_vertex(
         self,
         vertex: Hashable,
@@ -903,7 +710,7 @@ def remove_vertices(self, *vertices):
             >>> removed = G.remove_vertices(2, 3); removed
             VGroup(Line, Line, Dot, Dot)
             >>> G
-            Graph on 1 vertices and 0 edges
+            Undirected graph on 1 vertices and 0 edges
 
         """
         mobjects = []
@@ -1063,9 +870,7 @@ def _remove_edge(self, edge: tuple[Hashable]):
 
         """
         if edge not in self.edges:
-            edge = edge[::-1]
-            if edge not in self.edges:
-                raise ValueError(f"The graph does not contain a edge '{edge}'")
+            raise ValueError(f"The graph does not contain a edge '{edge}'")
 
         edge_mobject = self.edges.pop(edge)
 
@@ -1102,15 +907,18 @@ def _remove_edges_animation(self, *edges, anim_args=None):
         mobjects = self.remove_edges(*edges)
         return AnimationGroup(*(animation(mobj, **anim_args) for mobj in mobjects))
 
-    @staticmethod
-    def from_networkx(nxgraph: nx.classes.graph.Graph, **kwargs) -> Graph:
-        """Build a :class:`~.Graph` from a given ``networkx`` graph.
+    @classmethod
+    def from_networkx(
+        cls, nxgraph: nx.classes.graph.Graph | nx.classes.digraph.DiGraph, **kwargs
+    ):
+        """Build a :class:`~.Graph` or :class:`~.DiGraph` from a
+        given ``networkx`` graph.
 
         Parameters
         ----------
 
         nxgraph
-            A ``networkx`` graph.
+            A ``networkx`` graph or digraph.
         **kwargs
             Keywords to be passed to the constructor of :class:`~.Graph`.
 
@@ -1133,7 +941,7 @@ def construct(self):
                     self.play(Uncreate(G))
 
         """
-        return Graph(list(nxgraph.nodes), list(nxgraph.edges), **kwargs)
+        return cls(list(nxgraph.nodes), list(nxgraph.edges), **kwargs)
 
     def change_layout(
         self,
@@ -1174,3 +982,522 @@ def construct(self):
         for v in self.vertices:
             self[v].move_to(self._layout[v])
         return self
+
+
+class Graph(GenericGraph):
+    """An undirected graph (vertices connected with edges).
+
+    The graph comes with an updater which makes the edges stick to
+    the vertices when moved around. See :class:`.DiGraph` for
+    a version with directed edges.
+
+    See also
+    --------
+
+    :class:`.GenericGraph`
+
+    Parameters
+    ----------
+
+    vertices
+        A list of vertices. Must be hashable elements.
+    edges
+        A list of edges, specified as tuples ``(u, v)`` where both ``u``
+        and ``v`` are vertices. The vertex order is irrelevant.
+    labels
+        Controls whether or not vertices are labeled. If ``False`` (the default),
+        the vertices are not labeled; if ``True`` they are labeled using their
+        names (as specified in ``vertices``) via :class:`~.MathTex`. Alternatively,
+        custom labels can be specified by passing a dictionary whose keys are
+        the vertices, and whose values are the corresponding vertex labels
+        (rendered via, e.g., :class:`~.Text` or :class:`~.Tex`).
+    label_fill_color
+        Sets the fill color of the default labels generated when ``labels``
+        is set to ``True``. Has no effect for other values of ``labels``.
+    layout
+        Either one of ``"spring"`` (the default), ``"circular"``, ``"kamada_kawai"``,
+        ``"planar"``, ``"random"``, ``"shell"``, ``"spectral"``, ``"spiral"``, ``"tree"``, and ``"partite"``
+        for automatic vertex positioning using ``networkx``
+        (see `their documentation <https://networkx.org/documentation/stable/reference/drawing.html#module-networkx.drawing.layout>`_
+        for more details), or a dictionary specifying a coordinate (value)
+        for each vertex (key) for manual positioning.
+    layout_config
+        Only for automatically generated layouts. A dictionary whose entries
+        are passed as keyword arguments to the automatic layout algorithm
+        specified via ``layout`` of ``networkx``.
+        The ``tree`` layout also accepts a special parameter ``vertex_spacing``
+        passed as a keyword argument inside the ``layout_config`` dictionary.
+        Passing a tuple ``(space_x, space_y)`` as this argument overrides
+        the value of ``layout_scale`` and ensures that vertices are arranged
+        in a way such that the centers of siblings in the same layer are
+        at least ``space_x`` units apart horizontally, and neighboring layers
+        are spaced ``space_y`` units vertically.
+    layout_scale
+        The scale of automatically generated layouts: the vertices will
+        be arranged such that the coordinates are located within the
+        interval ``[-scale, scale]``. Some layouts accept a tuple ``(scale_x, scale_y)``
+        causing the first coordinate to be in the interval ``[-scale_x, scale_x]``,
+        and the second in ``[-scale_y, scale_y]``. Default: 2.
+    vertex_type
+        The mobject class used for displaying vertices in the scene.
+    vertex_config
+        Either a dictionary containing keyword arguments to be passed to
+        the class specified via ``vertex_type``, or a dictionary whose keys
+        are the vertices, and whose values are dictionaries containing keyword
+        arguments for the mobject related to the corresponding vertex.
+    vertex_mobjects
+        A dictionary whose keys are the vertices, and whose values are
+        mobjects to be used as vertices. Passing vertices here overrides
+        all other configuration options for a vertex.
+    edge_type
+        The mobject class used for displaying edges in the scene.
+    edge_config
+        Either a dictionary containing keyword arguments to be passed
+        to the class specified via ``edge_type``, or a dictionary whose
+        keys are the edges, and whose values are dictionaries containing
+        keyword arguments for the mobject related to the corresponding edge.
+
+
+    Examples
+    --------
+
+    First, we create a small graph and demonstrate that the edges move
+    together with the vertices.
+
+    .. manim:: MovingVertices
+
+        class MovingVertices(Scene):
+            def construct(self):
+                vertices = [1, 2, 3, 4]
+                edges = [(1, 2), (2, 3), (3, 4), (1, 3), (1, 4)]
+                g = Graph(vertices, edges)
+                self.play(Create(g))
+                self.wait()
+                self.play(g[1].animate.move_to([1, 1, 0]),
+                          g[2].animate.move_to([-1, 1, 0]),
+                          g[3].animate.move_to([1, -1, 0]),
+                          g[4].animate.move_to([-1, -1, 0]))
+                self.wait()
+
+    There are several automatic positioning algorithms to choose from:
+
+    .. manim:: GraphAutoPosition
+        :save_last_frame:
+
+        class GraphAutoPosition(Scene):
+            def construct(self):
+                vertices = [1, 2, 3, 4, 5, 6, 7, 8]
+                edges = [(1, 7), (1, 8), (2, 3), (2, 4), (2, 5),
+                         (2, 8), (3, 4), (6, 1), (6, 2),
+                         (6, 3), (7, 2), (7, 4)]
+                autolayouts = ["spring", "circular", "kamada_kawai",
+                               "planar", "random", "shell",
+                               "spectral", "spiral"]
+                graphs = [Graph(vertices, edges, layout=lt).scale(0.5)
+                          for lt in autolayouts]
+                r1 = VGroup(*graphs[:3]).arrange()
+                r2 = VGroup(*graphs[3:6]).arrange()
+                r3 = VGroup(*graphs[6:]).arrange()
+                self.add(VGroup(r1, r2, r3).arrange(direction=DOWN))
+
+    Vertices can also be positioned manually:
+
+    .. manim:: GraphManualPosition
+        :save_last_frame:
+
+        class GraphManualPosition(Scene):
+            def construct(self):
+                vertices = [1, 2, 3, 4]
+                edges = [(1, 2), (2, 3), (3, 4), (4, 1)]
+                lt = {1: [0, 0, 0], 2: [1, 1, 0], 3: [1, -1, 0], 4: [-1, 0, 0]}
+                G = Graph(vertices, edges, layout=lt)
+                self.add(G)
+
+    The vertices in graphs can be labeled, and configurations for vertices
+    and edges can be modified both by default and for specific vertices and
+    edges.
+
+    .. note::
+
+        In ``edge_config``, edges can be passed in both directions: if
+        ``(u, v)`` is an edge in the graph, both ``(u, v)`` as well
+        as ``(v, u)`` can be used as keys in the dictionary.
+
+    .. manim:: LabeledModifiedGraph
+        :save_last_frame:
+
+        class LabeledModifiedGraph(Scene):
+            def construct(self):
+                vertices = [1, 2, 3, 4, 5, 6, 7, 8]
+                edges = [(1, 7), (1, 8), (2, 3), (2, 4), (2, 5),
+                         (2, 8), (3, 4), (6, 1), (6, 2),
+                         (6, 3), (7, 2), (7, 4)]
+                g = Graph(vertices, edges, layout="circular", layout_scale=3,
+                          labels=True, vertex_config={7: {"fill_color": RED}},
+                          edge_config={(1, 7): {"stroke_color": RED},
+                                       (2, 7): {"stroke_color": RED},
+                                       (4, 7): {"stroke_color": RED}})
+                self.add(g)
+
+    You can also lay out a partite graph on columns by specifying
+    a list of the vertices on each side and choosing the partite layout.
+
+    .. note::
+
+        All vertices in your graph which are not listed in any of the partitions
+        are collected in their own partition and rendered in the rightmost column.
+
+    .. manim:: PartiteGraph
+        :save_last_frame:
+
+        import networkx as nx
+
+        class PartiteGraph(Scene):
+            def construct(self):
+                G = nx.Graph()
+                G.add_nodes_from([0, 1, 2, 3])
+                G.add_edges_from([(0, 2), (0,3), (1, 2)])
+                graph = Graph(list(G.nodes), list(G.edges), layout="partite", partitions=[[0, 1]])
+                self.play(Create(graph))
+
+    The representation of a linear artificial neural network is facilitated
+    by the use of the partite layout and defining partitions for each layer.
+
+    .. manim:: LinearNN
+        :save_last_frame:
+
+        class LinearNN(Scene):
+            def construct(self):
+                edges = []
+                partitions = []
+                c = 0
+                layers = [2, 3, 3, 2]  # the number of neurons in each layer
+
+                for i in layers:
+                    partitions.append(list(range(c + 1, c + i + 1)))
+                    c += i
+                for i, v in enumerate(layers[1:]):
+                        last = sum(layers[:i+1])
+                        for j in range(v):
+                            for k in range(last - layers[i], last):
+                                edges.append((k + 1, j + last + 1))
+
+                vertices = np.arange(1, sum(layers) + 1)
+
+                graph = Graph(
+                    vertices,
+                    edges,
+                    layout='partite',
+                    partitions=partitions,
+                    layout_scale=3,
+                    vertex_config={'radius': 0.20},
+                )
+                self.add(graph)
+
+    The custom tree layout can be used to show the graph
+    by distance from the root vertex. You must pass the root vertex
+    of the tree.
+
+    .. manim:: Tree
+
+        import networkx as nx
+
+        class Tree(Scene):
+            def construct(self):
+                G = nx.Graph()
+
+                G.add_node("ROOT")
+
+                for i in range(5):
+                    G.add_node("Child_%i" % i)
+                    G.add_node("Grandchild_%i" % i)
+                    G.add_node("Greatgrandchild_%i" % i)
+
+                    G.add_edge("ROOT", "Child_%i" % i)
+                    G.add_edge("Child_%i" % i, "Grandchild_%i" % i)
+                    G.add_edge("Grandchild_%i" % i, "Greatgrandchild_%i" % i)
+
+                self.play(Create(
+                    Graph(list(G.nodes), list(G.edges), layout="tree", root_vertex="ROOT")))
+
+    The following code sample illustrates the use of the ``vertex_spacing``
+    layout parameter specific to the ``"tree"`` layout. As mentioned
+    above, setting ``vertex_spacing`` overrides the specified value
+    for ``layout_scale``, and as such it is harder to control the size
+    of the mobject. However, we can adjust the captured frame and
+    zoom out by using a :class:`.MovingCameraScene`::
+
+        class LargeTreeGeneration(MovingCameraScene):
+            DEPTH = 4
+            CHILDREN_PER_VERTEX = 3
+            LAYOUT_CONFIG = {"vertex_spacing": (0.5, 1)}
+            VERTEX_CONF = {"radius": 0.25, "color": BLUE_B, "fill_opacity": 1}
+
+            def expand_vertex(self, g, vertex_id: str, depth: int):
+                new_vertices = [f"{vertex_id}/{i}" for i in range(self.CHILDREN_PER_VERTEX)]
+                new_edges = [(vertex_id, child_id) for child_id in new_vertices]
+                g.add_edges(
+                    *new_edges,
+                    vertex_config=self.VERTEX_CONF,
+                    positions={
+                        k: g.vertices[vertex_id].get_center() + 0.1 * DOWN for k in new_vertices
+                    },
+                )
+                if depth < self.DEPTH:
+                    for child_id in new_vertices:
+                        self.expand_vertex(g, child_id, depth + 1)
+
+                return g
+
+            def construct(self):
+                g = Graph(["ROOT"], [], vertex_config=self.VERTEX_CONF)
+                g = self.expand_vertex(g, "ROOT", 1)
+                self.add(g)
+
+                self.play(
+                    g.animate.change_layout(
+                        "tree",
+                        root_vertex="ROOT",
+                        layout_config=self.LAYOUT_CONFIG,
+                    )
+                )
+                self.play(self.camera.auto_zoom(g, margin=1), run_time=0.5)
+    """
+
+    @staticmethod
+    def _empty_networkx_graph() -> nx.Graph:
+        return nx.Graph()
+
+    def _populate_edge_dict(
+        self, edges: list[tuple[Hashable, Hashable]], edge_type: type[Mobject]
+    ):
+        self.edges = {
+            (u, v): edge_type(
+                self[u].get_center(),
+                self[v].get_center(),
+                z_index=-1,
+                **self._edge_config[(u, v)],
+            )
+            for (u, v) in edges
+        }
+
+    def update_edges(self, graph):
+        for (u, v), edge in graph.edges.items():
+            # Undirected graph has a Line edge
+            edge.put_start_and_end_on(graph[u].get_center(), graph[v].get_center())
+
+    def __repr__(self: Graph) -> str:
+        return f"Undirected graph on {len(self.vertices)} vertices and {len(self.edges)} edges"
+
+
+class DiGraph(GenericGraph):
+    """A directed graph.
+
+    .. note::
+
+        In contrast to undirected graphs, the order in which vertices in a given
+        edge are specified is relevant here.
+
+    See also
+    --------
+
+    :class:`.GenericGraph`
+
+    Parameters
+    ----------
+
+    vertices
+        A list of vertices. Must be hashable elements.
+    edges
+        A list of edges, specified as tuples ``(u, v)`` where both ``u``
+        and ``v`` are vertices. The edge is directed from ``u`` to ``v``.
+    labels
+        Controls whether or not vertices are labeled. If ``False`` (the default),
+        the vertices are not labeled; if ``True`` they are labeled using their
+        names (as specified in ``vertices``) via :class:`~.MathTex`. Alternatively,
+        custom labels can be specified by passing a dictionary whose keys are
+        the vertices, and whose values are the corresponding vertex labels
+        (rendered via, e.g., :class:`~.Text` or :class:`~.Tex`).
+    label_fill_color
+        Sets the fill color of the default labels generated when ``labels``
+        is set to ``True``. Has no effect for other values of ``labels``.
+    layout
+        Either one of ``"spring"`` (the default), ``"circular"``, ``"kamada_kawai"``,
+        ``"planar"``, ``"random"``, ``"shell"``, ``"spectral"``, ``"spiral"``, ``"tree"``, and ``"partite"``
+        for automatic vertex positioning using ``networkx``
+        (see `their documentation <https://networkx.org/documentation/stable/reference/drawing.html#module-networkx.drawing.layout>`_
+        for more details), or a dictionary specifying a coordinate (value)
+        for each vertex (key) for manual positioning.
+    layout_config
+        Only for automatically generated layouts. A dictionary whose entries
+        are passed as keyword arguments to the automatic layout algorithm
+        specified via ``layout`` of ``networkx``.
+        The ``tree`` layout also accepts a special parameter ``vertex_spacing``
+        passed as a keyword argument inside the ``layout_config`` dictionary.
+        Passing a tuple ``(space_x, space_y)`` as this argument overrides
+        the value of ``layout_scale`` and ensures that vertices are arranged
+        in a way such that the centers of siblings in the same layer are
+        at least ``space_x`` units apart horizontally, and neighboring layers
+        are spaced ``space_y`` units vertically.
+    layout_scale
+        The scale of automatically generated layouts: the vertices will
+        be arranged such that the coordinates are located within the
+        interval ``[-scale, scale]``. Some layouts accept a tuple ``(scale_x, scale_y)``
+        causing the first coordinate to be in the interval ``[-scale_x, scale_x]``,
+        and the second in ``[-scale_y, scale_y]``. Default: 2.
+    vertex_type
+        The mobject class used for displaying vertices in the scene.
+    vertex_config
+        Either a dictionary containing keyword arguments to be passed to
+        the class specified via ``vertex_type``, or a dictionary whose keys
+        are the vertices, and whose values are dictionaries containing keyword
+        arguments for the mobject related to the corresponding vertex.
+    vertex_mobjects
+        A dictionary whose keys are the vertices, and whose values are
+        mobjects to be used as vertices. Passing vertices here overrides
+        all other configuration options for a vertex.
+    edge_type
+        The mobject class used for displaying edges in the scene.
+    edge_config
+        Either a dictionary containing keyword arguments to be passed
+        to the class specified via ``edge_type``, or a dictionary whose
+        keys are the edges, and whose values are dictionaries containing
+        keyword arguments for the mobject related to the corresponding edge.
+        You can further customize the tip by adding a ``tip_config`` dictionary
+        for global styling, or by adding the dict to a specific ``edge_config``.
+
+    Examples
+    --------
+
+    .. manim:: MovingDiGraph
+
+        class MovingDiGraph(Scene):
+            def construct(self):
+                vertices = [1, 2, 3, 4]
+                edges = [(1, 2), (2, 3), (3, 4), (1, 3), (1, 4)]
+
+                g = DiGraph(vertices, edges)
+
+                self.add(g)
+                self.play(
+                    g[1].animate.move_to([1, 1, 1]),
+                    g[2].animate.move_to([-1, 1, 2]),
+                    g[3].animate.move_to([1, -1, -1]),
+                    g[4].animate.move_to([-1, -1, 0]),
+                )
+                self.wait()
+
+    You can customize the edges and arrow tips globally or locally.
+
+    .. manim:: CustomDiGraph
+
+        class CustomDiGraph(Scene):
+            def construct(self):
+                vertices = [i for i in range(5)]
+                edges = [
+                    (0, 1),
+                    (1, 2),
+                    (3, 2),
+                    (3, 4),
+                ]
+
+                edge_config = {
+                    "stroke_width": 2,
+                    "tip_config": {
+                        "tip_shape": ArrowSquareTip,
+                        "tip_length": 0.15,
+                    },
+                    (3, 4): {
+                        "color": RED,
+                        "tip_config": {"tip_length": 0.25, "tip_width": 0.25}
+                    },
+                }
+
+                g = DiGraph(
+                    vertices,
+                    edges,
+                    labels=True,
+                    layout="circular",
+                    edge_config=edge_config,
+                ).scale(1.4)
+
+                self.play(Create(g))
+                self.wait()
+
+    Since this implementation respects the labels boundary you can also use
+    it for an undirected moving graph with labels.
+
+    .. manim:: UndirectedMovingDiGraph
+
+        class UndirectedMovingDiGraph(Scene):
+            def construct(self):
+                vertices = [i for i in range(5)]
+                edges = [
+                    (0, 1),
+                    (1, 2),
+                    (3, 2),
+                    (3, 4),
+                ]
+
+                edge_config = {
+                    "stroke_width": 2,
+                    "tip_config": {"tip_length": 0, "tip_width": 0},
+                    (3, 4): {"color": RED},
+                }
+
+                g = DiGraph(
+                    vertices,
+                    edges,
+                    labels=True,
+                    layout="circular",
+                    edge_config=edge_config,
+                ).scale(1.4)
+
+                self.play(Create(g))
+                self.wait()
+
+                self.play(
+                    g[1].animate.move_to([1, 1, 1]),
+                    g[2].animate.move_to([-1, 1, 2]),
+                    g[3].animate.move_to([-1.5, -1.5, -1]),
+                    g[4].animate.move_to([1, -2, -1]),
+                )
+                self.wait()
+
+    """
+
+    @staticmethod
+    def _empty_networkx_graph() -> nx.DiGraph:
+        return nx.DiGraph()
+
+    def _populate_edge_dict(
+        self, edges: list[tuple[Hashable, Hashable]], edge_type: type[Mobject]
+    ):
+        self.edges = {
+            (u, v): edge_type(
+                self[u],
+                self[v],
+                z_index=-1,
+                **self._edge_config[(u, v)],
+            )
+            for (u, v) in edges
+        }
+
+        for (u, v), edge in self.edges.items():
+            edge.add_tip(**self._tip_config[(u, v)])
+
+    def update_edges(self, graph):
+        """Updates the edges to stick at their corresponding vertices.
+
+        Arrow tips need to be repositioned since otherwise they can be
+        deformed.
+        """
+        for (u, v), edge in graph.edges.items():
+            edge_type = type(edge)
+            tip = edge.pop_tips()[0]
+            new_edge = edge_type(self[u], self[v], **self._edge_config[(u, v)])
+            edge.become(new_edge)
+            edge.add_tip(tip)
+
+    def __repr__(self: DiGraph) -> str:
+        return f"Directed graph on {len(self.vertices)} vertices and {len(self.edges)} edges"


Test patch:
diff --git a/tests/module/mobject/test_graph.py b/tests/module/mobject/test_graph.py
--- a/tests/module/mobject/test_graph.py
+++ b/tests/module/mobject/test_graph.py
@@ -1,6 +1,6 @@
 from __future__ import annotations
 
-from manim import Graph, Scene, Text, tempconfig
+from manim import DiGraph, Graph, Scene, Text, tempconfig
 
 
 def test_graph_creation():
@@ -8,17 +8,19 @@ def test_graph_creation():
     edges = [(1, 2), (2, 3), (3, 4), (4, 1)]
     layout = {1: [0, 0, 0], 2: [1, 1, 0], 3: [1, -1, 0], 4: [-1, 0, 0]}
     G_manual = Graph(vertices=vertices, edges=edges, layout=layout)
-    assert str(G_manual) == "Graph on 4 vertices and 4 edges"
+    assert str(G_manual) == "Undirected graph on 4 vertices and 4 edges"
     G_spring = Graph(vertices=vertices, edges=edges)
-    assert str(G_spring) == "Graph on 4 vertices and 4 edges"
+    assert str(G_spring) == "Undirected graph on 4 vertices and 4 edges"
+    G_directed = DiGraph(vertices=vertices, edges=edges)
+    assert str(G_directed) == "Directed graph on 4 vertices and 4 edges"
 
 
 def test_graph_add_vertices():
     G = Graph([1, 2, 3], [(1, 2), (2, 3)])
     G.add_vertices(4)
-    assert str(G) == "Graph on 4 vertices and 2 edges"
+    assert str(G) == "Undirected graph on 4 vertices and 2 edges"
     G.add_vertices(5, labels={5: Text("5")})
-    assert str(G) == "Graph on 5 vertices and 2 edges"
+    assert str(G) == "Undirected graph on 5 vertices and 2 edges"
     assert 5 in G._labels
     assert 5 in G._vertex_config
     G.add_vertices(6, 7, 8)
@@ -30,12 +32,12 @@ def test_graph_remove_vertices():
     G = Graph([1, 2, 3, 4, 5], [(1, 2), (2, 3), (3, 4), (4, 5)])
     removed_mobjects = G.remove_vertices(3)
     assert len(removed_mobjects) == 3
-    assert str(G) == "Graph on 4 vertices and 2 edges"
+    assert str(G) == "Undirected graph on 4 vertices and 2 edges"
     assert list(G.vertices.keys()) == [1, 2, 4, 5]
     assert list(G.edges.keys()) == [(1, 2), (4, 5)]
     removed_mobjects = G.remove_vertices(4, 5)
     assert len(removed_mobjects) == 3
-    assert str(G) == "Graph on 2 vertices and 1 edges"
+    assert str(G) == "Undirected graph on 2 vertices and 1 edges"
     assert list(G.vertices.keys()) == [1, 2]
     assert list(G.edges.keys()) == [(1, 2)]
 
@@ -44,19 +46,19 @@ def test_graph_add_edges():
     G = Graph([1, 2, 3, 4, 5], [(1, 2), (2, 3)])
     added_mobjects = G.add_edges((1, 3))
     assert str(added_mobjects.submobjects) == "[Line]"
-    assert str(G) == "Graph on 5 vertices and 3 edges"
+    assert str(G) == "Undirected graph on 5 vertices and 3 edges"
     assert set(G.vertices.keys()) == {1, 2, 3, 4, 5}
     assert set(G.edges.keys()) == {(1, 2), (2, 3), (1, 3)}
 
     added_mobjects = G.add_edges((1, 42))
     assert str(added_mobjects.submobjects) == "[Dot, Line]"
-    assert str(G) == "Graph on 6 vertices and 4 edges"
+    assert str(G) == "Undirected graph on 6 vertices and 4 edges"
     assert set(G.vertices.keys()) == {1, 2, 3, 4, 5, 42}
     assert set(G.edges.keys()) == {(1, 2), (2, 3), (1, 3), (1, 42)}
 
     added_mobjects = G.add_edges((4, 5), (5, 6), (6, 7))
     assert len(added_mobjects) == 5
-    assert str(G) == "Graph on 8 vertices and 7 edges"
+    assert str(G) == "Undirected graph on 8 vertices and 7 edges"
     assert set(G.vertices.keys()) == {1, 2, 3, 4, 5, 42, 6, 7}
     assert set(G._graph.nodes()) == set(G.vertices.keys())
     assert set(G.edges.keys()) == {
@@ -75,13 +77,13 @@ def test_graph_remove_edges():
     G = Graph([1, 2, 3, 4, 5], [(1, 2), (2, 3), (3, 4), (4, 5), (1, 5)])
     removed_mobjects = G.remove_edges((1, 2))
     assert str(removed_mobjects.submobjects) == "[Line]"
-    assert str(G) == "Graph on 5 vertices and 4 edges"
+    assert str(G) == "Undirected graph on 5 vertices and 4 edges"
     assert set(G.edges.keys()) == {(2, 3), (3, 4), (4, 5), (1, 5)}
     assert set(G._graph.edges()) == set(G.edges.keys())
 
-    removed_mobjects = G.remove_edges((2, 3), (3, 4), (4, 5), (5, 1))
+    removed_mobjects = G.remove_edges((2, 3), (3, 4), (4, 5), (1, 5))
     assert len(removed_mobjects) == 4
-    assert str(G) == "Graph on 5 vertices and 0 edges"
+    assert str(G) == "Undirected graph on 5 vertices and 0 edges"
     assert set(G._graph.edges()) == set()
     assert set(G.edges.keys()) == set()
 
@@ -93,8 +95,8 @@ def test_custom_animation_mobject_list():
     assert scene.mobjects == [G]
     with tempconfig({"dry_run": True, "quality": "low_quality"}):
         scene.play(G.animate.add_vertices(4))
-        assert str(G) == "Graph on 4 vertices and 2 edges"
+        assert str(G) == "Undirected graph on 4 vertices and 2 edges"
         assert scene.mobjects == [G]
         scene.play(G.animate.remove_vertices(2))
-        assert str(G) == "Graph on 3 vertices and 0 edges"
+        assert str(G) == "Undirected graph on 3 vertices and 0 edges"
         assert scene.mobjects == [G]
diff --git a/tests/opengl/test_graph_opengl.py b/tests/opengl/test_graph_opengl.py
--- a/tests/opengl/test_graph_opengl.py
+++ b/tests/opengl/test_graph_opengl.py
@@ -92,7 +92,7 @@ def test_graph_remove_edges(using_opengl_renderer):
     assert set(G.edges.keys()) == {(2, 3), (3, 4), (4, 5), (1, 5)}
     assert set(G._graph.edges()) == set(G.edges.keys())
 
-    removed_mobjects = G.remove_edges((2, 3), (3, 4), (4, 5), (5, 1))
+    removed_mobjects = G.remove_edges((2, 3), (3, 4), (4, 5), (1, 5))
     assert len(removed_mobjects) == 4
     assert len(G.vertices) == 5
     assert len(G.edges) == 0