Drawing graphs and rules#

Every graph and rule can draw itself. print() adds the depiction to the HTML summary (or shows it inline in a notebook, as on this page), and svg() returns the same picture as an SVG string. Both take an optional GraphPrinter that decides how the depiction looks:

p = GraphPrinter()
p.drawMode = DrawMode.Circles
g.print(p)         # into the summary / the notebook
svg = g.svg(p)     # the same drawing as an SVG string

A printer is a plain bag of settings: create one per style and reuse it for as many drawings as you like. This page shows every setting in turn, first on molecules, then on rules and derivations.

Setting

Values (default first)

Graphs

Rules

Derivations

drawMode

DrawMode: Mol, Circles

✓

collapseHydrogens

True, False

✓

withIndex

False, True

✓

✓

✓

withExternalId

False, True

✓

drawStyle

DrawStyle: Default, xkcd

✓

✓

✓

layout

GraphLayout: Mol, Force

✓

✓

darkMode

DarkMode: Dynamic, On, Off

✓

✓

✓

drawRuleConditions

False, True

✓

pushVertexOverlay()

a function returning a VertexOverlay or None

✓

Graphs#

The default depiction#

Without a printer a molecule is drawn as a skeletal formula: carbons are bare line junctions, heteroatoms are labeled, and hydrogens are folded into the label of the atom that carries them.

pyruvic_acid = smiles("CC(=O)C(=O)O", "pyruvic acid")
pyruvic_acid.print()
pyruvic acid
O O OH

Draw mode#

drawMode picks the depiction. DrawMode.Mol (the default) is the skeletal formula above; DrawMode.Circles draws the graph exactly as the rewriting engine sees it — every vertex, hydrogens included, is a labeled circle and every edge a bond line.

for mode in (DrawMode.Mol, DrawMode.Circles):
    p = GraphPrinter()
    p.drawMode = mode
    smiles("CC(=O)C(=O)O", f"drawMode = DrawMode.{mode.name}").print(p)
drawMode = DrawMode.Mol
O O OH
drawMode = DrawMode.Circles
H H H H O O O C C C

Hydrogens#

In DrawMode.Mol, collapseHydrogens = False draws every hydrogen as an atom of its own instead of folding it into a label. DrawMode.Circles always draws every hydrogen, so the setting has no effect there.

p = GraphPrinter()
p.collapseHydrogens = False
pyruvic_acid.print(p)
pyruvic acid
H H H H O O O

Vertex ids and external ids#

withIndex = True labels each vertex with its id — the v.id you get when iterating g.vertices — so you can find an atom by number:

for mode in (DrawMode.Mol, DrawMode.Circles):
    p = GraphPrinter()
    p.drawMode = mode
    p.withIndex = True
    smiles("CC(=O)C(=O)O", f"withIndex, DrawMode.{mode.name}").print(p)
withIndex, DrawMode.Mol
O O OH 4 5 6 7 8 9
withIndex, DrawMode.Circles
H H H H O O O C C C 0 1 2 3 4 5 6 7 8 9

withExternalId = True appends each atom’s external id to its label, as C:2. External ids are the atom-map numbers of a SMILES string, the node ids of a GML file, or ids set with setExternalId(); atoms without one keep their plain label.

mapped = smiles("[CH3:1][C:2](=[O:3])[C:4](=[O:5])[OH:6]", "atom-mapped pyruvic acid")
for mode in (DrawMode.Mol, DrawMode.Circles):
    p = GraphPrinter()
    p.drawMode = mode
    p.withExternalId = True
    mapped.print(p)
atom-mapped pyruvic acid
O:3 O:5 OH:6 C:4 C:2 C:1
atom-mapped pyruvic acid
H H H H O:3 O:5 O:6 C:4 C:2 C:1

Drawing style#

drawStyle = DrawStyle.xkcd swaps the precise strokes for a hand-drawn look — wobbly lines and handwritten glyphs — in either draw mode. It suits slides and posters where a drawing should read as a sketch rather than a measurement.

for mode in (DrawMode.Mol, DrawMode.Circles):
    p = GraphPrinter()
    p.drawMode = mode
    p.drawStyle = DrawStyle.xkcd
    smiles("CC(=O)C(=O)O", f"DrawStyle.xkcd, DrawMode.{mode.name}").print(p)
DrawStyle.xkcd, DrawMode.Mol
DrawStyle.xkcd, DrawMode.Circles

Layout#

layout picks the engine that places the atoms. GraphLayout.Mol (the default) is chemistry-aware: regular rings, fused ring templates and standard bond angles. GraphLayout.Force is a generic force-directed layout that treats the molecule as an arbitrary graph — useful for graphs that are not molecules at all.

for layout in (GraphLayout.Mol, GraphLayout.Force):
    p = GraphPrinter()
    p.layout = layout
    smiles("CN1C=NC2=C1C(=O)N(C(=O)N2C)C", f"caffeine, GraphLayout.{layout.name}").print(p)
caffeine, GraphLayout.Mol
O O N N N N
caffeine, GraphLayout.Force
O O N N N N

Light and dark theme#

darkMode picks the color theme. DarkMode.On and DarkMode.Off bake the dark or light theme into the SVG, which is what you want for a drawing saved to a file. DarkMode.Dynamic (the default) follows the page the SVG is embedded in: it switches with the light/dark toggle of the HTML summary (the Bootstrap data-bs-theme attribute) and draws light where the page sets no theme.

for theme in (DarkMode.Dynamic, DarkMode.On, DarkMode.Off):
    p = GraphPrinter()
    p.darkMode = theme
    smiles("CC(=O)C(=O)O", f"DarkMode.{theme.name}").print(p)
DarkMode.Dynamic
O O OH
DarkMode.On
O O OH
DarkMode.Off
O O OH

Electron graphs#

A molecule loaded with graphType=GraphType.Electron carries every valence electron as a vertex of its own (labeled <e>). Both draw modes depict those electrons: DrawMode.Mol as a Lewis structure — a bar for each lone pair, a dot for each unpaired electron — and DrawMode.Circles as one dot per electron around its atom.

for mode in (DrawMode.Mol, DrawMode.Circles):
    p = GraphPrinter()
    p.drawMode = mode
    smiles("CC(=O)C(=O)O", f"electron graph, DrawMode.{mode.name}",
           graphType=GraphType.Electron).print(p)
electron graph, DrawMode.Mol
O O HO
electron graph, DrawMode.Circles
H H H H C C C O O O

Open-shell species show the difference between the two kinds of non-bonding electrons. An ordinary molecule graph marks only radicals, as dots (its lone pairs follow from the octet), while the electron graph of the aminyl radical shows its lone pair and its unpaired electron side by side:

smiles("[CH3]", "methyl radical").print()
smiles("[NH2]", "aminyl radical (electron graph)", graphType=GraphType.Electron).print()
methyl radical
CH4
aminyl radical (electron graph)
NH2

Vertex overlays#

An overlay puts a colored marker with a short label on chosen atoms — to highlight a reaction center, show where an atom came from, or number atoms in your own scheme. Register a function with pushVertexOverlay(): it is called with each vertex and returns a VertexOverlay for the atoms to mark, or None for the rest. A marker is a filled shape from OverlayShape; here each carbon of naphthalene gets a different one, labeled with its vertex id:

naphthalene = smiles("c1ccc2ccccc2c1", "naphthalene")
colors = ["#e6194b", "#3cb44b", "#4363d8", "#f58231",
          "#911eb4", "#42d4f4", "#f032e6", "#9a6324"]
carbons = [v for v in naphthalene.vertices if v.stringLabel == "C"]
marker_of = {v.id: (shape, color) for v, shape, color in zip(carbons, OverlayShape, colors)}

def by_shape(v):
    if v.id not in marker_of:
        return None
    shape, color = marker_of[v.id]
    return VertexOverlay(color=color, label=v.id, shape=shape)

p = GraphPrinter()
p.pushVertexOverlay(by_shape)
naphthalene.print(p)
naphthalene
8 9 10 11 12 13 14 15

The eight shapes, in the order assigned above, are Circle, Square, TriangleUp, TriangleDown, TriangleLeft, TriangleRight, Hexagon and none — the last draws only the label, leaving the atom underneath visible. Naphthalene has ten carbons, so the last two stay unmarked.

Overlay functions form a stack. Each atom gets the marker of the most recently pushed function that returns one, so a second function can override the first for a few atoms and fall through to it for the rest; popVertexOverlay() removes it again. Here the two ring-fusion carbons — the ones without a hydrogen — get a larger gold marker; radius sets the marker size and text_color the label color:

fusion = {v.id for v in carbons
          if all(e.target.stringLabel != "H" for e in v.incidentEdges)}
p.pushVertexOverlay(lambda v: VertexOverlay(color="#ffd700", label="F", radius=14,
                                            text_color="#000000")
                    if v.id in fusion else None)
naphthalene.print(p)
p.popVertexOverlay()
naphthalene
F F 10 11 12 13 14 15

Overlays combine with every other setting — draw mode, drawing style, theme — and, through a derivation-graph printer’s graphPrinter, mark the molecules inside a reaction network as well.

Rules#

A rule is drawn as its three graphs side by side — L (left), K (context) and R (right) — with every vertex a labeled circle and vertices that correspond across the three panels in the same position. drawMode, collapseHydrogens, withExternalId and vertex overlays apply to graphs only; the remaining settings work for rules as well.

KETO_ENOL = '''rule [
    ruleID "keto to enol"
    left  [ edge [ source 1 target 4 label "-" ] edge [ source 1 target 2 label "-" ]
            edge [ source 2 target 3 label "=" ] ]
    context [ node [ id 1 label "C" ] node [ id 2 label "C" ]
              node [ id 3 label "O" ] node [ id 4 label "H" ] ]
    right [ edge [ source 1 target 2 label "=" ] edge [ source 2 target 3 label "-" ]
            edge [ source 3 target 4 label "-" ] ]
]'''
keto_enol = ruleGMLString(KETO_ENOL)
keto_enol.print()
keto to enol
C C O H C C O H C C O H

Rule node ids#

withIndex = True labels every vertex in each panel with its rule node id — the id from the GML — which ties the drawing to the rule source:

p = GraphPrinter()
p.withIndex = True
keto_enol.print(p)
keto to enol
C C O H 1 2 3 4 C C O H 1 2 3 4 C C O H 1 2 3 4

Application conditions#

A rule may carry an application condition that restricts where it fires — here a negative condition: the carbon must not carry a hydrogen. Conditions are hidden by default; drawRuleConditions = True draws them in a band below the three panels:

oxidize = ruleGMLString('''rule [ ruleID "oxidize (only if no C–H)"
  left    [ edge [ source 0 target 1 label "-" ] ]
  context [ node [ id 0 label "C" ] node [ id 1 label "O" ] ]
  right   [ edge [ source 0 target 1 label "=" ] ]
  condition [
    not [
      exists [ node [ id 100 label "H" ] edge [ source 0 target 100 label "-" ] ]
    ]
  ]
]''')
p = GraphPrinter()
p.drawRuleConditions = True
oxidize.print(p)
oxidize (only if no C–H)
C O C O C O Application condition ∄ [H] 0–100:- 0 H

The graph-rewriting guide explains the condition language and its constraint shorthands.

Drawing style#

The hand-drawn style works for rules too:

p = GraphPrinter()
p.drawStyle = DrawStyle.xkcd
keto_enol.print(p)
keto to enol

Layout#

A rule is laid out the first time it is drawn, and later drawings reuse that layout — whatever their printer says — so the panels of a rule always look the same. Choose layout on the first drawing; to compare engines, load the rule once per engine. The Diels–Alder rule shows the difference: the chemistry-aware layout closes the new ring into a hexagon.

DIELS_ALDER = '''rule [
    ruleID "Diels-Alder"
    left [ edge [ source 1 target 2 label "=" ] edge [ source 2 target 3 label "-" ]
           edge [ source 3 target 4 label "=" ] edge [ source 5 target 6 label "=" ] ]
    context [ node [ id 1 label "C" ] node [ id 2 label "C" ] node [ id 3 label "C" ]
              node [ id 4 label "C" ] node [ id 5 label "C" ] node [ id 6 label "C" ] ]
    right [ edge [ source 1 target 2 label "-" ] edge [ source 2 target 3 label "=" ]
            edge [ source 3 target 4 label "-" ] edge [ source 4 target 5 label "-" ]
            edge [ source 5 target 6 label "-" ] edge [ source 6 target 1 label "-" ] ]
]'''
for layout in (GraphLayout.Mol, GraphLayout.Force):
    p = GraphPrinter()
    p.layout = layout
    ruleGMLString(DIELS_ALDER, f"Diels-Alder, GraphLayout.{layout.name}").print(p)
Diels-Alder, GraphLayout.Mol
C C C C C C C C C C C C C C C C C C
Diels-Alder, GraphLayout.Force
C C C C C C C C C C C C C C C C C C

Light and dark theme#

darkMode behaves exactly as for graphs:

for theme in (DarkMode.Dynamic, DarkMode.On, DarkMode.Off):
    p = GraphPrinter()
    p.darkMode = theme
    ruleGMLString(KETO_ENOL, f"keto to enol, DarkMode.{theme.name}").print(p)
keto to enol, DarkMode.Dynamic
C C O H C C O H C C O H
keto to enol, DarkMode.On
C C O H C C O H C C O H
keto to enol, DarkMode.Off
C C O H C C O H C C O H

Electron rules#

A rule of type "electron" rewrites electron graphs: its vertices include the <e> electrons, and moving an edge between them moves electrons between atoms. It is drawn in the Lewis style of electron graphs, with the atoms in the same place in all three panels. Here two hydrogen radicals pair their electrons into a bond:

recombination = ruleGMLString('''rule [
  ruleID "radical recombination" type "electron"
  context [
    node [ id 0 label "H" ] node [ id 1 label "H" ]
    node [ id 2 label "<e>" ] node [ id 3 label "<e>" ]
    edge [ source 0 target 2 ] edge [ source 1 target 3 ]
  ]
  right [ edge [ source 2 target 3 ] ]
]''')
recombination.print()
radical recombination
H H H H H H

Derivations#

A derivation — one application of a rule to concrete molecules — is drawn as a double-pushout diagram of six panels: the rule’s L, K and R, and the matching G (the educts), D (the part of the educts that the rule leaves untouched) and H (the products). Apply the keto→enol rule to acetone and draw the resulting reaction with withIndex = True: the rule panels show rule node ids, the molecule panels each atom’s vertex id in its educt.

acetone = smiles("CC(=O)C", "acetone")
dg = DG()
dg.build().apply([acetone], keto_enol)
p = GraphPrinter()
p.withIndex = True
for reaction in dg.edges:
    reaction.print(p)
Derivation {acetone} -> {g_{19}}
C C O H 1 2 3 4 C C O H 1 2 3 4 C C O H 1 2 3 4 H H H H H H O C C C 0 1 2 3 4 5 6 7 8 9 H H H H H H O C C C 0 1 2 3 4 5 6 7 8 9 H H H H H H O C C C 0 1 2 3 4 5 6 7 8 9 L K R G D H

A derivation of an electron rule is drawn in the electron-pushing style instead: the educts as electron graphs, with blue arrows showing where the electrons go. withIndex, drawStyle and darkMode apply to both kinds of diagram.

h_radical = smiles("[H]", "hydrogen radical", graphType=GraphType.Electron)
dg = DG()
dg.build().apply([h_radical, h_radical], recombination)
for reaction in dg.edges:
    reaction.print()
Derivation {hydrogen radical, hydrogen radical} -> {g_{21}}
H H

Saving a drawing#

svg() returns the drawing as a self-contained SVG document. Bake the theme in with DarkMode.On or DarkMode.Off before writing it to a file, since a file has no page theme to follow:

p = GraphPrinter()
p.darkMode = DarkMode.Off
with open("pyruvic_acid.svg", "w", encoding="utf-8") as f:
    f.write(pyruvic_acid.svg(p))

Whole reaction networks are drawn with a DGPrinter; its graphPrinter is a GraphPrinter that styles every molecule in the network, so all the graph settings above carry over. See Depicting derivation graphs.