Entity Relationship Diagram

The Graphviz gallery includes a Chen-style entity relationship diagram. We can reproduce that example by transcribing the gallery’s DOT language input file to gvdot calls.

dot = Dot().graph(layout="neato")
dot.all_default(fontname="Helvetica,Arial,sans-serif")

dot.node_role("entity",shape="box")
dot.node_role("attribute",shape="ellipse")
dot.node_role("relationship",shape="diamond",style="filled",color="lightgrey")

dot.node('course', role="entity")
dot.node('institute', role="entity")
dot.node('student', role="entity")

dot.node('name0', label='name', role="attribute")
dot.node('name1', label='name', role="attribute")
dot.node('name2', label='name', role="attribute")

dot.node('code', role="attribute")
dot.node('grade', role="attribute")
dot.node('number', role="attribute")

dot.node('C-I', role="relationship")
dot.node('S-C', role="relationship")
dot.node('S-I', role="relationship")

dot.edge('name0', 'course')
dot.edge('code', 'course')
dot.edge('course', 'C-I', label='n', len=1.0)
dot.edge('C-I', 'institute', label='1', len=1.0)
dot.edge('institute', 'name1')
dot.edge('institute', 'S-I', label='1', len=1.0)
dot.edge('S-I', 'student', label='n', len=1.0)
dot.edge('student', 'grade')
dot.edge('student', 'name2')
dot.edge('student', 'number')
dot.edge('student', 'S-C', label='m', len=1.0)
dot.edge('S-C', 'course', label='n', len=1.0)

dot.graph(fontsize=20, label='\n\nEntity Relation Diagram\ndrawn by NEATO')

dot.show(size="5,5")

Aside from method calls instead of DOT statements, the primary difference between the original DOT file and the transcription is that the DOT file restates node defaults three times to reduce explicit attribute value assignments, while the code uses gvdot roles.

Transcribed ER-diagram

The transcription output matches the original.

Restating defaults is convenient when writing DOT by hand, but it can be awkward when programmatically generating DOT directly because it forces code to group together nodes and edges in a way that may be inconsistent with the structure of the code’s input. That’s not a problem for gvdot applications because gvdot supports order independence and roles.

Suppose we want to generate diagrams from ER models represented by the following classes.

@dataclass
class Entity:
    name : str
    attributes : list[str]

@dataclass
class Participant:
    entity : str
    cardinality : str

@dataclass
class Relationship:
    name : str
    participants : list[Participant]

@dataclass
class Model:
    entities : list[Entity]
    relationships : list[Relationship]

Let’s create a model based on the gallery example.

example_model = Model(
    entities = [
        Entity("course",["name","code"]),
        Entity("institute",["name"]),
        Entity("student",["name","number","grade"]),
    ],
    relationships = [
        Relationship("C-I",[
            Participant("course","n"),
            Participant("institute","1")]),
        Relationship("S-I",[
            Participant("institute","1"),
            Participant("student","n")]),
        Relationship("S-C",[
            Participant("course","n"),
            Participant("student","m"),
        ])
    ])

Chen ER-diagrams use rectangles for entities, ellipses for attributes, and diamonds for relationships. We define an ER-diagram theme with those shapes. We also choose the same font and layout engine as the gallery, and define edge roles as well.

er_base_theme = (Dot()
    .graph(layout="neato")
    .all_default(fontname="Helvetica,Arial,sans-serif")
    .node_role("entity", shape="box")
    .node_role("attribute", shape="ellipse")
    .node_role("relationship", shape="diamond")
    .edge_role("e-to-r", len=1.0)  # entity-to-relationship
    .edge_role("a-to-e"))          # attribute-to-entity

The gallery example uses a solid light grey for relationships, so we specialize the theme a bit.

er_gallery_theme = (Dot().use_theme(er_base_theme)
    .node_role("relationship", style="filled", color="lightgrey"))

Now we write our code to generate diagrams from models. Observe there are no presentation-related attribute assignments. We could have them, of course, but relying on roles and defaults from the theme makes the code easy to understand and maintain.

def er_diagram(model:Model, title:str, theme:Dot) -> Dot:
    dot = Dot().use_theme(theme)
    for entity in model.entities:
        dot.node(entity.name, role="entity")
        for attribute in entity.attributes:
            attribute_id = Nonce("attribute")
            dot.node(attribute_id, role="attribute", label=attribute)
            dot.edge(entity.name, attribute_id, role="a-to-e")
    for relationship in model.relationships:
        dot.node(relationship.name, role="relationship")
        for participant in relationship.participants:
            dot.edge(relationship.name,participant.entity,
                    label=participant.cardinality, role="e-to-r")
    dot.graph(fontsize=16, label=f"\n{title}")
    return dot

diagram = er_diagram(example_model, "Generated Using gvdot", er_gallery_theme)
diagram.show(size="5,5")

Notice we use Nonce objects to identify attribute nodes. We can’t use attribute names because attribute names are not unique within a model.

Generated Black and White ER-diagram

The layout differs because neato depends on statement order.

The generated diagram is equivalent to the transcribed version. We can see how gvdot incorporated the theme and roles by examining its DOT language representation by either executing something like

print(diagram)

or in a notebook

diagram.show_source()

which displays

graph {

    graph [fontname="Helvetica,Arial,sans-serif"]
    node [fontname="Helvetica,Arial,sans-serif"]
    edge [fontname="Helvetica,Arial,sans-serif"]

    layout=neato
    fontsize=16

    course [shape=box]
    attribute_1 [label="name" shape=ellipse]
    attribute_2 [label="code" shape=ellipse]
    institute [shape=box]
    attribute_3 [label="name" shape=ellipse]
    student [shape=box]
    attribute_4 [label="name" shape=ellipse]
    attribute_5 [label="number" shape=ellipse]
    attribute_6 [label="grade" shape=ellipse]
    "C-I" [shape=diamond style=filled color=lightgrey]
    "S-I" [shape=diamond style=filled color=lightgrey]
    "S-C" [shape=diamond style=filled color=lightgrey]

    course -- attribute_1
    course -- attribute_2
    institute -- attribute_3
    student -- attribute_4
    student -- attribute_5
    student -- attribute_6
    "C-I" -- course [label="n" len=1.0]
    "C-I" -- institute [label="1" len=1.0]
    "S-I" -- institute [label="1" len=1.0]
    "S-I" -- student [label="n" len=1.0]
    "S-C" -- course [label="n" len=1.0]
    "S-C" -- student [label="m" len=1.0]

    label="\nGenerated Using gvdot"
}

The gallery theme is quite plain looking. Let’s render something with color. All we need to do is change the theme – we don’t even need to rebuild the diagram.

er_color_theme = (Dot().use_theme(er_base_theme)
    .node_role("entity",style="filled",fillcolor="darkolivegreen2")
    .node_role("attribute",style="filled",fillcolor="lightblue")
    .node_role("relationship",style="filled",fillcolor="sandybrown"))

diagram.use_theme(er_color_theme)
diagram.show(size="5,5")
Generated Color ER-diagram

Much nicer.

Let’s save this one.

diagram.save("er-diagram.svg")

You can run the Entity Relationship Diagram example yourself by downloading the notebook from GitHub.