Skip to content
Feeding a clanker? Grab this page as raw .md

GraphWidget API#

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

GraphWidget(nodes=["Alpha", "Beta"], edges=[("Alpha", "Beta")])

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 #

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 #

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 #

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 #

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 #

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 #

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 #

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 #

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 #

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 #

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)

Synced traitlets#

Traitlet Type Notes
nodes list[dict] Node dicts with normalized id and optional name, size, color, and data.
edges list[dict] Edge dicts with normalized id, source, target, and optional name, width, color, and data.
directed bool Draw directed edges when true.
bounded bool Keep nodes inside the visible SVG bounds when true. Disable it for graphs that should spread beyond the viewport and be explored with pan/zoom.
width int \| None Canvas width in pixels. None (default) makes the widget fill its container's width and reflow when the container resizes.
height int Canvas height in pixels (default 400).
selected_nodes list[str] IDs of currently selected nodes.
selected_edges list[str] IDs of currently selected edges.

Layout Notes#

GraphWidget preserves browser-side node positions when nodes or edges change. Newly connected nodes are initialized near the existing endpoint they attach to. Use attach_node(source, name, ...) when adding one new node plus its connecting edge from Python; if name or id already resolves to a node, only the edge is added. Use detach_node(node) to remove all edges attached to a node while keeping the node visible, or detach_node(node, delete=True) to remove the node too.