Drawing graphs with Mondrian by example

TL;DR

Mondrian is a builder for graph-based visualizations that supports a fluent API. This tutorial will show you how to create graphs, control their layout, and define interactions.

Nodes, edges and layouts

The most basic parts of a Mondrian view are:

Nodes

Edges

Layout

The things we feed Nodes with are called Models

Let's see how we create these. First the nodes.

multipleNodes
	<gtExample>
	| view |
	view := self emptyView.
	view nodes with: {1 . 2 . 3 . 4}.
	self assert: view topStep root children size equals: 4.
	^ view
    

Given those nodes, we can build edges.

edgesBetweenNodes
	<gtExample>
	| view |
	view := self multipleNodes.
	view edges connect: {1 . 2 . 3 . 4} from: [ :x | x // 2 ] to: [ :x | x ].
	self assert: view topStep root graph edgeChildren size equals: 3.
	^ view
    

And finally, we lay them out.

layoutOnEdgesBetweenNodes
	<gtExample>
	| view |
	view := self edgesBetweenNodes.
	view layout tree.
	self assert: (view root layout isKindOf: BlOnceLayout).
	self assert: (view root layout layout notNil).
	^ view
    

Stencils

The look and feel of nodes and edges is specified by stencils, which can be included in the construction of nodes and edges.

A stencil can be defined by a block that takes the node's model object as its argument and returns an instance of a Bloc element. In the example below, we see nine nodes drawn as circles with black borders, whose sizes depend on the number which each node represents.

nodesWithEllipses
	<gtExample>
	| view |
	view := GtMondrian new.
	view nodes
		stencil: [ :x | 
			BlElement new 
				border: (BlBorder paint: Color black);
				geometry: BlEllipseGeometry new; 
				size: (x * 2) @ (x * 2) ];
		with: (1 to: 9).
	self assert: view topStep root graph nodeChildren size = 9.
	^ view
    

We can define the stencil of edges in a similar way. In the example below, the nodes are connected by blue edges whose width varies depending on the node it tries to connect to.

edgesWithThickerLines
	<gtExample>
	| view |
	view := self nodesWithEllipses.
	view edges
		stencil: [ :x | BlLineElement new border: (BlBorder paint: (Color blue alpha: 0.5) width: x) ];
		connectFrom: [ :x | x // 2 ].
	view topStep root graph edgeChildren do: [ :edgeChild | 
			self assert: edgeChild border width = edgeChild graph connectedNodes asArray last graph model ].
	view layout tree.
	^ view
    

In the previous example, the stencil: block shows one input argument x for the object representing the model behind the node the edge should connect to.

We know that because the connection is specified with connectFrom:.

But how can we make it depend on the other side of the edge?

Luckily, the stencil: block can actually receive arguments with the fromElement and the toElement.

Here is an example in which the thickness of the edge depends on the element the edge connects from:

edgesWithThickerLinesDependingOnTheFromNode
	<gtExample>
	| view |
	view := self nodesWithEllipses.
	view edges
		stencil: [ :aNumber :fromElement :toElement | 
			BlLineElement new
				border: (BlBorder paint: (Color blue alpha: 0.5) width: fromElement graph model) ];
		connectFrom: [ :x | x // 2 ].
	view topStep root graph edgeChildren
		do: [ :edgeChild | self assert: edgeChild border width = edgeChild graph connectedNodes asArray first graph model ].
	view layout tree.
	^ view
    

Find other edge creation method in GtMondrian Object subclass: #GtMondrian instanceVariableNames: 'stack' classVariableNames: '' package: 'GToolkit-Mondrian' and GtMondrianEdgeBuilder GtMondrianGraphBuilder subclass: #GtMondrianEdgeBuilder instanceVariableNames: 'fromAnchorClass toAnchorClass areEdgesPassive' classVariableNames: '' package: 'GToolkit-Mondrian' .

Nesting

Mondrian graphs can also be nested. In other words, a node can have a subgraph.

nestedNodes
	<gtExample>
	| view |
	view := self emptyView.
	view nodes
		shape: [ :x | 
			BlElement new  
				background: Color paleOrange;
				constraintsDo: [ :c | c margin: (BlInsets all: 10) ] ];
		with: (1 to: 9)
		forEach: [ :each |
			view nodes 
				shape: [ :x | BlTextElement new text: x asString asRopedText ];
				with: (10 * each to: (10 * each + 4)).
			view layout circle radius: 20 ].
	self assert: view root children size = 9.
	view root children do: [:child | self assert: child children size = 5 ].
	^ view
    

Nested nodes behave like leaf nodes in that they can be connected through edges and laid out accordingly.

nestedWithEdges
	<gtExample>
	| view |
	view := self nestedNodes.
	view edges 
		fromCenterBottom;
		toCenterTop;
		connectFrom: [ :x | x // 2 ].
	view layout tree.
	self assert: (view root children select: [ :each | each graph isEdge ]) size = 8.
	^ view
    

Handling Interaction

Mondrian works directly with Bloc elements. This allows us to utilize all abilities of an element, including dealing with interaction. Here is an example of highlighting incoming and outgoing edges of a node when we hover with the mouse over the corresponding element.

edgesWithChangingColorsOnHover
	<gtExample>
	| m |
	m := GtMondrian new.
	m nodes
		stencil: [ :x | 
			BlElement new
				border: (BlBorder paint: Color black);
				geometry: BlEllipseGeometry new;
				size: x * 2 @ (x * 2);
				when: BlMouseEnterEvent do: [ :anEvent | 
					anEvent currentTarget graph connectedEdges do: [ :inner | 
							inner element border: (BlBorder paint: (Color red alpha: 0.5)) ] ];
				when: BlMouseLeaveEvent do: [ :anEvent | 
					anEvent currentTarget graph connectedEdges do: [ :inner | 
							inner element border: (BlBorder paint: (Color blue alpha: 0.5)) ] ] ];
		with: (1 to: 9).
	m edges
		stencil: [ :x | 
			BlLineElement new
				zIndex: -1;
				border: (BlBorder paint: (Color blue alpha: 0.5)) ];
		connectFrom: [ :x | x // 2 ].
	m layout tree.
	^ m
    

Differences to previous incarnations of Mondrian

The API has changed in comparison with the Roassal version because of the constraints and opportunities offered by Bloc:

Nodes are defined through view nodes with: { ... }, instead of view nodes: { ... }. One reason is to make the definition more similar to the one of edges.

Stencils are defined within the scope of a view nodes or view edges definition.

Stencils are mainly defined explicitly by instantiating of BlElement Object subclass: #BlElement uses: TBlTransformable + TBlEventTarget + TBlDebug instanceVariableNames: 'spaceReference parent children bounds measuredBounds boundsCache eventDispatcher constraints layout transformation taskQueue errorHandler userData visuals flags' classVariableNames: '' package: 'Bloc-Core' and its subclasses.

The forEach: clause can be cascaded after view nodes with: {...}. This allows us to have multiple forEach: statements per one nodes definition.

The new forEach:in: allows the user to define the id of the container for the children. In this way, we can define children in different parts of the parent node.