Skip to content

visualizer

visualizer #

Utilities for creating visual representations of procedure graphs.

Uses Graphviz: the visualizer converts procedure DAGs into flowcharts that help users understand workflow structure, data flow, and procedure dependencies.

The generated visualizations show: - Procedure nodes with their types and configurations - Data flow connections between procedures with input names - Sequential execution steps with step numbers - Hierarchical grouping options for complex workflows

procedure_to_graphviz(procedure, group_by_depth=False) #

Converts a Procedure DAG into a Graphviz diagram for visualization.

Creates a directed graph showing the structure and data flow of a procedure workflow. Each procedure becomes a node showing its type, label, and settings. Edges show data flow (inputs) and control flow (sequential steps).

Parameters:

Name Type Description Default
procedure Procedure

The root procedure to visualize (any procedure in the DAG)

required
group_by_depth bool

Whether to visually group nodes by their depth level in the procedure hierarchy for better layout

False

Returns:

Type Description
Digraph

A Graphviz Digraph object that can be rendered to various formats

Digraph

(PNG, SVG, PDF, etc.)

Example
import capturegraph as cgp

# Create your procedure
my_procedure = my_capture_procedure()

# Generate visualization
graph = cgp.procedure_to_graphviz(my_procedure, group_by_depth=True)

# Render to file
graph.render("my_procedure", format="png", cleanup=True)

# Or view directly (if supported)
graph.view()
Source code in capturegraph-lib/capturegraph/procedures/procedure/exporting/visualizer.py
def procedure_to_graphviz(procedure: Procedure, group_by_depth: bool = False) -> Digraph:
    """Converts a Procedure DAG into a Graphviz diagram for visualization.

    Creates a directed graph showing the structure and data flow of a procedure
    workflow. Each procedure becomes a node showing its type, label, and settings.
    Edges show data flow (inputs) and control flow (sequential steps).

    Args:
        procedure (Procedure): The root procedure to visualize (any procedure in
            the DAG)
        group_by_depth (bool): Whether to visually group nodes by their depth level
                              in the procedure hierarchy for better layout

    Returns:
        A Graphviz Digraph object that can be rendered to various formats
        (PNG, SVG, PDF, etc.)

    Example:
        ```python
        import capturegraph as cgp

        # Create your procedure
        my_procedure = my_capture_procedure()

        # Generate visualization
        graph = cgp.procedure_to_graphviz(my_procedure, group_by_depth=True)

        # Render to file
        graph.render("my_procedure", format="png", cleanup=True)

        # Or view directly (if supported)
        graph.view()
        ```
    """

    def graphviz_html(text: str) -> str:
        text = "".join(line.strip() for line in text.splitlines() if line.strip())

        return f"<{text}>"

    def shorten_value(value: str) -> str:
        value = f"{value}"
        if len(value) > 20:
            return value[:20] + "..."
        else:
            return value

    def list_item(name: str, value: str) -> str:
        return f"""<tr>
        <td align="left"><font point-size="11" face="Courier New">
            <b> • {name}:</b>
        </font></td>
        <td align="left"><font point-size="11">
            {shorten_value(value)}
        </font></td>
        </tr>"""

    def procedure_to_graph_label(procedure: Procedure) -> str:
        return graphviz_html(f"""
        <table border = "0" cellborder = "0" cellspacing = "0" align = "left">
        <tr>
            <td colspan = "2"> <font point-size = "14">
                <b> {procedure.label or procedure.kind.__name__} </b>
            </font> </td>
        </tr>
        {list_item("kind", procedure.kind.__name__) if procedure.label else ""}
        {"".join(list_item(name, value) for name, value in procedure._settings.items())}
        </table>
        """)

    def procedure_input_to_edge_label(name: str) -> str:
        return graphviz_html(f"""
        <table border = "1" cellborder = "0" cellspacing = "0" bgcolor = "white">
        <tr>
        <td>
            <font face = "Courier New" point-size = "10">
            <b> {name} </b>
            </font>
        </td>
        </tr>
        </table>
        """)

    def procedure_step_to_edge_label(number: int) -> str:
        return graphviz_html(f"""
        <table border = "1" cellborder = "0" cellspacing = "0" bgcolor = "white">
        <tr>
        <td align = "left">
            <font face = "Courier New" point-size = "11">
            <b> Step  # {number}</b>
            </font>
        </td>
        </tr>
        </table>
        """)

    node_depth: dict[str, int] = {}
    dot = Digraph(
        # 1) Default node style
        node_attr={
            "shape": "box",
            "style": "rounded,filled",
            "fillcolor": "lightgrey",
            "fontname": "Helvetica",
        },
        # 2) Global graph settings
        graph_attr={
            "rankdir": "TB",  # top->bottom tree
            "ranksep": "1.0",  # vertical spacing between ranks
            "nodesep": "0.1",  # vertical spacing between ranks
        },
    )

    def add_procedure(procedure: Procedure, depth: int) -> None:
        if procedure._uuid in node_depth:
            node_depth[procedure._uuid] = max(node_depth[procedure._uuid], depth)
            return
        else:
            node_depth[procedure._uuid] = depth

        dot.node(procedure._uuid, label=procedure_to_graph_label(procedure), labeljust="l")

        for name, input_procedure in procedure._inputs.items():
            add_procedure(input_procedure, depth + 1)

            dot.edge(
                f"{input_procedure._uuid}",
                f"{procedure._uuid}",
                xlabel=procedure_input_to_edge_label(name),
            )

        for step, substep_procedure in enumerate(procedure._substeps):
            add_procedure(substep_procedure, depth + 1)

            dot.edge(
                f"{substep_procedure._uuid}",
                f"{procedure._uuid}",
                xlabel=procedure_step_to_edge_label(step + 1),
            )

    add_procedure(procedure, 0)

    if group_by_depth:
        for depth in sorted({d for _, d in node_depth.items()}):
            nodes = [uuid for uuid, d in node_depth.items() if d == depth]

            with dot.subgraph(name=f"cluster_{depth}") as s:  # type: ignore
                s.attr(style="invis")
                s.attr(rank="same")

                for uuid in nodes:
                    s.node(uuid)

    return dot