RalfNorthman/elm-zoom-plot/2.0.1

NOTE: the latest version is 3.0.1

ZoomPlot

Minimal configuration

easyConfig : List Point -> Config Point

Use this configuration as a starting point when your data is just a List Point:

type alias Point =
    { x : Float, y : Float }


points =
    [ Point 11 120
    , Point 12 121
    , Point 13 120.5
    ]


myConfig =
    Plot.easyConfig points

Storing the plot state in your model

type State data

You need to store the internal state of the plot in your model.

It can be as simple as:

type alias Model =
    { plotState : Plot.State Point }

or if you have multiple plots on your page:

type alias Model =
    { plot1 : Plot.State Point
    , plot2 : Plot.State Point
    , plot3 : Plot.State Point
    }
init : State data

All plots have the same initial state regardless of individual configuration.

init : Model
init =
    { plot1 = Plot.init
    , plot2 = Plot.init
    , plot3 = Plot.init
    }

Including Plot.Msg in your Msg

type Msg data

Include it within a constructor for easy pattern matching in your update function:

type Msg
    = MyPlotMsg (Plot.Msg Point)
    | MsgNotConcerningPlots

or if you need routing for several plots:

type PlotNr
    = Plot1
    | Plot2
    | Plot3


type Msg
    = ToPlot PlotNr (Plot.Msg ExampleType)

Drawing your linechart

Float
-> Float
-> Config data
-> State data
-> (Msg data -> msg)
-> Element msg

Use this function to place your linechart within your view.

import Element as E


view model =
    E.layout [ E.padding 20 ] <|
        E.el
            [ E.width <| px 800
            , E.height <| px 600
            ]
        <|
            Plot.draw
                800
                600
                myConfig
                model.plotState
                MyPlotMsg

The first two arguments are width and height, which mostly determines the aspect ratio of your plot since it fills the container you place it in.

Small dimension values in large containers can make the text in the plot unreasonably large.

Float
-> Float
-> Config data
-> State data
-> (Msg data -> msg)
-> Html msg

If you, for some reason, are not using mdgriffith/elm-ui you can use this draw function instead which outputs regular Html msg.

Updating your plot state

update : Config data -> Msg data -> State data -> State data

Naturally you need to handle the plot messages in your update function. This is what Plot.update is for.

update : Msg -> Model -> Model
update msg model =
    case msg of
        MyPlotMsg plotMsg ->
            { model
                | plotState =
                    Plot.update
                        myConfig
                        plotMsg
                        model.plotState
            }

If you need routing to update multiple plots:

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        ToPlot Plot1 plotMsg ->
            ( { model
                | plot1 =
                    Plot.update
                        plotConfig1
                        plotMsg
                        model.plot1
              }
            , Cmd.none
            )


        ToPlot Plot2 plotMsg ->
            ( { model
                | plot2 =
                    Plot.update
                        plotConfig2
                        plotMsg
                        model.plot2
              }
            , Cmd.none
            )

Customizing your plot

Let's say that you want to customize your configuration to show legends for your different lines and also extend the right margin so that the legends don't get cut off. You can do it like this:

myConfig =
    let
        default =
            Plot.easyConfig points
    in
    { default
        | showLegends = True
        , marginRight = 70
    }

What if your data isn't in the form of { x : Float, y : Float}?

Then you will need:

List data
-> (data -> Float)
-> (data -> Float)
-> (Point -> data)
-> Config data

Let's say that your data is a list of this type:

type alias ExampleType =
    { time : Posix
    , value : Float
    , text : String
    , otherValue : Float
    }

If you want to plot data like this you can't use easyConfig, because it only works with data of type Point. Instead you will have to use defaultConfigWith to create your starting point configuration.

You need to supply it with two accessor functions (getters), one for each chart coordinate (x and y).

myDefaultConfig : Plot.Config ExampleType
myDefaultConfig =
    Plot.defaultConfigWith
        listOfExampleTypes
        (.time >> Time.posixToMillis >> toFloat)
        .value
        myPointDecoder

It also needs a point decoder. The internals of the package needs a way to convert Point back to your type. You basically have to write a function that puts the coordinates of a Point into an "empty" instance of your type:

myPointDecoder { x, y } =
    let
        decodedX =
            x |> floor |> Time.millisToPosix
    in
    ExampleType decodedX y "" 0

If you want to start customizing every aspect of your plot you need to know about all the fields of the Plot.Config type:

type alias Config data =
{ lines : List (Series data)
, xAcc : data -> Float
, yAcc : data -> Float
, pointDecoder : Point -> data
, xIsTime : Bool
, language : Language
, numberFormat : Float -> String
, timezone : Zone
, showLegends : Bool
, labelFunc : data -> String
, xAxisLabel : String
, yAxisLabel : String
, marginTop : Float
, marginRight : Float
, marginBottom : Float
, marginLeft : Float
, xAxisLabelOffsetX : Float
, xAxisLabelOffsetY : Float
, yAxisLabelOffsetX : Float
, yAxisLabelOffsetY : Float
}

So, what does all this mean?

  • lines : A list where you set how the different "lines" on your chart should look: color, point shape and legend name. Most importantly it is also here you supply your data to the configuration.

    import LineChart
    import LineChart.Colors as Colors
    import LineChart.Dots as Dots
    
    
    [ LineChart.line
        Colors.tealLight
        Dots.circle
        "cos"
        myCosData
    , LineChart.line
        Colors.goldLight
        Dots.diamond
        "polysin"
        myPolySinData
    ]

    Note: This package uses the fork peterszerzo/line-charts since it contains necessary updates to terezka's great original package. I advise you to do the same, otherwise namespace collisions seem inevitable.

  • xAcc : Accessor function (getter) from your data type to the float value used for x coordinates. If you want a time axis (don't forget to set xIsTime to True) for your Time.Posix values you can do something like this:

    .time >> Time.posixToMillis >> toFloat
  • yAcc : Accessor function (getter) from your data type to the float value used for y coordinates.

    .rocketThrust
  • pointDecoder : The internals of the package needs a way to convert Point back to your type. You basically have to write a function that puts the coordinates of a Point into an "empty" instance of your type.

    \{ x, y } -> RocketData 0 0 "" x 0 "" 0 y Nothing
  • xIsTime : Whether the x-axis should display its values as time or not.

    True
  • language : What language dates on time axes will be in.

    import DateFormat.Language
    
    
    DateFormat.Language.swedish
  • numberFormat : The function which turns floats to strings for the axis labels (on non-time axes).

    import FormatNumber
    import FormatNumber.Locales
    
    
    \float ->
        FormatNumber.format
            FormatNumber.Locales.frenchLocale
            float
  • timeZone : Which timezone your Time.Posix values should be converted into for your time axis.

    import Time
    
    
    Time.utc -- This is the default.
  • showLegends : Whether legends for your plot lines should be drawn. If you turn this on you most likely also need to adjust marginRight for them to not get cut off.

    True
  • labelFunc : Hoverlabels for the x and y coordinates are always on, but if you want to add something extra on a row above them you use this field. One of the more straightforward ways to use this is to just use a string field from your data type:

    .rocketInventorName

    or something more involved:

    \d -> d.firstName ++ " " ++ d.lastName
  • xAxisLabel / yAxisLabel : Strings for labeling the axes. Adjustment of margins and label offsets could be required to get the desired result.

    "Rocket velocity [km/s]"
  • marginTop / marginRight / marginBottom / marginLeft : Size in pixels of the different margins around the actual plot. This is real estate that may or may not be needed by tick labels, legends and axis labels.

    60
  • xAxisLabelOffsetX / xAxisLabelOffsetY / yAxisLabelOffsetX / yAxisLabelOffsetY : Distance in pixels of how much you want to adjust the positioning of the axis title labels.

    20

Configuration defaults

It can be useful to know what the defaults are:

    { lines = [ LineChart.line
                    Colors.tealLight
                    Dots.circle
                    ""
                    inputPoints
              ]
    , xAcc = .x
    , yAcc = .y
    , pointDecoder = \p -> p
    , xIsTime = False
    , language = english
    , numberFormat = defaultFormat
    , timezone = Time.utc
    , showLegends = False
    , labelFunc = \_ -> ""
    , xAxisLabel = ""
    , yAxisLabel = ""
    , marginTop = 20
    , marginRight = 30
    , marginBottom = 30
    , marginLeft = 60
    , xAxisLabelOffsetX = 0
    , xAxisLabelOffsetY = 0
    , yAxisLabelOffsetX = 0
    , yAxisLabelOffsetY = 0
    }


    defaultFormat : Float -> String
    defaultFormat number =
        FormatNumber.format
            FormatNumber.Locales.usLocale
            number

Above are the defaults from easyConfig, where inputPoints comes from the user. The defaults for defaultConfigWith are the same except the fields xAcc, yAcc and pointDecoder which are user-provided as well.