What’s Mermaid
Mermaid is a tool that lets you create markdown-type descriptions of graphs/flowcharts and converts them to diagrams.
As an example:
graph LR;
A--> B & C & D;
B--> A & E;
C--> A & E;
C--> A & E; E--> B & C & D;
becomes this when rendered:
It’s possible to dynamically edit them with VSCode extensions or with online editors.
This is great for visualizing small graphs and programtically producing flow charts. Manually creating a flowchart is painful enough, not too mention having to maintain it as the inevitable changes roll in. Keeping the diagram in code reduces these headaches.
Examples
To use these in markdown, put them in a code block with type mermaid
(or type {mermaid}
if using Quarto). Meaning, put the the example (stuff
) like this (replacing the single quotes '
with backticks ` because making markdown examples in markdown is difficult):
'''mermaid
stuff '''
Minimal
graph LR; A;
You have to say:
graph
- this is a graph-type diagramLR
- nodes are produced left to right. Without this orientation it bugs out.A;
- the first node has the label “A”
Decision Flowchart
graph TB;
Start-->|evaluate| Consider;
Consider-->|certain| Done;[More Research];
Consider-->|uncertain| mr mr -->|re-evaluate| Consider;
graph
andflowchart
are interchangeable diagram typesid[longer description]
allows you to alias the contents of a node (and also handles spaces in node labels-->|edge label|
provides a text label for an edge
Larger Graph
graph LR;
a --- b;
a --- b;
b --- c;
a --- c;
c --- d;
d --- e;
e --- f;
c --- f;
g --- h;
g --- i;
g --- j;
g --- k;
i --- i1;
i --- i2;
i --- i3; i --- i4;
- we use
---
for undirected edges and-->
for directed - the graph can be disconnected
- you can have multiple edges between nodes (a and b here)
Application: Scrape Python Code to Generate a Dependency Graph
Let’s say you have some code where you want to show which modules or calling which other modules. This is nicely described using a dependency graph. For example:
This tells us the main function calls the worker, the worker calls two helpers, and one of the helpers calls numpy
. Generating this by hand would be a nuisance! Not only to just go through all of the files and read the imports then open up your least favorite flow chart tool (powerpoint). Then, what happens if the code changes (which happens more than you think)?
Luckily, it’s easy to create a parser and a script to generate the Mermaid description of the code.
To show how, let’s double-down and demonstrate a quick parser by first viewing the parser dependencies using the parser itself:
Then we have these four snippets comprising a simple parser and Mermaid dependency graph description:
create_python_code_graph.py
from pathlib import Path
from py_code_scraper import scrape_module_graph
from generate_mermaid_desc import generate_desc
= Path(".")
code_directory = scrape_module_graph(code_directory)
module_graph_edges = generate_desc(module_graph_edges)
mermaid_desc
print(mermaid_desc)
py_code_scraper.py
import os
from import_finder import find_imports
def scrape_module_graph(dir_name):
= []
module_graph for file in os.listdir(dir_name):
if file.endswith("py"):
= dir_name / file
code_file = file.split(".")[0]
module_name with open(code_file, "r") as f:
= find_imports(f.readlines())
imported_modules = [(module_name, import_module) for import_module in imported_modules]
edges if edges:
module_graph.extend(edges)return module_graph
generate_mermaid_desc.py
def update_module_name_lookup(module_name, module_lookup_dict):
if len(module_name) > 20:
= module_name[:20]
module_lookup_dict[module_name] else:
= module_name
module_lookup_dict[module_name]
def generate_desc(import_graph_edges):
= []
contents = "```{mermaid}"
header = "graph LR;"
figure_type = "```"
footer
contents.append(header)
contents.append(figure_type)= {}
module_lookup for (s, t) in import_graph_edges:
= module_lookup.get(s, "")
s_name = module_lookup.get(t, "")
t_name if not s_name:
update_module_name_lookup(s, module_lookup)= module_lookup[s]
s_name if not t_name:
update_module_name_lookup(t, module_lookup)= module_lookup[t]
t_name
= f"\t{s_name}[{s}] --> {t_name}[{t}];"
edge_line
contents.append(edge_line)
contents.append(footer)return "\n".join(contents)
import_finder.py
import re
def find_imports(code):
= []
import_list for line in code:
if line.startswith("import"):
= line.split(" ")
import_parts = import_parts[1].strip()
imported_module
import_list.append(imported_module)if line.startswith("from"):
= line.split(" ")
import_parts = import_parts[1].strip()
imported_module
import_list.append(imported_module)return import_list
So, when running the dependency graph creator on itself we get:
graph LR;[py_code_scraper] --> os[os];
py_code_scraper[py_code_scraper] --> import_finder[import_finder];
py_code_scraper[import_finder] --> re[re];
import_finder[create_python_code_graph] --> pathlib[pathlib];
create_python_code_g[create_python_code_graph] --> py_code_scraper[py_code_scraper];
create_python_code_g[create_python_code_graph] --> generate_mermaid_des[generate_mermaid_desc]; create_python_code_g
Fairly straight-forward, if not full-featured (for instance, I’m not handling folder crawling here or providing more detail on the dependencies). But this shows you how you can quickly hack together an automated graph descriptions.
If you are interested in this particular use case, there is a similar tool to create a dependency graph figure with Python (though it doesn’t use Mermaid from what I can see) called pydeps
and I’m sure if you search more you can find one that does created the Mermaid description.