professortX / elm-visualization / Shape

Visualizations typically consist of discrete graphical marks, such as symbols, arcs, lines and areas. While the rectangles of a bar chart may be easy enough to generate directly using SVG or Canvas, other shapes are complex, such as rounded annular sectors and centripetal Catmull–Rom splines. This module provides a variety of shape generators for your convenience.

Arcs

Pie Chart

arc : Arc -> Path

The arc generator produces a circular or annular sector, as in a pie or donut chart. If the difference between the start and end angles (the angular span) is greater than τ, the arc generator will produce a complete circle or annulus. If it is less than τ, arcs may have rounded corners and angular padding. Arcs are always centered at ⟨0,0⟩; use a transform to move the arc to a different position.

See also the pie generator, which computes the necessary angles to represent an array of data as a pie or donut chart; these angles can then be passed to an arc generator.


type alias Arc =
{ innerRadius : Basics.Float
, outerRadius : Basics.Float
, cornerRadius : Basics.Float
, startAngle : Basics.Float
, endAngle : Basics.Float
, padAngle : Basics.Float
, padRadius : Basics.Float 
}

Used to configure an arc. These can be generated by a pie, but you can easily modify these later.

innerRadius : Float

Usefull for creating a donut chart. A negative value is treated as zero. If larger than outerRadius they are swapped.

outerRadius : Float

The radius of the arc. A negative value is treated as zero. If smaller than innerRadius they are swapped.

cornerRadius : Float

If the corner radius is greater than zero, the corners of the arc are rounded using circles of the given radius. For a circular sector, the two outer corners are rounded; for an annular sector, all four corners are rounded. The corner circles are shown in this diagram:

Corner Radius

The corner radius may not be larger than (outerRadius - innerRadius) / 2. In addition, for arcs whose angular span is less than π, the corner radius may be reduced as two adjacent rounded corners intersect. This is occurs more often with the inner corners.

startAngle : Float

The angle is specified in radians, with 0 at -y (12 o’clock) and positive angles proceeding clockwise. If |endAngle - startAngle| ≥ τ, a complete circle or annulus is generated rather than a sector.

endAngle : Float

The angle is specified in radians, with 0 at -y (12 o’clock) and positive angles proceeding clockwise. If |endAngle - startAngle| ≥ τ, a complete circle or annulus is generated rather than a sector.

padAngle : Float

The pad angle is converted to a fixed linear distance separating adjacent arcs, defined as padRadius * padAngle. This distance is subtracted equally from the start and end of the arc. If the arc forms a complete circle or annulus, as when |endAngle - startAngle| ≥ τ, the pad angle is ignored.

If the inner radius or angular span is small relative to the pad angle, it may not be possible to maintain parallel edges between adjacent arcs. In this case, the inner edge of the arc may collapse to a point, similar to a circular sector. For this reason, padding is typically only applied to annular sectors (i.e., when innerRadius is positive), as shown in this diagram:

Pad Angle

The recommended minimum inner radius when using padding is outerRadius * padAngle / sin(θ), where θ is the angular span of the smallest arc before padding. For example, if the outer radius is 200 pixels and the pad angle is 0.02 radians, a reasonable θ is 0.04 radians, and a reasonable inner radius is 100 pixels.

Often, the pad angle is not set directly on the arc generator, but is instead computed by the pie generator so as to ensure that the area of padded arcs is proportional to their value. If you apply a constant pad angle to the arc generator directly, it tends to subtract disproportionately from smaller arcs, introducing distortion.

padRadius : Float

The pad radius determines the fixed linear distance separating adjacent arcs, defined as padRadius * padAngle.

centroid : Arc -> ( Basics.Float, Basics.Float )

Computes the midpoint (x, y) of the center line of the arc that would be generated by the given arguments. The midpoint is defined as (startAngle + endAngle) / 2 and (innerRadius + outerRadius) / 2. For example:

Centroid

Note that this is not the geometric center of the arc, which may be outside the arc; this function is merely a convenience for positioning labels.

Pies


type alias PieConfig a =
{ startAngle : Basics.Float
, endAngle : Basics.Float
, padAngle : Basics.Float
, sortingFn : a -> a -> Basics.Order
, valueFn : a -> Basics.Float
, innerRadius : Basics.Float
, outerRadius : Basics.Float
, cornerRadius : Basics.Float
, padRadius : Basics.Float 
}

Used to configure a pie generator function.

innerRadius, outerRadius, cornerRadius and padRadius are simply forwarded to the Arc result. They are provided here simply for convenience.

valueFn : a -> Float

This is used to compute the actual numerical value used for computing the angles. You may use a List.map to preprocess data into numbers instead, but this is useful if trying to use sortingFn.

sortingFn : a -> a -> Order

Sorts the data. Sorting does not affect the order of the generated arc list, which is always in the same order as the input data list; it merely affects the computed angles of each arc. The first arc starts at the start angle and the last arc ends at the end angle.

startAngle : Float

The start angle here means the overall start angle of the pie, i.e., the start angle of the first arc. The units of angle are arbitrary, but if you plan to use the pie generator in conjunction with an arc generator, you should specify an angle in radians, with 0 at -y (12 o’clock) and positive angles proceeding clockwise.

endAngle : Float

The end angle here means the overall end angle of the pie, i.e., the end angle of the last arc. The units of angle are arbitrary, but if you plan to use the pie generator in conjunction with an arc generator, you should specify an angle in radians, with 0 at -y (12 o’clock) and positive angles proceeding clockwise.

The value of the end angle is constrained to startAngle ± τ, such that |endAngle - startAngle| ≤ τ.

padAngle : Float

The pad angle here means the angular separation between each adjacent arc. The total amount of padding reserved is the specified angle times the number of elements in the input data list, and at most |endAngle - startAngle|; the remaining space is then divided proportionally by value such that the relative area of each arc is preserved.

pie : PieConfig a -> List a -> List Arc

The pie generator does not produce a shape directly, but instead computes the necessary angles to represent a tabular dataset as a pie or donut chart; these angles can then be passed to an arc generator.

defaultPieConfig : PieConfig Basics.Float

The default config for generating pies.

import Shape exposing (defaultPieConfig)

pieData =
    Shape.pie { defaultPieConfig | outerRadius = 230 } model

Note that if you change valueFn, you will likely also want to change sortingFn.

Lines

Line Chart

line : (List ( Basics.Float, Basics.Float ) -> SubPath) -> List (Maybe ( Basics.Float, Basics.Float )) -> Path

Generates a line for the given array of points which can be passed to the d attribute of the path SVG element. It needs to be suplied with a curve function. Points accepted are Maybes, Nothing represent gaps in the data and corresponding gaps will be rendered in the line.

Note: A single point (surrounded by Nothing) may not be visible.

Usually you will need to convert your data into a format supported by this function. For example, if your data is a List (Date, Float), you might use something like:

lineGenerator : ( Date, Float ) -> Maybe ( Float, Float )
lineGenerator ( x, y ) =
    Just ( Scale.convert xScale x, Scale.convert yScale y )

linePath : List ( Date, Float ) -> Path
linePath data =
    List.map lineGenerator data
        |> Shape.line Shape.linearCurve

where xScale and yScale would be appropriate Scales.

lineRadial : (List ( Basics.Float, Basics.Float ) -> SubPath) -> List (Maybe ( Basics.Float, Basics.Float )) -> Path

This works exactly like line, except it interprets the points it recieves as (angle, radius) pairs, where radius is in radians. Therefore it renders a radial layout with a center at (0, 0).

Use a transform to position the layout in final rendering.

area : (List ( Basics.Float, Basics.Float ) -> SubPath) -> List (Maybe ( ( Basics.Float, Basics.Float ), ( Basics.Float, Basics.Float ) )) -> Path

The area generator produces an area, as in an area chart. An area is defined by two bounding lines, either splines or polylines. Typically, the two lines share the same x-values (x0 = x1), differing only in y-value (y0 and y1); most commonly, y0 is defined as a constant representing zero. The first line (the topline) is defined by x1 and y1 and is rendered first; the second line (the baseline) is defined by x0 and y0 and is rendered second, with the points in reverse order. With a linearCurve curve, this produces a clockwise polygon.

The data attribute you pass in should be a [Just ((x0, y0), (x1, y1))]. Passing in Nothing represents gaps in the data and corresponding gaps in the area will be rendered.

Usually you will need to convert your data into a format supported by this function. For example, if your data is a List (Date, Float), you might use something like:

areaGenerator : ( Date, Float ) -> Maybe ( ( Float, Float ), ( Float, Float ) )
areaGenerator ( x, y ) =
    Just
        ( ( Scale.convert xScale x, Tuple.first (Scale.rangeExtent yScale) )
        , ( Scale.convert xScale x, Scale.convert yScale y )
        )

areaPath : List ( Date, Float ) -> Path
areaPath data =
    List.map areaGenerator data
        |> Shape.area Shape.linearCurve

where xScale and yScale would be appropriate Scales.

areaRadial : (List ( Basics.Float, Basics.Float ) -> SubPath) -> List (Maybe ( ( Basics.Float, Basics.Float ), ( Basics.Float, Basics.Float ) )) -> Path

This works exactly like area, except it interprets the points it recieves as (angle, radius) pairs, where radius is in radians. Therefore it renders a radial layout with a center at (0, 0).

Use a transform to position the layout in final rendering.

Curves

While lines are defined as a sequence of two-dimensional [x, y] points, and areas are similarly defined by a topline and a baseline, there remains the task of transforming this discrete representation into a continuous shape: i.e., how to interpolate between the points. A variety of curves are provided for this purpose.

linearCurve : List ( Basics.Float, Basics.Float ) -> SubPath

Produces a polyline through the specified points.

linear curve illustration

basisCurve : List ( Basics.Float, Basics.Float ) -> SubPath

Produces a cubic basis spline using the specified control points. The first and last points are triplicated such that the spline starts at the first point and ends at the last point, and is tangent to the line between the first and second points, and to the line between the penultimate and last points.

basis curve illustration

basisCurveClosed : List ( Basics.Float, Basics.Float ) -> SubPath

Produces a closed cubic basis spline using the specified control points. When a line segment ends, the first three control points are repeated, producing a closed loop with C2 continuity.

closed basis curve illustration

basisCurveOpen : List ( Basics.Float, Basics.Float ) -> SubPath

Produces a cubic basis spline using the specified control points. Unlike basis, the first and last points are not repeated, and thus the curve typically does not intersect these points.

open basis curve illustration

bumpXCurve : List ( Basics.Float, Basics.Float ) -> SubPath

Produces a Bézier curve between each pair of points, with horizontal tangents at each point.

bumpX curve illustration

bumpYCurve : List ( Basics.Float, Basics.Float ) -> SubPath

Produces a Bézier curve between each pair of points, with vertical tangents at each point.

bumpX curve illustration

bundleCurve : Basics.Float -> List ( Basics.Float, Basics.Float ) -> SubPath

Produces a straightened cubic basis spline using the specified control points, with the spline straightened according to the curve’s beta (a reasonable default is 0.85). This curve is typically used in hierarchical edge bundling to disambiguate connections, as proposed by Danny Holten in Hierarchical Edge Bundles: Visualization of Adjacency Relations in Hierarchical Data.

This curve is not suitable to be used with areas.

bundle curve illustration

cardinalCurve : Basics.Float -> List ( Basics.Float, Basics.Float ) -> SubPath

Produces a cubic cardinal spline using the specified control points, with one-sided differences used for the first and last piece.

The tension parameter determines the length of the tangents: a tension of one yields all zero tangents, equivalent to linearCurve; a tension of zero produces a uniform Catmull–Rom spline.

cardinal curve illustration

cardinalCurveClosed : Basics.Float -> List ( Basics.Float, Basics.Float ) -> SubPath

Produces a cubic cardinal spline using the specified control points. At the end, the first three control points are repeated, producing a closed loop.

The tension parameter determines the length of the tangents: a tension of one yields all zero tangents, equivalent to linearCurve; a tension of zero produces a uniform Catmull–Rom spline.

cardinal closed curve illustration

cardinalCurveOpen : Basics.Float -> List ( Basics.Float, Basics.Float ) -> SubPath

Produces a cubic cardinal spline using the specified control points. Unlike curveCardinal, one-sided differences are not used for the first and last piece, and thus the curve starts at the second point and ends at the penultimate point.

The tension parameter determines the length of the tangents: a tension of one yields all zero tangents, equivalent to linearCurve; a tension of zero produces a uniform Catmull–Rom spline.

cardinal open curve illustration

catmullRomCurve : Basics.Float -> List ( Basics.Float, Basics.Float ) -> SubPath

Produces a cubic Catmull–Rom spline using the specified control points and the parameter alpha (a good default is 0.5), as proposed by Yuksel et al. in On the Parameterization of Catmull–Rom Curves, with one-sided differences used for the first and last piece.

If alpha is zero, produces a uniform spline, equivalent to curveCardinal with a tension of zero; if alpha is one, produces a chordal spline; if alpha is 0.5, produces a centripetal spline. Centripetal splines are recommended to avoid self-intersections and overshoot.

Catmul-Rom curve illustration

catmullRomCurveClosed : Basics.Float -> List ( Basics.Float, Basics.Float ) -> SubPath

Produces a cubic Catmull–Rom spline using the specified control points and the parameter alpha (a good default is 0.5), as proposed by Yuksel et al. When a line segment ends, the first three control points are repeated, producing a closed loop.

If alpha is zero, produces a uniform spline, equivalent to curveCardinal with a tension of zero; if alpha is one, produces a chordal spline; if alpha is 0.5, produces a centripetal spline. Centripetal splines are recommended to avoid self-intersections and overshoot.

Catmul-Rom closed curve illustration

catmullRomCurveOpen : Basics.Float -> List ( Basics.Float, Basics.Float ) -> SubPath

Produces a cubic Catmull–Rom spline using the specified control points and the parameter alpha (a good default is 0.5), as proposed by Yuksel et al. Unlike curveCatmullRom, one-sided differences are not used for the first and last piece, and thus the curve starts at the second point and ends at the penultimate point.

If alpha is zero, produces a uniform spline, equivalent to curveCardinal with a tension of zero; if alpha is one, produces a chordal spline; if alpha is 0.5, produces a centripetal spline. Centripetal splines are recommended to avoid self-intersections and overshoot.

Catmul-Rom open curve illustration

monotoneInXCurve : List ( Basics.Float, Basics.Float ) -> SubPath

Produces a cubic spline that preserves monotonicity in y, assuming monotonicity in x, as proposed by Steffen in A simple method for monotonic interpolation in one dimension: “a smooth curve with continuous first-order derivatives that passes through any given set of data points without spurious oscillations. Local extrema can occur only at grid points where they are given by the data, but not in between two adjacent grid points.”

monotone in x curve illustration

monotoneInYCurve : List ( Basics.Float, Basics.Float ) -> SubPath

Produces a cubic spline that preserves monotonicity in y, assuming monotonicity in y, as proposed by Steffen in A simple method for monotonic interpolation in one dimension: “a smooth curve with continuous first-order derivatives that passes through any given set of data points without spurious oscillations. Local extrema can occur only at grid points where they are given by the data, but not in between two adjacent grid points.”

stepCurve : Basics.Float -> List ( Basics.Float, Basics.Float ) -> SubPath

Produces a piecewise constant function (a step function) consisting of alternating horizontal and vertical lines.

The factor parameter changes when the y-value changes between each pair of adjacent x-values.

step curve illustration

naturalCurve : List ( Basics.Float, Basics.Float ) -> SubPath

Produces a natural cubic spline with the second derivative of the spline set to zero at the endpoints.

natural curve illustration

Stack

A stack is a way to fit multiple graphs into one drawing. Rather than drawing graphs on top of each other, the layers are stacked. This is useful when the relation between the graphs is of interest.

In most cases, the absolute size of a piece of data becomes harder to determine for the reader.


type alias StackConfig a =
{ data : List ( a
, List Basics.Float )
, offset : List (List ( Basics.Float
, Basics.Float )) -> List (List ( Basics.Float
, Basics.Float ))
, order : List ( a
, List Basics.Float ) -> List ( a
, List Basics.Float ) 
}

Configuration for a stacked chart.

Some example configs:

stackedBarChart : StackConfig String
stackedBarChart =
    { data = myData
    , offset = Shape.stackOffsetNone
    , order =
        -- stylistic choice: largest (by sum of values)
        -- category at the bottom
        List.sortBy (Tuple.second >> List.sum >> negate)
    }

streamgraph : StackConfig String
streamgraph =
    { data = myData
    , offset = Shape.stackOffsetWiggle
    , order = Shape.sortByInsideOut (Tuple.second >> List.sum)
    }


type alias StackResult a =
{ values : List (List ( Basics.Float
, Basics.Float ))
, labels : List a
, extent : ( Basics.Float
, Basics.Float ) 
}

The basis for constructing a stacked chart

stack : StackConfig a -> StackResult a

Create a stack result

Stack Offset

The method of stacking.

stackOffsetNone : List (List ( Basics.Float, Basics.Float )) -> List (List ( Basics.Float, Basics.Float ))

Stack offset none

Stacks the values on top of each other, starting at 0.

stackOffsetNone [ [ (0, 42) ], [ (0, 70) ] ]
            --> [ [ (0, 42) ], [ (42, 112 ) ] ]

stackOffsetNone [ [ (0, 42) ], [ (20, 70) ] ]
            --> [ [ (0, 42) ], [ (42, 112 ) ] ]

stackOffsetDiverging : List (List ( Basics.Float, Basics.Float )) -> List (List ( Basics.Float, Basics.Float ))

Stack offset diverging

Positive values are stacked above zero, negative values below zero.

stackOffsetDiverging [ [ (0, 42) ], [ (0, -24) ] ]
            --> [ [ (0, 42) ], [ (-24, 0 ) ] ]

stackOffsetDiverging [ [ (0, 42), (0, -20) ], [ (0, -24), (0, -24) ] ]
            --> [[(0,42),(-20,0)],[(-24,0),(-44,-20)]]

stackOffsetExpand : List (List ( Basics.Float, Basics.Float )) -> List (List ( Basics.Float, Basics.Float ))

stackOffsetExpand

Applies a zero baseline and normalizes the values for each point such that the topline is always one.

stackOffsetExpand [ [ (0, 50) ], [ (50, 100) ] ]
            --> [[(0,0.5)],[(0.5,1)]]

stackOffsetSilhouette : List (List ( Basics.Float, Basics.Float )) -> List (List ( Basics.Float, Basics.Float ))

stackOffsetSilhouette

Shifts the baseline down such that the center of the streamgraph is always at zero.

stackOffsetSilhouette [ [ (0, 50) ], [ (50, 100) ] ]
            --> [[(-75,-25)],[(-25,75)]]

stackOffsetWiggle : List (List ( Basics.Float, Basics.Float )) -> List (List ( Basics.Float, Basics.Float ))

stackOffsetWiggle

Shifts the baseline so as to minimize the weighted wiggle of layers.

Visually, high wiggle means peaks going in both directions very close to each other. The silhouette stack offset above often suffers from having high wiggle.

stackOffsetWiggle [ [ (0, 50) ], [ (50, 100) ] ]
            --> [[(0,50)],[(50,150)]]

Stack Order

The order of the layers. Normal list functions can be used, for instance

-- keep order of the input data
identity

-- reverse
List.reverse

-- decreasing by sum of the values (largest is lowest)
List.sortBy (Tuple.second >> List.sum >> negate)

sortByInsideOut : (a -> Basics.Float) -> List a -> List a

Sort such that small values are at the outer edges, and large values in the middle.

This is the recommended order for stream graphs.