{- | 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, assignCluster, setDir, -- * 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. 'blankParams' may be useful for creating initial definitions of 'GraphvizParams', especially for 'graphvizClusters'. -} -- | Convert the 'GraphData' into 'DotGraph' format. graphviz :: (Ord cl) => GraphvizParams Node nl el cl l -> GraphData nl el -> DotGraph Node graphviz = setDir graphToDot -- | Convert the clustered 'GraphData' into 'DotGraph' format. -- Cluster the nodes based upon their 'ClusterLabel' clusters. graphvizClusters :: (ClusterLabel nl) => GraphvizParams Node nl el (Cluster nl) (NodeLabel nl) -> GraphData nl el -> DotGraph Node graphvizClusters ps = graphviz params where params = ps { clusterBy = assignCluster , clusterID = toGraphID } -- | A function to convert an 'LNode' to the required 'LNodeCluster' -- for use with the GraphViz library. assignCluster :: (ClusterLabel cl) => LNode cl -> LNodeCluster (Cluster cl) (NodeLabel cl) assignCluster (n,a) = C (cluster a) $ N (n, nodeLabel a) -- | A cross between 'applyDirAlg' and 'setDirectedness'. setDir :: (GraphvizParams Node nl el cl l -> AGr nl el -> a) -> GraphvizParams Node nl el cl l -> GraphData nl el -> a setDir f params gd = f params' (graph gd) where params' = params { isDirected = directedData gd } -- ----------------------------------------------------------------------------- {- $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'