module Text.HTML.TreeMap ( treeMapExt
, treeMap
, defaultTreeOpts
, TreeMapOptions(..))
where
import Data.Tree
import Data.List (sortBy, groupBy)
import Text.HTML.TreeUtils
import Text.Html
--
-- Configuration Data Types
--
data TreeMapOptions = TreeMapOptions { toLeafColor :: String -- background color of boxes the leafs
, toAltColors :: [String] -- background colors of non-leafs. they are alternated
}
-- Good default settings for the colors
defaultTreeOpts :: TreeMapOptions
defaultTreeOpts = TreeMapOptions "white" ["yellow", "orange", "#9ACD32", "#FFD700" , "#FF6347" ]
--
-- Box Data Types. It is used for better expressing layouting algorithm result.
-- Basically layouting algorithm has to decide how many tables with how many
-- columns are used for nested boxes representation. Current implementation
-- uses only one big TableBox in BoxGrid. But for future there migh be better
-- approach. The goal is to display treemap with as much as possible taken place
-- and unused gaps.
--
data Box = BoxText { boName :: String,
boId :: Int }
| BoxGrid { boName :: String,
boId :: Int,
boGrid :: [TableBox] } deriving (Ord,Eq,Show)
-- Type used witin Box with already decided layoting, for nested box groups
data TableBox = TableBox { tbCols :: Int, tbCels :: [Box] } deriving (Ord,Eq,Show)
--
-- Size Basic Functions
--
type IntW = Int
type IntH = Int
type BoxSize = (IntW,IntH)
concatRowSize :: [BoxSize] -> BoxSize
concatRowSize [] = (0,0)
concatRowSize (x:xs) = plusSize x (concatRowSize xs)
where
plusSize (a1,b1) (a2,b2) = (a1+a2,max b1 b2)
--
-- Get Size of Elements
--
tableSize :: TableBox -> BoxSize
tableSize (TableBox _ []) = (0,0)
tableSize (TableBox n xs) = (maximum (rowW xs), sum (rowH xs))
where
rowW [] = [0]
rowW ys = (fst $ concatRowSize $ map (boxSize) (take n ys)):(rowW (drop n ys))
rowH [] = [0]
rowH ys = (snd $ concatRowSize $ map (boxSize) (take n ys)):(rowH (drop n ys))
labelSize :: String -> BoxSize
labelSize cs = (maximum $ map length (lines cs),length (lines cs))
-- size2square :: BoxSize -> Int
-- size2square (w,h) = w*h
-- boxSquare :: Box -> Int
-- boxSquare = size2square . boxSize
boxSize :: Box -> BoxSize
boxSize (BoxText cs _ ) = labelSize cs
boxSize (BoxGrid cs _ xs) = (boxW, boxH)
where
boxW = maximum (rowW xs)
boxH = round (fontCorr * (fromIntegral $ sum (rowH xs)) :: Double)
rowW [] = [fst $ labelSize cs]
rowW ys = (fst $ concatRowSize $ map (tableSize) (take 1 ys)):(rowW (drop 1 ys))
rowH [] = [snd $ labelSize cs]
rowH ys = (snd $ concatRowSize $ map (tableSize) (take 1 ys)):(rowH (drop 1 ys))
fontCorr = 1.3 -- this will increase height. It is heuristic constant :)
-- Function which figure <1..0) number with following meaning
-- 1 : the Box is really Square
-- 0 : the Box is actually Rectangle
boxFactor :: BoxSize -> Float
boxFactor (w,h) | h > w = (fromIntegral w)/(fromIntegral h)
| otherwise = (fromIntegral h)/(fromIntegral w)
--
-- Layout Functions
--
mkBox :: String -> Int -> [Box] -> Box
mkBox cs i [] = BoxText cs i
mkBox cs i sx = BoxGrid cs i (sortBy (\t1 t2 -> (tbCols t2) `compare` (tbCols t1)) $ map findBestTable boxesGroupsSorted)
where
boxesGroupsSorted = map (sortBoxesByH) boxesGroups
boxesGroups = groupBy (\_ _ -> True) (sortBoxesBySize sx)
--boxesGroups = groupBy (\a b -> (abs $ (snd $ boxSize a) - (snd $ boxSize b)) < thr) (sortBoxesBySize sx)
--thr = 300
sortBoxesBySize, sortBoxesByH :: [Box] -> [Box]
sortBoxesBySize = sortBy (\a b -> boxSize a `compare` boxSize b)
sortBoxesByH = sortBy (\a b -> (snd $ boxSize a) `compare` (snd $ boxSize b))
findBestTable :: [Box] -> TableBox
findBestTable ys = TableBox (bestBoxCols ys) ys
where
tables xs = [ TableBox c xs | c <- [1..50] ]
bestBoxFactor xs = maximum [boxFactor $ tableSize t | t <- tables xs]
bestBoxCols xs = head [tbCols t | t <- tables xs, (boxFactor $ tableSize t) == bestBoxFactor xs]
--
-- Render Functions
--
renderTableBox :: TreeMapOptions -> Int -> TableBox -> Html
renderTableBox opts i (TableBox n xs) = simpleTable tAttr [] (filter (not . null) $ mkRows xs)
where
tAttr = [identifier ("boxId"++(show i)), thestyle "display: none;"]
mkRows [] = [[]]
mkRows ys = (map (renderBox (rotColors opts)) $ take n ys):(mkRows (drop n ys))
renderBox :: TreeMapOptions -> Box -> Html
renderBox opts (BoxText l i ) = mkHTMLBox l i [] 1 (defBoxProps { boxFillColor = toLeafColor opts })
renderBox opts (BoxGrid l i xs) = mkHTMLBox l i tbl 1 (defBoxProps { boxFillColor = head $ toAltColors opts })
where
tbl = map (renderTableBox opts i) xs
-- Rotate colors during folding tree
rotColors :: TreeMapOptions -> TreeMapOptions
rotColors (TreeMapOptions lc []) = TreeMapOptions lc []
rotColors (TreeMapOptions lc (c:acs)) = TreeMapOptions lc (acs++[c])
--
-- Render given Tree of String into HTML with default
-- options.
--
treeMap :: Tree String -> String
treeMap xs = treeMapExt defaultTreeOpts xs
--
-- Render given Tree of String into HTML using options
-- provided.
--
treeMapExt :: TreeMapOptions -> Tree String -> String
treeMapExt opts xs = renderHtml $ concatHtml [displayBoxJS,boxes]
where
boxes = renderBox opts $ treeMapLBox lxs
lxs = tree2ltree xs
--
-- Create for the Tree of Strings the Box which can
-- be than rendered to String.
--
treeMapLBox :: Tree (Int,String) -> Box
treeMapLBox (Node (i,s) xs) = mkBox s i (map treeMapLBox xs)
--
-- Testing Section, example & main
--
{-
exampleTree :: Tree String
exampleTree = Node "rado" [Node "peter" [Node "jano" [] ], Node "dusan" [Node "jozef" [Node "Longer Name" [] ] ]]
main = putStrLn (treeMap exampleTree)
-}