How to turn metadata into lineage diagrams, ERDs, and business-friendly mappings — and let yEd handle the layout.
Summary
Creating lineage, impact analysis, and ERD diagrams by hand is slow and fragile. In this article, I show how to generate GraphML with Python and open it directly in yEd (free). With one click of yEd’s autolayout, your metadata becomes a clean, professional diagram.
We’ll cover:
- Why yEd is a great (free) visualization engine
- A tiny Python “package” (helper module) for building GraphML
- Examples: Lineage, ERD with crow’s-foot, and combined ERD + Lineage with swimlanes
- Optionality (0..N) vs mandatory (1..N) using dashed vs solid edges
- Demo code you can copy-paste

Why yEd?
yEd Graph Editor is a free, desktop graph tool from yWorks. It loads GraphML (XML for graphs), supports thousands of nodes, and has world-class layout algorithms (Hierarchical, Orthogonal, Organic, Circular, etc.). Because GraphML is just text, we can generate it directly from DuckDB or any metadata store — and let yEd handle the visual magic.
Inspiration
I first explored code-generated GraphML at ASN Bank (De Volksbank).
Credit to Juan Jose van der Linden for sparking the idea: don’t draw diagrams by hand — generate them from metadata.

A Tiny Python “Package”: yed_graphml
To make this easy to reuse, here’s a small helper module you can drop into your project (e.g., yed_graphml.py). It wraps the yFiles GraphML tags and gives you simple functions for nodes, edges, groups (swimlanes), labels, colors, dashed/solid styles, positions, and legends.
Features
- yFiles GraphML: emits
<y:ShapeNode>,<y:PolyLineEdge>,<y:GroupNode>, etc. - Styling: colors, dashed/solid, borders, arrowheads
- Labels: node labels, edge end-labels (for crow’s-foot 1 vs N)
- Groups/Swimlanes: wrap nodes in logical sections (Raw, Stage, Integrated, ERD)
- Positions: optional x/y geometry for sensible defaults before autolayout
- Legend helper: drop a labeled box into the canvas
- Plain Python (no external deps), plus optional
duckdbfor metadata queries
Installation: copy the snippet below into
_yed_graphml.py_and import it in your scripts.
yed_graphml.py — minimal helper for yEd GraphML
import xml.sax.saxutils as saxutils
def esc(t):
return saxutils.escape(t if t is not None else "")
HEADER = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
xmlns:y="http://www.yworks.com/xml/graphml"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
<key id="d0" for="node" yfiles.type="nodegraphics"/>
<key id="d1" for="edge" yfiles.type="edgegraphics"/>
<graph id="G" edgedefault="directed">
"""
FOOTER = """
</graph>
</graphml>"""
def node(node_id, label, fill="#FFFFFF", x=None, y=None, w=170, h=50,
shape="roundrectangle", align="center", autosize=False, border="#2D3640"):
geo = ""
if x is not None and y is not None:
geo = f'<y:Geometry x="{x}" y="{y}" width="{w}" height="{h}"/>'
else:
geo = f'<y:Geometry width="{w}" height="{h}"/>'
auto = ' autoSizePolicy="content"' if autosize else ""
return f"""
<node id="{esc(node_id)}">
<data key="d0">
<y:ShapeNode>
{geo}
<y:Fill color="{esc(fill)}" transparent="false"/>
<y:BorderStyle color="{esc(border)}" type="line" width="1.0"/>
<y:NodeLabel alignment="{esc(align)}"{auto}>{esc(label)}</y:NodeLabel>
<y:Shape type="{esc(shape)}"/>
</y:ShapeNode>
</data>
</node>
"""
def edge(edge_id, source, target, color="#000000", dashed=False, width=1.2,
arrow_source="none", arrow_target="standard",
src_label=None, tgt_label=None):
line_type = "dashed" if dashed else "line"
src_lbl = (f'<y:EdgeLabel alignment="center" distance="10.0" '
f'modelName="twoPos" preferredPlacement="source">{esc(src_label)}</y:EdgeLabel>') if src_label else ""
tgt_lbl = (f'<y:EdgeLabel alignment="center" distance="10.0" '
f'modelName="twoPos" preferredPlacement="target">{esc(tgt_label)}</y:EdgeLabel>') if tgt_label else ""
return f"""
<edge id="{esc(edge_id)}" source="{esc(source)}" target="{esc(target)}">
<data key="d1">
<y:PolyLineEdge>
<y:LineStyle color="{esc(color)}" type="{line_type}" width="{width}"/>
<y:Arrows source="{esc(arrow_source)}" target="{esc(arrow_target)}"/>
{src_lbl}
{tgt_lbl}
</y:PolyLineEdge>
</data>
</edge>
"""
def open_group(group_id, title, x=0, y=0, w=400, h=300, fill="#F7F7F7"):
return f"""
<node id="{esc(group_id)}" yfiles.foldertype="group">
<data key="d0">
<y:ProxyAutoBoundsNode>
<y:Realizers active="0">
<y:GroupNode>
<y:Geometry x="{x}" y="{y}" width="{w}" height="{h}"/>
<y:Fill color="{esc(fill)}" transparent="false"/>
<y:BorderStyle color="#A0A0A0" type="dashed" width="1.0"/>
<y:NodeLabel alignment="center" modelName="internal" modelPosition="t">{esc(title)}</y:NodeLabel>
<y:Shape type="roundrectangle"/>
</y:GroupNode>
</y:Realizers>
</y:ProxyAutoBoundsNode>
</data>
<graph edgedefault="directed">
"""
def close_group():
return """
</graph>
</node>
"""
def legend(node_id, text, x=0, y=0, w=260, h=140):
return node(node_id, text, fill="#FFFFFF", x=x, y=y, w=w, h=h, shape="rectangle", align="left")
def write_graphml(nodes_xml, edges_xml, path):
content = HEADER + "".join(nodes_xml) + "".join(edges_xml) + FOOTER
with open(path, "w", encoding="utf-8") as f:
f.write(content)
return path
Example 1 — Lineage (Raw → Stage → Integrated)
from yed_graphml import node, edge, write_graphml
nodes = [
node("raw_customers", "customers_raw", "#DAE8FC"),
node("raw_orders", "orders_raw", "#DAE8FC"),
node("stg_customers", "customers_stage", "#FFF2CC"),
node("stg_orders", "orders_stage", "#FFF2CC"),
node("int_sales", "sales", "#D5F5E3"),
]
edges = [
edge("e1", "raw_customers", "stg_customers"),
edge("e2", "raw_orders", "stg_orders"),
edge("e3", "stg_customers", "int_sales"),
edge("e4", "stg_orders", "int_sales"),
]
write_graphml(nodes, edges, "lineage_demo.graphml")
Open lineage_demo.graphml in yEd → Layout → Hierarchical.
Example 2 — ERD with Crow’s-Foot + Colors + Optionality
We emulate crow’s-foot using end labels (1 near parent, N near child). Solid = mandatory (1..N). Dashed = optional (0..N).
from yed_graphml import node, edge, write_graphml
entities = [
node("cust", "customers\n-\nPK customer_id\nname\nemail", "#CFE8FF",
w=240, h=150, shape="rectangle", align="left", autosize=True),
node("ord", "orders\n-\nPK order_id\nFK customer_id → customers.customer_id\norder_date\ntotal_amount",
"#FFF2CC", w=300, h=180, shape="rectangle", align="left", autosize=True),
node("line", "order_lines\n-\nPK order_line_id\nFK order_id → orders.order_id\nproduct_id\nquantity\nprice",
"#D5F5E3", w=300, h=180, shape="rectangle", align="left", autosize=True),
]
rels = [
# customers (1) -> (N) orders - mandatory: solid
edge("r1", "cust", "ord", color="#2D3640", dashed=False, src_label="1", tgt_label="N", arrow_target="none"),
# orders (1) -> (N) order_lines - optional: dashed (0..N)
edge("r2", "ord", "line", color="#2D3640", dashed=True, src_label="0..1", tgt_label="0..N", arrow_target="none"),
]
write_graphml(entities, rels, "erd_crowsfoot_optional.graphml")
Example 3 — ERD + Lineage Together (Different Edge Styles)
- Lineage: solid black
- ERD FKs: dashed gray, crow’s-foot labels
- Legend: included
from yed_graphml import node, edge, legend, write_graphml
nodes = [ # Lineage layers node("lr0", "customers_raw", "#DAE8FC"), node("lr1", "orders_raw", "#DAE8FC"), node("ls0", "customers_stage", "#FFF2CC"), node("ls1", "orders_stage", "#FFF2CC"), node("li0", "sales", "#D5F5E3"), # ERD entities node("ecust", "customers\n-\nPK customer_id\nname\nemail", "#CFE8FF", w=240, h=150, shape="rectangle", align="left", autosize=True), node("eord", "orders\n-\nPK order_id\nFK customer_id → customers.customer_id\norder_date\ntotal_amount", "#FFF2CC", w=300, h=180, shape="rectangle", align="left", autosize=True), node("eline", "order_lines\n-\nPK order_line_id\nFK order_id → orders.order_id\nproduct_id\nquantity\nprice", "#D5F5E3", w=300, h=180, shape="rectangle", align="left", autosize=True), legend("legend", "Legend:\nLineage edges = solid black\nERD FK edges = dashed gray (1/N end labels)\nLayers: Blue=Raw, Yellow=Stage, Green=Integrated", x=0, y=0),]
edges = [ # Lineage (solid) edge("L0", "lr0", "ls0", color="#000000", dashed=False), edge("L1", "lr1", "ls1", color="#000000", dashed=False), edge("L2", "ls0", "li0", color="#000000", dashed=False), edge("L3", "ls1", "li0", color="#000000", dashed=False), # ERD (dashed gray, crow's-foot)
edge("E0", "ecust", "eord", color="#808080", dashed=True, arrow_target="none", src_label="1", tgt_label="N"),
edge("E1", "eord", "eline", color="#808080", dashed=True, arrow_target="none", src_label="1", tgt_label="N"),
]
write_graphml(nodes, edges, "erd_lineage_combined_styled.graphml")
Open and Layout → Hierarchical (good for flows) or Orthogonal (ERDish).
Example 4 — Swimlanes + Pre-Positioning + Legend
Here we split Raw / Stage / Integrated / ERD Entities into group nodes (swimlanes), pre-place nodes for a sensible initial view, keep the same edge styles, and add a legend.
from yed_graphml import (open_group, close_group, node, edge, legend, write_graphml)
parts = []
# RAW
parts.append(open_group("g_raw", "Raw", x=40, y=40, w=520, h=220))
parts.append(node("lr0", "customers_raw", "#DAE8FC", x=60, y=100, w=170, h=50))
parts.append(node("lr1", "orders_raw", "#DAE8FC", x=260, y=100, w=170, h=50))
parts.append(close_group())
# STAGE
parts.append(open_group("g_stage", "Stage", x=600, y=40, w=520, h=220))
parts.append(node("ls0", "customers_stage", "#FFF2CC", x=620, y=100, w=170, h=50))
parts.append(node("ls1", "orders_stage", "#FFF2CC", x=820, y=100, w=170, h=50))
parts.append(close_group())
# INTEGRATED
parts.append(open_group("g_int", "Integrated", x=1160, y=40, w=520, h=220))
parts.append(node("li0", "sales", "#D5F5E3", x=1180, y=110, w=170, h=50))
parts.append(close_group())
# ERD
parts.append(open_group("g_erd", "ERD Entities", x=40, y=320, w=1280, h=320))
parts.append(node("ecust", "customers\n-\nPK customer_id\nname\nemail", "#CFE8FF",
x=60, y=380, w=260, h=160, shape="rectangle", align="left", autosize=True))
parts.append(node("eord", "orders\n-\nPK order_id\nFK customer_id → customers.customer_id\norder_date\ntotal_amount",
"#FFF2CC", x=380, y=380, w=300, h=180, shape="rectangle", align="left", autosize=True))
parts.append(node("eline", "order_lines\n-\nPK order_line_id\nFK order_id → orders.order_id\nproduct_id\nquantity\nprice",
"#D5F5E3", x=740, y=380, w=300, h=180, shape="rectangle", align="left", autosize=True))
parts.append(close_group())
# Legend
parts.append(legend("legend", "Legend:\nLineage = solid black\nERD FK = dashed gray (crow's-foot labels)\nSwimlanes: Raw / Stage / Integrated / ERD",
x=1360, y=60, w=320, h=180))
# Edges
edges = [
# Lineage
edge("L0", "lr0", "ls0", color="#000000", dashed=False),
edge("L1", "lr1", "ls1", color="#000000", dashed=False),
edge("L2", "ls0", "li0", color="#000000", dashed=False),
edge("L3", "ls1", "li0", color="#000000", dashed=False),
# ERD (crow's-foot)
edge("E0", "ecust", "eord", color="#808080", dashed=True, arrow_target="none", src_label="1", tgt_label="N"),
edge("E1", "eord", "eline", color="#808080", dashed=True, arrow_target="none", src_label="1", tgt_label="N"),
]
# Write file
from yed_graphml import HEADER, FOOTER
with open("erd_lineage_swimlanes.graphml", "w", encoding="utf-8") as f:
f.write(HEADER)
f.write("".join(parts))
f.write("".join(edges))
f.write(FOOTER)
Open in yEd — you already have a good layout, but Autolayout refines it while keeping groups meaningful.
(Optional) ERD from DuckDB’s information_schema
You can auto-discover tables and FKs from DuckDB and plug them into the helpers above.
import duckdb
from yed_graphml import node, edge, write_graphml
con = duckdb.connect(":memory:")
con.execute("create table customers (customer_id integer primary key, name varchar, email varchar)")
con.execute("""
create table orders (
order_id integer primary key,
customer_id integer references customers(customer_id),
amount decimal
)""")
tables = [r[0] for r in con.execute("select table_name from information_schema.tables where table_schema='main'").fetchall()]
fks = con.execute("select table_name, foreign_table_name from information_schema.foreign_keys").fetchall()
nodes = [node(t, t, "#DAE8FC") for t in tables]
edges = [edge(f"fk_{i}", fk[1], fk[0], color="#808080", dashed=True, arrow_target="none", src_label="1", tgt_label="N")
for i, fk in enumerate(fks)]
write_graphml(nodes, edges, "duckdb_erd.graphml")
Wrap-Up
With a few lightweight helpers, you can:
- Generate GraphML from metadata (DuckDB, catalogs, YAML, LinkML, etc.)
- Open in yEd (free), click Autolayout, and get clean diagrams
- Combine ERD + Lineage and add business-friendly visuals (swimlanes, legends, colors)
- Model cardinality & optionality clearly (1..N vs 0..N)
No manual drawing. Always in sync with metadata.
What’s Next
In a follow-up, I’ll connect this to business-friendly mapping: draw.io/YAML recipes → generate SQL/dbt and diagrams (LinkML/LinkML-Map as the rigorous backbone, with a visual front-end).