Skip to content
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.