Tailoring Penrose domains to your needs
Or, a tour of a simple domain for directed graphs and how to use Penrose's Domain and Style languages to extend it.
To facilitate creating diagrams quickly, Penrose has many existing Domain and Style programs, which you may want to tailor to fit your own diagramming needs. We walk through an example of such modification by extending the simple-directed-graph
domain to support highlighting specific vertices and arcs, as seen in the difference between the diagrams below.
Directed graph domain: just vertices and arcs
The simplest Domain schema that describes a directed graph, composed of vertices and directed edges (or, arcs) can be expressed in the following manner:
type Vertex
predicate Arc(Vertex a, Vertex b)
Given a Vertex
type and an Arc
predicate, we can now create vertices and draw arcs between them in the Substance language. Let's write a Substance program in this domain; one, for example, depicting an intricate Hamiltonian graph (which is a graph that contains a cycle that hits each vertex exactly once before returning to the start node). The Substance code could be written as follows:
Vertex a, b, c, d, e, f, g
-- Hamiltonian cycle
Arc(a, b)
Arc(b, c)
Arc(c, d)
Arc(d, e)
Arc(e, f)
Arc(f, g)
Arc(g, a)
-- Additional edges that are not in the Hamiltonian cycle
Arc(a, c)
Arc(a, d)
Arc(b, e)
Arc(b, f)
Arc(c, g)
Arc(d, f)
Here's what the resulting diagram should look like:
Extending the Domain and understanding the Style
In the graph shown above, it's rather unclear where exactly the Hamiltonian cycle is, so we might want to highlight its path. This would mean highlighting certain vertices and arcs. Let's therefore add predicates to the Domain which allow us to do so.
type Vertex
predicate Arc(Vertex a, Vertex b)
predicate HighlightVertex(Vertex a)
predicate HighlightArc(Vertex a, Vertex b)
This addition defines two new predicates which take existing vertices and (vertices which form) arcs and highlights them. Now we need to describe how to display this relation in a diagram – in this case, highlighting would involve changing the color of the vertices and nodes. We can inspect the Style program to identify which parts we need to add on to or otherwise modify. The following excerpt is from the simple-directed-graph
Style program:
forall Vertex v {
v.dot = Circle { // [!code focus]
center: (? in dots, ? in dots)
r: num.radius
fillColor : color.black // [!code focus]
} // [!code focus]
v.text = Text { // [!code focus]
string: v.label
fillColor: color.black // [!code focus]
fontFamily: "serif"
fontSize: "18px"
strokeColor: color.white
strokeWidth: 4
paintOrder: "stroke"
} // [!code focus]
v.halfSize = (v.text.width / 2, v.text.height / 2)
v.bottomLeft = v.text.center - v.halfSize
v.topRight = v.text.center + v.halfSize
v.text above v.dot
encourage shapeDistance(v.dot, v.text) == num.labelDist in text
}
For a vertex, we need to change the fillColor
of both the actual dot
and the text
.
And the relevant portion of the Style program for arcs is below:
forall Vertex u; Vertex v where Arc(u, v) as e {
a = u.dot.center
b = v.dot.center
t = normalize(b - a) -- tangent
n = rot90(t) -- normal
m = (a + b) / 2 -- midpoint
e.start = a
e.end = b
e.offset = ? in dots
e.arrow = Path { // [!code focus]
d: quadraticCurveFromPoints("open", [a, m + e.offset * n, b])
strokeColor: color.black // [!code focus]
} // [!code focus]
e.step = ? in arrows
e.pointerCenter = m + (e.offset / 2) * n + e.step * t
p = e.pointerCenter
x = num.pointerX
y = num.pointerY
e.pointer = Path { // [!code focus]
d: pathFromPoints("closed", [p - x * t + y * n, p + x * t, p - x * t - y * n])
strokeColor: none()
fillColor: color.black // [!code focus]
} // [!code focus]
e.arrow below u.dot
e.arrow below v.dot
e.pointer below e.arrow
encourage vdist(u.dot.center, v.dot.center) < num.edgeDist in dots
encourage minimal(sqr(e.offset)) in dots
encourage minimal(sqr(e.step))
}
We need to modify the strokeColor
of the arrow
and the fillColor
of the pointer
.
Modifying the Style
Given this knowledge, we can modify the Style program to fit our needs. First, we may find it useful to define the new color signifying a highlighted vertex or arc, so we add redOrange
to the color
object in the Style program:
color {
black = #000000
white = #ffffff
redOrange = #FE4A49 -- added for highlighting
}
Then, we match on the specific cases of Vertex
and Arc
with predicates HighlightVertex
and HighlightArc
, as shown below.
forall Vertex v where HighlightVertex(v) {
override v.dot.fillColor = color.redOrange
override v.text.fillColor = color.redOrange
}
forall Vertex a, b where Arc(a,b) as e; HighlightArc(a,b) {
override e.arrow.strokeColor = color.redOrange
override e.pointer.fillColor = color.redOrange
}
Where there is a Vertex
and a call to the HighlightVertex
predicate, we override the fillColor
of its components; we perform the corresponding extensions for Arc
and HighlightArc
.
The final outcome!
Now we can add to the Substance program to display the highlighting we wanted:
-- Highlight start/end vertex of cycle
HighlightVertex(a)
-- Highlight all arcs in cycle
HighlightArc(a, b)
HighlightArc(b, c)
HighlightArc(c, d)
HighlightArc(d, e)
HighlightArc(e, f)
HighlightArc(f, g)
HighlightArc(g, a)
Ultimately, the highlighted diagram looks like this:
In this process, we've modified the simple-directed-graph
Domain and Style programs to add new capabilities. You can see this in action in the Hamiltonian graph example in the Penrose gallery. We hope this helps and inspires you to modify the Domain and Style programs of Penrose's existing domains to fit your own diagramming needs!