manim 0.17.0__py3-none-any.whl → 0.19.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. manim/__init__.py +11 -6
  2. manim/__main__.py +62 -19
  3. manim/_config/__init__.py +10 -9
  4. manim/_config/cli_colors.py +26 -9
  5. manim/_config/default.cfg +1 -3
  6. manim/_config/logger_utils.py +23 -13
  7. manim/_config/utils.py +662 -468
  8. manim/animation/animation.py +164 -18
  9. manim/animation/changing.py +34 -23
  10. manim/animation/composition.py +265 -67
  11. manim/animation/creation.py +208 -26
  12. manim/animation/fading.py +16 -18
  13. manim/animation/growing.py +35 -15
  14. manim/animation/indication.py +150 -76
  15. manim/animation/movement.py +56 -22
  16. manim/animation/numbers.py +64 -6
  17. manim/animation/rotation.py +78 -7
  18. manim/animation/specialized.py +6 -7
  19. manim/animation/speedmodifier.py +13 -10
  20. manim/animation/transform.py +14 -11
  21. manim/animation/transform_matching_parts.py +3 -4
  22. manim/animation/updaters/mobject_update_utils.py +152 -30
  23. manim/animation/updaters/update.py +10 -7
  24. manim/camera/camera.py +182 -118
  25. manim/camera/mapping_camera.py +34 -3
  26. manim/camera/moving_camera.py +95 -74
  27. manim/camera/multi_camera.py +23 -15
  28. manim/camera/three_d_camera.py +70 -52
  29. manim/cli/__init__.py +17 -0
  30. manim/cli/cfg/group.py +76 -44
  31. manim/cli/checkhealth/checks.py +192 -0
  32. manim/cli/checkhealth/commands.py +90 -0
  33. manim/cli/default_group.py +158 -25
  34. manim/cli/init/commands.py +33 -25
  35. manim/cli/plugins/commands.py +16 -3
  36. manim/cli/render/commands.py +72 -60
  37. manim/cli/render/ease_of_access_options.py +4 -3
  38. manim/cli/render/global_options.py +59 -17
  39. manim/cli/render/output_options.py +6 -5
  40. manim/cli/render/render_options.py +98 -33
  41. manim/constants.py +109 -59
  42. manim/data_structures.py +31 -0
  43. manim/mobject/frame.py +8 -5
  44. manim/mobject/geometry/__init__.py +1 -0
  45. manim/mobject/geometry/arc.py +277 -135
  46. manim/mobject/geometry/boolean_ops.py +32 -31
  47. manim/mobject/geometry/labeled.py +376 -0
  48. manim/mobject/geometry/line.py +192 -87
  49. manim/mobject/geometry/polygram.py +224 -58
  50. manim/mobject/geometry/shape_matchers.py +61 -25
  51. manim/mobject/geometry/tips.py +122 -48
  52. manim/mobject/graph.py +1027 -419
  53. manim/mobject/graphing/coordinate_systems.py +533 -278
  54. manim/mobject/graphing/functions.py +53 -32
  55. manim/mobject/graphing/number_line.py +123 -65
  56. manim/mobject/graphing/probability.py +88 -62
  57. manim/mobject/graphing/scale.py +33 -19
  58. manim/mobject/logo.py +118 -28
  59. manim/mobject/matrix.py +87 -83
  60. manim/mobject/mobject.py +912 -442
  61. manim/mobject/opengl/dot_cloud.py +16 -5
  62. manim/mobject/opengl/opengl_compatibility.py +4 -2
  63. manim/mobject/opengl/opengl_geometry.py +254 -153
  64. manim/mobject/opengl/opengl_image_mobject.py +3 -1
  65. manim/mobject/opengl/opengl_mobject.py +779 -482
  66. manim/mobject/opengl/opengl_point_cloud_mobject.py +41 -14
  67. manim/mobject/opengl/opengl_surface.py +14 -92
  68. manim/mobject/opengl/opengl_three_dimensions.py +12 -8
  69. manim/mobject/opengl/opengl_vectorized_mobject.py +98 -100
  70. manim/mobject/svg/brace.py +173 -41
  71. manim/mobject/svg/svg_mobject.py +139 -53
  72. manim/mobject/table.py +61 -68
  73. manim/mobject/text/code_mobject.py +193 -539
  74. manim/mobject/text/numbers.py +81 -34
  75. manim/mobject/text/tex_mobject.py +130 -78
  76. manim/mobject/text/text_mobject.py +288 -164
  77. manim/mobject/three_d/polyhedra.py +111 -13
  78. manim/mobject/three_d/three_d_utils.py +17 -8
  79. manim/mobject/three_d/three_dimensions.py +239 -106
  80. manim/mobject/types/image_mobject.py +50 -30
  81. manim/mobject/types/point_cloud_mobject.py +120 -75
  82. manim/mobject/types/vectorized_mobject.py +841 -408
  83. manim/mobject/value_tracker.py +105 -38
  84. manim/mobject/vector_field.py +50 -31
  85. manim/opengl/__init__.py +3 -3
  86. manim/plugins/__init__.py +14 -1
  87. manim/plugins/plugins_flags.py +10 -14
  88. manim/renderer/cairo_renderer.py +65 -50
  89. manim/renderer/opengl_renderer.py +89 -69
  90. manim/renderer/opengl_renderer_window.py +39 -18
  91. manim/renderer/shader.py +123 -87
  92. manim/renderer/shader_wrapper.py +44 -28
  93. manim/renderer/vectorized_mobject_rendering.py +38 -10
  94. manim/scene/moving_camera_scene.py +32 -3
  95. manim/scene/scene.py +507 -242
  96. manim/scene/scene_file_writer.py +371 -220
  97. manim/scene/section.py +20 -16
  98. manim/scene/three_d_scene.py +14 -22
  99. manim/scene/vector_space_scene.py +223 -129
  100. manim/scene/zoomed_scene.py +46 -41
  101. manim/typing.py +990 -0
  102. manim/utils/bezier.py +1823 -371
  103. manim/utils/caching.py +12 -5
  104. manim/utils/color/AS2700.py +236 -0
  105. manim/utils/color/BS381.py +318 -0
  106. manim/utils/color/DVIPSNAMES.py +96 -0
  107. manim/utils/color/SVGNAMES.py +179 -0
  108. manim/utils/color/X11.py +533 -0
  109. manim/utils/color/XKCD.py +952 -0
  110. manim/utils/color/__init__.py +61 -0
  111. manim/utils/color/core.py +1667 -0
  112. manim/utils/color/manim_colors.py +218 -0
  113. manim/utils/commands.py +48 -20
  114. manim/utils/config_ops.py +39 -19
  115. manim/utils/debug.py +8 -7
  116. manim/utils/deprecation.py +86 -39
  117. manim/utils/docbuild/__init__.py +17 -0
  118. manim/utils/docbuild/autoaliasattr_directive.py +236 -0
  119. manim/utils/docbuild/autocolor_directive.py +99 -0
  120. manim/utils/docbuild/manim_directive.py +94 -41
  121. manim/utils/docbuild/module_parsing.py +245 -0
  122. manim/utils/exceptions.py +6 -0
  123. manim/utils/family.py +5 -3
  124. manim/utils/family_ops.py +17 -4
  125. manim/utils/file_ops.py +27 -17
  126. manim/utils/hashing.py +55 -45
  127. manim/utils/images.py +13 -7
  128. manim/utils/ipython_magic.py +13 -7
  129. manim/utils/iterables.py +163 -120
  130. manim/utils/module_ops.py +66 -24
  131. manim/utils/opengl.py +77 -24
  132. manim/utils/parameter_parsing.py +32 -0
  133. manim/utils/paths.py +30 -33
  134. manim/utils/polylabel.py +235 -0
  135. manim/utils/qhull.py +218 -0
  136. manim/utils/rate_functions.py +98 -32
  137. manim/utils/simple_functions.py +25 -33
  138. manim/utils/sounds.py +7 -1
  139. manim/utils/space_ops.py +188 -115
  140. manim/utils/testing/__init__.py +17 -0
  141. manim/utils/testing/_frames_testers.py +13 -8
  142. manim/utils/testing/_show_diff.py +5 -3
  143. manim/utils/testing/_test_class_makers.py +34 -18
  144. manim/utils/testing/frames_comparison.py +37 -19
  145. manim/utils/tex.py +130 -198
  146. manim/utils/tex_file_writing.py +77 -47
  147. manim/utils/tex_templates.py +2 -1
  148. manim/utils/unit.py +6 -5
  149. {manim-0.17.0.dist-info → manim-0.19.1.dist-info}/METADATA +64 -65
  150. manim-0.19.1.dist-info/RECORD +220 -0
  151. {manim-0.17.0.dist-info → manim-0.19.1.dist-info}/WHEEL +1 -1
  152. manim-0.19.1.dist-info/entry_points.txt +3 -0
  153. {manim-0.17.0.dist-info → manim-0.19.1.dist-info/licenses}/LICENSE.community +1 -1
  154. manim/cli/new/group.py +0 -189
  155. manim/communitycolors.py +0 -9
  156. manim/gui/__init__.py +0 -0
  157. manim/gui/gui.py +0 -82
  158. manim/plugins/import_plugins.py +0 -43
  159. manim/utils/color.py +0 -552
  160. manim-0.17.0.dist-info/RECORD +0 -206
  161. manim-0.17.0.dist-info/entry_points.txt +0 -4
  162. /manim/cli/{new → checkhealth}/__init__.py +0 -0
  163. {manim-0.17.0.dist-info → manim-0.19.1.dist-info/licenses}/LICENSE +0 -0
manim/mobject/graph.py CHANGED
@@ -4,134 +4,341 @@ from __future__ import annotations
4
4
 
5
5
  __all__ = [
6
6
  "Graph",
7
+ "DiGraph",
7
8
  ]
8
9
 
9
10
  import itertools as it
11
+ from collections.abc import Hashable, Iterable, Sequence
10
12
  from copy import copy
11
- from typing import Hashable, Iterable
13
+ from typing import TYPE_CHECKING, Any, Literal, Protocol, cast
12
14
 
13
15
  import networkx as nx
14
16
  import numpy as np
15
17
 
18
+ if TYPE_CHECKING:
19
+ from typing_extensions import TypeAlias
20
+
21
+ from manim.scene.scene import Scene
22
+ from manim.typing import Point3D, Point3DLike
23
+
24
+ NxGraph: TypeAlias = nx.classes.graph.Graph | nx.classes.digraph.DiGraph
25
+
26
+ from manim.animation.composition import AnimationGroup
27
+ from manim.animation.creation import Create, Uncreate
16
28
  from manim.mobject.geometry.arc import Dot, LabeledDot
17
29
  from manim.mobject.geometry.line import Line
30
+ from manim.mobject.mobject import Mobject, override_animate
18
31
  from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
19
32
  from manim.mobject.opengl.opengl_mobject import OpenGLMobject
20
33
  from manim.mobject.text.tex_mobject import MathTex
34
+ from manim.mobject.types.vectorized_mobject import VMobject
35
+ from manim.utils.color import BLACK
21
36
 
22
- from ..animation.composition import AnimationGroup
23
- from ..animation.creation import Create, Uncreate
24
- from ..utils.color import BLACK
25
- from .mobject import Mobject, override_animate
26
- from .types.vectorized_mobject import VMobject
27
37
 
38
+ class LayoutFunction(Protocol):
39
+ """A protocol for automatic layout functions that compute a layout for a graph to be used in :meth:`~.Graph.change_layout`.
28
40
 
29
- def _determine_graph_layout(
30
- nx_graph: nx.classes.graph.Graph,
31
- layout: str | dict = "spring",
32
- layout_scale: float = 2,
33
- layout_config: dict | None = None,
34
- partitions: list[list[Hashable]] | None = None,
35
- root_vertex: Hashable | None = None,
36
- ) -> dict:
37
- automatic_layouts = {
38
- "circular": nx.layout.circular_layout,
39
- "kamada_kawai": nx.layout.kamada_kawai_layout,
40
- "planar": nx.layout.planar_layout,
41
- "random": nx.layout.random_layout,
42
- "shell": nx.layout.shell_layout,
43
- "spectral": nx.layout.spectral_layout,
44
- "partite": nx.layout.multipartite_layout,
45
- "tree": _tree_layout,
46
- "spiral": nx.layout.spiral_layout,
47
- "spring": nx.layout.spring_layout,
48
- }
49
-
50
- custom_layouts = ["random", "partite", "tree"]
41
+ .. note:: The layout function must be a pure function, i.e., it must not modify the graph passed to it.
51
42
 
52
- if layout_config is None:
53
- layout_config = {}
43
+ Examples
44
+ --------
54
45
 
55
- if isinstance(layout, dict):
56
- return layout
57
- elif layout in automatic_layouts and layout not in custom_layouts:
58
- auto_layout = automatic_layouts[layout](
59
- nx_graph, scale=layout_scale, **layout_config
60
- )
61
- # NetworkX returns a dictionary of 3D points if the dimension
62
- # is specified to be 3. Otherwise, it returns a dictionary of
63
- # 2D points, so adjusting is required.
64
- if layout_config.get("dim") == 3:
65
- return auto_layout
66
- else:
67
- return {k: np.append(v, [0]) for k, v in auto_layout.items()}
68
- elif layout == "tree":
69
- return _tree_layout(
70
- nx_graph, root_vertex=root_vertex, scale=layout_scale, **layout_config
71
- )
72
- elif layout == "partite":
73
- if partitions is None or len(partitions) == 0:
74
- raise ValueError(
75
- "The partite layout requires the 'partitions' parameter to contain the partition of the vertices",
76
- )
77
- partition_count = len(partitions)
78
- for i in range(partition_count):
79
- for v in partitions[i]:
80
- if nx_graph.nodes[v] is None:
81
- raise ValueError(
82
- "The partition must contain arrays of vertices in the graph",
83
- )
84
- nx_graph.nodes[v]["subset"] = i
85
- # Add missing vertices to their own side
86
- for v in nx_graph.nodes:
87
- if "subset" not in nx_graph.nodes[v]:
88
- nx_graph.nodes[v]["subset"] = partition_count
89
-
90
- auto_layout = automatic_layouts["partite"](
91
- nx_graph, scale=layout_scale, **layout_config
92
- )
93
- return {k: np.append(v, [0]) for k, v in auto_layout.items()}
94
- elif layout == "random":
95
- # the random layout places coordinates in [0, 1)
96
- # we need to rescale manually afterwards...
97
- auto_layout = automatic_layouts["random"](nx_graph, **layout_config)
98
- for k, v in auto_layout.items():
99
- auto_layout[k] = 2 * layout_scale * (v - np.array([0.5, 0.5]))
100
- return {k: np.append(v, [0]) for k, v in auto_layout.items()}
101
- else:
46
+ Here is an example that arranges nodes in an n x m grid in sorted order.
47
+
48
+ .. manim:: CustomLayoutExample
49
+ :save_last_frame:
50
+
51
+ class CustomLayoutExample(Scene):
52
+ def construct(self):
53
+ import numpy as np
54
+ import networkx as nx
55
+
56
+ # create custom layout
57
+ def custom_layout(
58
+ graph: nx.Graph,
59
+ scale: float | tuple[float, float, float] = 2,
60
+ n: int | None = None,
61
+ *args: Any,
62
+ **kwargs: Any,
63
+ ):
64
+ nodes = sorted(list(graph))
65
+ height = len(nodes) // n
66
+ return {
67
+ node: (scale * np.array([
68
+ (i % n) - (n-1)/2,
69
+ -(i // n) + height/2,
70
+ 0
71
+ ])) for i, node in enumerate(graph)
72
+ }
73
+
74
+ # draw graph
75
+ n = 4
76
+ graph = Graph(
77
+ [i for i in range(4 * 2 - 1)],
78
+ [(0, 1), (0, 4), (1, 2), (1, 5), (2, 3), (2, 6), (4, 5), (5, 6)],
79
+ labels=True,
80
+ layout=custom_layout,
81
+ layout_config={'n': n}
82
+ )
83
+ self.add(graph)
84
+
85
+ Several automatic layouts are provided by manim, and can be used by passing their name as the ``layout`` parameter to :meth:`~.Graph.change_layout`.
86
+ Alternatively, a custom layout function can be passed to :meth:`~.Graph.change_layout` as the ``layout`` parameter. Such a function must adhere to the :class:`~.LayoutFunction` protocol.
87
+
88
+ The :class:`~.LayoutFunction` s provided by manim are illustrated below:
89
+
90
+ - Circular Layout: places the vertices on a circle
91
+
92
+ .. manim:: CircularLayout
93
+ :save_last_frame:
94
+
95
+ class CircularLayout(Scene):
96
+ def construct(self):
97
+ graph = Graph(
98
+ [1, 2, 3, 4, 5, 6],
99
+ [(1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 1), (5, 1), (1, 3), (3, 5)],
100
+ layout="circular",
101
+ labels=True
102
+ )
103
+ self.add(graph)
104
+
105
+ - Kamada Kawai Layout: tries to place the vertices such that the given distances between them are respected
106
+
107
+ .. manim:: KamadaKawaiLayout
108
+ :save_last_frame:
109
+
110
+ class KamadaKawaiLayout(Scene):
111
+ def construct(self):
112
+ from collections import defaultdict
113
+ distances: dict[int, dict[int, float]] = defaultdict(dict)
114
+
115
+ # set desired distances
116
+ distances[1][2] = 1 # distance between vertices 1 and 2 is 1
117
+ distances[2][3] = 1 # distance between vertices 2 and 3 is 1
118
+ distances[3][4] = 2 # etc
119
+ distances[4][5] = 3
120
+ distances[5][6] = 5
121
+ distances[6][1] = 8
122
+
123
+ graph = Graph(
124
+ [1, 2, 3, 4, 5, 6],
125
+ [(1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 1)],
126
+ layout="kamada_kawai",
127
+ layout_config={"dist": distances},
128
+ layout_scale=4,
129
+ labels=True
130
+ )
131
+ self.add(graph)
132
+
133
+ - Partite Layout: places vertices into distinct partitions
134
+
135
+ .. manim:: PartiteLayout
136
+ :save_last_frame:
137
+
138
+ class PartiteLayout(Scene):
139
+ def construct(self):
140
+ graph = Graph(
141
+ [1, 2, 3, 4, 5, 6],
142
+ [(1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 1), (5, 1), (1, 3), (3, 5)],
143
+ layout="partite",
144
+ layout_config={"partitions": [[1,2],[3,4],[5,6]]},
145
+ labels=True
146
+ )
147
+ self.add(graph)
148
+
149
+ - Planar Layout: places vertices such that edges do not cross
150
+
151
+ .. manim:: PlanarLayout
152
+ :save_last_frame:
153
+
154
+ class PlanarLayout(Scene):
155
+ def construct(self):
156
+ graph = Graph(
157
+ [1, 2, 3, 4, 5, 6],
158
+ [(1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 1), (5, 1), (1, 3), (3, 5)],
159
+ layout="planar",
160
+ layout_scale=4,
161
+ labels=True
162
+ )
163
+ self.add(graph)
164
+
165
+ - Random Layout: randomly places vertices
166
+
167
+ .. manim:: RandomLayout
168
+ :save_last_frame:
169
+
170
+ class RandomLayout(Scene):
171
+ def construct(self):
172
+ graph = Graph(
173
+ [1, 2, 3, 4, 5, 6],
174
+ [(1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 1), (5, 1), (1, 3), (3, 5)],
175
+ layout="random",
176
+ labels=True
177
+ )
178
+ self.add(graph)
179
+
180
+ - Shell Layout: places vertices in concentric circles
181
+
182
+ .. manim:: ShellLayout
183
+ :save_last_frame:
184
+
185
+ class ShellLayout(Scene):
186
+ def construct(self):
187
+ nlist = [[1, 2, 3], [4, 5, 6, 7, 8, 9]]
188
+ graph = Graph(
189
+ [1, 2, 3, 4, 5, 6, 7, 8, 9],
190
+ [(1, 2), (2, 3), (3, 1), (4, 1), (4, 2), (5, 2), (6, 2), (6, 3), (7, 3), (8, 3), (8, 1), (9, 1)],
191
+ layout="shell",
192
+ layout_config={"nlist": nlist},
193
+ labels=True
194
+ )
195
+ self.add(graph)
196
+
197
+ - Spectral Layout: places vertices using the eigenvectors of the graph Laplacian (clusters nodes which are an approximation of the ratio cut)
198
+
199
+ .. manim:: SpectralLayout
200
+ :save_last_frame:
201
+
202
+ class SpectralLayout(Scene):
203
+ def construct(self):
204
+ graph = Graph(
205
+ [1, 2, 3, 4, 5, 6],
206
+ [(1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 1), (5, 1), (1, 3), (3, 5)],
207
+ layout="spectral",
208
+ labels=True
209
+ )
210
+ self.add(graph)
211
+
212
+ - Sprial Layout: places vertices in a spiraling pattern
213
+
214
+ .. manim:: SpiralLayout
215
+ :save_last_frame:
216
+
217
+ class SpiralLayout(Scene):
218
+ def construct(self):
219
+ graph = Graph(
220
+ [1, 2, 3, 4, 5, 6],
221
+ [(1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 1), (5, 1), (1, 3), (3, 5)],
222
+ layout="spiral",
223
+ labels=True
224
+ )
225
+ self.add(graph)
226
+
227
+ - Spring Layout: places nodes according to the Fruchterman-Reingold force-directed algorithm (attempts to minimize edge length while maximizing node separation)
228
+
229
+ .. manim:: SpringLayout
230
+ :save_last_frame:
231
+
232
+ class SpringLayout(Scene):
233
+ def construct(self):
234
+ graph = Graph(
235
+ [1, 2, 3, 4, 5, 6],
236
+ [(1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 1), (5, 1), (1, 3), (3, 5)],
237
+ layout="spring",
238
+ labels=True
239
+ )
240
+ self.add(graph)
241
+
242
+ - Tree Layout: places vertices into a tree with a root node and branches (can only be used with legal trees)
243
+
244
+ .. manim:: TreeLayout
245
+ :save_last_frame:
246
+
247
+ class TreeLayout(Scene):
248
+ def construct(self):
249
+ graph = Graph(
250
+ [1, 2, 3, 4, 5, 6, 7],
251
+ [(1, 2), (1, 3), (2, 4), (2, 5), (3, 6), (3, 7)],
252
+ layout="tree",
253
+ layout_config={"root_vertex": 1},
254
+ labels=True
255
+ )
256
+ self.add(graph)
257
+
258
+ """
259
+
260
+ def __call__(
261
+ self,
262
+ graph: NxGraph,
263
+ scale: float | tuple[float, float, float] = 2,
264
+ *args: Any,
265
+ **kwargs: Any,
266
+ ) -> dict[Hashable, Point3D]:
267
+ """Given a graph and a scale, return a dictionary of coordinates.
268
+
269
+ Parameters
270
+ ----------
271
+ graph
272
+ The underlying NetworkX graph to be laid out. DO NOT MODIFY.
273
+ scale
274
+ Either a single float value, or a tuple of three float values specifying the scale along each axis.
275
+
276
+ Returns
277
+ -------
278
+ dict[Hashable, Point3D]
279
+ A dictionary mapping vertices to their positions.
280
+ """
281
+ ...
282
+
283
+
284
+ def _partite_layout(
285
+ nx_graph: NxGraph,
286
+ scale: float = 2,
287
+ partitions: Sequence[Sequence[Hashable]] | None = None,
288
+ **kwargs: Any,
289
+ ) -> dict[Hashable, Point3D]:
290
+ if partitions is None or len(partitions) == 0:
102
291
  raise ValueError(
103
- f"The layout '{layout}' is neither a recognized automatic layout, "
104
- "nor a vertex placement dictionary.",
292
+ "The partite layout requires partitions parameter to contain the partition of the vertices",
105
293
  )
294
+ partition_count = len(partitions)
295
+ for i in range(partition_count):
296
+ for v in partitions[i]:
297
+ if nx_graph.nodes[v] is None:
298
+ raise ValueError(
299
+ "The partition must contain arrays of vertices in the graph",
300
+ )
301
+ nx_graph.nodes[v]["subset"] = i
302
+ # Add missing vertices to their own side
303
+ for v in nx_graph.nodes:
304
+ if "subset" not in nx_graph.nodes[v]:
305
+ nx_graph.nodes[v]["subset"] = partition_count
306
+
307
+ return nx.layout.multipartite_layout(nx_graph, scale=scale, **kwargs)
308
+
309
+
310
+ def _random_layout(nx_graph: NxGraph, scale: float = 2, **kwargs: Any):
311
+ # the random layout places coordinates in [0, 1)
312
+ # we need to rescale manually afterwards...
313
+ auto_layout = nx.layout.random_layout(nx_graph, **kwargs)
314
+ for k, v in auto_layout.items():
315
+ auto_layout[k] = 2 * scale * (v - np.array([0.5, 0.5]))
316
+ return {k: np.append(v, [0]) for k, v in auto_layout.items()}
106
317
 
107
318
 
108
319
  def _tree_layout(
109
- T: nx.classes.graph.Graph,
110
- root_vertex: Hashable | None,
320
+ T: NxGraph,
321
+ root_vertex: Hashable | None = None,
111
322
  scale: float | tuple | None = 2,
112
323
  vertex_spacing: tuple | None = None,
113
324
  orientation: str = "down",
114
325
  ):
115
- children = {root_vertex: list(T.neighbors(root_vertex))}
116
-
117
- if not nx.is_tree(T):
118
- raise ValueError("The tree layout must be used with trees")
119
326
  if root_vertex is None:
120
327
  raise ValueError("The tree layout requires the root_vertex parameter")
328
+ if not nx.is_tree(T):
329
+ raise ValueError("The tree layout must be used with trees")
121
330
 
331
+ children = {root_vertex: list(T.neighbors(root_vertex))}
122
332
  # The following code is SageMath's tree layout implementation, taken from
123
333
  # https://github.com/sagemath/sage/blob/cc60cfebc4576fed8b01f0fc487271bdee3cefed/src/sage/graphs/graph_plot.py#L1447
124
334
 
125
335
  # Always make a copy of the children because they get eaten
126
336
  stack = [list(children[root_vertex]).copy()]
127
337
  stick = [root_vertex]
128
- parent = {u: root_vertex for u in children[root_vertex]}
338
+ parent = dict.fromkeys(children[root_vertex], root_vertex)
129
339
  pos = {}
130
340
  obstruction = [0.0] * len(T)
131
- if orientation == "down":
132
- o = -1
133
- else:
134
- o = 1
341
+ o = -1 if orientation == "down" else 1
135
342
 
136
343
  def slide(v, dx):
137
344
  """
@@ -194,15 +401,9 @@ def _tree_layout(
194
401
  if isinstance(scale, (float, int)) and (width > 0 or height > 0):
195
402
  sf = 2 * scale / max(width, height)
196
403
  elif isinstance(scale, tuple):
197
- if scale[0] is not None and width > 0:
198
- sw = 2 * scale[0] / width
199
- else:
200
- sw = 1
404
+ sw = 2 * scale[0] / width if scale[0] is not None and width > 0 else 1
201
405
 
202
- if scale[1] is not None and height > 0:
203
- sh = 2 * scale[1] / height
204
- else:
205
- sh = 1
406
+ sh = 2 * scale[1] / height if scale[1] is not None and height > 0 else 1
206
407
 
207
408
  sf = np.array([sw, sh, 0])
208
409
  else:
@@ -213,18 +414,89 @@ def _tree_layout(
213
414
  return {v: (np.array([x, y, 0]) - center) * sf for v, (x, y) in pos.items()}
214
415
 
215
416
 
216
- class Graph(VMobject, metaclass=ConvertToOpenGL):
217
- """An undirected graph (that is, a collection of vertices connected with edges).
417
+ LayoutName = Literal[
418
+ "circular",
419
+ "kamada_kawai",
420
+ "partite",
421
+ "planar",
422
+ "random",
423
+ "shell",
424
+ "spectral",
425
+ "spiral",
426
+ "spring",
427
+ "tree",
428
+ ]
429
+
430
+ _layouts: dict[LayoutName, LayoutFunction] = {
431
+ "circular": cast(LayoutFunction, nx.layout.circular_layout),
432
+ "kamada_kawai": cast(LayoutFunction, nx.layout.kamada_kawai_layout),
433
+ "partite": cast(LayoutFunction, _partite_layout),
434
+ "planar": cast(LayoutFunction, nx.layout.planar_layout),
435
+ "random": cast(LayoutFunction, _random_layout),
436
+ "shell": cast(LayoutFunction, nx.layout.shell_layout),
437
+ "spectral": cast(LayoutFunction, nx.layout.spectral_layout),
438
+ "spiral": cast(LayoutFunction, nx.layout.spiral_layout),
439
+ "spring": cast(LayoutFunction, nx.layout.spring_layout),
440
+ "tree": cast(LayoutFunction, _tree_layout),
441
+ }
442
+
443
+
444
+ def _determine_graph_layout(
445
+ nx_graph: nx.classes.graph.Graph | nx.classes.digraph.DiGraph,
446
+ layout: LayoutName | dict[Hashable, Point3DLike] | LayoutFunction = "spring",
447
+ layout_scale: float | tuple[float, float, float] = 2,
448
+ layout_config: dict[str, Any] | None = None,
449
+ ) -> dict[Hashable, Point3DLike]:
450
+ if layout_config is None:
451
+ layout_config = {}
452
+
453
+ if isinstance(layout, dict):
454
+ return layout
455
+ elif layout in _layouts:
456
+ auto_layout = _layouts[layout](nx_graph, scale=layout_scale, **layout_config)
457
+ # NetworkX returns a dictionary of 3D points if the dimension
458
+ # is specified to be 3. Otherwise, it returns a dictionary of
459
+ # 2D points, so adjusting is required.
460
+ if (
461
+ layout_config.get("dim") == 3
462
+ or auto_layout[next(auto_layout.__iter__())].shape[0] == 3
463
+ ):
464
+ return auto_layout
465
+ else:
466
+ return {k: np.append(v, [0]) for k, v in auto_layout.items()}
467
+ else:
468
+ try:
469
+ return cast(LayoutFunction, layout)(
470
+ nx_graph, scale=layout_scale, **layout_config
471
+ )
472
+ except TypeError as e:
473
+ raise ValueError(
474
+ f"The layout '{layout}' is neither a recognized layout, a layout function,"
475
+ "nor a vertex placement dictionary.",
476
+ ) from e
477
+
478
+
479
+ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
480
+ """Abstract base class for graphs (that is, a collection of vertices
481
+ connected with edges).
218
482
 
219
483
  Graphs can be instantiated by passing both a list of (distinct, hashable)
220
484
  vertex names, together with list of edges (as tuples of vertex names). See
221
- the examples below for details.
485
+ the examples for concrete implementations of this class for details.
222
486
 
223
487
  .. note::
224
488
 
225
489
  This implementation uses updaters to make the edges move with
226
490
  the vertices.
227
491
 
492
+
493
+ See also
494
+ --------
495
+
496
+ :class:`.Graph`
497
+ :class:`.DiGraph`
498
+
499
+
228
500
  Parameters
229
501
  ----------
230
502
 
@@ -246,14 +518,14 @@ class Graph(VMobject, metaclass=ConvertToOpenGL):
246
518
  layout
247
519
  Either one of ``"spring"`` (the default), ``"circular"``, ``"kamada_kawai"``,
248
520
  ``"planar"``, ``"random"``, ``"shell"``, ``"spectral"``, ``"spiral"``, ``"tree"``, and ``"partite"``
249
- for automatic vertex positioning using ``networkx``
521
+ for automatic vertex positioning primarily using ``networkx``
250
522
  (see `their documentation <https://networkx.org/documentation/stable/reference/drawing.html#module-networkx.drawing.layout>`_
251
- for more details), or a dictionary specifying a coordinate (value)
252
- for each vertex (key) for manual positioning.
523
+ for more details), a dictionary specifying a coordinate (value)
524
+ for each vertex (key) for manual positioning, or a .:class:`~.LayoutFunction` with a user-defined automatic layout.
253
525
  layout_config
254
- Only for automatically generated layouts. A dictionary whose entries
255
- are passed as keyword arguments to the automatic layout algorithm
256
- specified via ``layout`` of``networkx``.
526
+ Only for automatic layouts. A dictionary whose entries
527
+ are passed as keyword arguments to the named layout or automatic layout function
528
+ specified via ``layout``.
257
529
  The ``tree`` layout also accepts a special parameter ``vertex_spacing``
258
530
  passed as a keyword argument inside the ``layout_config`` dictionary.
259
531
  Passing a tuple ``(space_x, space_y)`` as this argument overrides
@@ -280,244 +552,73 @@ class Graph(VMobject, metaclass=ConvertToOpenGL):
280
552
  all other configuration options for a vertex.
281
553
  edge_type
282
554
  The mobject class used for displaying edges in the scene.
555
+ Must be a subclass of :class:`~.Line` for default updaters to work.
283
556
  edge_config
284
557
  Either a dictionary containing keyword arguments to be passed
285
558
  to the class specified via ``edge_type``, or a dictionary whose
286
559
  keys are the edges, and whose values are dictionaries containing
287
560
  keyword arguments for the mobject related to the corresponding edge.
561
+ """
288
562
 
289
- Examples
290
- --------
563
+ def __init__(
564
+ self,
565
+ vertices: Sequence[Hashable],
566
+ edges: Sequence[tuple[Hashable, Hashable]],
567
+ labels: bool | dict = False,
568
+ label_fill_color: str = BLACK,
569
+ layout: LayoutName | dict[Hashable, Point3DLike] | LayoutFunction = "spring",
570
+ layout_scale: float | tuple[float, float, float] = 2,
571
+ layout_config: dict | None = None,
572
+ vertex_type: type[Mobject] = Dot,
573
+ vertex_config: dict | None = None,
574
+ vertex_mobjects: dict | None = None,
575
+ edge_type: type[Mobject] = Line,
576
+ partitions: Sequence[Sequence[Hashable]] | None = None,
577
+ root_vertex: Hashable | None = None,
578
+ edge_config: dict | None = None,
579
+ ) -> None:
580
+ super().__init__()
291
581
 
292
- First, we create a small graph and demonstrate that the edges move
293
- together with the vertices.
582
+ nx_graph = self._empty_networkx_graph()
583
+ nx_graph.add_nodes_from(vertices)
584
+ nx_graph.add_edges_from(edges)
585
+ self._graph = nx_graph
294
586
 
295
- .. manim:: MovingVertices
587
+ if isinstance(labels, dict):
588
+ self._labels = labels
589
+ elif isinstance(labels, bool):
590
+ if labels:
591
+ self._labels = {
592
+ v: MathTex(v, fill_color=label_fill_color) for v in vertices
593
+ }
594
+ else:
595
+ self._labels = {}
296
596
 
297
- class MovingVertices(Scene):
298
- def construct(self):
299
- vertices = [1, 2, 3, 4]
300
- edges = [(1, 2), (2, 3), (3, 4), (1, 3), (1, 4)]
301
- g = Graph(vertices, edges)
302
- self.play(Create(g))
303
- self.wait()
304
- self.play(g[1].animate.move_to([1, 1, 0]),
305
- g[2].animate.move_to([-1, 1, 0]),
306
- g[3].animate.move_to([1, -1, 0]),
307
- g[4].animate.move_to([-1, -1, 0]))
308
- self.wait()
597
+ if self._labels and vertex_type is Dot:
598
+ vertex_type = LabeledDot
309
599
 
310
- There are several automatic positioning algorithms to choose from:
600
+ if vertex_mobjects is None:
601
+ vertex_mobjects = {}
311
602
 
312
- .. manim:: GraphAutoPosition
313
- :save_last_frame:
603
+ # build vertex_config
604
+ if vertex_config is None:
605
+ vertex_config = {}
606
+ default_vertex_config = {}
607
+ if vertex_config:
608
+ default_vertex_config = {
609
+ k: v for k, v in vertex_config.items() if k not in vertices
610
+ }
611
+ self._vertex_config = {
612
+ v: vertex_config.get(v, copy(default_vertex_config)) for v in vertices
613
+ }
614
+ self.default_vertex_config = default_vertex_config
615
+ for v, label in self._labels.items():
616
+ self._vertex_config[v]["label"] = label
314
617
 
315
- class GraphAutoPosition(Scene):
316
- def construct(self):
317
- vertices = [1, 2, 3, 4, 5, 6, 7, 8]
318
- edges = [(1, 7), (1, 8), (2, 3), (2, 4), (2, 5),
319
- (2, 8), (3, 4), (6, 1), (6, 2),
320
- (6, 3), (7, 2), (7, 4)]
321
- autolayouts = ["spring", "circular", "kamada_kawai",
322
- "planar", "random", "shell",
323
- "spectral", "spiral"]
324
- graphs = [Graph(vertices, edges, layout=lt).scale(0.5)
325
- for lt in autolayouts]
326
- r1 = VGroup(*graphs[:3]).arrange()
327
- r2 = VGroup(*graphs[3:6]).arrange()
328
- r3 = VGroup(*graphs[6:]).arrange()
329
- self.add(VGroup(r1, r2, r3).arrange(direction=DOWN))
618
+ self.vertices = {v: vertex_type(**self._vertex_config[v]) for v in vertices}
619
+ self.vertices.update(vertex_mobjects)
330
620
 
331
- Vertices can also be positioned manually:
332
-
333
- .. manim:: GraphManualPosition
334
- :save_last_frame:
335
-
336
- class GraphManualPosition(Scene):
337
- def construct(self):
338
- vertices = [1, 2, 3, 4]
339
- edges = [(1, 2), (2, 3), (3, 4), (4, 1)]
340
- lt = {1: [0, 0, 0], 2: [1, 1, 0], 3: [1, -1, 0], 4: [-1, 0, 0]}
341
- G = Graph(vertices, edges, layout=lt)
342
- self.add(G)
343
-
344
- The vertices in graphs can be labeled, and configurations for vertices
345
- and edges can be modified both by default and for specific vertices and
346
- edges.
347
-
348
- .. note::
349
-
350
- In ``edge_config``, edges can be passed in both directions: if
351
- ``(u, v)`` is an edge in the graph, both ``(u, v)`` as well
352
- as ``(v, u)`` can be used as keys in the dictionary.
353
-
354
- .. manim:: LabeledModifiedGraph
355
- :save_last_frame:
356
-
357
- class LabeledModifiedGraph(Scene):
358
- def construct(self):
359
- vertices = [1, 2, 3, 4, 5, 6, 7, 8]
360
- edges = [(1, 7), (1, 8), (2, 3), (2, 4), (2, 5),
361
- (2, 8), (3, 4), (6, 1), (6, 2),
362
- (6, 3), (7, 2), (7, 4)]
363
- g = Graph(vertices, edges, layout="circular", layout_scale=3,
364
- labels=True, vertex_config={7: {"fill_color": RED}},
365
- edge_config={(1, 7): {"stroke_color": RED},
366
- (2, 7): {"stroke_color": RED},
367
- (4, 7): {"stroke_color": RED}})
368
- self.add(g)
369
-
370
- You can also lay out a partite graph on columns by specifying
371
- a list of the vertices on each side and choosing the partite layout.
372
-
373
- .. note::
374
-
375
- All vertices in your graph which are not listed in any of the partitions
376
- are collected in their own partition and rendered in the rightmost column.
377
-
378
- .. manim:: PartiteGraph
379
- :save_last_frame:
380
-
381
- import networkx as nx
382
-
383
- class PartiteGraph(Scene):
384
- def construct(self):
385
- G = nx.Graph()
386
- G.add_nodes_from([0, 1, 2, 3])
387
- G.add_edges_from([(0, 2), (0,3), (1, 2)])
388
- graph = Graph(list(G.nodes), list(G.edges), layout="partite", partitions=[[0, 1]])
389
- self.play(Create(graph))
390
-
391
- The representation of a linear artificial neural network is facilitated
392
- by the use of the partite layout and defining partitions for each layer.
393
-
394
- .. manim:: LinearNN
395
- :save_last_frame:
396
-
397
- class LinearNN(Scene):
398
- def construct(self):
399
- edges = []
400
- partitions = []
401
- c = 0
402
- layers = [2, 3, 3, 2] # the number of neurons in each layer
403
-
404
- for i in layers:
405
- partitions.append(list(range(c + 1, c + i + 1)))
406
- c += i
407
- for i, v in enumerate(layers[1:]):
408
- last = sum(layers[:i+1])
409
- for j in range(v):
410
- for k in range(last - layers[i], last):
411
- edges.append((k + 1, j + last + 1))
412
-
413
- vertices = np.arange(1, sum(layers) + 1)
414
-
415
- graph = Graph(
416
- vertices,
417
- edges,
418
- layout='partite',
419
- partitions=partitions,
420
- layout_scale=3,
421
- vertex_config={'radius': 0.20},
422
- )
423
- self.add(graph)
424
-
425
- The custom tree layout can be used to show the graph
426
- by distance from the root vertex. You must pass the root vertex
427
- of the tree.
428
-
429
- .. manim:: Tree
430
-
431
- import networkx as nx
432
-
433
- class Tree(Scene):
434
- def construct(self):
435
- G = nx.Graph()
436
-
437
- G.add_node("ROOT")
438
-
439
- for i in range(5):
440
- G.add_node("Child_%i" % i)
441
- G.add_node("Grandchild_%i" % i)
442
- G.add_node("Greatgrandchild_%i" % i)
443
-
444
- G.add_edge("ROOT", "Child_%i" % i)
445
- G.add_edge("Child_%i" % i, "Grandchild_%i" % i)
446
- G.add_edge("Grandchild_%i" % i, "Greatgrandchild_%i" % i)
447
-
448
- self.play(Create(
449
- Graph(list(G.nodes), list(G.edges), layout="tree", root_vertex="ROOT")))
450
-
451
- The following code sample illustrates the use of the ``vertex_spacing``
452
- layout parameter specific to the ``"tree"`` layout. As mentioned
453
- above, setting ``vertex_spacing`` overrides the specified value
454
- for ``layout_scale``, and as such it is harder to control the size
455
- of the mobject. However, we can adjust the captured frame and
456
- zoom out by using a :class:`.MovingCameraScene`::
457
-
458
- class LargeTreeGeneration(MovingCameraScene):
459
- DEPTH = 4
460
- CHILDREN_PER_VERTEX = 3
461
- LAYOUT_CONFIG = {"vertex_spacing": (0.5, 1)}
462
- VERTEX_CONF = {"radius": 0.25, "color": BLUE_B, "fill_opacity": 1}
463
-
464
- def expand_vertex(self, g, vertex_id: str, depth: int):
465
- new_vertices = [f"{vertex_id}/{i}" for i in range(self.CHILDREN_PER_VERTEX)]
466
- new_edges = [(vertex_id, child_id) for child_id in new_vertices]
467
- g.add_edges(
468
- *new_edges,
469
- vertex_config=self.VERTEX_CONF,
470
- positions={
471
- k: g.vertices[vertex_id].get_center() + 0.1 * DOWN for k in new_vertices
472
- },
473
- )
474
- if depth < self.DEPTH:
475
- for child_id in new_vertices:
476
- self.expand_vertex(g, child_id, depth + 1)
477
-
478
- return g
479
-
480
- def construct(self):
481
- g = Graph(["ROOT"], [], vertex_config=self.VERTEX_CONF)
482
- g = self.expand_vertex(g, "ROOT", 1)
483
- self.add(g)
484
-
485
- self.play(
486
- g.animate.change_layout(
487
- "tree",
488
- root_vertex="ROOT",
489
- layout_config=self.LAYOUT_CONFIG,
490
- )
491
- )
492
- self.play(self.camera.auto_zoom(g, margin=1), run_time=0.5)
493
- """
494
-
495
- def __init__(
496
- self,
497
- vertices: list[Hashable],
498
- edges: list[tuple[Hashable, Hashable]],
499
- labels: bool | dict = False,
500
- label_fill_color: str = BLACK,
501
- layout: str | dict = "spring",
502
- layout_scale: float | tuple = 2,
503
- layout_config: dict | None = None,
504
- vertex_type: type[Mobject] = Dot,
505
- vertex_config: dict | None = None,
506
- vertex_mobjects: dict | None = None,
507
- edge_type: type[Mobject] = Line,
508
- partitions: list[list[Hashable]] | None = None,
509
- root_vertex: Hashable | None = None,
510
- edge_config: dict | None = None,
511
- ) -> None:
512
- super().__init__()
513
-
514
- nx_graph = nx.Graph()
515
- nx_graph.add_nodes_from(vertices)
516
- nx_graph.add_edges_from(edges)
517
- self._graph = nx_graph
518
-
519
- self._layout = _determine_graph_layout(
520
- nx_graph,
621
+ self.change_layout(
521
622
  layout=layout,
522
623
  layout_scale=layout_scale,
523
624
  layout_config=layout_config,
@@ -525,99 +626,67 @@ class Graph(VMobject, metaclass=ConvertToOpenGL):
525
626
  root_vertex=root_vertex,
526
627
  )
527
628
 
528
- if isinstance(labels, dict):
529
- self._labels = labels
530
- elif isinstance(labels, bool):
531
- if labels:
532
- self._labels = {
533
- v: MathTex(v, fill_color=label_fill_color) for v in vertices
534
- }
535
- else:
536
- self._labels = {}
537
-
538
- if self._labels and vertex_type is Dot:
539
- vertex_type = LabeledDot
540
-
541
- if vertex_mobjects is None:
542
- vertex_mobjects = {}
543
-
544
- # build vertex_config
545
- if vertex_config is None:
546
- vertex_config = {}
547
- default_vertex_config = {}
548
- if vertex_config:
549
- default_vertex_config = {
550
- k: v for k, v in vertex_config.items() if k not in vertices
551
- }
552
- self._vertex_config = {
553
- v: vertex_config.get(v, copy(default_vertex_config)) for v in vertices
554
- }
555
- self.default_vertex_config = default_vertex_config
556
- for v, label in self._labels.items():
557
- self._vertex_config[v]["label"] = label
558
-
559
- self.vertices = {v: vertex_type(**self._vertex_config[v]) for v in vertices}
560
- self.vertices.update(vertex_mobjects)
561
- for v in self.vertices:
562
- self[v].move_to(self._layout[v])
563
-
564
629
  # build edge_config
565
630
  if edge_config is None:
566
631
  edge_config = {}
632
+ default_tip_config = {}
567
633
  default_edge_config = {}
568
634
  if edge_config:
635
+ default_tip_config = edge_config.pop("tip_config", {})
569
636
  default_edge_config = {
570
637
  k: v
571
638
  for k, v in edge_config.items()
572
- if k not in edges and k[::-1] not in edges
639
+ if not isinstance(
640
+ k, tuple
641
+ ) # everything that is not an edge is an option
573
642
  }
574
643
  self._edge_config = {}
644
+ self._tip_config = {}
575
645
  for e in edges:
576
646
  if e in edge_config:
647
+ self._tip_config[e] = edge_config[e].pop(
648
+ "tip_config", copy(default_tip_config)
649
+ )
577
650
  self._edge_config[e] = edge_config[e]
578
- elif e[::-1] in edge_config:
579
- self._edge_config[e] = edge_config[e[::-1]]
580
651
  else:
652
+ self._tip_config[e] = copy(default_tip_config)
581
653
  self._edge_config[e] = copy(default_edge_config)
582
654
 
583
655
  self.default_edge_config = default_edge_config
584
- self.edges = {
585
- (u, v): edge_type(
586
- self[u].get_center(),
587
- self[v].get_center(),
588
- z_index=-1,
589
- **self._edge_config[(u, v)],
590
- )
591
- for (u, v) in edges
592
- }
656
+ self._populate_edge_dict(edges, edge_type)
593
657
 
594
658
  self.add(*self.vertices.values())
595
659
  self.add(*self.edges.values())
596
660
 
597
- def update_edges(graph):
598
- for (u, v), edge in graph.edges.items():
599
- edge.put_start_and_end_on(graph[u].get_center(), graph[v].get_center())
661
+ self.add_updater(self.update_edges)
662
+
663
+ @staticmethod
664
+ def _empty_networkx_graph() -> nx.classes.graph.Graph:
665
+ """Return an empty networkx graph for the given graph type."""
666
+ raise NotImplementedError("To be implemented in concrete subclasses")
600
667
 
601
- self.add_updater(update_edges)
668
+ def _populate_edge_dict(
669
+ self, edges: list[tuple[Hashable, Hashable]], edge_type: type[Mobject]
670
+ ):
671
+ """Helper method for populating the edges of the graph."""
672
+ raise NotImplementedError("To be implemented in concrete subclasses")
602
673
 
603
674
  def __getitem__(self: Graph, v: Hashable) -> Mobject:
604
675
  return self.vertices[v]
605
676
 
606
- def __repr__(self: Graph) -> str:
607
- return f"Graph on {len(self.vertices)} vertices and {len(self.edges)} edges"
608
-
609
677
  def _create_vertex(
610
678
  self,
611
679
  vertex: Hashable,
612
- position: np.ndarray | None = None,
680
+ position: Point3DLike | None = None,
613
681
  label: bool = False,
614
682
  label_fill_color: str = BLACK,
615
683
  vertex_type: type[Mobject] = Dot,
616
684
  vertex_config: dict | None = None,
617
685
  vertex_mobject: dict | None = None,
618
- ) -> tuple[Hashable, np.ndarray, dict, Mobject]:
619
- if position is None:
620
- position = self.get_center()
686
+ ) -> tuple[Hashable, Point3D, dict, Mobject]:
687
+ np_position: Point3D = (
688
+ self.get_center() if position is None else np.asarray(position)
689
+ )
621
690
 
622
691
  if vertex_config is None:
623
692
  vertex_config = {}
@@ -627,13 +696,11 @@ class Graph(VMobject, metaclass=ConvertToOpenGL):
627
696
  f"Vertex identifier '{vertex}' is already used for a vertex in this graph.",
628
697
  )
629
698
 
630
- if isinstance(label, (Mobject, OpenGLMobject)):
631
- label = label
632
- elif label is True:
699
+ if label is True:
633
700
  label = MathTex(vertex, fill_color=label_fill_color)
634
701
  elif vertex in self._labels:
635
702
  label = self._labels[vertex]
636
- else:
703
+ elif not isinstance(label, (Mobject, OpenGLMobject)):
637
704
  label = None
638
705
 
639
706
  base_vertex_config = copy(self.default_vertex_config)
@@ -648,14 +715,14 @@ class Graph(VMobject, metaclass=ConvertToOpenGL):
648
715
  if vertex_mobject is None:
649
716
  vertex_mobject = vertex_type(**vertex_config)
650
717
 
651
- vertex_mobject.move_to(position)
718
+ vertex_mobject.move_to(np_position)
652
719
 
653
- return (vertex, position, vertex_config, vertex_mobject)
720
+ return (vertex, np_position, vertex_config, vertex_mobject)
654
721
 
655
722
  def _add_created_vertex(
656
723
  self,
657
724
  vertex: Hashable,
658
- position: np.ndarray,
725
+ position: Point3DLike,
659
726
  vertex_config: dict,
660
727
  vertex_mobject: Mobject,
661
728
  ) -> Mobject:
@@ -681,7 +748,7 @@ class Graph(VMobject, metaclass=ConvertToOpenGL):
681
748
  def _add_vertex(
682
749
  self,
683
750
  vertex: Hashable,
684
- position: np.ndarray | None = None,
751
+ position: Point3DLike | None = None,
685
752
  label: bool = False,
686
753
  label_fill_color: str = BLACK,
687
754
  vertex_type: type[Mobject] = Dot,
@@ -736,22 +803,22 @@ class Graph(VMobject, metaclass=ConvertToOpenGL):
736
803
  vertex_type: type[Mobject] = Dot,
737
804
  vertex_config: dict | None = None,
738
805
  vertex_mobjects: dict | None = None,
739
- ) -> Iterable[tuple[Hashable, np.ndarray, dict, Mobject]]:
806
+ ) -> Iterable[tuple[Hashable, Point3D, dict, Mobject]]:
740
807
  if positions is None:
741
808
  positions = {}
742
809
  if vertex_mobjects is None:
743
810
  vertex_mobjects = {}
744
811
 
745
812
  graph_center = self.get_center()
746
- base_positions = {v: graph_center for v in vertices}
813
+ base_positions = dict.fromkeys(vertices, graph_center)
747
814
  base_positions.update(positions)
748
815
  positions = base_positions
749
816
 
750
817
  if isinstance(labels, bool):
751
- labels = {v: labels for v in vertices}
818
+ labels = dict.fromkeys(vertices, labels)
752
819
  else:
753
820
  assert isinstance(labels, dict)
754
- base_labels = {v: False for v in vertices}
821
+ base_labels = dict.fromkeys(vertices, False)
755
822
  base_labels.update(labels)
756
823
  labels = base_labels
757
824
 
@@ -776,7 +843,7 @@ class Graph(VMobject, metaclass=ConvertToOpenGL):
776
843
  label_fill_color=label_fill_color,
777
844
  vertex_type=vertex_type,
778
845
  vertex_config=vertex_config[v],
779
- vertex_mobject=vertex_mobjects[v] if v in vertex_mobjects else None,
846
+ vertex_mobject=vertex_mobjects.get(v),
780
847
  )
781
848
  for v in vertices
782
849
  ]
@@ -905,7 +972,7 @@ class Graph(VMobject, metaclass=ConvertToOpenGL):
905
972
  >>> removed = G.remove_vertices(2, 3); removed
906
973
  VGroup(Line, Line, Dot, Dot)
907
974
  >>> G
908
- Graph on 1 vertices and 0 edges
975
+ Undirected graph on 1 vertices and 0 edges
909
976
 
910
977
  """
911
978
  mobjects = []
@@ -968,7 +1035,10 @@ class Graph(VMobject, metaclass=ConvertToOpenGL):
968
1035
  self._edge_config[(u, v)] = edge_config
969
1036
 
970
1037
  edge_mobject = edge_type(
971
- self[u].get_center(), self[v].get_center(), z_index=-1, **edge_config
1038
+ start=self[u].get_center(),
1039
+ end=self[v].get_center(),
1040
+ z_index=-1,
1041
+ **edge_config,
972
1042
  )
973
1043
  self.edges[(u, v)] = edge_mobject
974
1044
 
@@ -1065,9 +1135,7 @@ class Graph(VMobject, metaclass=ConvertToOpenGL):
1065
1135
 
1066
1136
  """
1067
1137
  if edge not in self.edges:
1068
- edge = edge[::-1]
1069
- if edge not in self.edges:
1070
- raise ValueError(f"The graph does not contain a edge '{edge}'")
1138
+ raise ValueError(f"The graph does not contain a edge '{edge}'")
1071
1139
 
1072
1140
  edge_mobject = self.edges.pop(edge)
1073
1141
 
@@ -1104,15 +1172,18 @@ class Graph(VMobject, metaclass=ConvertToOpenGL):
1104
1172
  mobjects = self.remove_edges(*edges)
1105
1173
  return AnimationGroup(*(animation(mobj, **anim_args) for mobj in mobjects))
1106
1174
 
1107
- @staticmethod
1108
- def from_networkx(nxgraph: nx.classes.graph.Graph, **kwargs) -> Graph:
1109
- """Build a :class:`~.Graph` from a given ``networkx`` graph.
1175
+ @classmethod
1176
+ def from_networkx(
1177
+ cls, nxgraph: nx.classes.graph.Graph | nx.classes.digraph.DiGraph, **kwargs
1178
+ ):
1179
+ """Build a :class:`~.Graph` or :class:`~.DiGraph` from a
1180
+ given ``networkx`` graph.
1110
1181
 
1111
1182
  Parameters
1112
1183
  ----------
1113
1184
 
1114
1185
  nxgraph
1115
- A ``networkx`` graph.
1186
+ A ``networkx`` graph or digraph.
1116
1187
  **kwargs
1117
1188
  Keywords to be passed to the constructor of :class:`~.Graph`.
1118
1189
 
@@ -1135,13 +1206,13 @@ class Graph(VMobject, metaclass=ConvertToOpenGL):
1135
1206
  self.play(Uncreate(G))
1136
1207
 
1137
1208
  """
1138
- return Graph(list(nxgraph.nodes), list(nxgraph.edges), **kwargs)
1209
+ return cls(list(nxgraph.nodes), list(nxgraph.edges), **kwargs)
1139
1210
 
1140
1211
  def change_layout(
1141
1212
  self,
1142
- layout: str | dict = "spring",
1143
- layout_scale: float = 2,
1144
- layout_config: dict | None = None,
1213
+ layout: LayoutName | dict[Hashable, Point3DLike] | LayoutFunction = "spring",
1214
+ layout_scale: float | tuple[float, float, float] = 2,
1215
+ layout_config: dict[str, Any] | None = None,
1145
1216
  partitions: list[list[Hashable]] | None = None,
1146
1217
  root_vertex: Hashable | None = None,
1147
1218
  ) -> Graph:
@@ -1165,14 +1236,551 @@ class Graph(VMobject, metaclass=ConvertToOpenGL):
1165
1236
  self.play(G.animate.change_layout("circular"))
1166
1237
  self.wait()
1167
1238
  """
1239
+ layout_config = {} if layout_config is None else layout_config
1240
+ if partitions is not None and "partitions" not in layout_config:
1241
+ layout_config["partitions"] = partitions
1242
+ if root_vertex is not None and "root_vertex" not in layout_config:
1243
+ layout_config["root_vertex"] = root_vertex
1244
+
1168
1245
  self._layout = _determine_graph_layout(
1169
1246
  self._graph,
1170
1247
  layout=layout,
1171
1248
  layout_scale=layout_scale,
1172
1249
  layout_config=layout_config,
1173
- partitions=partitions,
1174
- root_vertex=root_vertex,
1175
1250
  )
1251
+
1176
1252
  for v in self.vertices:
1177
1253
  self[v].move_to(self._layout[v])
1178
1254
  return self
1255
+
1256
+
1257
+ class Graph(GenericGraph):
1258
+ """An undirected graph (vertices connected with edges).
1259
+
1260
+ The graph comes with an updater which makes the edges stick to
1261
+ the vertices when moved around. See :class:`.DiGraph` for
1262
+ a version with directed edges.
1263
+
1264
+ See also
1265
+ --------
1266
+
1267
+ :class:`.GenericGraph`
1268
+
1269
+ Parameters
1270
+ ----------
1271
+
1272
+ vertices
1273
+ A list of vertices. Must be hashable elements.
1274
+ edges
1275
+ A list of edges, specified as tuples ``(u, v)`` where both ``u``
1276
+ and ``v`` are vertices. The vertex order is irrelevant.
1277
+ labels
1278
+ Controls whether or not vertices are labeled. If ``False`` (the default),
1279
+ the vertices are not labeled; if ``True`` they are labeled using their
1280
+ names (as specified in ``vertices``) via :class:`~.MathTex`. Alternatively,
1281
+ custom labels can be specified by passing a dictionary whose keys are
1282
+ the vertices, and whose values are the corresponding vertex labels
1283
+ (rendered via, e.g., :class:`~.Text` or :class:`~.Tex`).
1284
+ label_fill_color
1285
+ Sets the fill color of the default labels generated when ``labels``
1286
+ is set to ``True``. Has no effect for other values of ``labels``.
1287
+ layout
1288
+ Either one of ``"spring"`` (the default), ``"circular"``, ``"kamada_kawai"``,
1289
+ ``"planar"``, ``"random"``, ``"shell"``, ``"spectral"``, ``"spiral"``, ``"tree"``, and ``"partite"``
1290
+ for automatic vertex positioning using ``networkx``
1291
+ (see `their documentation <https://networkx.org/documentation/stable/reference/drawing.html#module-networkx.drawing.layout>`_
1292
+ for more details), or a dictionary specifying a coordinate (value)
1293
+ for each vertex (key) for manual positioning.
1294
+ layout_config
1295
+ Only for automatically generated layouts. A dictionary whose entries
1296
+ are passed as keyword arguments to the automatic layout algorithm
1297
+ specified via ``layout`` of ``networkx``.
1298
+ The ``tree`` layout also accepts a special parameter ``vertex_spacing``
1299
+ passed as a keyword argument inside the ``layout_config`` dictionary.
1300
+ Passing a tuple ``(space_x, space_y)`` as this argument overrides
1301
+ the value of ``layout_scale`` and ensures that vertices are arranged
1302
+ in a way such that the centers of siblings in the same layer are
1303
+ at least ``space_x`` units apart horizontally, and neighboring layers
1304
+ are spaced ``space_y`` units vertically.
1305
+ layout_scale
1306
+ The scale of automatically generated layouts: the vertices will
1307
+ be arranged such that the coordinates are located within the
1308
+ interval ``[-scale, scale]``. Some layouts accept a tuple ``(scale_x, scale_y)``
1309
+ causing the first coordinate to be in the interval ``[-scale_x, scale_x]``,
1310
+ and the second in ``[-scale_y, scale_y]``. Default: 2.
1311
+ vertex_type
1312
+ The mobject class used for displaying vertices in the scene.
1313
+ vertex_config
1314
+ Either a dictionary containing keyword arguments to be passed to
1315
+ the class specified via ``vertex_type``, or a dictionary whose keys
1316
+ are the vertices, and whose values are dictionaries containing keyword
1317
+ arguments for the mobject related to the corresponding vertex.
1318
+ vertex_mobjects
1319
+ A dictionary whose keys are the vertices, and whose values are
1320
+ mobjects to be used as vertices. Passing vertices here overrides
1321
+ all other configuration options for a vertex.
1322
+ edge_type
1323
+ The mobject class used for displaying edges in the scene.
1324
+ edge_config
1325
+ Either a dictionary containing keyword arguments to be passed
1326
+ to the class specified via ``edge_type``, or a dictionary whose
1327
+ keys are the edges, and whose values are dictionaries containing
1328
+ keyword arguments for the mobject related to the corresponding edge.
1329
+
1330
+
1331
+ Examples
1332
+ --------
1333
+
1334
+ First, we create a small graph and demonstrate that the edges move
1335
+ together with the vertices.
1336
+
1337
+ .. manim:: MovingVertices
1338
+
1339
+ class MovingVertices(Scene):
1340
+ def construct(self):
1341
+ vertices = [1, 2, 3, 4]
1342
+ edges = [(1, 2), (2, 3), (3, 4), (1, 3), (1, 4)]
1343
+ g = Graph(vertices, edges)
1344
+ self.play(Create(g))
1345
+ self.wait()
1346
+ self.play(g[1].animate.move_to([1, 1, 0]),
1347
+ g[2].animate.move_to([-1, 1, 0]),
1348
+ g[3].animate.move_to([1, -1, 0]),
1349
+ g[4].animate.move_to([-1, -1, 0]))
1350
+ self.wait()
1351
+
1352
+ There are several automatic positioning algorithms to choose from:
1353
+
1354
+ .. manim:: GraphAutoPosition
1355
+ :save_last_frame:
1356
+
1357
+ class GraphAutoPosition(Scene):
1358
+ def construct(self):
1359
+ vertices = [1, 2, 3, 4, 5, 6, 7, 8]
1360
+ edges = [(1, 7), (1, 8), (2, 3), (2, 4), (2, 5),
1361
+ (2, 8), (3, 4), (6, 1), (6, 2),
1362
+ (6, 3), (7, 2), (7, 4)]
1363
+ autolayouts = ["spring", "circular", "kamada_kawai",
1364
+ "planar", "random", "shell",
1365
+ "spectral", "spiral"]
1366
+ graphs = [Graph(vertices, edges, layout=lt).scale(0.5)
1367
+ for lt in autolayouts]
1368
+ r1 = VGroup(*graphs[:3]).arrange()
1369
+ r2 = VGroup(*graphs[3:6]).arrange()
1370
+ r3 = VGroup(*graphs[6:]).arrange()
1371
+ self.add(VGroup(r1, r2, r3).arrange(direction=DOWN))
1372
+
1373
+ Vertices can also be positioned manually:
1374
+
1375
+ .. manim:: GraphManualPosition
1376
+ :save_last_frame:
1377
+
1378
+ class GraphManualPosition(Scene):
1379
+ def construct(self):
1380
+ vertices = [1, 2, 3, 4]
1381
+ edges = [(1, 2), (2, 3), (3, 4), (4, 1)]
1382
+ lt = {1: [0, 0, 0], 2: [1, 1, 0], 3: [1, -1, 0], 4: [-1, 0, 0]}
1383
+ G = Graph(vertices, edges, layout=lt)
1384
+ self.add(G)
1385
+
1386
+ The vertices in graphs can be labeled, and configurations for vertices
1387
+ and edges can be modified both by default and for specific vertices and
1388
+ edges.
1389
+
1390
+ .. note::
1391
+
1392
+ In ``edge_config``, edges can be passed in both directions: if
1393
+ ``(u, v)`` is an edge in the graph, both ``(u, v)`` as well
1394
+ as ``(v, u)`` can be used as keys in the dictionary.
1395
+
1396
+ .. manim:: LabeledModifiedGraph
1397
+ :save_last_frame:
1398
+
1399
+ class LabeledModifiedGraph(Scene):
1400
+ def construct(self):
1401
+ vertices = [1, 2, 3, 4, 5, 6, 7, 8]
1402
+ edges = [(1, 7), (1, 8), (2, 3), (2, 4), (2, 5),
1403
+ (2, 8), (3, 4), (6, 1), (6, 2),
1404
+ (6, 3), (7, 2), (7, 4)]
1405
+ g = Graph(vertices, edges, layout="circular", layout_scale=3,
1406
+ labels=True, vertex_config={7: {"fill_color": RED}},
1407
+ edge_config={(1, 7): {"stroke_color": RED},
1408
+ (2, 7): {"stroke_color": RED},
1409
+ (4, 7): {"stroke_color": RED}})
1410
+ self.add(g)
1411
+
1412
+ You can also lay out a partite graph on columns by specifying
1413
+ a list of the vertices on each side and choosing the partite layout.
1414
+
1415
+ .. note::
1416
+
1417
+ All vertices in your graph which are not listed in any of the partitions
1418
+ are collected in their own partition and rendered in the rightmost column.
1419
+
1420
+ .. manim:: PartiteGraph
1421
+ :save_last_frame:
1422
+
1423
+ import networkx as nx
1424
+
1425
+ class PartiteGraph(Scene):
1426
+ def construct(self):
1427
+ G = nx.Graph()
1428
+ G.add_nodes_from([0, 1, 2, 3])
1429
+ G.add_edges_from([(0, 2), (0,3), (1, 2)])
1430
+ graph = Graph(list(G.nodes), list(G.edges), layout="partite", partitions=[[0, 1]])
1431
+ self.play(Create(graph))
1432
+
1433
+ The representation of a linear artificial neural network is facilitated
1434
+ by the use of the partite layout and defining partitions for each layer.
1435
+
1436
+ .. manim:: LinearNN
1437
+ :save_last_frame:
1438
+
1439
+ class LinearNN(Scene):
1440
+ def construct(self):
1441
+ edges = []
1442
+ partitions = []
1443
+ c = 0
1444
+ layers = [2, 3, 3, 2] # the number of neurons in each layer
1445
+
1446
+ for i in layers:
1447
+ partitions.append(list(range(c + 1, c + i + 1)))
1448
+ c += i
1449
+ for i, v in enumerate(layers[1:]):
1450
+ last = sum(layers[:i+1])
1451
+ for j in range(v):
1452
+ for k in range(last - layers[i], last):
1453
+ edges.append((k + 1, j + last + 1))
1454
+
1455
+ vertices = np.arange(1, sum(layers) + 1)
1456
+
1457
+ graph = Graph(
1458
+ vertices,
1459
+ edges,
1460
+ layout='partite',
1461
+ partitions=partitions,
1462
+ layout_scale=3,
1463
+ vertex_config={'radius': 0.20},
1464
+ )
1465
+ self.add(graph)
1466
+
1467
+ The custom tree layout can be used to show the graph
1468
+ by distance from the root vertex. You must pass the root vertex
1469
+ of the tree.
1470
+
1471
+ .. manim:: Tree
1472
+
1473
+ import networkx as nx
1474
+
1475
+ class Tree(Scene):
1476
+ def construct(self):
1477
+ G = nx.Graph()
1478
+
1479
+ G.add_node("ROOT")
1480
+
1481
+ for i in range(5):
1482
+ G.add_node("Child_%i" % i)
1483
+ G.add_node("Grandchild_%i" % i)
1484
+ G.add_node("Greatgrandchild_%i" % i)
1485
+
1486
+ G.add_edge("ROOT", "Child_%i" % i)
1487
+ G.add_edge("Child_%i" % i, "Grandchild_%i" % i)
1488
+ G.add_edge("Grandchild_%i" % i, "Greatgrandchild_%i" % i)
1489
+
1490
+ self.play(Create(
1491
+ Graph(list(G.nodes), list(G.edges), layout="tree", root_vertex="ROOT")))
1492
+
1493
+ The following code sample illustrates the use of the ``vertex_spacing``
1494
+ layout parameter specific to the ``"tree"`` layout. As mentioned
1495
+ above, setting ``vertex_spacing`` overrides the specified value
1496
+ for ``layout_scale``, and as such it is harder to control the size
1497
+ of the mobject. However, we can adjust the captured frame and
1498
+ zoom out by using a :class:`.MovingCameraScene`::
1499
+
1500
+ class LargeTreeGeneration(MovingCameraScene):
1501
+ DEPTH = 4
1502
+ CHILDREN_PER_VERTEX = 3
1503
+ LAYOUT_CONFIG = {"vertex_spacing": (0.5, 1)}
1504
+ VERTEX_CONF = {"radius": 0.25, "color": BLUE_B, "fill_opacity": 1}
1505
+
1506
+ def expand_vertex(self, g, vertex_id: str, depth: int):
1507
+ new_vertices = [
1508
+ f"{vertex_id}/{i}" for i in range(self.CHILDREN_PER_VERTEX)
1509
+ ]
1510
+ new_edges = [(vertex_id, child_id) for child_id in new_vertices]
1511
+ g.add_edges(
1512
+ *new_edges,
1513
+ vertex_config=self.VERTEX_CONF,
1514
+ positions={
1515
+ k: g.vertices[vertex_id].get_center() + 0.1 * DOWN
1516
+ for k in new_vertices
1517
+ },
1518
+ )
1519
+ if depth < self.DEPTH:
1520
+ for child_id in new_vertices:
1521
+ self.expand_vertex(g, child_id, depth + 1)
1522
+
1523
+ return g
1524
+
1525
+ def construct(self):
1526
+ g = Graph(["ROOT"], [], vertex_config=self.VERTEX_CONF)
1527
+ g = self.expand_vertex(g, "ROOT", 1)
1528
+ self.add(g)
1529
+
1530
+ self.play(
1531
+ g.animate.change_layout(
1532
+ "tree",
1533
+ root_vertex="ROOT",
1534
+ layout_config=self.LAYOUT_CONFIG,
1535
+ )
1536
+ )
1537
+ self.play(self.camera.auto_zoom(g, margin=1), run_time=0.5)
1538
+ """
1539
+
1540
+ @staticmethod
1541
+ def _empty_networkx_graph() -> nx.Graph:
1542
+ return nx.Graph()
1543
+
1544
+ def _populate_edge_dict(
1545
+ self, edges: list[tuple[Hashable, Hashable]], edge_type: type[Mobject]
1546
+ ):
1547
+ self.edges = {
1548
+ (u, v): edge_type(
1549
+ start=self[u].get_center(),
1550
+ end=self[v].get_center(),
1551
+ z_index=-1,
1552
+ **self._edge_config[(u, v)],
1553
+ )
1554
+ for (u, v) in edges
1555
+ }
1556
+
1557
+ def update_edges(self, graph):
1558
+ for (u, v), edge in graph.edges.items():
1559
+ # Undirected graph has a Line edge
1560
+ edge.set_points_by_ends(
1561
+ graph[u].get_center(),
1562
+ graph[v].get_center(),
1563
+ buff=self._edge_config.get("buff", 0),
1564
+ path_arc=self._edge_config.get("path_arc", 0),
1565
+ )
1566
+
1567
+ def __repr__(self: Graph) -> str:
1568
+ return f"Undirected graph on {len(self.vertices)} vertices and {len(self.edges)} edges"
1569
+
1570
+
1571
+ class DiGraph(GenericGraph):
1572
+ """A directed graph.
1573
+
1574
+ .. note::
1575
+
1576
+ In contrast to undirected graphs, the order in which vertices in a given
1577
+ edge are specified is relevant here.
1578
+
1579
+ See also
1580
+ --------
1581
+
1582
+ :class:`.GenericGraph`
1583
+
1584
+ Parameters
1585
+ ----------
1586
+
1587
+ vertices
1588
+ A list of vertices. Must be hashable elements.
1589
+ edges
1590
+ A list of edges, specified as tuples ``(u, v)`` where both ``u``
1591
+ and ``v`` are vertices. The edge is directed from ``u`` to ``v``.
1592
+ labels
1593
+ Controls whether or not vertices are labeled. If ``False`` (the default),
1594
+ the vertices are not labeled; if ``True`` they are labeled using their
1595
+ names (as specified in ``vertices``) via :class:`~.MathTex`. Alternatively,
1596
+ custom labels can be specified by passing a dictionary whose keys are
1597
+ the vertices, and whose values are the corresponding vertex labels
1598
+ (rendered via, e.g., :class:`~.Text` or :class:`~.Tex`).
1599
+ label_fill_color
1600
+ Sets the fill color of the default labels generated when ``labels``
1601
+ is set to ``True``. Has no effect for other values of ``labels``.
1602
+ layout
1603
+ Either one of ``"spring"`` (the default), ``"circular"``, ``"kamada_kawai"``,
1604
+ ``"planar"``, ``"random"``, ``"shell"``, ``"spectral"``, ``"spiral"``, ``"tree"``, and ``"partite"``
1605
+ for automatic vertex positioning using ``networkx``
1606
+ (see `their documentation <https://networkx.org/documentation/stable/reference/drawing.html#module-networkx.drawing.layout>`_
1607
+ for more details), or a dictionary specifying a coordinate (value)
1608
+ for each vertex (key) for manual positioning.
1609
+ layout_config
1610
+ Only for automatically generated layouts. A dictionary whose entries
1611
+ are passed as keyword arguments to the automatic layout algorithm
1612
+ specified via ``layout`` of ``networkx``.
1613
+ The ``tree`` layout also accepts a special parameter ``vertex_spacing``
1614
+ passed as a keyword argument inside the ``layout_config`` dictionary.
1615
+ Passing a tuple ``(space_x, space_y)`` as this argument overrides
1616
+ the value of ``layout_scale`` and ensures that vertices are arranged
1617
+ in a way such that the centers of siblings in the same layer are
1618
+ at least ``space_x`` units apart horizontally, and neighboring layers
1619
+ are spaced ``space_y`` units vertically.
1620
+ layout_scale
1621
+ The scale of automatically generated layouts: the vertices will
1622
+ be arranged such that the coordinates are located within the
1623
+ interval ``[-scale, scale]``. Some layouts accept a tuple ``(scale_x, scale_y)``
1624
+ causing the first coordinate to be in the interval ``[-scale_x, scale_x]``,
1625
+ and the second in ``[-scale_y, scale_y]``. Default: 2.
1626
+ vertex_type
1627
+ The mobject class used for displaying vertices in the scene.
1628
+ vertex_config
1629
+ Either a dictionary containing keyword arguments to be passed to
1630
+ the class specified via ``vertex_type``, or a dictionary whose keys
1631
+ are the vertices, and whose values are dictionaries containing keyword
1632
+ arguments for the mobject related to the corresponding vertex.
1633
+ vertex_mobjects
1634
+ A dictionary whose keys are the vertices, and whose values are
1635
+ mobjects to be used as vertices. Passing vertices here overrides
1636
+ all other configuration options for a vertex.
1637
+ edge_type
1638
+ The mobject class used for displaying edges in the scene.
1639
+ edge_config
1640
+ Either a dictionary containing keyword arguments to be passed
1641
+ to the class specified via ``edge_type``, or a dictionary whose
1642
+ keys are the edges, and whose values are dictionaries containing
1643
+ keyword arguments for the mobject related to the corresponding edge.
1644
+ You can further customize the tip by adding a ``tip_config`` dictionary
1645
+ for global styling, or by adding the dict to a specific ``edge_config``.
1646
+
1647
+ Examples
1648
+ --------
1649
+
1650
+ .. manim:: MovingDiGraph
1651
+
1652
+ class MovingDiGraph(Scene):
1653
+ def construct(self):
1654
+ vertices = [1, 2, 3, 4]
1655
+ edges = [(1, 2), (2, 3), (3, 4), (1, 3), (1, 4)]
1656
+
1657
+ g = DiGraph(vertices, edges)
1658
+
1659
+ self.add(g)
1660
+ self.play(
1661
+ g[1].animate.move_to([1, 1, 1]),
1662
+ g[2].animate.move_to([-1, 1, 2]),
1663
+ g[3].animate.move_to([1, -1, -1]),
1664
+ g[4].animate.move_to([-1, -1, 0]),
1665
+ )
1666
+ self.wait()
1667
+
1668
+ You can customize the edges and arrow tips globally or locally.
1669
+
1670
+ .. manim:: CustomDiGraph
1671
+
1672
+ class CustomDiGraph(Scene):
1673
+ def construct(self):
1674
+ vertices = [i for i in range(5)]
1675
+ edges = [
1676
+ (0, 1),
1677
+ (1, 2),
1678
+ (3, 2),
1679
+ (3, 4),
1680
+ ]
1681
+
1682
+ edge_config = {
1683
+ "stroke_width": 2,
1684
+ "tip_config": {
1685
+ "tip_shape": ArrowSquareTip,
1686
+ "tip_length": 0.15,
1687
+ },
1688
+ (3, 4): {
1689
+ "color": RED,
1690
+ "tip_config": {"tip_length": 0.25, "tip_width": 0.25}
1691
+ },
1692
+ }
1693
+
1694
+ g = DiGraph(
1695
+ vertices,
1696
+ edges,
1697
+ labels=True,
1698
+ layout="circular",
1699
+ edge_config=edge_config,
1700
+ ).scale(1.4)
1701
+
1702
+ self.play(Create(g))
1703
+ self.wait()
1704
+
1705
+ Since this implementation respects the labels boundary you can also use
1706
+ it for an undirected moving graph with labels.
1707
+
1708
+ .. manim:: UndirectedMovingDiGraph
1709
+
1710
+ class UndirectedMovingDiGraph(Scene):
1711
+ def construct(self):
1712
+ vertices = [i for i in range(5)]
1713
+ edges = [
1714
+ (0, 1),
1715
+ (1, 2),
1716
+ (3, 2),
1717
+ (3, 4),
1718
+ ]
1719
+
1720
+ edge_config = {
1721
+ "stroke_width": 2,
1722
+ "tip_config": {"tip_length": 0, "tip_width": 0},
1723
+ (3, 4): {"color": RED},
1724
+ }
1725
+
1726
+ g = DiGraph(
1727
+ vertices,
1728
+ edges,
1729
+ labels=True,
1730
+ layout="circular",
1731
+ edge_config=edge_config,
1732
+ ).scale(1.4)
1733
+
1734
+ self.play(Create(g))
1735
+ self.wait()
1736
+
1737
+ self.play(
1738
+ g[1].animate.move_to([1, 1, 1]),
1739
+ g[2].animate.move_to([-1, 1, 2]),
1740
+ g[3].animate.move_to([-1.5, -1.5, -1]),
1741
+ g[4].animate.move_to([1, -2, -1]),
1742
+ )
1743
+ self.wait()
1744
+
1745
+ """
1746
+
1747
+ @staticmethod
1748
+ def _empty_networkx_graph() -> nx.DiGraph:
1749
+ return nx.DiGraph()
1750
+
1751
+ def _populate_edge_dict(
1752
+ self, edges: list[tuple[Hashable, Hashable]], edge_type: type[Mobject]
1753
+ ):
1754
+ self.edges = {
1755
+ (u, v): edge_type(
1756
+ start=self[u],
1757
+ end=self[v],
1758
+ z_index=-1,
1759
+ **self._edge_config[(u, v)],
1760
+ )
1761
+ for (u, v) in edges
1762
+ }
1763
+
1764
+ for (u, v), edge in self.edges.items():
1765
+ edge.add_tip(**self._tip_config[(u, v)])
1766
+
1767
+ def update_edges(self, graph):
1768
+ """Updates the edges to stick at their corresponding vertices.
1769
+
1770
+ Arrow tips need to be repositioned since otherwise they can be
1771
+ deformed.
1772
+ """
1773
+ for (u, v), edge in graph.edges.items():
1774
+ tip = edge.pop_tips()[0]
1775
+ # Passing the Mobject instead of the vertex makes the tip
1776
+ # stop on the bounding box of the vertex.
1777
+ edge.set_points_by_ends(
1778
+ graph[u],
1779
+ graph[v],
1780
+ buff=self._edge_config.get("buff", 0),
1781
+ path_arc=self._edge_config.get("path_arc", 0),
1782
+ )
1783
+ edge.add_tip(tip)
1784
+
1785
+ def __repr__(self: DiGraph) -> str:
1786
+ return f"Directed graph on {len(self.vertices)} vertices and {len(self.edges)} edges"