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.
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?
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:
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)
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
DotRootGraph
instance passed as the dotRoot
argument to toDot
or
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]
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.
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
.
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
.
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.
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.