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



```
from wigglystuff import FormulaDAG

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.


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