from __future__ import annotations
from abc import ABC, abstractmethod
from copy import deepcopy
from math import *
from typing import Self, override
import networkx as nx
from manim import *
from manim.typing import Point3D, Vector3D
from manim_dsa.constants import *
from manim_dsa.m_collection.m_collection import *
from manim_dsa.utils.utils import *
[docs]
class MGraph(VDict, Labelable):
"""Manim Graph: a class for visualizing the graph data structure using the Manim animation engine.
Parameters
----------
graph : :class:`GraphType`
The graph representation, which can be weighted or unweighted. Can be:
- ``list[list[str]]`` or ``dict[str, list[str]]`` for unweighted graph
- ``list[list[tuple[str, str | int]]]`` or ``dict[str, list[tuple[str, str | int]]]`` for weighted graph
nodes_position : dict[str, :class:`~manim.typing.Vector3D`], optional
A dictionary mapping node labels to their positions as 3D vectors. Defaults to an empty dict.
style : :class:`MGraphStyle._DefaultStyle`, optional
The style configuration to be applied to the graph. Defaults to ``MGraphStyle.DEFAULT``.
"""
def __init__(
self,
graph: GraphType,
nodes_position: dict[str, Vector3D] = {},
style: MGraphStyle._DefaultStyle = MGraphStyle.DEFAULT,
):
super().__init__()
self.nodes: dict[str, MGraph.Node] = {}
self.edges: dict[tuple[str, str], MGraph.Edge] = {}
self.style = deepcopy(style)
nxGraph = get_nx_graph(graph)
for node in nxGraph.nodes():
pos: Vector3D = nodes_position.get(str(node), ORIGIN)
self.add_node(str(node), pos)
for u, v, wt in nxGraph.edges.data("weight"):
if wt:
self.add_edge(u, v, wt)
else:
self.add_edge(u, v)
if not nodes_position:
self.node_layout()
[docs]
class Node(VGroup, Highlightable):
"""A class that represents a node (or vertex) of the graph.
Parameters
----------
circle : :class:`~manim.mobject.geometry.arc.Circle`
The circular shape that visually represents the node.
label : :class:`~manim.mobject.text.text_mobject.Text`
The text label associated with the node.
"""
def __init__(self, circle: Circle, label: Text):
super().__init__()
self.circle = circle.set_z_index(2)
self.label = label.set_z_index(3)
self._add_highlight(self.circle)
self += self.circle
self += self.label
[docs]
def get_radius(self) -> float:
"""Returns the radius of the node's circle.
Returns
-------
float
The radius of the node's circle.
"""
return self.width / 2
[docs]
class Edge(VGroup, Highlightable, ABC):
"""An abstract class that represents an edge in the graph.
Parameters
----------
line : :class:`~manim.mobject.geometry.line.Line` or :class:`~manim.mobject.geometry.arc.ArcBetweenPoints`
The line or arc that visually represents the edge between two nodes.
start : :class:`~manim.typing.Point3D`
The starting point of the edge.
end : :class:`~manim.typing.Point3D`
The ending point of the edge.
arrow : :class:`~manim.mobject.geometry.tips.ArrowTriangleFilledTip` or ``None``
The arrow tip to be added to the edge, if any.
"""
def __init__(
self,
line: Line | ArcBetweenPoints,
start: Point3D,
end: Point3D,
arrow: ArrowTriangleFilledTip | None,
):
super().__init__()
self.label: Text = None
self.line: Line | ArcBetweenPoints = line.set_z_index(0)
self.line.put_start_and_end_on(start, end)
if arrow:
self.line.add_tip(arrow)
self += self.line
[docs]
def weighted(self, label: Text) -> Self:
"""Assigns a label to the edge, indicating that it is weighted.
Parameters
----------
label : :class:`~manim.mobject.text.text_mobject.Text`
The label to be assigned to the edge, representing its weight or any other relevant information.
Returns
-------
self
The instance of the :class:`MGraph.Edge` with the applied highlight.
"""
self.label: Text = label
self += self.label
return self
[docs]
def is_weighted(self) -> bool:
"""Checks if the edge is weighted by examining the presence of a label.
Returns
-------
bool
``True`` if the edge has a label (indicating it is weighted), ``False`` otherwise.
"""
return self.label is not None
[docs]
def set_highlight(
self,
stroke_color: ManimColor = RED,
stroke_width: float = 8,
) -> Self:
"""Sets the highlight properties for the edge.
Parameters
----------
stroke_color : :class:`~manim.utils.color.ManimColor`, optional
The color to be used for highlighting the edge. Defaults to ``RED``.
stroke_width : float, optional
The width of the stroke used for highlighting. Defaults to ``8``.
Returns
-------
self
The instance of the :class:`MGraph.Edge` with the applied highlight.
"""
super().set_highlight(stroke_color, stroke_width)
if self.line.has_tip():
arrow_width: float = self.line.get_tip().get_width()
self.highlighting.get_tip().set_stroke(width=arrow_width).set_color(
stroke_color
).set_opacity(1)
return self
@abstractmethod
def _get_line_start_end(
self,
node1: Circle,
node2: Circle,
start_distance: float,
) -> tuple[Point3D, Point3D]:
"""Abstract method to determine the start and end points of a line between two nodes.
This method should be implemented to calculate the exact positions where the line
should start and end, taking into account the positions and radii of the nodes.
Parameters
----------
node1 : :class:`~manim.mobject.geometry.arc.Circle`
The start node (circle) of the edge.
node2 : :class:`~manim.mobject.geometry.arc.Circle`
The destination node (circle) of the edge.
start_distance : float
Specifies how far the line starts from the node, rather than starting directly at its edge. Expressed as a percentage of the node’s radius.
Returns
-------
tuple[:class:`~manim.typing.Point3D`, :class:`~manim.typing.Point3D`]
A tuple containing two :class:`~manim.typing.Point3D` objects representing the start and end points
of the line connecting the two nodes.
"""
pass
@abstractmethod
def _get_label_position(self, label_distance: float) -> Point3D:
"""Abstract method to determine the position of the weight relative to the edge.
Parameters
----------
label_distance : float
The distance from the line or edge where the label should be positioned.
Returns
-------
:class:`~manim.typing.Point3D`
The :class:`~manim.typing.Point3D` coordinates representing the position of the label.
"""
pass
[docs]
class StraightEdge(Edge):
"""Represents a straight edge in the graph, connecting two nodes with a straight line.
Parameters
----------
line : :class:`~manim.mobject.geometry.line.Line`
The Line object representing the edge.
node1 : :class:`~manim.mobject.geometry.arc.Circle`
The start node (circle) of the edge.
node2 : :class:`~manim.mobject.geometry.arc.Circle`
The destination node (circle) of the edge.
arrow : :class:`~manim.mobject.geometry.tips.ArrowTriangleFilledTip` or ``None``
The arrow tip to be added to the edge, if any.
"""
def __init__(
self,
line: Line,
node1: Circle,
node2: Circle,
arrow: ArrowTriangleFilledTip | None,
start_distance: float,
):
start, end = self._get_line_start_end(
node1,
node2,
start_distance,
)
super().__init__(line, start, end, arrow)
self._add_highlight(self.line)
[docs]
def weighted(
self,
label: Text,
label_distance: float = 0.3,
) -> Self:
"""Assigns a label (the weight) to the edge and positions it relative to the edge.
Parameters
----------
label : :class:`~manim.mobject.text.text_mobject.Text`
The label to be assigned to the edge.
label_distance : float, optional
The distance from the edge to position the label. Defaults to ``0.3``.
Returns
-------
self
The instance of the :class:`MGraph.Edge` with the applied highlight.ssigned label.
"""
super().weighted(label)
self.label_distance: float = label_distance
label_position: Point3D = self._get_label_position(label_distance)
self.label.move_to(label_position)
return self
def _get_line_start_end(
self,
node1: Circle,
node2: Circle,
start_distance: float,
) -> tuple[Point3D, Point3D]:
"""Determines the start and end points of the line based on node positions and radii.
Parameters
----------
node1 : :class:`~manim.mobject.geometry.arc.Circle`
The start node (circle) of the edge.
node2 : :class:`~manim.mobject.geometry.arc.Circle`
The destination node (circle) of the edge.
start_distance : float
Specifies how far the line starts from the node, rather than starting directly at its edge. Expressed as a percentage of the node’s radius.
Returns
-------
tuple[:class:`~manim.typing.Point3D`, :class:`~manim.typing.Point3D`]
A tuple containing two :class:`~manim.typing.Point3D` objects representing the start and end points of the line.
"""
c1, c2 = node1.get_center(), node2.get_center()
r1, r2 = node1.get_radius(), node2.get_radius()
direction = Line(c1, c2).get_unit_vector()
start = c1 + direction * r1 * (1 + start_distance)
end = c2 - direction * r2 * (1 + start_distance)
if np.array_equal(start, end):
start = LEFT
end = RIGHT
return start, end
def _get_label_position(self, label_distance: float) -> Point3D:
"""Calculates the position of the label (the weight) relative to the edge.
Parameters
----------
label_distance : float
The distance from the edge to position the label.
Returns
-------
:class:`~manim.typing.Point3D`
The 3D coordinates where the label should be positioned.
"""
direction: Point3D = self.line.get_unit_vector()
mean: Point3D = (
self.line.get_start() + direction * self.line.get_length() / 2
)
orthogonal_dir: Point3D = np.array([direction[1], -direction[0], 0])
position: Point3D = mean + orthogonal_dir * label_distance
return position
[docs]
class CurvedEdge(Edge):
"""Represents a curved edge in the graph, connecting two nodes with an arc.
Parameters
----------
line : :class:`~manim.mobject.geometry.arc.ArcBetweenPoints`
The ArcBetweenPoints object representing the edge.
node1 : :class:`~manim.mobject.geometry.arc.Circle`
The start node (circle) of the edge.
node2 : :class:`~manim.mobject.geometry.arc.Circle`
The destination node (circle) of the edge.
arrow : :class:`~manim.mobject.geometry.tips.ArrowTriangleFilledTip` or ``None``
The arrow tip to be added to the edge, if any.
node_angle : float, optional
The angle between the line connecting the nodes and the direction of the arc. Defaults to ``PI/3``.
arc_angle : float, optional
The angle of the arc between the two nodes. Defaults to ``PI/3``.
"""
def __init__(
self,
line: ArcBetweenPoints,
node1: Circle,
node2: Circle,
arrow: ArrowTriangleFilledTip | None,
start_distance: float,
node_angle: float = PI / 3,
arc_angle: float = PI / 3,
):
start, end = self._get_line_start_end(
node1,
node2,
start_distance,
node_angle,
)
super().__init__(line, start, end, arrow)
self.arc_angle: float = arc_angle
self._add_highlight(self.line)
[docs]
def weighted(
self,
label: Text,
label_distance: float = 0.3,
) -> Self:
"""Assigns a label (the weight) to the edge and positions it relative to the edge.
Parameters
----------
label : :class:`~manim.mobject.text.text_mobject.Text`
The label to be assigned to the edge.
label_distance : float, optional
The distance from the edge to position the label. Defaults to ``0.3``.
Returns
-------
self
The instance of the :class:`MGraph.Edge` with the applied highlight.ssigned label.
"""
super().weighted(label)
self.label_distance: float = label_distance
label_position = self._get_label_position(label_distance)
self.label.move_to(label_position)
return self
def _get_line_start_end(
self,
node1: Circle,
node2: Circle,
start_distance: float,
start_angle: float = PI / 3,
) -> tuple[Point3D, Point3D]:
"""Calculates the start and end points of the arc considering node positions, radii, and the start angle.
This method computes the positions where the edge should start and end relative to the given nodes,
taking into account their radii and the starting angle.
Parameters
----------
node1 : :class:`~manim.mobject.geometry.arc.Circle`
The start node (circle) of the edge.
node2 : :class:`~manim.mobject.geometry.arc.Circle`
The destination node (circle) of the edge.
start_distance : float, optional
Specifies how far the line starts from the node, rather than starting directly at its edge. Expressed as a percentage of the node’s radius.
start_angle : float, optional
The angle between the edge direction and the line's start direction. Defaults to ``PI/3``.
Returns
-------
tuple[:class:`~manim.typing.Point3D`, :class:`~manim.typing.Point3D`]
A tuple containing two :class:`~manim.typing.Point3D` objects representing the start and end points of the edge.
"""
c1, c2 = node1.get_center(), node2.get_center()
r1, r2 = node1.get_radius(), node2.get_radius()
edge_dir = Line(c1, c2).get_unit_vector()
edge_angle = acos(edge_dir[0])
if edge_dir[1] < 0:
edge_angle = -edge_angle
vector_start = [
cos(edge_angle - start_angle),
sin(edge_angle - start_angle),
0,
]
vector_end = [
cos(edge_angle - (PI - start_angle)),
sin(edge_angle - (PI - start_angle)),
0,
]
start_dir = normalize(vector_start)
end_dir = normalize(vector_end)
start = c1 + start_dir * r1 * (1 + start_distance)
end = c2 + end_dir * r2 * (1 + start_distance)
return start, end
def _get_label_position(self, label_distance: float) -> Point3D:
"""Calculates the position of the label relative to the edge.
This method computes where the label should be placed based on the distance from the edge and the arc of the edge.
Parameters
----------
label_distance : float
The distance from the edge at which the label should be positioned.
Returns
-------
:class:`~manim.typing.Point3D`
The 3D coordinates where the label should be positioned relative to the edge.
"""
arc = ArcBetweenPoints(
self.line.get_start(),
self.line.get_end(),
self.arc_angle,
)
line = Line(self.line.get_start(), self.line.get_end())
direction = line.get_unit_vector()
orthogonal_dir = np.array([direction[1], -direction[0], 0])
position = (
arc.get_boundary_point(orthogonal_dir)
+ orthogonal_dir * len(line) * label_distance
)
return position
[docs]
def add_node(self, name: str, position: Point3D = ORIGIN) -> Self:
"""Adds a new node to the graph with a specified name and position.
Parameters
----------
name : str
The name of the node to be added.
position : :class:`~manim.typing.Point3D`, optional
The 3D position where the node will be placed. Defaults to ``ORIGIN``.
Returns
-------
self
The updated instance of the :class:`MGraph` with the new node added.
"""
new_node = self.Node(
Circle(**self.style.node_circle).move_to(position),
Text(str(name), **self.style.node_label).move_to(position),
)
self.nodes[name] = new_node
self.add([(name, new_node)])
return self
@override_animate(add_node)
def _add_node_animation(
self,
name: str,
position: Point3D = ORIGIN,
anim_args: dict = None,
) -> Create:
"""Animates the addition of a new node to the graph.
Parameters
----------
name : str
The name of the node to be added.
position : :class:`~manim.typing.Point3D`, optional
The 3D position where the node will be placed. Defaults to ``ORIGIN``.
anim_args : dict, optional
Additional arguments to be passed to the animation. Defaults to ``None``.
Returns
-------
:class:`~manim.animation.creation.Create`
The animation for adding the node.
"""
self.add_node(name, position)
return Create(self.nodes[name], **anim_args)
[docs]
def add_edge(
self,
node1_name: str,
node2_name: str,
weight: float = None,
label_distance: float = 0.25,
) -> Self:
"""Adds a new edge between two nodes in the graph.
Parameters
----------
node1_name : str
The name of the first node.
node2_name : str
The name of the second node.
weight : float, optional
The weight of the edge. If not provided, the edge will be unweighted.
label_distance : float, optional
The distance from the edge where the label should be placed. Defaults to ``0.2``.
Returns
-------
self
The updated instance of the :class:`MGraph` with the new edge added.
"""
edge_name = (node1_name, node2_name)
edge_name_rev = (node2_name, node1_name)
node1 = self.nodes[node1_name].circle
node2 = self.nodes[node2_name].circle
reverse_exists = edge_name_rev in self.edges
line = Line(**self.style.edge_line)
arrow = (
ArrowTriangleFilledTip(**self.style.edge_tip)
if not reverse_exists
else None
)
new_edge = self.StraightEdge(
line,
node1,
node2,
arrow,
self.style.start_distance,
)
if weight:
new_edge.weighted(
Text(str(weight), **self.style.edge_weight),
label_distance,
)
if edge_name_rev in self.edges:
new_edge_rev_node = self.StraightEdge(
line,
node2,
node1,
None,
self.style.start_distance,
)
if weight:
new_edge_rev_node.weighted(new_edge.label, label_distance)
self.remove(edge_name_rev)
self.edges[edge_name_rev] = new_edge_rev_node
self.add([(edge_name_rev, new_edge_rev_node)])
self.edges[edge_name] = new_edge
self.add([(edge_name, new_edge)])
return self
@override_animate(add_edge)
def _add_edge_animation(
self,
node1_name: str,
node2_name: str,
weight: float = None,
label_distance: float = 0.3,
anim_args: dict = None,
) -> Create:
"""Animates the addition of an edge between two nodes in the graph.
Parameters
----------
node1_name : str
The name of the first node.
node2_name : str
The name of the second node.
weight : float, optional
The weight of the edge. If not provided, the edge will be unweighted.
label_distance : float, optional
The distance from the edge where the label should be placed. Defaults to ``0.3``.
anim_args : dict, optional
Additional arguments to be passed to the animation. Defaults to ``None``.
Returns
-------
:class:`~manim.animation.creation.Create`
The animation for adding the edge.
"""
self.add_edge(node1_name, node2_name, weight, label_distance)
return Create(
self.edges[(node1_name, node2_name)],
**anim_args,
)
[docs]
def add_curved_edge(
self,
node1_name: str,
node2_name: str,
weight: float = None,
label_distance: float = 0.3,
node_angle: float = PI / 3,
arc_angle: float = PI / 3,
) -> Self:
"""Adds a new curved edge between two nodes in the graph.
Parameters
----------
node1_name : str
The name of the first node.
node2_name : str
The name of the second node.
weight : float, optional
The weight of the edge. If not provided, the edge will be unweighted.
label_distance : float, optional
The distance from the edge where the label should be placed. Defaults to ``0.2``.
node_angle : float, optional
The start angle of the arc between the two nodes. Defaults to ``PI/3``.
arc_angle : float, optional
The angle of the arc between the two nodes. Defaults to ``PI/3``.
Returns
-------
self
The updated instance of the :class:`MGraph` with the new curved edge added.
"""
edge_name = (node1_name, node2_name)
edge_name_rev = (node2_name, node1_name)
node1 = self.nodes[node1_name].circle
node2 = self.nodes[node2_name].circle
reverse_exists = edge_name_rev in self.edges
line = ArcBetweenPoints(LEFT, RIGHT, **self.style.edge_line, angle=arc_angle)
arrow = (
ArrowTriangleFilledTip(**self.style.edge_tip)
if not reverse_exists
else None
)
new_edge = self.CurvedEdge(
line,
node1,
node2,
arrow,
self.style.start_distance,
node_angle,
arc_angle,
)
if weight:
new_edge.weighted(
Text(str(weight), **self.style.edge_weight),
label_distance,
)
if edge_name_rev in self.edges:
new_edge_rev = self.CurvedEdge(
new_edge.line,
node1,
node2,
None,
self.style.start_distance,
node_angle,
arc_angle,
)
if weight:
new_edge.weighted(
new_edge.label,
label_distance,
)
self.remove(edge_name_rev)
self.edges[edge_name_rev] = new_edge_rev
self.add([(edge_name_rev, new_edge_rev)])
self.edges[edge_name] = new_edge
self.add([(edge_name, new_edge)])
return self
@override_animate(add_curved_edge)
def _add_curved_edge_animation(
self,
node1_name: str,
node2_name: str,
weight: float = None,
label_distance: float = 0.3,
node_angle: float = PI / 3,
arc_angle: float = PI / 3,
anim_args: dict = None,
) -> Create:
"""Animates the addition of a curved edge between two nodes in the graph.
Parameters
----------
node1_name : str
The name of the first node.
node2_name : str
The name of the second node.
weight : float, optional
The weight of the edge. If not provided, the edge will be unweighted.
label_distance : float, optional
The distance from the edge where the label should be placed. Defaults to ``0.2``.
node_angle : float, optional
The start angle of the arc between the two nodes. Defaults to ``PI/3``.
arc_angle : float, optional
The angle of the arc between the two nodes. Defaults to ``PI/3``.
anim_args : dict, optional
Additional arguments to be passed to the animation. Defaults to ``None``.
Returns
-------
:class:`~manim.animation.creation.Create`
The animation for adding the curved edge.
"""
self.add_curved_edge(
node1_name,
node2_name,
weight,
label_distance,
node_angle,
arc_angle,
)
return Create(
self.edges[(node1_name, node2_name)],
**anim_args,
)
# TODO
def show_backward_edge(
self,
node1_name: str,
node2_name: str,
forward_weight: float,
backward_weight: float,
label_distance: float = 0.3,
node_angle: float = PI / 6,
arc_angle: float = PI / 6,
) -> Self:
edge_name = (node1_name, node2_name)
edge_name_rev = (node2_name, node1_name)
node1 = self.nodes[node1_name].circle
node2 = self.nodes[node2_name].circle
line = ArcBetweenPoints(LEFT, RIGHT, **self.style.edge_line, angle=arc_angle)
arrow = ArrowTriangleFilledTip(**self.style.edge_tip)
new_edge_1 = self.CurvedEdge(line, node1, node2, arrow, node_angle, arc_angle)
new_edge_1.weighted(
Text(
str(forward_weight),
**self.style.edge_weight,
),
label_distance,
)
line = ArcBetweenPoints(LEFT, RIGHT, **self.style.edge_line, angle=arc_angle)
arrow = ArrowTriangleFilledTip(**self.style.edge_tip)
new_edge_2 = self.CurvedEdge(
line,
node2,
node1,
arrow,
node_angle,
)
new_edge_2.weighted(
Text(
str(backward_weight),
**self.style.edge_weight,
),
label_distance,
)
self.edges[edge_name] = self[edge_name] = new_edge_1
self.edges[edge_name_rev] = new_edge_2
self.add([(edge_name_rev, new_edge_2)])
return self
@override_animate(show_backward_edge)
def _show_backward_edge_animation(
self,
node1_name: str,
node2_name: str,
forward_weight: float,
backward_weight: float,
label_distance: float = 0.3,
node_angle: float = PI / 6,
arc_angle: float = PI / 6,
anim_args: dict = None,
):
edge_name = (node1_name, node2_name)
edge_name_rev = (node2_name, node1_name)
old_edge = self.edges[edge_name]
self.show_backward_edge(
node1_name,
node2_name,
forward_weight,
backward_weight,
label_distance,
node_angle,
arc_angle,
)
return Succession(
ReplacementTransform(
old_edge,
VGroup(
self.edges[edge_name],
self.edges[edge_name_rev],
),
**anim_args,
)
)
def _node_layout(
self, pos: dict[str, tuple[float, float]], fit: bool = True
) -> Self:
"""Helper method that rearranges the nodes and edges based on the provided positions of the nodes.
Parameters
----------
pos : dict[str, tuple[float, float]]
A dictionary mapping node labels to their positions as tuples of (x, y) coordinates.
fit : bool, optional
If ``True``, the graph will be scaled to fit within the frame. Defaults to ``True``.
Returns
-------
self
The updated instance of the :class:`MGraph` with nodes arranged according to the specified layout.
"""
labels = list(pos.keys())
x = [x for x, _ in pos.values()]
y = [y for _, y in pos.values()]
# Calculate coefficients to fit the graph within the frame
coeff_x = config.frame_x_radius / (abs(max(x) - min(x))) if fit else 1
coeff_y = config.frame_y_radius / (abs(max(y) - min(y))) if fit else 1
positions = []
for label in labels:
positions.append(
[
pos.get(label)[0] * coeff_x,
pos.get(label)[1] * coeff_y,
0,
]
)
nodes_and_positions = dict(zip(labels, positions, strict=False))
for node in nodes_and_positions:
self.nodes[node].move_to(nodes_and_positions[node])
for edge in self.edges:
node1 = self.nodes[edge[0]].circle
node2 = self.nodes[edge[1]].circle
start, end = self.edges[edge]._get_line_start_end(
node1,
node2,
self.style.start_distance,
)
mEdge = self.edges[edge]
# Workaround cause tipped lines can't be changed of start/end, we have to delete the tip for a moment
if mEdge.line.has_tip():
tip = mEdge.line.get_tip()
mEdge.line.remove(tip)
mEdge.line.put_start_and_end_on(start, end)
mEdge.line.add_tip()
else:
mEdge.line.put_start_and_end_on(start, end)
mEdge.highlighting.put_start_and_end_on(start, end)
if mEdge.is_weighted():
label_position = mEdge._get_label_position(mEdge.label_distance)
mEdge.label.move_to(label_position)
self.move_to(ORIGIN)
return self
[docs]
def node_layout(self, layout: str = "kamada_kawai_layout") -> Self:
"""Applies a specified layout algorithm to arrange the nodes in the graph.
Parameters
----------
layout : str, optional
The name of the layout algorithm to be applied to the nodes.
Defaults to 'kamada_kawai_layout'. Other common layout options may include 'spring_layout', 'circular_layout',
'shell_layout', and others supported by the underlying graph library.
A full list of available layouts can be found in the NetworkX documentation:
https://networkx.org/documentation/stable/reference/drawing.html#module-networkx.drawing.layout
Returns
-------
self
The updated instance of the :class:`MGraph` with nodes arranged according to the specified layout.
"""
G = nx.DiGraph()
G.add_edges_from(self.edges.keys())
try:
layout_function = eval(f"nx.{layout}")
pos: dict[str, tuple[float, float]] = layout_function(G)
except AttributeError:
print("Layout not available")
pos: dict[str, tuple[float, float]] = nx.kamada_kawai_layout(G)
return self._node_layout(pos)
[docs]
def set_nodes_highlight(
self,
color: ManimColor = RED,
width: float = 8,
) -> Self:
"""This method iterates through all the nodes in the graph and applies the specified highlight color and stroke width.
Parameters
----------
color : :class:`~manim.utils.color.ManimColor`, optional
The color to be used for highlighting the nodes.
Defaults to ``RED``.
width : float, optional
The stroke width of the highlight.
Defaults to ``8``.
Returns
-------
self
The updated instance of the :class:`MGraph` with all nodes highlighted.
"""
for node in self.nodes:
self.nodes[node].set_highlight(color, width)
return self
[docs]
def set_edges_highlight(
self,
color: ManimColor = RED,
width: float = 8,
) -> Self:
"""This method iterates through all the edges in the graph and applies the specified highlight color and stroke width.
Parameters
----------
color : :class:`~manim.utils.color.ManimColor`, optional
The color to be used for highlighting the edges.
Defaults to ``RED``.
width : float, optional
The stroke width of the highlight.
Defaults to ``8``.
Returns
-------
self
The updated instance of the :class:`MGraph` with all edges highlighted.
"""
for edge in self.edges:
self.edges[edge].set_highlight(color, width)
return self
[docs]
def add_label(
self,
text: Text,
direction: Vector3D = UP,
buff: float = 0.5,
**kwargs,
) -> Self:
"""Adds a label to the graph with specified alignment and buffer.
Parameters
----------
text : :class:`~manim.mobject.text.text_mobject.Text`
The label text to be added to the graph.
direction : :class:`~manim.typing.Vector3D`, optional
The direction in which the label should be positioned relative to the graph.
Defaults to ``UP``.
buff : float, optional
The distance between the graph and the label.
Defaults to ``0.5``.
**kwargs
Additional keyword arguments that are passed to the function next_to() of the
underlying add_label method.
Returns
-------
self
The updated instance of the :class:`MGraph` with the label added.
"""
super().add_label(text, direction, buff, **kwargs)
self["label"] = self.label
return self