ELM-VISUALIZATION

examples

/

Population Minnesota

PopulationMinnesota

Make negative numbers appear as positive in the ticks

module PopulationMinnesota exposing (main)

import Axis
import Color exposing (Color)
import List.Extra as List
import SampleData exposing (Gender(..))
import Scale exposing (BandScale, ContinuousScale, OrdinalScale, QuantizeScale, Scale, defaultBandConfig)
import Scale.Color
import Shape exposing (StackConfig, StackResult)
import Statistics
import TypedSvg exposing (g, rect, svg, text_)
import TypedSvg.Attributes exposing (class, dy, fill, fontFamily, textAnchor, transform, viewBox)
import TypedSvg.Attributes.InPx exposing (fontSize, height, width, x, y)
import TypedSvg.Core exposing (Svg, text)
import TypedSvg.Types exposing (AnchorAlignment(..), Fill(..), Transform(..), em)


main : Svg msg
main =
    view (Shape.stack config)


populationMinnesota1850 : { categories : List Int, data : List ( Int, List Float ), extent : ( Float, Float ) }
populationMinnesota1850 =
    let
        partitioned =
            -- assumes sorted by year, then age
            SampleData.populationMinnesota1850
                |> List.filter (\{ year } -> year == 1850)
                |> List.partition (\{ gender } -> gender == M)

        categories =
            partitioned
                |> Tuple.first
                |> List.map .age
                |> List.reverse

        ( m, f ) =
            partitioned
                |> (\( a, b ) -> List.map2 Tuple.pair a b)
                |> List.sortBy (Tuple.second >> .people)
                |> List.unzip
    in
    { categories = categories
    , data = [ ( 1850, List.map (.people >> toFloat) m ), ( 1850, List.map (.people >> toFloat >> negate) f ) ]
    , extent =
        Statistics.extent categories
            |> Maybe.map (\( a, b ) -> ( toFloat a, toFloat b ))
            |> Maybe.withDefault ( 0, 0 )
    }


w : Float
w =
    990


h : Float
h =
    504


padding : number
padding =
    60


config : StackConfig Int
config =
    { data = populationMinnesota1850.data
    , offset = Shape.stackOffsetDiverging
    , order = identity
    }


colors : List Color
colors =
    [ Scale.Color.viridisInterpolator 0.3
    , Scale.Color.viridisInterpolator 0.7
    ]


column : BandScale Int -> ( Int, List ( Float, Float ) ) -> Svg msg
column yScale ( year, values ) =
    let
        bandwidth =
            Scale.bandwidth yScale

        block color ( upperY, lowerY ) =
            rect
                [ y <| Scale.convert yScale year
                , x lowerY
                , height bandwidth
                , width (abs <| upperY - lowerY)
                , fill (Fill color)
                ]
                []
    in
    values
        |> List.map2 block colors
        |> g [ class [ "column" ] ]


view : StackResult Int -> Svg msg
view { values, labels, extent } =
    let
        scaledValues =
            values
                |> List.transpose
                |> List.map (List.map (\( y1, y2 ) -> ( Scale.convert xScale y1, Scale.convert xScale y2 )))

        xScale : ContinuousScale Float
        xScale =
            Scale.linear ( w - padding, padding ) extent
                |> Scale.nice 4

        yScale : BandScale Int
        yScale =
            Scale.band { defaultBandConfig | paddingInner = 0.1, paddingOuter = 0.2 } ( padding, h - padding ) populationMinnesota1850.categories
    in
    svg [ viewBox 0 0 w h ]
        [ g [ transform [ Translate 0 (h - padding) ] ]
            [ Axis.bottom [ Axis.tickFormat (absoluteTickFormat xScale 10) ] xScale ]
        , g [ class [ "series" ] ] <|
            List.map (column yScale) (List.map2 (\a b -> ( a, b )) populationMinnesota1850.categories scaledValues)
        , g [ transform [ Translate padding 0 ] ]
            [ Axis.left [] (Scale.toRenderable String.fromInt yScale)
            , text_ [ fontFamily [ "sans-serif" ], fontSize 14, x 5, y 65 ] [ text "Age" ]
            ]
        , g [ transform [ Translate (w - padding) (padding + 20) ] ]
            [ text_ [ fontFamily [ "sans-serif" ], fontSize 20, textAnchor AnchorEnd ] [ text "1850" ]
            , text_ [ fontFamily [ "sans-serif" ], fontSize 10, textAnchor AnchorEnd, dy (em 1) ] [ text "population distribution in Minnesota" ]
            ]
        , text_
            [ transform [ Translate (Scale.convert xScale 500000) (2 * padding) ]
            , fontFamily [ "sans-serif" ]
            , fontSize 20
            , textAnchor AnchorMiddle
            ]
            [ text "Men" ]
        , text_
            [ transform [ Translate (Scale.convert xScale -500000) (2 * padding) ]
            , fontFamily [ "sans-serif" ]
            , fontSize 20
            , textAnchor AnchorMiddle
            ]
            [ text "Women" ]
        , text_
            [ transform [ Translate (Scale.convert xScale 0) (h - padding / 2) ]
            , fontFamily [ "sans-serif" ]
            , fontSize 14
            , textAnchor AnchorMiddle
            , dy (em 1)
            ]
            [ text "People" ]
        ]



absoluteTickFormat :
    Scale
        { a
            | convert : domain -> range -> number -> b
            , domain : domain
            , tickFormat : domain -> Int -> number -> String
        }
    -> Int
    -> number
    -> String
absoluteTickFormat scale tickCount value =
    Scale.tickFormat scale tickCount (abs value)