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.
- manim/__init__.py +11 -6
- manim/__main__.py +62 -19
- manim/_config/__init__.py +10 -9
- manim/_config/cli_colors.py +26 -9
- manim/_config/default.cfg +1 -3
- manim/_config/logger_utils.py +23 -13
- manim/_config/utils.py +662 -468
- manim/animation/animation.py +164 -18
- manim/animation/changing.py +34 -23
- manim/animation/composition.py +265 -67
- manim/animation/creation.py +208 -26
- manim/animation/fading.py +16 -18
- manim/animation/growing.py +35 -15
- manim/animation/indication.py +150 -76
- manim/animation/movement.py +56 -22
- manim/animation/numbers.py +64 -6
- manim/animation/rotation.py +78 -7
- manim/animation/specialized.py +6 -7
- manim/animation/speedmodifier.py +13 -10
- manim/animation/transform.py +14 -11
- manim/animation/transform_matching_parts.py +3 -4
- manim/animation/updaters/mobject_update_utils.py +152 -30
- manim/animation/updaters/update.py +10 -7
- manim/camera/camera.py +182 -118
- manim/camera/mapping_camera.py +34 -3
- manim/camera/moving_camera.py +95 -74
- manim/camera/multi_camera.py +23 -15
- manim/camera/three_d_camera.py +70 -52
- manim/cli/__init__.py +17 -0
- manim/cli/cfg/group.py +76 -44
- manim/cli/checkhealth/checks.py +192 -0
- manim/cli/checkhealth/commands.py +90 -0
- manim/cli/default_group.py +158 -25
- manim/cli/init/commands.py +33 -25
- manim/cli/plugins/commands.py +16 -3
- manim/cli/render/commands.py +72 -60
- manim/cli/render/ease_of_access_options.py +4 -3
- manim/cli/render/global_options.py +59 -17
- manim/cli/render/output_options.py +6 -5
- manim/cli/render/render_options.py +98 -33
- manim/constants.py +109 -59
- manim/data_structures.py +31 -0
- manim/mobject/frame.py +8 -5
- manim/mobject/geometry/__init__.py +1 -0
- manim/mobject/geometry/arc.py +277 -135
- manim/mobject/geometry/boolean_ops.py +32 -31
- manim/mobject/geometry/labeled.py +376 -0
- manim/mobject/geometry/line.py +192 -87
- manim/mobject/geometry/polygram.py +224 -58
- manim/mobject/geometry/shape_matchers.py +61 -25
- manim/mobject/geometry/tips.py +122 -48
- manim/mobject/graph.py +1027 -419
- manim/mobject/graphing/coordinate_systems.py +533 -278
- manim/mobject/graphing/functions.py +53 -32
- manim/mobject/graphing/number_line.py +123 -65
- manim/mobject/graphing/probability.py +88 -62
- manim/mobject/graphing/scale.py +33 -19
- manim/mobject/logo.py +118 -28
- manim/mobject/matrix.py +87 -83
- manim/mobject/mobject.py +912 -442
- manim/mobject/opengl/dot_cloud.py +16 -5
- manim/mobject/opengl/opengl_compatibility.py +4 -2
- manim/mobject/opengl/opengl_geometry.py +254 -153
- manim/mobject/opengl/opengl_image_mobject.py +3 -1
- manim/mobject/opengl/opengl_mobject.py +779 -482
- manim/mobject/opengl/opengl_point_cloud_mobject.py +41 -14
- manim/mobject/opengl/opengl_surface.py +14 -92
- manim/mobject/opengl/opengl_three_dimensions.py +12 -8
- manim/mobject/opengl/opengl_vectorized_mobject.py +98 -100
- manim/mobject/svg/brace.py +173 -41
- manim/mobject/svg/svg_mobject.py +139 -53
- manim/mobject/table.py +61 -68
- manim/mobject/text/code_mobject.py +193 -539
- manim/mobject/text/numbers.py +81 -34
- manim/mobject/text/tex_mobject.py +130 -78
- manim/mobject/text/text_mobject.py +288 -164
- manim/mobject/three_d/polyhedra.py +111 -13
- manim/mobject/three_d/three_d_utils.py +17 -8
- manim/mobject/three_d/three_dimensions.py +239 -106
- manim/mobject/types/image_mobject.py +50 -30
- manim/mobject/types/point_cloud_mobject.py +120 -75
- manim/mobject/types/vectorized_mobject.py +841 -408
- manim/mobject/value_tracker.py +105 -38
- manim/mobject/vector_field.py +50 -31
- manim/opengl/__init__.py +3 -3
- manim/plugins/__init__.py +14 -1
- manim/plugins/plugins_flags.py +10 -14
- manim/renderer/cairo_renderer.py +65 -50
- manim/renderer/opengl_renderer.py +89 -69
- manim/renderer/opengl_renderer_window.py +39 -18
- manim/renderer/shader.py +123 -87
- manim/renderer/shader_wrapper.py +44 -28
- manim/renderer/vectorized_mobject_rendering.py +38 -10
- manim/scene/moving_camera_scene.py +32 -3
- manim/scene/scene.py +507 -242
- manim/scene/scene_file_writer.py +371 -220
- manim/scene/section.py +20 -16
- manim/scene/three_d_scene.py +14 -22
- manim/scene/vector_space_scene.py +223 -129
- manim/scene/zoomed_scene.py +46 -41
- manim/typing.py +990 -0
- manim/utils/bezier.py +1823 -371
- manim/utils/caching.py +12 -5
- manim/utils/color/AS2700.py +236 -0
- manim/utils/color/BS381.py +318 -0
- manim/utils/color/DVIPSNAMES.py +96 -0
- manim/utils/color/SVGNAMES.py +179 -0
- manim/utils/color/X11.py +533 -0
- manim/utils/color/XKCD.py +952 -0
- manim/utils/color/__init__.py +61 -0
- manim/utils/color/core.py +1667 -0
- manim/utils/color/manim_colors.py +218 -0
- manim/utils/commands.py +48 -20
- manim/utils/config_ops.py +39 -19
- manim/utils/debug.py +8 -7
- manim/utils/deprecation.py +86 -39
- manim/utils/docbuild/__init__.py +17 -0
- manim/utils/docbuild/autoaliasattr_directive.py +236 -0
- manim/utils/docbuild/autocolor_directive.py +99 -0
- manim/utils/docbuild/manim_directive.py +94 -41
- manim/utils/docbuild/module_parsing.py +245 -0
- manim/utils/exceptions.py +6 -0
- manim/utils/family.py +5 -3
- manim/utils/family_ops.py +17 -4
- manim/utils/file_ops.py +27 -17
- manim/utils/hashing.py +55 -45
- manim/utils/images.py +13 -7
- manim/utils/ipython_magic.py +13 -7
- manim/utils/iterables.py +163 -120
- manim/utils/module_ops.py +66 -24
- manim/utils/opengl.py +77 -24
- manim/utils/parameter_parsing.py +32 -0
- manim/utils/paths.py +30 -33
- manim/utils/polylabel.py +235 -0
- manim/utils/qhull.py +218 -0
- manim/utils/rate_functions.py +98 -32
- manim/utils/simple_functions.py +25 -33
- manim/utils/sounds.py +7 -1
- manim/utils/space_ops.py +188 -115
- manim/utils/testing/__init__.py +17 -0
- manim/utils/testing/_frames_testers.py +13 -8
- manim/utils/testing/_show_diff.py +5 -3
- manim/utils/testing/_test_class_makers.py +34 -18
- manim/utils/testing/frames_comparison.py +37 -19
- manim/utils/tex.py +130 -198
- manim/utils/tex_file_writing.py +77 -47
- manim/utils/tex_templates.py +2 -1
- manim/utils/unit.py +6 -5
- {manim-0.17.0.dist-info → manim-0.19.1.dist-info}/METADATA +64 -65
- manim-0.19.1.dist-info/RECORD +220 -0
- {manim-0.17.0.dist-info → manim-0.19.1.dist-info}/WHEEL +1 -1
- manim-0.19.1.dist-info/entry_points.txt +3 -0
- {manim-0.17.0.dist-info → manim-0.19.1.dist-info/licenses}/LICENSE.community +1 -1
- manim/cli/new/group.py +0 -189
- manim/communitycolors.py +0 -9
- manim/gui/__init__.py +0 -0
- manim/gui/gui.py +0 -82
- manim/plugins/import_plugins.py +0 -43
- manim/utils/color.py +0 -552
- manim-0.17.0.dist-info/RECORD +0 -206
- manim-0.17.0.dist-info/entry_points.txt +0 -4
- /manim/cli/{new → checkhealth}/__init__.py +0 -0
- {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
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
43
|
+
Examples
|
|
44
|
+
--------
|
|
54
45
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
217
|
-
""
|
|
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
|
|
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),
|
|
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
|
|
255
|
-
are passed as keyword arguments to the automatic layout
|
|
256
|
-
specified via ``layout
|
|
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
|
-
|
|
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
|
-
|
|
293
|
-
|
|
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
|
-
|
|
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
|
-
|
|
298
|
-
|
|
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
|
-
|
|
600
|
+
if vertex_mobjects is None:
|
|
601
|
+
vertex_mobjects = {}
|
|
311
602
|
|
|
312
|
-
|
|
313
|
-
:
|
|
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
|
-
|
|
316
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
-
|
|
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:
|
|
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,
|
|
619
|
-
|
|
620
|
-
position
|
|
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
|
|
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
|
-
|
|
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(
|
|
718
|
+
vertex_mobject.move_to(np_position)
|
|
652
719
|
|
|
653
|
-
return (vertex,
|
|
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:
|
|
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:
|
|
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,
|
|
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 =
|
|
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 =
|
|
818
|
+
labels = dict.fromkeys(vertices, labels)
|
|
752
819
|
else:
|
|
753
820
|
assert isinstance(labels, dict)
|
|
754
|
-
base_labels =
|
|
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
|
|
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
|
-
|
|
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(),
|
|
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
|
-
|
|
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
|
-
@
|
|
1108
|
-
def from_networkx(
|
|
1109
|
-
|
|
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
|
|
1209
|
+
return cls(list(nxgraph.nodes), list(nxgraph.edges), **kwargs)
|
|
1139
1210
|
|
|
1140
1211
|
def change_layout(
|
|
1141
1212
|
self,
|
|
1142
|
-
layout:
|
|
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"
|