RalfNorthman / elm-zoom-plot / ZoomPlot

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:

import ZoomPlot as Plot

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)
    | NoOp

or if you need routing for several plots:

type MyPlot
    = Plot1
    | Plot2

type Msg
    = ToPlot MyPlot (Plot.Msg Point)

Updating your plot state

update : 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
        ToPlot plotMsg ->
            { model
                | plotState =
                    Plot.update
                        plotMsg
                        model.plotState
            }

If you need routing to update multiple plots:

case msg of
    ToPlot Plot1 plotMsg ->
        { model
            | plot1 =
                Plot.update
                    plotMsg
                    model.plot1
        }

    ToPlot Plot2 plotMsg ->
        { model
            | plot2 =
                Plot.update
                    plotMsg
                    model.plot2
        }

Drawing your line chart

points : { toMsg : Msg LineChart.Coordinate.Point -> msg, data : List LineChart.Coordinate.Point } -> Config LineChart.Coordinate.Point msg

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

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

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

Plot.points
    { toMsg = ToPlot
    , data = myPoints
    }
    |> Plot.drawHtml model.plotState

draw : State data -> Config data msg -> Element msg

Use this function to place your line chart within your view.

import Element

view model =
    Element.layout
        [ Element.padding 20 ]
        (Plot.points
            { toMsg = ToPlot
            , data = myPoints
            }
            |> Plot.draw model.plotState
        )

drawHtml : State data -> Config 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.

Customizing your plot

Customization is done by mutating the Config output from Plot.points and Plot.custom before it is inserted into draw.


type Config data msg

Customizers:

width : Basics.Float -> Config data msg -> Config data msg

Set the plot width in pixels.

height : Basics.Float -> Config data msg -> Config data msg

Set the plot height in pixels.

xAxisLabel : String -> Config data msg -> Config data msg

Set a string for labeling of the x-axis. Adjustment of margins and label offsets could be required to get the desired result.

Default is ""

yAxisLabel : String -> Config data msg -> Config data msg

Set a string for labeling of the y-axis. Adjustment of margins and label offsets could be required to get the desired result.

Default is ""

showLegends : Basics.Bool -> Config data msg -> Config data msg

Set whether legends for your plot lines should be drawn or not. If you turn this on you most likely also need to adjust marginRight for them to not get cut off.

Default is False.

numberFormat : (Basics.Float -> String) -> Config data msg -> Config data msg

Set the function which turns floats to strings for the axis labels (on non-time axes).

    import FormatNumber
    import FormatNumber.Locales

    myPlotConfig
    |> Plot.numberFormat
        (\float ->
          FormatNumber.format
            FormatNumber.Locales.frenchLocale
            float
        )
    |> Plot.draw model.myPlot

Default is:

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

xIsTime : Basics.Bool -> Config data msg -> Config data msg

Set whether the x-axis should display its values as time or not.

Default is False.

timezone : Time.Zone -> Config data msg -> Config data msg

Set which timezone your Time.Posix values should be converted into for your time axis tick labels.

Only matters when Plot.xIsTime True.

default is Time.utc.

language : DateFormat.Language.Language -> Config data msg -> Config data msg

Set what language dates on the time axis will be in.

Only matters when Plot.xIsTime True.

    import DateFormat.Language

    myPlotConfig
    |> Plot.language DateFormat.Language.swedish
    |> Plot.draw model.myPlot

Default is DateFormat.Language.english.

labelFunc : (data -> String) -> Config data msg -> Config data msg

Set extra hover label row. 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.

You can for example just use a string field from your data type:

    myPlotConfig
    |> Plot.labelFunc .rocketInventorName
    |> Plot.draw model.myPlot

Default is \_ -> ""

Margin customizers

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.

marginLeft : Basics.Float -> Config data msg -> Config data msg

Default is 60

marginRight : Basics.Float -> Config data msg -> Config data msg

Default is 30

marginTop : Basics.Float -> Config data msg -> Config data msg

Default is 20

marginBottom : Basics.Float -> Config data msg -> Config data msg

Default is 30

Axis label offset customizers

Amount in pixels for moving around the axis labels.

Positive y adjusts upwards contrary to Svg standard.

xAxisLabelOffsetX : Basics.Float -> Config data msg -> Config data msg

Default is 0

xAxisLabelOffsetY : Basics.Float -> Config data msg -> Config data msg

Default is 0

yAxisLabelOffsetX : Basics.Float -> Config data msg -> Config data msg

Default is 0

yAxisLabelOffsetY : Basics.Float -> Config data msg -> Config data msg

Default is 0

Plotting custom types

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

Then you will need:

custom : { lines : List (LineChart.Series data), toMsg : Msg data -> msg, xAcc : data -> Basics.Float, yAcc : data -> Basics.Float, pointDecoder : LineChart.Coordinate.Point -> data } -> Config data msg

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

import Time

type alias ExampleType =
    { time : Time.Posix
    , thrust : Float
    , altitude : Float
    , latestBufferLine : String
    , errorLog : Maybe String
    }

If you want to plot data like this you can't use Plot.points, because it only works with data of type List Point.

Instead you can use Plot.custom like this:

import LineChart
import LineChart.Dots as Dots
import LineChart.Colors as Colors

Plot.custom
    { lines =
        [ LineChart.line
            Colors.blue
            Dots.circle
            "rocket 1"
            listOfExampleType1
        , LineChart.line
            Colors.purple
            Dots.square
            "rocket 2"
            listOfExampleType2

        ]
    , toMsg = ToPlot
    , xAcc = .time >> posixToMillis >> toFloat
    , yAcc = .thrust
    , pointDecoder = myPointDecoder
    }

The inputs xAcc and yAcc are whatever functions that will turn your type into floats to be plotted on the line chart.

Most often they are just getters plus whatever functions are needed to convert their values into floats.

You may also have noticed the myPointDecoder above.

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 : Point -> ExampleType
myPointDecoder { x, y } =
    let
        xTime =
            x |> round |> millisToPosix
    in
    ExampleType xTime y 0 "" Nothing

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 (at least in projects using elm-zoom-plot).