Graphalyze-0.7.0.0: Data/Graph/Analysis/Visualisation.hs
{- |
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'