{-# LANGUAGE BangPatterns, FlexibleContexts #-} -- | This module contain functions for drawing diagrams of -- dendrograms. module Diagrams.Dendrogram ( -- * High-level interface -- $runnableExample dendrogram , Width(..) -- * Low-level interface , dendrogramPath , fixedWidth , variableWidth , X ) where -- from base import Control.Arrow (first, second) -- from hierarchical-clustering import Data.Clustering.Hierarchical (Dendrogram(..), elements) -- from diagrams-lib import Diagrams.Prelude -- $runnableExample -- -- Given a dendrogram @dendro :: 'Dendrogram' a@ and a function -- @drawItem :: a -> Diagram b R2@ for drawing the items on the -- leaves of @dendro@, just use @'dendrogram' 'Variable' drawItem -- dendro :: Diagram b R2@ to draw a diagram of @dendro@. -- -- Runnable example which produces something like -- : -- -- @ --import Data.Clustering.Hierarchical (Dendrogram(..)) --import Diagrams.Prelude (Diagram, R2, atop, lw, pad, roundedRect, text, (\#)) --import Diagrams.Backend.Cairo.CmdLine (Cairo, defaultMain) --import qualified Diagrams.Dendrogram as D -- --main :: IO () --main = defaultMain diagram -- --diagram :: Diagram Cairo R2 --diagram = D.'dendrogram' 'D.Fixed' char test \# lw 0.1 \# pad 1.1 -- --char :: Char -> Diagram Cairo R2 --char c = pad 1.3 $ roundedRect (1,1) 0.1 \`atop\` text [c] -- --test :: Dendrogram Char --test = Branch 5 -- (Branch 2 -- (Branch 1 -- (Leaf \'A\') -- (Leaf \'B\')) -- (Leaf \'C\')) -- (Leaf \'D\') -- @ -- | @dendrogram width drawItem dendro@ is a drawing of the -- dendrogram @dendro@ using @drawItem@ to draw its leafs. The -- @width@ parameter controls how whether all items have the same -- width or not ('Fixed' or 'Variable', respectively, see -- 'Width'). -- -- Note: you should probably use 'alignT' to align your items. dendrogram :: (Monoid m, Semigroup m, Renderable (Path R2) b) => Width -> (a -> QDiagram b R2 m) -> Dendrogram a -> QDiagram b R2 m dendrogram width_ drawItem dendro = (stroke path_ # value mempty) === (items # alignL) where (path_, items) = case width_ of Fixed -> let drawnItems = map drawItem (elements dendro) w = width (head drawnItems) (dendro', _) = fixedWidth w dendro in (dendrogramPath dendro', hcat drawnItems) Variable -> first dendrogramPath $ variableWidth drawItem dendro -- | The width of the items on the leafs of a dendrogram. data Width = Fixed -- ^ @Fixed@ assumes that all items have a fixed width -- (which is automatically calculated). This mode is -- faster than @Variable@, especially when you have many -- items. | Variable -- ^ @Variable@ does not assume that all items have a fixed -- width, so each item may have a different width. This -- mode is slower since it has to calculate the width of -- each item separately. -- | A dendrogram path that can be 'stroke'@d@ later. This function -- assumes that the 'Leaf'@s@ of your 'Dendrogram' are already in -- the right position. dendrogramPath :: Dendrogram X -> Path R2 dendrogramPath = mconcat . fst . go [] where go acc (Leaf x) = (acc, (x, 0)) go acc (Branch d l r) = (path : acc'', pos) where (acc', (!xL, !yL)) = go acc l (acc'', (!xR, !yR)) = go acc' r path = fromVertices [ p2 (xL, yL) , p2 (xL, d) , p2 (xR, d) , p2 (xR, yR)] pos = (xL + (xR - xL) / 2, d) -- | The horizontal position of a dendrogram Leaf. type X = Double -- | @fixedWidth w@ positions the 'Leaf'@s@ of a 'Dendrogram' -- assuming that they have the same width @w@. Also returns the -- total width. fixedWidth :: Double -> Dendrogram a -> (Dendrogram X, Double) fixedWidth w = second (subtract half_w) . go half_w where half_w = w/2 go !y (Leaf _) = (Leaf y, y + w) go !y (Branch d l r) = (Branch d l' r', y'') where (l', !y') = go y l (r', !y'') = go y' r -- | @variableWidth draw@ positions the 'Leaf'@s@ of a -- 'Dendrogram' according to the diagram generated by 'draw'. -- Each 'Leaf' may have a different width. Also returns the -- resulting diagram having all 'Leaf'@s@ drawn side-by-side. -- -- Note: you should probably use 'alignT' to align your items. variableWidth :: (Semigroup m, Monoid m) => (a -> QDiagram b R2 m) -> Dendrogram a -> (Dendrogram X, QDiagram b R2 m) variableWidth draw = finish . go 0 [] where go !y acc (Leaf a) = (Leaf y', y'', dia : acc) where dia = draw a !w = width dia !y' = y + w/2 !y'' = y + w go !y acc (Branch d l r) = (Branch d l' r', y'', acc'') where (l', !y', acc'') = go y acc' l -- yes, this is acc' (r', !y'', acc') = go y' acc r finish (dendro, _, dias) = (dendro, hcat dias) -- We used to concatenate diagrams inside 'go' using (|||). -- However, pathological dendrograms (such as those created -- using single linkage) may be highly unbalanced, creating -- a performance problem for 'variableWidth'.