DOT Export

Introduction

This document inducts the user of Graph for Scala, who is interested in visualizing graphs, into how to translate Graph instances to the DOT Language. Thus it may be viewed as a supplement of the Core User Guide.

At present DOT import is not supported.

Graph for Scala DOT is supplied as an extra module (jar). As a rule of thumb, Graph-dot_majorX.minorX.patchX depends on Graph-core_majorY.minorY.patchY with equaling major and minor versions. In absence of versions being equal in this sense please refer the actual releases listed on Download.

The following examples are based on a labeled graph shown on wikipedia, more specifically at commutative diagram.

For the complete example please refer to TExport.scala.

Exporting Graphs

To export a graph instance 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 dotRoot and edgeTransformer arguments?

Structuring and Enriching the DOT Graph

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

Parameter dotRoot

dotRoot: DotRootGraph
	

This parameter serves to define the DOT graph header and an arbitrary list of root level attributes but no edges or nodes. Let's supply

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. Then the DOT graph will start with

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

For completeness, a REPLable version of the above example looks like

import scalax.collection.Graph,
       scalax.collection.GraphEdge._
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].toDot(root, _ => None)

Parameter edgeTransformer

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

edgeTransformer, as the only obligatory transformer, will be called for each edge of the graph except for working with hEdgeTransformer. The caller, toDot, passes an inner edge to it and edgeTransformer returns either Some tuple of (DotGraph, DotEdgeStmt) or None.

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

  • the DotRootGraph instance passed as the dotRoot argument to toDot or
  • any other value of type DotSubGraph. This way you may assign the edge to a DOT subgraph dynamically. Also, 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 means of the second member of the tuple of type DotEdgeStmt.

It is also possible to return None for specific edges in order to exclude them from the DOT graph.

Given the following edge transformer function edgeTransformer

import scalax.collection.Graph
import scalax.collection.edge.LDiEdge, scalax.collection.edge.Implicits._
import scalax.collection.io.dot._
import implicits._

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

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

  A1 -> A2 [label = f]

Parameter hEdgeTransformer

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

hEdgeTransformer comes in handy when transforming a hypergraph to the DOT language. hEdgeTransformer allows to break down each hyperedge to a sequence of DOT edge statements and thus to overcome the lack of support for hyperedges in the DOT language. See the diHyper test in TExport.scala for a simple example.

Parameter cNodeTransformer

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

The optional transformer argument cNodeTransformer, with its prefix ‘c’ denoting connected, is called for connected nodes. This transformer may be used, for example, 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: Graph[String,LDiEdge]#NodeT):
    Option[(DotGraph,DotNodeStmt)] =
  Some(sub, DotNodeStmt(innerNode.toString, Nil))
g.toDot(root, edgeTransformer, Some(nodeTransformer))

Once the above nodeTransformer is passed to toDot for its cNodeTransformer argument, 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 TExport.scala.

Parameter iNodeTransformer

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

The prefix ‘i’ of this second optional transformer argument denotes isolated. If supplied, iNodeTransformer is called for each isolated node. Unless your graph is guaranteed 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 we did not supply an argument different from DefaultSpacing. For more details see the API Scaladoc.

Working with DOT record shapes

When working with DOT records you may bank on the Record object. For the usage of this little grammar see def `Colons (':') in node_id's are handled correctly` in TExport.scala.