{- | Module : Data.Graph.Analysis.Visualisation Description : Graphviz wrapper functions Copyright : (c) Ivan Lazar Miljenovic 2009 License : 2-Clause BSD Maintainer : Ivan.Miljenovic@gmail.com Functions to assist in visualising graphs and components of graphs. -} module Data.Graph.Analysis.Visualisation ( -- * Graph visualisation. -- $graphviz graphviz, graphvizClusters, graphvizClusters', assignCluster, noAttributes, -- * Showing node groupings. -- $other showPath, showPath', showCycle, showCycle', showNodes, showNodes', -- * Various printing functions. blockPrint, blockPrint', blockPrintList, blockPrintList', blockPrintWith, blockPrintWith', ) where import Data.Graph.Analysis.Types import Data.Graph.Analysis.Utils import Data.Graph.Inductive.Graph import Data.GraphViz import Data.List(intersperse, unfoldr) -- ----------------------------------------------------------------------------- {- $graphviz Simple wrappers around the Haskell "Data.GraphViz" library to turn 'GraphData's into basic 'DotGraph's for processing by the GraphViz suite of applications. -} -- | Convert the 'GraphData' into 'DotGraph' format with the given -- 'Attribute's. graphviz :: GraphData n e -> [GlobalAttributes] -> (LNode n -> Attributes) -> (LEdge e -> Attributes) -> DotGraph Node graphviz = applyDirAlg graphToDot -- | Convert the clustered 'GraphData' into 'DotGraph' format with the -- given 'Attribute's. Cluster the nodes based upon their -- 'ClusterLabel' clusters. graphvizClusters :: (ClusterLabel cl) => GraphData cl e -> [GlobalAttributes] -> (Cluster cl -> [GlobalAttributes]) -> (LNode (NodeLabel cl) -> Attributes) -> (LEdge e -> Attributes) -> DotGraph Node graphvizClusters g gas = graphvizClusters' g gas assignCluster clusterID -- | Convert the 'GraphData' into a clustered 'DotGraph' format using -- the given clustering function and with the given 'Attribute's. graphvizClusters' :: (Ord c) => GraphData n e -> [GlobalAttributes] -> (LNode n -> NodeCluster c l) -> (c -> Maybe GraphID) -> (c -> [GlobalAttributes]) -> (LNode l -> Attributes) -> (LEdge e -> Attributes) -> DotGraph Node graphvizClusters' = applyDirAlg clusterGraphToDot -- | A function to convert an 'LNode' to the required 'NodeCluster' -- for use with the GraphViz library. assignCluster :: (ClusterLabel cl) => LNode cl -> NodeCluster (Cluster cl) (NodeLabel cl) assignCluster (n,a) = C (cluster a) $ N (n, nodeLabel a) -- | Used to state that GraphViz should use the default 'Attribute's -- for the given value. noAttributes :: a -> Attributes noAttributes = const [] -- ----------------------------------------------------------------------------- {- $other Printing different lists of labels. -} -- | Print a path, with \"->\" between each element. showPath :: (Show a) => [a] -> String showPath = showPath' show -- | Print a path, with \"->\" between each element. showPath' :: (a -> String) -> [a] -> String showPath' _ [] = "" showPath' f ls = blockPrint' (l:ls'') where -- Can't use blockPrintWith above, as it only takes a per-row spacer. (l:ls') = map f ls ls'' = map ("-> "++) ls' -- | Print a cycle: copies the first node to the end of the list, -- and then calls 'showPath'. showCycle :: (Show a) => [a] -> String showCycle = showCycle' show -- | Print a cycle: copies the first node to the end of the list, -- and then calls 'showPath''. showCycle' :: (a -> String) -> [a] -> String showCycle' _ [] = "" showCycle' f ls@(l:_) = showPath' f (ls ++ [l]) -- | Show a group of nodes, with no implicit ordering. showNodes :: (Show a) => [a] -> String showNodes = showNodes' show -- | Show a group of nodes, with no implicit ordering. showNodes' :: (a -> String) -> [a] -> String showNodes' _ [] = "" showNodes' f ls = blockPrint' . addCommas $ map f ls where addCommas [] = [] addCommas [l] = [l] addCommas (l:ls') = (l ++ ", ") : addCommas ls' -- ----------------------------------------------------------------------------- -- | Attempt to convert the @String@ form of a list into -- as much of a square shape as possible, using a single -- space as a separation string. blockPrint :: (Show a) => [a] -> String blockPrint = blockPrintWith " " -- | Attempt to convert a list of @String@s into a single @String@ -- that is roughly a square shape, with a single space as a row -- separator. blockPrint' :: [String] -> String blockPrint' = blockPrintWith' " " -- | Attempt to convert the @String@ form of a list into -- as much of a square shape as possible, separating values -- with commas. blockPrintList :: (Show a) => [a] -> String blockPrintList = blockPrintWith ", " -- | Attempt to combine a list of @String@s into as much of a -- square shape as possible, separating values with commas. blockPrintList' :: [String] -> String blockPrintList' = blockPrintWith' ", " -- | Attempt to convert the @String@ form of a list into -- as much of a square shape as possible, using the given -- separation string between elements in the same row. blockPrintWith :: (Show a) => String -> [a] -> String blockPrintWith str = blockPrintWith' str . map show -- | Attempt to convert the combined form of a list of @String@s -- into as much of a square shape as possible, using the given -- separation string between elements in the same row. blockPrintWith' :: String -> [String] -> String blockPrintWith' sep as = init -- Remove the final '\n' on the end. . unlines $ map unwords' lns where lsep = length sep las = addLengths as -- Scale this, to take into account the height:width ratio. sidelen :: Double -- Suppress defaulting messages sidelen = (1.75*) . sqrt . fromIntegral . sum $ map fst las slen = round sidelen serr = round $ sidelen/10 lns = unfoldr (takeLen slen serr lsep) las unwords' = concat . intersperse sep -- | Using the given line length and allowed error, take the elements of -- the next line. takeLen :: Int -> Int -> Int -> [(Int,String)] -> Maybe ([String],[(Int,String)]) takeLen _ _ _ [] = Nothing takeLen len err lsep ((l,a):als) = Just lr where lmax = len + err lr = if l > len then ([a],als) -- Overflow line of single item else (a:as,als') -- We subtract lsep here to take into account the spacer. (as,als') = takeLine (lmax - l - lsep) lsep als -- | Recursively build the rest of the line with given maximum length. takeLine :: Int -> Int -> [(Int,String)] -> ([String],[(Int,String)]) takeLine len lsep als | null als = ([],als) | len <= 0 = ([],als) -- This should be covered by the next guard, -- but just in case... | l > len = ([],als) | otherwise = (a:as,als'') where ((l,a):als') = als -- We subtract lsep here to take into account the spacer. len' = len - l - lsep (as,als'') = takeLine len' lsep als'