Skip to content

pydsa.graphs

draw_graph

draw_graph(
    G: Dict[Any, Union[Dict[Any, Any], Any]],
) -> None

Utility function to draw a graph (stored as dict of dicts, dict of lists, or dict with (row, col) tuples as keys) in the terminal as a colored adjacency matrix. Requires 'tabulate' library: pip install tabulate For colors, requires 'colorama': pip install colorama Colors edges based on values (e.g., 1 in red, 0 in blue). Assumes nodes are comparable for sorting.

Source code in pydsa/graphs.py
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
def draw_graph(G: Dict[Any, Union[Dict[Any, Any], Any]]) -> None:
    """
    Utility function to draw a graph (stored as dict of dicts, dict of lists, or dict with (row, col) tuples as keys) in the terminal as a colored adjacency matrix.
    Requires 'tabulate' library: pip install tabulate
    For colors, requires 'colorama': pip install colorama
    Colors edges based on values (e.g., 1 in red, 0 in blue).
    Assumes nodes are comparable for sorting.
    """

    # duck typing, auto convert edge list to graph
    if is_edge_list(G):
        G = build_graph_from_edge_list(G)
    G = as_dict_of_dicts(G)

    try:
        from tabulate import tabulate
    except ImportError:
        print("Please install tabulate: pip install tabulate")
        return

    try:
        import colorama  # noqa: F401
    except ImportError:
        print("Please install colorama: pip install colorama")
        return

    if not G:
        print("Empty graph")
        return

    print("\n")

    # Determine the graph format
    is_tuple_key = False
    if G:
        first_key = next(iter(G))
        if isinstance(first_key, tuple) and len(first_key) == 2:
            is_tuple_key = True

    # Get all nodes
    all_nodes = set()
    if is_tuple_key:
        for src, dst in G:
            all_nodes.add(src)
            all_nodes.add(dst)
    else:
        all_nodes.update(G.keys())
        for neighbors in G.values():
            all_nodes.update(neighbors.keys())
    nodes = sorted(all_nodes)

    # Create table data
    headers = [str(node) for node in nodes]
    table = []

    for src in nodes:
        row = []
        for dst in nodes:
            val = None
            if is_tuple_key:
                key = (src, dst)
                if key in G:
                    val = G[key]
            else:
                if src in G and dst in G[src]:
                    val = G[src][dst]

            if val is not None:
                row.append(_cell(val))
            else:
                row.append("")
        table.append(row)

    # Use tabulate to print the table with headers and row labels
    print(
        tabulate(
            table,
            headers=headers,
            tablefmt="grid",
            showindex=headers,  # Use node labels for rows as well
            numalign="center",
            stralign="center",
        )
    )

as_dict_of_dicts

as_dict_of_dicts(G: Any) -> Any

Accept a dict of lists (or tuples or sets) as well as a dict of dicts. Each list neighbor becomes a dict entry with weight 1.

Also accept a dict of scalars (each key has one neighbor, its value) and a list of ints (a parent array: index i has one neighbor parent[i]). A self loop, parent[i] == i, draws as a node with no outgoing edge.

Source code in pydsa/graphs.py
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
def as_dict_of_dicts(G: Any) -> Any:
    """Accept a dict of lists (or tuples or sets) as well as a dict of dicts.
    Each list neighbor becomes a dict entry with weight 1.

    Also accept a dict of scalars (each key has one neighbor, its value)
    and a list of ints (a parent array: index i has one neighbor
    parent[i]). A self loop, parent[i] == i, draws as a node with no
    outgoing edge."""
    if isinstance(G, list) and all(isinstance(v, int) for v in G):
        G = dict(enumerate(G))
    if not isinstance(G, dict):
        return G
    out: Dict[Any, Dict[Any, Any]] = {}
    for k, v in G.items():
        if isinstance(v, dict):
            out[k] = v
        elif isinstance(v, (list, tuple, set)):
            out[k] = _labelled_neighbors(v)
        else:
            out[k] = {} if v == k else {v: 1}
    return out

draw_ascii_graph

draw_ascii_graph(
    graph: Dict[Any, Union[Dict[Any, Any], List[Any]]],
) -> None

Utility function to draw a graph (stored as dict of dicts or dict of lists) in the terminal using PHART for ASCII rendering. Requires 'networkx' and 'phart' libraries: pip install networkx phart Supports directed graphs. Edge weights (like 0/1) are ignored in rendering but structure is shown.

Source code in pydsa/graphs.py
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
def draw_ascii_graph(graph: Dict[Any, Union[Dict[Any, Any], List[Any]]]) -> None:
    """
    Utility function to draw a graph (stored as dict of dicts or dict of lists) in the terminal using PHART for ASCII rendering.
    Requires 'networkx' and 'phart' libraries: pip install networkx phart
    Supports directed graphs. Edge weights (like 0/1) are ignored in rendering but structure is shown.
    """
    G: Dict[Any, Dict[Any, Any]] = as_dict_of_dicts(graph)
    if not G:
        print("Empty graph")
        return

    # An image terminal gets a graphviz layout: phart puts every node on
    # one row and routes edges through the others, which is unreadable
    # past a handful of edges.
    if _image_terminal():
        draw_graphviz(graph, type="undirected" if _symmetric(G) else "directed")
        return

    # imported here: networkx costs ~50ms and every interpreter start pays
    # for sitecustomize's imports
    try:
        import networkx as nx
        from phart import ASCIIRenderer
    except ImportError:
        print("Please install networkx and phart: pip install networkx phart")
        return

    print("\n")

    # Create NetworkX DiGraph from the dict of dicts
    nx_graph = nx.DiGraph()
    all_nodes = set(G.keys())
    for neighbors in G.values():
        all_nodes.update(neighbors.keys())
    nx_graph.add_nodes_from(all_nodes)

    # Add edges, ignoring weights for now (PHART doesn't support labels)
    for src in G:
        for dst in G[src]:
            nx_graph.add_edge(src, dst)

    # Render with PHART
    try:
        renderer = ASCIIRenderer(nx_graph)
        print(_colorize_ascii_graph(renderer.render()))
    except Exception as e:
        print(f"Failed to render with PHART: {e}")
        # Fallback to adjacency list
        print("Fallback: Adjacency list")
        for src in sorted(G):
            shown = [f"{dst}({G[src][dst]})" for dst in sorted(G[src])]
            print(f"{src}: {', '.join(shown)}")