PopulationMinnesota


PopulationMinnesota

Make negative numbers appear as positive in the ticks

module PopulationMinnesota exposing (main)

import SampleData exposing (Gender(M, F))
import Color.Convert exposing (colorToCssRgba)
import Visualization.Scale as Scale exposing (ContinuousScale, OrdinalScale, Scale, BandScale, defaultBandConfig, QuantizeScale)
import Visualization.Axis as Axis exposing (Orientation(..))
import List.Extra as List
import Visualization.List
import Svg exposing (..)
import Svg.Attributes exposing (..)
import Visualization.Shape as Shape exposing (StackConfig, StackResult)


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
                |> uncurry (List.map2 (,))
                |> 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 =
            Visualization.List.extent categories
                |> Maybe.map (\( a, b ) -> ( toFloat a, toFloat b ))
                |> Maybe.withDefault ( 0, 0 )
        }


width : number
width =
    990


height : number
height =
    504


padding : number
padding =
    60


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


colors : List String
colors =
    [ Scale.viridisInterpolator 0.3
    , Scale.viridisInterpolator 0.7
    ]
        |> List.map colorToCssRgba


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

        block color ( upperY, lowerY ) =
            rect
                [ y <| toString <| Scale.convert yScale year
                , x <| toString <| lowerY
                , Svg.Attributes.height <| toString <| bandwidth
                , Svg.Attributes.width <| toString <| (abs <| upperY - lowerY)
                , 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 )))

        axisOptions =
            Axis.defaultOptions

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

        yScale : BandScale Int
        yScale =
            Scale.band { defaultBandConfig | paddingInner = 0.1, paddingOuter = 0.2 } populationMinnesota1850.categories ( padding, height - padding )

        xAxis : Svg msg
        xAxis =
            Axis.axis { axisOptions | orientation = Axis.Bottom, tickFormat = Just (absoluteTickFormat xScale 10) } xScale

        yAxis : Svg msg
        yAxis =
            let
                tickCount =
                    populationMinnesota1850
                        |> .extent
                        |> Tuple.second
                        |> (\v -> round v // 10 * 2)
            in
                Axis.axis { axisOptions | orientation = Axis.Left } (Scale.toRenderable yScale)
    in
        Svg.svg [ Svg.Attributes.width (toString width ++ "px"), Svg.Attributes.height (toString height ++ "px") ]
            [ g [ translate 0 (height - padding) ]
                [ xAxis ]
            , g [ class "series" ] <|
                List.map (column yScale) (List.map2 (,) populationMinnesota1850.categories scaledValues)
            , g [ translate padding 0 ]
                [ yAxis
                , text_ [ fontFamily "sans-serif", fontSize "14", x "5", y "65" ] [ text "Age" ]
                ]
            , g [ translate (width - padding) (padding + 20) ]
                [ text_ [ fontFamily "sans-serif", fontSize "20", textAnchor "end" ] [ text "1850" ]
                , text_ [ fontFamily "sans-serif", fontSize "10", textAnchor "end", dy "1em" ] [ text "population distribution in Minnesota" ]
                ]
            , text_
                [ translate (Scale.convert xScale 500000) (2 * padding)
                , fontFamily "sans-serif"
                , fontSize "20"
                , textAnchor "middle"
                ]
                [ text "Men" ]
            , text_
                [ translate (Scale.convert xScale -500000) (2 * padding)
                , fontFamily "sans-serif"
                , fontSize "20"
                , textAnchor "middle"
                ]
                [ text "Women" ]
            , text_
                [ translate (Scale.convert xScale 0) (height - padding / 2)
                , fontFamily "sans-serif"
                , fontSize "14"
                , textAnchor "middle"
                , dy "1em"
                ]
                [ 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)


translate : number -> number -> Svg.Attribute msg
translate x y =
    transform ("translate(" ++ toString x ++ ", " ++ toString y ++ ")")