Feeding a clanker? Grab this page as raw `.md` →

# WidgetDAG API


Lay widgets out as a DAG and draw the arrows -- like `mo.hstack`, but columns come from edge depth and the connections are drawn for you.


`nodes` maps an id to any renderable (an input widget, an image, a chart -- anything marimo can show). `edges` is a list of `(src_id, dst_id)`. `layout` is `(nodes, edges) -> {id: column}` (default `layered_layout`). The widgets stay live and reactive; this only arranges references to them.


This is a marimo-only display helper: the arrow overlay reaches into marimo's rendered DOM, so it is not wired for plain Jupyter.


Instead of spelling out `edges` by hand, `WidgetDAG.from_widgets` derives them from marimo's own dataflow graph -- pass the widgets and the arrows follow the notebook's dependency order (see that method).

 Example

```
import marimo as mo
from wigglystuff import Matrix, WidgetDAG

kernel = mo.ui.anywidget(Matrix(rows=3, cols=3))
WidgetDAG(
    nodes={"kernel": kernel, "image": mo.image("cat.png"), "conv": out},
    edges=[("image", "conv"), ("kernel", "conv")],
)
```

  Source code in `wigglystuff/widget_dag.py`

```
def __init__(self, nodes, edges, layout=layered_layout):
    self.nodes = dict(nodes)
    self.edges = [list(e) for e in edges]
    self.layout = layout
```


## from_widgets `classmethod`


```
from_widgets(widgets, *, layout=layered_layout)
```


Build a `WidgetDAG` from a list of widgets, deriving the arrows from marimo's dataflow graph.


Pass the widget objects (not their names); each node is labelled with the Python variable it is bound to, and an edge is drawn between two widgets whenever the cell defining one depends on the cell defining the other.


```
WidgetDAG.from_widgets([paint, angle, kernel, conv, kernel2, conv2])
```


This only works when each widget is a top-level variable defined in its own cell -- marimo's graph is cell-level, so two variables computed in the same cell can't be ordered, and inline expressions aren't variables at all. For those cases use `WidgetDAG(nodes, edges)` directly. Requires a running marimo kernel.

 Source code in `wigglystuff/widget_dag.py`

```
@classmethod
def from_widgets(cls, widgets, *, layout=layered_layout):
    """Build a ``WidgetDAG`` from a list of widgets, deriving the arrows from
    marimo's dataflow graph.

    Pass the widget objects (not their names); each node is labelled with the
    Python variable it is bound to, and an edge is drawn between two widgets
    whenever the cell defining one depends on the cell defining the other.

    ```python
    WidgetDAG.from_widgets([paint, angle, kernel, conv, kernel2, conv2])
    ```

    This only works when each widget is a top-level variable defined in its
    own cell -- marimo's graph is cell-level, so two variables computed in the
    same cell can't be ordered, and inline expressions aren't variables at
    all. For those cases use ``WidgetDAG(nodes, edges)`` directly. Requires a
    running marimo kernel.
    """
    try:
        from marimo._runtime.context.types import get_context

        ctx = get_context()
        graph = ctx.graph
        glb = ctx.globals
    except Exception as e:  # not in a marimo kernel
        raise RuntimeError(
            "WidgetDAG.from_widgets needs a running marimo kernel; "
            "use WidgetDAG(nodes, edges) directly outside marimo."
        ) from e

    # object identity -> variable name (first match wins), preserving order
    name_of = {}
    for w in widgets:
        hit = next((n for n, val in glb.items() if val is w), None)
        if hit is None:
            raise ValueError(
                "Every widget passed to from_widgets must be a top-level "
                "variable; one object was not found in the notebook globals. "
                "Assign it to a variable or use WidgetDAG(nodes, edges)."
            )
        name_of[id(w)] = hit

    order = [name_of[id(w)] for w in widgets]
    nodes = {name_of[id(w)]: w for w in widgets}
    name_to_cell = {n: next(iter(graph.definitions[n])) for n in order}
    ancestors = {c: graph.ancestors(c) for c in name_to_cell.values()}
    edges = _reduce_edges(order, name_to_cell, ancestors)
    return cls(nodes, edges, layout=layout)
```


## Layout


A layout is any callable `(nodes, edges) -> {id: column}`. The default is `layered_layout`; pass your own to `WidgetDAG(..., layout=...)` to swap in a different algorithm.


Assign each node a column (0 = leftmost).


Longest-path layering, then pull every node rightward to sit just before its earliest child. That keeps edges between adjacent columns for pipeline/tree shapes, so arcs stay inside the empty column gaps and never cross a widget. (True long edges would still skip -- that needs dummy waypoint nodes; a job for a future layout strategy.)


A layout is just `(nodes, edges) -> {id: column}`; pass your own to `WidgetDAG(..., layout=...)` to swap in a different algorithm.

 Source code in `wigglystuff/widget_dag.py`

```
def layered_layout(nodes, edges):
    """Assign each node a column (0 = leftmost).

    Longest-path layering, then pull every node rightward to sit just before
    its earliest child. That keeps edges between adjacent columns for
    pipeline/tree shapes, so arcs stay inside the empty column gaps and never
    cross a widget. (True long edges would still skip -- that needs dummy
    waypoint nodes; a job for a future layout strategy.)

    A layout is just ``(nodes, edges) -> {id: column}``; pass your own to
    ``WidgetDAG(..., layout=...)`` to swap in a different algorithm.
    """
    parents = {k: [] for k in nodes}
    children = {k: [] for k in nodes}
    for src, dst in edges:
        parents[dst].append(src)
        children[src].append(dst)
    col = {}

    def longest(k):
        if k not in col:
            col[k] = 0 if not parents[k] else 1 + max(longest(p) for p in parents[k])
        return col[k]

    for k in nodes:
        longest(k)
    changed = True
    while changed:  # pull right: latest column still left of every child
        changed = False
        for k in nodes:
            if children[k]:
                latest = min(col[c] for c in children[k]) - 1
                if latest > col[k]:
                    col[k] = latest
                    changed = True
    return col
```


## Notes


`WidgetDAG` is a marimo-only display helper. Its arrow overlay reaches into marimo's rendered DOM to draw connections in the same coordinate space as the node boxes, so it is not wired for plain Jupyter. The nodes stay live and reactive — embedding a widget (e.g. a `Matrix` or `Paint`) as a node keeps it interactive, and editing it re-runs the cell that built the DAG.
