DOT Export

Introduction

As a user of Graph for Scala, you may also be interested in visualizing your graphs. Learn about how to transform your graph to the DOT Language. Note that DOT import is not supported at present.

Loosely coupled with the core module, Graph for Scala DOT is supplied as an extra artifact. The core and dot modules need not have the very same version. Please refer to the latest release of graph-dot to ensure compatibility.

The following examples are based on the labeled graph shown on Wikipedia - commutative diagram.

For complete code, please refer to ExportSpec.scala.

Invoke DOT export

To export your graph to a DOT graph, simply call toDOT:

import scalax.collection.io.dot._
val dot = g.toDot(dotRoot, edgeTransformer)

Clearly, dot of type String will contain the resulting DOT graph, but what about the arguments?

Structure and enrich your DOT graph

Fine-grained control over the resulting DOT graph is achieved by means of transformers. Let’s examine the parameters of the toDot method of class Export in detail:

Parameter dotRoot

dotRoot: DotRootGraph
	

This parameter allows to define the DOT graph header and an arbitrary list of root level attributes but no edges or nodes. If you pass

DotRootGraph(directed  = true,
             id        = Some("MyDot"),
             attrStmts = List(DotAttrStmt(Elem.node, List(DotAttr("shape", "record")))),
             attrList  = List(DotAttr("attr_1", """"one""""),
                              DotAttr("attr_2", "<two>")))

for dotRoot your DOT graph will start like

digraph MyDot {
  node [shape = record]
  attr_1 = "one"
  attr_2 = <two>

For completeness, here is a REPLable version of the above:

import scalax.collection.immutable.Graph
import scalax.collection.edges._
import scalax.collection.io.dot._
import implicits._

val root = DotRootGraph (
    directed = true,
    id        = Some("MyDot"),
    attrStmts = List(DotAttrStmt(Elem.node, List(DotAttr("shape", "record")))),
    attrList  = List(DotAttr("attr_1", """"one""""),
                     DotAttr("attr_2", "<two>")))
Graph.empty[Int,DiEdge[Int]].toDot(root, _ => None)

Parameter edgeTransformer

edgeTransformer: Graph[N,E]#EdgeT => Option[(DotGraph, DotEdgeStmt)]
	

edgeTransformer, the only required transformer, will be called for each edge of the graph unless you work with hEdgeTransformer. edgeTransformer will get passed an inner edge and must return either Some tuple of (DotGraph, DotEdgeStmt) or None.

The first member of the tuple of type DotGraph must be either

  • the DotRootGraph instance passed as the dotRoot argument to toDot or
  • any other value of type DotSubGraph. This way you may assign edges to a DOT subgraph dynamically. Further, controlled by the ancestor member of DotSubGraph, you are free to define any tree structure of DOT subgraphs.

The edge attributes to be included in the DOT graph are determined by the second member of the tuple of DotEdgeStmt.

To exclude specific edges from the DOT graph, return None.

Given the edge transformer function

import scalax.collection.immutable.Graph
import wikipedia._ // typed graph `ExampleGraph` defined in ExportSpec
import scalax.collection.io.dot._
import implicits._

val g = ExampleGraph("A1" ~> "A2" :+ "f")
val root = DotRootGraph(directed = true,
                        id       = Some("Wikipedia_Example"))
def edgeTransformer(innerEdge: ExampleGraph#EdgeT): Option[(DotGraph, DotEdgeStmt)] = {
  val edge  = innerEdge.outer
  val label = edge.label
  Some(
    root,
    DotEdgeStmt(
      NodeId(edge.source),
      NodeId(edge.target),
      if (label.nonEmpty) List(DotAttr(Id("label"), Id(label)))
      else Nil
    )
  )
}
g.toDot(root, edgeTransformer)

the single edge ("A1"~+>"A2")("f") will be transformed to

A1 -> A2 [label = f]

Parameter hEdgeTransformer

hEdgeTransformer: Graph[N,E]#EdgeT => Iterable[(DotGraph, DotEdgeStmt)]
	

hEdgeTransformer comes in handy whenever you need to transform a hypergraph to the DOT language. It breaks down every hyperedge to a sequence of DOT edge statements to overcoming the lack of hyperedge support in the DOT language. See the diHyper test in ExportSpec.scala for a simple example.

Parameter cNodeTransformer

cNodeTransformer:
  Option[ (Graph[N,E]#NodeT) => Option[(DotGraph, DotNodeStmt)]] = None
	

The optional transformer cNodeTransformer, prefix by ‘c’ to denote connected, is called for connected nodes. This transformer may be used, for instance, to assign a list of nodes to a DOT subgraph like

val sub = DotSubGraph(ancestor   = root, 
                      subgraphId = "S1",
                      attrList     = Seq(DotAttr("rank", "same")))
def nodeTransformer(innerNode: ExampleGraph#NodeT): Option[(DotGraph,DotNodeStmt)] =
  Some(sub, DotNodeStmt(innerNode.toString, Nil))
g.toDot(root, edgeTransformer, cNodeTransformer = Some(nodeTransformer))

Once you pass the above nodeTransformer to toDot, all connected nodes will be listed in the DOT subgraph S1. Given root and g from 0, nodeTransformer will produce

subgraph S1 {
  rank = same
  A2
  A1
}

Of course, a DOT subgraph that contains all nodes would be meaningless in real life, so we’ll have to append some program logic to group nodes in the desired subgraphs – as done in ExportSpec.

Parameter iNodeTransformer

iNodeTransformer:
  Option[Graph[N,E]#NodeT => Option[(DotGraph, DotNodeStmt)]] = None
	

The prefix ‘i’ of this second optional transformer denotes isolated. If supplied, it will be called for every isolated node. Unless your graph is known to be connected or you want isolated nodes to be discarded in the DOT graph, it is necessary to provide this transformer in addition to edgeTransformer.

Parameter spacing

spacing: Spacing = DefaultSpacing
	

Finally, the spacing argument allows you to determine the layout of the resulting DOT graph including indentation, separators and line breaks.

While testing the above code examples in REPL you might have noticed that toDot's resulting lines were indented by TABs. The reason for this is that the default argument DefaultSpacing was applied. For more details see Scaladoc.

Work with DOT record shapes

Whenever you make use of DOT records, Record comes in handy. Check out def `Colons in https://www.graphviz.org/doc/info/shapes.html#record are handled correctly`() in ExportSpec for the usage of this tiny grammar.