szubtsovskiy / elm-visualization / Hierarchy

Hierarchical data has its own visualization requirements, since usually the parent-child relationships tend to be important in understanding the dataset.

This module implements several layout techniques for visualizing such data.

Basic types

Tree a

The tree type used here comes from the gampleman/elm-rosetree package, which is a fully featured and performant library fro dealing with trees. It has several methods you can use to convert other datastructures you may have into that format and use it for visualizing your data.


type Attribute a attr

Each of the layout functions can be customized using a number of optional arguments; these are represented by this type.

The first type argument represents the data contained in your tree, the second is a phantom type that ensures only valid options are passed to each layout method.


type Supported

Used to indicate which attributes go with which layout functions.

For instance, padding returns :

Attribute a { b | padding = Supported }

now partition takes as its first argument:

List (Attribute a { padding = Supported, size = Supported })

The compiler is quite happy to unify these types, but for instance passing this to tidy would cause a type error. It also makes it quite easy to understand from the type signature which options are supported.

none : Attribute a b

Attribute that doesn't affect the settings at all. Can be useful when settings are produced conditionally:

[ Hierarchy.size 230 520
, if doLayered then
    Hierarchy.layered

  else
    Hierarchy.none
]

Return types

Most of the layouts return a record with x, y, width and height attributes. Naturally the simplest is to take these values literally and simply produce a rectangle with these properties. However, these can be profitably interpreted abstractly. For instance one may produce a horizontal diagram by simply switching x with y and width with height. Or treat x and x + width as angles in a radial layout.

Layouts

tidy : List (Attribute a { size : Supported, nodeSize : Supported, layered : Supported, parentChildMargin : Supported, peerMargin : Supported }) -> Tree a -> Tree { height : Basics.Float, node : a, width : Basics.Float, x : Basics.Float, y : Basics.Float }

Produces a tidy node-link diagram of a tree, based on a linear time algorithm by van der Ploeg.

Tidy Tree

partition : List (Attribute a { padding : Supported, size : Supported }) -> (a -> Basics.Float) -> Tree a -> Tree { x : Basics.Float, y : Basics.Float, width : Basics.Float, height : Basics.Float, value : Basics.Float, node : a }

The partition layout produces adjacency diagrams: a space-filling variant of a node-link tree diagram. Rather than drawing a link between parent and child in the hierarchy, nodes are drawn as solid areas (either arcs or rectangles), and their placement relative to other nodes reveals their position in the hierarchy. The size of the nodes encodes a quantitative dimension that would be difficult to show in a node-link diagram.

Sunburst diagram

treemap : List (Attribute a { padding : Supported, paddingInner : Supported, paddingOuter : Supported, tile : Supported, size : Supported }) -> (a -> Basics.Float) -> Tree a -> Tree { x : Basics.Float, y : Basics.Float, width : Basics.Float, height : Basics.Float, value : Basics.Float, node : a }

A treemap recursively subdivides area into rectangles according to each node’s associated value. This implementation supports an extensible tiling method.

Treemap

Options

size : Basics.Float -> Basics.Float -> Attribute a { b | size : Supported }

Sets the size of the entire layout. For most layouts omitting this option will cause it to have a default size of 1.

nodeSize : (a -> ( Basics.Float, Basics.Float )) -> Attribute a { b | nodeSize : Supported }

Sets the size of the actual node to be layed out. This will be the actual size if the size option isn't passed, otherwise this size will get proportionally scaled (preserving aspect ratio).

The default size of a node is ( 1, 1 ).

layered : Attribute a { b | layered : Supported }

Layered behavior

Passing this option causes each "layer" (i.e. nodes in the tree that have the same number of ancestor nodes) to be layed out with the same y value. This makes the layers much more emphasized (if you are for instance visualizing the organization of an army unit, then this might neatly show the rank of each member) at the cost of needing more space.

This only makes a difference if nodeSize returns different heights for different children.

parentChildMargin : Basics.Float -> Attribute a { b | parentChildMargin : Supported }

The vertical distance between a parent and a child in a tree.

peerMargin : Basics.Float -> Attribute a { b | peerMargin : Supported }

The horizontal distance between nodes layed out next to each other.

padding : (a -> Basics.Float) -> Attribute a { b | padding : Supported }

Sets the distances between nodes. For treemaps, this is a shortcut to setting both paddingInner and paddingOuter.

paddingOuter : (a -> Basics.Float) -> Attribute a { b | paddingOuter : Supported }

Sets paddingLeft, paddingRight, paddingTop and paddingBottom in one go.

paddingInner : (a -> Basics.Float) -> Attribute a { b | paddingInner : Supported }

The inner padding is used to separate a node’s adjacent children.

paddingTop : (a -> Basics.Float) -> Attribute a { b | paddingOuter : Supported }

The top padding is used to separate the top edge of a node from its children.

paddingBottom : (a -> Basics.Float) -> Attribute a { b | paddingOuter : Supported }

The bottom padding is used to separate the bottom edge of a node from its children.

paddingLeft : (a -> Basics.Float) -> Attribute a { b | paddingOuter : Supported }

The left padding is used to separate the left edge of a node from its children.

paddingRight : (a -> Basics.Float) -> Attribute a { b | paddingOuter : Supported }

The right padding is used to separate the right edge of a node from its children.

tile : TilingMethod -> Attribute a { b | tile : Supported }

Sets the tiling method to be used. The default is squarify.

Tiling methods

slice : TilingMethod

Divides the rectangular area vertically. The children are positioned in order, starting with the top edge (y0) of the given rectangle.

If the sum of the children’s values is less than the specified node’s value (i.e., if the specified node has a non-zero internal value), the remaining empty space will be positioned on the bottom edge (y1) of the given rectangle.

dice : TilingMethod

Divides the rectangular area horizontally. The children are positioned in order, starting with the left edge (x0) of the given rectangle.

sliceDice : TilingMethod

If the depth is odd, delegates to slice; otherwise delegates to dice.

squarify : TilingMethod

Implements the squarified treemap algorithm by Bruls et al., which seeks to produce rectangles of a given aspect ratio, in this case the golden ratio φ = (1 + sqrt(5)) / 2.

squarifyRatio : Basics.Float -> TilingMethod

Implements the squarified treemap algorithm by Bruls et al., which seeks to produce rectangles of the given aspect ratio. The ratio must be specified as a number greater than or equal to one. Note that the orientation of the generated rectangles (tall or wide) is not implied by the ratio; for example, a ratio of two will attempt to produce a mixture of rectangles whose width:height ratio is either 2:1 or 1:2. (However, you can approximately achieve this result by generating a square treemap at different dimensions, and then stretching the treemap to the desired aspect ratio.) Furthermore, the specified ratio is merely a hint to the tiling algorithm; the rectangles are not guaranteed to have the specified aspect ratio.


type alias TilingMethod =
Basics.Int -> { x0 : Basics.Float
, x1 : Basics.Float
, y0 : Basics.Float
, y1 : Basics.Float } -> Basics.Float -> List Basics.Float -> List { x0 : Basics.Float
, x1 : Basics.Float
, y0 : Basics.Float
, y1 : Basics.Float 
}

You can implement your own tiling method as it's just a function. It recieves the following arguments:

It is expected to return the bounding boxes of the children.

For example, slice can be implemented like this (slightly simplified):

slice : TilingMethod
slice _ { x0, x1, y0, y1 } value children =
    List.foldl
        (\childValue ( prevY, lst ) ->
            let
                nextY =
                    prevY + childValue * ((y1 - y0) / value)
            in
            ( nextY, { x0 = x0, x1 = x1, y0 = prevY, y1 = nextY } :: lst )
        )
        ( y0, [] )
        children
        |> Tuple.second
        |> List.reverse

Note that padding and such will be applied later.