Skip to content

Instantly share code, notes, and snippets.

@Hermann-SW
Last active September 11, 2026 09:21
Show Gist options
  • Select an option

  • Save Hermann-SW/12c7644ac0c75b4eb019f76c3f023fe5 to your computer and use it in GitHub Desktop.

Select an option

Save Hermann-SW/12c7644ac0c75b4eb019f76c3f023fe5 to your computer and use it in GitHub Desktop.
Latex labels for Graphviz (work in progress)
""" Latex labels for Graphviz """
import html
import re
import subprocess
from pylatexenc.latex2text import (
LatexNodes2Text,
MacroTextSpec,
get_default_latex_context_db as get_default_l2t_context_db,
)
from pylatexenc.latexwalker import (
LatexMacroNode,
LatexEnvironmentNode,
LatexWalker,
get_default_latex_context_db as get_default_walker_context_db,
)
from pylatexenc.macrospec import EnvironmentSpec, MacroSpec
BLACKBOARD_MAP = {"Z": "ℤ", "N": "ℕ", "C": "ℂ", "R": "ℝ", "Q": "ℚ"}
def custom_mathbb(node, l2tobj):
""" foobar """
arg_nodes = getattr(
node.nodeargd, "argnlist", getattr(node.nodeargd, "argnodelist", [])
)
arg = l2tobj.nodelist_to_text(arg_nodes)
return BLACKBOARD_MAP.get(arg, arg)
EXTRA_WALKER_MACROS = [
MacroSpec("textbf", "{"),
MacroSpec("cong"),
MacroSpec("mathbb", "{"),
MacroSpec("text", "{"),
MacroSpec("longrightarrow"),
MacroSpec("Longrightarrow"),
MacroSpec("iff"),
MacroSpec("implies"),
MacroSpec("sim"),
MacroSpec("approx"),
MacroSpec("equiv"),
MacroSpec("lcm"),
MacroSpec("lbr"),
MacroSpec("bar", "{"),
MacroSpec("vspace", "{"),
MacroSpec("ord"),
MacroSpec("gcd"),
MacroSpec("mod"),
]
walker_ctx = get_default_walker_context_db()
walker_ctx.add_context_category(
"math-extra", macros=EXTRA_WALKER_MACROS, prepend=True
)
walker_ctx.add_context_category(
"env-extra", environments=[EnvironmentSpec("cases")], prepend=True
)
l2t_ctx = get_default_l2t_context_db()
l2t_ctx.add_context_category(
"math-extra-override",
macros=[
MacroTextSpec("textbf", "@@@BOLDSTART@@@%(1)s@@@BOLDEND@@@"),
MacroTextSpec("cong", " ≅ "),
MacroTextSpec("mathbb", custom_mathbb),
MacroTextSpec("text", "%(1)s"),
MacroTextSpec("vert", "|"),
MacroTextSpec("longrightarrow", "⟶"),
MacroTextSpec("Longrightarrow", "⟹"),
MacroTextSpec("iff", "⇔ "),
MacroTextSpec("implies", "⇒"),
MacroTextSpec("sim", "~"),
MacroTextSpec("approx", "≈"),
MacroTextSpec("equiv", "≡"),
MacroTextSpec("Z", "ℤ"),
MacroTextSpec("N", "ℕ"),
MacroTextSpec("lcm", "lcm"),
MacroTextSpec("lbr", "@@@BRLEFT@@@"),
MacroTextSpec("bar", "@@@BARSTART@@@%(1)s@@@BAREND@@@"),
MacroTextSpec("vspace", "@@@VSPACESTART@@@%(1)s@@@VSPACEEND@@@"),
MacroTextSpec("ord", "ord"),
MacroTextSpec("gcd", "gcd"),
MacroTextSpec("mod", "mod"),
],
prepend=True,
)
def preprocess_sub_super_scripts(text):
"""Replaces superscripts/subscripts with safe placeholder tags free of underscores."""
text = re.sub(r"\^\{([^}]+)\}", r"@@@SUPSTART@@@\1@@@SUPEND@@@", text)
text = re.sub(
r"\^([a-zA-Z0-9\+\-]|\\iota)", r"@@@SUPSTART@@@\1@@@SUPEND@@@", text
)
text = re.sub(r"_\{([^}]+)\}", r"@@@SUBSTART@@@\1@@@SUBEND@@@", text)
text = re.sub(r"_([a-zA-Z0-9\+\-])", r"@@@SUBSTART@@@\1@@@SUBEND@@@", text)
return text
def postprocess_html_tags(text):
"""Converts safe tokens to valid Graphviz HTML labels."""
# Convert \vspace{N} to a small empty break tag
text = re.sub(
r"@@@VSPACESTART@@@(\d+)@@@VSPACEEND@@@",
r'<BR ALIGN="LEFT"/><FONT POINT-SIZE="\1">&nbsp;</FONT><BR ALIGN="LEFT"/>',
text
)
return (
text.replace("@@@BOLDSTART@@@", "<B>")
.replace("@@@BOLDEND@@@", "</B>")
.replace("@@@SUPSTART@@@", "<SUP>")
.replace("@@@SUPEND@@@", "</SUP>")
.replace("@@@SUBSTART@@@", "<SUB>")
.replace("@@@SUBEND@@@", "</SUB>")
.replace("@@@BRLEFT@@@", '<BR ALIGN="LEFT"/>')
.replace("@@@BARSTART@@@", "<O>")
.replace("@@@BAREND@@@", "</O>")
)
def find_unknown_macro(node, ctx):
""" foobar """
if node is None:
return None
if isinstance(node, LatexMacroNode):
if ctx.get_macro_spec(node.macroname) is None:
return node
if hasattr(node, "nodeargd") and node.nodeargd:
arg_list = getattr(
node.nodeargd,
"argnlist",
getattr(node.nodeargd, "argnodelist", []),
)
for child in arg_list:
err_node = find_unknown_macro(child, ctx)
if err_node:
return err_node
elif hasattr(node, "nodelist") and node.nodelist:
for child in node.nodelist:
err_node = find_unknown_macro(child, ctx)
if err_node:
return err_node
return None
# pylint: disable=too-many-locals
def process_cases_environment(env_node, converter, full_latex_str):
""" foobar """
rows = [[]]
for child in env_node.nodelist:
if isinstance(child, LatexMacroNode) and child.macroname == "\\":
rows.append([])
else:
rows[-1].append(child)
table_rows = []
num_rows = len([r for r in rows if r])
for row_nodes in rows:
if not row_nodes:
continue
first_pos = row_nodes[0].pos
last_node = row_nodes[-1]
last_pos = last_node.pos + last_node.len
row_str = full_latex_str[first_pos:last_pos]
if "&" in row_str:
expr_str, cond_str = row_str.split("&", 1)
else:
expr_str, cond_str = row_str, ""
w_expr = LatexWalker(expr_str, latex_context=walker_ctx)
n_expr, _, _ = w_expr.get_latex_nodes()
expr = converter.nodelist_to_text(n_expr).strip()
w_cond = LatexWalker(cond_str, latex_context=walker_ctx)
n_cond, _, _ = w_cond.get_latex_nodes()
cond = converter.nodelist_to_text(n_cond).strip()
expr = postprocess_html_tags(html.escape(expr))
cond = postprocess_html_tags(html.escape(cond))
if len(table_rows) == 0:
table_rows.append(
f"<TR>"
f'<TD ROWSPAN="{num_rows}" VALIGN="MIDDLE" ALIGN="RIGHT" BORDER="0">'
f'<FONT POINT-SIZE="22">&#123;</FONT></TD>'
f'<TD ALIGN="LEFT" VALIGN="MIDDLE" BORDER="0">{expr}</TD>'
f'<TD ALIGN="LEFT" VALIGN="MIDDLE" BORDER="0">&nbsp;&nbsp;&nbsp;&nbsp;{cond}</TD>'
f"</TR>"
)
else:
table_rows.append(
f"<TR>"
f'<TD ALIGN="LEFT" VALIGN="MIDDLE" BORDER="0">{expr}</TD>'
f'<TD ALIGN="LEFT" VALIGN="MIDDLE" BORDER="0">&nbsp;&nbsp;&nbsp;&nbsp;{cond}</TD>'
f"</TR>"
)
return (
'<TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" CELLPADDING="1">'
+ "".join(table_rows)
+ "</TABLE>"
)
def latex_to_graphviz_html(latex_str):
""" foobar """
latex_str = preprocess_sub_super_scripts(latex_str)
walker = LatexWalker(latex_str, latex_context=walker_ctx)
nodes, _, _ = walker.get_latex_nodes()
for node in nodes:
missing_node = find_unknown_macro(node, l2t_ctx)
if missing_node:
raise ValueError(f"Unknown macro \\{missing_node.macroname}")
converter = LatexNodes2Text(latex_context=l2t_ctx)
if r"\begin{cases}" in latex_str:
for node in nodes:
if (
isinstance(node, LatexEnvironmentNode)
and node.environmentname == "cases"
):
cases_table = process_cases_environment(
node, converter, latex_str
)
prefix_str = latex_str[: node.pos]
suffix_str = latex_str[node.pos + node.len :]
if r"\\" in prefix_str:
lines = prefix_str.split(r"\\")
line1_html = latex_to_graphviz_html(lines[0])[1:-1]
line2_html = latex_to_graphviz_html(lines[1])[1:-1]
suffix_html = latex_to_graphviz_html(suffix_str)[1:-1]
full_html = (
'<TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" CELLPADDING="0">'
f'<TR><TD BORDER="0" ALIGN="LEFT" COLSPAN="2">{line1_html}</TD></TR>'
f'<TR><TD BORDER="0" VALIGN="MIDDLE" ALIGN="LEFT">{line2_html}</TD>'
f'<TD BORDER="0" VALIGN="MIDDLE" ALIGN="LEFT">{cases_table}</TD>'
f'<TD BORDER="0" VALIGN="MIDDLE" ALIGN="LEFT">{suffix_html}</TD></TR>'
"</TABLE>"
)
return f"<{full_html}>"
prefix_html = latex_to_graphviz_html(prefix_str)[1:-1]
suffix_html = latex_to_graphviz_html(suffix_str)[1:-1]
full_html = (
'<TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" CELLPADDING="0"><TR>'
f'<TD BORDER="0" VALIGN="MIDDLE">{prefix_html}</TD>'
f'<TD BORDER="0" VALIGN="MIDDLE">{cases_table}</TD>'
f'<TD BORDER="0" VALIGN="MIDDLE">{suffix_html}</TD>'
"</TR></TABLE>"
)
return f"<{full_html}>"
plain_text = converter.nodelist_to_text(nodes)
escaped_text = html.escape(plain_text)
# Convert placeholders to Graphviz HTML tags for standard nodes
html_label_body = postprocess_html_tags(escaped_text.replace("\n", "<BR/>"))
return f"<{html_label_body}>"
def_label = latex_to_graphviz_html(
r"""\textbf{Def 2.1 }Number-theoretic function:\\"""
r"""a: \mathbb{N}\to \mathbb{C}"""
)
ex_label = latex_to_graphviz_html(
r"""\textbf{Ex 2.2a }for s\in\mathbb{C}: \iota^{s}(n) := n^s \forall n \in
\mathbb{N} is s-th power\\function (s=0 constant one function, identity \iota^1 =: \iota)"""
)
def_3_13_label = latex_to_graphviz_html(
r"""\textbf{Def 3.13 } Ring R, x\in R: if \exists y\in R\setminus \{0\}: xy=0,
then x is called zero divisor"""
)
ex_3_14_label = latex_to_graphviz_html(
r"""\textbf{Ex 3.14 } zero divisors in \Z/3\Z: \bar{0}, so has no zero divisors\vspace{3}"""
r"""zero divisors in \Z/4\Z: \bar{0},\bar{2}, so has zero divisor\\"""
)
def_3_15_label = latex_to_graphviz_html(
r"""\textbf{Def 3.15 } Ring R, x\in R: if \exists y\in R: xy=1,\\then x is called a unit"""
)
ex_3_16_label = latex_to_graphviz_html(
r"""\textbf{Ex 3.16 } units in \Z/3\Z: \bar{1},\bar{2}\vspace{3}"""
r"""units in \Z/4\Z: \bar{1},\bar{3}"""
)
prop_3_17_label = latex_to_graphviz_html(
r"""\textbf{Prop 3.17 } R ring\lbr"""
r"""(a) R^{\times}:=\{x\in R: x is unit\} with \cdot\ is (abelian) group of units\lbr"""
r"""(b) x\in R^{\times} \implies\ x no zero divisor\lbr"""
r"""(c) R finite, then also: x\in R no zero divisor \implies\ x is unit\lbr"""
)
ex_3_18_label = latex_to_graphviz_html(
r"""\textbf{Ex 3.18 } Finite ring R=\Z/n\Z, n\in \N, x\in R:\vspace{5}"""
r"""\bar{x} unit \iff \bar{x} no zero divisor\lbr """
r"""Units in R are called coprime residue classes modulo n\lbr """
r"""(\Z/n\Z)^{\times} is called multiplicative group of integers modulo n"""
)
def_3_19_label = latex_to_graphviz_html(
r"""\textbf{Def 3.19 } (Commutative) ring R
with R^{\times} = R \setminus \{0\} is called field"""
)
ex_3_20_label = latex_to_graphviz_html(
r"""\textbf{Ex 3.20 } \Z/3\Z\ is field, \Z/4\Z\ and zero ring are no fields."""
)
thm_3_21_label = latex_to_graphviz_html(
r"""\textbf{Thm 3.21 } n\in \N, these are equivalent\lbr"""
r"""(i) n is prime\lbr"""
r"""(ii) \Z/n\Z\ is field\lbr"""
r"""(iii) \Z/n\Z\ has no zero divisors\lbr"""
)
prop_3_22_label = latex_to_graphviz_html(
r"""\textbf{Prop 3.22 } a\in \mathbb{N}, \bar{a}\in \Z/n\Z:\vspace{3}"""
r"""\bar{a}\in (\Z/n\Z)^{\times} \iff \gcd(a,n)=1"""
)
cor_3_23_label = latex_to_graphviz_html(
r"""\textbf{Cor 3.23 } n\in \mathbb{N}, a\in \Z:\vspace{3}"""
r"""(\exists x\in \Z: ax\equiv 1 (\mod\ n)) \iff \gcd(a,n)=1"""
)
ex_3_24_label = latex_to_graphviz_html(
r"""\textbf{Ex 3.24 } Solutions for\\"""
r"""3x\equiv 1 (\mod\ 37): L=25+37\Z"""
)
prop_3_25_label = latex_to_graphviz_html(
r"""\textbf{Prop 3.25 } a,b\in \Z, n\in\N\\"""
r"""(\exists x\in\Z: ax\equiv b (\mod\ n)) \iff \gcd(a,n)\vert b"""
)
ex_3_26_label = latex_to_graphviz_html(
r"""\textbf{Ex 3.26 } (a) \nexists x\in \Z: 15x\equiv 7 (\mod\ 21)\lbr"""
r"""(b) \exists x\in\Z: 15x\equiv 6 (\mod\ 21)\lbr"""
)
def_3_27_label = latex_to_graphviz_html(
r"""\textbf{Def 3.27 } Order \vert G\vert\ of group\\"""
r"""is number of its elements"""
)
ex_3_28_label = latex_to_graphviz_html(
r"""\textbf{Ex 3.28 }\vert(\Z/n\Z)^{\times}\vert=\phi(n)"""
)
prop_3_29_label = latex_to_graphviz_html(
r"""\textbf{Prop 3.29 } G finite abelian group.\lbr
\forall g\in G: g^{\vert G\vert} =1 """
)
thm_3_30_label = latex_to_graphviz_html(
r"""\textbf{Thm 3.30 } (Fermat-Euler theorem) \vspace{5}"""
r"""n\in \mathbb{N}. """
r"""\forall \bar{a}\in (\Z/n\Z)^{\times}:\ \bar{a}^{\phi(n)} =\bar{1} """
)
ex_3_31_label = latex_to_graphviz_html(
r"""\textbf{Ex 3.31 } 3^{19}\equiv 10 (\mod\ 17)"""
)
cor_3_32_label = latex_to_graphviz_html(
r"""\textbf{Cor 3.32 } (Fermat's little theorem) p prime:\vspace{3}"""
r"""(a) \forall \bar{a}\in \mathbb{F}_p^{\times}: \bar{a}^{p-1}=\bar{1}\vspace{5}"""
r"""(b) \forall \bar{a}\in \mathbb{F}_p: \bar{a}^p=\bar{a}\lbr"""
)
def_3_33_label = latex_to_graphviz_html(
r"""\textbf{Def 3.33 } Group G is cyclic:\lbr
\exists g\in G generator with\lbr
G = \{g^n: n\in \Z\}=:<g>\lbr"""
)
rem_3_34_label = latex_to_graphviz_html(
r"""\textbf{Rem 3.34 } Every cyclic group is abelian."""
)
ex_3_35_label = latex_to_graphviz_html(
r"""\textbf{Ex 3.35 }(a) group (\Z/5\Z)^{\times} = \{\bar{1},\bar{2},"""
r"""\bar{3},\bar{4}\} is cyclic\vspace{3}"""
r"""(b) group (\Z/8\Z)^{\times} = \{\bar{1},\bar{3},\bar{5},\bar{7}\} is not cyclic\lbr"""
r"""(c) additive group \Z\ is cyclic\vspace{3}"""
r"""(d) additive group \Z/m\Z =\{\bar{0},\bar{1},\dots,\bar{m-1}\}\ is cyclic\lbr"""
)
def_3_36_label = latex_to_graphviz_html(
r"""\textbf{Def 3.36 } G finite abelian group.\lbr
Order \ord(G):=\min\{n\in \mathbb{N}: g^n=1\}\lbr """
)
ex_3_37_label = latex_to_graphviz_html(
r"""\textbf{Ex 3.37 }For G=(\Z/5\Z)^{\times}:\vspace{3}"""
r"""(a) ord(\bar{2})=4\vspace{3}"""
r"""(b) ord(\bar{4})=2\lbr"""
)
prop_3_38_label = latex_to_graphviz_html(
r"""\textbf{Prop 3.38 }G finite abelian group:
G is cyclic \iff \exists g\in G with ord(g)=\vert G\vert"""
)
prop_3_39_label = latex_to_graphviz_html(
r"""\textbf{Prop 3.39 }G finite abelian group,\\g\in G, m\in \Z: ord(g)\vert m \iff g^m=1"""
)
cor_3_40_label = latex_to_graphviz_html(
r"""\textbf{Cor 3.40 } G finite abelian group.\lbr
\forall g\in G: ord(g) \vert\ \vert G\vert """
)
def_3_41_label = latex_to_graphviz_html(
r"""\textbf{Def 3.41 } G finite abelian group.\lbr
Exponent \exp(G):=\min\{n\in \mathbb{N}: g^n=1 \forall g\in G\}\lbr """
)
ex_3_42_label = latex_to_graphviz_html(
r"""\textbf{Ex 3.42 }(a) exp((\Z/5\Z)^{\times})=4\lbr
(b) exp((\Z/8\Z)^{\times})=2\lbr"""
)
prop_3_43_label = latex_to_graphviz_html(
r"""\textbf{Prop 3.43 }G finite abelian group G:\lbr """
r"""(a) \exp(G) \vert\ \vert G\vert\ \lbr """
r"""(b) \exp(G)= \lcm(\{ord(g): g\in G\})\lbr"""
)
def_3_44_label = latex_to_graphviz_html(
r"""\textbf{Def 3.44 } homomorphism\\ isomorphism, \cong """
)
ex_3_45_label = latex_to_graphviz_html(
r"""\textbf{Ex 3.45 }K field implies\lbr
det:GL_n(K)\to K^{\times} homomorphism"""
)
prop_3_46_label = latex_to_graphviz_html(
r"""\textbf{Prop 3.46 }G, H groups, \psi: G\to\ H homomorphism:\lbr """
r"""(a) \psi \text{ injective} \iff \text{ker}(\psi):=\{g\in G: \psi(g)=1\}=\{1\}\lbr """
r"""(b) \psi \text{ isomorphism} \text{implies} \psi^{-1}: H\to G isomorphism\lbr"""
)
thm_3_47_label = latex_to_graphviz_html(
r"""\textbf{Thm 3.47 }For cyclic group G:\\ G \cong \begin{cases}
\mathbb{Z} & \text{for }G\text{ infinite,}\\
\Z /|G|\mathbb{Z} & \text{for }G\text{ finite.} \end{cases}"""
)
ex_3_48_label = latex_to_graphviz_html(
r"""\textbf{Ex 3.48 } (\Z/5\Z)^{\times} \cong \Z/4\Z\\ \psi: \begin{cases}
\Z/4\Z &\to\ (\Z/5\Z)^{\times}\\
a &\mapsto\ \bar{2}^a \end{cases}\lbr"""
)
thm_3_49_label = latex_to_graphviz_html(
r"""\textbf{Thm 3.49 }G finite abelian group:
G is cyclic \iff \exp(G) = \vert G\vert"""
)
dot_content = f"""digraph G {{
layout=neato;
overlap="false";
sep="+15";
node [shape=box, fontsize=12, margin="0.15,0.1"];
labelloc=t
label="3.3 prime residue classes and Fermat-Euler theorem (Def 3.13 — Cor 3.32)\n3.4 cyclic groups (Def 3.33 — Thm 3.49)\n\n "
# def_2_1 [label={def_label}];
# ex_2_2a [label={ex_label}];
def_3_13 [label={def_3_13_label}];
ex_3_14 [label={ex_3_14_label}];
def_3_15 [label={def_3_15_label}];
ex_3_16 [label={ex_3_16_label}];
prop_3_17 [label={prop_3_17_label}];
ex_3_18 [label={ex_3_18_label}];
def_3_19 [label={def_3_19_label}];
ex_3_20 [label={ex_3_20_label}];
thm_3_21 [label={thm_3_21_label}];
prop_3_22 [label={prop_3_22_label}];
cor_3_23 [label={cor_3_23_label}];
ex_3_24 [label={ex_3_24_label}];
prop_3_25 [label={prop_3_25_label}];
ex_3_26 [label={ex_3_26_label}];
def_3_27 [label={def_3_27_label}];
ex_3_28 [label={ex_3_28_label}];
prop_3_29 [label={prop_3_29_label}];
thm_3_30 [label={thm_3_30_label}];
ex_3_31 [label={ex_3_31_label}];
cor_3_32 [label={cor_3_32_label}];
def_3_33 [label={def_3_33_label}];
rem_3_34 [label={rem_3_34_label}];
ex_3_35 [label={ex_3_35_label}];
def_3_36 [label={def_3_36_label}];
ex_3_37 [label={ex_3_37_label}];
prop_3_38 [label={prop_3_38_label}];
prop_3_39 [label={prop_3_39_label}];
cor_3_40 [label={cor_3_40_label}];
def_3_41 [label={def_3_41_label}];
ex_3_42 [label={ex_3_42_label}];
prop_3_43 [label={prop_3_43_label}];
def_3_44 [label={def_3_44_label}];
ex_3_45 [label={ex_3_45_label}];
prop_3_46 [label={prop_3_46_label}];
thm_3_47 [label={thm_3_47_label}];
ex_3_48 [label={ex_3_48_label}];
thm_3_49 [label={thm_3_49_label}];
def_3_13 -> ex_3_14 [style=dotted];
def_3_15 -> ex_3_16 [style=dotted];
{{ def_3_13 def_3_15 }} -> prop_3_17 [style=dotted];
ex_3_14 -> ex_3_16 [style=invis,arrowhead=none];
prop_3_17 -> ex_3_18 [style=dotted];
prop_3_17 -> thm_3_21;
{{ ex_3_16 def_3_19 }} -> ex_3_20 [style=dotted];
ex_3_20 -> thm_3_21;
prop_3_29 -> def_3_36;
thm_3_21 -> prop_3_22 [style=dotted];
prop_3_22 -> cor_3_23 [style=dotted];
def_3_19 -> thm_3_21;
cor_3_23 -> ex_3_24 [style=dotted];
cor_3_23 -> prop_3_25 [style=dotted];
prop_3_25 -> ex_3_26 [style=dotted];
ex_3_24 -> def_3_27 [style=invis,arrowhead=none];
prop_3_22 -> ex_3_28;
def_3_27 -> ex_3_28 [style=dotted];
def_3_33 -> ex_3_35 [style=dotted];
def_3_36 -> ex_3_37 [style=dotted];
thm_3_30 -> {{ ex_3_31 cor_3_32 }};
{{ ex_3_28 prop_3_29 }} -> thm_3_30;
ex_3_31 -> cor_3_32 [style=invis,arrowhead=none];
def_3_33 -> rem_3_34 [style=dotted label="https://gist.github.com/Hermann-SW/12c7644ac0c75b4eb019f76c3f023fe5 "
URL="https://gist.github.com/Hermann-SW/12c7644ac0c75b4eb019f76c3f023fe5"]
def_3_41 -> prop_3_43 [style=dotted];
prop_3_39 -> cor_3_40;
prop_3_39 -> prop_3_43;
def_3_44 -> {{ ex_3_45 prop_3_46 }} [style=dotted];
prop_3_38 -> thm_3_47;
prop_3_39 -> thm_3_47;
def_3_36 -> prop_3_38 [style=dotted];
prop_3_46 -> thm_3_47 [style=dotted];
prop_3_29 -> cor_3_40;
def_3_41 -> ex_3_42 [style=dotted];
{{ ex_3_35 thm_3_47 }} -> ex_3_48;
{{ prop_3_39 prop_3_43 }} -> thm_3_49;
prop_3_38 -> thm_3_49;
}}"""
DOTFILENAME = "graph_unicode.dot"
with open(DOTFILENAME, "w", encoding="utf-8") as f:
f.write(dot_content)
print("Generated .dot file:\n")
print(dot_content)
subprocess.run(
["dot", "-Tpdf", DOTFILENAME, "-o", "graph_output.pdf"], check=True
)
@Hermann-SW

Hermann-SW commented Sep 3, 2026

Copy link
Copy Markdown
Author

Work in progress, but does some nice stuff already.
(many many iterations working with gemini.google.com)

Below is the rendered graph_output.pdf, with Latex cases environment already working!

...
theo_3_47_label = latex_to_graphviz_html(
        r"""\textbf{Theo 3.47 }For cyclic group G:\\ G \cong \begin{cases}
            \mathbb{Z} & \text{for }G\text{ infinite,}\\
            \Z /|G|\mathbb{Z} & \text{for }G\text{ finite.} \end{cases}"""
)
...
image

@Hermann-SW

Hermann-SW commented Sep 3, 2026

Copy link
Copy Markdown
Author

Execute:

$ python build_graph.py 
Generated .dot file:

digraph G {
    layout=neato;
    overlap=prism;
    sep="+15";
    node [shape=box, fontsize=12, margin="0.15,0.1"];

    rankdir=LR

    Def_2_1 [label=<<B>Def 2.1 </B>a: ℕ →ℂ is &quot;number-theoretic function&quot; >];
    Ex_2_2a [label=<<B>Ex 2.2a </B>for s∈ℂ: ι<SUP>s</SUP>(n) := n<SUP>s</SUP> ∀n ∈ℕ is s-th power<BR/>function (s=0 constant one function, identity ι<SUP>1</SUP> =: ι)>];
    prop_3_38 [label=<<B>Prop 3.38 </B>G finite abelian group:<BR/>        G is cyclic ⇔∃g∈G with ord(g)=|G|>];
    prop_3_39 [label=<<B>Prop 3.39 </B>G finite abelian group,<BR/>g∈G, m∈ℤ: ord(g)|m ⇔g<SUP>m</SUP>=1>];
    theo_3_47 [label=<<TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" CELLPADDING="0"><TR><TD BORDER="0" ALIGN="LEFT" COLSPAN="2"><B>Theo 3.47 </B>For cyclic group G:</TD></TR><TR><TD BORDER="0" VALIGN="MIDDLE" ALIGN="LEFT"> G  ≅ </TD><TD BORDER="0" VALIGN="MIDDLE" ALIGN="LEFT"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" CELLPADDING="1"><TR><TD ROWSPAN="2" VALIGN="MIDDLE" ALIGN="RIGHT" BORDER="0"><FONT POINT-SIZE="22">&#123;</FONT></TD><TD ALIGN="LEFT" VALIGN="MIDDLE" BORDER="0">ℤ</TD><TD ALIGN="LEFT" VALIGN="MIDDLE" BORDER="0">&nbsp;&nbsp;&nbsp;&nbsp;for G infinite,</TD></TR><TR><TD ALIGN="LEFT" VALIGN="MIDDLE" BORDER="0">ℤ/|G|ℤ</TD><TD ALIGN="LEFT" VALIGN="MIDDLE" BORDER="0">&nbsp;&nbsp;&nbsp;&nbsp;for G finite.</TD></TR></TABLE></TD><TD BORDER="0" VALIGN="MIDDLE" ALIGN="LEFT"></TD></TR></TABLE>>];
    { prop_3_38 prop_3_39 } -> theo_3_47;
}
$ 

graph_output.pdf file gets created from created .dot file automatically:

$ ls -lst | head -2
total 13891660
     24 -rw-rw-r-- 1 hermann hermann      24281 Sep  3 17:24 graph_output.pdf
$ 

@Hermann-SW

Hermann-SW commented Sep 3, 2026

Copy link
Copy Markdown
Author

build_graph.py transpiles Latex "...^..." and "..._..." to "<SUP>...</SUP>" and "<SUB>...</SUB>" which works nicely in Graphviz HTML labels as seen above. Unfortunately SUP/SUB are not supported by viz-js from Mike Daines, so I created new issue:
mdaines/viz-js#409

GraphvizFiddle share link
image

@Hermann-SW

Hermann-SW commented Sep 3, 2026

Copy link
Copy Markdown
Author

Nice short script eg. allowing generation of new output in vi editor [my preferred editor since 1990 ;-)] while editing build_graph.py:

  • press :, then w and <ENTER> to write changes
  • press :, then !, P and <ENTER> to run the script and update PDF viewer displaying graph_output.pdf
$ cat /usr/local/bin/P
#!/bin/bash
python build_graph.py
$ 

build_graph.py does abort in case of an unknown macro and reports the macro name as well as the line number with the unknown macro (here when executed from within vi as described before):

Traceback (most recent call last):
  File "/home/hermann/Downloads/build_graph.py", line 248, in <module>
    def_label = latex_to_graphviz_html(
                ^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/hermann/Downloads/build_graph.py", line 194, in latex_to_graphviz_html
    raise ValueError(f"Unknown macro \\{missing_node.macroname}")
ValueError: Unknown macro \mathBb

shell returned 1

Press ENTER or type command to continue

@Hermann-SW

Copy link
Copy Markdown
Author

Latest gist output …
image

@Hermann-SW

Hermann-SW commented Sep 5, 2026

Copy link
Copy Markdown
Author

Added sections 3.3 “prime residue classes and Fermat-Euler theorem” and 3.4 “cyclic groups” of (German language) lecture script sitting behind Heidelberg University VPN. From definition 3.13 until theorem 3.49. More:
https://forum.graphviz.org/t/latex-labels-for-graphviz-work-in-progress/3402/3

Latest 10% screenshot below, full PDF for browser view:
https://stamm-wilbrandt.de/prime_residue_classess_and_Fermat-Euler_theorem.cyclic_groups.pdf
image

@Hermann-SW

Hermann-SW commented Sep 11, 2026

Copy link
Copy Markdown
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment