|
| 1 | +"""Mobjects used to represent mathematical graphs (think graph theory, not plotting).""" |
| 2 | + |
| 3 | +__all__ = [ |
| 4 | + "Graph", |
| 5 | +] |
| 6 | + |
| 7 | +from ..constants import UP |
| 8 | +from ..utils.color import BLACK |
| 9 | +from .types.vectorized_mobject import VMobject |
| 10 | +from .geometry import Dot, Line, LabeledDot |
| 11 | +from .svg.tex_mobject import MathTex |
| 12 | + |
| 13 | +from typing import Hashable, Union, List, Tuple |
| 14 | + |
| 15 | +from copy import copy |
| 16 | +import networkx as nx |
| 17 | +import numpy as np |
| 18 | + |
| 19 | + |
| 20 | +class Graph(VMobject): |
| 21 | + """An undirected graph (that is, a collection of vertices connected with edges). |
| 22 | +
|
| 23 | + Graphs can be instantiated by passing both a list of (distinct, hashable) |
| 24 | + vertex names, together with list of edges (as tuples of vertex names). See |
| 25 | + the examples below for details. |
| 26 | +
|
| 27 | + .. note:: |
| 28 | +
|
| 29 | + This implementation uses updaters to make the edges move with |
| 30 | + the vertices. |
| 31 | +
|
| 32 | + Parameters |
| 33 | + ---------- |
| 34 | +
|
| 35 | + vertices |
| 36 | + A list of vertices. Must be hashable elements. |
| 37 | + edges |
| 38 | + A list of edges, specified as tuples ``(u, v)`` where both ``u`` |
| 39 | + and ``v`` are vertices. |
| 40 | + labels |
| 41 | + Controls whether or not vertices are labeled. If ``False`` (the default), |
| 42 | + the vertices are not labeled; if ``True`` they are labeled using their |
| 43 | + names (as specified in ``vertices``) via :class:`~.MathTex`. Alternatively, |
| 44 | + custom labels can be specified by passing a dictionary whose keys are |
| 45 | + the vertices, and whose values are the corresponding vertex labels |
| 46 | + (rendered via, e.g., :class:`~.Text` or :class:`~.Tex`). |
| 47 | + label_fill_color |
| 48 | + Sets the fill color of the default labels generated when ``labels`` |
| 49 | + is set to ``True``. Has no effect for other values of ``labels``. |
| 50 | + layout |
| 51 | + Either one of ``"spring"`` (the default), ``"circular"``, ``"kamada_kawai"``, |
| 52 | + ``"planar"``, ``"random"``, ``"shell"``, ``"spectral"``, and ``"spiral"`` |
| 53 | + for automatic vertex positioning using ``networkx`` |
| 54 | + (see `their documentation <https://networkx.org/documentation/stable/reference/drawing.html#module-networkx.drawing.layout>`_ |
| 55 | + for more details), or a dictionary specifying a coordinate (value) |
| 56 | + for each vertex (key) for manual positioning. |
| 57 | + layout_scale |
| 58 | + The scale of automatically generated layouts: the vertices will |
| 59 | + be arranged such that the coordinates are located within the |
| 60 | + interval ``[-scale, scale]``. Default: 2. |
| 61 | + layout_config |
| 62 | + Only for automatically generated layouts. A dictionary whose entries |
| 63 | + are passed as keyword arguments to the automatic layout algorithm |
| 64 | + specified via ``layout`` of``networkx``. |
| 65 | + vertex_type |
| 66 | + The mobject class used for displaying vertices in the scene. |
| 67 | + vertex_config |
| 68 | + Either a dictionary containing keyword arguments to be passed to |
| 69 | + the class specified via ``vertex_type``, or a dictionary whose keys |
| 70 | + are the vertices, and whose values are dictionaries containing keyword |
| 71 | + arguments for the mobject related to the corresponding vertex. |
| 72 | + edge_type |
| 73 | + The mobject class used for displaying edges in the scene. |
| 74 | + edge_config |
| 75 | + Either a dictionary containing keyword arguments to be passed |
| 76 | + to the class specified via ``edge_type``, or a dictionary whose |
| 77 | + keys are the edges, and whose values are dictionaries containing |
| 78 | + keyword arguments for the mobject related to the corresponding edge. |
| 79 | +
|
| 80 | + Examples |
| 81 | + -------- |
| 82 | +
|
| 83 | + First, we create a small graph and demonstrate that the edges move |
| 84 | + together with the vertices. |
| 85 | +
|
| 86 | + .. manim:: MovingVertices |
| 87 | +
|
| 88 | + class MovingVertices(Scene): |
| 89 | + def construct(self): |
| 90 | + vertices = [1, 2, 3, 4] |
| 91 | + edges = [(1, 2), (2, 3), (3, 4), (1, 3), (1, 4)] |
| 92 | + g = Graph(vertices, edges) |
| 93 | + self.play(ShowCreation(g)) |
| 94 | + self.wait() |
| 95 | + self.play(g[1].animate.move_to([1, 1, 0]), |
| 96 | + g[2].animate.move_to([-1, 1, 0]), |
| 97 | + g[3].animate.move_to([1, -1, 0]), |
| 98 | + g[4].animate.move_to([-1, -1, 0])) |
| 99 | + self.wait() |
| 100 | +
|
| 101 | + There are several automatic positioning algorithms to choose from: |
| 102 | +
|
| 103 | + .. manim:: GraphAutoPosition |
| 104 | + :save_last_frame: |
| 105 | +
|
| 106 | + class GraphAutoPosition(Scene): |
| 107 | + def construct(self): |
| 108 | + vertices = [1, 2, 3, 4, 5, 6, 7, 8] |
| 109 | + edges = [(1, 7), (1, 8), (2, 3), (2, 4), (2, 5), |
| 110 | + (2, 8), (3, 4), (6, 1), (6, 2), |
| 111 | + (6, 3), (7, 2), (7, 4)] |
| 112 | + autolayouts = ["spring", "circular", "kamada_kawai", |
| 113 | + "planar", "random", "shell", |
| 114 | + "spectral", "spiral"] |
| 115 | + graphs = [Graph(vertices, edges, layout=lt).scale(0.5) |
| 116 | + for lt in autolayouts] |
| 117 | + r1 = VGroup(*graphs[:3]).arrange() |
| 118 | + r2 = VGroup(*graphs[3:6]).arrange() |
| 119 | + r3 = VGroup(*graphs[6:]).arrange() |
| 120 | + self.add(VGroup(r1, r2, r3).arrange(direction=DOWN)) |
| 121 | +
|
| 122 | + Vertices can also be positioned manually: |
| 123 | +
|
| 124 | + .. manim:: GraphManualPosition |
| 125 | + :save_last_frame: |
| 126 | +
|
| 127 | + class GraphManualPosition(Scene): |
| 128 | + def construct(self): |
| 129 | + vertices = [1, 2, 3, 4] |
| 130 | + edges = [(1, 2), (2, 3), (3, 4), (4, 1)] |
| 131 | + lt = {1: [0, 0, 0], 2: [1, 1, 0], 3: [1, -1, 0], 4: [-1, 0, 0]} |
| 132 | + G = Graph(vertices, edges, layout=lt) |
| 133 | + self.add(G) |
| 134 | +
|
| 135 | + The vertices in graphs can be labeled, and configurations for vertices |
| 136 | + and edges can be modified both by default and for specific vertices and |
| 137 | + edges. |
| 138 | +
|
| 139 | + .. note:: |
| 140 | +
|
| 141 | + In ``edge_config``, edges can be passed in both directions: if |
| 142 | + ``(u, v)`` is an edge in the graph, both ``(u, v)`` as well |
| 143 | + as ``(v, u)`` can be used as keys in the dictionary. |
| 144 | +
|
| 145 | + .. manim:: LabeledModifiedGraph |
| 146 | + :save_last_frame: |
| 147 | +
|
| 148 | + class LabeledModifiedGraph(Scene): |
| 149 | + def construct(self): |
| 150 | + vertices = [1, 2, 3, 4, 5, 6, 7, 8] |
| 151 | + edges = [(1, 7), (1, 8), (2, 3), (2, 4), (2, 5), |
| 152 | + (2, 8), (3, 4), (6, 1), (6, 2), |
| 153 | + (6, 3), (7, 2), (7, 4)] |
| 154 | + g = Graph(vertices, edges, layout="circular", layout_scale=3, |
| 155 | + labels=True, vertex_config={7: {"fill_color": RED}}, |
| 156 | + edge_config={(1, 7): {"stroke_color": RED}, |
| 157 | + (2, 7): {"stroke_color": RED}, |
| 158 | + (4, 7): {"stroke_color": RED}}) |
| 159 | + self.add(g) |
| 160 | +
|
| 161 | + """ |
| 162 | + |
| 163 | + def __init__( |
| 164 | + self, |
| 165 | + vertices: List[Hashable], |
| 166 | + edges: List[Tuple[Hashable, Hashable]], |
| 167 | + labels: bool = False, |
| 168 | + label_fill_color: str = BLACK, |
| 169 | + layout: Union[str, dict] = "spring", |
| 170 | + layout_scale: float = 2, |
| 171 | + layout_config: Union[dict, None] = None, |
| 172 | + vertex_type: "Mobject" = Dot, |
| 173 | + vertex_config: Union[dict, None] = None, |
| 174 | + edge_type: "Mobject" = Line, |
| 175 | + edge_config: Union[dict, None] = None, |
| 176 | + ) -> None: |
| 177 | + VMobject.__init__(self) |
| 178 | + |
| 179 | + nx_graph = nx.Graph() |
| 180 | + nx_graph.add_nodes_from(vertices) |
| 181 | + nx_graph.add_edges_from(edges) |
| 182 | + self._graph = nx_graph |
| 183 | + |
| 184 | + automatic_layouts = { |
| 185 | + "circular": nx.layout.circular_layout, |
| 186 | + "kamada_kawai": nx.layout.kamada_kawai_layout, |
| 187 | + "planar": nx.layout.planar_layout, |
| 188 | + "random": nx.layout.random_layout, |
| 189 | + "shell": nx.layout.shell_layout, |
| 190 | + "spectral": nx.layout.spectral_layout, |
| 191 | + "spiral": nx.layout.spiral_layout, |
| 192 | + "spring": nx.layout.spring_layout, |
| 193 | + } |
| 194 | + |
| 195 | + if layout_config is None: |
| 196 | + layout_config = {} |
| 197 | + |
| 198 | + if isinstance(layout, dict): |
| 199 | + self._layout = layout |
| 200 | + elif layout in automatic_layouts and layout != "random": |
| 201 | + self._layout = automatic_layouts[layout]( |
| 202 | + nx_graph, scale=layout_scale, **layout_config |
| 203 | + ) |
| 204 | + self._layout = dict( |
| 205 | + [(k, np.append(v, [0])) for k, v in self._layout.items()] |
| 206 | + ) |
| 207 | + elif layout == "random": |
| 208 | + # the random layout places coordinates in [0, 1) |
| 209 | + # we need to rescale manually afterwards... |
| 210 | + self._layout = automatic_layouts["random"](nx_graph, **layout_config) |
| 211 | + for k, v in self._layout.items(): |
| 212 | + self._layout[k] = 2 * layout_scale * (v - np.array([0.5, 0.5])) |
| 213 | + self._layout = dict( |
| 214 | + [(k, np.append(v, [0])) for k, v in self._layout.items()] |
| 215 | + ) |
| 216 | + else: |
| 217 | + raise ValueError( |
| 218 | + f"The layout '{layout}' is neither a recognized automatic layout, " |
| 219 | + "nor a vertex placement dictionary." |
| 220 | + ) |
| 221 | + |
| 222 | + if isinstance(labels, dict): |
| 223 | + self._labels = labels |
| 224 | + elif isinstance(labels, bool): |
| 225 | + if labels: |
| 226 | + self._labels = dict( |
| 227 | + [(v, MathTex(v, fill_color=label_fill_color)) for v in vertices] |
| 228 | + ) |
| 229 | + else: |
| 230 | + self._labels = dict() |
| 231 | + |
| 232 | + if self._labels and vertex_type is Dot: |
| 233 | + vertex_type = LabeledDot |
| 234 | + |
| 235 | + # build vertex_config |
| 236 | + if vertex_config is None: |
| 237 | + vertex_config = {} |
| 238 | + default_vertex_config = {} |
| 239 | + if vertex_config: |
| 240 | + default_vertex_config = dict( |
| 241 | + [(k, v) for k, v in vertex_config.items() if k not in vertices] |
| 242 | + ) |
| 243 | + self._vertex_config = dict( |
| 244 | + [(v, vertex_config.get(v, copy(default_vertex_config))) for v in vertices] |
| 245 | + ) |
| 246 | + for v, label in self._labels.items(): |
| 247 | + self._vertex_config[v]["label"] = label |
| 248 | + |
| 249 | + self.vertices = dict( |
| 250 | + [(v, vertex_type(**self._vertex_config[v])) for v in vertices] |
| 251 | + ) |
| 252 | + for v in self.vertices: |
| 253 | + self[v].move_to(self._layout[v]) |
| 254 | + |
| 255 | + # build edge_config |
| 256 | + if edge_config is None: |
| 257 | + edge_config = {} |
| 258 | + default_edge_config = {} |
| 259 | + if edge_config: |
| 260 | + default_edge_config = dict( |
| 261 | + (k, v) |
| 262 | + for k, v in edge_config.items() |
| 263 | + if k not in edges and k[::-1] not in edges |
| 264 | + ) |
| 265 | + self._edge_config = {} |
| 266 | + for e in edges: |
| 267 | + if e in edge_config: |
| 268 | + self._edge_config[e] = edge_config[e] |
| 269 | + elif e[::-1] in edge_config: |
| 270 | + self._edge_config[e] = edge_config[e[::-1]] |
| 271 | + else: |
| 272 | + self._edge_config[e] = copy(default_edge_config) |
| 273 | + |
| 274 | + self.edges = dict( |
| 275 | + [ |
| 276 | + ( |
| 277 | + (u, v), |
| 278 | + edge_type( |
| 279 | + self[u].get_center(), |
| 280 | + self[v].get_center(), |
| 281 | + z_index=-1, |
| 282 | + **self._edge_config[(u, v)], |
| 283 | + ), |
| 284 | + ) |
| 285 | + for (u, v) in edges |
| 286 | + ] |
| 287 | + ) |
| 288 | + |
| 289 | + self.add(*self.vertices.values()) |
| 290 | + self.add(*self.edges.values()) |
| 291 | + |
| 292 | + for (u, v), edge in self.edges.items(): |
| 293 | + |
| 294 | + def update_edge(e, u=u, v=v): |
| 295 | + e.set_start_and_end_attrs(self[u].get_center(), self[v].get_center()) |
| 296 | + e.generate_points() |
| 297 | + |
| 298 | + update_edge(edge) |
| 299 | + edge.add_updater(update_edge) |
| 300 | + |
| 301 | + def __getitem__(self: "Graph", v: Hashable) -> "Mobject": |
| 302 | + return self.vertices[v] |
| 303 | + |
| 304 | + def __repr__(self: "Graph") -> str: |
| 305 | + return f"Graph on {len(self.vertices)} vertices and {len(self.edges)} edges" |
0 commit comments