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

FormulaDAG API#

FormulaDAG lays out a set of LaTeX formulas as a DAG. Name each formula and write \use{name} inside another one: the widget renders that formula's output symbol in place, infers the edge, and draws an arrow from the producer straight onto the spot where it's used. Formulas flow top to bottom by dependency depth, and each producer gets one color for its symbol, its arrows, and every use. Hover a formula to highlight what it depends on and what depends on it; click it to set selected.

Draggable \tangle{name} numbers and \note{name}{...} notes work like in TangleLatex, shared across the whole DAG. Give a formula an fn (its argument names pick parameters or other formulas) and its value is computed live into outputs. While you drag a number, every formula that depends on it shows its live value in place of its symbol, both where it's defined and wherever it's used.

Call to_html() for a self-contained, static HTML page of the current state (for docs, blog posts, or attachments), or save("dag.html") to write it to a file. KaTeX still loads from a CDN, so the page needs internet access.

See also: TangleLatex for a single formula, and WidgetDAG for arranging live widgets as a DAG.

Bases: AnyWidget

Lay out LaTeX formulas as a DAG, wired by where each output is used.

Each formula has a name. Writing \use{name} inside another formula renders that formula's output symbol (by default the left-hand side of its first =) and draws an arrow from the producing formula straight onto the spot where it is used. The edges are inferred from those markers, so there is no edge list to maintain; cycles are rejected.

Formulas flow top to bottom: a formula sits one row below the deepest formula it uses. Each producer gets a color shared by its output symbol, its arrows, and every place it is used. Hover a formula to highlight what it depends on and what depends on it; click it to set selected.

Numbers and notes work like TangleLatex, shared across the whole DAG: \tangle{name} in any formula is a draggable number configured in parameters (the same name in two formulas is one value), and \note{name}{tex} attaches the plain-language note notes[name] to that part of its formula. values follows a drag live; committed_values keeps the pre-drag values until release.

Examples:

from wigglystuff import FormulaDAG

FormulaDAG(
    {
        "mu": r"\mu = \frac{1}{n}\sum_i x_i",
        "var": r"\sigma^2 = \frac{1}{n}\sum_i (x_i - \use{mu})^2",
        "sigma": r"\sigma = \sqrt{\use{var}}",
        "z": r"z_i = \frac{x_i - \use{mu}}{\note{scale}{\use{sigma}}}",
        "band": r"b = \use{mu} \pm \tangle{k}\,\use{sigma}",
    },
    parameters={"k": {"value": 2, "min_value": 0, "max_value": 4, "step": 0.5}},
    notes={"scale": "measured in standard deviations"},
)

Create a FormulaDAG widget.

Parameters:

Name Type Description Default
formulas Mapping[str, Any]

Mapping from a formula name to its LaTeX, or to a dict with tex plus optional symbol (what \use{name} renders as; defaults to the left-hand side of the first top-level =, else the name) and label (the caption under the formula; defaults to the name). Reference another formula's output with \use{other_name}. Add fn to compute the formula's value live: a callable whose argument names pick parameters or other formulas, e.g. "fn": lambda d1, vol: d1 - vol. The value shows in the legend and, while you drag or type a number it depends on, in place of the formula's symbol and its uses (rounded to digits, default 3). It lands in outputs; committed_outputs holds the pre-drag values, like committed_values. By default each formula sits one row below the deepest formula it uses, so formulas that use nothing share the top row. Add row (0 is the top) to push a formula down to at least that row; the formulas that use it move down with it.

required
parameters Mapping[str, Mapping[str, Any]] | None

Draggable numbers, shared by every formula. Mark one with \tangle{name} and configure it exactly as in TangleLatex: value is required; optional keys are min_value, max_value, step, digits, display ("number" or "symbol"), symbol, label, pixels_per_step, and color.

None
notes Mapping[str, Any] | None

Plain-language notes, as in TangleLatex. Wrap a part of a formula in \note{name}{tex} and map name to its text, or to a dict with text, side ("auto", "top", "bottom") and color. Each note lives in one formula.

None
legend bool

Show a strip under the DAG with one chip per number: its symbol, its label (say what it means there, e.g. "spot price"), and its live value. Chips drag and edit like the numbers in the formulas, and hovering one lights up every place that number appears.

True
theme str

Color theme: "auto", "light", or "dark".

'auto'
**kwargs Any

Forwarded to anywidget.AnyWidget.

{}
Source code in wigglystuff/formula_dag.py
def __init__(
    self,
    formulas: Mapping[str, Any],
    parameters: Mapping[str, Mapping[str, Any]] | None = None,
    notes: Mapping[str, Any] | None = None,
    *,
    legend: bool = True,
    theme: str = "auto",
    **kwargs: Any,
) -> None:
    r"""Create a FormulaDAG widget.

    Args:
        formulas: Mapping from a formula name to its LaTeX, or to a dict
            with ``tex`` plus optional ``symbol`` (what ``\use{name}``
            renders as; defaults to the left-hand side of the first
            top-level ``=``, else the name) and ``label`` (the caption
            under the formula; defaults to the name). Reference another
            formula's output with ``\use{other_name}``. Add ``fn`` to
            compute the formula's value live: a callable whose argument
            names pick parameters or other formulas, e.g.
            ``"fn": lambda d1, vol: d1 - vol``. The value shows in the
            legend and, while you drag or type a number it depends on,
            in place of the formula's symbol and its uses (rounded to
            ``digits``, default 3). It lands in ``outputs``;
            ``committed_outputs`` holds the pre-drag values, like
            ``committed_values``. By default each formula sits one row
            below the deepest formula it uses, so formulas that use
            nothing share the top row. Add ``row`` (0 is the top) to push
            a formula down to at least that row; the formulas that use it
            move down with it.
        parameters: Draggable numbers, shared by every formula. Mark one
            with ``\tangle{name}`` and configure it exactly as in
            ``TangleLatex``: ``value`` is required; optional keys are
            ``min_value``, ``max_value``, ``step``, ``digits``,
            ``display`` (``"number"`` or ``"symbol"``), ``symbol``,
            ``label``, ``pixels_per_step``, and ``color``.
        notes: Plain-language notes, as in ``TangleLatex``. Wrap a part of
            a formula in ``\note{name}{tex}`` and map ``name`` to its text,
            or to a dict with ``text``, ``side`` (``"auto"``, ``"top"``,
            ``"bottom"``) and ``color``. Each note lives in one formula.
        legend: Show a strip under the DAG with one chip per number: its
            symbol, its ``label`` (say what it means there, e.g.
            ``"spot price"``), and its live value. Chips drag and edit
            like the numbers in the formulas, and hovering one lights up
            every place that number appears.
        theme: Color theme: ``"auto"``, ``"light"``, or ``"dark"``.
        **kwargs: Forwarded to ``anywidget.AnyWidget``.
    """
    if theme not in {"auto", "light", "dark"}:
        raise ValueError("theme must be 'auto', 'light', or 'dark'.")
    specs = _normalize_formulas(formulas)
    names = [s["name"] for s in specs]
    symbols = {s["name"]: s["symbol"] for s in specs}

    edges = []
    for spec in specs:
        for used in _USE_RE.findall(spec["tex"]):
            if used == spec["name"]:
                raise ValueError(f"formula {used!r} cannot \\use itself.")
            if used not in symbols:
                raise ValueError(
                    f"formula {spec['name']!r} uses unknown formula {used!r}."
                )
            if [used, spec["name"]] not in edges:
                edges.append([used, spec["name"]])
    cycle = _find_cycle(names, edges)
    if cycle:
        raise ValueError("formulas must not form a cycle: " + " -> ".join(cycle))

    params, normalized_notes = _tangle_and_notes(specs, parameters, notes)
    values = {name: spec["value"] for name, spec in params.items()}
    self._fns = {s["name"]: s["fn"] for s in specs if s["fn"] is not None}
    self._fn_args = _fn_arguments(specs, set(params))
    inputs = _fn_inputs(self._fn_args, set(params))
    outputs, output_errors = self._compute(values)

    # Producers take palette colors in order; formulas nobody uses stay ink.
    # The browser expands \use{...} and the output symbol itself, so it can
    # swap in live values while a number they depend on changes.
    producers = [n for n in names if any(src == n for src, _ in edges)]
    colors = {
        n: dict(_PRODUCER_COLORS[i % len(_PRODUCER_COLORS)]) for i, n in enumerate(producers)
    }
    normalized = []
    for spec in specs:
        split = _split_lhs(spec["tex"])
        lhs, body = (split[0], split[1]) if split else ("", spec["tex"])
        normalized.append(
            {
                "name": spec["name"],
                "lhs": lhs,
                "tex": body,
                "out_note": _whole_note(lhs) if lhs else None,
                "inputs": inputs.get(spec["name"], []),
                "label": spec["label"],
                "symbol": spec["symbol"],
                "digits": spec["digits"],
                "has_fn": spec["fn"] is not None,
                "color": colors.get(spec["name"], dict(_PARAM_COLOR)),
            }
        )

    rows = _formula_rows(names, edges, {s["name"]: s["row"] for s in specs})
    layers, routes, _ = _order_and_route(names, rows, edges)
    super().__init__(
        formulas=normalized,
        layers=[layers[r] for r in sorted(layers) if layers[r]],
        routes=routes,
        parameters=params,
        notes=normalized_notes,
        values=values,
        committed_values=dict(values),
        outputs=outputs,
        committed_outputs=dict(outputs),
        output_errors=output_errors,
        legend=legend,
        theme=theme,
        **kwargs,
    )
    self._edges = [tuple(e) for e in edges]

edges property #

edges: list[tuple[str, str]]

The inferred (producer, consumer) pairs.

downstream #

downstream(name: str) -> set[str]

Every formula that depends on name, directly or transitively.

Source code in wigglystuff/formula_dag.py
def downstream(self, name: str) -> set[str]:
    """Every formula that depends on ``name``, directly or transitively."""
    return self._reach(name, reverse=False)

save #

save(path: str | Path) -> Path

Write :meth:to_html to path and return it as a Path.

Source code in wigglystuff/formula_dag.py
def save(self, path: str | Path) -> Path:
    """Write :meth:`to_html` to ``path`` and return it as a ``Path``."""
    path = Path(path)
    path.write_text(self.to_html(), encoding="utf-8")
    return path

to_html #

to_html(title: str | None = None) -> str

Render the DAG as a self-contained HTML page.

The page inlines this widget's JavaScript and CSS, embeds the current state (including live fn outputs), and renders with interactive: false — a static artifact needing no widget comm. KaTeX still loads from the same CDN the widget uses, so the file needs internet access to display math. Use save() to write it.

Source code in wigglystuff/formula_dag.py
def to_html(self, title: str | None = None) -> str:
    """Render the DAG as a self-contained HTML page.

    The page inlines this widget's JavaScript and CSS, embeds the current
    state (including live ``fn`` outputs), and renders with
    ``interactive: false`` — a static artifact needing no widget comm.
    KaTeX still loads from the same CDN the widget uses, so the file
    needs internet access to display math. Use ``save()`` to write it.
    """
    state = {name: getattr(self, name) for name in _STATE_TRAITS}
    state["interactive"] = False
    return _static_page(state, title or "Formula DAG")

upstream #

upstream(name: str) -> set[str]

Every formula that name depends on, directly or transitively.

Source code in wigglystuff/formula_dag.py
def upstream(self, name: str) -> set[str]:
    """Every formula that ``name`` depends on, directly or transitively."""
    return self._reach(name, reverse=True)

Synced traitlets#

Traitlet Type Notes
formulas list[dict] Normalized formulas: name, lhs, tex, symbol, label, inputs, color, ...
layers / routes list Row layout and edge routes (computed in Python).
parameters dict Normalized \tangle number configuration.
notes dict Normalized \note configuration, each with its home formula.
values dict Live number values; follows a drag.
committed_values dict Values from before the current drag.
dragging bool True while a number is being dragged.
outputs dict Live fn results per formula (None on failure).
committed_outputs dict fn results for committed_values.
output_errors dict Exception messages for formulas whose fn failed.
hovered str Note under the pointer.
selected str Clicked formula name.
legend bool Show the legend strip of number and output chips.
theme str "auto", "light", or "dark".
error str KaTeX load or render errors.