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.
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.
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.
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
]
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.
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.
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.
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.
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 }
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
.
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.
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.