During the past week, Conrad Parker announced on Google+ (though it wasn’t a public post so I can’t seem to link to it) that he had decided to take over maintainership (at least until someone else says they want to do it) of vacuum since Matt Morrow hasn’t been seen for a while.
I decided to take the opportunity to replace the current explicit String
-based (well, actually Doc
-based) mangling used to create Dot graphs for use with Graphviz from vacuum with usage of my graphviz library. I’ve just sent Conrad a pull request on his GitHub repo, and I decided that this would make a suitable “intro” tutorial on how to use graphviz
.
First of all, have a look at the current implementation of the GHC.Vacuum.Pretty.Dot
module. If you read through it, it’s pretty straight-forward: convert a graph in an adjacency-list format into Dot code by mapping a transformation function over it, then attach the required header and footer.
Note though that this way, the layout/printing aspects are mixed in with the actual conversion part: rather than separating the creation of the Dot code from how it actually appears, it’s all done together.
There’s also a mistake in there that probably isn’t obvious: the “arrowname=onormal
” part of the definition of gStyle
is completely useless: there is no such attribute as “arrowname”; what is probably meant there is “arrowhead” or “arrowtail”.
Let’s now consider the implementation using version 2999.12.*
of graphviz
(the version is important because even whilst doing this I spotted some changes I’m likely to make in the next version for usability purposes):
{-# LANGUAGE OverloadedStrings #-} module GHC.Vacuum.Pretty.Dot ( graphToDot ,graphToDotParams ,vacuumParams ) where import Data.GraphViz hiding (graphToDot) import Data.GraphViz.Attributes.Complete( Attribute(RankDir, Splines, FontName) , RankDir(FromLeft), EdgeType(SplineEdges)) import Control.Arrow(second) ------------------------------------------------ graphToDot :: (Ord a) => [(a, [a])] -> DotGraph a graphToDot = graphToDotParams vacuumParams graphToDotParams :: (Ord a, Ord cl) => GraphvizParams a () () cl l -> [(a, [a])] -> DotGraph a graphToDotParams params nes = graphElemsToDot params ns es where ns = map (second $ const ()) nes es = concatMap mkEs nes mkEs (f,ts) = map (\t -> (f,t,())) ts ------------------------------------------------ vacuumParams :: GraphvizParams a () () () () vacuumParams = defaultParams { globalAttributes = gStyle } gStyle :: [GlobalAttributes] gStyle = [ GraphAttrs [RankDir FromLeft, Splines SplineEdges, FontName "courier"] , NodeAttrs [textLabel "\\N", shape PlainText, fontColor Blue] , EdgeAttrs [color Black, style dotted] ]
(The OverloadedStrings
extension is needed for the FontName
attribute.)
First of all, note that there is no mention or concept of the overall printing/structure of the Dot code: this is all done behind the scenes. It’s also simpler this way to choose custom attributes: Don Stewart’s vacuum-cairo package ends up copying all of this and extra functions from vacuum just to have different attributes; here, you merely need to provide a custom GraphvizParams
value!
So let’s have a look more at what’s being done here. In graphToDotParams
, the provided adjacency list representation [(a,[a])]
is converted to explicit node and edge lists; the addition of ()
to each node/edge is because in many cases you would have some additional label attached to each node/edge, but for vacuum we don’t. There is a slight possible error in this, in that there may be nodes present in an edge list but not specified directly (e.g. [(1,[2])]
doesn’t specify the “2” node). However, Graphviz doesn’t require explicit listing of every node if it’s also present in an edge, and we’re not specifying custom attributes for each node, so it doesn’t matter. The actual grunt work of converting these node and edge lists is then done by graphElemsToDot
in graphviz.
The type signature of graphToDotParams
has been left loose enough so that if someone wants to specify clusters, it is possible to do so. However, by default, graphToDot
uses the specified vacuumParams
which have no clusters, no specific attributes for each node or edge but does have top-level global attributes. Rather than using String
s, we have a list of GlobalAttributes
, with one entry for each of top-level graph, node and edge attributes (the latter two applying to every node/edge respectively). I’ve just converted over the attributes specified in the original (though dropping off the useless “arrowname” one). Some of these attributes have more user-friendly wrappers that are re-exported by Data.GraphViz
; the other three need to be explicitly imported from the complete list of attributes (for these cases I prefer to do explicit named imports rather than importing the entire module so I know which actual attributes I’m using). I am adding more attributes to the “user-friendly” module all the time; RankDir
will probably make it’s way over there for the next release, with a better name and documentation (and thus not requiring any more imports).
Now, you might be wondering how I’ve managed to avoid a (a -> String)
or similar function like the original implementation had. That’s because the actual conversion uses the PrintDot class (which is going to have a nicer export location in the next version of graphviz). As such, as long as a type has an instance – and ones like String
, Int
, etc. all do – then it will be printed when the actual Dot code is created from the DotGraph a
value.
So how to actually use this? In the original source, there’s a commented out function to produce a png
image file. This is achieved by saving the Dot code to a file, then explicitly calling the dot
command and saving the output as an image. Here’s the version using graphviz
:
{-# LANGUAGE ScopedTypeVariables #-} import GHC.Vacuum.Pretty.Dot import Data.GraphViz.Exception graphToDotPng :: FilePath -> [(String,[String])] -> IO Bool graphToDotPng fpre g = handle (\(e::GraphvizException) -> return False) $ addExtension (runGraphviz (graphToDot g)) Png fpre >> return True
(Note: The exception-handling stuff is just used to provide the same IO Bool
result as the original.)
I hope you’ve seen how convenient graphviz can be rather than manually trying to produce Dot code and calling the Graphviz tools to visualise it. There are still some cludgy spots in the API (e.g. I would be more tempted now to have the graph to visualise be the last parameter; at the time I was considering more about using different image outputs for the same graph), so I appreciate people telling me how the API can be improved (including which attributes are commonly used).