Bases: AnyWidget
Programmatic force-directed graph widget.
GraphWidget renders nodes and edges supplied from Python. Nodes may be
strings, numbers, or dicts. Edges may be (source, target) pairs or dicts
with source and target keys.
width=None (the default) makes the widget fill its container's width
and reflow when the container resizes. Passing an integer width pins
the SVG to that exact pixel size. height is always an exact pixel
height (default 400).
Example
import marimo as mo
from wigglystuff import GraphWidget
widget = mo.ui.anywidget(GraphWidget(nodes=["Alpha", "Beta"], edges=[("Alpha", "Beta")]))
widget
Source code in wigglystuff/graph_widget.py
| def __init__(
self,
nodes: Iterable[Any] | None = None,
edges: Iterable[Any] | None = None,
*,
directed: bool = True,
bounded: bool = True,
width: int | None = None,
height: int = 400,
**kwargs: Any,
) -> None:
prepared_nodes = self._coerce_nodes(nodes or [])
prepared_edges = self._coerce_edges(edges or [], prepared_nodes)
super().__init__(
nodes=prepared_nodes,
edges=prepared_edges,
directed=directed,
bounded=bounded,
width=width,
height=height,
**kwargs,
)
|
add_edge(source: Any, target: Any, *, id: Any = None, name: Any = None, width: int | float | None = None, color: str | None = None, data: Any = None, **attrs: Any) -> str
Add an edge and return its normalized id.
Source code in wigglystuff/graph_widget.py
| def add_edge(
self,
source: Any,
target: Any,
*,
id: Any = None,
name: Any = None,
width: int | float | None = None,
color: str | None = None,
data: Any = None,
**attrs: Any,
) -> str:
"""Add an edge and return its normalized id."""
edge = {"source": source, "target": target, **attrs}
if id is not None:
edge["id"] = id
if name is not None:
edge["name"] = name
if width is not None:
edge["width"] = width
if color is not None:
edge["color"] = color
if data is not None:
edge["data"] = data
new_edges = self._coerce_edges([*self.edges, edge], self.nodes)
self.edges = new_edges
return new_edges[-1]["id"]
|
add_node(name: Any = None, *, id: Any = None, size: int | float | None = None, color: str | None = None, data: Any = None, **attrs: Any) -> str
Add a node and return its normalized id.
Source code in wigglystuff/graph_widget.py
| def add_node(
self,
name: Any = None,
*,
id: Any = None,
size: int | float | None = None,
color: str | None = None,
data: Any = None,
**attrs: Any,
) -> str:
"""Add a node and return its normalized id."""
node = dict(attrs)
if id is not None:
node["id"] = id
if name is not None:
node["name"] = name
if size is not None:
node["size"] = size
if color is not None:
node["color"] = color
if data is not None:
node["data"] = data
new_nodes = self._coerce_nodes([*self.nodes, node])
self.nodes = new_nodes
return new_nodes[-1]["id"]
|
attach_node(source: Any, name: Any = None, *, id: Any = None, edge_id: Any = None, edge_name: Any = None, size: int | float | None = None, color: str | None = None, data: Any = None, edge_width: int | float | None = None, edge_color: str | None = None, edge_data: Any = None, **attrs: Any) -> tuple[str, str]
Attach a node to an existing source node.
If id or name resolves to an existing node, only the edge is added.
Otherwise a new node is created first.
Returns:
| Type |
Description |
tuple[str, str]
|
The normalized (node_id, edge_id) pair.
|
Source code in wigglystuff/graph_widget.py
| def attach_node(
self,
source: Any,
name: Any = None,
*,
id: Any = None,
edge_id: Any = None,
edge_name: Any = None,
size: int | float | None = None,
color: str | None = None,
data: Any = None,
edge_width: int | float | None = None,
edge_color: str | None = None,
edge_data: Any = None,
**attrs: Any,
) -> tuple[str, str]:
"""Attach a node to an existing source node.
If ``id`` or ``name`` resolves to an existing node, only the edge is added.
Otherwise a new node is created first.
Returns:
The normalized ``(node_id, edge_id)`` pair.
"""
source_id = self._resolve_endpoint(source, self.nodes, self._node_lookup(self.nodes))
lookup = self._node_lookup(self.nodes)
node = dict(attrs)
if id is not None:
node["id"] = id
if name is not None:
node["name"] = name
if size is not None:
node["size"] = size
if color is not None:
node["color"] = color
if data is not None:
node["data"] = data
node_id = None
for endpoint in (id, name):
if endpoint is None:
continue
try:
node_id = self._resolve_endpoint(endpoint, self.nodes, lookup)
break
except ValueError:
pass
if node_id is None:
new_nodes = self._coerce_nodes([*self.nodes, node])
node_id = new_nodes[-1]["id"]
else:
updates = dict(attrs)
if name is not None and id is not None:
updates["name"] = name
if size is not None:
updates["size"] = size
if color is not None:
updates["color"] = color
if data is not None:
updates["data"] = data
new_nodes = [
{**existing, **updates} if existing["id"] == node_id else existing
for existing in self.nodes
]
edge: dict[str, Any] = {"source": source_id, "target": node_id}
if edge_id is not None:
edge["id"] = edge_id
if edge_name is not None:
edge["name"] = edge_name
if edge_width is not None:
edge["width"] = edge_width
if edge_color is not None:
edge["color"] = edge_color
if edge_data is not None:
edge["data"] = edge_data
new_edges = self._coerce_edges([*self.edges, edge], new_nodes)
edge_id = new_edges[-1]["id"]
with self.hold_sync():
self.nodes = new_nodes
self.edges = new_edges
return node_id, edge_id
|
clear_selection() -> None
Clear selected node and edge ids.
Source code in wigglystuff/graph_widget.py
| def clear_selection(self) -> None:
"""Clear selected node and edge ids."""
self.selected_nodes = []
self.selected_edges = []
|
detach_node(node: Any, *, delete: bool = False) -> None
Remove all edges attached to a node.
Set delete=True to remove the node as well.
Source code in wigglystuff/graph_widget.py
| def detach_node(self, node: Any, *, delete: bool = False) -> None:
"""Remove all edges attached to a node.
Set ``delete=True`` to remove the node as well.
"""
node_id = self._resolve_endpoint(node, self.nodes, self._node_lookup(self.nodes))
new_nodes = [
n for n in self.nodes if not delete or n["id"] != node_id
]
new_edges = [
e for e in self.edges if e["source"] != node_id and e["target"] != node_id
]
remaining_edges = {edge["id"] for edge in new_edges}
with self.hold_sync():
self.nodes = new_nodes
self.edges = new_edges
if delete:
self.selected_nodes = [n for n in self.selected_nodes if n != node_id]
self.selected_edges = [
e for e in self.selected_edges if e in remaining_edges
]
|
get_adjacency_matrix(directed: bool | None = None)
Return an adjacency matrix for the current graph.
Source code in wigglystuff/graph_widget.py
| def get_adjacency_matrix(self, directed: bool | None = None):
"""Return an adjacency matrix for the current graph."""
import numpy as np
if directed is None:
directed = self.directed
node_ids = [node["id"] for node in self.nodes]
index = {node_id: i for i, node_id in enumerate(node_ids)}
matrix = np.zeros((len(node_ids), len(node_ids)))
for edge in self.edges:
if edge["source"] not in index or edge["target"] not in index:
continue
src = index[edge["source"]]
dst = index[edge["target"]]
matrix[src][dst] = 1
if not directed:
matrix[dst][src] = 1
return matrix
|
get_selected_edge_data() -> list[dict]
Return full edge dicts for currently selected edges.
Source code in wigglystuff/graph_widget.py
| def get_selected_edge_data(self) -> list[dict]:
"""Return full edge dicts for currently selected edges."""
selected = set(self.selected_edges)
return [edge for edge in self.edges if edge["id"] in selected]
|
get_selected_node_data() -> list[dict]
Return full node dicts for currently selected nodes.
Source code in wigglystuff/graph_widget.py
| def get_selected_node_data(self) -> list[dict]:
"""Return full node dicts for currently selected nodes."""
selected = set(self.selected_nodes)
return [node for node in self.nodes if node["id"] in selected]
|
remove_edge(edge: Any) -> None
Remove an edge by id or index.
Source code in wigglystuff/graph_widget.py
| def remove_edge(self, edge: Any) -> None:
"""Remove an edge by id or index."""
if isinstance(edge, int) and 0 <= edge < len(self.edges):
edge_id = self.edges[edge]["id"]
else:
edge_id = self._stringify(edge)
self.edges = [e for e in self.edges if e["id"] != edge_id]
self.selected_edges = [e for e in self.selected_edges if e != edge_id]
|
remove_node(node: Any) -> None
Remove a node by id, unique name, or index, including incident edges.
Source code in wigglystuff/graph_widget.py
| def remove_node(self, node: Any) -> None:
"""Remove a node by id, unique name, or index, including incident edges."""
node_id = self._resolve_endpoint(node, self.nodes, self._node_lookup(self.nodes))
self.detach_node(node_id, delete=True)
|