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.



| 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.
