Accessible Directed Graphs in networkx with py-maidr

Make networkx drawings of directed graphs accessible with py-maidr: walk a graph node by node from the keyboard, hearing what feeds each node, what it feeds, and where the graph branches and merges.

A directed graph – a pipeline, a dependency graph, a workflow – is drawn in Python with networkx: nx.draw(), nx.draw_networkx() and the layout variants (nx.draw_circular(), nx.draw_spring(), …). Each draws every node as a point and every edge as an arrow, and the arrows are the whole content of the graph: what depends on what.

Read as points, a graph says only where the layout algorithm happened to put each node, which is not data at all, and none of its edges. maidr reads the graph instead: Left and Right walk the nodes, and each one announces what feeds it, what it feeds, and whether it is an input or output of the whole graph, a branch point or a merge point, with the node outlined.

DIRECTED_GRAPH is experimental and may change without a deprecation period: see Plot Type Stability. Only directed graphs are read this way – an undirected graph (nx.Graph) keeps the point reading it had, since an undirected edge has no direction to follow.

WarningPrototype

This is one of the experimental plot types. It has not been through a user study, and it may change without a deprecation period. See Plot type stability.

Setup

The examples on this page need networkx, which is an optional extra: pip install maidr[networkx]. Nothing in maidr imports it; the reading is wired up when your own code imports networkx.

import matplotlib.pyplot as plt

# Just import maidr package: plt.show() now renders accessible output 
import maidr  

A pipeline

import matplotlib.pyplot as plt
import networkx as nx

import maidr  

pipeline = nx.DiGraph()
pipeline.add_edges_from(
    [
        ("load", "clean"),
        ("clean", "features"),
        ("clean", "labels"),
        ("features", "train"),
        ("labels", "train"),
        ("train", "evaluate"),
    ]
)

fig, ax = plt.subplots()
nx.draw_networkx(  
    pipeline, pos=nx.spring_layout(pipeline, seed=1), ax=ax, node_color="lightblue"
)  
ax.set_title("A training pipeline")

plt.show()  

Each stop announces the node and its connections: clean is fed by load and feeds features and labels, so it is announced as a branch point, and train, fed by both, as a merge point.

Labels and node data

import matplotlib.pyplot as plt
import networkx as nx

import maidr  

tasks = nx.DiGraph([("fetch", "parse"), ("parse", "store"), ("parse", "index")])
tasks.nodes["fetch"]["retries"] = 3
tasks.nodes["store"]["backend"] = "postgres"

fig, ax = plt.subplots()
nx.draw_circular(  
    tasks,
    ax=ax,
    with_labels=True,
    labels={"fetch": "Fetch pages", "parse": "Parse HTML",
            "store": "Store rows", "index": "Build index"},
    node_size=1800,
    node_color="wheat",
)  

plt.show()  

A node is announced by the label it is drawn with (labels=), and the node’s own data – strings, numbers and flags, such as retries and backend here – is announced with it. Only the nodes and edges drawn are read: pass nodelist= or edgelist= and the reading follows them.

Keras model graphs drawn by maidr.keras.plot_model() and maidr.read_tensorboard_graph() are read the same way.