{- |
Module : Data.GraphViz
Description : GraphViz bindings for Haskell.
Copyright : (c) Matthew Sackman, Ivan Lazar Miljenovic
License : 3-Clause BSD-style
Maintainer : Ivan.Miljenovic@gmail.com
This is the top-level module for the graphviz library. It provides
functions to convert 'Data.Graph.Inductive.Graph.Graph's into the
/Dot/ language used by the /GraphViz/ suite of programs (as well as
a limited ability to perform the reverse operation).
Information about GraphViz and the Dot language can be found at:
Commands for converting graphs to Dot format have two options: one
in which the user specifies whether the graph is directed or
undirected, and a primed version which attempts to automatically
infer if the graph is directed or not. Note that these conversion
functions assume that undirected graphs have every edge being
duplicated (or at least that if there exists an edge from /n1/ to
/n2/, then /n1 <= n2/).
-}
module Data.GraphViz
( -- * Conversion from graphs to /Dot/ format.
graphToDot
, graphToDot'
-- ** Conversion with support for clusters.
, NodeCluster(..)
, clusterGraphToDot
, clusterGraphToDot'
-- * Passing the graph through GraphViz.
-- ** Type aliases for @Node@ and @Edge@ labels.
, AttributeNode
, AttributeEdge
-- ** For normal graphs.
, graphToGraph
, graphToGraph'
, dotizeGraph
, dotizeGraph'
-- ** For clustered graphs.
, clusterGraphToGraph
, clusterGraphToGraph'
, dotizeClusterGraph
, dotizeClusterGraph'
-- * Re-exporting other modules.
, module Data.GraphViz.Types
, module Data.GraphViz.Attributes
, module Data.GraphViz.Commands
) where
import Data.GraphViz.Types
import Data.GraphViz.Types.Clustering
import Data.GraphViz.Attributes
import Data.GraphViz.Commands
import Data.Graph.Inductive.Graph
import qualified Data.Set as Set
import Control.Arrow((&&&))
import Data.Maybe(mapMaybe, fromJust)
import qualified Data.Map as Map
import Control.Parallel.Strategies(rnf)
import System.IO(hGetContents)
import System.IO.Unsafe(unsafePerformIO)
-- -----------------------------------------------------------------------------
-- | Determine if the given graph is undirected.
isUndirected :: (Ord b, Graph g) => g a b -> Bool
isUndirected g = all hasFlip es
where
es = labEdges g
eSet = Set.fromList es
hasFlip e = Set.member (flippedEdge e) eSet
flippedEdge (f,t,l) = (t,f,l)
-- | Determine if the given graph is directed.
isDirected :: (Ord b, Graph g) => g a b -> Bool
isDirected = not . isUndirected
-- -----------------------------------------------------------------------------
-- | Convert a graph to GraphViz's /Dot/ format. The 'Bool' value is
-- 'True' for directed graphs, 'False' otherwise.
graphToDot :: (Graph gr) => Bool -> gr a b -> [GlobalAttributes]
-> (LNode a -> Attributes) -> (LEdge b -> Attributes)
-> DotGraph Node
graphToDot isDir graph gAttributes
= clusterGraphToDot isDir graph gAttributes clustBy cID fmtClust
where
clustBy :: LNode a -> NodeCluster () a
clustBy = N
cID = const Nothing
fmtClust = const []
-- | Convert a graph to GraphViz's /Dot/ format with automatic
-- direction detection.
graphToDot' :: (Ord b, Graph gr) => gr a b -> [GlobalAttributes]
-> (LNode a -> Attributes) -> (LEdge b -> Attributes)
-> DotGraph Node
graphToDot' graph = graphToDot (isDirected graph) graph
-- | Convert a graph to /Dot/ format, using the specified clustering function
-- to group nodes into clusters.
-- Clusters can be nested to arbitrary depth.
-- The 'Bool' argument is 'True' for directed graphs, 'False' otherwise.
clusterGraphToDot :: (Ord c, Graph gr) => Bool -> gr a b
-> [GlobalAttributes] -> (LNode a -> NodeCluster c l)
-> (c -> Maybe GraphID) -> (c -> [GlobalAttributes])
-> (LNode l -> Attributes) -> (LEdge b -> Attributes)
-> DotGraph Node
clusterGraphToDot dirGraph graph gAttrs clusterBy cID fmtCluster fmtNode fmtEdge
= DotGraph { strictGraph = False
, directedGraph = dirGraph
, graphID = Nothing
, graphStatements = stmts
}
where
stmts = DotStmts { attrStmts = gAttrs
, subGraphs = cs
, nodeStmts = ns
, edgeStmts = es
}
(cs, ns) = clustersToNodes clusterBy cID fmtCluster fmtNode graph
es = mapMaybe mkDotEdge . labEdges $ graph
mkDotEdge e@(f,t,_) = if dirGraph || f <= t
then Just DotEdge { edgeFromNodeID = f
, edgeToNodeID = t
, edgeAttributes = fmtEdge e
, directedEdge = dirGraph
}
else Nothing
-- | Convert a graph to /Dot/ format, using the specified clustering function
-- to group nodes into clusters.
-- Clusters can be nested to arbitrary depth.
-- Graph direction is automatically inferred.
clusterGraphToDot' :: (Ord c, Ord b, Graph gr) => gr a b
-> [GlobalAttributes] -> (LNode a -> NodeCluster c l)
-> (c -> Maybe GraphID) -> (c -> [GlobalAttributes])
-> (LNode l -> Attributes) -> (LEdge b -> Attributes)
-> DotGraph Node
clusterGraphToDot' gr = clusterGraphToDot (isDirected gr) gr
-- -----------------------------------------------------------------------------
type AttributeNode a = (Attributes, a)
type AttributeEdge b = (Attributes, b)
-- | Run the appropriate GraphViz command on the graph to get
-- positional information and then combine that information back
-- into the original graph. Note that for the edge information to
-- be parsed properly when using multiple edges, each edge between
-- two nodes needs to have a unique label.
--
-- The 'Bool' argument is 'True' for directed graphs, 'False'
-- otherwise. Directed graphs are passed through /dot/, and
-- undirected graphs through /neato/.
graphToGraph :: (Graph gr) => Bool -> gr a b -> [GlobalAttributes]
-> (LNode a -> Attributes) -> (LEdge b -> Attributes)
-> IO (gr (AttributeNode a) (AttributeEdge b))
graphToGraph isDir gr gAttributes fmtNode fmtEdge
= dotAttributes isDir gr dot
where
dot = graphToDot isDir gr gAttributes fmtNode fmtEdge
dotAttributes :: (Graph gr) => Bool -> gr a b -> DotGraph Node
-> IO (gr (AttributeNode a) (AttributeEdge b))
dotAttributes isDir gr dot
= do (Just output) <- graphvizWithHandle command dot DotOutput hToString
return $ rebuildGraphWithAttributes output
where
command = if isDir then dirCommand else undirCommand
hToString h = do s <- hGetContents h
rnf s `seq` return s
rebuildGraphWithAttributes dotResult = mkGraph lnodes ledges
where
lnodes = map (\(n, l) -> (n, (nodeMap Map.! n, l)))
$ labNodes gr
ledges = map createEdges $ labEdges gr
createEdges (f, t, l) = if isDir || f <= t
then (f, t, getLabel (f,t))
else (f, t, getLabel (t,f))
where
getLabel c = (fromJust $ Map.lookup c edgeMap, l)
g' = parseDotGraph dotResult
ns = graphNodes g'
es = graphEdges g'
nodeMap = Map.fromList $ map (nodeID &&& nodeAttributes) ns
edgeMap = Map.fromList $ map ( (edgeFromNodeID &&& edgeToNodeID)
&&& edgeAttributes) es
-- | Run the appropriate GraphViz command on the graph to get
-- positional information and then combine that information back
-- into the original graph.
--
-- Graph direction is automatically inferred.
graphToGraph' :: (Ord b, Graph gr) => gr a b -> [GlobalAttributes]
-> (LNode a -> Attributes) -> (LEdge b -> Attributes)
-> IO (gr (AttributeNode a) (AttributeEdge b))
graphToGraph' gr = graphToGraph (isDirected gr) gr
-- | Run the appropriate GraphViz command on the clustered graph to
-- get positional information and then combine that information back
-- into the original graph. Note that for the edge information to
-- be parsed properly when using multiple edges, each edge between
-- two nodes needs to have a unique label.
--
-- The 'Bool' argument is 'True' for directed graphs, 'False'
-- otherwise. Directed graphs are passed through /dot/, and
-- undirected graphs through /neato/.
clusterGraphToGraph :: (Ord c, Graph gr) => Bool -> gr a b
-> [GlobalAttributes] -> (LNode a -> NodeCluster c l)
-> (c -> Maybe GraphID) -> (c -> [GlobalAttributes])
-> (LNode l -> Attributes) -> (LEdge b -> Attributes)
-> IO (gr (AttributeNode a) (AttributeEdge b))
clusterGraphToGraph isDir gr gAtts clBy cID fmtClust fmtNode fmtEdge
= dotAttributes isDir gr dot
where
dot = clusterGraphToDot isDir gr gAtts clBy cID fmtClust fmtNode fmtEdge
-- | Run the appropriate GraphViz command on the clustered graph to
-- get positional information and then combine that information back
-- into the original graph.
--
-- Graph direction is automatically inferred.
clusterGraphToGraph' :: (Ord b, Ord c, Graph gr) => gr a b
-> [GlobalAttributes] -> (LNode a -> NodeCluster c l)
-> (c -> Maybe GraphID) -> (c -> [GlobalAttributes])
-> (LNode l -> Attributes) -> (LEdge b -> Attributes)
-> IO (gr (AttributeNode a) (AttributeEdge b))
clusterGraphToGraph' gr = clusterGraphToGraph (isDirected gr) gr
-- | Pass the graph through 'graphToGraph' with no 'Attribute's. This
-- is an @'IO'@ action, however since the state doesn't change it's
-- safe to use 'unsafePerformIO' to convert this to a normal
-- function.
--
-- The 'Bool' argument is 'True' for directed graphs, 'False'
-- otherwise. Directed graphs are passed through /dot/, and
-- undirected graphs through /neato/.
dotizeGraph :: (Graph gr) => Bool -> gr a b
-> gr (AttributeNode a) (AttributeEdge b)
dotizeGraph isDir g = unsafePerformIO
$ graphToGraph isDir g gAttrs noAttrs noAttrs
where
gAttrs = []
noAttrs = const []
-- | Pass the graph through 'graphToGraph' with no 'Attribute's. This
-- is an @'IO'@ action, however since the state doesn't change it's
-- safe to use 'unsafePerformIO' to convert this to a normal
-- function.
--
-- The graph direction is automatically inferred.
dotizeGraph' :: (Graph gr, Ord b) => gr a b
-> gr (AttributeNode a) (AttributeEdge b)
dotizeGraph' g = dotizeGraph (isDirected g) g
-- | Pass the clustered graph through 'clusterGraphToGraph' with no
-- 'Attribute's. This is an @'IO'@ action, however since the state
-- doesn't change it's safe to use 'unsafePerformIO' to convert this
-- to a normal function.
--
-- The 'Bool' argument is 'True' for directed graphs, 'False'
-- otherwise. Directed graphs are passed through /dot/, and
-- undirected graphs through /neato/.
dotizeClusterGraph :: (Ord c, Graph gr) => Bool -> gr a b
-> (LNode a -> NodeCluster c l)
-> gr (AttributeNode a) (AttributeEdge b)
dotizeClusterGraph isDir g clustBy = unsafePerformIO
$ clusterGraphToGraph isDir g
gAttrs clustBy
cID cAttrs
noAttrs noAttrs
where
gAttrs = []
cID = const Nothing
cAttrs = const gAttrs
noAttrs = const []
-- | Pass the clustered graph through 'graphToGraph' with no
-- 'Attribute's. This is an @'IO'@ action, however since the state
-- doesn't change it's safe to use 'unsafePerformIO' to convert this
-- to a normal function.
--
-- The graph direction is automatically inferred.
dotizeClusterGraph' :: (Ord b, Ord c, Graph gr) => gr a b
-> (LNode a -> NodeCluster c l)
-> gr (AttributeNode a) (AttributeEdge b)
dotizeClusterGraph' g = dotizeClusterGraph (isDirected g) g