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
. 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:
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:
.
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:
:
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:
. 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
.
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
to collect the GtMondrianEdgeNotCreated
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:
instead of GtMondrianEdgeBuilder>>#connect:from:toAll:
and as a consequence, it could not find any edge.