Skip to content

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:

domain
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:

substance
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.

domain
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:

style
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:

style
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:

style
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.

style
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:

substance
-- 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!

Released under the MIT License.