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
and GtMondrianEdgeBuilder
.
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
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.