Elm logo
elm
examples

elm-visualization

/

examples

/

Slopechart

Edit on Ellie

Slopechart

A polar plot of sin(2x)cos(2x).

module Slopechart exposing (main)


import Color
import Force
import Html
import Html.Attributes exposing (style)
import List.Extra
import Scale exposing (ContinuousScale)
import Statistics
import TypedSvg exposing (g, svg)
import TypedSvg.Attributes exposing (dy, fill, stroke, strokeLinejoin, textAnchor, textDecoration, viewBox)
import TypedSvg.Attributes.InPx exposing (fontSize, height, strokeWidth, width, x, y)
import TypedSvg.Core exposing (Svg, text)
import TypedSvg.Types exposing (AnchorAlignment(..), Paint(..), StrokeLinejoin(..), em)


w : Float
w =
    500


h : Float
h =
    804


xPadding : Float
xPadding =
    150


yPadding : Float
yPadding =
    16


xScale : ContinuousScale Float
xScale =
    data
        |> List.concatMap (Tuple.second >> List.map (Tuple.first >> toFloat))
        |> Statistics.extent
        |> Maybe.withDefault ( 0, 0 )
        |> Scale.linear ( xPadding, w - xPadding )


yScale : ContinuousScale Float
yScale =
    Scale.linear ( h - 5, yPadding ) ( 0, 100 )


years =
    [ 5, 10, 15, 20 ]


main : Svg msg
main =
    Html.div [ style "display" "flex" ]
        [ Html.div [ style "height" "504px", style "overflow-y" "auto" ]
            [ svg
                [ viewBox 0 0 w h, width w, height h ]
                [ data
                    |> List.map
                        (\( _, values ) ->
                            TypedSvg.polyline
                                [ fill PaintNone
                                , stroke (Paint Color.gray)
                                , List.map
                                    (\( year, survival ) ->
                                        ( Scale.convert xScale (toFloat year), Scale.convert yScale survival )
                                    )
                                    values
                                    |> TypedSvg.Attributes.points
                                ]
                                []
                        )
                    |> g []
                , years
                    |> List.map
                        (\year ->
                            labelsAtYear year
                                |> yOcclusion
                                |> List.map
                                    (\( label, xPos, yPos ) ->
                                        TypedSvg.text_
                                            [ x xPos
                                            , y yPos
                                            , fill (Paint Color.black)
                                            , stroke (Paint Color.white)
                                            , strokeWidth 5
                                            , strokeLinejoin StrokeLinejoinRound
                                            , TypedSvg.Core.attribute "paint-order" "stroke"
                                            , dy (em 0.32)
                                            , fontSize 10
                                            , textAnchor
                                                (if year == 5 then
                                                    AnchorEnd

                                                 else if year == 20 then
                                                    AnchorStart

                                                 else
                                                    AnchorMiddle
                                                )
                                            ]
                                            [ text label ]
                                    )
                                >> g []
                        )
                    |> g []
                , years
                    |> List.map
                        (\year ->
                            TypedSvg.text_
                                [ x (Scale.convert xScale (toFloat year))
                                , y 5
                                , fill (Paint Color.gray)
                                , dy (em 0.32)
                                , fontSize 10
                                , textAnchor AnchorMiddle
                                , textDecoration "underline"
                                ]
                                [ TypedSvg.tspan [ fill (Paint Color.black) ] [ text (String.fromInt year ++ " year") ] ]
                        )
                    |> g []
                ]
            ]
        , Html.div []
            [ Html.h1 [ style "font-size" "24px", style "margin" "0" ] [ text "Estimates of relative survival rates" ]
            , Html.h2 [ style "font-size" "24px", style "margin-top" "0" ] [ text "by cancer site" ]
            , Html.p [] [ Html.text "Based on ", Html.a [ Html.Attributes.href "https://www.edwardtufte.com/bboard/q-and-a-fetch-msg?msg_id=0003nk" ] [ Html.text "Tufte" ] ]
            , Html.p [ style "position" "absolute", style "bottom" "10px" ] [ text "↓ Scroll to see more" ]
            ]
        ]


yOcclusion : List ( String, Float, Float ) -> List ( String, Float, Float )
yOcclusion labels =
    let
        initialState =
            Force.simulation
                [ Force.towardsY (List.map (\( l, _, y ) -> { node = l, target = y, strength = 0.1 }) labels)
                , Force.collision 6.5 (List.map (\( l, _, _ ) -> l) labels)
                ]

        helper i state entities =
            if i <= 0 then
                List.map (\e -> ( e.id, e.fx, e.y )) entities

            else
                let
                    ( newState, newEntities ) =
                        Force.tick state entities
                in
                helper (i - 1) newState (List.map (\e -> { e | x = e.fx }) newEntities)
    in
    helper 20 initialState (List.map (\( l, x, y ) -> { x = x, y = y, fx = x, vx = 0, vy = 0, id = l }) labels)


labelsAtYear : Int -> List ( String, Float, Float )
labelsAtYear y =
    data
        |> List.filterMap
            (\( name, values ) ->
                List.Extra.find (\( year, _ ) -> year == y) values
                    |> Maybe.map
                        (\( year, survival ) ->
                            ( if year == 5 then
                                name ++ " " ++ String.fromFloat survival

                              else if year == 20 then
                                String.fromFloat survival ++ " " ++ name

                              else
                                String.fromFloat survival
                            , Scale.convert xScale (toFloat year)
                            , Scale.convert yScale survival
                            )
                        )
            )
        |> List.Extra.uniqueBy (\( n, _, _ ) -> n)


type alias Data =
    List ( String, List ( Int, Float ) )


data : Data
data =
    [ ( "Prostate", [ ( 5, 99 ), ( 10, 95 ), ( 15, 87 ), ( 20, 81 ) ] )
    , ( "Thyroid", [ ( 5, 96 ), ( 10, 96 ), ( 15, 94 ), ( 20, 95 ) ] )
    , ( "Testis", [ ( 5, 95 ), ( 10, 94 ), ( 15, 91 ), ( 20, 88 ) ] )
    , ( "Melanomas", [ ( 5, 89 ), ( 10, 87 ), ( 15, 84 ), ( 20, 83 ) ] )
    , ( "Breast", [ ( 5, 86 ), ( 10, 78 ), ( 15, 71 ), ( 20, 65 ) ] )
    , ( "Hodgkin’s disease", [ ( 5, 85 ), ( 10, 80 ), ( 15, 74 ), ( 20, 67 ) ] )
    , ( "Corpus uteri,uterus", [ ( 5, 84 ), ( 10, 83 ), ( 15, 81 ), ( 20, 79 ) ] )
    , ( "Urinary, bladder", [ ( 5, 82 ), ( 10, 76 ), ( 15, 70 ), ( 20, 68 ) ] )
    , ( "Cervix, uteri", [ ( 5, 71 ), ( 10, 64 ), ( 15, 63 ), ( 20, 60 ) ] )
    , ( "Larynx", [ ( 5, 69 ), ( 10, 57 ), ( 15, 46 ), ( 20, 38 ) ] )
    , ( "Rectum", [ ( 5, 63 ), ( 10, 55 ), ( 15, 52 ), ( 20, 49 ) ] )
    , ( "Kidney, renal pelvis", [ ( 5, 62 ), ( 10, 54 ), ( 15, 50 ), ( 20, 47 ) ] )
    , ( "Colon", [ ( 5, 62 ), ( 10, 55 ), ( 15, 54 ), ( 20, 52 ) ] )
    , ( "Non-Hodgkin’s", [ ( 5, 58 ), ( 10, 46 ), ( 15, 38 ), ( 20, 34 ) ] )
    , ( "Oral cavity, pharynx", [ ( 5, 57 ), ( 10, 44 ), ( 15, 38 ), ( 20, 33 ) ] )
    , ( "Ovary", [ ( 5, 55 ), ( 10, 49 ), ( 15, 50 ), ( 20, 50 ) ] )
    , ( "Leukemia", [ ( 5, 43 ), ( 10, 32 ), ( 15, 30 ), ( 20, 26 ) ] )
    , ( "Brain, nervous system", [ ( 5, 32 ), ( 10, 29 ), ( 15, 28 ), ( 20, 26 ) ] )
    , ( "Multiple myeloma", [ ( 5, 30 ), ( 10, 13 ), ( 15, 7 ), ( 20, 5 ) ] )
    , ( "Stomach", [ ( 5, 24 ), ( 10, 19 ), ( 15, 19 ), ( 20, 15 ) ] )
    , ( "Lung and bronchus", [ ( 5, 15 ), ( 10, 11 ), ( 15, 8 ), ( 20, 6 ) ] )
    , ( "Esophagus", [ ( 5, 14 ), ( 10, 8 ), ( 15, 8 ), ( 20, 5 ) ] )
    , ( "Liver, bile duct", [ ( 5, 8 ), ( 10, 6 ), ( 15, 6 ), ( 20, 8 ) ] )
    , ( "Pancreas", [ ( 5, 4 ), ( 10, 3 ), ( 15, 3 ), ( 20, 3 ) ] )
    ]