Creating edges in Mondrian

Mondrian allows you to create graphical graph scenes through a builder API. While creating nodes is as simple as providing a list of items, creating and controlling edges is a bit more involved.

Let's consider an example of drawing the hierarchy of Collection Object subclass: #Collection instanceVariableNames: '' classVariableNames: '' package: 'Collections-Abstract-Base' . The nodes are the list of all classes. The edges are constructed by using the reference from every class to its superclass.

definingEdgesWithConnectItemsFromToBlock
	<gtExample>
	| m |
	m := GtMondrian new.
	m nodes with: Collection withAllSubclasses.
	m edges
		connect: Collection withAllSubclasses
		from: [ :aClass | aClass superclass ]
		to: [ :aClass | aClass ].
	m layout tree.
	^ m
    

To build the edges, we rely on GtMondrianEdgeBuilder>>#connect:from:to: connect: aCollection from: aFromBlock to: aToBlock | elements | elements := self privateConnect: aCollection from: aToBlock to: aFromBlock. self topStep root addChildren: elements. ^ elements which takes as input a collection of the objects and two blocks that are applied on each collection item to identify the from and to nodes. The matching of a node is made by evaluating the block and then finding the node in the graph that has the same model behind the node.

This is a long form of defining edges. In this specific case, the collection of the nodes and the collection we pass to the edges are the same. So, we could just reuse the collection in the edges specification using GtMondrianEdgeBuilder>>#connectFrom:to: connectFrom: aFromBlock to: aToBlock ^ self connect: (self topStep root graph nodeChildren collect: [ :each | each graph model ]) from: aFromBlock to: aToBlock .

definingEdgesWithConnectFromToBlock
	<gtExample>
	| m |
	m := GtMondrian new.
	m nodes with: Collection withAllSubclasses.
	m edges
		connectFrom: [ :aClass | aClass superclass ]
		to: [ :aClass | aClass ].
	m layout tree.
	^ m
    

Looking closer, the to block is also not particularly interesting as it simply returns the input object. So, we can use an even simpler form using GtMondrianEdgeBuilder>>#connectFrom: connectFrom: aFromBlock ^ self connectFrom: aFromBlock to: [ :each | each ] :

definingEdgesWithConnectFromBlock
	<gtExample>
	| m |
	m := GtMondrian new.
	m nodes with: Collection withAllSubclasses.
	m edges connectFrom: [ :aClass | aClass superclass ].
	m layout tree.
	^ m
    

In the previous examples, we built the tree by creating the edges by defining for each class a connection starting from its superclass. We can also create the edges the other way around: for each class to specify the connections to all its subclasses.

definingEdgesWithConnectFromToAllBlock
	<gtExample>
	| m |
	m := GtMondrian new.
	m nodes with: Collection withAllSubclasses.
	m edges
		connect: Collection withAllSubclasses
		from: [ :aClass | aClass ]
		toAll: [ :aClass | aClass subclasses ].
	m layout tree.
	^ m
    

The result is the same, but we are now relying on GtMondrianEdgeBuilder>>#connect:from:toAll: connect: aCollection from: aFromBlock toAll: aToAllBlock " ^ aCollection flatCollect: [ :each | self connect: (aToAllBlock value: each) from: [ :x | aFromBlock value: each ] to: [ :x | x ] ]" "connect: aCollection fromAll: aFromAllBlock to: aToBlock" | elements | elements := aCollection flatCollect: [ :each | (aToAllBlock value: each) flatCollect: [ :to | self connect: {each} from: [ :x | aFromBlock value: each ] to: [ :x | to ] ] ]. "self topStep root addChildren: elements." ^ elements . The difference here is that the toAll: block must return a collection of objects and not just a single object as is the case for the to:`. There are, of course, variations for the from side as well. Take a look at GtMondrianEdgeBuilder GtMondrianGraphBuilder subclass: #GtMondrianEdgeBuilder instanceVariableNames: 'fromAnchorClass toAnchorClass areEdgesPassive' classVariableNames: '' package: 'GToolkit-Mondrian' .

Creating edges, while simple on the surface, entails multiple steps and spotting the root cause when an edge is not created as expected can be tricky. The good news is that the engine provides hints to help identifying such problems by emitting Beacon signals.

loggingTheEdgesNotCreated
	<gtExample>
	^ MemoryLogger
		reset;
		startFor: GtMondrianEdgeNotCreated;
		runDuring: [ | m |
			m := GtMondrian new.
			m nodes with: Collection withAllSubclasses.
			m edges
				connect: Collection withAllSubclasses
				from: [ :aClass | aClass ]
				to: [ :aClass | aClass subclasses ].
			m layout tree ]
    

To get the signals, simply wrap the execution with a MemoryLogger SignalLogger subclass: #MemoryLogger instanceVariableNames: 'recordings announcer mutex interestingAnnouncements' classVariableNames: 'WriteFilename WritePeriod WriteProcess' package: 'Beacon-Core-Loggers' to collect the GtMondrianEdgeNotCreated BeaconSignal subclass: #GtMondrianEdgeNotCreated instanceVariableNames: 'fromModel toModel fromElement toElement model' classVariableNames: '' package: 'GToolkit-Mondrian' signals. Such a signal offers four slots: - toModel and fromModel provide the objects resulting from executing the blocks - toElement and fromElement offer the detected visual objects from the scene. If they are nil, they were not found and you can go to identify what part of the connection definition does not work.

So, what was the problem in our case? We used GtMondrianEdgeBuilder>>#connect:from:to: connect: aCollection from: aFromBlock to: aToBlock | elements | elements := self privateConnect: aCollection from: aToBlock to: aFromBlock. self topStep root addChildren: elements. ^ elements instead of GtMondrianEdgeBuilder>>#connect:from:toAll: connect: aCollection from: aFromBlock toAll: aToAllBlock " ^ aCollection flatCollect: [ :each | self connect: (aToAllBlock value: each) from: [ :x | aFromBlock value: each ] to: [ :x | x ] ]" "connect: aCollection fromAll: aFromAllBlock to: aToBlock" | elements | elements := aCollection flatCollect: [ :each | (aToAllBlock value: each) flatCollect: [ :to | self connect: {each} from: [ :x | aFromBlock value: each ] to: [ :x | to ] ] ]. "self topStep root addChildren: elements." ^ elements and as a consequence, it could not find any edge.