23. Creating graphs

Key points:

  1. Build graphs from nodes, edges and subgraphs.
  2. Learn about Abjad’s model of LilyPond contexts.
  3. Use mappings to coordinate between data structures.
  4. Style and render graphs via Graphviz.

23.1. Graph basics

>>> my_graph = graphtools.GraphvizGraph()
>>> node_a = graphtools.GraphvizNode(name='A', attributes={'label': 'A'})
>>> node_b = graphtools.GraphvizNode(name='B', attributes={'label': 'B'})
>>> node_c = graphtools.GraphvizNode(name='C', attributes={'label': 'C'})
>>> node_d = graphtools.GraphvizNode(name='D', attributes={'label': 'D'})
>>> my_graph.extend([node_a, node_b, node_c, node_d])
>>> graph(my_graph)
>>> my_graph['B'].attributes['shape'] = 'diamond'
>>> graph(my_graph)
>>> ab_edge = my_graph['A'].attach(my_graph['B'])
>>> bc_edge = my_graph['B'].attach(my_graph['C'])
>>> bd_edge = my_graph['B'].attach(my_graph['D'])
>>> graph(my_graph)
>>> bc_edge.attributes['style'] = 'dotted'
>>> bd_edge.attributes['style'] = 'dashed'
>>> my_graph.attributes['bgcolor'] = 'transparent'
>>> my_graph.node_attributes.update(
...     fontname='Arial',
...     penwidth=2,
...     )
>>> my_graph.edge_attributes.update(
...     color='grey',
...     penwidth=2,
...     )
>>> graph(my_graph)
>>> print(format(my_graph, 'graphviz'))
digraph G {
    graph [bgcolor=transparent];
    node [fontname=Arial,
        penwidth=2];
    edge [color=grey,
        penwidth=2];
    A [label=A];
    B [label=B,
        shape=diamond];
    C [label=C];
    D [label=D];
    A -> B;
    B -> C [style=dotted];
    B -> D [style=dashed];
}

23.2. Collecting data for the graph

>>> for context in lilypondnametools.LilyPondContext.list_all_contexts():
...     print(context.name)
...     for child_context in context.accepts:
...         print('\t' + child_context.name)
... 
ChoirStaff
	ChoirStaff
	ChordNames
	DrumStaff
	FiguredBass
	GrandStaff
	Lyrics
	PianoStaff
	RhythmicStaff
	Staff
	StaffGroup
ChordNames
CueVoice
Devnull
DrumStaff
	CueVoice
	DrumVoice
	NullVoice
DrumVoice
Dynamics
FiguredBass
FretBoards
Global
	Score
GrandStaff
	ChordNames
	DrumStaff
	Dynamics
	FiguredBass
	Lyrics
	RhythmicStaff
	Staff
	TabStaff
GregorianTranscriptionStaff
	CueVoice
	GregorianTranscriptionVoice
	NullVoice
GregorianTranscriptionVoice
KievanStaff
	CueVoice
	KievanVoice
	NullVoice
KievanVoice
Lyrics
MensuralStaff
	CueVoice
	MensuralVoice
	NullVoice
MensuralVoice
NoteNames
NullVoice
PetrucciStaff
	CueVoice
	NullVoice
	PetrucciVoice
PetrucciVoice
PianoStaff
	ChordNames
	DrumStaff
	Dynamics
	FiguredBass
	Lyrics
	RhythmicStaff
	Staff
	TabStaff
RhythmicStaff
	CueVoice
	NullVoice
	Voice
Score
	ChoirStaff
	ChordNames
	Devnull
	DrumStaff
	FiguredBass
	FretBoards
	GrandStaff
	GregorianTranscriptionStaff
	KievanStaff
	Lyrics
	MensuralStaff
	NoteNames
	PetrucciStaff
	PianoStaff
	RhythmicStaff
	Staff
	StaffGroup
	TabStaff
	VaticanaStaff
Staff
	CueVoice
	NullVoice
	Voice
StaffGroup
	ChoirStaff
	ChordNames
	DrumStaff
	FiguredBass
	FretBoards
	GrandStaff
	Lyrics
	PianoStaff
	RhythmicStaff
	Staff
	StaffGroup
	TabStaff
TabStaff
	CueVoice
	NullVoice
	TabVoice
TabVoice
VaticanaStaff
	CueVoice
	NullVoice
	VaticanaVoice
VaticanaVoice
Voice

23.3. Populating the graph

>>> context_graph = graphtools.GraphvizGraph(name='All Contexts')
>>> global_subgraph = graphtools.GraphvizSubgraph(name='Global Contexts')
>>> score_subgraph = graphtools.GraphvizSubgraph(name='Score Contexts')
>>> staff_group_subgraph = graphtools.GraphvizSubgraph(name='StaffGroup Contexts')
>>> staff_subgraph = graphtools.GraphvizSubgraph(name='Staff Contexts')
>>> bottom_subgraph = graphtools.GraphvizSubgraph(name='Bottom Contexts')
>>> context_graph.extend([
...     global_subgraph,
...     score_subgraph,
...     staff_group_subgraph,
...     staff_subgraph,
...     bottom_subgraph,
...     ])
>>> context_mapping = {}
>>> for context in lilypondnametools.LilyPondContext.list_all_contexts():
...     node = graphtools.GraphvizNode(
...         name=context.name,
...         attributes={'label': context.name},
...         )
...     context_mapping[context] = node
... 
>>> for context, node in context_mapping.items():
...     if context.is_global_context:
...         global_subgraph.append(node)
...     elif context.is_score_context:
...         score_subgraph.append(node)
...     elif context.is_staff_group_context:
...         staff_group_subgraph.append(node)
...     elif context.is_staff_context:
...         staff_subgraph.append(node)
...     elif context.is_bottom_context:
...         bottom_subgraph.append(node)
... 
>>> for parent_context, parent_node in context_mapping.items():
...     for child_context in parent_context.accepts:
...         child_node = context_mapping[child_context]
...         edge = graphtools.GraphvizEdge()
...         if (
...             parent_context.default_child is not None and
...             child_context is parent_context.default_child
...             ):
...             edge.attributes['color'] = 'black'
...         edge.attach(parent_node, child_node)
... 

23.4. Configuring the graph’s attributes

>>> graph(context_graph)
>>> graph(context_graph, layout='twopi')
>>> context_graph.attributes.update(
...     output_order='edgesfirst',
...     overlap='prism',
...     root='Global',
...     splines='spline',
...     )
>>> graph(context_graph)
>>> graph(context_graph, layout='twopi')
>>> context_graph.attributes.update(
...     bgcolor='transparent',
...     color='lightslategrey',
...     penwidth=2,
...     style=('dotted', 'rounded'),
...     truecolor=True,
...     )
>>> graph(context_graph)
>>> context_graph.edge_attributes.update(
...     color='lightsteelblue2',
...     penwidth=2,
...     )
>>> graph(context_graph)
>>> context_graph.node_attributes.update(
...     fontname='Arial',
...     fontsize=12,
...     penwidth=2,
...     shape='box',
...     style=('bold', 'filled', 'rounded'),
...     )
>>> graph(context_graph)
>>> context_graph.node_attributes['colorscheme'] = 'pastel19'
>>> for i, node in enumerate(context_mapping.values()):
...     fillcolor = i % 9 + 1
...     node.attributes['fillcolor'] = fillcolor
... 
>>> graph(context_graph)
>>> for node in context_mapping.values():
...     label = node.attributes['label']
...     words = stringtools.delimit_words(label)
...     node.attributes['label'] = r'\n'.join(words)
... 
>>> graph(context_graph)
>>> graph(context_graph, layout='twopi')

23.5. Putting it all together

def create_context_graph():
    # Create context graph with subgraphs and styling.
    context_graph = graphtools.GraphvizGraph(
        name='All Contexts',
        children=[
            graphtools.GraphvizSubgraph(name='Global Contexts'),
            graphtools.GraphvizSubgraph(name='Score Contexts'),
            graphtools.GraphvizSubgraph(name='StaffGroup Contexts'),
            graphtools.GraphvizSubgraph(name='Staff Contexts'),
            graphtools.GraphvizSubgraph(name='Bottom Contexts'),
            ],
        attributes=dict(
            bgcolor='transparent',
            color='lightslategrey',
            output_order='edgesfirst',
            overlap='prism',
            penwidth=2,
            root='Global',
            splines='spline',
            style=('dotted', 'rounded'),
            truecolor=True,
            ),
        edge_attributes=dict(
            color='lightsteelblue2',
            penwidth=2,
            ),
        node_attributes=dict(
            colorscheme='pastel19',
            fontname='Arial',
            fontsize=12,
            penwidth=2,
            shape='box',
            style=('bold', 'filled', 'rounded'),
            ),
        )
    # Build context mapping.
    context_mapping = {}
    for i, context in enumerate(lilypondnametools.LilyPondContext.list_all_contexts()):
        name = context.name
        fillcolor = i % 9 + 1
        label = r'\n'.join(stringtools.delimit_words(name))
        node_attributes = {'label': label}
        node = graphtools.GraphvizNode(
            name=context.name,
            attributes=dict(fillcolor=fillcolor, label=label),
            )
        context_mapping[context] = node
        # Add context nodes to subgraphs.
        if context.is_global_context:
            context_graph['Global Contexts'].append(node)
        elif context.is_score_context:
            context_graph['Score Contexts'].append(node)
        elif context.is_staff_group_context:
            context_graph['StaffGroup Contexts'].append(node)
        elif context.is_staff_context:
            context_graph['Staff Contexts'].append(node)
        elif context.is_bottom_context:
            context_graph['Bottom Contexts'].append(node)
    # Attach edges.
    for parent_context, parent_node in context_mapping.items():
        for child_context in parent_context.accepts:
            child_node = context_mapping[child_context]
            edge = graphtools.GraphvizEdge()
            if (
                parent_context.default_child is not None and
                child_context is parent_context.default_child
                ):
                edge.attributes['color'] = 'black'
            edge.attach(parent_node, child_node)
    # All done!
    return context_graph
>>> context_graph = create_context_graph()
>>> graph(context_graph)